diff --git a/packages/sdk/client/README.i18n.yaml b/packages/sdk/client/README.i18n.yaml index 4751a5b061..4b1f7ef5fe 100644 --- a/packages/sdk/client/README.i18n.yaml +++ b/packages/sdk/client/README.i18n.yaml @@ -21,11 +21,11 @@ en: 01c58dddd7e62de2 zh: 6dbd46e244f3f135 /deepseek-ai-dsh-sdk-client/use-this-package/sending-input-and-receiving-responses: - en: b34bfbb810f55724 - zh: ec4756e11d7fb037 + en: 7f66344ec3aa4c03 + zh: e5454af3eac36ec3 /deepseek-ai-dsh-sdk-client/use-this-package/lower-level-control-with-harnessclient: - en: 73217d8d1817f532 - zh: cf92102afb34acfc + en: 5096b15f67e083b5 + zh: 746068458a423a2b /deepseek-ai-dsh-sdk-client/use-this-package/native-user-questions: en: f5b9d6a7363bd9e5 zh: f53b3786d22a729d diff --git a/packages/sdk/client/README.md b/packages/sdk/client/README.md index b3e8e78f35..b3b417b5f1 100644 --- a/packages/sdk/client/README.md +++ b/packages/sdk/client/README.md @@ -55,7 +55,7 @@ The subprocess starts lazily on first use and stays owned by the instance across ### Sending input and receiving responses -`await session.query(text, { delivery: 'queue' })` starts the runtime if needed, sends text, and returns without awaiting runtime routing or execution. Delivery defaults to `queue`; `steer` targets the next available step for ordinary input. The full SDK intercepts bare `/compact`, `/permission ` with a non-blank preset, `/plan [off|message]`, and `/goal [objective|clear|edit objective|pause|resume]`, including their registered Desktop aliases. Bare `/permission` or `/权限`, `/model`, and `/compact extra` remain ordinary prompts. `run()` never dispatches these commands. Query is text-only; use `run()` for content-block prompts. +`await session.query(text, { delivery: 'queue' })` starts the runtime if needed, sends text, and returns without awaiting runtime routing or execution. Delivery defaults to `queue`; `steer` targets the next available step for ordinary input. The runtime recognizes the [supported command forms and Desktop aliases](../server/README.md#command-input); unrecognized input remains ordinary text. `run()` never dispatches these commands. Query is text-only; use `run()` for content-block prompts. ```ts import { DeepSeekHarness } from '@deepseek-ai/dsh-sdk-client' @@ -79,7 +79,7 @@ Launch options provide initial values, not a shared live configuration object. A ### Lower-level control with HarnessClient -`client.query(sessionId, text, delivery, signal?)` waits for a validated prompt receipt or command result. `client.executeCommand(sessionId, line)` is the strict entry for `/compact`, `/permission `, `/plan [off|message]`, and `/goal [objective|clear|edit objective|pause|resume]`; it rejects bare `/permission`. A non-blank unknown preset reaches the native handler and returns `result.kind === "error"`. Subscribe before these calls when notifications are needed. An optional fourth `request()` argument supplies an `AbortSignal` that abandons only the local wait; runtime work can continue after timeout or local abandonment. +`client.query(sessionId, text, delivery, signal?)` waits for a validated prompt receipt or command result. `client.executeCommand(sessionId, line)` executes supported command names without prompt fallback; it requires a non-blank permission preset. A non-blank unknown preset reaches the native handler and returns `result.kind === "error"`. Subscribe before these calls when notifications are needed. An optional fourth `request()` argument supplies an `AbortSignal` that abandons only the local wait; runtime work can continue after timeout or local abandonment. `/goal` uses the native goal handler and can start automatic rounds after its `CommandExecution`. Subscribe before sending; observe a terminal `goal/change` operation before using `session/wait` for the closing reply. Idle between rounds is not goal completion. See the [server semantics](../server/README.md) for pause, clear, restoration, and permission behavior. diff --git a/packages/sdk/client/README.zh.md b/packages/sdk/client/README.zh.md index 68a05b063b..55614e0f39 100644 --- a/packages/sdk/client/README.zh.md +++ b/packages/sdk/client/README.zh.md @@ -55,7 +55,7 @@ console.log(result.finalResponse) ### 发送输入与接收响应 -`await session.query(text, { delivery: 'queue' })` 在需要时启动运行时、发送文本,然后返回,不等待运行时的路由或执行结果。delivery 默认为 `queue`;对普通输入,`steer` 指向下一个可用步骤。完整 SDK 接管裸 `/compact`、带非空 preset 的 `/permission `、`/plan [off|message]` 和 `/goal [objective|clear|edit objective|pause|resume]`,包括绑定到其定义的 Desktop 别名。裸 `/permission` 或 `/权限`、`/model` 和 `/compact extra` 仍是普通提示词。`run()` 不分派这些命令。query 只接受文本,内容块提示词请使用 `run()`。 +`await session.query(text, { delivery: 'queue' })` 在需要时启动运行时、发送文本,然后返回,不等待运行时的路由或执行结果。delivery 默认为 `queue`;对普通输入,`steer` 指向下一个可用步骤。运行时识别[支持的命令形式与 Desktop 别名](../server/README.zh.md#command-input);未识别的输入保持普通文本。`run()` 不分派这些命令。query 只接受文本,内容块提示词请使用 `run()`。 ```ts import { DeepSeekHarness } from '@deepseek-ai/dsh-sdk-client' @@ -79,7 +79,7 @@ for await (const message of session.receiveResponse()) { ### 用 HarnessClient 做低层控制 -`client.query(sessionId, text, delivery, signal?)` 等待经过校验的提示词回执或命令结果。`client.executeCommand(sessionId, line)` 是 `/compact`、`/permission `、`/plan [off|message]` 和 `/goal [objective|clear|edit objective|pause|resume]` 的严格入口,会拒绝裸 `/permission`。非空的未知 preset 会进入原生 handler,返回 `result.kind === "error"`。需要通知时,应在调用前订阅。`request()` 的可选第四个参数接受 `AbortSignal`,仅放弃本地等待;超时或本地放弃后,运行时工作仍可继续。 +`client.query(sessionId, text, delivery, signal?)` 等待经过校验的提示词回执或命令结果。`client.executeCommand(sessionId, line)` 执行支持的命令名,不回退为提示词;permission preset 必须非空。非空的未知 preset 会进入原生 handler,返回 `result.kind === "error"`。需要通知时,应在调用前订阅。`request()` 的可选第四个参数接受 `AbortSignal`,仅放弃本地等待;超时或本地放弃后,运行时工作仍可继续。 `/goal` 使用原生目标 handler,可在 `CommandExecution` 返回后启动自动轮次。发送前应订阅;先观察终态 `goal/change` 操作,再用 `session/wait` 等待收尾回复。轮次之间的 idle 不代表目标完成。暂停、清除、恢复和权限行为见[服务端语义](../server/README.zh.md)。 diff --git a/packages/sdk/client/src/api.ts b/packages/sdk/client/src/api.ts index b99fca3aee..26dcfe3273 100644 --- a/packages/sdk/client/src/api.ts +++ b/packages/sdk/client/src/api.ts @@ -190,6 +190,37 @@ interface PendingQuery { readonly commandNotifications: HarnessNotification[] } +/** Collect an ordinary prompt's notifications from its inbox receipt through idle. */ +class PromptActivity { + private received = false + private readonly events: SessionEvent[] = [] + private readonly notifications: HarnessNotification[] = [] + + constructor(private readonly sessionId: string, private readonly messageId: string) {} + + /** Ignore pre-receipt traffic; validate root events before invoking the observer. */ + record(notification: HarnessNotification, onNotification?: RunOptions['onNotification']): boolean { + if (!this.received) { + if (notification.method !== 'session.event' || notification.params.sessionId !== this.sessionId + || !isInboxReceipt(notification.params.event, this.messageId)) return false + this.received = true + } + const event = notification.method === 'session.event' && notification.params.sessionId === this.sessionId + ? validatedSessionEvent(notification.params.event) : undefined + this.notifications.push(notification) + onNotification?.(notification) + if (event !== undefined) this.events.push(event) + return true + } + + result(): RunResult { + return { + sessionId: this.sessionId, finalResponse: finalResponse(this.events), + events: this.events, notifications: this.notifications, + } + } +} + /** * One SDK session: a stable id plus owned activity intervals. */ @@ -264,27 +295,18 @@ export class HarnessSession { yield outcome.result.execution return } - const events: SessionEvent[] = [] - const notifications: HarnessNotification[] = [] - let received = false + const activity = new PromptActivity(sessionId, outcome.result.messageId) while (!isClosed()) { const notification = await pending.subscription.next() - if (!received) { - if (notification.method !== 'session.event' || notification.params.sessionId !== sessionId - || !isInboxReceipt(notification.params.event, outcome.result.messageId)) continue - received = true - } - if (notification.method === 'session.event' && notification.params.sessionId === sessionId) { - events.push(validatedSessionEvent(notification.params.event)) - } - notifications.push(notification) + if (isClosed()) return + if (!activity.record(notification)) continue yield notification if (notification.method === 'session.status' && notification.params.sessionId === sessionId && notification.params.status === 'idle') break } if (isClosed()) return close() - yield { sessionId, finalResponse: finalResponse(events), events, notifications } + yield activity.result() } catch (error: unknown) { if (!isClosed()) throw error } finally { @@ -331,36 +353,14 @@ export class HarnessSession { await this.harness.start() const client = this.harness.client const contentBlocks = normalizeInput(input) - const events: SessionEvent[] = [] - const notifications: HarnessNotification[] = [] - const subscription = client.subscribeSessionTree(this.id) - const collect = (notification: HarnessNotification): void => { - if (notification.method === 'session.event' && notification.params.sessionId === this.id) { - // Wire boundary: the envelope feeds the typed RunResult, so a - // malformed runtime surfaces as a protocol error, not as type-invalid - // data (or a TypeError out of finalResponse). - const event = validatedSessionEvent(notification.params.event) - notifications.push(notification) - options?.onNotification?.(notification) - events.push(event) - return - } - notifications.push(notification) - options?.onNotification?.(notification) - } + let activity: PromptActivity try { const messageId = await client.prompt(this.id, contentBlocks) - let received = false + activity = new PromptActivity(this.id, messageId) while (true) { const notification = await subscription.next() - if (!received) { - if (notification.method !== 'session.event' - || notification.params.sessionId !== this.id - || !isInboxReceipt(notification.params.event, messageId)) continue - received = true - } - collect(notification) + if (!activity.record(notification, options?.onNotification)) continue if (notification.method === 'session.status' && notification.params.sessionId === this.id && notification.params.status === 'idle') break @@ -369,12 +369,7 @@ export class HarnessSession { subscription.close() } - return { - sessionId: this.id, - finalResponse: finalResponse(events), - events, - notifications, - } + return activity.result() } } diff --git a/packages/sdk/client/tests/query.spec.ts b/packages/sdk/client/tests/query.spec.ts index 38e0c198cf..dc36f66db7 100644 --- a/packages/sdk/client/tests/query.spec.ts +++ b/packages/sdk/client/tests/query.spec.ts @@ -208,33 +208,48 @@ it.each(['hold', '/compact'])('return releases a pending next for %s without can await harness.client.request('test/release') }) -it('ends a receiver closed during a pre-receipt read without yielding a RunResult', async () => { - const harness = harnessWith() - const subscribed = vi.spyOn(harness.client, 'subscribeSessionTree') - const session = harness.session('main') - await session.query('hold') - const created = subscribed.mock.results[0] - if (created?.type !== 'return') throw new Error('query did not create a subscription') - const next = created.value.next.bind(created.value) - const arrived = Promise.withResolvers() - const release = Promise.withResolvers() - onTestFinished(() => { release.resolve(undefined) }) - vi.spyOn(created.value, 'next').mockImplementationOnce(async () => { - const notification = await next() - arrived.resolve(undefined) - await release.promise - return notification - }) - const receiver = session.receiveResponse() - const receiving = receiver.next() - await arrived.promise - const returning = receiver.return?.() - release.resolve(undefined) - await expect(receiving).resolves.toEqual({ done: true, value: undefined }) - await expect(returning).resolves.toEqual({ done: true, value: undefined }) - expect(await harness.client.request('test/state')).toMatchObject({ waitingPrompts: 1 }) - await harness.client.request('test/release') -}) +it.each(['before receipt', 'receipt', 'after receipt'] as const)( + 'does not emit an acquired %s notification after the receiver closes', async (position) => { + const harness = harnessWith() + const subscribed = vi.spyOn(harness.client, 'subscribeSessionTree') + const session = harness.session('main') + await session.query('hold') + const created = subscribed.mock.results[0] + if (created?.type !== 'return') throw new Error('query did not create a subscription') + const next = created.value.next.bind(created.value) + const arrived = Promise.withResolvers() + const release = Promise.withResolvers() + onTestFinished(() => { release.resolve(undefined) }) + vi.spyOn(created.value, 'next').mockImplementation(async () => { + const notification = await next() + const event = notification.params.event as { type: string } | undefined + const matches = position === 'before receipt' + ? notification.method === 'session.event' && event?.type === 'assistant/message' + : position === 'receipt' + ? notification.method === 'session.event' && event?.type === 'agent/inbox/spliced' + : notification.method === 'session.status' && notification.params.status === 'running' + if (matches) { + arrived.resolve(undefined) + await release.promise + } + return notification + }) + const receiver = session.receiveResponse() + const receiving = collect(receiver) + await arrived.promise + const returning = receiver.return?.() + release.resolve(undefined) + const messages = await receiving + if (position === 'after receipt') { + expect(messages).toMatchObject([{ method: 'session.event', params: { event: { type: 'agent/inbox/spliced' } } }]) + } else { + expect(messages).toEqual([]) + } + await expect(returning).resolves.toEqual({ done: true, value: undefined }) + expect(await harness.client.request('test/state')).toMatchObject({ waitingPrompts: 1 }) + await harness.client.request('test/release') + }, +) it('closes prompt subscriptions before yielding the final RunResult', async () => { const harness = harnessWith() @@ -256,6 +271,34 @@ it('closes prompt subscriptions before yielding the final RunResult', async () = await receiver.return?.() }) +it('ends an already-queued read when the receiver closes while yielding idle', async () => { + const harness = harnessWith() + const subscribed = vi.spyOn(harness.client, 'subscribeSessionTree') + const session = harness.session('main') + await session.query('complete') + const created = subscribed.mock.results[0] + if (created?.type !== 'return') throw new Error('query did not create a subscription') + const next = created.value.next.bind(created.value) + const receiver = session.receiveResponse() + let returning: ReturnType> | undefined + const observerErrors: unknown[] = [] + vi.spyOn(created.value, 'next').mockImplementation(() => { + const pending = next() + void pending.then((notification) => { + if (notification.method === 'session.status' && notification.params.status === 'idle') { + queueMicrotask(() => { returning = receiver.return?.() }) + } + }, (error: unknown) => { observerErrors.push(error) }) + return pending + }) + // Five admitted notifications precede the final result; queue its read before idle is yielded. + const reads = await Promise.all(Array.from({ length: 6 }, () => receiver.next())) + expect(reads.at(-1)).toEqual({ done: true, value: undefined }) + expect(reads.filter(item => !item.done && 'finalResponse' in item.value)).toEqual([]) + await expect(returning).resolves.toEqual({ done: true, value: undefined }) + expect(observerErrors).toEqual([]) +}) + it('reports local request timeout from the receiver and leaves the runtime responsive', async () => { const harness = harnessWith({ QUERY_HOLD_COMMAND: '1' }, 50) const session = harness.session('main') diff --git a/packages/sdk/protocol/README.i18n.yaml b/packages/sdk/protocol/README.i18n.yaml index 5035bd7ce4..10553a536e 100644 --- a/packages/sdk/protocol/README.i18n.yaml +++ b/packages/sdk/protocol/README.i18n.yaml @@ -21,8 +21,8 @@ en: be53c71505c0d1b4 zh: 6210dee577715539 /deepseek-ai-dsh-sdk-protocol/use-this-package/the-sdk-methods: - en: 08dfe39d0df5a035 - zh: 079f9d60f8510642 + en: 7496fd093913fdfa + zh: 02667b3c02ba77f2 /deepseek-ai-dsh-sdk-protocol/use-this-package/native-user-questions: en: 65ea66231b17b6cf zh: 8e1c57d0c27f842f diff --git a/packages/sdk/protocol/README.md b/packages/sdk/protocol/README.md index 6bca4f737d..ea9859dadb 100644 --- a/packages/sdk/protocol/README.md +++ b/packages/sdk/protocol/README.md @@ -62,7 +62,7 @@ Both wire ends share one method set: client-to-server requests, opt-in server-to `session/query` accepts exactly `{ sessionId, text, delivery }`, where text is non-blank and delivery is `queue` or `steer`. The runtime replies with `{ kind: "prompt", messageId }` for an ordinary input, or `{ kind: "command", execution }` after a supported command settles. A prompt receipt is not a model result; command completion does not require an inbox receipt or idle notification. Recognition and the supported command set belong to the [SDK server](../server/README.md). -`session/command` accepts exactly `{ sessionId, line }` after initialization and delegates `/compact`, `/permission `, `/plan [off|message]`, or `/goal [objective|clear|edit objective|pause|resume]` to the native registry. A non-blank preset is required; bare `/permission` is rejected. It returns `CommandExecution` after the handler settles, including an unknown preset or other business failure as `result.kind: "error"`. Unavailable or unsupported commands and malformed requests reject with a JSON-RPC error. There is no prompt fallback or attachment input. Command lifecycle notifications precede the response; no inbox receipt or ordinary turn is required. `session/prompt` remains unchanged. +`session/command` accepts exactly `{ sessionId, line }` after initialization and delegates the [supported command names](../server/README.md#command-input) to the native registry. A non-blank preset is required; bare `/permission` is rejected. It returns `CommandExecution` after the handler settles, including an unknown preset or other business failure as `result.kind: "error"`. Unavailable or unsupported commands and malformed requests reject with a JSON-RPC error. There is no prompt fallback or attachment input. Command lifecycle notifications precede the response; no inbox receipt or ordinary turn is required. `session/prompt` remains unchanged. `session/resume` accepts a non-empty persisted `{ sessionId }` and returns `{}` after restoring that exact session. It requires initialization and the original persistence store, sends no new input, and rejects missing, unreadable, incompatible, or occupied sessions without creating a replacement. Historical events remain in the stored log; notifications report newly committed events. diff --git a/packages/sdk/protocol/README.zh.md b/packages/sdk/protocol/README.zh.md index 96a877cd51..44a51f3d76 100644 --- a/packages/sdk/protocol/README.zh.md +++ b/packages/sdk/protocol/README.zh.md @@ -62,7 +62,7 @@ kind: "package-library" `session/query` 接受且仅接受 `{ sessionId, text, delivery }`,text 不能全为空白,delivery 为 `queue` 或 `steer`。普通输入返回 `{ kind: "prompt", messageId }`,受支持命令结束后返回 `{ kind: "command", execution }`。Prompt 回执不是模型结果,命令完成也不要求 inbox 回执或 idle 通知。识别规则及受支持命令由 [SDK 服务端](../server/README.zh.md)定义。 -`session/command` 在初始化后接受且仅接受 `{ sessionId, line }`,将 `/compact`、`/permission `、`/plan [off|message]` 或 `/goal [objective|clear|edit objective|pause|resume]` 委托给原生注册器。preset 必须非空,裸 `/permission` 会被拒绝。Handler 结束后返回 `CommandExecution`,未知 preset 或其他业务失败以 `result.kind: "error"` 返回。命令不可用、不受支持或请求格式错误时返回 JSON-RPC 错误。不回退为普通 prompt,也不接受附件。命令生命周期通知在响应前到达,不要求 inbox 回执或普通 turn。`session/prompt` 保持不变。 +`session/command` 在初始化后接受且仅接受 `{ sessionId, line }`,将[支持的命令名](../server/README.zh.md#command-input)委托给原生注册器。preset 必须非空,裸 `/permission` 会被拒绝。Handler 结束后返回 `CommandExecution`,未知 preset 或其他业务失败以 `result.kind: "error"` 返回。命令不可用、不受支持或请求格式错误时返回 JSON-RPC 错误。不回退为普通 prompt,也不接受附件。命令生命周期通知在响应前到达,不要求 inbox 回执或普通 turn。`session/prompt` 保持不变。 `session/resume` 接受非空的持久化 `{ sessionId }`,恢复该会话后返回 `{}`。它要求先完成初始化并使用原持久化存储,不发送新输入;会话缺失、不可读、不兼容或被占用时,请求失败且不创建替代会话。历史事件保留在存储日志中,通知只报告新提交的事件。 diff --git a/packages/sdk/server/README.i18n.yaml b/packages/sdk/server/README.i18n.yaml index 7a5fe8781a..b397f6fab4 100644 --- a/packages/sdk/server/README.i18n.yaml +++ b/packages/sdk/server/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-sdk-jsonrpc-server/use-this-package: - en: 195506cccf805d45 - zh: f8c03003567028f4 + en: d8b2dbbba9fd4ba4 + zh: a50fbb7e2dc73043 /deepseek-ai-dsh-sdk-jsonrpc-server/use-this-package/wiring: en: 47a33e9397e36e15 zh: e6d173820f9ea6e5 diff --git a/packages/sdk/server/README.md b/packages/sdk/server/README.md index 9f166b1a23..0ee4f737b1 100644 --- a/packages/sdk/server/README.md +++ b/packages/sdk/server/README.md @@ -33,9 +33,11 @@ Set `initialize.approvalRequests` to `true` to send internal approvals as server The plugin publishes the `sdkSessionActivation` readiness marker once its reminder activation handler is installed; an SDK composition injects this marker into Schedule instead of the Web Session controller. Scheduled delivery uses this same SDK Session owner. Due startup reminders wait for successful SDK initialization, then resume with its provider, model, reasoning effort, and output limit. The client can resume a reminder-restored Session once; concurrent reminder and client restoration share one handle. Shutdown releases reminders waiting for initialization and drains accepted restorations. + + `session/query` is the unified text entry. Ordinary text retains its original content and uses the selected queue/steer operation. The full SDK recognizes bare `/compact`, `/permission ` with a non-blank preset, `/plan [off|message]`, and `/goal [objective|clear|edit objective|pause|resume]`, including Desktop aliases bound to their current definitions. Bare `/permission` or `/权限`, `/compact extra`, and all other input including `/model` remain ordinary prompts. A missing command also preserves the prompt path; a selected handler's failure never falls back. An unknown preset is a native command error. Inputs are text-only; `session/prompt` retains its content-block API. -`session/command` accepts `{ sessionId, line }` for `/compact`, `/permission `, `/plan [off|message]`, or `/goal [objective|clear|edit objective|pause|resume]` and rejects bare `/permission`. The full sdk profile composes these native handlers; profiles without the requested command reject it. The server resolves its own live Agent and calls `commands.execute()` without queueing a prompt or waiting for idle. Compaction owns its auxiliary model request, history replacement and checkpoint, and refuses a busy Agent. Permission changes retain their native state and notification behavior. Shutdown cancels pending commands and drains SDK command waiters before releasing Agents. Timeout is not rollback; clients must not retry automatically. See the [protocol](../protocol/README.md) for results and errors. +`session/command` accepts `{ sessionId, line }` for the supported command names above, requires a non-blank permission preset, and lets native handlers validate other arguments. The full sdk profile composes these native handlers; profiles without the requested command reject it. The server resolves its own live Agent and calls `commands.execute()` without queueing a prompt or waiting for idle. Compaction owns its auxiliary model request, history replacement and checkpoint, and refuses a busy Agent. Permission changes retain their native state and notification behavior. Shutdown cancels pending commands and drains SDK command waiters before releasing Agents. Timeout is not rollback; clients must not retry automatically. See the [protocol](../protocol/README.md) for results and errors. Permission commands change only the receiving Session. Later queries and restored Sessions retain its persisted permission facts; other Sessions keep their own state and new Sessions use the configured defaults. Neither the caller's launch configuration nor saved user defaults are rewritten. Model selection is not an SDK slash command; its resume override remains the separate behavior described above. diff --git a/packages/sdk/server/README.zh.md b/packages/sdk/server/README.zh.md index 4bda05e91c..29f1c77ea4 100644 --- a/packages/sdk/server/README.zh.md +++ b/packages/sdk/server/README.zh.md @@ -33,9 +33,11 @@ kind: "package-reference" 插件在注册提醒激活处理方后发布 `sdkSessionActivation` 就绪标记;SDK 组合让 Schedule 依赖此标记,而非 Web Session controller。定时提醒也通过同一个 SDK Session 所有者投递。启动时已到期的提醒等待 SDK 成功初始化,再使用初始化指定的提供方、模型、推理强度和输出上限恢复会话。客户端可以首次接管提醒恢复的 Session;并发的提醒与客户端恢复共用一个 handle。关闭时释放等待初始化的提醒,并等待已经接纳的恢复完成。 + + `session/query` 是统一文本入口。普通文本保留原文,使用选定的 queue/steer 操作。完整 SDK 识别裸 `/compact`、带非空 preset 的 `/permission `、`/plan [off|message]` 和 `/goal [objective|clear|edit objective|pause|resume]`,包括绑定到当前定义的 Desktop 别名。裸 `/permission` 或 `/权限`、`/compact extra` 以及 `/model` 等其余输入仍是普通提示词。命令缺失时也保留提示词路径;选中的 handler 失败后绝不回退。未知 preset 是原生命令错误。此入口仅接受文本,`session/prompt` 保留内容块 API。 -`session/command` 接受用于 `/compact`、`/permission `、`/plan [off|message]` 或 `/goal [objective|clear|edit objective|pause|resume]` 的 `{ sessionId, line }`,并拒绝裸 `/permission`。完整 sdk profile 组成这些原生 handler;缺少所请求命令的 profile 会拒绝调用。服务端解析自己拥有的存活 Agent,并调用 `commands.execute()`,不排入普通 prompt,也不等待 idle。压缩负责其辅助模型请求、历史替换和 checkpoint,并拒绝忙碌 Agent。权限变更保留原生状态和通知行为。Shutdown 取消待决命令,在释放 Agent 前排空 SDK 命令等待者。超时不等于回滚,客户端不能自动重试。结果和错误见[协议](../protocol/README.zh.md)。 +`session/command` 接受上述命令名的 `{ sessionId, line }`,要求 permission preset 非空,其余参数由原生 handler 校验。完整 sdk profile 组成这些原生 handler;缺少所请求命令的 profile 会拒绝调用。服务端解析自己拥有的存活 Agent,并调用 `commands.execute()`,不排入普通 prompt,也不等待 idle。压缩负责其辅助模型请求、历史替换和 checkpoint,并拒绝忙碌 Agent。权限变更保留原生状态和通知行为。Shutdown 取消待决命令,在释放 Agent 前排空 SDK 命令等待者。超时不等于回滚,客户端不能自动重试。结果和错误见[协议](../protocol/README.zh.md)。 权限命令只修改接收命令的 Session。后续 query 和恢复的 Session 保留其持久化权限事实;其他 Session 保留各自状态,新 Session 使用配置的默认值。调用方的启动配置和已保存的用户默认值均不会被重写。模型选择不是 SDK slash 命令;恢复时的模型覆盖仍按上文描述的独立行为执行。 diff --git a/packages/sdk/server/tests/permission-parity.spec.ts b/packages/sdk/server/tests/permission-parity.spec.ts index 2fe5b35e6c..e9ef908409 100644 --- a/packages/sdk/server/tests/permission-parity.spec.ts +++ b/packages/sdk/server/tests/permission-parity.spec.ts @@ -146,6 +146,8 @@ async function switchAndContinue(path: 'sdk' | 'native', cwd: string) { expect(initialConfig).toEqual({ cwd, provider: 'permission-parity', model: 'permission-model', maxTokens: 321 }) return { requests: model.requests, + approvalInputs: events.flatMap(event => event.type === 'user/message' && event.data.source.kind === 'user-approval' + ? [event.data.content] : []), defaults, permissionEvents: events.flatMap((event) => { if (event.type === 'permission/preset' || event.type === 'sandbox/mode' || event.type === 'approval/policy') { @@ -176,9 +178,9 @@ it('preserves native permission events, unchanged defaults, and the next model r const content = sdk.requests[1]?.messages.flatMap(message => message.content) ?? [] const texts = content.flatMap(block => block.type === 'text' ? [block.text] : []) expect(texts).toContain('PERMISSION_READY') - expect(texts).toContain('The approval policy changed from "never" to "ask" (changed by the user).') + expect(sdk.approvalInputs).toHaveLength(1) + expect(content).toEqual(expect.arrayContaining(sdk.approvalInputs.flat())) expect(texts.some(text => text.includes('Current DSH file policy: read-only.'))).toBe(true) - expect(texts.some(text => text.includes('Approval policy: ask.'))).toBe(true) expect(texts).not.toContain('/permission read-only') expect(texts).not.toContain('preset read-only') } finally { diff --git a/python/sdk/README.i18n.yaml b/python/sdk/README.i18n.yaml index ec94bd5f66..fbfffcb7ec 100644 --- a/python/sdk/README.i18n.yaml +++ b/python/sdk/README.i18n.yaml @@ -12,8 +12,8 @@ en: c4c30913fe2642ef zh: 0ed42bca37fba60b /deepseek-harness-python-sdk/query-and-response: - en: c9e73f3b9934032f - zh: bf44fb5d471dbe75 + en: 7cc5feefefd05142 + zh: ea346bf354ec266d /deepseek-harness-python-sdk/client-approvals: en: fc69be1beb8abff1 zh: 91b56781931a6f68 diff --git a/python/sdk/README.md b/python/sdk/README.md index fb7c480e12..ea41fe307f 100644 --- a/python/sdk/README.md +++ b/python/sdk/README.md @@ -61,13 +61,13 @@ with DeepSeekHarness(dsh_home="/absolute/path/to/saved-home") as harness: ## Query and response -Call `session.query(text)` to send text, then iterate `session.receive_response()` to observe its result. `query()` returns `None` after sending, without waiting for runtime routing or execution. The full SDK intercepts bare `/compact`, `/permission ` with a non-blank preset, `/plan [off|message]`, and `/goal [objective|clear|edit objective|pause|resume]`, including their registered Desktop aliases. Bare `/permission` or `/权限`, `/model` and other input remain ordinary prompts. This API accepts text strings only. `delivery="queue"` is the default; `delivery="steer"` directs an ordinary prompt to the next step. Command timing remains owned by the command. +Call `session.query(text)` to send text, then iterate `session.receive_response()` to observe its result. `query()` returns `None` after sending, without waiting for runtime routing or execution. The runtime recognizes the [supported command forms and Desktop aliases](../../packages/sdk/server/README.md#command-input); unrecognized input remains ordinary text. This API accepts text strings only. `delivery="queue"` is the default; `delivery="steer"` directs an ordinary prompt to the next step. Command timing remains owned by the command. `receive_response()` claims the next query in send order. It first waits for the routing response, then yields `Notification` objects followed by one terminal value: `RunResult` for the prompt's inbox-receipt-to-idle interval, or `CommandExecution` after the command settles. Notifications can accumulate before receiving begins. Separate receivers can observe their queries concurrently, and another `query()` can be sent while a receiver waits. Close an unfinished iterator to release only its local observation; this does not cancel or roll back runtime work. Resources are released before yielding the terminal value. `CommandExecution` contains `command_id` and a `result` discriminated by `kind`. Success may include `text` and `source_event_seq`; an error includes `text`. Handler errors return `kind="error"`; routing, protocol and transport failures raise during receiving. Sending failures raise from `query()`. `request_timeout_seconds` bounds waiting for the routing response, not later prompt activity; an already-arrived response remains receivable after that deadline. -The full `sdk` profile provides these commands; the default `sdk-minimal` profile does not provide command execution. `/compact` can make an auxiliary model request and completes without requiring a normal turn or idle notification; empty history can succeed without a summary. `run()` retains ordinary prompt semantics for slash text. The lower-level blocking `HarnessClient.session_command(session_id, line)` strictly accepts `/compact`, `/permission `, `/plan [off|message]`, or `/goal [objective|clear|edit objective|pause|resume]` and rejects bare `/permission`. See the [compaction example](examples/README.md#compact-a-full-sdk-session). +The full `sdk` profile provides these commands; the default `sdk-minimal` profile does not provide command execution. `/compact` can make an auxiliary model request and completes without requiring a normal turn or idle notification; empty history can succeed without a summary. `run()` retains ordinary prompt semantics for slash text. The lower-level blocking `HarnessClient.session_command(session_id, line)` executes supported command names without prompt fallback; it requires a non-blank permission preset. See the [compaction example](examples/README.md#compact-a-full-sdk-session). `/permission ` delegates to the native permission handler. An unknown preset returns `CommandExecution.result.kind="error"` without changing permission state. A successful change belongs to that Session: later queries and restoration retain it, other Sessions retain their own state, and new Sessions use configured defaults. `DeepSeekHarnessConfig` remains startup input, not a shared live state mirror; the command does not rewrite it or saved user defaults. A running approval-policy change can inject a notice and cause another model step. See the [permission example](examples/README.md#change-session-permissions). diff --git a/python/sdk/README.zh.md b/python/sdk/README.zh.md index 120a10c697..c040fe75a2 100644 --- a/python/sdk/README.zh.md +++ b/python/sdk/README.zh.md @@ -61,13 +61,13 @@ with DeepSeekHarness(dsh_home="/absolute/path/to/saved-home") as harness: ## 查询与接收 -调用 `session.query(text)` 发送文本,再迭代 `session.receive_response()` 观察结果。`query()` 发送后即返回 `None`,不等待运行时路由或执行。完整 SDK 接管裸 `/compact`、带非空 preset 的 `/permission `、`/plan [off|message]` 和 `/goal [objective|clear|edit objective|pause|resume]`,包括绑定到其定义的 Desktop 别名。裸 `/permission` 或 `/权限`、`/model` 及其余输入仍是普通提示词。此 API 只接受文本字符串。`delivery="queue"` 是默认值;`delivery="steer"` 将普通提示词交给下一个 step。命令执行时机仍由命令决定。 +调用 `session.query(text)` 发送文本,再迭代 `session.receive_response()` 观察结果。`query()` 发送后即返回 `None`,不等待运行时路由或执行。运行时识别[支持的命令形式与 Desktop 别名](../../packages/sdk/server/README.zh.md#command-input);未识别的输入保持普通文本。此 API 只接受文本字符串。`delivery="queue"` 是默认值;`delivery="steer"` 将普通提示词交给下一个 step。命令执行时机仍由命令决定。 `receive_response()` 按发送顺序领取下一次 query。它先等待路由响应,再逐个产出 `Notification`,最后产出一个终态值:普通提示词从收件箱回执到 idle 区间的 `RunResult`,或命令处理结束后的 `CommandExecution`。通知可以在开始接收前积累。不同接收器可以并行观察各自的 query,也可以在一个接收器等待期间发送另一个 `query()`。关闭未完成的迭代器只释放它自己的本地观察资源,不取消或回滚运行时操作。终态值产出前会释放资源。 `CommandExecution` 包含 `command_id` 和以 `kind` 区分的 `result`。成功结果可以包含 `text` 和 `source_event_seq`;错误结果包含 `text`。处理器错误返回 `kind="error"`;路由、协议和传输失败在接收时抛出。发送失败从 `query()` 抛出。`request_timeout_seconds` 限制路由响应的等待时间,不限制后续提示词活动;已经到达的响应在期限过后仍可接收。 -完整 `sdk` profile 提供这些命令;默认 `sdk-minimal` profile 不提供命令执行能力。`/compact` 可能发起辅助模型请求,完成时不要求普通轮次或 idle 通知;空历史可以不生成摘要而成功返回。`run()` 对 slash 文本保留普通提示词语义。底层阻塞式 `HarnessClient.session_command(session_id, line)` 严格接受 `/compact`、`/permission `、`/plan [off|message]` 或 `/goal [objective|clear|edit objective|pause|resume]`,并拒绝裸 `/permission`。另见[压缩示例](examples/README.zh.md#compact-a-full-sdk-session)。 +完整 `sdk` profile 提供这些命令;默认 `sdk-minimal` profile 不提供命令执行能力。`/compact` 可能发起辅助模型请求,完成时不要求普通轮次或 idle 通知;空历史可以不生成摘要而成功返回。`run()` 对 slash 文本保留普通提示词语义。底层阻塞式 `HarnessClient.session_command(session_id, line)` 执行支持的命令名,不回退为提示词;permission preset 必须非空。另见[压缩示例](examples/README.zh.md#compact-a-full-sdk-session)。 `/permission ` 委托给原生权限 handler。未知 preset 返回 `CommandExecution.result.kind="error"`,不改变权限状态。成功的变更属于该 Session:后续 query 和恢复保留该状态,其他 Session 保留各自状态,新 Session 使用配置的默认值。`DeepSeekHarnessConfig` 仍是启动输入,不是共享的实时状态镜像;命令不重写它或已保存的用户默认值。运行中改变审批策略可能注入通知并触发额外模型 step。另见[权限示例](examples/README.zh.md#change-session-permissions)。 diff --git a/python/sdk/examples/README.i18n.yaml b/python/sdk/examples/README.i18n.yaml index 45cc6d2c04..f97e3f117c 100644 --- a/python/sdk/examples/README.i18n.yaml +++ b/python/sdk/examples/README.i18n.yaml @@ -15,8 +15,8 @@ en: 3471642d8444d0e9 zh: 7831f28295a6e68a /python-sdk-example/review-a-plan: - en: bd0da5138a033eba - zh: f291c3ae8967e936 + en: 46758c022608af0f + zh: 09b5a59176c11176 /python-sdk-example/run-a-goal: en: ae73e9fd9d402047 zh: f9694bc6a6e12929 diff --git a/python/sdk/examples/README.md b/python/sdk/examples/README.md index 6f43ac535e..1e3c7ad402 100644 --- a/python/sdk/examples/README.md +++ b/python/sdk/examples/README.md @@ -68,7 +68,7 @@ python python/sdk/examples/plan.py \ "Plan the requested change and present it for review." ``` -The native plan is carried in `exit_plan_mode`, not written to a workspace document. Permissions remain workspace-write. Dismissal leaves plan mode active and ends the current turn. The example waits through `session/wait`; its question callback is independent of the protocol reader. +The native plan is carried in `exit_plan_mode`, not written to a workspace document. Permissions remain workspace-write. Dismissal leaves plan mode active and ends the current turn. The example waits through `session/wait` and rejects truncated or failed root turns; its question callback is independent of the protocol reader. ## Run a goal diff --git a/python/sdk/examples/README.zh.md b/python/sdk/examples/README.zh.md index 4ca66cac69..8b05b4806d 100644 --- a/python/sdk/examples/README.zh.md +++ b/python/sdk/examples/README.zh.md @@ -68,7 +68,7 @@ python python/sdk/examples/plan.py \ "Plan the requested change and present it for review." ``` -原生计划通过 `exit_plan_mode` 携带,而不是写入工作区文档。权限仍为 workspace-write。关闭审查会保留计划模式并结束当前轮次。示例通过 `session/wait` 等待,问答回调独立于协议读取线程。 +原生计划通过 `exit_plan_mode` 携带,而不是写入工作区文档。权限仍为 workspace-write。关闭审查会保留计划模式并结束当前轮次。示例通过 `session/wait` 等待,并报告根轮次截断或失败;问答回调独立于协议读取线程。 ## 运行目标 diff --git a/python/sdk/examples/plan.py b/python/sdk/examples/plan.py index eacc8f8170..6368c4d861 100644 --- a/python/sdk/examples/plan.py +++ b/python/sdk/examples/plan.py @@ -40,14 +40,19 @@ def review(request: UserQuestionRequest, cancelled: threading.Event) -> dict: return {"cancelled": True} -def display(notification: Notification) -> None: - """Print completed assistant text while the Session runs.""" +def display(notification: Notification, session_id: str) -> None: + """Print assistant text and report an unsuccessful root Session turn.""" if notification.method == "session.event": event = notification.payload.get("event") if isinstance(event, dict) and event.get("type") == "assistant/message": text = final_response([event]) if text: print(text) + elif (isinstance(event, dict) and event.get("type") == "turn/end" + and notification.payload.get("sessionId") == session_id): + reason = event["data"]["reason"] + if reason["kind"] != "completed": + raise RuntimeError(f"Agent turn ended: {reason}") def main() -> None: @@ -75,7 +80,7 @@ def main() -> None: raise RuntimeError(message.result.text) harness.client.request( "session/wait", {"sessionId": session.id}, response_model=WaitResult, - on_notification=display, notification_subscription=notifications, + on_notification=lambda notice: display(notice, session.id), notification_subscription=notifications, ) diff --git a/python/sdk/src/deepseek_harness/api.py b/python/sdk/src/deepseek_harness/api.py index ba61b14a24..fa2eaeb368 100644 --- a/python/sdk/src/deepseek_harness/api.py +++ b/python/sdk/src/deepseek_harness/api.py @@ -53,6 +53,37 @@ class RunResult: notifications: list[Notification] +class _PromptActivity: + """Collect an ordinary prompt's notifications from its inbox receipt through idle.""" + + def __init__(self, session_id: str, message_id: str) -> None: + self.session_id = session_id + self.message_id = message_id + self.received = False + self.events: list[JsonObject] = [] + self.notifications: list[Notification] = [] + + def record(self, notification: Notification, on_notification: Callable[[Notification], None] | None = None) -> bool: + if not self.received: + if not _is_inbox_receipt(notification, self.session_id, self.message_id): + return False + self.received = True + self.notifications.append(notification) + if on_notification is not None: + on_notification(notification) + if notification.method == "session.event" and notification.payload.get("sessionId") == self.session_id: + event = notification.payload.get("event") + if isinstance(event, dict): + self.events.append(event) + return True + + def result(self) -> RunResult: + return RunResult( + session_id=self.session_id, final_response=final_response(self.events), + finish_reason=finish_reason(self.events), events=self.events, notifications=self.notifications, + ) + + class _QueryResponseIterator(Iterator[Notification | RunResult | CommandExecution]): def __init__( self, client: HarnessClient, session_id: str, request: _PendingRequest, @@ -66,10 +97,7 @@ class _QueryResponseIterator(Iterator[Notification | RunResult | CommandExecutio self._closed = False self._released = False self._routed = False - self._received = False - self._message_id = "" - self._notifications: list[Notification] = [] - self._events: list[JsonObject] = [] + self._activity: _PromptActivity | None = None self._buffer: deque[Notification] = deque() self._terminal: RunResult | CommandExecution | None = None @@ -119,7 +147,7 @@ class _QueryResponseIterator(Iterator[Notification | RunResult | CommandExecutio if _is_command_done(notification, self._session_id, self._terminal.command_id): break elif response.get("kind") == "prompt" and isinstance(response.get("messageId"), str) and response["messageId"]: - self._message_id = response["messageId"] + self._activity = _PromptActivity(self._session_id, response["messageId"]) else: raise SdkProtocolError("session/query requires a prompt messageId or command execution result") self._ensure_open() @@ -131,25 +159,16 @@ class _QueryResponseIterator(Iterator[Notification | RunResult | CommandExecutio self._release() self._closed = True return terminal + assert self._activity is not None while True: notification = self._subscription.next() self._ensure_open() - if not self._received: - if not _is_inbox_receipt(notification, self._session_id, self._message_id): - continue - self._received = True - self._notifications.append(notification) - if notification.method == "session.event" and notification.payload.get("sessionId") == self._session_id: - event = notification.payload.get("event") - if isinstance(event, dict): - self._events.append(event) + if not self._activity.record(notification): + continue if (notification.method == "session.status" and notification.payload.get("sessionId") == self._session_id and notification.payload.get("status") == "idle"): - self._terminal = RunResult( - session_id=self._session_id, final_response=final_response(self._events), - finish_reason=finish_reason(self._events), events=self._events, notifications=self._notifications, - ) + self._terminal = self._activity.result() self._release() return notification except BaseException as error: @@ -313,21 +332,6 @@ class Session: on_notification: Callable[[Notification], None] | None = None, ) -> RunResult: content_blocks = normalize_input(input) - notifications: list[Notification] = [] - events: list[JsonObject] = [] - - def collect(notification: Notification) -> None: - notifications.append(notification) - if on_notification is not None: - on_notification(notification) - if ( - notification.method == "session.event" - and notification.payload.get("sessionId") == self.id - ): - event = notification.payload.get("event") - if isinstance(event, dict): - events.append(event) - with self.harness.client.subscribe_session_notifications(self.id) as subscription: message_id = self.harness.client.session_prompt( self.id, @@ -335,14 +339,11 @@ class Session: notification_subscription=subscription, ) - received = False + activity = _PromptActivity(self.id, message_id) while True: notification = subscription.next() - if not received: - if not _is_inbox_receipt(notification, self.id, message_id): - continue - received = True - collect(notification) + if not activity.record(notification, on_notification): + continue if ( notification.method == "session.status" and notification.payload.get("sessionId") == self.id @@ -350,13 +351,7 @@ class Session: ): break - return RunResult( - session_id=self.id, - final_response=final_response(events), - finish_reason=finish_reason(events), - events=events, - notifications=notifications, - ) + return activity.result() def _is_inbox_receipt(notification: Notification, session_id: str, message_id: str) -> bool: diff --git a/python/sdk/tests/test_commands_runtime.py b/python/sdk/tests/test_commands_runtime.py index ad24494dd0..a2cbc2ab25 100644 --- a/python/sdk/tests/test_commands_runtime.py +++ b/python/sdk/tests/test_commands_runtime.py @@ -493,7 +493,7 @@ def test_unimplemented_slash_remains_plain_text_without_recording_feedback(tmp_p _PERMISSION_FACTS = {"permission/preset", "sandbox/mode", "approval/policy"} -_ASK_POLICY = "Approval policy: ask." +_ASK_POLICY = "Approval prompts are enabled in this session" _NEVER_POLICY = "Approval prompts are disabled in this session" @@ -562,8 +562,6 @@ def test_permission_changes_only_the_selected_session_and_retains_prompt_semanti _assert_request_permission(requests[index], "workspace-write", "ask") _assert_request_permission(requests[2], "read-only", "ask") _assert_request_permission(requests[3], "danger-full-access", "never") - assert 'The approval policy changed from "ask" to "never" (changed by the user).' in _request_texts(requests[3]) - assert 'The approval policy changed from "never" to "ask" (changed by the user).' in _request_texts(requests[4]) for index in [2, 3, 4]: assert requests[index]["system"] == requests[0]["system"] assert requests[index]["tools"] == requests[0]["tools"] @@ -571,6 +569,11 @@ def test_permission_changes_only_the_selected_session_and_retains_prompt_semanti assert "/permission" in _request_texts(requests[8]) events = _read_events(runtime.logs) + notices = [event["data"]["content"] for event in _of_type(events, "user/message") + if event["data"]["source"]["kind"] == "user-approval"] + assert len(notices) == 2 + for content, request in zip(notices, requests[3:5]): + assert all(text in _request_texts(request) for text in _text_blocks(content)) executions = [readonly, invalid, repeated, full, restored] assert len(_of_type(events, "command/run")) == len(executions) for execution in executions: @@ -613,12 +616,14 @@ def test_running_permission_change_keeps_the_native_extra_step_and_notifications _assert_request_permission(requests[1], "danger-full-access", "never") assert requests[0]["system"] == requests[1]["system"] assert requests[0]["tools"] == requests[1]["tools"] - notice = 'The approval policy changed from "ask" to "never" (changed by the user).' - assert notice not in _request_texts(requests[0]) - assert notice in _request_texts(requests[1]) assert runtime.harness.config.env["DSH_PERMISSION_MODE"] == "workspace-write" events = _read_events(runtime.logs) _command_pair(events, execution, "permission") + [notice] = [event["data"]["content"] for event in _of_type(events, "user/message") + if event["data"]["source"]["kind"] == "user-approval"] + for text in _text_blocks(notice): + assert text not in _request_texts(requests[0]) + assert text in _request_texts(requests[1]) assert [event["data"] for event in _of_type(events, "step/start")] == [ {"turn": 1, "step": 1}, {"turn": 1, "step": 2}, ] diff --git a/python/sdk/tests/test_goal_runtime.py b/python/sdk/tests/test_goal_runtime.py index 7d1644adee..fe9f9602e5 100644 --- a/python/sdk/tests/test_goal_runtime.py +++ b/python/sdk/tests/test_goal_runtime.py @@ -13,7 +13,7 @@ from test_commands_runtime import ( _command_pair, _of_type, _permission_facts, _read_events, _receive_result, _request_texts, _runtime, _text_blocks, ) -from test_plan_runtime import _WaitResult, _wait_activity +from test_plan_runtime import _wait_activity def _command(runtime: _Runtime, session: Session, text: str, *, kind: str = "success") -> CommandExecution: @@ -73,11 +73,13 @@ def test_goal_continues_rounds_and_finishes_wrapup_after_completion_event(tmp_pa completed = _until(events, "goal/change", "complete") assert completed["goal"]["phase"] == "complete" and completed["roundsStarted"] == 2 assert final.entered.wait(_WAIT_SECONDS) - waiting = runtime.workers.submit(runtime.harness.client.request, "session/wait", - {"sessionId": session.id}, response_model=_WaitResult) - assert not waiting.done() and not final.release.is_set() + waiting = runtime.harness.client._begin_request("session/wait", {"sessionId": session.id}) + # A later RPC confirms admission on the same pipe; keep the wait's + # response queued so worker scheduling cannot hide an early return. + runtime.call(runtime.harness.client.get_working_directory, session.id) + assert waiting.waiter.empty() and not final.release.is_set() final.release.set() - waiting.result(timeout=_WAIT_SECONDS) + assert runtime.call(runtime.harness.client._wait_request, waiting) == {} assert _until(events, "turn/end")["turn"] == 2 requests = runtime.model.captured() assert len(requests) == 4 diff --git a/python/sdk/tests/test_plan_example.py b/python/sdk/tests/test_plan_example.py new file mode 100644 index 0000000000..6d21060ead --- /dev/null +++ b/python/sdk/tests/test_plan_example.py @@ -0,0 +1,85 @@ +"""Run the plan example against the SDK profile with controlled model replies.""" + +from __future__ import annotations + +import json +import runpy +import sys +from pathlib import Path + +import pytest + +from deepseek_harness import DeepSeekHarness, Notification +import test_commands_runtime as runtime_helpers +from test_commands_runtime import _Completion, _ModelServer, _dsh_bin, _of_type, _read_events + + +_EXAMPLE = Path(__file__).resolve().parents[1] / "examples/plan.py" +_PLAN = "# Small change\n\n- Inspect the code.\n- Implement after approval." + + +@pytest.mark.parametrize("outcome", ["max-tokens", "dismiss"]) +def test_plan_example_reports_truncation_and_accepts_explicit_dismissal( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], outcome: str, +) -> None: + example = runpy.run_path(str(_EXAMPLE)) + workspace = tmp_path / "workspace" + workspace.mkdir() + logs = tmp_path / "sessions" + patch = tmp_path / "persistence.yml" + patch.write_text( + "- id: session-persistence-jsonl\n config:\n root: " + json.dumps(str(logs)) + + "\n compression: none\n", encoding="utf-8", + ) + completion = (_Completion("PARTIAL_PLAN") if outcome == "max-tokens" + else _Completion("", tool_call=("exit_plan_mode", {"plan": _PLAN}))) + if outcome == "max-tokens": + response = runtime_helpers._messages_response + + def truncated_response(*args: object) -> bytes: + return response(*args).replace(b'"stop_reason": "end_turn"', b'"stop_reason": "max_tokens"') + + monkeypatch.setattr(runtime_helpers, "_messages_response", truncated_response) + choices: list[str] = [] + + def dismiss(prompt: str) -> str: + choices.append(prompt) + return "d" + + monkeypatch.setattr("builtins.input", dismiss) + monkeypatch.setenv("DSH_TELEMETRY_DISABLED", "1") + monkeypatch.setenv("DSH_TOOLS_MODE", "native") + monkeypatch.setattr(sys, "argv", [str(_EXAMPLE), "--dsh-home", str(tmp_path / "home"), + "--workspace", str(workspace), "Plan a small change."]) + with _ModelServer([completion]) as model: + def configured_harness(**options: object) -> DeepSeekHarness: + return DeepSeekHarness( + **options, dsh_bin=_dsh_bin(), patches=(str(patch),), + base_url=model.url, api_key="keyless-plan-example", + initialize_timeout_seconds=30, request_timeout_seconds=20, shutdown_timeout_seconds=20, + ) + + monkeypatch.setitem(example["main"].__globals__, "DeepSeekHarness", configured_harness) + if outcome == "max-tokens": + with pytest.raises(RuntimeError, match="max-tokens"): + example["main"]() + else: + example["main"]() + assert len(model.captured()) == 1 + [log] = logs.rglob("session*.jsonl") + session_id = json.loads(log.read_text(encoding="utf-8").splitlines()[0])["id"] + events = _read_events(logs, session_id=session_id) + assert [event["data"]["reason"]["kind"] for event in _of_type(events, "turn/end")] == [ + "max-tokens" if outcome == "max-tokens" else "completed", + ] + assert [event["data"] for event in _of_type(events, "plan/mode")] == [{"active": True}] + assert len(choices) == (0 if outcome == "max-tokens" else 1) + assert ("PARTIAL_PLAN" if outcome == "max-tokens" else _PLAN) in capsys.readouterr().out + + +def test_plan_display_rejects_root_abort_without_rejecting_descendant_abort() -> None: + display = runpy.run_path(str(_EXAMPLE))["display"] + event = {"type": "turn/end", "data": {"turn": 1, "reason": {"kind": "aborted"}}} + display(Notification(method="session.event", payload={"sessionId": "child", "event": event}), "root") + with pytest.raises(RuntimeError, match="aborted"): + display(Notification(method="session.event", payload={"sessionId": "root", "event": event}), "root") diff --git a/snapshots/sdk/goal-rounds/session.v4.jsonl b/snapshots/sdk/goal-rounds/session.v4.jsonl index e035053d1b..80359a7083 100644 --- a/snapshots/sdk/goal-rounds/session.v4.jsonl +++ b/snapshots/sdk/goal-rounds/session.v4.jsonl @@ -8,7 +8,7 @@ {"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":"Reply SDK_GOAL_READY without tools."}],"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 working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Time sampled while preparing turn 1, step 1: 2026-10-16T00:00:00+00:00[UTC]\nBrowser time zone for this request: unavailable. Ask the user to clarify otherwise-unqualified dates and times.\nElapsed since the preceding model-visible message: unavailable."}],"source":{"kind":"time-context","form":"snapshot","sections":[{"name":"time-context","text":"Time sampled while preparing turn 1, step 1: 2026-10-16T00:00:00+00:00[UTC]\nBrowser time zone for this request: unavailable. Ask the user to clarify otherwise-unqualified dates and times.\nElapsed since the preceding model-visible message: unavailable."}]},"role":"user","id":"{{message:4}}"},"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"}} diff --git a/snapshots/sdk/manual-compact/session.v4.jsonl b/snapshots/sdk/manual-compact/session.v4.jsonl index 5786bc1b59..b440c61c57 100644 --- a/snapshots/sdk/manual-compact/session.v4.jsonl +++ b/snapshots/sdk/manual-compact/session.v4.jsonl @@ -8,7 +8,7 @@ {"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":"DSH_COMMANDS_OLD_USER_INPUT\nKeep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. Keep the original request only in the stored history. "}],"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 working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Time sampled while preparing turn 1, step 1: 2026-10-16T00:00:00+00:00[UTC]\nBrowser time zone for this request: unavailable. Ask the user to clarify otherwise-unqualified dates and times.\nElapsed since the preceding model-visible message: unavailable."}],"source":{"kind":"time-context","form":"snapshot","sections":[{"name":"time-context","text":"Time sampled while preparing turn 1, step 1: 2026-10-16T00:00:00+00:00[UTC]\nBrowser time zone for this request: unavailable. Ask the user to clarify otherwise-unqualified dates and times.\nElapsed since the preceding model-visible message: unavailable."}]},"role":"user","id":"{{message:4}}"},"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"}} @@ -20,16 +20,16 @@ {"type":"command/run","data":{"commandId":"{{command:1}}","name":"compact","args":"","source":{"kind":"user"}}} {"type":"compaction/start","data":{"compactionId":"{{id:1}}","sourceCommandId":"{{command:1}}","turn":null}} {"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session:1}}","sessionFormatVersion":4,"throughSeq":19}} -{"type":"compaction/summary","data":{"compactionId":"{{id:1}}","sourceCommandId":"{{command:1}}","summary":[{"type":"text","text":"## Primary Request and Intent\n- DSH_COMMANDS_CHECKPOINT: retain the earlier task."}],"rawOutput":[{"type":"text","text":"## Primary Request and Intent\n- DSH_COMMANDS_CHECKPOINT: retain the earlier task."}],"llmStreamCall":true,"shadowedRange":{"start":8,"end":10},"shadowedSeqs":[8,9,10],"shadowedTokenCount":3764,"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":65536,"usage":{"inputTokens":10000,"outputTokens":32,"totalTokens":10032}}} +{"type":"compaction/summary","data":{"compactionId":"{{id:1}}","sourceCommandId":"{{command:1}}","summary":[{"type":"text","text":"## Primary Request and Intent\n- DSH_COMMANDS_CHECKPOINT: retain the earlier task."}],"rawOutput":[{"type":"text","text":"## Primary Request and Intent\n- DSH_COMMANDS_CHECKPOINT: retain the earlier task."}],"llmStreamCall":true,"shadowedRange":{"start":8,"end":10},"shadowedSeqs":[8,9,10],"shadowedTokenCount":3770,"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":65536,"usage":{"inputTokens":10000,"outputTokens":32,"totalTokens":10032}}} {"type":"user/message","data":{"content":[{"type":"text","text":"This is an automatically generated checkpoint condensing an earlier span of the conversation to free up context. Treat the captured context as established background and build on it without restating it. Continue the task directly from the messages that follow, without acknowledging this checkpoint.\n\n"},{"type":"text","text":"## Primary Request and Intent\n- DSH_COMMANDS_CHECKPOINT: retain the earlier task."},{"type":"text","text":""}],"source":{"kind":"compact-checkpoint","compactionId":"{{id:1}}","sourceCommandId":"{{command:1}}"},"role":"user","id":"{{message:6}}"},"sourceEventSeqs":[19,21,8,9,10],"surfaceOp":{"op":"replace","startSeq":8,"endSeq":10}} {"type":"compaction/end","data":{"compactionId":"{{id:1}}","sourceCommandId":"{{command:1}}","turn":null}} -{"type":"command/done","data":{"commandId":"{{command:1}}","kind":"success","text":"Compacted 3 history items (~3764 tokens).","sourceEventSeq":21}} +{"type":"command/done","data":{"commandId":"{{command:1}}","kind":"success","text":"Compacted 3 history items (~3770 tokens).","sourceEventSeq":21}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"DSH_COMMANDS_CONTINUE: answer using the retained checkpoint."}],"source":{"kind":"user"},"role":"user","id":"{{message:7}}"}]}} {"type":"turn/start","data":{"turn":2}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":2,"step":1}} {"type":"user/message","data":{"content":[{"type":"text","text":"DSH_COMMANDS_CONTINUE: answer using the retained checkpoint."}],"source":{"kind":"user"},"role":"user","id":"{{message:7}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{message:8}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{message:8}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"series"}} {"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session:1}}","sessionFormatVersion":4,"throughSeq":31}} {"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SDK_COMPACT_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"text"}]}},"id":"{{message:9}}"},"usage":{"inputTokens":10000,"outputTokens":32,"totalTokens":10032},"stream":[{"type":"chunk","time":1792108800000,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1792108800000,"index":0,"dt":[],"texts":["SDK_COMPACT_OK"]},{"type":"chunk","time":1792108800000,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SDK_COMPACT_OK"}}},{"type":"chunk","time":1792108800000,"chunk":{"type":"usage","usage":{"inputTokens":10000,"outputTokens":32,"totalTokens":10032}}},{"type":"chunk","time":1792108800000,"chunk":{"type":"finish","reason":{"kind":"stop"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"text"}]}}}]},"surfaceOp":"append"} diff --git a/snapshots/sdk/permission-switch/session.v4.jsonl b/snapshots/sdk/permission-switch/session.v4.jsonl index 3f2955a361..9191f5169b 100644 --- a/snapshots/sdk/permission-switch/session.v4.jsonl +++ b/snapshots/sdk/permission-switch/session.v4.jsonl @@ -21,7 +21,7 @@ {"type":"permission/preset","data":{"preset":"read-only"}} {"type":"sandbox/mode","data":{"mode":"read-only"}} {"type":"approval/policy","data":{"policy":"ask"}} -{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"The approval policy changed from \"never\" to \"ask\" (changed by the user)."}],"source":{"kind":"user-approval"},"role":"user","id":"{{message:6}}"}]}} +{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"The user changed the approval policy from \"never ask the user for approval\" to \"ask the user for approval\"."}],"source":{"kind":"user-approval"},"role":"user","id":"{{message:6}}"}]}} {"type":"command/done","data":{"commandId":"{{command:1}}","kind":"success","text":"preset read-only"}} {"type":"command/run","data":{"commandId":"{{command:2}}","name":"permission","args":" read-only","source":{"kind":"user"}}} {"type":"command/done","data":{"commandId":"{{command:2}}","kind":"success","text":"preset read-only"}} @@ -32,9 +32,9 @@ {"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":2,"step":1}} -{"type":"user/message","data":{"content":[{"type":"text","text":"The approval policy changed from \"never\" to \"ask\" (changed by the user)."}],"source":{"kind":"user-approval"},"role":"user","id":"{{message:6}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"The user changed the approval policy from \"never ask the user for approval\" to \"ask the user for approval\"."}],"source":{"kind":"user-approval"},"role":"user","id":"{{message:6}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Continue the earlier task under the current permissions. Reply SDK_PERMISSION_OK without using tools."}],"source":{"kind":"user"},"role":"user","id":"{{message:7}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{message:8}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{message:8}}"},"surfaceOp":"append"} {"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session:1}}","sessionFormatVersion":4,"throughSeq":35}} {"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SDK_PERMISSION_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"text"}]}},"id":"{{message:9}}"},"usage":{"inputTokens":10000,"outputTokens":32,"totalTokens":10032},"stream":[{"type":"chunk","time":1792108800000,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1792108800000,"index":0,"dt":[],"texts":["SDK_PERMISSION_OK"]},{"type":"chunk","time":1792108800000,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SDK_PERMISSION_OK"}}},{"type":"chunk","time":1792108800000,"chunk":{"type":"usage","usage":{"inputTokens":10000,"outputTokens":32,"totalTokens":10032}}},{"type":"chunk","time":1792108800000,"chunk":{"type":"finish","reason":{"kind":"stop"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"text"}]}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} diff --git a/snapshots/sdk/plan-review/session.v4.jsonl b/snapshots/sdk/plan-review/session.v4.jsonl index e9ab4b260a..a9c5c3f265 100644 --- a/snapshots/sdk/plan-review/session.v4.jsonl +++ b/snapshots/sdk/plan-review/session.v4.jsonl @@ -8,7 +8,7 @@ {"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":"Reply SDK_PLAN_INITIAL without tools."}],"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 working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Time sampled while preparing turn 1, step 1: 2026-10-16T00:00:00+00:00[UTC]\nBrowser time zone for this request: unavailable. Ask the user to clarify otherwise-unqualified dates and times.\nElapsed since the preceding model-visible message: unavailable."}],"source":{"kind":"time-context","form":"snapshot","sections":[{"name":"time-context","text":"Time sampled while preparing turn 1, step 1: 2026-10-16T00:00:00+00:00[UTC]\nBrowser time zone for this request: unavailable. Ask the user to clarify otherwise-unqualified dates and times.\nElapsed since the preceding model-visible message: unavailable."}]},"role":"user","id":"{{message:4}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["ask_user_question","bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","present","read","read_image","schedule_create","schedule_delete","schedule_list","schedule_update","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} @@ -17,36 +17,35 @@ {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SDK_PLAN_INITIAL"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"text"}]}},"id":"{{message:5}}"},"usage":{"inputTokens":10000,"outputTokens":32,"totalTokens":10032},"stream":[{"type":"chunk","time":1792108800000,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1792108800000,"index":0,"dt":[],"texts":["SDK_PLAN_INITIAL"]},{"type":"chunk","time":1792108800000,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SDK_PLAN_INITIAL"}}},{"type":"chunk","time":1792108800000,"chunk":{"type":"usage","usage":{"inputTokens":10000,"outputTokens":32,"totalTokens":10032}}},{"type":"chunk","time":1792108800000,"chunk":{"type":"finish","reason":{"kind":"stop"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"text"}]}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} -{"type":"command/run","data":{"commandId":"{{command:1}}","name":"plan","args":"","source":{"kind":"user"}}} +{"type":"command/run","data":{"commandId":"{{command:1}}","name":"plan","args":" Prepare a plan through exit_plan_mode; revise after feedback and acknowledge approval.","source":{"kind":"user"}}} {"type":"plan/mode","data":{"active":true}} {"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"The user switched this session to plan mode."}],"source":{"kind":"plan-mode","form":"notice","summary":"The user switched this session to plan mode."},"role":"user","id":"{{message:6}}"}]}} -{"type":"command/done","data":{"commandId":"{{command:1}}","kind":"success","text":"Plan mode on. Use /plan off to leave."}} -{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Prepare a plan through exit_plan_mode; revise after feedback and acknowledge approval."}],"source":{"kind":"user"},"role":"user","id":"{{message:7}}"}]}} +{"type":"agent/inbox/spliced","data":{"target":"next-step","start":1,"inserted":[{"content":[{"type":"text","text":"Prepare a plan through exit_plan_mode; revise after feedback and acknowledge approval."}],"source":{"kind":"user"},"role":"user","id":"{{message:7}}"}]}} {"type":"turn/start","data":{"turn":2}} -{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}} -{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} +{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":2,"inserted":[]}} +{"type":"command/done","data":{"commandId":"{{command:1}}","kind":"success","text":"Plan mode on. Use /plan off to leave."}} {"type":"step/start","data":{"turn":2,"step":1}} {"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:8}}"}},"sourceEventSeqs":[7],"surfaceOp":{"op":"replace","startSeq":7,"endSeq":7}} {"type":"user/message","data":{"content":[{"type":"text","text":"The user switched this session to plan mode."}],"source":{"kind":"plan-mode","form":"notice","summary":"The user switched this session to plan mode."},"role":"user","id":"{{message:6}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Prepare a plan through exit_plan_mode; revise after feedback and acknowledge approval."}],"source":{"kind":"user"},"role":"user","id":"{{message:7}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["ask_user_question","bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","present","read","read_image","schedule_create","schedule_delete","schedule_list","schedule_update","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change","startsSeries":true}} -{"type":"developer/message","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"exit_plan_mode"}],"role":"developer","id":"{{message:9}}"},"headerSeq":30},"surfaceOp":"append"} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session:1}}","sessionFormatVersion":4,"throughSeq":31}} +{"type":"developer/message","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"exit_plan_mode"}],"role":"developer","id":"{{message:9}}"},"headerSeq":29},"surfaceOp":"append"} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session:1}}","sessionFormatVersion":4,"throughSeq":30}} {"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"commands-tool-1","name":"exit_plan_mode","arguments":"{\"plan\": \"# Implement the change\\n\\n- Inspect existing code.\\n- Make the smallest change.\\n- Run focused tests.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"tool-call"}]}},"id":"{{message:10}}"},"usage":{"inputTokens":10000,"outputTokens":32,"totalTokens":10032},"stream":[{"type":"chunk","time":1792108800000,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792108800000,"index":0,"dt":[],"id":"commands-tool-1","name":"exit_plan_mode","args":[""]},{"type":"tool-call-chunks","time0":1792108800000,"index":0,"dt":[],"id":"commands-tool-1","args":["{\"plan\": \"# Implement the change\\n\\n- Inspect existing code.\\n- Make the smallest change.\\n- Run focused tests.\"}"]},{"type":"chunk","time":1792108800000,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"commands-tool-1","name":"exit_plan_mode","arguments":"{\"plan\": \"# Implement the change\\n\\n- Inspect existing code.\\n- Make the smallest change.\\n- Run focused tests.\"}"}}},{"type":"chunk","time":1792108800000,"chunk":{"type":"usage","usage":{"inputTokens":10000,"outputTokens":32,"totalTokens":10032}}},{"type":"chunk","time":1792108800000,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":2,"step":1,"callId":"commands-tool-1","name":"exit_plan_mode","arguments":"{\"plan\": \"# Implement the change\\n\\n- Inspect existing code.\\n- Make the smallest change.\\n- Run focused tests.\"}"}} -{"type":"tool/result","data":{"turn":2,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"commands-tool-1"},"toolCallId":"commands-tool-1","content":[{"type":"text","text":"Error: The user chose to keep planning; their feedback: Add rollback."}],"isError":true,"id":"{{message:11}}"}},"sourceEventSeqs":[34],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":2,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"commands-tool-1"},"toolCallId":"commands-tool-1","content":[{"type":"text","text":"Error: The user chose to keep planning; their feedback: Add rollback."}],"isError":true,"id":"{{message:11}}"}},"sourceEventSeqs":[33],"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"step/start","data":{"turn":2,"step":2}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session:1}}","sessionFormatVersion":4,"throughSeq":37}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session:1}}","sessionFormatVersion":4,"throughSeq":36}} {"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"commands-tool-2","name":"exit_plan_mode","arguments":"{\"plan\": \"# Revised plan\\n\\n- Include a rollback procedure.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"tool-call"}]}},"id":"{{message:12}}"},"usage":{"inputTokens":10000,"outputTokens":32,"totalTokens":10032},"stream":[{"type":"chunk","time":1792108800000,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792108800000,"index":0,"dt":[],"id":"commands-tool-2","name":"exit_plan_mode","args":[""]},{"type":"tool-call-chunks","time0":1792108800000,"index":0,"dt":[],"id":"commands-tool-2","args":["{\"plan\": \"# Revised plan\\n\\n- Include a rollback procedure.\"}"]},{"type":"chunk","time":1792108800000,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"commands-tool-2","name":"exit_plan_mode","arguments":"{\"plan\": \"# Revised plan\\n\\n- Include a rollback procedure.\"}"}}},{"type":"chunk","time":1792108800000,"chunk":{"type":"usage","usage":{"inputTokens":10000,"outputTokens":32,"totalTokens":10032}}},{"type":"chunk","time":1792108800000,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":2,"step":2,"callId":"commands-tool-2","name":"exit_plan_mode","arguments":"{\"plan\": \"# Revised plan\\n\\n- Include a rollback procedure.\"}"}} -{"type":"tool/result","data":{"turn":2,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"commands-tool-2"},"toolCallId":"commands-tool-2","content":[{"type":"text","text":"Plan approved — plan mode exited; carry out the plan starting with your next step."}],"isError":false,"id":"{{message:13}}"}},"sourceEventSeqs":[40],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":2,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"commands-tool-2"},"toolCallId":"commands-tool-2","content":[{"type":"text","text":"Plan approved — plan mode exited; carry out the plan starting with your next step."}],"isError":false,"id":"{{message:13}}"}},"sourceEventSeqs":[39],"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":2}} {"type":"plan/mode","data":{"active":false}} {"type":"step/start","data":{"turn":2,"step":3}} -{"type":"system/message","data":{"turn":2,"step":3,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:14}}"}},"sourceEventSeqs":[27],"surfaceOp":{"op":"replace","startSeq":27,"endSeq":27}} +{"type":"system/message","data":{"turn":2,"step":3,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:14}}"}},"sourceEventSeqs":[26],"surfaceOp":{"op":"replace","startSeq":26,"endSeq":26}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["ask_user_question","bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","present","read","read_image","schedule_create","schedule_delete","schedule_list","schedule_update","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"series"}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session:1}}","sessionFormatVersion":4,"throughSeq":46}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session:1}}","sessionFormatVersion":4,"throughSeq":45}} {"type":"assistant/message","data":{"turn":2,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"SDK_PLAN_APPROVED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"text"}]}},"id":"{{message:15}}"},"usage":{"inputTokens":10000,"outputTokens":32,"totalTokens":10032},"stream":[{"type":"chunk","time":1792108800000,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1792108800000,"index":0,"dt":[],"texts":["SDK_PLAN_APPROVED"]},{"type":"chunk","time":1792108800000,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SDK_PLAN_APPROVED"}}},{"type":"chunk","time":1792108800000,"chunk":{"type":"usage","usage":{"inputTokens":10000,"outputTokens":32,"totalTokens":10032}}},{"type":"chunk","time":1792108800000,"chunk":{"type":"finish","reason":{"kind":"stop"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"text"}]}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":3}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} @@ -59,14 +58,14 @@ {"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":3,"step":1}} -{"type":"system/message","data":{"turn":3,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:18}}"}},"sourceEventSeqs":[45],"surfaceOp":{"op":"replace","startSeq":45,"endSeq":45}} +{"type":"system/message","data":{"turn":3,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:18}}"}},"sourceEventSeqs":[44],"surfaceOp":{"op":"replace","startSeq":44,"endSeq":44}} {"type":"user/message","data":{"content":[{"type":"text","text":"The user switched this session to plan mode."}],"source":{"kind":"plan-mode","form":"notice","summary":"The user switched this session to plan mode."},"role":"user","id":"{{message:16}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Present another plan for review."}],"source":{"kind":"user"},"role":"user","id":"{{message:17}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["ask_user_question","bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","present","read","read_image","schedule_create","schedule_delete","schedule_list","schedule_update","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"series"}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session:1}}","sessionFormatVersion":4,"throughSeq":63}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session:1}}","sessionFormatVersion":4,"throughSeq":62}} {"type":"assistant/message","data":{"turn":3,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"commands-tool-4","name":"exit_plan_mode","arguments":"{\"plan\": \"# Implement the change\\n\\n- Inspect existing code.\\n- Make the smallest change.\\n- Run focused tests.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"tool-call"}]}},"id":"{{message:19}}"},"usage":{"inputTokens":10000,"outputTokens":32,"totalTokens":10032},"stream":[{"type":"chunk","time":1792108800000,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1792108800000,"index":0,"dt":[],"id":"commands-tool-4","name":"exit_plan_mode","args":[""]},{"type":"tool-call-chunks","time0":1792108800000,"index":0,"dt":[],"id":"commands-tool-4","args":["{\"plan\": \"# Implement the change\\n\\n- Inspect existing code.\\n- Make the smallest change.\\n- Run focused tests.\"}"]},{"type":"chunk","time":1792108800000,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"commands-tool-4","name":"exit_plan_mode","arguments":"{\"plan\": \"# Implement the change\\n\\n- Inspect existing code.\\n- Make the smallest change.\\n- Run focused tests.\"}"}}},{"type":"chunk","time":1792108800000,"chunk":{"type":"usage","usage":{"inputTokens":10000,"outputTokens":32,"totalTokens":10032}}},{"type":"chunk","time":1792108800000,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":3,"step":1,"callId":"commands-tool-4","name":"exit_plan_mode","arguments":"{\"plan\": \"# Implement the change\\n\\n- Inspect existing code.\\n- Make the smallest change.\\n- Run focused tests.\"}"}} -{"type":"tool/result","data":{"turn":3,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"commands-tool-4"},"toolCallId":"commands-tool-4","content":[{"type":"text","text":"The user dismissed the plan review to reply in their own words; plan mode remains active."}],"isError":false,"id":"{{message:20}}"}},"sourceEventSeqs":[66],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":3,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"commands-tool-4"},"toolCallId":"commands-tool-4","content":[{"type":"text","text":"The user dismissed the plan review to reply in their own words; plan mode remains active."}],"isError":false,"id":"{{message:20}}"}},"sourceEventSeqs":[65],"surfaceOp":"append"} {"type":"step/end","data":{"turn":3,"step":1}} {"type":"turn/end","data":{"turn":3,"reason":{"kind":"completed"}}} {"type":"command/run","data":{"commandId":"{{command:3}}","name":"plan","args":" off","source":{"kind":"user"}}} @@ -78,11 +77,11 @@ {"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":4,"step":1}} -{"type":"system/message","data":{"turn":4,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:23}}"}},"sourceEventSeqs":[60],"surfaceOp":{"op":"replace","startSeq":60,"endSeq":60}} +{"type":"system/message","data":{"turn":4,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:23}}"}},"sourceEventSeqs":[59],"surfaceOp":{"op":"replace","startSeq":59,"endSeq":59}} {"type":"user/message","data":{"content":[{"type":"text","text":"The user switched this session back to the default mode."}],"source":{"kind":"plan-mode","form":"notice","summary":"The user switched this session back to the default mode."},"role":"user","id":"{{message:21}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Reply SDK_PLAN_OK without tools."}],"source":{"kind":"user"},"role":"user","id":"{{message:22}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["ask_user_question","bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","present","read","read_image","schedule_create","schedule_delete","schedule_list","schedule_update","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"series"}} -{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session:1}}","sessionFormatVersion":4,"throughSeq":82}} +{"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session:1}}","sessionFormatVersion":4,"throughSeq":81}} {"type":"assistant/message","data":{"turn":4,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SDK_PLAN_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"text"}]}},"id":"{{message:24}}"},"usage":{"inputTokens":10000,"outputTokens":32,"totalTokens":10032},"stream":[{"type":"chunk","time":1792108800000,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1792108800000,"index":0,"dt":[],"texts":["SDK_PLAN_OK"]},{"type":"chunk","time":1792108800000,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SDK_PLAN_OK"}}},{"type":"chunk","time":1792108800000,"chunk":{"type":"usage","usage":{"inputTokens":10000,"outputTokens":32,"totalTokens":10032}}},{"type":"chunk","time":1792108800000,"chunk":{"type":"finish","reason":{"kind":"stop"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"text"}]}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":4,"step":1}} {"type":"turn/end","data":{"turn":4,"reason":{"kind":"completed"}}} diff --git a/snapshots/sdk/plan-review/system-prompt.expected.md b/snapshots/sdk/plan-review/system-prompt.expected.md index 0bc43de2fb..d76bb0ad7b 100644 --- a/snapshots/sdk/plan-review/system-prompt.expected.md +++ b/snapshots/sdk/plan-review/system-prompt.expected.md @@ -26,7 +26,7 @@ create_goal may infer goal intent from a direct human request in any language. A Use the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls. -Start independent delegations with `subagent` or `subagent_fork` together in one assistant message and continue useful work while they run. +Start independent delegations with `subagent` or `subagent_fork` together in one assistant message and continue useful work while they run. When only waiting for their results remains, end your turn instead of sleeping or polling; the next result starts a new turn. Prefer showing the primary results within your final response alongside a brief explanation. Use ![Description]() when an image supports an explanation or comparison. Use [Description]() when referring to an image or listing files. Enclose Markdown file destinations in angle brackets, especially paths containing spaces. Do not call present just to list edited source files, or run commands to check whether a diff view will appear. Use present to publish complete deliverables, including images, Office documents, spreadsheets, and slide decks, when separate access helps the user. Hosts that support file cards can show the presented file with preview and open actions. Avoid repeating results already shown inline unless the separate card adds useful access. Outside commands, configuration expressions, and code blocks, link every mention of an existing file, including repeats and tables, to its full path relative to the working directory or absolute; append #L24 or #L24-L30 to the target for known lines. Use the filename or a clear alias as the label, adding only enough parent directories to distinguish files; keep full paths out of labels. Default to the name alone; when precise locations matter, append :24 or :24–30, with no # or L in the line suffix. @@ -73,7 +73,7 @@ create_goal may infer goal intent from a direct human request in any language. A Use the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls. -Start independent delegations with `subagent` or `subagent_fork` together in one assistant message and continue useful work while they run. +Start independent delegations with `subagent` or `subagent_fork` together in one assistant message and continue useful work while they run. When only waiting for their results remains, end your turn instead of sleeping or polling; the next result starts a new turn. Prefer showing the primary results within your final response alongside a brief explanation. Use ![Description]() when an image supports an explanation or comparison. Use [Description]() when referring to an image or listing files. Enclose Markdown file destinations in angle brackets, especially paths containing spaces. Do not call present just to list edited source files, or run commands to check whether a diff view will appear. Use present to publish complete deliverables, including images, Office documents, spreadsheets, and slide decks, when separate access helps the user. Hosts that support file cards can show the presented file with preview and open actions. Avoid repeating results already shown inline unless the separate card adds useful access. Outside commands, configuration expressions, and code blocks, link every mention of an existing file, including repeats and tables, to its full path relative to the working directory or absolute; append #L24 or #L24-L30 to the target for known lines. Use the filename or a clear alias as the label, adding only enough parent directories to distinguish files; keep full paths out of labels. Default to the name alone; when precise locations matter, append :24 or :24–30, with no # or L in the line suffix. @@ -107,7 +107,7 @@ create_goal may infer goal intent from a direct human request in any language. A Use the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls. -Start independent delegations with `subagent` or `subagent_fork` together in one assistant message and continue useful work while they run. +Start independent delegations with `subagent` or `subagent_fork` together in one assistant message and continue useful work while they run. When only waiting for their results remains, end your turn instead of sleeping or polling; the next result starts a new turn. Prefer showing the primary results within your final response alongside a brief explanation. Use ![Description]() when an image supports an explanation or comparison. Use [Description]() when referring to an image or listing files. Enclose Markdown file destinations in angle brackets, especially paths containing spaces. Do not call present just to list edited source files, or run commands to check whether a diff view will appear. Use present to publish complete deliverables, including images, Office documents, spreadsheets, and slide decks, when separate access helps the user. Hosts that support file cards can show the presented file with preview and open actions. Avoid repeating results already shown inline unless the separate card adds useful access. Outside commands, configuration expressions, and code blocks, link every mention of an existing file, including repeats and tables, to its full path relative to the working directory or absolute; append #L24 or #L24-L30 to the target for known lines. Use the filename or a clear alias as the label, adding only enough parent directories to distinguish files; keep full paths out of labels. Default to the name alone; when precise locations matter, append :24 or :24–30, with no # or L in the line suffix. @@ -154,7 +154,7 @@ create_goal may infer goal intent from a direct human request in any language. A Use the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls. -Start independent delegations with `subagent` or `subagent_fork` together in one assistant message and continue useful work while they run. +Start independent delegations with `subagent` or `subagent_fork` together in one assistant message and continue useful work while they run. When only waiting for their results remains, end your turn instead of sleeping or polling; the next result starts a new turn. Prefer showing the primary results within your final response alongside a brief explanation. Use ![Description]() when an image supports an explanation or comparison. Use [Description]() when referring to an image or listing files. Enclose Markdown file destinations in angle brackets, especially paths containing spaces. Do not call present just to list edited source files, or run commands to check whether a diff view will appear. Use present to publish complete deliverables, including images, Office documents, spreadsheets, and slide decks, when separate access helps the user. Hosts that support file cards can show the presented file with preview and open actions. Avoid repeating results already shown inline unless the separate card adds useful access. Outside commands, configuration expressions, and code blocks, link every mention of an existing file, including repeats and tables, to its full path relative to the working directory or absolute; append #L24 or #L24-L30 to the target for known lines. Use the filename or a clear alias as the label, adding only enough parent directories to distinguish files; keep full paths out of labels. Default to the name alone; when precise locations matter, append :24 or :24–30, with no # or L in the line suffix. @@ -188,6 +188,6 @@ create_goal may infer goal intent from a direct human request in any language. A Use the workflow tool ONLY when the user explicitly asks for a workflow or for large multi-agent orchestration: you write a JavaScript script (the tool description documents the exact format) that fans work out across many subagents with phases and structured results. For one or two delegations, prefer plain subagent calls. -Start independent delegations with `subagent` or `subagent_fork` together in one assistant message and continue useful work while they run. +Start independent delegations with `subagent` or `subagent_fork` together in one assistant message and continue useful work while they run. When only waiting for their results remains, end your turn instead of sleeping or polling; the next result starts a new turn. Prefer showing the primary results within your final response alongside a brief explanation. Use ![Description]() when an image supports an explanation or comparison. Use [Description]() when referring to an image or listing files. Enclose Markdown file destinations in angle brackets, especially paths containing spaces. Do not call present just to list edited source files, or run commands to check whether a diff view will appear. Use present to publish complete deliverables, including images, Office documents, spreadsheets, and slide decks, when separate access helps the user. Hosts that support file cards can show the presented file with preview and open actions. Avoid repeating results already shown inline unless the separate card adds useful access. Outside commands, configuration expressions, and code blocks, link every mention of an existing file, including repeats and tables, to its full path relative to the working directory or absolute; append #L24 or #L24-L30 to the target for known lines. Use the filename or a clear alias as the label, adding only enough parent directories to distinguish files; keep full paths out of labels. Default to the name alone; when precise locations matter, append :24 or :24–30, with no # or L in the line suffix. diff --git a/snapshots/sdk/sdk.snapshot.ts b/snapshots/sdk/sdk.snapshot.ts index 199e17486b..08cdf6fe57 100644 --- a/snapshots/sdk/sdk.snapshot.ts +++ b/snapshots/sdk/sdk.snapshot.ts @@ -526,18 +526,30 @@ function modelFromSession(log: string): { provider: string; model: string } { /** Replay caller inputs; profile fixtures recreate their own in-turn command activity. */ function sdkActions(log: string, replayStandaloneCommands: boolean): SdkAction[] { const actions: SdkAction[] = [] + const commandInputs = new Set() + let commandId: string | undefined let current: TurnAction | undefined for (const record of records(log)) { if (record.type === 'command/run') { if (!replayStandaloneCommands || current !== undefined) continue const data = record.data as JsonObject | undefined if ((data?.source as JsonObject | undefined)?.kind !== 'user') continue - if (typeof data?.name !== 'string' || typeof data.args !== 'string') { - throw new Error('SDK snapshot command/run requires a name and recorded string arguments') + if (typeof data?.name !== 'string' || typeof data.args !== 'string' || typeof data.commandId !== 'string') { + throw new Error('SDK snapshot command/run requires commandId, name and recorded string arguments') } + commandId = data.commandId actions.push({ kind: 'command', line: `/${data.name}${data.args}` }) continue } + const data = record.data as JsonObject | undefined + // Standalone callers send no other input until command/done. Inputs admitted + // within that invocation belong to its handler, even when their source is user. + if (commandId !== undefined && record.type === 'agent/inbox/spliced' && Array.isArray(data?.inserted)) { + for (const message of data.inserted as JsonObject[]) { + if (typeof message.id === 'string') commandInputs.add(message.id) + } + } + if (record.type === 'command/done' && data?.commandId === commandId) commandId = undefined if (record.type === 'turn/start') { const data = record.data as JsonObject | undefined if (typeof data?.turn !== 'number') throw new Error('SDK snapshot turn/start has no turn') @@ -547,7 +559,8 @@ function sdkActions(log: string, replayStandaloneCommands: boolean): SdkAction[] if (record.type === 'user/message' && current !== undefined && current.content === undefined) { const data = record.data as JsonObject | undefined const source = data?.source as JsonObject | undefined - if (source?.kind === 'user' && Array.isArray(data?.content)) { + if (source?.kind === 'user' && Array.isArray(data?.content) + && (typeof data.id !== 'string' || !commandInputs.has(data.id))) { current = { kind: 'turn', turn: current.turn, content: data.content as JsonObject[] } } continue @@ -1171,7 +1184,8 @@ describe('TypeScript SDK snapshots over the jsonrpc runtime', () => { expect(events.filter(event => event.type === 'plan/mode').map(event => event.data.active)) .toEqual([true, false, true, false]) expect(events.filter(event => event.type === 'command/run').map(event => ({ name: event.data.name, args: event.data.args }))) - .toEqual([{ name: 'plan', args: '' }, { name: 'plan', args: '' }, { name: 'plan', args: ' off' }]) + .toEqual([{ name: 'plan', args: ' Prepare a plan through exit_plan_mode; revise after feedback and acknowledge approval.' }, + { name: 'plan', args: '' }, { name: 'plan', args: ' off' }]) expect(events.filter(event => event.type === 'permission/preset').map(event => event.data.preset)).toEqual(['workspace-write']) expect(events.filter(event => event.type === 'sandbox/mode').map(event => event.data.mode)).toEqual(['workspace-write']) expect(events.filter(event => event.type === 'approval/policy').map(event => event.data.policy)).toEqual(['ask']) @@ -1215,8 +1229,6 @@ describe('TypeScript SDK snapshots over the jsonrpc runtime', () => { expect(events.filter(event => event.type === 'step/start')).toHaveLength(2) const notices = events.filter(event => event.type === 'user/message' && event.data.source.kind === 'user-approval') expect(notices).toHaveLength(1) - const noticeText = 'The approval policy changed from "never" to "ask" (changed by the user).' - expect(notices[0]?.data.content).toEqual([{ type: 'text', text: noticeText }]) expect(modelRequests).toHaveLength(2) const [initialRequest, continuationRequest] = modelRequests if (initialRequest === undefined || continuationRequest === undefined) { @@ -1231,11 +1243,10 @@ describe('TypeScript SDK snapshots over the jsonrpc runtime', () => { const runtimeContexts = continuationMessages.filter(message => (message.source as JsonObject | undefined)?.kind === 'runtime-context') const runtimeText = JSON.stringify(runtimeContexts.at(-1)) expect(runtimeText).toContain('Current DSH file policy: read-only.') - expect(runtimeText).toContain('Approval policy: ask.') const texts = continuationMessages.flatMap(message => message.content as JsonObject[]) .flatMap(block => block.type === 'text' ? [block.text] : []) expect(texts).toContain('SDK_PERMISSION_READY') - expect(texts).toContain(noticeText) + expect(continuationMessages.map(message => message.content)).toContainEqual(notices[0]?.data.content) expect(texts).not.toContain('/permission read-only') expect(texts).not.toContain('preset read-only') } @@ -1438,7 +1449,8 @@ describe('TypeScript SDK snapshots over the jsonrpc runtime', () => { const parent = ordered[0] if (parent === undefined) throw new Error(`${scenario.name}: no primary session log`) const turnEnds = records(parent.content).filter(record => record.type === 'turn/end') - expect(turnEnds, `${scenario.name}: completed turns`).toHaveLength(results.length) + const expectedTurns = records(replayContents[0]!).filter(record => record.type === 'turn/end') + expect(turnEnds, `${scenario.name}: completed turns`).toHaveLength(expectedTurns.length) for (const turnEnd of turnEnds) expect(turnEnd).toMatchObject({ data: { reason: { kind: 'completed' } } }) }