mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
fix(jobs): read job output at settlement only for notices that inline it
- tool-jobs: inlining is decided before any read from the job's whole stream, so larger jobs keep their output and retention in the registry; a capped inline notice keeps the output's tail; job_output withdraws a pending notice from the agent now registered for the owner session - subagent: track the relay through logged tool calls, which tool policy cannot skip - snapshots: order the background-job-notice settlement with a sentinel file
This commit is contained in:
1 parent
111a6b4965
commit
ed645016cd
17 files changed
+189
-113
No files matched your search
@@ -3806,10 +3806,10 @@ export interface Config {
|
||||
*/
|
||||
maxConsecutiveWakes?: number
|
||||
/**
|
||||
* Largest complete completion notice, in UTF-8 bytes, that carries the
|
||||
* job's unread output (default 2048); a larger one leaves that output for
|
||||
* `job_output`. The producer's `outputLimitBytes` also bounds it, and 0
|
||||
* never inlines output.
|
||||
* Largest output stream, in UTF-8 bytes counted from the job's start, that a
|
||||
* completion notice carries inline with the job's final result (default
|
||||
* 2048); a job that wrote more leaves its output for `job_output`. The producer's `outputLimitBytes`
|
||||
* bounds the complete notice, and 0 never inlines output.
|
||||
*/
|
||||
inlineOutputBytes?: number
|
||||
}
|
||||
|
||||
@@ -3808,10 +3808,10 @@ export interface Config {
|
||||
*/
|
||||
maxConsecutiveWakes?: number
|
||||
/**
|
||||
* Largest complete completion notice, in UTF-8 bytes, that carries the
|
||||
* job's unread output (default 2048); a larger one leaves that output for
|
||||
* `job_output`. The producer's `outputLimitBytes` also bounds it, and 0
|
||||
* never inlines output.
|
||||
* Largest output stream, in UTF-8 bytes counted from the job's start, that a
|
||||
* completion notice carries inline with the job's final result (default
|
||||
* 2048); a job that wrote more leaves its output for `job_output`. The producer's `outputLimitBytes`
|
||||
* bounds the complete notice, and 0 never inlines output.
|
||||
*/
|
||||
inlineOutputBytes?: number
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`subagent`](../packages/subagent/subagent), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/experimental/webhook) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` |
|
||||
@@ -76,7 +76,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:209`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:165`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
|
||||
| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:177`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
|
||||
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:154`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:154`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:191`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
|
||||
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:199`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) |
|
||||
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:157`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` |
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:288`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`subagent`](../packages/subagent/subagent), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) |
|
||||
| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:320`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) |
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:337`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/experimental/webhook) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:353`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:280`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` |
|
||||
@@ -78,7 +78,7 @@
|
||||
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:209`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) |
|
||||
| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:165`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` |
|
||||
| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:177`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
|
||||
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:154`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:154`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) |
|
||||
| `tools/ptc-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:191`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
|
||||
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:199`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`subagent`](../packages/subagent/subagent), [`tool-present`](../packages/deliverables/tool-present) |
|
||||
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:157`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` |
|
||||
|
||||
@@ -21,11 +21,11 @@
|
||||
en: e9e9cb2ee5cea462
|
||||
zh: 833beb2bca84bd6a
|
||||
/deepseek-ai-dsh-tool-jobs/use-this-package/completion-notices:
|
||||
en: d39d17ea069c8efd
|
||||
zh: 20d9b35959cba853
|
||||
en: aed5691311860527
|
||||
zh: 09fd2d052a3bc5c9
|
||||
/deepseek-ai-dsh-tool-jobs/use-this-package/minimal-configuration:
|
||||
en: 7cce8568898e2dd8
|
||||
zh: 2979e3584d4ee34d
|
||||
en: d4afc6bc590bd3af
|
||||
zh: ba1ab1b04eddbda0
|
||||
/deepseek-ai-dsh-tool-jobs/use-this-package/what-can-go-wrong:
|
||||
en: a09c5c8225dec06c
|
||||
zh: 3e9fcab42613775e
|
||||
@@ -42,8 +42,8 @@
|
||||
en: ca18e4bdfb12a1e2
|
||||
zh: 6592697c561c6e6d
|
||||
/deepseek-ai-dsh-tool-jobs/understand-the-implementation/notice-delivery-lanes:
|
||||
en: eaf309257bd77534
|
||||
zh: 1f0b7119676c9db7
|
||||
en: 1e1545305716c2ff
|
||||
zh: d963684075bda668
|
||||
/deepseek-ai-dsh-tool-jobs/further-exploration:
|
||||
en: 11250fc631d55d99
|
||||
zh: 7885ce173bd1cd5f
|
||||
|
||||
@@ -37,7 +37,7 @@ The three tools return `{ text, job }`, `PublicJobSnapshot[]`, and `{ outcome: '
|
||||
|
||||
### Completion notices
|
||||
|
||||
When a job finishes, the owning agent receives `background job <id> (<kind>: <label>) finished [status: ...]. Output:` followed by the output it has not read, or `. No new output.` when there is none. A notice that would exceed `inlineOutputBytes` (2048 bytes by default) or the producer's `outputLimitBytes` ends `. Read its output with job_output.` instead. The label is its first line, bounded to 120 characters. A `job_output` call that collects the job while its notice is still pending withdraws the notice, and a notice that never reaches the model leaves its output for `job_output`. A busy agent has the notice injected into its next step — the turn cannot close while the inbox holds it, so several jobs settling together cost one step rather than one turn each. An idle agent is instead woken with a follow-up turn, because an unclaimed notice is a completion the model never learns about. Completions the model already collected get no notice: the registry reports a settlement that released a live `wait` as `awaited` — whether a `job_output` wait or a shell tool waiting on its own foreground command — and the plugin remembers the kills the model requested through `job_kill`; a settlement caused by owner or service teardown is skipped because nobody is left to read it.
|
||||
When a job finishes, the owning agent receives `background job <id> (<kind>: <label>) finished [status: ...]. Output:` followed by the output it has not read and the job's final result, or `. No new output.` when there is none. A job that wrote more than `inlineOutputBytes` (2048 bytes by default) gets a notice ending `. Read its output with job_output.` instead, and its output stays in the registry. The final result is carried whole, as `job_output` would return it, so a large result makes a large notice; the producer's `outputLimitBytes` keeps the tail of an inline notice's output, as it does for `job_output`. The label is its first line, bounded to 120 characters. A `job_output` call that collects the job while its notice is still pending withdraws the notice, and a notice that never reaches the model leaves its output for `job_output`. A busy agent has the notice injected into its next step — the turn cannot close while the inbox holds it, so several jobs settling together cost one step rather than one turn each. An idle agent is instead woken with a follow-up turn, because an unclaimed notice is a completion the model never learns about. Completions the model already collected get no notice: the registry reports a settlement that released a live `wait` as `awaited` — whether a `job_output` wait or a shell tool waiting on its own foreground command — and the plugin remembers the kills the model requested through `job_kill`; a settlement caused by owner or service teardown is skipped because nobody is left to read it.
|
||||
|
||||
Waking is unbounded by default: an unattended agent that chains background commands and one-shot subagents is woken for every completion. `maxConsecutiveWakes` caps that: each owner may be woken that many times before further notices degrade to injection, and claiming any user-authored message restores the budget. The cap bounds the self-exciting chain — a woken turn may start the background job whose completion wakes it again — but a notice past it waits silently until the next user input, so a session that relies on wakes to finish its work stalls there. `completionDelivery: quiet` keeps even idle owners on the injection lane, which deterministic transcripts need.
|
||||
|
||||
@@ -55,7 +55,7 @@ Loading the plugin with no config is the common path; a `waitTimeoutMs` above `m
|
||||
| `maxWaitTimeoutMs` | `600,000` | Cap for model-supplied waits; larger values clamp down to it |
|
||||
| `completionDelivery` | `wakeup` | `wakeup` opens a turn on an idle owner; `quiet` leaves the notice pending |
|
||||
| `maxConsecutiveWakes` | unset | Turns one owner may open by wake before notices degrade to injection; unset means no cap |
|
||||
| `inlineOutputBytes` | `2048` | Largest complete notice that carries the job's unread output; `0` never inlines |
|
||||
| `inlineOutputBytes` | `2048` | Largest output stream a notice carries inline with the final result; `0` never inlines |
|
||||
|
||||
The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-tool-jobs) is the exhaustive source for every accepted field and its JSDoc.
|
||||
|
||||
@@ -94,7 +94,7 @@ This section explains the design decisions behind the tools and points at the co
|
||||
|
||||
The settlement subscription (`{ owners: 'scope' }`) skips settlements the registry reports as `awaited`, jobs the model killed through `job_kill`, unowned jobs, and teardown settlements, then resolves the agent registered for the owner session. A `wakeup` delivery opens a turn on an idle owner; with `maxConsecutiveWakes` set, only while the budget lasts, tracked per exact `Agent` in a `WeakMap`, and claiming a user-authored message (`agent/inbox/claimed`) resets that owner's budget. A busy owner — or any notice past a configured budget, or `quiet` delivery — is injected into the next-step inbox instead. The registry counts only a wait still owed the projection at settlement, so a wait that timed out or was aborted leaves a later settlement to notify as usual; a removal drops the job from the model-kill set, which only ever holds live jobs the model killed.
|
||||
|
||||
Each notice takes the job's one consuming read at settlement and holds it with the notice's message id. When an inline notice is logged as a `user/message` (`session/event`), the held read is spent; a notice claimed by a step that is then rejected or cancelled is never logged, so its read stays held. Otherwise `job_output` returns the held read ahead of its own read and removes the notice from the owner's inbox while it is still pending. A job removal drops it.
|
||||
Whether a notice inlines is decided before any read, from the job's whole output stream (`JobView.output.total`) and the producer's cap. Only an inline notice takes the consuming read at settlement; it holds that read until the notice is logged as a `user/message` (`session/event`). A notice claimed by a step that is then rejected or cancelled is never logged, so `job_output` returns its held read. `job_output` also removes a pending notice from the inbox of the agent registered for the owner session. A job removal drops the entry, and a plugin reload drops a held read whose notice was never logged.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ kind: "package-reference"
|
||||
|
||||
### 完成通知
|
||||
|
||||
任务完成时,拥有它的 agent 会收到 `background job <id> (<kind>: <label>) finished [status: ...]. Output:`,其后是它尚未读取的输出;没有输出时则以 `. No new output.` 结尾。若通知会超过 `inlineOutputBytes`(默认 2048 字节)或生产方的 `outputLimitBytes`,则改以 `. Read its output with job_output.` 结尾。label 取其首行,上限 120 个字符。在通知仍待领时收走该任务的 `job_output` 调用会撤回该通知;从未抵达模型的通知会把输出留给 `job_output`。繁忙的 agent 会在下一步收到注入的通知——inbox 尚有内容时轮次无法结束,因此同时结算的多个任务只花掉一步,而不是各占一轮。空闲的 agent 则被一个 follow-up 轮次唤醒,因为无人领取的通知就是模型永远不会知道的完成。模型已经收走的完成不再通知:注册表把释放了存活 `wait` 的结算报告为 `awaited`——无论是 `job_output` 的等待,还是 shell 工具在等待自己的前台命令——插件则记着模型经 `job_kill` 请求的杀停;所有者或服务 teardown 导致的结算会被跳过,因为没有人能读它。
|
||||
任务完成时,拥有它的 agent 会收到 `background job <id> (<kind>: <label>) finished [status: ...]. Output:`,其后是它尚未读取的输出和任务的最终结果;没有输出时则以 `. No new output.` 结尾。写出超过 `inlineOutputBytes`(默认 2048 字节)输出的任务,其通知改以 `. Read its output with job_output.` 结尾,输出保留在注册表中。最终结果按 `job_output` 返回的样子完整携带,因此结果较大时通知也较大;生产方的 `outputLimitBytes` 会保留内联通知输出的尾部,与 `job_output` 的做法相同。label 取其首行,上限 120 个字符。在通知仍待领时收走该任务的 `job_output` 调用会撤回该通知;从未抵达模型的通知会把输出留给 `job_output`。繁忙的 agent 会在下一步收到注入的通知——inbox 尚有内容时轮次无法结束,因此同时结算的多个任务只花掉一步,而不是各占一轮。空闲的 agent 则被一个 follow-up 轮次唤醒,因为无人领取的通知就是模型永远不会知道的完成。模型已经收走的完成不再通知:注册表把释放了存活 `wait` 的结算报告为 `awaited`——无论是 `job_output` 的等待,还是 shell 工具在等待自己的前台命令——插件则记着模型经 `job_kill` 请求的杀停;所有者或服务 teardown 导致的结算会被跳过,因为没有人能读它。
|
||||
|
||||
唤醒默认不设上限:无人值守的 agent 连续串起后台命令与一次性 subagent 时,每次完成都会唤醒它。`maxConsecutiveWakes` 可以封顶:每个所有者最多被唤醒这么多次,此后的通知降级为注入;领取任何用户撰写的消息都会恢复预算。上限用来约束自激链——被唤醒的一轮可能启动某个后台任务,而它的完成又会唤醒同一个所有者——但超出上限的通知会静默等到用户下一次输入,依赖唤醒完成工作的会话会停在那里。`completionDelivery: quiet` 让空闲所有者也在注入通道上,确定性 transcript(文本记录)需要的正是这一点。
|
||||
|
||||
@@ -55,7 +55,7 @@ kind: "package-reference"
|
||||
| `maxWaitTimeoutMs` | `600,000` | 模型所给等待时间的上限;更大的值向下收敛到它 |
|
||||
| `completionDelivery` | `wakeup` | `wakeup` 为空闲所有者开启一轮;`quiet` 让通知继续待领 |
|
||||
| `maxConsecutiveWakes` | 未设置 | 一个所有者可由唤醒开启的轮数,超出后通知降级为注入;未设置即不封顶 |
|
||||
| `inlineOutputBytes` | `2048` | 携带任务未读输出的完整通知的最大长度;`0` 表示从不内联 |
|
||||
| `inlineOutputBytes` | `2048` | 通知连同最终结果内联携带的输出流的最大长度;`0` 表示从不内联 |
|
||||
|
||||
生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-tool-jobs)是每个受支持字段及其 JSDoc 的穷尽式真源。
|
||||
|
||||
@@ -94,7 +94,7 @@ kind: "package-reference"
|
||||
|
||||
结算订阅(`{ owners: 'scope' }`)跳过注册表报告为 `awaited` 的结算、模型经 `job_kill` 杀停的任务、无所有者的任务与 teardown 结算,然后解析登记在拥有者会话下的 agent。`wakeup` 投递为空闲所有者开启一轮;设置了 `maxConsecutiveWakes` 时只在预算内开启,按确切 `Agent` 记录在 `WeakMap` 中,领取用户撰写的消息(`agent/inbox/claimed`)会重置该所有者的预算。繁忙的所有者——或超出所配预算的任何通知,以及 `quiet` 投递——改为注入 next-step inbox。注册表只计入结算时仍被欠着投影的等待,因此超时或中止的等待会让之后的结算照常通知;移除会把任务从模型杀停集合里删掉,该集合只保有模型杀停过的存活任务。
|
||||
|
||||
每条通知在结算时取走该任务唯一的一次消费式读取,并以通知的消息 id 暂存。内联通知被记录为 `user/message`(`session/event`)时,暂存的读取即被用掉;被某一步领取、但该步随后被拒绝或取消的通知不会被记录,因此其读取仍保持暂存。否则 `job_output` 会先于自己的读取返回暂存的读取,并在通知仍待领时将其从所有者的 inbox 中移除。任务移除会丢弃它。
|
||||
通知是否内联在任何读取之前决定,依据是任务的完整输出流(`JobView.output.total`)和生产方的上限。只有内联通知会在结算时取走消费式读取;它暂存该读取,直到通知被记录为 `user/message`(`session/event`)。被某一步领取、但该步随后被拒绝或取消的通知不会被记录,因此 `job_output` 会返回其暂存的读取。`job_output` 还会将待领通知从为所有者会话注册的 agent 的 inbox 中移除。任务移除会丢弃该条目;插件重新加载会丢弃通知从未被记录的暂存读取。
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
* collected to the owning agent: injected into a busy owner's next step, or
|
||||
* opening a turn on an idle one under the default `wakeup` delivery, unbounded
|
||||
* unless `maxConsecutiveWakes` caps it per owner. A notice carries the job's
|
||||
* unread output when it fits `inlineOutputBytes`, and `job_output` withdraws a
|
||||
* notice still pending when it returns the same output.
|
||||
* output when its stream fits `inlineOutputBytes`, and `job_output` withdraws
|
||||
* a notice still pending.
|
||||
* @module @deepseek-ai/dsh-tool-jobs
|
||||
*/
|
||||
|
||||
@@ -57,10 +57,10 @@ export interface Config {
|
||||
*/
|
||||
maxConsecutiveWakes?: number
|
||||
/**
|
||||
* Largest complete completion notice, in UTF-8 bytes, that carries the
|
||||
* job's unread output (default 2048); a larger one leaves that output for
|
||||
* `job_output`. The producer's `outputLimitBytes` also bounds it, and 0
|
||||
* never inlines output.
|
||||
* Largest output stream, in UTF-8 bytes counted from the job's start, that a
|
||||
* completion notice carries inline with the job's final result (default
|
||||
* 2048); a job that wrote more leaves its output for `job_output`. The producer's `outputLimitBytes`
|
||||
* bounds the complete notice, and 0 never inlines output.
|
||||
*/
|
||||
inlineOutputBytes?: number
|
||||
}
|
||||
@@ -142,18 +142,39 @@ function noticeDetail(job: JobView): string {
|
||||
return ` (${job.kind}: ${label}) finished ${statusLine(publicJob(job))}`
|
||||
}
|
||||
|
||||
const INLINE_OMITTED = '\n[output truncated]'
|
||||
|
||||
/**
|
||||
* The notice that carries the job's unread output, when the complete text fits.
|
||||
* Whether a notice can carry the job's output, decided before any read: its
|
||||
* whole output stream fits `maxBytes`, and the producer's cap leaves room for
|
||||
* the notice head and the truncation marker.
|
||||
* @param job - the settled job.
|
||||
* @param maxBytes - the configured inline bound on the output stream.
|
||||
* @returns whether the notice takes the consuming read and carries it.
|
||||
*/
|
||||
function inlinesOutput(job: JobView, maxBytes: number): boolean {
|
||||
if (maxBytes === 0 || job.output.total > maxBytes) return false
|
||||
const limit = job.outputLimitBytes
|
||||
return limit === undefined
|
||||
|| encoder.encode(`background job ${job.id}${noticeDetail(job)}. Output:\n${INLINE_OMITTED}`).byteLength <= limit
|
||||
}
|
||||
|
||||
/**
|
||||
* The notice that carries the job's output and final result. The producer's
|
||||
* cap keeps the tail of the output, as it does for `job_output`.
|
||||
* @param job - the settled job.
|
||||
* @param output - the consuming read taken at settlement.
|
||||
* @param maxBytes - the configured inline bound.
|
||||
* @returns the notice text, or undefined when it exceeds either bound.
|
||||
* @returns the notice text.
|
||||
*/
|
||||
function inlineCompletionNotice(job: JobView, output: string, maxBytes: number): string | undefined {
|
||||
const body = output.endsWith('\n') ? output.slice(0, -1) : output
|
||||
const text = `background job ${job.id}${noticeDetail(job)}${body.length === 0 ? '. No new output.' : `. Output:\n${body}`}`
|
||||
const bytes = encoder.encode(text).byteLength
|
||||
return bytes <= maxBytes && bytes <= (job.outputLimitBytes ?? Infinity) ? text : undefined
|
||||
function inlineCompletionNotice(job: JobView, output: string): string {
|
||||
const head = `background job ${job.id}${noticeDetail(job)}`
|
||||
if (output.length === 0) return `${head}. No new output.`
|
||||
const prefix = `${head}. Output:\n`
|
||||
const limit = job.outputLimitBytes
|
||||
const body = output.replace(/\n$/, '')
|
||||
return limit === undefined
|
||||
? `${prefix}${body}`
|
||||
: `${prefix}${fitWithSuffix(body, '', limit - encoder.encode(prefix).byteLength, INLINE_OMITTED)}`
|
||||
}
|
||||
|
||||
function fitCompletionNotice(job: JobView): string {
|
||||
@@ -217,14 +238,12 @@ function presentJobCall(title: string, kind: 'read' | 'execute', rawInput?: stri
|
||||
return { card: 'generic', title, kind, ...rawInput !== undefined ? { rawInput } : {} }
|
||||
}
|
||||
|
||||
/** A settlement read held for the model; see the `held` map in {@link apply}. */
|
||||
interface HeldRead {
|
||||
readonly owner: Agent
|
||||
/** The notice that reported the settlement. */
|
||||
/** A completion notice not yet logged; see the `pending` map in {@link apply}. */
|
||||
interface PendingNotice {
|
||||
readonly owner: NonNullable<JobView['owner']>
|
||||
readonly messageId: MessageId
|
||||
readonly text: string
|
||||
/** Whether the notice carries `text`. */
|
||||
readonly inline: boolean
|
||||
/** The consuming read an inline notice carries. */
|
||||
readonly held?: string
|
||||
}
|
||||
|
||||
/** The consuming read as the model sees it: the delta, then the result once, then the status line. */
|
||||
@@ -256,13 +275,13 @@ export function apply(ctx: Context, config: Config): void {
|
||||
throw new Error(`tool-jobs: maxConsecutiveWakes (${wakeBudget}) must be a whole number of turns`)
|
||||
}
|
||||
|
||||
// The consuming read each settlement notice took, until the model receives
|
||||
// it: through the notice's logged `user/message` when the notice carries it
|
||||
// inline, otherwise through `job_output`. A claimed notice can still miss
|
||||
// the model (a rejected or cancelled step), so only the logged message
|
||||
// spends it. `job_output` withdraws a notice still pending, which would only
|
||||
// Each notice still pending in its owner's inbox. An inline notice took
|
||||
// the job's consuming read at settlement and holds it until the notice is
|
||||
// logged as a `user/message`, the point it reaches the model; a notice
|
||||
// claimed by a step that is then rejected or cancelled leaves the read for
|
||||
// `job_output`. `job_output` withdraws a pending notice, which would only
|
||||
// repeat its result.
|
||||
const held = new Map<JobId, HeldRead>()
|
||||
const pending = new Map<JobId, PendingNotice>()
|
||||
|
||||
if (delivery === 'wakeup' && wakeBudget !== undefined) {
|
||||
ctx.on('agent/inbox/claimed', ({ agent, message }) => {
|
||||
@@ -273,8 +292,8 @@ export function apply(ctx: Context, config: Config): void {
|
||||
}
|
||||
ctx.on('session/event', (session, event) => {
|
||||
if (event.type !== 'user/message' || event.data.source.kind !== 'tool-jobs') return
|
||||
for (const [id, entry] of held) {
|
||||
if (entry.inline && entry.messageId === event.data.id && entry.owner.id === session.id) held.delete(id)
|
||||
for (const [id, notice] of pending) {
|
||||
if (notice.messageId === event.data.id && notice.owner === session.id) pending.delete(id)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -333,7 +352,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
ctx.jobs.events.subscribe({ owners: 'scope' }, (event) => {
|
||||
if (event.type === 'removed') {
|
||||
killedByModel.delete(event.job.id)
|
||||
held.delete(event.job.id)
|
||||
pending.delete(event.job.id)
|
||||
return
|
||||
}
|
||||
if (event.type !== 'settled') return
|
||||
@@ -344,12 +363,13 @@ export function apply(ctx: Context, config: Config): void {
|
||||
// absent here when it left before settlement — and then no inbox is left.
|
||||
const owner = ctx.get('agents')?.get(event.job.owner)
|
||||
if (owner === undefined) return
|
||||
const output = readBody(ctx.jobs.read(event.job.id, event.job.owner)).text
|
||||
const inline = inlineCompletionNotice(event.job, output, inlineBytes)
|
||||
const held = inlinesOutput(event.job, inlineBytes)
|
||||
? readBody(ctx.jobs.read(event.job.id, event.job.owner)).text
|
||||
: undefined
|
||||
const message = createUserMessage({
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: inline ?? fitCompletionNotice(event.job),
|
||||
text: held === undefined ? fitCompletionNotice(event.job) : inlineCompletionNotice(event.job, held),
|
||||
}],
|
||||
source: {
|
||||
kind: 'tool-jobs',
|
||||
@@ -357,7 +377,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
summary: completionSummary(event.job),
|
||||
},
|
||||
})
|
||||
held.set(event.job.id, { owner, messageId: message.id, text: output, inline: inline !== undefined })
|
||||
pending.set(event.job.id, { owner: event.job.owner, messageId: message.id, ...held === undefined ? {} : { held } })
|
||||
if (delivery === 'wakeup' && owner.status === 'idle') {
|
||||
if (wakeBudget === undefined) {
|
||||
owner.followup(message)
|
||||
@@ -410,12 +430,12 @@ export function apply(ctx: Context, config: Config): void {
|
||||
await jobs.wait(id, Math.min(args.timeout_ms ?? waitDefault, waitCap), exec.agent?.id, exec.signal)
|
||||
}
|
||||
const read = readBody(jobs.read(id, exec.agent?.id))
|
||||
const notice = held.get(id)
|
||||
const notice = pending.get(id)
|
||||
if (notice === undefined) return read
|
||||
held.delete(id)
|
||||
// This result repeats the notice's output, so a notice still pending goes.
|
||||
if (ctx.get('agents')?.get(notice.owner.id) === notice.owner) notice.owner.inbox.remove(notice.messageId)
|
||||
return { text: `${notice.text}${read.text}`, job: read.job }
|
||||
pending.delete(id)
|
||||
// This result covers everything the pending notice reports.
|
||||
ctx.get('agents')?.get(notice.owner)?.inbox.remove(notice.messageId)
|
||||
return { text: `${notice.held ?? ''}${read.text}`, job: read.job }
|
||||
},
|
||||
presentCall: args => presentJobCall(`Read output from background job ${args.job_id}`, 'read', args.job_id),
|
||||
}))
|
||||
|
||||
@@ -1266,19 +1266,57 @@ describe('completion notice output', () => {
|
||||
expect(text(await call(ctx, 'job_output', { job_id: id }, owner))).toBe('(no new output)\n[status: completed, exit code: 0]')
|
||||
})
|
||||
|
||||
it('leaves larger output for job_output', async () => {
|
||||
const { ctx } = await setup({ inlineOutputBytes: 40 })
|
||||
it('inlines a stream of exactly inlineOutputBytes, counted in UTF-8 bytes, and leaves a longer one unread', async () => {
|
||||
const { ctx } = await setup({ inlineOutputBytes: 9 })
|
||||
const inject = vi.fn()
|
||||
const owner = await fakeAgent(ctx, 'sess-1', { inject })
|
||||
const p = producer({ owner: owner.id, label: 'pnpm test' })
|
||||
const id = ctx.jobs.start(p.spec)
|
||||
p.append('ok 3 tests\n', { channel: 'stdout' })
|
||||
p.settle({ status: 'completed', detail: 'exit code: 0' })
|
||||
// Three characters, nine bytes: a character count would also admit the longer stream.
|
||||
const exact = producer({ owner: owner.id, label: 'exact' })
|
||||
const exactId = ctx.jobs.start(exact.spec)
|
||||
exact.append('日本語')
|
||||
exact.settle({ status: 'completed' })
|
||||
const over = producer({ owner: owner.id, label: 'over' })
|
||||
const overId = ctx.jobs.start(over.spec)
|
||||
over.append('日本語x')
|
||||
over.settle({ status: 'completed' })
|
||||
await tick()
|
||||
|
||||
expect(notice(inject).content[0].text).toBe(`background job ${id} (bash: pnpm test) finished [status: completed, exit code: 0]. Read its output with job_output.`)
|
||||
logged(ctx, owner, notice(inject))
|
||||
expect(text(await call(ctx, 'job_output', { job_id: id }, owner))).toBe('ok 3 tests\n[status: completed, exit code: 0]')
|
||||
expect(notice(inject, 0).content[0].text).toBe(`background job ${exactId} (bash: exact) finished [status: completed]. Output:\n日本語`)
|
||||
expect(notice(inject, 1).content[0].text).toBe(`background job ${overId} (bash: over) finished [status: completed]. Read its output with job_output.`)
|
||||
logged(ctx, owner, notice(inject, 1))
|
||||
// The registry kept the output the notice did not carry.
|
||||
expect(text(await call(ctx, 'job_output', { job_id: overId }, owner))).toBe('日本語x\n[status: completed]')
|
||||
})
|
||||
|
||||
it('never inlines when inlineOutputBytes is 0', async () => {
|
||||
const { ctx } = await setup({ inlineOutputBytes: 0 })
|
||||
const inject = vi.fn()
|
||||
const owner = await fakeAgent(ctx, 'sess-1', { inject })
|
||||
const p = producer({ owner: owner.id, label: 'quiet' })
|
||||
const id = ctx.jobs.start(p.spec)
|
||||
p.settle({ status: 'completed' })
|
||||
await tick()
|
||||
|
||||
expect(notice(inject).content[0].text).toBe(`background job ${id} (bash: quiet) finished [status: completed]. Read its output with job_output.`)
|
||||
})
|
||||
|
||||
it('bounds an inline notice by the producer cap and skips inlining when the cap cannot hold its head', async () => {
|
||||
const { ctx } = await setup()
|
||||
const inject = vi.fn()
|
||||
const owner = await fakeAgent(ctx, 'sess-1', { inject })
|
||||
const capped = producer({ owner: owner.id, kind: 'test-task', label: 'capped', outputLimitBytes: 140 })
|
||||
const cappedId = ctx.jobs.start(capped.spec)
|
||||
capped.settle({ status: 'completed', result: `${'r'.repeat(499)}z` })
|
||||
const tiny = producer({ owner: owner.id, kind: 'test-task', label: 'tiny', outputLimitBytes: 60 })
|
||||
const tinyId = ctx.jobs.start(tiny.spec)
|
||||
tiny.settle({ status: 'completed', result: 'answer' })
|
||||
await tick()
|
||||
|
||||
const cappedText = notice(inject, 0).content[0].text
|
||||
expect(Buffer.byteLength(cappedText)).toBeLessThanOrEqual(140)
|
||||
expect(cappedText).toMatch(new RegExp(`^background job ${cappedId} \\(test-task: capped\\) finished \\[status: completed\\]\\. Output:\\nr+z\\n\\[output truncated\\]$`))
|
||||
expect(notice(inject, 1).content[0].text).toBe(`background job ${tinyId}\nDone; job_output.`)
|
||||
expect(text(await call(ctx, 'job_output', { job_id: tinyId }, owner))).toContain('answer')
|
||||
})
|
||||
|
||||
it('withdraws a pending notice when job_output returns the same output', async () => {
|
||||
@@ -1333,19 +1371,34 @@ describe('completion notice output', () => {
|
||||
expect(text(await call(ctx, 'job_output', { job_id: id }, owner))).toBe('kept\n[status: completed]')
|
||||
})
|
||||
|
||||
it('leaves the inbox of an owner that is no longer registered', async () => {
|
||||
it('withdraws the notice from the agent now registered for the owner session', async () => {
|
||||
const { ctx } = await setup()
|
||||
const remove = vi.fn(() => true)
|
||||
const oldOwner = await fakeAgent(ctx, 'shared', { remove })
|
||||
const oldRemove = vi.fn(() => true)
|
||||
const inject = vi.fn()
|
||||
const oldOwner = await fakeAgent(ctx, 'shared', { inject, remove: oldRemove })
|
||||
const p = producer({ owner: oldOwner.id })
|
||||
const id = ctx.jobs.start(p.spec)
|
||||
p.settle({ status: 'completed' })
|
||||
await tick()
|
||||
|
||||
await detachAgent(oldOwner)
|
||||
const replacement = await fakeAgent(ctx, 'shared')
|
||||
const remove = vi.fn(() => true)
|
||||
const replacement = await fakeAgent(ctx, 'shared', { remove })
|
||||
expect(text(await call(ctx, 'job_output', { job_id: id }, replacement))).toBe('(no new output)\n[status: completed]')
|
||||
expect(remove).not.toHaveBeenCalled()
|
||||
expect(oldRemove).not.toHaveBeenCalled()
|
||||
expect(remove).toHaveBeenCalledWith(notice(inject).id)
|
||||
})
|
||||
|
||||
it('withdraws nothing when the owner session has no live agent', async () => {
|
||||
const { ctx } = await setup()
|
||||
const owner = await fakeAgent(ctx, 'gone')
|
||||
const p = producer({ owner: owner.id })
|
||||
const id = ctx.jobs.start(p.spec)
|
||||
p.settle({ status: 'completed' })
|
||||
await tick()
|
||||
|
||||
await detachAgent(owner)
|
||||
expect(text(await call(ctx, 'job_output', { job_id: id }, owner))).toBe('(no new output)\n[status: completed]')
|
||||
})
|
||||
|
||||
it('names a multi-line label by its first line', async () => {
|
||||
|
||||
@@ -63,8 +63,8 @@
|
||||
en: f7cec22ba9d3fb4f
|
||||
zh: 67b1d40f9b136f71
|
||||
/deepseek-ai-dsh-subagent/model-experience/settlement-notice/what-the-model-sees:
|
||||
en: 2305a255f1343108
|
||||
zh: a8db6c6dc7a7ce00
|
||||
en: 39093d8f3c69b6ce
|
||||
zh: 48cafaf6490db402
|
||||
/deepseek-ai-dsh-subagent/model-experience/settlement-notice/token-effect:
|
||||
en: f98ad834e8ce8950
|
||||
zh: cf509805dd934094
|
||||
|
||||
@@ -153,7 +153,7 @@ Read these pages when the package-level contract is not enough. They move from t
|
||||
|
||||
#### What the model sees
|
||||
|
||||
Parent delivery emits a user-role status notice containing the child’s nonempty final text blocks. When the child's last action before that text was a message to its parent, with no input claimed and no tool call started after it, the notice replaces them with `It sent you a message just before finishing, so its closing message is omitted.`, because that closing text usually restates the message. External notices also state that further messages are unsupported. Reasoning and other nontext blocks are excluded. Every parent notice includes available structured output as `Structured result: <JSON>` and the provider’s safe diagnostic as a separate text block. Empty closing text is reported as `It left no closing message.` Caller delivery emits no notice; SDK lifecycle notifications retain complete child output when cleanup succeeds.
|
||||
Parent delivery emits a user-role status notice containing the child’s nonempty final text blocks. When the child's last action before that text was a message to its parent, as the only tool call of its step with no input claimed and no tool call logged after it, the notice replaces them with `It sent you a message just before finishing, so its closing message is omitted.`; the parent recovers omitted closing text only by messaging the child again. External notices also state that further messages are unsupported. Reasoning and other nontext blocks are excluded. Every parent notice includes available structured output as `Structured result: <JSON>` and the provider’s safe diagnostic as a separate text block. Empty closing text is reported as `It left no closing message.` Caller delivery emits no notice; SDK lifecycle notifications retain complete child output when cleanup succeeds.
|
||||
|
||||
#### Token effect
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ kind: "package-reference"
|
||||
|
||||
#### 模型看到什么
|
||||
|
||||
父级投递发送包含子级最终输出中非空文本块的 user-role 状态通知。若子级在该文本之前的最后一个动作是向父级发送消息,且此后既未领取输入也未发起工具调用,通知会以 `It sent you a message just before finishing, so its closing message is omitted.` 替代这些文本块,因为收尾文本通常只是复述该消息。外部通知还说明不支持后续消息。推理及其他非文本块被排除。每条父级通知都会以 `Structured result: <JSON>` 附带可用的结构化输出,并以独立文本块附带提供方安全诊断。收尾文本为空时,通知使用 `It left no closing message.`。调用方投递不发送通知;清理成功时 SDK 生命周期通知保留完整子级输出。
|
||||
父级投递发送包含子级最终输出中非空文本块的 user-role 状态通知。若子级在该文本之前的最后一个动作是向父级发送消息,且该消息是其所在步骤唯一的工具调用、此后既未领取输入也未记录任何工具调用,通知会以 `It sent you a message just before finishing, so its closing message is omitted.` 替代这些文本块;父级只有再次向子级发送消息才能取回被省略的收尾文本。外部通知还说明不支持后续消息。推理及其他非文本块被排除。每条父级通知都会以 `Structured result: <JSON>` 附带可用的结构化输出,并以独立文本块附带提供方安全诊断。收尾文本为空时,通知使用 `It left no closing message.`。调用方投递不发送通知;清理成功时 SDK 生命周期通知保留完整子级输出。
|
||||
|
||||
#### Token 影响
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ interface ActivationBase {
|
||||
* message.
|
||||
*/
|
||||
relayed: boolean
|
||||
/** Tool calls started in the child's current step, which decides {@link relayed}. */
|
||||
/** Tool calls the child's current step logged, which decides {@link relayed}. */
|
||||
stepToolCalls: number
|
||||
}
|
||||
|
||||
|
||||
@@ -948,20 +948,21 @@ export class SubagentManager {
|
||||
if (activation.kind === 'local') {
|
||||
observeLocalFailure(activation, wakeSettlement)
|
||||
activation.handle.agent.ctx.on('agent/inbox/inserted', wakeSettlement)
|
||||
activation.handle.agent.ctx.on('agent/inbox/claimed', wakeSettlement)
|
||||
activation.handle.agent.ctx.on('agent/inbox/discarded', wakeSettlement)
|
||||
// Input, a later tool call, or another call in the same step as the
|
||||
// child's message to its parent means the closing text may report work
|
||||
// that message did not.
|
||||
activation.handle.agent.ctx.on('agent/inbox/claimed', () => { activation.relayed = false })
|
||||
activation.handle.agent.ctx.on('agent/pre-step', (_payload, next) => {
|
||||
activation.stepToolCalls = 0
|
||||
return next()
|
||||
})
|
||||
activation.handle.agent.ctx.on('tools/pre-execute', (_exec, next) => {
|
||||
activation.stepToolCalls += 1
|
||||
activation.handle.agent.ctx.on('agent/inbox/claimed', () => {
|
||||
// Claimed input means the closing text may report work the child's
|
||||
// last message to its parent did not.
|
||||
activation.relayed = false
|
||||
return next()
|
||||
wakeSettlement()
|
||||
})
|
||||
activation.handle.agent.ctx.on('agent/inbox/discarded', wakeSettlement)
|
||||
// Logged tool calls cannot be skipped by tool policy: a call in a later
|
||||
// step, or another call in the step that sent the message, also voids it.
|
||||
activation.handle.agent.ctx.on('session/event', (_session, event) => {
|
||||
if (event.type === 'step/start') activation.stepToolCalls = 0
|
||||
else if (event.type === 'tool/call') {
|
||||
activation.stepToolCalls += 1
|
||||
activation.relayed = false
|
||||
}
|
||||
})
|
||||
}
|
||||
observer.start(activationAgent(activation))
|
||||
|
||||
@@ -3023,7 +3023,7 @@ describe('continuable settlement delivery', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('reports the closing message when the child ran a tool after its message', async () => {
|
||||
it('reports the closing message when the child ran a tool after its message, even past a short-circuiting tool policy', async () => {
|
||||
const release = Promise.withResolvers<undefined>()
|
||||
const adapter = new GatedAdapter([
|
||||
{ chunks: toolCallResponse('t1', 'noop', {}), gate: release.promise },
|
||||
@@ -3042,6 +3042,8 @@ describe('continuable settlement delivery', () => {
|
||||
},
|
||||
execute: () => Promise.resolve({}),
|
||||
}))
|
||||
// A policy that allows the call without delegating hides it from later pre-execute listeners.
|
||||
ctx.on('tools/pre-execute', (exec, next) => exec.name === 'noop' ? Promise.resolve({ kind: 'allow' as const }) : next(), { prepend: true })
|
||||
const started = await ctx.subagents.startActivation({ ...startSpec(parent), delivery: 'parent' })
|
||||
const child = await vi.waitFor(() => {
|
||||
const live = ctx.agents.get(started.childId)
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"index": 0,
|
||||
"id": "notice-start",
|
||||
"name": "bash",
|
||||
"argumentsDelta": "{\"command\": \"sleep 1; printf 'NOTICE_OUTPUT\\\\n'\", \"description\": \"Start a short background command\", \"run_in_background\": true}"
|
||||
"argumentsDelta": "{\"command\": \"while [ ! -e .notice-go ]; do sleep 0.05; done; printf 'NOTICE_OUTPUT\\\\n'\", \"description\": \"Start a short background command\", \"run_in_background\": true}"
|
||||
},
|
||||
{
|
||||
"type": "block-end",
|
||||
@@ -21,7 +21,7 @@
|
||||
"type": "tool-call",
|
||||
"id": "notice-start",
|
||||
"name": "bash",
|
||||
"arguments": "{\"command\": \"sleep 1; printf 'NOTICE_OUTPUT\\\\n'\", \"description\": \"Start a short background command\", \"run_in_background\": true}"
|
||||
"arguments": "{\"command\": \"while [ ! -e .notice-go ]; do sleep 0.05; done; printf 'NOTICE_OUTPUT\\\\n'\", \"description\": \"Start a short background command\", \"run_in_background\": true}"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -52,7 +52,7 @@
|
||||
"index": 0,
|
||||
"id": "notice-wait",
|
||||
"name": "bash",
|
||||
"argumentsDelta": "{\"command\": \"sleep 4\", \"description\": \"Keep working while the job finishes\"}"
|
||||
"argumentsDelta": "{\"command\": \"touch .notice-go; sleep 1; rm -f .notice-go\", \"description\": \"Release the background job and keep working\"}"
|
||||
},
|
||||
{
|
||||
"type": "block-end",
|
||||
@@ -61,7 +61,7 @@
|
||||
"type": "tool-call",
|
||||
"id": "notice-wait",
|
||||
"name": "bash",
|
||||
"arguments": "{\"command\": \"sleep 4\", \"description\": \"Keep working while the job finishes\"}"
|
||||
"arguments": "{\"command\": \"touch .notice-go; sleep 1; rm -f .notice-go\", \"description\": \"Release the background job and keep working\"}"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,33 +2,33 @@
|
||||
{"type":"permission/preset","data":{"preset":"danger-full-access"}}
|
||||
{"type":"sandbox/mode","data":{"mode":"danger-full-access"}}
|
||||
{"type":"approval/policy","data":{"policy":"never"}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Start sleep 1; printf 'NOTICE_OUTPUT\\n' as a background bash job, then run sleep 4 in the foreground. When the job's completion notice arrives, reply with exactly the output it reported and stop."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Start a background bash job that waits for .notice-go and then prints NOTICE_OUTPUT, then create .notice-go in the foreground and keep working briefly. When the job's completion notice arrives, reply with exactly the output it reported and stop."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}}
|
||||
{"type":"turn/start","data":{"turn":1}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","data":{"turn":1,"step":1}}
|
||||
{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Start sleep 1; printf 'NOTICE_OUTPUT\\n' as a background bash job, then run sleep 4 in the foreground. When the job's completion notice arrives, reply with exactly the output it reported and stop."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"Start a background bash job that waits for .notice-go and then prints NOTICE_OUTPUT, then create .notice-go in the foreground and keep working briefly. When the job's completion notice arrives, reply with exactly the output it reported and stop."}],"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: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"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`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"}
|
||||
{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}
|
||||
{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}
|
||||
{"type":"session/title","data":{"title":"Start sleep 1; printf 'NOTICE_OUTPUT\\n'","messageSeqs":[8],"source":{"kind":"fallback"}}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"notice-start","name":"bash","arguments":"{\"command\": \"sleep 1; printf 'NOTICE_OUTPUT\\\\n'\", \"description\": \"Start a short background command\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1791807726901,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791807726901,"index":0,"dt":[],"id":"notice-start","name":"bash","args":["{\"command\": \"sleep 1; printf 'NOTICE_OUTPUT\\\\n'\", \"description\": \"Start a short background command\", \"run_in_background\": true}"]},{"type":"chunk","time":1791807726901,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"notice-start","name":"bash","arguments":"{\"command\": \"sleep 1; printf 'NOTICE_OUTPUT\\\\n'\", \"description\": \"Start a short background command\", \"run_in_background\": true}"}}},{"type":"chunk","time":1791807726901,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1791807726901,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":1,"callId":"notice-start","name":"bash","arguments":"{\"command\": \"sleep 1; printf 'NOTICE_OUTPUT\\\\n'\", \"description\": \"Start a short background command\", \"run_in_background\": true}"}}
|
||||
{"type":"job/registered","data":{"jobId":"{{id:1}}","owner":"{{session:1}}","kind":"bash","label":"sleep 1; printf 'NOTICE_OUTPUT\\n'","startedAt":1791807726914,"toolCall":{"callId":"notice-start","rootCallId":"notice-start"}}}
|
||||
{"type":"session/title","data":{"title":"Start a background bash job","messageSeqs":[8],"source":{"kind":"fallback"}}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"notice-start","name":"bash","arguments":"{\"command\": \"while [ ! -e .notice-go ]; do sleep 0.05; done; printf 'NOTICE_OUTPUT\\\\n'\", \"description\": \"Start a short background command\", \"run_in_background\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1791807726901,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791807726901,"index":0,"dt":[],"id":"notice-start","name":"bash","args":["{\"command\": \"while [ ! -e .notice-go ]; do sleep 0.05; done; printf 'NOTICE_OUTPUT\\\\n'\", \"description\": \"Start a short background command\", \"run_in_background\": true}"]},{"type":"chunk","time":1791807726901,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"notice-start","name":"bash","arguments":"{\"command\": \"while [ ! -e .notice-go ]; do sleep 0.05; done; printf 'NOTICE_OUTPUT\\\\n'\", \"description\": \"Start a short background command\", \"run_in_background\": true}"}}},{"type":"chunk","time":1791807726901,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1791807726901,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":1,"callId":"notice-start","name":"bash","arguments":"{\"command\": \"while [ ! -e .notice-go ]; do sleep 0.05; done; printf 'NOTICE_OUTPUT\\\\n'\", \"description\": \"Start a short background command\", \"run_in_background\": true}"}}
|
||||
{"type":"job/registered","data":{"jobId":"{{id:1}}","owner":"{{session:1}}","kind":"bash","label":"while [ ! -e .notice-go ]; do sleep 0.05; done; printf 'NOTICE_OUTPUT\\n'","startedAt":1791807726914,"toolCall":{"callId":"notice-start","rootCallId":"notice-start"}}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"notice-start"},"toolCallId":"notice-start","content":[{"type":"text","text":"started background job {{id:1}}"}],"isError":false,"id":"{{message:5}}"},"meta":{"cwd":"{{cwd}}"}},"sourceEventSeqs":[14],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"notice-wait","name":"bash","arguments":"{\"command\": \"sleep 4\", \"description\": \"Keep working while the job finishes\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1791807726930,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791807726930,"index":0,"dt":[],"id":"notice-wait","name":"bash","args":["{\"command\": \"sleep 4\", \"description\": \"Keep working while the job finishes\"}"]},{"type":"chunk","time":1791807726930,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"notice-wait","name":"bash","arguments":"{\"command\": \"sleep 4\", \"description\": \"Keep working while the job finishes\"}"}}},{"type":"chunk","time":1791807726930,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1791807726930,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":2,"callId":"notice-wait","name":"bash","arguments":"{\"command\": \"sleep 4\", \"description\": \"Keep working while the job finishes\"}"}}
|
||||
{"type":"job/registered","data":{"jobId":"{{id:2}}","owner":"{{session:1}}","kind":"bash","label":"sleep 4","startedAt":1791807726938,"toolCall":{"callId":"notice-wait","rootCallId":"notice-wait"}}}
|
||||
{"type":"job/settled","data":{"jobId":"{{id:1}}","owner":"{{session:1}}","kind":"bash","label":"sleep 1; printf 'NOTICE_OUTPUT\\n'","startedAt":1791807726914,"toolCall":{"callId":"notice-start","rootCallId":"notice-start"},"finishedAt":1791807727923,"status":"completed","cause":"producer","producerReported":true,"detail":"exit code: 0"}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"background job {{id:1}} (bash: sleep 1; printf 'NOTICE_OUTPUT\\n') finished [status: completed, exit code: 0]. Output:\nNOTICE_OUTPUT"}],"source":{"kind":"tool-jobs","form":"notice","summary":"bash sleep 1; printf 'NOTICE_OUTPUT\\n' [status: completed, exit code: 0]"},"role":"user","id":"{{message:7}}"}]}}
|
||||
{"type":"job/settled","data":{"jobId":"{{id:2}}","owner":"{{session:1}}","kind":"bash","label":"sleep 4","startedAt":1791807726938,"toolCall":{"callId":"notice-wait","rootCallId":"notice-wait"},"finishedAt":1791807729950,"status":"completed","cause":"producer","producerReported":true,"detail":"exit code: 0"}}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"notice-wait","name":"bash","arguments":"{\"command\": \"touch .notice-go; sleep 1; rm -f .notice-go\", \"description\": \"Release the background job and keep working\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1791807726930,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791807726930,"index":0,"dt":[],"id":"notice-wait","name":"bash","args":["{\"command\": \"touch .notice-go; sleep 1; rm -f .notice-go\", \"description\": \"Release the background job and keep working\"}"]},{"type":"chunk","time":1791807726930,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"notice-wait","name":"bash","arguments":"{\"command\": \"touch .notice-go; sleep 1; rm -f .notice-go\", \"description\": \"Release the background job and keep working\"}"}}},{"type":"chunk","time":1791807726930,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1791807726930,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}
|
||||
{"type":"tool/call","data":{"turn":1,"step":2,"callId":"notice-wait","name":"bash","arguments":"{\"command\": \"touch .notice-go; sleep 1; rm -f .notice-go\", \"description\": \"Release the background job and keep working\"}"}}
|
||||
{"type":"job/registered","data":{"jobId":"{{id:2}}","owner":"{{session:1}}","kind":"bash","label":"touch .notice-go; sleep 1; rm -f .notice-go","startedAt":1791807726938,"toolCall":{"callId":"notice-wait","rootCallId":"notice-wait"}}}
|
||||
{"type":"job/settled","data":{"jobId":"{{id:1}}","owner":"{{session:1}}","kind":"bash","label":"while [ ! -e .notice-go ]; do sleep 0.05; done; printf 'NOTICE_OUTPUT\\n'","startedAt":1791807726914,"toolCall":{"callId":"notice-start","rootCallId":"notice-start"},"finishedAt":1791807727923,"status":"completed","cause":"producer","producerReported":true,"detail":"exit code: 0"}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"background job {{id:1}} (bash: while [ ! -e .notice-go ]; do sleep 0.05; done; printf 'NOTICE_OUTPUT\\n') finished [status: completed, exit code: 0]. Output:\nNOTICE_OUTPUT"}],"source":{"kind":"tool-jobs","form":"notice","summary":"bash while [ ! -e .notice-go ]; do sleep 0.05; done; printf 'NOTICE_OUTPUT\\n' [status: completed, exit code: 0]"},"role":"user","id":"{{message:7}}"}]}}
|
||||
{"type":"job/settled","data":{"jobId":"{{id:2}}","owner":"{{session:1}}","kind":"bash","label":"touch .notice-go; sleep 1; rm -f .notice-go","startedAt":1791807726938,"toolCall":{"callId":"notice-wait","rootCallId":"notice-wait"},"finishedAt":1791807729950,"status":"completed","cause":"producer","producerReported":true,"detail":"exit code: 0"}}
|
||||
{"type":"tool/result","data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"notice-wait"},"toolCallId":"notice-wait","content":[{"type":"text","text":"(no output)"}],"isError":false,"id":"{{message:8}}"},"meta":{"cwd":"{{cwd}}"}},"sourceEventSeqs":[20],"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":2}}
|
||||
{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}
|
||||
{"type":"step/start","data":{"turn":1,"step":3}}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"background job {{id:1}} (bash: sleep 1; printf 'NOTICE_OUTPUT\\n') finished [status: completed, exit code: 0]. Output:\nNOTICE_OUTPUT"}],"source":{"kind":"tool-jobs","form":"notice","summary":"bash sleep 1; printf 'NOTICE_OUTPUT\\n' [status: completed, exit code: 0]"},"role":"user","id":"{{message:7}}"},"surfaceOp":"append"}
|
||||
{"type":"user/message","data":{"content":[{"type":"text","text":"background job {{id:1}} (bash: while [ ! -e .notice-go ]; do sleep 0.05; done; printf 'NOTICE_OUTPUT\\n') finished [status: completed, exit code: 0]. Output:\nNOTICE_OUTPUT"}],"source":{"kind":"tool-jobs","form":"notice","summary":"bash while [ ! -e .notice-go ]; do sleep 0.05; done; printf 'NOTICE_OUTPUT\\n' [status: completed, exit code: 0]"},"role":"user","id":"{{message:7}}"},"surfaceOp":"append"}
|
||||
{"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"NOTICE_OUTPUT"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:9}}"},"usage":{"inputTokens":10,"outputTokens":2},"stream":[{"type":"chunk","time":1791807729968,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1791807729968,"index":0,"dt":[],"texts":["NOTICE_OUTPUT"]},{"type":"chunk","time":1791807729968,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"NOTICE_OUTPUT"}}},{"type":"chunk","time":1791807729968,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":2}}},{"type":"chunk","time":1791807729968,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}
|
||||
{"type":"step/end","data":{"turn":1,"step":3}}
|
||||
{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}}
|
||||
Reference in new issue
Block a user