From d17fad9b9ee150e5b1372af2f7c06776568fc726 Mon Sep 17 00:00:00 2001 From: liukx0205 Date: Fri, 6 Nov 2026 14:43:56 +0800 Subject: [PATCH] fix(agent): classify closed admission and document migration --- docs/subsystems/core.i18n.yaml | 4 ++-- docs/subsystems/core.md | 12 +++++++----- docs/subsystems/core.zh.md | 12 +++++++----- .../agent-closed-admission/guide.i18n.yaml | 16 ++++++++++++++++ .../v0.2.2-rc.1/agent-closed-admission/guide.md | 17 +++++++++++++++++ .../agent-closed-admission/guide.zh.md | 17 +++++++++++++++++ packages/core/agent-loop/src/index.ts | 7 ++----- packages/core/agent-loop/tests/cancel.spec.ts | 2 +- .../tests/fixtures/serial-created.mjs | 9 +++------ .../agent-loop/tests/input-lifecycle.spec.ts | 17 ++++++++++++----- packages/core/agent/README.i18n.yaml | 4 ++-- packages/core/agent/README.md | 2 ++ packages/core/agent/README.zh.md | 2 ++ packages/core/agent/src/closed-error.ts | 8 ++++---- packages/core/agent/src/runtime-types.ts | 10 +++++----- packages/sdk/server/src/server.ts | 5 ++--- packages/subagent/subagent/src/manager.ts | 3 +-- scripts/smoke-python-runtime.py | 1 - .../python-sdk-single-exe/advanced/result.json | 8 ++++---- .../advanced/session.1.v4.jsonl | 4 ++-- .../advanced/session.2.v4.jsonl | 4 ++-- 21 files changed, 110 insertions(+), 54 deletions(-) create mode 100644 docs/upgrade-guide/v0.2.2-rc.1/agent-closed-admission/guide.i18n.yaml create mode 100644 docs/upgrade-guide/v0.2.2-rc.1/agent-closed-admission/guide.md create mode 100644 docs/upgrade-guide/v0.2.2-rc.1/agent-closed-admission/guide.zh.md diff --git a/docs/subsystems/core.i18n.yaml b/docs/subsystems/core.i18n.yaml index 363584a5fd..c3495dec81 100644 --- a/docs/subsystems/core.i18n.yaml +++ b/docs/subsystems/core.i18n.yaml @@ -12,8 +12,8 @@ en: d71c669c1a36bf52 zh: 9b335f90c84a894e /core/the-agent-handle: - en: 6ee15fec81bc0686 - zh: b6f3fcd48823ea75 + en: bb0aec7a50800c1e + zh: a58a3392368f644a /core/initiating-agent: en: 6c8c47ac78de2dde zh: c9d527a25a85d1d9 diff --git a/docs/subsystems/core.md b/docs/subsystems/core.md index 7b8503cb1a..7e86e6e0d1 100644 --- a/docs/subsystems/core.md +++ b/docs/subsystems/core.md @@ -98,7 +98,7 @@ interface Agent { * inbox until the task settles, while public status stays `idle`. * `whenIdle()` follows both the task and any waking work released behind it. * @param task - operation whose fulfillment or rejection is preserved, with a signal aborted by {@link cancel}. - * @throws AgentClosedError after disposal begins, or an Error when another activity owns the agent. + * @throws AgentClosedError once the Agent lifetime is aborted, or an Error when another activity owns the agent. * @returns the task promise. */ runMaintenance(task: (signal: AbortSignal) => Promise): Promise @@ -113,7 +113,7 @@ interface Agent { * @param message - identified content and the source that supplied it. * @param target - the preferred next-turn or next-step inbox boundary. * @param wakeup - whether delivery may wake the driver. - * @throws AgentClosedError after disposal begins; no input is inserted. + * @throws AgentClosedError once the Agent lifetime is aborted. The inbox is unchanged. */ send(message: UserMessage, target: InboxTarget, wakeup: boolean): void @@ -121,7 +121,7 @@ interface Agent { * Queue an ordinary follow-up turn and wake the driver. The item becomes the * sole ordinary message of its own turn. * @param message - identified prompt content and the source that supplied it. - * @throws AgentClosedError after disposal begins; no input is inserted. + * @throws AgentClosedError once the Agent lifetime is aborted. The inbox is unchanged. */ followup(message: UserMessage): void @@ -131,7 +131,7 @@ interface Agent { * A rejected step leaves steering parked in the inbox until the next * wake; cancellation or disposal may discard pending steering. * @param message - identified steering content and the source that supplied it. - * @throws AgentClosedError after disposal begins; no input is inserted. + * @throws AgentClosedError once the Agent lifetime is aborted. The inbox is unchanged. */ steer(message: UserMessage): void @@ -142,7 +142,7 @@ interface Agent { * wakes them. It may miss a request whose pre-step already claimed its * batch. Cancellation or disposal may discard pending context. * @param message - identified injected context and the source that supplied it. - * @throws AgentClosedError after disposal begins; no input is inserted. + * @throws AgentClosedError once the Agent lifetime is aborted. The inbox is unchanged. */ inject(message: UserMessage): void } @@ -374,6 +374,8 @@ The cause is a TypeScript-enforced same-process input. An active cancellation ho The [event taxonomy](../architecture.md#events) owns the `agent/*` lifecycle, checkpoint, and waterfall contracts. Turn and step boundaries are durable session events rather than agent emits. +`AgentClosedError` extends `HarnessError` with code `AGENT_CLOSED` and the affected `agentId`. Admission closes when the caller cancels creation, the Agent starts disposal, or its factory begins shutdown. The creation signal stops governing admission after creation succeeds. + ## Initiating Agent The process-local initiator carried by `ctx.agents` is the exact `Agent` above, not a separate frame or copied identity. Ambient presence is neither liveness proof nor authorization; the [initiator-scope decision](../../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.md) defines its lifetime and scope rules. diff --git a/docs/subsystems/core.zh.md b/docs/subsystems/core.zh.md index 9f0bf51061..c82be05f03 100644 --- a/docs/subsystems/core.zh.md +++ b/docs/subsystems/core.zh.md @@ -102,7 +102,7 @@ interface Agent { * inbox until the task settles, while public status stays `idle`. * `whenIdle()` follows both the task and any waking work released behind it. * @param task - operation whose fulfillment or rejection is preserved, with a signal aborted by {@link cancel}. - * @throws AgentClosedError after disposal begins, or an Error when another activity owns the agent. + * @throws AgentClosedError once the Agent lifetime is aborted, or an Error when another activity owns the agent. * @returns the task promise. */ runMaintenance(task: (signal: AbortSignal) => Promise): Promise @@ -117,7 +117,7 @@ interface Agent { * @param message - identified content and the source that supplied it. * @param target - the preferred next-turn or next-step inbox boundary. * @param wakeup - whether delivery may wake the driver. - * @throws AgentClosedError after disposal begins; no input is inserted. + * @throws AgentClosedError once the Agent lifetime is aborted. The inbox is unchanged. */ send(message: UserMessage, target: InboxTarget, wakeup: boolean): void @@ -125,7 +125,7 @@ interface Agent { * Queue an ordinary follow-up turn and wake the driver. The item becomes the * sole ordinary message of its own turn. * @param message - identified prompt content and the source that supplied it. - * @throws AgentClosedError after disposal begins; no input is inserted. + * @throws AgentClosedError once the Agent lifetime is aborted. The inbox is unchanged. */ followup(message: UserMessage): void @@ -135,7 +135,7 @@ interface Agent { * A rejected step leaves steering parked in the inbox until the next * wake; cancellation or disposal may discard pending steering. * @param message - identified steering content and the source that supplied it. - * @throws AgentClosedError after disposal begins; no input is inserted. + * @throws AgentClosedError once the Agent lifetime is aborted. The inbox is unchanged. */ steer(message: UserMessage): void @@ -146,7 +146,7 @@ interface Agent { * wakes them. It may miss a request whose pre-step already claimed its * batch. Cancellation or disposal may discard pending context. * @param message - identified injected context and the source that supplied it. - * @throws AgentClosedError after disposal begins; no input is inserted. + * @throws AgentClosedError once the Agent lifetime is aborted. The inbox is unchanged. */ inject(message: UserMessage): void } @@ -380,6 +380,8 @@ cause 是由 TypeScript 强制约束的同进程输入。活跃的取消持有 +`AgentClosedError` 继承 `HarnessError`,包含错误码 `AGENT_CLOSED` 和受影响的 `agentId`。调用方取消创建、Agent 开始释放或工厂开始关闭时,Agent 停止接纳新工作。创建成功后,创建信号不再控制工作接纳。 + ## 发起 Agent `ctx.agents` 携带的进程本地 initiator 就是上面的确切 `Agent`,不是单独的 frame 或复制的标识。环境中存在该值既不能证明存活,也不代表授权;[initiator 作用域决策](../../.agents/notes/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md)定义其生命周期和作用域规则。 diff --git a/docs/upgrade-guide/v0.2.2-rc.1/agent-closed-admission/guide.i18n.yaml b/docs/upgrade-guide/v0.2.2-rc.1/agent-closed-admission/guide.i18n.yaml new file mode 100644 index 0000000000..83fa03b9c8 --- /dev/null +++ b/docs/upgrade-guide/v0.2.2-rc.1/agent-closed-admission/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.2-rc.1/agent-closed-admission/guide.md +/: + en: e4666ee6e813e7a3 + zh: ab68705b61d0da3c +/handle-agent-input-rejected-during-shutdown: + en: 5ec42a6575141728 + zh: a5174b0e04526c58 +/handle-agent-input-rejected-during-shutdown/change: + en: def59fb50e36f026 + zh: 91e79aad0ea758ad +/handle-agent-input-rejected-during-shutdown/migration: + en: e4c7ffd0e06c01bf + zh: 7fc1f43747209fb7 diff --git a/docs/upgrade-guide/v0.2.2-rc.1/agent-closed-admission/guide.md b/docs/upgrade-guide/v0.2.2-rc.1/agent-closed-admission/guide.md new file mode 100644 index 0000000000..7aaff2efc6 --- /dev/null +++ b/docs/upgrade-guide/v0.2.2-rc.1/agent-closed-admission/guide.md @@ -0,0 +1,17 @@ +--- +kind: upgrade-guide +description: "Agent input and maintenance methods throw AgentClosedError once the Agent stops accepting new work." +--- + +# Handle Agent input rejected during shutdown + +English | [中文](guide.zh.md) + +## Change + +In v0.2.2-rc.1, a retained `Agent` could accept input while teardown was in progress. The Agent now rejects new input and maintenance work with `AgentClosedError` as soon as its lifetime is aborted. It can still appear in the registry while admitted work settles. + +## Migration + +1. In extensions that call `Agent.send`, `followup`, `steer`, `inject`, or `runMaintenance`, handle the synchronous `AgentClosedError` from `@deepseek-ai/dsh-agent` at the call site. Treat code `AGENT_CLOSED` as an unavailable Agent and preserve handling for other failures. +2. Verify by disposing an Agent with a maintenance task still settling. A subsequent input call throws `AgentClosedError` without adding an inbox entry. diff --git a/docs/upgrade-guide/v0.2.2-rc.1/agent-closed-admission/guide.zh.md b/docs/upgrade-guide/v0.2.2-rc.1/agent-closed-admission/guide.zh.md new file mode 100644 index 0000000000..9ee1eb17a1 --- /dev/null +++ b/docs/upgrade-guide/v0.2.2-rc.1/agent-closed-admission/guide.zh.md @@ -0,0 +1,17 @@ +--- +kind: upgrade-guide +description: "Agent 停止接纳新工作后,输入和维护方法会抛出 AgentClosedError。" +--- + +# 处理关闭期间被拒绝的 Agent 输入 + +[English](guide.md) | 中文 + +## 变更 + +在 v0.2.2-rc.1 中,调用方持有的 `Agent` 在清理过程中仍可能接收输入。现在,Agent 的生命周期信号一旦中止,就会以 `AgentClosedError` 拒绝新输入和维护工作。已接纳的工作收尾期间,注册表中仍可能找到该 Agent。 + +## 迁移 + +1. 若扩展调用 `Agent.send`、`followup`、`steer`、`inject` 或 `runMaintenance`,请在调用处处理从 `@deepseek-ai/dsh-agent` 导出的同步异常 `AgentClosedError`。收到 `AGENT_CLOSED` 时按 Agent 不可用处理,保留其他失败的处理逻辑。 +2. 在维护任务尚未结束时开始释放 Agent 以验证改动。之后调用输入方法应抛出 `AgentClosedError`,且不向 inbox 添加条目。 diff --git a/packages/core/agent-loop/src/index.ts b/packages/core/agent-loop/src/index.ts index a0b02d4eee..ecdafadb9d 100644 --- a/packages/core/agent-loop/src/index.ts +++ b/packages/core/agent-loop/src/index.ts @@ -469,11 +469,8 @@ export class AgentLoop extends Service implements AgentFactory { const id = session.id const loopCtx = this.runtime.ctx - // Deactivation fuses three owners, each with its own reason: the caller's - // cancellation signal, the owner fiber's unload, and factory teardown. - // It is registered BEFORE any resource exists, over mutable slots, so an - // unload arriving while the scope is still minting finds a working - // disposer instead of a leak. + // The caller's signal governs creation. Owner disposal and factory + // shutdown can end the Agent's lifetime both during and after creation. const abort = new AbortController() const onCallerAbort = (): void => { abort.abort(callerSignal?.reason instanceof Error diff --git a/packages/core/agent-loop/tests/cancel.spec.ts b/packages/core/agent-loop/tests/cancel.spec.ts index 2ca5eb0299..434857afe9 100644 --- a/packages/core/agent-loop/tests/cancel.spec.ts +++ b/packages/core/agent-loop/tests/cancel.spec.ts @@ -324,7 +324,7 @@ describe('Agent.cancel()', () => { send(agent, 'active') await streaming.promise const disposal = handle.dispose() - expect(() =>{ send(agent, 'late wake') }).toThrow(AgentClosedError) + expect(() => { send(agent, 'late wake') }).toThrow(AgentClosedError) await disposal expect(adapter.requests).toHaveLength(1) diff --git a/packages/core/agent-loop/tests/fixtures/serial-created.mjs b/packages/core/agent-loop/tests/fixtures/serial-created.mjs index 1295611023..2b27a7f33b 100644 --- a/packages/core/agent-loop/tests/fixtures/serial-created.mjs +++ b/packages/core/agent-loop/tests/fixtures/serial-created.mjs @@ -6,11 +6,11 @@ export const name = 'serial-created-fixture' export const inject = ['agents', 'sessions'] /** Install ordered creation listeners whose context must precede the first turn. */ -export function apply(ctx, config = {}) { +export function apply(ctx) { const checkedCancellation = new WeakSet() ctx.on('agent/pre-step', async ({ agent, messages, signal }, next) => { if (checkedCancellation.has(agent) - || (config.checkRootCancellation !== true && agent.session.header.origin !== 'subagent')) return next() + || agent.session.header.origin !== 'subagent') return next() checkedCancellation.add(agent) const pending = { id: randomUUID(), role: 'user', @@ -35,10 +35,7 @@ export function apply(ctx, config = {}) { agent.inbox.remove(pending.id) for (const message of messages) { const ordinary = message.source.kind === 'user' - const restored = message.source.kind === `plugin:${name}` - ? { ...message, content: [{ type: 'text', text: message.content[0].text + ' Queue-clear failures still cancel active work.' }] } - : message - agent.send(restored, ordinary ? 'next-turn' : 'next-step', ordinary) + agent.send(message, ordinary ? 'next-turn' : 'next-step', ordinary) } return next() }) diff --git a/packages/core/agent-loop/tests/input-lifecycle.spec.ts b/packages/core/agent-loop/tests/input-lifecycle.spec.ts index 560fea3ca1..3e9bb2cf10 100644 --- a/packages/core/agent-loop/tests/input-lifecycle.spec.ts +++ b/packages/core/agent-loop/tests/input-lifecycle.spec.ts @@ -6,7 +6,7 @@ import { join } from 'node:path' import { Context } from '@deepseek-ai/cordis' import AgentRegistry, { AgentClosedError, type Agent } from '@deepseek-ai/dsh-agent' import AgentLoop from '@deepseek-ai/dsh-agent-loop' -import LlmRuntime, { createUserMessage } from '@deepseek-ai/dsh-llm' +import LlmRuntime, { HarnessError, createUserMessage } from '@deepseek-ai/dsh-llm' import SessionStore, { Session, SessionId, type SessionEvent } from '@deepseek-ai/dsh-session' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' @@ -52,7 +52,6 @@ async function harness() { return { ctx, root, adapter, handle, agent: handle.agent } } - it('cancellation still aborts the running request when queue clearing fails', async () => { const { ctx, agent } = await harness() const entered = Promise.withResolvers() @@ -73,6 +72,7 @@ it('cancellation still aborts the running request when queue clearing fails', as }) expect(() => { agent.cancel({ kind: 'user' }) }).toThrow('injected queue clear failure') expect(signal.aborted).toBe(true) + expect(agent.inbox.nextTurn).toEqual([queued]) } finally { stopRejecting() hold() @@ -82,7 +82,6 @@ it('cancellation still aborts the running request when queue clearing fails', as } }) - it('an Agent still in the registry rejects new input while its owner is closing', async () => { const { ctx, agent, handle } = await harness() const release = Promise.withResolvers() @@ -96,6 +95,16 @@ it('an Agent still in the registry rejects new input while its owner is closing' await aborted.promise expect(ctx.agents.get(agent.id)).toBe(agent) const message = input('during closing') + const error = vi.fn((failure: unknown) => failure) + try { + agent.followup(message) + } catch (failure) { + error(failure) + } + expect(error).toHaveBeenCalledWith(expect.objectContaining({ + name: 'AgentClosedError', code: 'AGENT_CLOSED', agentId: agent.id, + })) + expect(error.mock.calls[0]?.[0]).toBeInstanceOf(HarnessError) for (const submit of [() => { agent.followup(message) }, () => { agent.steer(message) }, () => { agent.inject(message) }, () => { agent.send(message, 'next-turn', true) }, () => agent.runMaintenance(async () => {}), () => { agent.inbox.append('next-turn', message) }, () => { agent.inbox.prepend('next-step', message) }, () => agent.inbox.splice('next-turn', 0, 0, [message]), () => agent.inbox.replace(message.id, message)]) { expect(submit).toThrow(AgentClosedError) } @@ -108,8 +117,6 @@ it('an Agent still in the registry rejects new input while its owner is closing' } }) - - it('waits for admitted maintenance after a queue-clear failure before releasing its Session', async () => { const { ctx, agent, handle } = await harness() const release = Promise.withResolvers() diff --git a/packages/core/agent/README.i18n.yaml b/packages/core/agent/README.i18n.yaml index df3e29c8a9..6c21a72d7d 100644 --- a/packages/core/agent/README.i18n.yaml +++ b/packages/core/agent/README.i18n.yaml @@ -27,8 +27,8 @@ en: ff2b530bb20bcfe2 zh: 4f9024c4881a4ba5 /deepseek-ai-dsh-agent/use-this-package/scope-registrations-to-one-agent: - en: 2594a1e02adea801 - zh: cae8860369728efb + en: cec57f2cb7ab3a5c + zh: 2896b42eb5310441 /deepseek-ai-dsh-agent/use-this-package/intercept-or-observe-work-in-flight: en: 371511fb13093dd2 zh: 4be81d246cf299b2 diff --git a/packages/core/agent/README.md b/packages/core/agent/README.md index 9772578fcc..024f69aeef 100644 --- a/packages/core/agent/README.md +++ b/packages/core/agent/README.md @@ -73,6 +73,8 @@ await handle.agent.whenIdle() `Agent.ctx` is the agent's scoped context: registrations made through it (tools, prompt sections, variables, event listeners, restrictions) apply to that agent alone and unwind on disposal. The same mechanism is what agent presets use to give one session a different capability set without affecting its neighbors. +Input and maintenance methods reject closed Agents with `AgentClosedError` (`AGENT_CLOSED`). See [Agent lifetime and admission](../../../docs/subsystems/core.md#the-agent-handle) for the lifecycle rules. + ### Intercept or observe work in flight `agent/request` is deprecated and retained for existing listeners. Do not add new listeners. Provider, model, and optional explicit reasoning effort are fixed at step preparation; listeners must preserve all three, including an absent effort. `AgentOptions.maxTokens` sets the output cap. Other per-request control overrides have no replacement API. See the [migration guide](../../../docs/upgrade-guide/v0.2.1-alpha.4/session-model-selection/guide.md) for affected callers. diff --git a/packages/core/agent/README.zh.md b/packages/core/agent/README.zh.md index b5b2af52d4..693ad615ce 100644 --- a/packages/core/agent/README.zh.md +++ b/packages/core/agent/README.zh.md @@ -73,6 +73,8 @@ await handle.agent.whenIdle() `Agent.ctx` 是该 agent 的作用域上下文:通过它进行的注册(工具、提示词段、变量、事件监听器、限制)只对该 agent 生效,并在 dispose(资源释放)时全部撤销。同一机制也是 agent preset 用来让一个会话获得不同能力集、同时不影响其邻居的方式。 +输入和维护方法会以 `AgentClosedError`(`AGENT_CLOSED`)拒绝已停止接纳新工作的 Agent。生命周期规则见 [Agent 句柄](../../../docs/subsystems/core.zh.md#the-agent-handle)。 + ### 拦截或观察进行中的工作 `agent/request` 已弃用,仅为现有监听器保留。不要添加新监听器。提供方、模型和可选的显式推理强度在步骤准备时固定;监听器必须保留三者,包括未指定强度的状态。`AgentOptions.maxTokens` 设置输出上限。其他逐请求控制参数覆盖没有替代 API。受影响的调用方参见[迁移指南](../../../docs/upgrade-guide/v0.2.1-alpha.4/session-model-selection/guide.zh.md)。 diff --git a/packages/core/agent/src/closed-error.ts b/packages/core/agent/src/closed-error.ts index abcb335320..a4ca452887 100644 --- a/packages/core/agent/src/closed-error.ts +++ b/packages/core/agent/src/closed-error.ts @@ -1,15 +1,15 @@ /** Rejection of new work after an Agent starts closing. @module @deepseek-ai/dsh-agent/closed-error */ +import { HarnessError } from '@deepseek-ai/dsh-llm' import type { SessionId } from '@deepseek-ai/dsh-session' -/** An Agent has stopped accepting new input and maintenance work. */ -export class AgentClosedError extends Error { +/** An Agent has stopped accepting new input and maintenance work (`AGENT_CLOSED`). */ +export class AgentClosedError extends HarnessError { /** * @param agentId - closing Agent identity. * @param options - original lifecycle cancellation, when available. */ constructor(readonly agentId: SessionId, options?: ErrorOptions) { - super(`agent "${agentId}" is closing`, options) - this.name = 'AgentClosedError' + super(`agent "${agentId}" is closing`, 'AGENT_CLOSED', options) } } diff --git a/packages/core/agent/src/runtime-types.ts b/packages/core/agent/src/runtime-types.ts index aa5efe6d0a..39b15064dc 100644 --- a/packages/core/agent/src/runtime-types.ts +++ b/packages/core/agent/src/runtime-types.ts @@ -226,7 +226,7 @@ declare module './types.ts' { * inbox until the task settles, while public status stays `idle`. * `whenIdle()` follows both the task and any waking work released behind it. * @param task - operation whose fulfillment or rejection is preserved, with a signal aborted by {@link cancel}. - * @throws AgentClosedError after disposal begins, or an Error when another activity owns the agent. + * @throws AgentClosedError once the Agent lifetime is aborted, or an Error when another activity owns the agent. * @returns the task promise. */ runMaintenance(task: (signal: AbortSignal) => Promise): Promise @@ -241,7 +241,7 @@ declare module './types.ts' { * @param message - identified content and the source that supplied it. * @param target - the preferred next-turn or next-step inbox boundary. * @param wakeup - whether delivery may wake the driver. - * @throws AgentClosedError after disposal begins; no input is inserted. + * @throws AgentClosedError once the Agent lifetime is aborted. The inbox is unchanged. */ send(message: UserMessage, target: InboxTarget, wakeup: boolean): void @@ -249,7 +249,7 @@ declare module './types.ts' { * Queue an ordinary follow-up turn and wake the driver. The item becomes the * sole ordinary message of its own turn. * @param message - identified prompt content and the source that supplied it. - * @throws AgentClosedError after disposal begins; no input is inserted. + * @throws AgentClosedError once the Agent lifetime is aborted. The inbox is unchanged. */ followup(message: UserMessage): void @@ -259,7 +259,7 @@ declare module './types.ts' { * A rejected step leaves steering parked in the inbox until the next * wake; cancellation or disposal may discard pending steering. * @param message - identified steering content and the source that supplied it. - * @throws AgentClosedError after disposal begins; no input is inserted. + * @throws AgentClosedError once the Agent lifetime is aborted. The inbox is unchanged. */ steer(message: UserMessage): void @@ -270,7 +270,7 @@ declare module './types.ts' { * wakes them. It may miss a request whose pre-step already claimed its * batch. Cancellation or disposal may discard pending context. * @param message - identified injected context and the source that supplied it. - * @throws AgentClosedError after disposal begins; no input is inserted. + * @throws AgentClosedError once the Agent lifetime is aborted. The inbox is unchanged. */ inject(message: UserMessage): void } diff --git a/packages/sdk/server/src/server.ts b/packages/sdk/server/src/server.ts index bccfa6634e..46cda8d44b 100644 --- a/packages/sdk/server/src/server.ts +++ b/packages/sdk/server/src/server.ts @@ -263,9 +263,8 @@ export class HarnessSdkJsonRpcServer { if (!this.initialized) throw new Error('SDK server is not initialized') const rec = await this.getOrCreateSession(params.sessionId) this.lifetime.signal.throwIfAborted() - // An agent-loop-only reload disposes the loop's agents while this record - // survives; a retained agent accepts followup() silently, so validate the - // record against the live registry before delivery. + // An agent-loop reload can dispose the Agent while the SDK still retains + // this record. Reject stale records before admitting prompt attachments. this.assertLiveAgent(rec, params.sessionId) const content = await durablePromptContent(this.ctx, params.contentBlocks) this.lifetime.signal.throwIfAborted() diff --git a/packages/subagent/subagent/src/manager.ts b/packages/subagent/subagent/src/manager.ts index f4a2a3d652..1f9108edae 100644 --- a/packages/subagent/subagent/src/manager.ts +++ b/packages/subagent/subagent/src/manager.ts @@ -10,8 +10,7 @@ import { randomUUID } from 'node:crypto' import { isAbsolute, resolve } from 'node:path' import type {} from '@deepseek-ai/dsh-working-directory' import type { Context } from '@deepseek-ai/cordis' -import type { Agent, AgentOptions, CreateAgentOptions } from '@deepseek-ai/dsh-agent' -import { AgentClosedError } from '@deepseek-ai/dsh-agent' +import { AgentClosedError, type Agent, type AgentOptions, type CreateAgentOptions } from '@deepseek-ai/dsh-agent' import { brandString } from '@deepseek-ai/dsh-brand' import { ReasoningEffortId, contentHasImage, createUserMessage, errorChain } from '@deepseek-ai/dsh-llm' import type { ContentBlock, MessageId, MessageSource } from '@deepseek-ai/dsh-llm' diff --git a/scripts/smoke-python-runtime.py b/scripts/smoke-python-runtime.py index d9baf1d7f9..92611f23bc 100644 --- a/scripts/smoke-python-runtime.py +++ b/scripts/smoke-python-runtime.py @@ -1787,7 +1787,6 @@ def smoke_sdk_snapshot_mode( {"insert": [{ "id": "serial-created-fixture", "name": str(Path(__file__).resolve().parents[1] / "packages/core/agent-loop/tests/fixtures/serial-created.mjs"), - "config": {"checkRootCancellation": False}, }]}, ]) with DeepSeekHarness( diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/result.json b/scripts/snapshots/python-sdk-single-exe/advanced/result.json index c5568f5a18..8aaf44b0b6 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/result.json +++ b/scripts/snapshots/python-sdk-single-exe/advanced/result.json @@ -2941,7 +2941,7 @@ "content": [ { "type": "text", - "text": "Serial agent creation completed. Queue-clear failures still cancel active work." + "text": "Serial agent creation completed." } ], "source": { @@ -3127,7 +3127,7 @@ "content": [ { "type": "text", - "text": "Serial agent creation completed. Queue-clear failures still cancel active work." + "text": "Serial agent creation completed." } ], "source": { @@ -3967,7 +3967,7 @@ "content": [ { "type": "text", - "text": "Serial agent creation completed. Queue-clear failures still cancel active work." + "text": "Serial agent creation completed." } ], "source": { @@ -4149,7 +4149,7 @@ "content": [ { "type": "text", - "text": "Serial agent creation completed. Queue-clear failures still cancel active work." + "text": "Serial agent creation completed." } ], "source": { diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.1.v4.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.1.v4.jsonl index 1a64fcd142..ee9fbfd3cb 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.1.v4.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.1.v4.jsonl @@ -10,7 +10,7 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Cancellation checkpoint probe."}],"source":{"kind":"plugin:serial-created-fixture"}}]}} {"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[],"outcome":"canceled"}} -{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed. Queue-clear failures still cancel active work."}],"source":{"kind":"plugin:serial-created-fixture"}}]}} +{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin:serial-created-fixture"}}]}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"aborted","reason":{"kind":"user"}}}} {"type":"turn/start","data":{"turn":2}} @@ -18,7 +18,7 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"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":"{{messageId}}"}},"surfaceOp":"append"} -{"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed. Queue-clear failures still cancel active work."}],"source":{"kind":"plugin:serial-created-fixture"}},"surfaceOp":"append"} +{"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin:serial-created-fixture"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"\nThe following information describes your current session:\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it.\n"}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","reasoningEffort":"high","maxTokens":256000},"adapterDefaults":{"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","snapshot_double","subagent","workflow","working_directory"]},"reason":"initial"}} diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.2.v4.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.2.v4.jsonl index 7f4bf22ad4..6fb5ebae1f 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.2.v4.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.2.v4.jsonl @@ -10,7 +10,7 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Cancellation checkpoint probe."}],"source":{"kind":"plugin:serial-created-fixture"}}]}} {"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[],"outcome":"canceled"}} -{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed. Queue-clear failures still cancel active work."}],"source":{"kind":"plugin:serial-created-fixture"}}]}} +{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin:serial-created-fixture"}}]}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"aborted","reason":{"kind":"user"}}}} {"type":"turn/start","data":{"turn":2}} @@ -18,7 +18,7 @@ {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"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":"{{messageId}}"}},"surfaceOp":"append"} -{"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed. Queue-clear failures still cancel active work."}],"source":{"kind":"plugin:serial-created-fixture"}},"surfaceOp":"append"} +{"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin:serial-created-fixture"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"\nThe following information describes your current session:\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it.\n"}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","reasoningEffort":"high","maxTokens":256000},"adapterDefaults":{"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","snapshot_double","subagent","workflow","working_directory"]},"reason":"initial"}}