diff --git a/docs/config-catalog.md b/docs/config-catalog.md index cc659f5e8c..f1c7726ad3 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -754,6 +754,8 @@ export interface ToolResultPruneConfig { export interface Config { /** Maximum synchronous VM evaluation time in milliseconds. */ vmTimeoutMs?: number + /** Maximum wait for a valid Client inspect response in milliseconds. */ + clientInspectTimeoutMs?: number } ``` diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index e89f722b88..82de52a10e 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -756,6 +756,8 @@ export interface ToolResultPruneConfig { export interface Config { /** Maximum synchronous VM evaluation time in milliseconds. */ vmTimeoutMs?: number + /** Maximum wait for a valid Client inspect response in milliseconds. */ + clientInspectTimeoutMs?: number } ``` diff --git a/docs/subsystems/extensions.md b/docs/subsystems/extensions.md index 540f3c477b..dd1ee924fc 100644 --- a/docs/subsystems/extensions.md +++ b/docs/subsystems/extensions.md @@ -46,7 +46,7 @@ list(): CordisInspectProviderView[] * @param input - optional lossless JSON input. * @param agent - requesting Agent and scope. * @param signal - tool-call cancellation. - * @returns provider JSON data. + * @returns provider JSON data; Client queries time out with the first failure, or reconnect/retry guidance if none was received. */ async query( platform: CordisInspectPlatform, providerId: string, methodName: string, input: JsonValue | undefined, agent: Agent, signal: AbortSignal, ): Promise @@ -55,7 +55,7 @@ async query( platform: CordisInspectPlatform, providerId: string, methodName: st * @param agent - Agent whose Session owns the query. * @param requestId - Pending Client query identity. * @param resolution - Client provider result or failure. - * @returns whether this response settled the still-pending query. + * @returns acknowledgement with accepted true only for a success that settles the query; failures remain diagnostics. */ resolveClientQuery( agent: Agent, requestId: CordisInspectRequestId, resolution: CordisInspectQueryResolution, ): CordisInspectResolveAck ``` diff --git a/docs/subsystems/extensions.zh.md b/docs/subsystems/extensions.zh.md index ebfe782748..8c87609938 100644 --- a/docs/subsystems/extensions.zh.md +++ b/docs/subsystems/extensions.zh.md @@ -46,7 +46,7 @@ list(): CordisInspectProviderView[] * @param input - optional lossless JSON input. * @param agent - requesting Agent and scope. * @param signal - tool-call cancellation. - * @returns provider JSON data. + * @returns provider JSON data; Client queries time out with the first failure, or reconnect/retry guidance if none was received. */ async query( platform: CordisInspectPlatform, providerId: string, methodName: string, input: JsonValue | undefined, agent: Agent, signal: AbortSignal, ): Promise @@ -55,7 +55,7 @@ async query( platform: CordisInspectPlatform, providerId: string, methodName: st * @param agent - Agent whose Session owns the query. * @param requestId - Pending Client query identity. * @param resolution - Client provider result or failure. - * @returns whether this response settled the still-pending query. + * @returns acknowledgement with accepted true only for a success that settles the query; failures remain diagnostics. */ resolveClientQuery( agent: Agent, requestId: CordisInspectRequestId, resolution: CordisInspectQueryResolution, ): CordisInspectResolveAck ``` diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml index 9aa2023296..242582b984 100644 --- a/docs/tool-catalog.i18n.yaml +++ b/docs/tool-catalog.i18n.yaml @@ -93,8 +93,8 @@ en: 594e5b42047969cc zh: 72189ca21345c210 /tool-schema-catalog/deepseek-ai-dsh-tool-cordis/cordis-inspect-query: - en: 8321129f661db9f7 - zh: 6d9c220c84d47b06 + en: 42a647ba98527f9e + zh: c53ade6a47ea523f /tool-schema-catalog/deepseek-ai-dsh-tool-bash-persistent: en: bdd9e88c49029e05 zh: bdd9e88c49029e05 diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index a77ed07737..b5e2065694 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -743,7 +743,7 @@ Source: [`packages/extensions/tool-cordis/src/index.ts`](../packages/extensions/ ### `cordis_inspect_query` -Run a read-only query declared by an Inspect Provider. platform, provider, and method must come from cordis_inspect_list, and input must satisfy that method's schema. Use this Tool before writing plugin code to read exact Service methods, Event modes, plugin Config schemas, Tool schemas, theme tokens, or live Slot trees and props. Host queries run locally. A Client query waits for the first valid page response and remains pending until a page answers or the Tool is cancelled. This Tool cannot invoke business Service methods or modify the runtime. +Run a read-only query declared by an Inspect Provider. platform, provider, and method must come from cordis_inspect_list, and input must satisfy that method's schema. Use this Tool before writing plugin code to read exact Service methods, Event modes, plugin Config schemas, Tool schemas, theme tokens, or live Slot trees and props. Host queries run locally. A Client query waits for the first valid page response within the configured timeout; otherwise it reports a Client failure or asks you to reconnect and retry. This Tool cannot invoke business Service methods or modify the runtime. ```json { diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index 88a9804c22..6e0fe11545 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -747,7 +747,7 @@ pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费 ### `cordis_inspect_query` -执行 Inspect Provider 声明的只读查询。platform、provider 和 method 必须来自 cordis_inspect_list,input 必须符合该方法的 schema。编写插件代码前,用本工具读取准确的 Service 方法、Event 模式、插件 Config schema、Tool schema、主题 token,或实时 Slot 树与 props。Host 查询在本地运行。Client 查询等待页面首个有效响应,直到页面回应或工具取消。本工具不能调用业务 Service 方法或修改运行时。 +执行 Inspect Provider 声明的只读查询。platform、provider 和 method 必须来自 cordis_inspect_list,input 必须符合该方法的 schema。编写插件代码前,用本工具读取准确的 Service 方法、Event 模式、插件 Config schema、Tool schema、主题 token,或实时 Slot 树与 props。Host 查询在本地运行。Client 查询在配置的超时内等待页面首个有效响应;否则返回 Client 错误,或提示重新连接后重试。本工具不能调用业务 Service 方法或修改运行时。 ```json { diff --git a/packages/extensions/cordis-host-runner/README.i18n.yaml b/packages/extensions/cordis-host-runner/README.i18n.yaml index 3eebc93d4f..b2cf4b2438 100644 --- a/packages/extensions/cordis-host-runner/README.i18n.yaml +++ b/packages/extensions/cordis-host-runner/README.i18n.yaml @@ -18,8 +18,11 @@ en: bdaba8036acf80c9 zh: 2dd8f972c3023a8a /deepseek-ai-dsh-cordis-host-runner/use-this-package/minimal-configuration: - en: 0be8b7f1580ca751 - zh: 984c4757e9d6e094 + en: ca844fa88b307274 + zh: da2032bfe3c1b792 +/deepseek-ai-dsh-cordis-host-runner/use-this-package/client-inspection: + en: cb787a95e31baebc + zh: 3a6d569c58d7a963 /deepseek-ai-dsh-cordis-host-runner/use-this-package/what-a-run-does: en: a3c86c081974a710 zh: 180e1b7824c070d4 diff --git a/packages/extensions/cordis-host-runner/README.md b/packages/extensions/cordis-host-runner/README.md index 67df264fe2..4e8467ecdc 100644 --- a/packages/extensions/cordis-host-runner/README.md +++ b/packages/extensions/cordis-host-runner/README.md @@ -38,9 +38,15 @@ Mount this plugin for the inspection registry or programmatic dynamic-package li | Field | Default | Meaning | |---|---|---| | `vmTimeoutMs` | `5000` | Milliseconds the synchronous portion of a host half may run in the vm before evaluation is aborted | +| `clientInspectTimeoutMs` | `10000` | Maximum wait for a valid Client inspect response; an integer from 1 to 2147483647 milliseconds | The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-cordis-host-runner) is the exhaustive source for every accepted field. + +### Client inspection + +Client queries accept the first valid page response within `clientInspectTimeoutMs`. A failed page does not prevent another page from answering successfully. If no valid result arrives, the query rejects with the first Client failure or output-validation diagnostic; when no page answers, it asks the caller to open or reconnect the Harness page and retry. Cancellation and registry disposal also end pending queries. Reconnecting a page does not replay a missed request, so retry after the connection is restored. Host queries are unaffected by this timeout. + ### What a run does Programmatic callers use `define`, `run`, `stop`, and `undefine`; the browser panel operates existing definitions. Host-only packages activate in this process. A package with a browser half waits for approval or cancellation, then loads Host before Client. `mode: "run"` starts the current version; `mode: "update"` replaces it. Stop disposes the live effects and retains the definition; undefine also forgets it. diff --git a/packages/extensions/cordis-host-runner/README.zh.md b/packages/extensions/cordis-host-runner/README.zh.md index 0c8659078e..4a60690a81 100644 --- a/packages/extensions/cordis-host-runner/README.zh.md +++ b/packages/extensions/cordis-host-runner/README.zh.md @@ -38,9 +38,15 @@ kind: "package-reference" | 字段 | 默认值 | 含义 | |---|---|---| | `vmTimeoutMs` | `5000` | host 半在 vm 中同步执行的那部分被中止求值前可运行的毫秒数 | +| `clientInspectTimeoutMs` | `10000` | 等待有效 Client 检查响应的最长时间;1 到 2147483647 毫秒之间的整数 | 生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-cordis-host-runner)是每个受支持字段的穷尽式真源。 + +### Client 检查 + +Client 查询在 `clientInspectTimeoutMs` 内接受首个有效页面响应。某个页面失败不会阻止其他页面成功回答。若没有有效结果,查询返回首个 Client 错误或输出校验诊断;若没有页面回答,则提示调用方打开或重新连接 Harness 页面后重试。取消和注册表卸载也会结束待处理查询。页面重连不会重放错过的请求,因此需在连接恢复后重试。Host 查询不受此超时影响。 + ### run 会做什么 程序调用方使用 `define`、`run`、`stop` 和 `undefine`;浏览器面板操作已有定义。仅含 Host 的包在本进程激活。带浏览器部分的包等待审批或取消,批准后先加载 Host 再加载 Client。`mode: "run"` 启动当前版本,`mode: "update"` 替换版本。Stop 释放运行中的 effect 并保留定义;undefine 还会移除定义。 diff --git a/packages/extensions/cordis-host-runner/src/index.ts b/packages/extensions/cordis-host-runner/src/index.ts index 67519f4d9f..0238447341 100644 --- a/packages/extensions/cordis-host-runner/src/index.ts +++ b/packages/extensions/cordis-host-runner/src/index.ts @@ -93,6 +93,8 @@ declare module '@deepseek-ai/cordis' { export interface Config { /** Maximum synchronous VM evaluation time in milliseconds. */ vmTimeoutMs?: number + /** Maximum wait for a valid Client inspect response in milliseconds. */ + clientInspectTimeoutMs?: number } type ResolvedConfig = Required @@ -131,6 +133,7 @@ export class DynamicCordisRunnerService extends TypertRemoteService { static Config: z = z.object({ vmTimeoutMs: z.number().min(1).default(5000), + clientInspectTimeoutMs: z.number().step(1).min(1).max(2_147_483_647).default(10_000), }) private readonly rootCtx: Context @@ -145,7 +148,7 @@ export class DynamicCordisRunnerService extends TypertRemoteService { super(ctx, 'dynamicCordisRunner') this.rootCtx = ctx this.resolved = config as ResolvedConfig - this.inspectRegistry = new CordisInspectRegistryService(ctx) + this.inspectRegistry = new CordisInspectRegistryService(ctx, this.resolved.clientInspectTimeoutMs) } /** diff --git a/packages/extensions/cordis-host-runner/src/inspect-registry.ts b/packages/extensions/cordis-host-runner/src/inspect-registry.ts index 7a43513489..ccad1bdee4 100644 --- a/packages/extensions/cordis-host-runner/src/inspect-registry.ts +++ b/packages/extensions/cordis-host-runner/src/inspect-registry.ts @@ -31,6 +31,7 @@ export interface HostCordisInspectProviderRegistration { interface PendingClientQuery { request: CordisInspectQueryRequest method: CordisInspectMethodManifest + failure?: string settle(resolution: CordisInspectQueryResolution): void } @@ -48,9 +49,18 @@ export class CordisInspectRegistryService extends Service { private clientManifest: readonly CordisInspectProviderManifest[] | undefined private nextRequest = 1 - /** Register the process-global Host registry. */ - constructor(ctx: Context) { + /** + * Register the process-global Host registry. + * @param ctx - owning Host context. + * @param clientQueryTimeoutMs - maximum wait for a valid Client response, in milliseconds. + */ + constructor(ctx: Context, private readonly clientQueryTimeoutMs: number) { super(ctx, 'cordisInspect') + ctx.effect(() => () => { + for (const pending of this.pending.values()) { + pending.settle({ ok: false, reason: 'cancelled', message: 'Client inspect registry was disposed' }) + } + }, 'cordis-inspect: pending queries') } /** @@ -102,7 +112,7 @@ export class CordisInspectRegistryService extends Service { * @param input - optional lossless JSON input. * @param agent - requesting Agent and scope. * @param signal - tool-call cancellation. - * @returns provider JSON data. + * @returns provider JSON data; Client queries time out with the first failure, or reconnect/retry guidance if none was received. */ async query( platform: CordisInspectPlatform, @@ -130,7 +140,7 @@ export class CordisInspectRegistryService extends Service { * @param agent - Agent whose Session owns the query. * @param requestId - Pending Client query identity. * @param resolution - Client provider result or failure. - * @returns whether this response settled the still-pending query. + * @returns acknowledgement with accepted true only for a success that settles the query; failures remain diagnostics. */ resolveClientQuery( agent: Agent, @@ -139,18 +149,21 @@ export class CordisInspectRegistryService extends Service { ): CordisInspectResolveAck { const pending = this.pending.get(requestId) if (pending === undefined || pending.request.agentId !== agent.id) return { accepted: false } - if (!resolution.ok) return { accepted: false } + // A failed page must not prevent another page from supplying a valid result. + if (!resolution.ok) { + pending.failure ??= `${resolution.reason}: ${resolution.message}` + return { accepted: false } + } try { resolution = { ok: true, data: validateOutput('Client', pending.request.provider, pending.method, resolution.data), } - } catch { + } catch (error) { + pending.failure ??= error instanceof Error ? error.message : String(error) return { accepted: false } } - this.pending.delete(requestId) pending.settle(resolution) - this.ctx.emit('cordis/inspect-query-resolved', { requestId }) return { accepted: true } } @@ -175,24 +188,50 @@ export class CordisInspectRegistryService extends Service { ...input === undefined ? {} : { input }, } const result = new Promise((resolve) => { - this.pending.set(requestId, { request, method, settle: resolve }) + this.pending.set(requestId, { + request, + method, + settle: (resolution) => { + this.pending.delete(requestId) + resolve(resolution) + try { + this.ctx.emit('cordis/inspect-query-resolved', { requestId }) + } catch (error) { + console.error('[cordis-host-runner] notifying Client inspect completion failed:', error) + } + }, + }) }) const onAbort = (): void => { + this.pending.get(requestId)?.settle({ + ok: false, + reason: 'cancelled', + message: `Client inspect query ${providerId}.${methodName} was cancelled`, + }) + } + const timer = setTimeout(() => { const pending = this.pending.get(requestId) if (pending === undefined) return - this.pending.delete(requestId) - pending.settle({ ok: false, reason: 'cancelled', message: `Client inspect query ${providerId}.${methodName} was cancelled` }) - this.ctx.emit('cordis/inspect-query-resolved', { requestId }) - } + const detail = pending.failure === undefined + ? 'Open or reconnect the Harness page, then retry.' + : `Client failure: ${pending.failure}` + pending.settle({ + ok: false, + reason: 'provider-error', + message: `Client inspect query ${providerId}.${methodName} timed out after ${this.clientQueryTimeoutMs}ms. ${detail}`, + }) + }, this.clientQueryTimeoutMs) signal.addEventListener('abort', onAbort, { once: true }) - if (signal.aborted) onAbort() - else this.ctx.emit('cordis/inspect-query', request) try { + if (signal.aborted) onAbort() + else this.ctx.emit('cordis/inspect-query', request) const resolution = await result if (!resolution.ok) throw new Error(`${providerId}.${methodName}: ${resolution.message}`) return resolution.data } finally { + clearTimeout(timer) signal.removeEventListener('abort', onAbort) + this.pending.delete(requestId) } } } diff --git a/packages/extensions/cordis-host-runner/src/types.ts b/packages/extensions/cordis-host-runner/src/types.ts index 5cf28e503e..439e626ca1 100644 --- a/packages/extensions/cordis-host-runner/src/types.ts +++ b/packages/extensions/cordis-host-runner/src/types.ts @@ -84,7 +84,7 @@ export interface CordisInspectQueryResolved { /** Whether a Client answer claimed the still-pending query. */ export interface CordisInspectResolveAck { - /** False for unknown, cancelled, stale, or late answers. */ + /** True only for a valid success that settles the query; false for retained failures and unknown, expired, cancelled, or late answers. */ accepted: boolean } diff --git a/packages/extensions/cordis-host-runner/tests/inspect-registry.spec.ts b/packages/extensions/cordis-host-runner/tests/inspect-registry.spec.ts new file mode 100644 index 0000000000..c70b24d8cd --- /dev/null +++ b/packages/extensions/cordis-host-runner/tests/inspect-registry.spec.ts @@ -0,0 +1,215 @@ +import { Context } from '@deepseek-ai/cordis' +import type { Fiber } from '@deepseek-ai/cordis' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import DynamicCordisRunnerService from '../src/index.ts' +import { CordisInspectRegistryService } from '../src/inspect-registry.ts' +import type { CordisInspectProviderManifest, CordisInspectQueryRequest, CordisInspectRequestId } from '../src/types.ts' +import { AGENT_A, AGENT_B } from './helpers.ts' + +const manifest: CordisInspectProviderManifest = { + id: 'Service', + description: 'Test Service provider.', + methods: [{ + name: 'listService', + description: 'Inspect one service.', + inputSchema: { type: 'object', properties: { service: { type: 'string' } }, additionalProperties: false }, + outputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, + }], +} +const timeout = 'Error: Service.listService: Client inspect query Service.listService timed out after 100ms. ' +const failure = { ok: false, reason: 'provider-error', message: 'page failed' } as const + +let ctx: Context +let fiber: Fiber +let registry: CordisInspectRegistryService +let requests: CordisInspectQueryRequest[] +let resolved: CordisInspectRequestId[] +let controllers: AbortController[] +let results: Promise[] +let disposers: (() => void)[] + +beforeEach(async () => { + vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) + requests = [] + resolved = [] + controllers = [] + results = [] + disposers = [] + ctx = new Context() + const mounted = ctx.plugin(CordisInspectRegistryService, 100) + fiber = mounted + await mounted + registry = ctx.cordisInspect + registry.syncClientManifest([manifest]) + disposers.push(ctx.on('cordis/inspect-query', (request) => { requests.push(request) })) + disposers.push(ctx.on('cordis/inspect-query-resolved', (event) => { resolved.push(event.requestId) })) +}) + +afterEach(async () => { + try { + for (const controller of controllers) controller.abort() + await Promise.allSettled(results) + await fiber.dispose() + } finally { + for (const dispose of disposers) dispose() + vi.restoreAllMocks() + vi.useRealTimers() + } +}) + +function start() { + const controller = new AbortController() + controllers.push(controller) + let settled = false + const result = registry.query('client', 'Service', 'listService', { service: 'remote' }, AGENT_A, controller.signal).then( + (value) => { settled = true; return { value } }, + (error: unknown) => { settled = true; return { error: String(error) } }, + ) + results.push(result) + return { controller, result, request: requests.at(-1)!, settled: () => settled } +} + +function answer(query: ReturnType) { + return registry.resolveClientQuery(AGENT_A, query.request.requestId, { ok: true, data: { name: 'theme' } }) +} + +describe('Client inspect completion', () => { + it('returns the Client catalog failure at the deadline instead of losing it', async () => { + const query = start() + expect(registry.resolveClientQuery(AGENT_A, query.request.requestId, { + ok: false, reason: 'provider-error', message: 'no catalogued Service named "remote"', + })).toEqual({ accepted: false }) + await vi.advanceTimersByTimeAsync(99) + expect(query.settled()).toBe(false) + await vi.advanceTimersByTimeAsync(1) + expect(await query.result).toEqual({ error: timeout + 'Client failure: provider-error: no catalogued Service named "remote"' }) + expect(resolved).toEqual([query.request.requestId]) + expect(answer(query)).toEqual({ accepted: false }) + expect(vi.getTimerCount()).toBe(0) + }) + + it('allows a later successful page to win after another page fails', async () => { + const query = start() + expect(registry.resolveClientQuery(AGENT_A, query.request.requestId, failure)).toEqual({ accepted: false }) + await vi.advanceTimersByTimeAsync(99) + expect(answer(query)).toEqual({ accepted: true }) + expect(await query.result).toEqual({ value: { name: 'theme' } }) + await vi.advanceTimersByTimeAsync(1000) + expect(resolved).toEqual([query.request.requestId]) + expect(vi.getTimerCount()).toBe(0) + }) + + it('keeps the first failure when several pages fail', async () => { + const query = start() + registry.resolveClientQuery(AGENT_A, query.request.requestId, failure) + registry.resolveClientQuery(AGENT_A, query.request.requestId, { ...failure, message: 'second failure' }) + await vi.advanceTimersByTimeAsync(100) + expect(await query.result).toEqual({ error: timeout + 'Client failure: provider-error: page failed' }) + }) + + it('retains output validation errors for timeout diagnostics', async () => { + const query = start() + expect(registry.resolveClientQuery(AGENT_A, query.request.requestId, { ok: true, data: {} })).toEqual({ accepted: false }) + await vi.advanceTimersByTimeAsync(100) + expect(await query.result).toHaveProperty('error', expect.stringContaining('Client Cordis inspect Service.listService returned invalid output')) + }) + + it('accepts valid output after an invalid response', async () => { + const query = start() + registry.resolveClientQuery(AGENT_A, query.request.requestId, { ok: true, data: {} }) + expect(answer(query)).toEqual({ accepted: true }) + expect(await query.result).toEqual({ value: { name: 'theme' } }) + expect(vi.getTimerCount()).toBe(0) + }) + + it('ignores responses from another Agent without retaining their error', async () => { + const query = start() + expect(registry.resolveClientQuery(AGENT_B, query.request.requestId, failure)).toEqual({ accepted: false }) + await vi.advanceTimersByTimeAsync(100) + expect(await query.result).toEqual({ error: timeout + 'Open or reconnect the Harness page, then retry.' }) + }) + + it('ends unanswered requests and accepts a fresh retry after manifest publication', async () => { + const first = start() + registry.syncClientManifest([manifest]) + expect(requests).toHaveLength(1) + await vi.advanceTimersByTimeAsync(100) + expect(await first.result).toEqual({ error: timeout + 'Open or reconnect the Harness page, then retry.' }) + const retry = start() + expect(retry.request.requestId).not.toBe(first.request.requestId) + expect(answer(first)).toEqual({ accepted: false }) + expect(answer(retry)).toEqual({ accepted: true }) + expect(await retry.result).toEqual({ value: { name: 'theme' } }) + expect(resolved).toHaveLength(2) + }) + + it('cancels before the deadline and removes its timer and abort listener', async () => { + const query = start() + const remove = vi.spyOn(query.controller.signal, 'removeEventListener') + query.controller.abort() + expect(await query.result).toEqual({ error: 'Error: Service.listService: Client inspect query Service.listService was cancelled' }) + expect(remove).toHaveBeenCalledWith('abort', expect.any(Function)) + expect(vi.getTimerCount()).toBe(0) + expect(answer(query)).toEqual({ accepted: false }) + await vi.advanceTimersByTimeAsync(100) + expect(resolved).toEqual([query.request.requestId]) + }) + + it('does not dispatch or start a timer for an already cancelled query', async () => { + const controller = new AbortController() + controller.abort(new Error('already cancelled')) + await expect(registry.query('client', 'Service', 'listService', {}, AGENT_A, controller.signal)).rejects.toThrow('already cancelled') + expect(requests).toEqual([]) + expect(vi.getTimerCount()).toBe(0) + }) + + it('settles all pending requests when the registry is disposed', async () => { + const first = start() + const second = start() + await fiber.dispose() + for (const query of [first, second]) { + expect(await query.result).toEqual({ error: 'Error: Service.listService: Client inspect registry was disposed' }) + expect(answer(query)).toEqual({ accepted: false }) + } + expect(resolved).toEqual([first.request.requestId, second.request.requestId]) + expect(vi.getTimerCount()).toBe(0) + }) + + it.each(['timeout', 'disposal'] as const)('contains throwing completion subscribers during %s', async (completion) => { + const error = new Error('subscriber failed') + const log = vi.spyOn(console, 'error').mockImplementation(() => {}) + disposers.push(ctx.on('cordis/inspect-query-resolved', () => { throw error })) + const queries = [start(), start()] + if (completion === 'timeout') await vi.advanceTimersByTimeAsync(100) + else await fiber.dispose() + for (const query of queries) { + expect(await query.result).toHaveProperty('error') + expect(answer(query)).toEqual({ accepted: false }) + } + expect(log).toHaveBeenCalledTimes(2) + expect(log).toHaveBeenCalledWith('[cordis-host-runner] notifying Client inspect completion failed:', error) + expect(vi.getTimerCount()).toBe(0) + expect(resolved).toHaveLength(2) + }) + + it('leaves local Host queries outside the Client response deadline', async () => { + disposers.push(registry.register({ manifest, query: async () => ({ name: 'local' }) })) + await expect(registry.query('host', 'Service', 'listService', {}, AGENT_A, new AbortController().signal)) + .resolves.toEqual({ name: 'local' }) + expect(requests).toEqual([]) + expect(vi.getTimerCount()).toBe(0) + }) +}) + +describe('Client inspect timeout configuration', () => { + it('defaults to ten seconds and accepts both timer limits', () => { + expect(DynamicCordisRunnerService.Config({}).clientInspectTimeoutMs).toBe(10_000) + for (const clientInspectTimeoutMs of [1, 100, 2_147_483_647]) { + expect(DynamicCordisRunnerService.Config({ clientInspectTimeoutMs }).clientInspectTimeoutMs).toBe(clientInspectTimeoutMs) + } + }) + + it.each([0, -1, 0.5, Infinity, NaN, 2_147_483_648])('rejects invalid timer budget %s', (clientInspectTimeoutMs) => { + expect(() => DynamicCordisRunnerService.Config({ clientInspectTimeoutMs })).toThrow() + }) +}) diff --git a/packages/extensions/tool-cordis/README.i18n.yaml b/packages/extensions/tool-cordis/README.i18n.yaml index 223182f332..9a445f9a0f 100644 --- a/packages/extensions/tool-cordis/README.i18n.yaml +++ b/packages/extensions/tool-cordis/README.i18n.yaml @@ -39,8 +39,8 @@ en: f57a20cd4e76d13c zh: 7b673116422bee87 /deepseek-ai-dsh-tool-cordis/known-limitations-and-deferred-work: - en: ffe15dd1fd333dbf - zh: b256afc74a1782cb + en: 3c135d9cb67790d2 + zh: ff2d0a444f55d28f /deepseek-ai-dsh-tool-cordis/known-limitations-and-deferred-work/dev-note: en: 0a1253c6f7b5e8b9 zh: 9f56528562c7c914 diff --git a/packages/extensions/tool-cordis/README.md b/packages/extensions/tool-cordis/README.md index 23e2d4a3d0..54f16c1d2c 100644 --- a/packages/extensions/tool-cordis/README.md +++ b/packages/extensions/tool-cordis/README.md @@ -68,7 +68,7 @@ Unchanged tool schemas remain prefix-stable. Query results append to history; en -- Client queries wait for a responding page or cancellation. Inspection cannot invoke service methods, configure plugins, or execute generated code. +- Client queries need a responding page and use the Host runner's [bounded wait and retry policy](../cordis-host-runner/README.md#client-inspection). Inspection cannot invoke service methods, configure plugins, or execute generated code. - `Config.listConfigs` walks the profile Loader tree only. Agent preset `plugins` lists mount in detached preset trees, so a plugin present only inside a preset declaration is not listed unless the profile tree also mounts it. diff --git a/packages/extensions/tool-cordis/README.zh.md b/packages/extensions/tool-cordis/README.zh.md index da855b7b1d..f6d7f1f85d 100644 --- a/packages/extensions/tool-cordis/README.zh.md +++ b/packages/extensions/tool-cordis/README.zh.md @@ -68,7 +68,7 @@ Host provider 结合生成的 Service/Event 目录、经 app-boot Config 投影 -- Client 查询等待页面响应或取消。检查不能调用服务方法、配置插件或执行生成代码。 +- Client 查询需要页面响应,并采用 Host runner 的[有界等待与重试策略](../cordis-host-runner/README.zh.md#client-inspection)。检查不能调用服务方法、配置插件或执行生成代码。 - `Config.listConfigs` 只遍历 profile 的 Loader 树。Agent preset 的 `plugins` 列表挂载在独立的 preset 树中,所以只出现在 preset 声明里的插件不会被列出,除非 profile 树也挂载了它。 diff --git a/packages/extensions/tool-cordis/src/index.ts b/packages/extensions/tool-cordis/src/index.ts index 1686e15cb2..eddc643cb9 100644 --- a/packages/extensions/tool-cordis/src/index.ts +++ b/packages/extensions/tool-cordis/src/index.ts @@ -44,9 +44,9 @@ export function apply(ctx: Context): void { 'Run a read-only query declared by an Inspect Provider. platform, provider, and method must come from ' + 'cordis_inspect_list, and input must satisfy that method\'s schema. Use this Tool before writing plugin code ' + 'to read exact Service methods, Event modes, plugin Config schemas, Tool schemas, theme tokens, or live ' - + 'Slot trees and props. Host queries run locally. A Client query waits for the first valid page response and ' - + 'remains pending until a page answers or the Tool is cancelled. This Tool cannot invoke business Service ' - + 'methods or modify the runtime.', + + 'Slot trees and props. Host queries run locally. A Client query waits for the first valid page response ' + + 'within the configured timeout; otherwise it reports a Client failure or asks you to reconnect and retry. ' + + 'This Tool cannot invoke business Service methods or modify the runtime.', parameters: { platform: { type: 'string', required: true, enum: ['host', 'client'], description: 'Runtime platform that owns the Provider.' }, provider: { type: 'string', required: true, description: 'Exact Provider ID returned by cordis_inspect_list.' }, diff --git a/packages/extensions/tool-cordis/tests/host.spec.ts b/packages/extensions/tool-cordis/tests/host.spec.ts index 87bd3b25f8..53e0b7db62 100644 --- a/packages/extensions/tool-cordis/tests/host.spec.ts +++ b/packages/extensions/tool-cordis/tests/host.spec.ts @@ -17,7 +17,7 @@ async function host(): Promise { // The tool registry injects `systemPrompt`; nothing under test registers a prompt section. await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) - await ctx.plugin(CordisInspectRegistryService) + await ctx.plugin(CordisInspectRegistryService, 10_000) return ctx } diff --git a/snapshots/session/cordis-inspect-jsdoc/system-prompt.expected.md b/snapshots/session/cordis-inspect-jsdoc/system-prompt.expected.md index f30cc50a1d..f9f8f0fe6d 100644 --- a/snapshots/session/cordis-inspect-jsdoc/system-prompt.expected.md +++ b/snapshots/session/cordis-inspect-jsdoc/system-prompt.expected.md @@ -67,7 +67,7 @@ interface ToolArgsMap { } & Record; /** List every Cordis Inspect Provider currently known to the Host, including local Host Providers and the latest manifests synchronized from the Client. Each entry includes its platform, purpose, read-only methods, and input/output schemas. Call this Tool before writing or configuring a plugin, then select the provider and method for cordis_inspect_query from its result. Do not guess names or treat an Inspect method as a business Service that Plugin code can call. */ cordis_inspect_list: Record; - /** Run a read-only query declared by an Inspect Provider. platform, provider, and method must come from cordis_inspect_list, and input must satisfy that method's schema. Use this Tool before writing plugin code to read exact Service methods, Event modes, plugin Config schemas, Tool schemas, theme tokens, or live Slot trees and props. Host queries run locally. A Client query waits for the first valid page response and remains pending until a page answers or the Tool is cancelled. This Tool cannot invoke business Service methods or modify the runtime. */ + /** Run a read-only query declared by an Inspect Provider. platform, provider, and method must come from cordis_inspect_list, and input must satisfy that method's schema. Use this Tool before writing plugin code to read exact Service methods, Event modes, plugin Config schemas, Tool schemas, theme tokens, or live Slot trees and props. Host queries run locally. A Client query waits for the first valid page response within the configured timeout; otherwise it reports a Client failure or asks you to reconnect and retry. This Tool cannot invoke business Service methods or modify the runtime. */ cordis_inspect_query: { /** Runtime platform that owns the Provider. */ platform: "host" | "client"; diff --git a/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json b/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json index 7b11913391..3eea9127a8 100644 --- a/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json +++ b/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json @@ -55,7 +55,7 @@ }, { "name": "cordis_inspect_query", - "description": "Run a read-only query declared by an Inspect Provider. platform, provider, and method must come from cordis_inspect_list, and input must satisfy that method's schema. Use this Tool before writing plugin code to read exact Service methods, Event modes, plugin Config schemas, Tool schemas, theme tokens, or live Slot trees and props. Host queries run locally. A Client query waits for the first valid page response and remains pending until a page answers or the Tool is cancelled. This Tool cannot invoke business Service methods or modify the runtime.", + "description": "Run a read-only query declared by an Inspect Provider. platform, provider, and method must come from cordis_inspect_list, and input must satisfy that method's schema. Use this Tool before writing plugin code to read exact Service methods, Event modes, plugin Config schemas, Tool schemas, theme tokens, or live Slot trees and props. Host queries run locally. A Client query waits for the first valid page response within the configured timeout; otherwise it reports a Client failure or asks you to reconnect and retry. This Tool cannot invoke business Service methods or modify the runtime.", "parameters": { "type": "object", "properties": { diff --git a/snapshots/session/cordis-inspect-timeout/client-fixture.mjs b/snapshots/session/cordis-inspect-timeout/client-fixture.mjs new file mode 100644 index 0000000000..2b47ff24df --- /dev/null +++ b/snapshots/session/cordis-inspect-timeout/client-fixture.mjs @@ -0,0 +1,57 @@ +/** Test-only Client transport: one provider failure, then a page that never answers. */ + +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' + +export const name = 'client-inspect-timeout-fixture' +export const inject = ['cordisInspect', 'agents', 'llm'] + +/** + * Mirror a Client manifest and deliver only the first query's failure. + * @param {import('@deepseek-ai/cordis').Context} ctx - isolated snapshot Host. + */ +export function apply(ctx) { + const expected = readFileSync(process.env.DSH_SNAPSHOT_FILE, 'utf8').trim().split('\n') + .map(line => JSON.parse(line)) + .filter(event => event.type === 'tool/result') + .map(event => event.data.message) + const resultFields = message => ({ + toolCallId: message.toolCallId, + content: message.content, + isError: message.isError, + }) + let requests = 0 + ctx.on('llm/stream', (options, next) => { + assert.deepEqual( + options.messages.filter(message => message.role === 'tool').map(resultFields), + expected.slice(0, requests++).map(resultFields), + 'Each continuing model request receives the recorded Client timeout errors', + ) + return next() + }) + ctx.cordisInspect.syncClientManifest([{ + id: 'Service', + description: 'Snapshot Client Service provider.', + methods: [{ + name: 'listService', + description: 'Inspect one Client Service.', + inputSchema: { + type: 'object', + properties: { service: { type: 'string' } }, + additionalProperties: false, + }, + outputSchema: { type: 'object' }, + }], + }]) + let queries = 0 + ctx.on('cordis/inspect-query', (request) => { + if (++queries !== 1) return + const agent = ctx.agents.get(request.agentId) + if (agent === undefined) throw new Error('Client inspect request has no owning Agent') + ctx.cordisInspect.resolveClientQuery(agent, request.requestId, { + ok: false, + reason: 'provider-error', + message: 'no catalogued Service named "remote"', + }) + }) +} diff --git a/snapshots/session/cordis-inspect-timeout/cordis.snapshot.yml b/snapshots/session/cordis-inspect-timeout/cordis.snapshot.yml new file mode 100644 index 0000000000..2d341f89bd --- /dev/null +++ b/snapshots/session/cordis-inspect-timeout/cordis.snapshot.yml @@ -0,0 +1,54 @@ +- id: llm-deepseek + name: '@deepseek-ai/dsh-llm-deepseek-api-key' + disabled: true + +- id: agent-default-model + name: '@deepseek-ai/dsh-agent-default-model' + config: + provider: deepseek-official + model: deepseek-v4-flash + +- id: session-persistence-jsonl + name: '@deepseek-ai/dsh-session-persistence-jsonl' + config: + root: !!js dshHomePath('sessions') + compression: none + +- id: agent-instructions + name: '@deepseek-ai/dsh-agent-instructions' + config: + maxBytes: 65536 + +- id: tools + name: '@deepseek-ai/dsh-tools' + config: + mode: both + +- id: system-prompt + name: '@deepseek-ai/dsh-system-prompt' + config: + personaPrefix: | + You are a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}. + + Verify your work by running the code or tests. Keep answers brief and factual. + +- insert: + - id: cordis-host-runner + name: '@deepseek-ai/dsh-cordis-host-runner' + config: + clientInspectTimeoutMs: 100 + - id: cordis-inspect-providers + name: '@deepseek-ai/dsh-tool-cordis/host' + - id: tool-cordis + name: '@deepseek-ai/dsh-tool-cordis' + - id: llm-replay + name: '@deepseek-ai/dsh-llm-replay' + config: + providers: + - id: deepseek-official + name: DeepSeek + models: + - id: deepseek-v4-flash + - id: deepseek-v4-pro + - id: client-inspect-timeout-fixture + name: ./client-fixture.mjs diff --git a/snapshots/session/cordis-inspect-timeout/cordis.yml b/snapshots/session/cordis-inspect-timeout/cordis.yml new file mode 100644 index 0000000000..b1f5fe2613 --- /dev/null +++ b/snapshots/session/cordis-inspect-timeout/cordis.yml @@ -0,0 +1,41 @@ +- id: agent-default-model + name: '@deepseek-ai/dsh-agent-default-model' + config: + provider: deepseek-official + model: deepseek-v4-pro + +- id: session-persistence-jsonl + name: '@deepseek-ai/dsh-session-persistence-jsonl' + config: + root: !!js dshHomePath('sessions') + compression: !!js 'process.env.DSH_SNAPSHOT === undefined ? ''zstd'' : ''none''' + +- id: agent-instructions + name: '@deepseek-ai/dsh-agent-instructions' + config: + maxBytes: 65536 + +- id: tools + name: '@deepseek-ai/dsh-tools' + config: + mode: both + +- id: system-prompt + name: '@deepseek-ai/dsh-system-prompt' + config: + personaPrefix: | + You are a coding assistant powered by the {{model}} model. Your working directory is {{cwd}}. + + Verify your work by running the code or tests. Keep answers brief and factual. + +- insert: + - id: cordis-host-runner + name: '@deepseek-ai/dsh-cordis-host-runner' + config: + clientInspectTimeoutMs: 100 + - id: cordis-inspect-providers + name: '@deepseek-ai/dsh-tool-cordis/host' + - id: tool-cordis + name: '@deepseek-ai/dsh-tool-cordis' + - id: client-inspect-timeout-fixture + name: ./client-fixture.mjs diff --git a/snapshots/session/cordis-inspect-timeout/session.v4.jsonl b/snapshots/session/cordis-inspect-timeout/session.v4.jsonl new file mode 100644 index 0000000000..fb9cdc45ca --- /dev/null +++ b/snapshots/session/cordis-inspect-timeout/session.v4.jsonl @@ -0,0 +1,27 @@ +{"type":"session","version":4,"id":"{{session:1}}","createdAt":1783951000000,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0} +{"type":"permission/preset","data":{"preset":"danger-full-access"}} +{"type":"sandbox/mode","data":{"mode":"danger-full-access"}} +{"type":"approval/policy","data":{"policy":"never"}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Inspect the Client Service named remote twice with cordis_inspect_query. After both errors, reply exactly CORDIS_INSPECT_TIMEOUT_RECOVERED."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}} +{"type":"turn/start","data":{"turn":1}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} +{"type":"step/start","data":{"turn":1,"step":1}} +{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Inspect the Client Service named remote twice with cordis_inspect_query. After both errors, reply exactly CORDIS_INSPECT_TIMEOUT_RECOVERED."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} +{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} +{"type":"session/title","data":{"title":"Inspect the Client Service named","messageSeqs":[8],"source":{"kind":"fallback"}}} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"inspect-client-1","name":"cordis_inspect_query","arguments":"{\"platform\":\"client\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"remote\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788778145981,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788778145981,"index":0,"dt":[],"id":"inspect-client-1","name":"cordis_inspect_query","args":["{\"platform\":\"client\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"remote\"}}"]},{"type":"chunk","time":1788778145981,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"inspect-client-1","name":"cordis_inspect_query","arguments":"{\"platform\":\"client\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"remote\"}}"}}},{"type":"chunk","time":1788778145981,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788778145981,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"tool/call","data":{"turn":1,"step":1,"callId":"inspect-client-1","name":"cordis_inspect_query","arguments":"{\"platform\":\"client\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"remote\"}}"}} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"inspect-client-1"},"toolCallId":"inspect-client-1","content":[{"type":"text","text":"Error: Service.listService: Client inspect query Service.listService timed out after 100ms. Client failure: provider-error: no catalogued Service named \"remote\""}],"isError":true,"id":"{{message:5}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":1}} +{"type":"step/start","data":{"turn":1,"step":2}} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"inspect-client-2","name":"cordis_inspect_query","arguments":"{\"platform\":\"client\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"remote\"}}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788778146017,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1788778146017,"index":0,"dt":[],"id":"inspect-client-2","name":"cordis_inspect_query","args":["{\"platform\":\"client\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"remote\"}}"]},{"type":"chunk","time":1788778146017,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"inspect-client-2","name":"cordis_inspect_query","arguments":"{\"platform\":\"client\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"remote\"}}"}}},{"type":"chunk","time":1788778146017,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788778146017,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"tool/call","data":{"turn":1,"step":2,"callId":"inspect-client-2","name":"cordis_inspect_query","arguments":"{\"platform\":\"client\",\"provider\":\"Service\",\"method\":\"listService\",\"input\":{\"service\":\"remote\"}}"}} +{"type":"tool/result","data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"inspect-client-2"},"toolCallId":"inspect-client-2","content":[{"type":"text","text":"Error: Service.listService: Client inspect query Service.listService timed out after 100ms. Open or reconnect the Harness page, then retry."}],"isError":true,"id":"{{message:7}}"}},"sourceEventSeqs":[19],"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":2}} +{"type":"step/start","data":{"turn":1,"step":3}} +{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"CORDIS_INSPECT_TIMEOUT_RECOVERED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1788778146037,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1788778146037,"index":0,"dt":[],"texts":["CORDIS_INSPECT_TIMEOUT_RECOVERED"]},{"type":"chunk","time":1788778146037,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CORDIS_INSPECT_TIMEOUT_RECOVERED"}}},{"type":"chunk","time":1788778146037,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1788778146037,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":3}} +{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/cordis-inspect-timeout/snapshot.yml b/snapshots/session/cordis-inspect-timeout/snapshot.yml new file mode 100644 index 0000000000..a861a54735 --- /dev/null +++ b/snapshots/session/cordis-inspect-timeout/snapshot.yml @@ -0,0 +1,10 @@ +version: 1 +scenario: cordis-inspect-timeout +profile: headless +composition: cordis-inspect-timeout +recording: authored +header: + class: advanced + pin: true + systemPromptSource: cordis-inspect-jsdoc + toolSchemasSource: cordis-inspect-jsdoc