refactor: remove per-followup result attribution
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/subagent/subagent-dsh-sdk/README.md
|
||||
README.md: e904ce3c09a1b44f8f5a0072b9ca85812898e74f
|
||||
README.zh.md: cec89ad9a65c68163fc136fe7b04cb135c57fb51
|
||||
README.md: 834c967354610e9ecbb76380f8ddbce988b51c8c
|
||||
README.zh.md: 4af03a6647da38adf8283d567b37a10203ad7c90
|
||||
|
||||
@@ -10,13 +10,13 @@ The SDK provider runs each subagent as a complete DeepSeek Harness runtime in a
|
||||
|
||||
The working directory resolves exactly like the ACP backend, through the seam's shared out-of-process helpers ([`dsh-subagent`](../subagent/README.md)): the configured `cwd` override when set (validated once at load), else the delegating parent session's cwd — never the server process's own cwd. The resolved path becomes the child process cwd and the workspace cwd of its SDK session.
|
||||
|
||||
The returned run id is minted in the parent namespace; the child runtime's session id exists only inside the child process. After publication the provider runs one SDK turn and reads the child's answer from its session events: the last complete `assistant/message`, or the `text-delta` stream accumulated so far when the turn was cut short — a partial answer survives cancel and error paths.
|
||||
The returned run id is minted in the parent namespace; the child runtime's session id exists only inside the child process. After publication the provider owns one SDK activity and reads the child's answer from its session events: the last complete `assistant/message`, or the `text-delta` stream accumulated before the activity was cut short — a partial answer survives cancel and error paths.
|
||||
|
||||
`dispose()` is idempotent: it settles the result locally as `aborted` (there is no wire-level prompt cancel), then closes the runtime — a bounded protocol `shutdown` request followed by the shared stdin-EOF → SIGTERM → SIGKILL ladder to actual exit.
|
||||
|
||||
## Stop-reason mapping
|
||||
|
||||
The child reports its turn outcome as a structured `TurnEndReason` on `session.finished`; the provider maps it into the seam vocabulary. `completed` → `completed`, `max-tokens` → `max-tokens`, `aborted` → `aborted`; everything else — `error`, `interrupted`, `disposed`, a future variant, or a turn that never ran — maps to `error`, so an unclean stop is never reported as success. Transport-level failures after publication flatten to `stopReason: 'error'` through the `onError` diagnostic sink (wired to `ctx.logger.warn`); the seam contract forbids `result` rejecting.
|
||||
The SDK client returns an owned child activity rather than a prompt result. The provider reads the last durable `turn/end` inside that activity and maps it into the seam vocabulary: `completed` → `completed`, `max-tokens` → `max-tokens`, `aborted` → `aborted`; everything else — `error`, `interrupted`, `disposed`, a future variant, or an activity with no turn — maps to `error`, so an unclean stop is never reported as success. Transport-level failures after publication flatten to `stopReason: 'error'` through the `onError` diagnostic sink (wired to `ctx.logger.warn`); the seam contract forbids `result` rejecting.
|
||||
|
||||
## Capabilities and context
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@ SDK 提供方会在全新的子进程中把每个 subagent 作为完整的 DeepS
|
||||
|
||||
工作目录的解析与 ACP 后端完全一致,并使用 seam 共享的进程外辅助工具([`dsh-subagent`](../subagent/README.md)):设置了 `cwd` 覆盖值时使用该值(加载时校验一次),否则使用发起委派的父会话 cwd,绝不使用服务器进程自身的 cwd。解析出的路径同时成为子进程 cwd 和其 SDK 会话的工作区 cwd。
|
||||
|
||||
返回的 run id 在父级命名空间中生成;子运行时的会话 id 只存在于子进程内部。发布后,提供方运行一个 SDK 轮次,并从子会话事件中读取答案:最后一条完整的 `assistant/message`,或轮次被截断时已累积的 `text-delta` 流;部分答案在取消和错误路径上都得以保留。
|
||||
返回的 run id 在父级命名空间中生成;子运行时的会话 id 只存在于子进程内部。发布后,提供方拥有一段 SDK 活动,并从子会话事件中读取答案:最后一条完整的 `assistant/message`,或该活动中断前已经累积的 `text-delta` 流;部分答案在取消和错误路径上都得以保留。
|
||||
|
||||
`dispose()`(资源释放)是幂等的:先在本地把结果确定为 `aborted`(协议层面没有提示词取消机制),再关闭运行时,即先发出一次有界的协议 `shutdown` 请求,随后通过共享的 stdin-EOF → SIGTERM → SIGKILL 阶梯使进程实际退出。
|
||||
|
||||
## 停止原因映射
|
||||
|
||||
子进程在 `session.finished` 上以结构化 `TurnEndReason` 报告轮次结果;提供方将其映射为 seam 词汇。`completed` → `completed`,`max-tokens` → `max-tokens`,`aborted` → `aborted`;其余情况,包括 `error`、`interrupted`、`disposed`、未来变体或根本未运行轮次,均映射为 `error`,因此非正常停止绝不会报告为成功。发布后的传输层失败会通过 `onError` 诊断接收器(连接到 `ctx.logger.warn`)压平为 `stopReason: 'error'`;seam 契约禁止 `result` 被拒绝。
|
||||
SDK 客户端返回自有子活动,而不是提示词结果。提供方读取该活动内最后一个持久 `turn/end`,并将其映射为 seam 词汇:`completed` → `completed`,`max-tokens` → `max-tokens`,`aborted` → `aborted`;其余情况,包括 `error`、`interrupted`、`disposed`、未来变体或不含轮次的活动,均映射为 `error`,因此非正常停止绝不会报告为成功。发布后的传输层失败会通过 `onError` 诊断接收器(连接到 `ctx.logger.warn`)压平为 `stopReason: 'error'`;seam 契约禁止 `result` 被拒绝。
|
||||
|
||||
## 能力与上下文
|
||||
|
||||
|
||||
@@ -70,8 +70,8 @@ export const DEFAULT_SHUTDOWN_TIMEOUT_MS = 1_000
|
||||
|
||||
/**
|
||||
* Map a child turn-end reason to a harness {@link SubagentStopReason}.
|
||||
* @param reason - the `session.finished` reason, or `undefined` when the
|
||||
* child settled without running a turn.
|
||||
* @param reason - the owned child run's final durable turn reason, or
|
||||
* `undefined` when it settled without running a turn.
|
||||
* @returns the harness equivalent; an absent or unknown reason maps to
|
||||
* `error`, so an unclean stop is never reported as `completed`.
|
||||
*/
|
||||
@@ -191,7 +191,10 @@ export async function startSdkRun(request: SubagentStartRequest, spec: SdkRunSpe
|
||||
cancelSettled.then(() => 'cancelled' as const),
|
||||
])
|
||||
if (turn === 'cancelled') return { output: collectOutput(), stopReason: 'aborted' }
|
||||
return { output: collectOutput(), stopReason: sdkStopReason(turn.reason) }
|
||||
const lastEnd = turn.events.findLast(
|
||||
(event): event is Extract<SessionEvent, { type: 'turn/end' }> => event.type === 'turn/end',
|
||||
)
|
||||
return { output: collectOutput(), stopReason: sdkStopReason(lastEnd?.data.reason) }
|
||||
},
|
||||
collectOutput,
|
||||
cancelled: () => flags.cancelled,
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/subagent/subagent-inprocess/README.md
|
||||
README.md: 980bc18de088c41dfe2f57a5ff0882a60892fc9f
|
||||
README.zh.md: 1ceb628371c3ae9cee6d8afa6bc1d95ba4cda8ae
|
||||
README.md: 67f0cf5dd1ecb18542af56953a0eaa40988aca0d
|
||||
README.zh.md: 58998ce06cd724d6a1f96f08ba31c5da30007b1b
|
||||
|
||||
@@ -14,10 +14,12 @@ The driver follows this sequence:
|
||||
2. Call `parent.ctx.agents.create` directly, passing the required request signal into the factory's creation transaction.
|
||||
3. During that transaction's unpublished setup window, install the requested persona, tool restriction, and structured-output runtime.
|
||||
4. Publish the child, retain the returned `AgentHandle`, and drive one task with `child.followup(prompt)` followed by `child.whenIdle()`.
|
||||
5. Read the child's own last assistant message and latest message-triggered turn reason, excluding any fork seed and later between-turn records.
|
||||
5. Read the child's own last assistant message and final durable turn reason from the complete owned child run, excluding any fork seed.
|
||||
|
||||
The child gets the parent's working-directory/session lineage and inherits the parent provider, model, and output-token cap unless `request.agentOptions` overrides them. It gets a fresh flat registration scope: parent ownership does not import parent tool restrictions or establish an authority subset.
|
||||
|
||||
This result boundary is valid because the provider owns an isolated child lifecycle from publication through quiescence. Steering submitted during that lifecycle belongs to the child run; the provider does not pretend the initial follow-up alone owns its output.
|
||||
|
||||
When the optional sandbox-policy or approval service is composed, the driver snapshots the parent's explicit session override before child creation and appends a source-tagged event during unpublished setup, after any fork history and before session publication. It never copies deployment defaults or one-shot grants; later child switches still win. See the [policy-inheritance decision](../../../.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md).
|
||||
|
||||
## Cancellation and ownership
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
2. 直接调用 `parent.ctx.agents.create`,把必需的请求信号传入工厂的创建事务。
|
||||
3. 在该事务未发布的设置窗口中,安装请求的 persona、工具限制和结构化输出运行时。
|
||||
4. 发布子 agent,保留返回的 `AgentHandle`,并通过先调用 `child.followup(prompt)`、再调用 `child.whenIdle()` 来驱动一项任务。
|
||||
5. 读取子 agent 自身最后一条 assistant 消息,以及由消息触发的最新轮次原因;排除任何 fork 初始内容和后续轮次间记录。
|
||||
5. 从完整的自有子运行中读取子 agent 自身最后一条 assistant 消息和最终持久轮次原因,并排除任何 fork 初始内容。
|
||||
|
||||
子 agent 会获得父 agent 的工作目录/会话谱系;除非 `request.agentOptions` 覆盖,否则还会继承父 agent 的提供方、模型和输出 token 上限。它获得全新的扁平注册作用域:父级所有权不会导入父 agent 的工具限制,也不会建立权限子集。
|
||||
|
||||
该结果边界成立,是因为提供方拥有从发布到完全停稳的隔离子 agent 生命周期。在该生命周期内提交的 steering(中途引导)属于子运行;提供方不会声称输出只归初始 follow-up 所有。
|
||||
|
||||
当组合中挂载了可选的沙箱策略或审批服务时,驱动器会在创建子 agent 前对父级的显式会话覆盖项获取快照,并在未发布的设置阶段追加一条带来源标记的事件,使其位于所有 fork 历史之后、会话发布之前。它绝不复制部署默认值或一次性授权;子 agent 后续的切换仍然优先。参见[策略继承决策](../../../.agents/notes/implemented/feature/2026-07-25-subagent-policy-inheritance.md)。
|
||||
|
||||
## 取消与所有权
|
||||
|
||||
Reference in New Issue
Block a user