mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 18:55:22 +00:00
feat(pi-permission-system): AutoReview 改为跟随主模型的对话式工具调用审查
AutoReview: - 审查器回到 Codex Guardian 风格的对话式审查,默认跟随会话当前主模型 (每次审查实时读取 ctx.model);provider/model 必须同时设置才固定审查 模型,只设其一视为配置错误并转人工。 - 结论只通过 approve / reject 两个工具给出,参数仅 reason;不强制 toolChoice,正文文本(即使是合法 JSON)一律忽略。 - approve 自动放行;reject 转人工并在弹窗显示理由(标注模型生成)。 超时、Provider 错误、截断、未调用工具、多次调用、参数无效等异常同样 转人工,并附固定的系统说明;用户按 Esc 中止时直接拒绝。 - 模型调用改为 modelRegistry.streamSimple(...).result():Pi 1.3.0 面向 扩展的 ModelRegistry 没有 completeSimple,原写法在真实环境中每次都会 转人工。 - 对话记录保留可信度标注;工具结果在有更短的 pi-compaction 摘要时替换 为摘要;token 估算改为非 ASCII 字符按 1 token 计;预算可配置。 - 同一条用户消息内完全相同的请求只缓存 approve 结论。 - 默认 reasoning low、总超时 30 秒、最多 2 次尝试、maxOutputTokens 4000。 删除: - 未发布过的 TypeSafe Jev 分类器实现、DenialCircuitBreaker。 - 整个 annotator 机制(model-explanation、注册表、registerAnnotator、 provider-timeout);annotators 配置键保留为已弃用并忽略,避免旧配置 被拒绝。 测试: - 新增 reviewer / transcript / 弹窗说明 / 默认配置测试;清理重复、恒真 或只测实现细节的旧测试。 - 使用 deepseek-v4.1-flash 实测 5 个场景各 5 次,25 次结论全部符合预期。 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
dec48dab6d
commit
7e6a8faae1
@@ -1,32 +1,74 @@
|
||||
import type { SessionEntry } from '@earendil-works/pi-coding-agent'
|
||||
|
||||
const MAX_RECENT_UNTRUSTED_ENTRIES = 40
|
||||
const MAX_MESSAGE_TRANSCRIPT_TOKENS = 10_000
|
||||
const MAX_TOOL_TRANSCRIPT_TOKENS = 10_000
|
||||
const MAX_MESSAGE_ENTRY_TOKENS = 2_000
|
||||
const MAX_TOOL_ENTRY_TOKENS = 1_000
|
||||
const TRUSTED_USER_INTERACTION_TOOLS = new Set(['ask_user_question', 'plan_mode_question'])
|
||||
|
||||
/** Transcript budgets, as configured on `autoReview.transcript`. */
|
||||
export interface TranscriptBudgets {
|
||||
readonly messageTokens: number
|
||||
readonly toolTokens: number
|
||||
readonly maxUntrustedEntries: number
|
||||
}
|
||||
|
||||
export const DEFAULT_TRANSCRIPT_BUDGETS: TranscriptBudgets = {
|
||||
messageTokens: 10_000,
|
||||
toolTokens: 10_000,
|
||||
maxUntrustedEntries: 40,
|
||||
}
|
||||
|
||||
/**
|
||||
* One tool-result summary record, as returned by the injected lookup.
|
||||
*
|
||||
* Read structurally: the fields mirror pi-compaction's `ToolHistoryRecord`,
|
||||
* but the transcript must not import pi-compaction code — the record reaches
|
||||
* it through a lookup function the caller resolves (or leaves absent).
|
||||
*/
|
||||
export interface ToolResultSummaryRecord {
|
||||
readonly toolCallId: string
|
||||
readonly resultTimestamp?: unknown
|
||||
readonly semanticSummary?: unknown
|
||||
readonly deterministicSummary?: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve summaries for a batch of tool-call ids, in one call per render.
|
||||
*
|
||||
* The caller binds the session id; the transcript asks for every toolResult
|
||||
* entry's id at once so the lookup performs a single service query.
|
||||
*/
|
||||
export type ToolResultSummaryQuery = (
|
||||
toolCallIds: readonly string[],
|
||||
) => readonly ToolResultSummaryRecord[]
|
||||
|
||||
/** Options accepted by {@link renderTranscript}. */
|
||||
export interface TranscriptRenderOptions {
|
||||
readonly budgets?: TranscriptBudgets
|
||||
/** Optional summary lookup; absent means no substitution. */
|
||||
readonly querySummaries?: ToolResultSummaryQuery
|
||||
}
|
||||
|
||||
type TranscriptKind = 'user' | 'user_interaction' | 'assistant' | 'tool'
|
||||
|
||||
export interface TranscriptEntry {
|
||||
index: number
|
||||
/** The SessionEntry `id` this transcript entry was rendered from. */
|
||||
entryId: string
|
||||
kind: TranscriptKind
|
||||
label: string
|
||||
text: string
|
||||
/** Set on plain toolResult entries, so a summary lookup can match them. */
|
||||
toolCallId?: string
|
||||
/** The message's own timestamp, read structurally; may be absent. */
|
||||
timestamp?: unknown
|
||||
truncated?: boolean
|
||||
}
|
||||
|
||||
export interface TranscriptStats {
|
||||
transcriptEntriesRetained: number
|
||||
transcriptEntriesOmitted: number
|
||||
transcriptEntriesTruncated: number
|
||||
directUserEntriesRetained: number
|
||||
directUserEntriesOmitted: number
|
||||
directUserEntriesTruncated: number
|
||||
userInteractionEntriesRetained: number
|
||||
userInteractionEntriesOmitted: number
|
||||
userInteractionEntriesTruncated: number
|
||||
entriesRetained: number
|
||||
entriesOmitted: number
|
||||
entriesTruncated: number
|
||||
toolResultsSummarized: number
|
||||
latestTrustedEntryRetained: boolean
|
||||
}
|
||||
|
||||
@@ -34,6 +76,12 @@ export interface RenderedTranscript {
|
||||
entries: string[]
|
||||
omittedCount: number
|
||||
stats: TranscriptStats
|
||||
/**
|
||||
* The SessionEntry `id` of the newest trusted (`user` or `user_interaction`)
|
||||
* entry in this render, or `null` when the branch has none. Lets a caller
|
||||
* key per-request caches to the conversation point that authorized the ask.
|
||||
*/
|
||||
latestTrustedEntryId: string | null
|
||||
}
|
||||
|
||||
interface ContentBlock {
|
||||
@@ -56,6 +104,7 @@ interface MessageLike {
|
||||
toolName?: unknown
|
||||
isError?: unknown
|
||||
details?: unknown
|
||||
timestamp?: unknown
|
||||
}
|
||||
|
||||
interface UserInteractionDetails {
|
||||
@@ -70,8 +119,24 @@ interface UserInteractionAnswer {
|
||||
notes?: unknown
|
||||
}
|
||||
|
||||
function approximateTokens(text: string): number {
|
||||
return Math.ceil(text.length / 4)
|
||||
/**
|
||||
* A conservative token estimate: ASCII characters count as a quarter token
|
||||
* each, every non-ASCII code point counts as a full token.
|
||||
*
|
||||
* The plain `length / 4` heuristic under-counts CJK by roughly 2–4x, which
|
||||
* would silently overflow every budget this module enforces.
|
||||
*/
|
||||
export function estimateTokens(text: string): number {
|
||||
let ascii = 0
|
||||
let nonAscii = 0
|
||||
for (const character of text) {
|
||||
if ((character.codePointAt(0) ?? 0) > 127) {
|
||||
nonAscii += 1
|
||||
} else {
|
||||
ascii += 1
|
||||
}
|
||||
}
|
||||
return Math.ceil(ascii / 4) + nonAscii
|
||||
}
|
||||
|
||||
function truncateToCharacters(text: string, maxCharacters: number): string {
|
||||
@@ -85,8 +150,31 @@ function truncateToCharacters(text: string, maxCharacters: number): string {
|
||||
return `${text.slice(0, headLength)}${tag}${text.slice(-tailLength)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Largest head/tail truncation whose estimate fits `maxTokens`.
|
||||
*
|
||||
* Character budgets and token budgets diverge on non-ASCII text, so the bound
|
||||
* is searched: binary search the character count while the estimate — not the
|
||||
* length — decides whether a candidate fits.
|
||||
*/
|
||||
export function truncateToApproximateTokens(text: string, maxTokens: number): string {
|
||||
return truncateToCharacters(text, maxTokens * 4)
|
||||
if (estimateTokens(text) <= maxTokens) {
|
||||
return text
|
||||
}
|
||||
let lower = 0
|
||||
let upper = text.length
|
||||
let result = truncateToCharacters(text, 0)
|
||||
while (lower <= upper) {
|
||||
const middle = Math.floor((lower + upper) / 2)
|
||||
const candidate = truncateToCharacters(text, middle)
|
||||
if (estimateTokens(candidate) <= maxTokens) {
|
||||
result = candidate
|
||||
lower = middle + 1
|
||||
} else {
|
||||
upper = middle - 1
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function serializeUnknown(value: unknown): string {
|
||||
@@ -194,13 +282,14 @@ function normalizedUserInteraction(
|
||||
function assistantEntries(
|
||||
message: MessageLike,
|
||||
index: number,
|
||||
entryId: string,
|
||||
interactionToolCalls: Map<string, string>,
|
||||
): TranscriptEntry[] {
|
||||
const content = Array.isArray(message.content) ? message.content : []
|
||||
const text = textFromContent(message.content)
|
||||
const entries: TranscriptEntry[] = []
|
||||
if (text) {
|
||||
entries.push({ index, kind: 'assistant', label: 'assistant', text })
|
||||
entries.push({ index, entryId, kind: 'assistant', label: 'assistant', text })
|
||||
}
|
||||
for (const rawBlock of content) {
|
||||
const block = rawBlock as ContentBlock
|
||||
@@ -214,6 +303,7 @@ function assistantEntries(
|
||||
}
|
||||
entries.push({
|
||||
index,
|
||||
entryId,
|
||||
kind: 'tool',
|
||||
label: `tool:${name}`,
|
||||
text: serializeUnknown(block.arguments),
|
||||
@@ -225,15 +315,16 @@ function assistantEntries(
|
||||
function entriesFromMessage(
|
||||
message: MessageLike,
|
||||
index: number,
|
||||
entryId: string,
|
||||
interactionToolCalls: Map<string, string>,
|
||||
): TranscriptEntry[] {
|
||||
switch (message.role) {
|
||||
case 'user': {
|
||||
const text = textFromContent(message.content)
|
||||
return text ? [{ index, kind: 'user', label: 'user', text }] : []
|
||||
return text ? [{ index, entryId, kind: 'user', label: 'user', text }] : []
|
||||
}
|
||||
case 'assistant':
|
||||
return assistantEntries(message, index, interactionToolCalls)
|
||||
return assistantEntries(message, index, entryId, interactionToolCalls)
|
||||
case 'toolResult': {
|
||||
const name = typeof message.toolName === 'string' ? message.toolName : 'unknown'
|
||||
const userInteraction = normalizedUserInteraction(message, interactionToolCalls)
|
||||
@@ -241,6 +332,7 @@ function entriesFromMessage(
|
||||
return [
|
||||
{
|
||||
index,
|
||||
entryId,
|
||||
kind: 'user_interaction',
|
||||
label: `user_interaction:${name}`,
|
||||
text: userInteraction,
|
||||
@@ -249,7 +341,19 @@ function entriesFromMessage(
|
||||
}
|
||||
const suffix = message.isError === true ? ' (error)' : ''
|
||||
const text = textFromContent(message.content)
|
||||
return text ? [{ index, kind: 'tool', label: `tool:${name}${suffix}`, text }] : []
|
||||
return text
|
||||
? [
|
||||
{
|
||||
index,
|
||||
entryId,
|
||||
kind: 'tool',
|
||||
label: `tool:${name}${suffix}`,
|
||||
text,
|
||||
...(typeof message.toolCallId === 'string' ? { toolCallId: message.toolCallId } : {}),
|
||||
timestamp: message.timestamp,
|
||||
},
|
||||
]
|
||||
: []
|
||||
}
|
||||
case 'bashExecution': {
|
||||
const command = serializeUnknown(message.command)
|
||||
@@ -257,6 +361,7 @@ function entriesFromMessage(
|
||||
return [
|
||||
{
|
||||
index,
|
||||
entryId,
|
||||
kind: 'tool',
|
||||
label: 'tool:user-bash',
|
||||
text: `${command}\n${output}`,
|
||||
@@ -266,11 +371,11 @@ function entriesFromMessage(
|
||||
case 'branchSummary':
|
||||
case 'compactionSummary': {
|
||||
const text = serializeUnknown(message.summary)
|
||||
return text ? [{ index, kind: 'assistant', label: String(message.role), text }] : []
|
||||
return text ? [{ index, entryId, kind: 'assistant', label: String(message.role), text }] : []
|
||||
}
|
||||
case 'custom': {
|
||||
const text = textFromContent(message.content)
|
||||
return text ? [{ index, kind: 'assistant', label: 'custom', text }] : []
|
||||
return text ? [{ index, entryId, kind: 'assistant', label: 'custom', text }] : []
|
||||
}
|
||||
default:
|
||||
return []
|
||||
@@ -281,12 +386,18 @@ export function collectTranscriptEntries(sessionEntries: SessionEntry[]): Transc
|
||||
const interactionToolCalls = new Map<string, string>()
|
||||
return sessionEntries.flatMap((entry, index) => {
|
||||
if (entry.type === 'message') {
|
||||
return entriesFromMessage(entry.message as MessageLike, index, interactionToolCalls)
|
||||
return entriesFromMessage(
|
||||
entry.message as MessageLike,
|
||||
index,
|
||||
entry.id,
|
||||
interactionToolCalls,
|
||||
)
|
||||
}
|
||||
if (entry.type === 'compaction' || entry.type === 'branch_summary') {
|
||||
return [
|
||||
{
|
||||
index,
|
||||
entryId: entry.id,
|
||||
kind: 'assistant' as const,
|
||||
label: entry.type,
|
||||
text: entry.summary,
|
||||
@@ -299,6 +410,7 @@ export function collectTranscriptEntries(sessionEntries: SessionEntry[]): Transc
|
||||
? [
|
||||
{
|
||||
index,
|
||||
entryId: entry.id,
|
||||
kind: 'assistant' as const,
|
||||
label: 'custom',
|
||||
text,
|
||||
@@ -320,23 +432,22 @@ function renderTranscriptEntry(entry: TranscriptEntry): string {
|
||||
}
|
||||
|
||||
function transcriptEntryTokens(entry: TranscriptEntry): number {
|
||||
return approximateTokens(renderTranscriptEntry(entry))
|
||||
return estimateTokens(renderTranscriptEntry(entry))
|
||||
}
|
||||
|
||||
function pretruncate(entry: TranscriptEntry): TranscriptEntry {
|
||||
const maxTokens = entry.kind === 'tool' ? MAX_TOOL_ENTRY_TOKENS : MAX_MESSAGE_ENTRY_TOKENS
|
||||
const maxCharacters = maxTokens * 4
|
||||
if (renderTranscriptEntry(entry).length <= maxCharacters) {
|
||||
if (transcriptEntryTokens(entry) <= maxTokens) {
|
||||
return entry
|
||||
}
|
||||
|
||||
let lower = 0
|
||||
let upper = Math.min(entry.text.length, maxCharacters)
|
||||
let upper = entry.text.length
|
||||
let text = truncateToCharacters(entry.text, 0)
|
||||
while (lower <= upper) {
|
||||
const middle = Math.floor((lower + upper) / 2)
|
||||
const candidate = truncateToCharacters(entry.text, middle)
|
||||
if (renderTranscriptEntry({ ...entry, text: candidate }).length <= maxCharacters) {
|
||||
if (estimateTokens(renderTranscriptEntry({ ...entry, text: candidate })) <= maxTokens) {
|
||||
text = candidate
|
||||
lower = middle + 1
|
||||
} else {
|
||||
@@ -363,8 +474,86 @@ function addWithinBudget(selected: Set<TranscriptEntry>, entries: TranscriptEntr
|
||||
return used
|
||||
}
|
||||
|
||||
export function renderTranscript(sessionEntries: SessionEntry[]): RenderedTranscript {
|
||||
const allEntries = collectTranscriptEntries(sessionEntries).map(pretruncate)
|
||||
function summaryText(record: ToolResultSummaryRecord): string | undefined {
|
||||
const summary = record.semanticSummary ?? record.deterministicSummary
|
||||
return typeof summary === 'string' && summary.length > 0 ? summary : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitute pi-compaction summaries for plain tool results.
|
||||
*
|
||||
* One lookup call per render, over every toolResult's call id at once. A
|
||||
* record replaces the entry text only when it is strictly shorter — a summary
|
||||
* longer than its source compresses nothing — and the label gains
|
||||
* ` (summarized)` so the reviewer reads it as a digest, not the raw output.
|
||||
* Entries normalized to `user_interaction` and assistant toolCall argument
|
||||
* entries carry no `toolCallId`, so they are never touched.
|
||||
*
|
||||
* Returns the entries (substituted in place on copies) and how many were
|
||||
* summarized. Any lookup failure keeps the originals.
|
||||
*/
|
||||
function substituteSummaries(
|
||||
entries: TranscriptEntry[],
|
||||
query: ToolResultSummaryQuery | undefined,
|
||||
): number {
|
||||
if (query === undefined) return 0
|
||||
const candidates = entries.filter(
|
||||
entry => entry.kind === 'tool' && entry.toolCallId !== undefined,
|
||||
)
|
||||
if (candidates.length === 0) return 0
|
||||
|
||||
let records: readonly ToolResultSummaryRecord[]
|
||||
try {
|
||||
records = query(candidates.map(entry => entry.toolCallId as string))
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
if (!Array.isArray(records) || records.length === 0) return 0
|
||||
|
||||
const byCallId = new Map<string, ToolResultSummaryRecord[]>()
|
||||
for (const record of records) {
|
||||
if (record === null || typeof record !== 'object' || typeof record.toolCallId !== 'string') {
|
||||
continue
|
||||
}
|
||||
const list = byCallId.get(record.toolCallId)
|
||||
if (list === undefined) {
|
||||
byCallId.set(record.toolCallId, [record])
|
||||
} else {
|
||||
list.push(record)
|
||||
}
|
||||
}
|
||||
|
||||
let summarized = 0
|
||||
for (const entry of candidates) {
|
||||
const list = byCallId.get(entry.toolCallId as string)
|
||||
if (list === undefined) continue
|
||||
const record =
|
||||
list.find(
|
||||
candidate =>
|
||||
entry.timestamp !== undefined &&
|
||||
candidate.resultTimestamp !== undefined &&
|
||||
candidate.resultTimestamp === entry.timestamp,
|
||||
) ?? list[0]
|
||||
if (record === undefined) continue
|
||||
const summary = summaryText(record)
|
||||
if (summary === undefined) continue
|
||||
const replacement = `${summary}\n[summarized]`
|
||||
if (replacement.length >= entry.text.length) continue
|
||||
entry.text = replacement
|
||||
entry.label = `${entry.label} (summarized)`
|
||||
summarized += 1
|
||||
}
|
||||
return summarized
|
||||
}
|
||||
|
||||
export function renderTranscript(
|
||||
sessionEntries: SessionEntry[],
|
||||
options?: TranscriptRenderOptions,
|
||||
): RenderedTranscript {
|
||||
const budgets = options?.budgets ?? DEFAULT_TRANSCRIPT_BUDGETS
|
||||
const collected = collectTranscriptEntries(sessionEntries)
|
||||
const toolResultsSummarized = substituteSummaries(collected, options?.querySummaries)
|
||||
const allEntries = collected.map(pretruncate)
|
||||
const selected = new Set<TranscriptEntry>()
|
||||
const trustedEntries = allEntries.filter(isTrusted)
|
||||
|
||||
@@ -383,22 +572,22 @@ export function renderTranscript(sessionEntries: SessionEntry[]): RenderedTransc
|
||||
}
|
||||
|
||||
const remainingTrusted = trustedEntries.filter(entry => !selected.has(entry)).toReversed()
|
||||
messageTokens += addWithinBudget(selected, remainingTrusted, MAX_MESSAGE_TRANSCRIPT_TOKENS - messageTokens)
|
||||
messageTokens += addWithinBudget(selected, remainingTrusted, budgets.messageTokens - messageTokens)
|
||||
|
||||
let toolTokens = 0
|
||||
let untrustedEntriesRetained = 0
|
||||
for (const entry of allEntries.toReversed()) {
|
||||
if (isTrusted(entry) || untrustedEntriesRetained >= MAX_RECENT_UNTRUSTED_ENTRIES) {
|
||||
if (isTrusted(entry) || untrustedEntriesRetained >= budgets.maxUntrustedEntries) {
|
||||
continue
|
||||
}
|
||||
const tokens = transcriptEntryTokens(entry)
|
||||
if (entry.kind === 'tool') {
|
||||
if (toolTokens + tokens > MAX_TOOL_TRANSCRIPT_TOKENS) {
|
||||
if (toolTokens + tokens > budgets.toolTokens) {
|
||||
continue
|
||||
}
|
||||
toolTokens += tokens
|
||||
} else {
|
||||
if (messageTokens + tokens > MAX_MESSAGE_TRANSCRIPT_TOKENS) {
|
||||
if (messageTokens + tokens > budgets.messageTokens) {
|
||||
continue
|
||||
}
|
||||
messageTokens += tokens
|
||||
@@ -409,28 +598,50 @@ export function renderTranscript(sessionEntries: SessionEntry[]): RenderedTransc
|
||||
|
||||
const retained = [...selected].sort((left, right) => left.index - right.index)
|
||||
const latestTrusted = trustedEntries.at(-1)
|
||||
const directUsers = allEntries.filter(entry => entry.kind === 'user')
|
||||
const userInteractions = allEntries.filter(entry => entry.kind === 'user_interaction')
|
||||
const directUserEntriesRetained = retained.filter(entry => entry.kind === 'user').length
|
||||
const userInteractionEntriesRetained = retained.filter(entry => entry.kind === 'user_interaction').length
|
||||
const stats: TranscriptStats = {
|
||||
transcriptEntriesRetained: retained.length,
|
||||
transcriptEntriesOmitted: allEntries.length - retained.length,
|
||||
transcriptEntriesTruncated: retained.filter(entry => entry.truncated === true).length,
|
||||
directUserEntriesRetained,
|
||||
directUserEntriesOmitted: directUsers.length - directUserEntriesRetained,
|
||||
directUserEntriesTruncated: retained.filter(entry => entry.kind === 'user' && entry.truncated === true).length,
|
||||
userInteractionEntriesRetained,
|
||||
userInteractionEntriesOmitted: userInteractions.length - userInteractionEntriesRetained,
|
||||
userInteractionEntriesTruncated: retained.filter(
|
||||
entry => entry.kind === 'user_interaction' && entry.truncated === true,
|
||||
).length,
|
||||
entriesRetained: retained.length,
|
||||
entriesOmitted: allEntries.length - retained.length,
|
||||
entriesTruncated: retained.filter(entry => entry.truncated === true).length,
|
||||
toolResultsSummarized,
|
||||
latestTrustedEntryRetained: latestTrusted !== undefined && selected.has(latestTrusted),
|
||||
}
|
||||
|
||||
return {
|
||||
entries: retained.map(renderTranscriptEntry),
|
||||
omittedCount: stats.transcriptEntriesOmitted,
|
||||
omittedCount: stats.entriesOmitted,
|
||||
stats,
|
||||
latestTrustedEntryId: latestTrusted?.entryId ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A summary lookup bound to the session's pi-compaction service, or
|
||||
* `undefined` when no service is published.
|
||||
*
|
||||
* The service is a `Symbol.for()` global — read structurally so this module
|
||||
* never imports pi-compaction code, and a missing, throwing, or differently
|
||||
* shaped service simply yields no lookup rather than a render failure.
|
||||
*/
|
||||
export function compactionSummaryQuery(
|
||||
sessionId: string,
|
||||
): ToolResultSummaryQuery | undefined {
|
||||
const service = (globalThis as Record<symbol, unknown>)[
|
||||
Symbol.for('my-pi.pi-compaction.service.v1')
|
||||
]
|
||||
if (service === null || typeof service !== 'object') {
|
||||
return undefined
|
||||
}
|
||||
const query = (service as Record<string, unknown>).query
|
||||
if (typeof query !== 'function') {
|
||||
return undefined
|
||||
}
|
||||
const queryFn = query as (
|
||||
this: unknown,
|
||||
session: string,
|
||||
ids: readonly string[],
|
||||
) => unknown
|
||||
return toolCallIds => {
|
||||
const records = queryFn.call(service, sessionId, toolCallIds)
|
||||
return Array.isArray(records) ? (records as ToolResultSummaryRecord[]) : []
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user