@civilization/dsh-agent-loop 0.0.0-stage → 0.2.1-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.i18n.yaml +112 -0
- package/README.md +229 -2
- package/README.zh.md +230 -0
- package/lib/index.js +2032 -0
- package/lib/types/agent.d.ts +70 -0
- package/lib/types/assistant-stream.d.ts +53 -0
- package/lib/types/constants.d.ts +6 -0
- package/lib/types/inbox.d.ts +99 -0
- package/lib/types/index.d.ts +179 -0
- package/lib/types/runtime-context.d.ts +75 -0
- package/lib/types/tool-calls.d.ts +38 -0
- package/locale/en.json +5 -0
- package/locale/zh.json +6 -0
- package/package.json +58 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
|
|
2
|
+
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
|
3
|
+
# After editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/core/agent-loop/README.md
|
|
5
|
+
/:
|
|
6
|
+
en: 5d57c28077cb670a
|
|
7
|
+
zh: 8113ea0248d66a17
|
|
8
|
+
/deepseek-ai-dsh-agent-loop:
|
|
9
|
+
en: b711ded29c7b9a17
|
|
10
|
+
zh: cbccba927743f35f
|
|
11
|
+
/deepseek-ai-dsh-agent-loop/summary:
|
|
12
|
+
en: 9256331cd3baf930
|
|
13
|
+
zh: 02b4c10cf3286eb3
|
|
14
|
+
/deepseek-ai-dsh-agent-loop/table-of-contents:
|
|
15
|
+
en: d152484eb41ac6b4
|
|
16
|
+
zh: 09388d293f9be9cb
|
|
17
|
+
/deepseek-ai-dsh-agent-loop/use-this-package:
|
|
18
|
+
en: 35a960c0100a6aa4
|
|
19
|
+
zh: 438e133fb4c24a9e
|
|
20
|
+
/deepseek-ai-dsh-agent-loop/use-this-package/configure-declarative-agents:
|
|
21
|
+
en: 96295035babf4f90
|
|
22
|
+
zh: 8848f9497019ee63
|
|
23
|
+
/deepseek-ai-dsh-agent-loop/use-this-package/create-or-resume-agents-programmatically:
|
|
24
|
+
en: 82ec792637e8e57f
|
|
25
|
+
zh: e32f8ec5ee17afcf
|
|
26
|
+
/deepseek-ai-dsh-agent-loop/use-this-package/what-a-step-does:
|
|
27
|
+
en: 15ce4783b53532a2
|
|
28
|
+
zh: b04a18337281237a
|
|
29
|
+
/deepseek-ai-dsh-agent-loop/understand-the-implementation:
|
|
30
|
+
en: ce7e776e9d516bdb
|
|
31
|
+
zh: 59d2751743db7795
|
|
32
|
+
/deepseek-ai-dsh-agent-loop/understand-the-implementation/design-concept:
|
|
33
|
+
en: 030202c112f89366
|
|
34
|
+
zh: 621d53ed755cafa3
|
|
35
|
+
/deepseek-ai-dsh-agent-loop/understand-the-implementation/request-headers-and-adapter-defaults:
|
|
36
|
+
en: b3b3f0ad930961c3
|
|
37
|
+
zh: f281e27136e686bc
|
|
38
|
+
/deepseek-ai-dsh-agent-loop/understand-the-implementation/source-map:
|
|
39
|
+
en: 2a8b93869eb5a2f9
|
|
40
|
+
zh: ddab8e738d188a61
|
|
41
|
+
/deepseek-ai-dsh-agent-loop/understand-the-implementation/creation-and-teardown:
|
|
42
|
+
en: d079cf181ece9267
|
|
43
|
+
zh: 047f76fecfc5497c
|
|
44
|
+
/deepseek-ai-dsh-agent-loop/understand-the-implementation/persistence-integration:
|
|
45
|
+
en: 3c5721bc00a0bbf4
|
|
46
|
+
zh: 711f832343ffbb75
|
|
47
|
+
/deepseek-ai-dsh-agent-loop/understand-the-implementation/turn-and-step-flow:
|
|
48
|
+
en: 28939261ccc7defe
|
|
49
|
+
zh: 9d157a321307ebd4
|
|
50
|
+
/deepseek-ai-dsh-agent-loop/understand-the-implementation/failure-and-cancellation:
|
|
51
|
+
en: eea56903c4ff82f5
|
|
52
|
+
zh: c0d5abb5767780ed
|
|
53
|
+
/deepseek-ai-dsh-agent-loop/further-exploration:
|
|
54
|
+
en: 7ac519f8e635acbc
|
|
55
|
+
zh: 975e514915dc6c9f
|
|
56
|
+
/deepseek-ai-dsh-agent-loop/model-experience:
|
|
57
|
+
en: 215e7ba838619b7b
|
|
58
|
+
zh: a311da3843709f90
|
|
59
|
+
/deepseek-ai-dsh-agent-loop/model-experience/complete-conversation-request:
|
|
60
|
+
en: 5507d83eb979c737
|
|
61
|
+
zh: 0ffde85d627b3933
|
|
62
|
+
/deepseek-ai-dsh-agent-loop/model-experience/complete-conversation-request/what-the-model-sees:
|
|
63
|
+
en: ed0e841bed26a836
|
|
64
|
+
zh: f30d91bc5a64fa20
|
|
65
|
+
/deepseek-ai-dsh-agent-loop/model-experience/complete-conversation-request/token-effect:
|
|
66
|
+
en: ad93d8d61968a25d
|
|
67
|
+
zh: 268ff858b0fa96e8
|
|
68
|
+
/deepseek-ai-dsh-agent-loop/model-experience/complete-conversation-request/kv-cache-effect:
|
|
69
|
+
en: c002d27c825d04e8
|
|
70
|
+
zh: b0dbdd95d3b3b6b5
|
|
71
|
+
/deepseek-ai-dsh-agent-loop/model-experience/retained-message-history:
|
|
72
|
+
en: 50d33d852dec1e28
|
|
73
|
+
zh: ec15695304f25f38
|
|
74
|
+
/deepseek-ai-dsh-agent-loop/model-experience/retained-message-history/what-the-model-sees:
|
|
75
|
+
en: 0a36273eff0815fd
|
|
76
|
+
zh: 1aa1e856d8b1afc9
|
|
77
|
+
/deepseek-ai-dsh-agent-loop/model-experience/retained-message-history/token-effect:
|
|
78
|
+
en: 91552b9cbef55638
|
|
79
|
+
zh: 6e835a66d984f62d
|
|
80
|
+
/deepseek-ai-dsh-agent-loop/model-experience/retained-message-history/kv-cache-effect:
|
|
81
|
+
en: 2b82cd95e6335e91
|
|
82
|
+
zh: 2275f0c808427bf1
|
|
83
|
+
/deepseek-ai-dsh-agent-loop/model-experience/undispatched-calls-after-cancellation:
|
|
84
|
+
en: a7afa95628111dd8
|
|
85
|
+
zh: 1d72925e9915f75f
|
|
86
|
+
/deepseek-ai-dsh-agent-loop/model-experience/undispatched-calls-after-cancellation/what-the-model-sees:
|
|
87
|
+
en: e3c07034229f15ed
|
|
88
|
+
zh: b0ee0507606b34ce
|
|
89
|
+
/deepseek-ai-dsh-agent-loop/model-experience/undispatched-calls-after-cancellation/token-effect:
|
|
90
|
+
en: 7c139b3584615343
|
|
91
|
+
zh: 02d5d8239b502b06
|
|
92
|
+
/deepseek-ai-dsh-agent-loop/model-experience/undispatched-calls-after-cancellation/kv-cache-effect:
|
|
93
|
+
en: 1750eea3cc5bf2d8
|
|
94
|
+
zh: 519d37b0cb4467a7
|
|
95
|
+
/deepseek-ai-dsh-agent-loop/model-experience/unanswered-calls-after-step-failure:
|
|
96
|
+
en: 0f150799ee294e40
|
|
97
|
+
zh: 4c283b07367dfa4a
|
|
98
|
+
/deepseek-ai-dsh-agent-loop/model-experience/unanswered-calls-after-step-failure/what-the-model-sees:
|
|
99
|
+
en: a62feae35f09ff13
|
|
100
|
+
zh: d20a0172d86f6515
|
|
101
|
+
/deepseek-ai-dsh-agent-loop/model-experience/unanswered-calls-after-step-failure/token-effect:
|
|
102
|
+
en: cefdf878a21a681a
|
|
103
|
+
zh: 96d8271b257478d6
|
|
104
|
+
/deepseek-ai-dsh-agent-loop/model-experience/unanswered-calls-after-step-failure/kv-cache-effect:
|
|
105
|
+
en: 83531f2a13cc1f0e
|
|
106
|
+
zh: 81a780d637dd8c5d
|
|
107
|
+
/deepseek-ai-dsh-agent-loop/known-limitations-and-deferred-work:
|
|
108
|
+
en: a57822fab5e6fdfd
|
|
109
|
+
zh: 39f97b0c5040f7c8
|
|
110
|
+
/deepseek-ai-dsh-agent-loop/known-limitations-and-deferred-work/dev-note:
|
|
111
|
+
en: e67f633dc6fe7773
|
|
112
|
+
zh: 3bb0c20c93f16f98
|
package/README.md
CHANGED
|
@@ -1,3 +1,230 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
description: "The default agent driver for users and maintainers choosing, configuring, or debugging how agents are created and how turns and steps run."
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
# @deepseek-ai/dsh-agent-loop
|
|
7
|
+
|
|
8
|
+
English | [中文](README.zh.md)
|
|
9
|
+
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
`dsh-agent-loop` creates fresh agents or resumes persisted sessions, then drives each turn through model requests, streamed responses, tool execution, and durable session history. Mount it for standard agent compositions; declarative entries start agents at boot, while the public `ctx.agents` API supports programmatic creation and resume. `maxParallelToolCalls` limits concurrent parallel-safe calls, and exclusive calls retain ordering. Cancellation preserves streamed text already delivered to the user. Choose a custom `Agent` implementation only when the standard "call model, run tools, repeat" lifecycle is insufficient.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Further Exploration](#further-exploration)
|
|
19
|
+
- [Model Experience](#model-experience)
|
|
20
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
21
|
+
- [Dev Note](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## Use this package
|
|
27
|
+
|
|
28
|
+
Mount `dsh-agent-loop` in any composition that should run agents. It supplies the driver behind `ctx.agents` and starts any agents you declare in its config; both [`dsh-base`](../../bundle/base/README.md) and [`dsh-sdk-minimal`](../../bundle/sdk-minimal/README.md) mount it as an explicit row.
|
|
29
|
+
|
|
30
|
+
### Configure declarative agents
|
|
31
|
+
|
|
32
|
+
Agents declared in the config start automatically when the plugin loads. Each entry needs an `id` label; a model call additionally requires both `provider` and `model` (`agent/request` may supply a missing pair before dispatch).
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
- name: '@deepseek-ai/dsh-agent-loop'
|
|
36
|
+
config:
|
|
37
|
+
maxParallelToolCalls: 10
|
|
38
|
+
agents:
|
|
39
|
+
- id: 'main'
|
|
40
|
+
provider: deepseek
|
|
41
|
+
model: deepseek-chat
|
|
42
|
+
reasoningEffort: high
|
|
43
|
+
cwd: /workspace
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
| Field | Default | Meaning |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `maxParallelToolCalls` | `10` | Parallel-safe tool calls in flight per step; `1` is serial |
|
|
49
|
+
| `agents[].id` | required | Stable label; a fresh session mints `${id}-session-<uuid>` unless `sessionId` is set |
|
|
50
|
+
| `agents[].provider` / `agents[].model` | — | Model route; both required before dispatch |
|
|
51
|
+
| `agents[].reasoningEffort` | — | Non-empty initial reasoning effort; `agent/request` may override it |
|
|
52
|
+
| `agents[].maxTokens` | — | Positive per-request output-token cap |
|
|
53
|
+
| `agents[].cwd` | — | Workspace directory for a fresh session |
|
|
54
|
+
| `agents[].sessionId` | — | Exact identity: first use creates, a remount resumes materialized history |
|
|
55
|
+
| `agents[].resumeSessionId` | — | Load this persisted session instead of creating one; mutually exclusive with `sessionId` |
|
|
56
|
+
|
|
57
|
+
The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-agent-loop) lists every accepted field. `maxParallelToolCalls` is a volatile Config field sampled for the next tool group; `agents` remains startup configuration.
|
|
58
|
+
|
|
59
|
+
### Create or resume agents programmatically
|
|
60
|
+
|
|
61
|
+
Plugins and hosts create agents through `ctx.agents.create()` and resume persisted sessions through `ctx.agents.resume()`; both return an `AgentHandle` whose `dispose()` owns exact teardown. The loop runs each created agent to completion — the handle is only needed when the caller must tear the agent down itself.
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
const handle = await ctx.agents.create({
|
|
65
|
+
sessionId,
|
|
66
|
+
agentOptions: { provider: 'deepseek', model: 'deepseek-chat' },
|
|
67
|
+
setup: (agentCtx, agent) => { /* scoped registrations plus explicit unpublished Agent */ },
|
|
68
|
+
})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Every inbox mutation commits one normalized `agent/inbox/spliced` event. The projection registry folds that event synchronously, so the live projection reflects the splice when `Session.append()` returns. Insertions, edits, removals, claiming, and cancellation replay through the same standard splice coordinates. Ordinary removals carry `outcome: 'canceled'` and emit `agent/inbox/discarded { message }`; claiming uses pure deletions with no outcome and emits `agent/inbox/claimed`. Every insertion emits `agent/inbox/inserted { message }`. `MessageId` stays unique across both pending lists. Consumers that need a removed message use the claimed or discarded notification instead of depending on a pre-splice `session/event` view.
|
|
72
|
+
|
|
73
|
+
### What a step does
|
|
74
|
+
|
|
75
|
+
Each step sends the session's derived history — with the latest non-empty `system/message` node as the effective prompt, or no system messages when the rendered prompt is empty — and its visible tool schemas; the model's tool calls run through the guarded tool pipeline and every accepted fact is appended to the session log before the next step derives from it. Parallel-safe calls may overlap up to `maxParallelToolCalls`; exclusive calls run alone as ordering barriers. Cancellation is cooperative: `agent.cancel()` aborts the current activity and, unless `keepInbox` is set, clears pending work; a cancelled stream finalizes the text already delivered to the user.
|
|
76
|
+
|
|
77
|
+
-----
|
|
78
|
+
|
|
79
|
+
<a id="understand-the-implementation"></a>
|
|
80
|
+
## Understand the implementation
|
|
81
|
+
|
|
82
|
+
<details>
|
|
83
|
+
<summary>Implementation internals — click to expand</summary>
|
|
84
|
+
|
|
85
|
+
This section explains how the package realizes the behavior above; the observable contract is covered in [Use this package](#use-this-package).
|
|
86
|
+
|
|
87
|
+
### Design concept
|
|
88
|
+
|
|
89
|
+
The package is the one concrete implementation of the public `Agent` contract. It registers itself as the `AgentFactory` on `ctx.agents`, so consumers never import this package; ownership of each created agent lives with the caller fiber and the loop provider, converging on one memoized quiescence boundary. Every observable effect happens through session events and the `agent/*` taxonomy — package internals are never part of the public surface.
|
|
90
|
+
|
|
91
|
+
### Request headers and adapter defaults
|
|
92
|
+
|
|
93
|
+
After `agent/request`, `ctx.llm.prepareCall()` validates adapter-owned fields and resolves reasoning-effort and output-token defaults under the active turn signal. The loop retains that exact adapter through resolution, `request/header` logging, and dispatch. It writes a full header for the first request, a changed envelope (config or tools — the prompt is not part of the header), an explicit message-series start, a request after a surface replacement or image-offload decision, and resume; unchanged steps, retries, and ordinary later turns in the same series inherit the latest header, and an in-history prompt append is not a replacement, so the request that follows it inherits the header too. Beside the header, the loop logs `request/context` — provider, model, `contextWindow`, and the route's `systemPromptUpdate` mode from `prepareCall()` — only when one of those differs from the latest snapshot. Before the next waterfall, the loop removes adapter-default fields so the current route resolves them again, while explicit settings persist. An unhandled route still fails with `NO_ADAPTER`.
|
|
94
|
+
|
|
95
|
+
The loop deep-freezes each derived message identity on its first request and reuses that proof only within the same agent. Restored messages keep their identity; request construction does not freeze their containing event wrappers. Each request freezes its local canonical header, fresh message array, and envelope while leaving the cancellation signal live. The [archived request-freeze decision](../../../.agents/notes/archived/simplification/2026-09-06-agent-request-freeze-evidence.md) explains ownership and measurement.
|
|
96
|
+
|
|
97
|
+
### Source map
|
|
98
|
+
|
|
99
|
+
| File | Role |
|
|
100
|
+
|---|---|
|
|
101
|
+
| [`src/index.ts`](src/index.ts) | Plugin entry: `AgentLoop` service, config schema, declarative agent startup, factory registration |
|
|
102
|
+
| [`src/agent.ts`](src/agent.ts) | The concrete `ReactLoopAgent` driver: inbox, turn/step machine, cancellation |
|
|
103
|
+
| [`src/inbox.ts`](src/inbox.ts) | Package-internal `ReactLoopInbox`: durable projection, structural commands, and loop-only claim state |
|
|
104
|
+
| [`src/tool-calls.ts`](src/tool-calls.ts) | Tool scheduling: exclusive barriers and the bounded parallel pool |
|
|
105
|
+
| [`src/runtime-context.ts`](src/runtime-context.ts) | Per-step runtime-context snapshot handling |
|
|
106
|
+
| [`src/constants.ts`](src/constants.ts) | `DEFAULT_MAX_PARALLEL_TOOL_CALLS` |
|
|
107
|
+
|
|
108
|
+
### Creation and teardown
|
|
109
|
+
|
|
110
|
+
Creation is one rollback-covered transaction: construct a private session, concrete agent, and scoped context; await optional setup; enter both registries; announce `session/created`; await serial `agent/created` listeners; then release queued input. A caller creating a runtime child sets `options.parentAgent`; the caller Context separately owns the transaction and live handle. Setup, commit, or listener failure and owner disposal roll back the prepared resources. Announcements already delivered remain observable and receive matching disposal notifications. Teardown stops and drains the driver, unwinds the scope, closes the session's write path, detaches the agent, then detaches the session. Every detach binds the exact entered object, so a stale disposer cannot remove a later same-id replacement.
|
|
111
|
+
|
|
112
|
+
Factory unload retains the inbox and turn-boundary projections through scope cleanup, including cancellation from scoped abort listeners, until every Agent teardown and startup task settles. Cleanup failures do not release those projections early. Caller unload, factory unload, and handle disposal await the same teardown.
|
|
113
|
+
|
|
114
|
+
### Persistence integration
|
|
115
|
+
|
|
116
|
+
The loop is the production acquisition point for session write handles. When `ctx.sessionPersistence` is mounted, `create`/`createAgent` call `persistence.create(header)` — storing the durable identity and taking write ownership before publication — and append the constructor seed through the handle; `resume` calls `persistence.open(id, 'write')` first (excluding a concurrent resume of the same id), reads the physically valid log through the handle, and appends `interruptedTurnClosers` for a log crashed mid-turn as an ordinary batch — semantic crash repair is the agent layer's job, not a storage entry point. Immediately before publication, `appendUnstoredSuffix` stores any events appended during the setup window (seed markers, delegation policy records), which never re-emit through `session/event`. Once published, the mounted backend routes the session's `session/event` batches, `session/flush` barriers, and `session/disposed` retirement into the active write handle by session id; the loop touches storage only through the handle it owns. With the backend still mounted, teardown closes the handle after the driver commits its closing events; close drains the routed buffer and releases write ownership. Without a backend, sessions are memory-only and nothing else changes.
|
|
117
|
+
|
|
118
|
+
### Turn and step flow
|
|
119
|
+
|
|
120
|
+
The driver owns one agent for its lifetime and runs inside `ctx.agents.withInitiator(agent, ...)`. `AgentLoop` registers the standard `inbox` projection for its service lifetime, so cold reads work before any Agent exists and after all Agents unload. Its package-internal `ReactLoopInbox` uses that shared projection for structural commands and loop-only claims. At a turn boundary it opens the durable turn, then atomically claims pending next-step input plus one queued prompt; between steps it claims only next-step input. The driver assembles the prompt and tools and runs `agent/pre-step`; the fallback projects runtime context after listeners run. A rejected decision or empty first batch opens no step. On the first attempt after acceptance, `step/start` precedes the `agent/request` waterfall and `prepareCall()`; neither async phase sees the pending system prompt or accepted users committed to history, and cancellation during either commits neither. For every attempt, the loop then synchronously reconciles the rendered prompt against the surviving `system/message` nodes using the prepared call capability, refreshes registered runtime facts without reassembling the bound prompt or tools, appends the accepted `user/message` batch only on the first attempt with its owned context candidate replaced in place, and restores removed runtime context, logs the header and context as needed, and derives and freezes the request before streaming through that bound prepared call. Retries reuse the same rendered assembly without repeating assembly, `agent/pre-step`, or user admission. Reconciliation sees pre-step and retry compaction; a broken series consolidates the prompt at the head rather than appending an update after already-committed users. The request is `header.config`, `deriveMessages()`, and `header.tools`; it carries no `system` field. Each model attempt emits one process-local `start`, emits every `chunk` only after the matching durable assistant-frame settlement, and emits exactly one terminal `end`; final assembly or message-append failure settles it as `aborted`, while `committed` follows the durable `assistant/message`. Each successful model call appends one message anchor, and a cancelled stream appends an `interrupted: true` anchor with the delivered prefix so the next request contains what the user saw. Within a step, exclusive calls form barriers and parallel-safe calls use the bounded rolling pool; policy, durable results... (line truncated to 2000 chars)
|
|
121
|
+
|
|
122
|
+
Prompt admission uses the actual `prepareCall()` result, not the preceding `request/context`. With no system node, even an empty prompt is appended (reserving node 0 without a wire message). On an incapable route or at a new request series, a non-empty rendering is consolidated at the first system node: each non-empty later system node receives a logged per-node empty replacement, then the head is rewritten if needed. Dormant empty tails need no replacement and do not determine the effective text. Consolidation applies even when the latest effective text is unchanged. On a continuing `in-history` series, an unchanged effective prompt produces no event and a non-empty change appends. An empty rendering clears every non-empty later system node through a logged per-node empty replacement, then empties the head if needed, regardless of route or series state. No older instructions remain model-visible. An empty head with no active later system node means no prompt; repeated clears and resume leave it empty. A restored non-empty prompt follows the same route/series rule: a continuing capable route may append it, while an incapable route or new series refills the head. A step starts a series when the pre-step decision declares `startsRequestSeries` or `session.surface.contentGeneration` changed since attachment or the last request (a replacement or image-offload decision). Visible tool-schema changes force prompt consolidation only when the prepared route lacks `toolUpdate`. Routes supporting both tool and system-prompt updates can append a changed prompt and activate added tools without rewriting earlier messages. If another admission condition replaces a system node, the request starts a new series with current tool declarations. Resume and a provider or model swap alone continue the series; the prepared route still governs admission. Per-node empty replacements preserve intervening history without a surface delete operation.
|
|
123
|
+
|
|
124
|
+
### Failure and cancellation
|
|
125
|
+
|
|
126
|
+
`turn/end` declares `TurnEndCancelCause`; cancellation records a fresh `AgentCancelCause` there, retaining the caller's `kind` and the hook's `reason` text. The live `AbortSignal.reason` remains the caller's object, which a transport may extend — Node's fetch assigns a `stack` onto it — so the copy keeps that trace out of the log and keeps the ending appendable.
|
|
127
|
+
|
|
128
|
+
Final adapter selection, dispatch, and iteration failures arrive as terminal finishes and enter `agent/request-error`; a handling listener returns `{ kind: 'retry' }` without calling `next()`, while an unhandled failure is terminal. Middleware, result-processing, tool, and other extension failures remain thrown and close the turn directly — plugin failure ends the turn, not the loop. Undispatched model tool calls after cancellation receive synthetic `tool/call` plus `ABORTED_BEFORE_DISPATCH` result pairs.
|
|
129
|
+
|
|
130
|
+
Before closing a failed step, the driver records an error result for each unanswered assistant tool call. A recorded `tool/call` without a committed result receives `TOOL_OUTCOME_UNKNOWN`; a request without a call record receives `TOOL_NOT_STARTED`. Committed results remain intact, started dispatches settle before recovery, and the turn retains the original failure. These results let later requests use paired tool history without automatically retrying uncertain operations.
|
|
131
|
+
|
|
132
|
+
</details>
|
|
133
|
+
|
|
134
|
+
-----
|
|
135
|
+
|
|
136
|
+
<a id="further-exploration"></a>
|
|
137
|
+
## Further Exploration
|
|
138
|
+
|
|
139
|
+
The package-level contract is enough for most consumers; read these when you need the surrounding domain and the design rationale.
|
|
140
|
+
|
|
141
|
+
- [agent package](../agent/README.md) — the `Agent` handle, registry, and `agent/*` events this loop implements.
|
|
142
|
+
- [Core subsystem](../../../docs/subsystems/core.md) — the turn flow and interception decisions.
|
|
143
|
+
- [Session subsystem](../../../docs/subsystems/session.md) — the durable log the loop writes and derives from.
|
|
144
|
+
- [Tools subsystem](../../../docs/subsystems/tools.md) — the pipeline the loop dispatches through.
|
|
145
|
+
- [Core group map](../README.md) — how the core packages compose.
|
|
146
|
+
|
|
147
|
+
-----
|
|
148
|
+
|
|
149
|
+
<a id="model-experience"></a>
|
|
150
|
+
## Model Experience
|
|
151
|
+
|
|
152
|
+
### Complete conversation request
|
|
153
|
+
|
|
154
|
+
#### What the model sees
|
|
155
|
+
|
|
156
|
+
For each step, the loop sends the session's derived messages and visible tool schemas. Non-empty `system/message` nodes carry the prompt, with the latest as the effective version; an empty rendering clears all prompt versions from derived history. It supplies `provider` and `model` variable values but no additional fixed prose. The working-directory service contributes the active directory as user context. The request header always records active tools. Comparing their names with the preceding header emits one `developer/message` for additions and removals, independently of model capability; additions reference the new header. Requests also carry `Session.toolHistory()` for the LLM runtime to construct provider declarations.
|
|
157
|
+
|
|
158
|
+
#### Token effect
|
|
159
|
+
|
|
160
|
+
System text and schemas are paid again on every step, and on an `in-history` route every retained prompt version is paid until compaction shadows it or prompt reconciliation empties it. Per-agent scoping chooses the contributions, while the authoritative assembly waterfall can alter the final request and makes its listener responsible for protocol coherence.
|
|
161
|
+
|
|
162
|
+
#### KV Cache effect
|
|
163
|
+
|
|
164
|
+
Prefix reuse requires earlier messages and declarations to stay byte-identical under the same provider and model route. An unchanged rendered prompt keeps the cached prefix unless an incapable route or a new request series must consolidate retained in-history system nodes. A prompt change that replaces a system node in place makes the request differ from that node's first token — in full when the node is node 0 — so the provider prefix cache misses from there; when the prepared call declares `systemPromptUpdate: 'in-history'`, a non-empty prompt change inside a continuing request series is appended after the cached history, so the prefix through that history stays reusable. Changing a retained tool definition or rebuilding declarations invalidates reuse from the first altered request token.
|
|
165
|
+
|
|
166
|
+
### Retained message history
|
|
167
|
+
|
|
168
|
+
#### What the model sees
|
|
169
|
+
|
|
170
|
+
Accepted user messages, assistant messages, tool calls and results, injected context, and steering are logged and sent on later steps. Raw stream chunks, lifecycle boundaries, and other log-only events are excluded.
|
|
171
|
+
|
|
172
|
+
#### Token effect
|
|
173
|
+
|
|
174
|
+
Input grows with every surface message until a compaction replacement shadows older nodes; a multi-step tool turn resends the accumulated history each step.
|
|
175
|
+
|
|
176
|
+
#### KV Cache effect
|
|
177
|
+
|
|
178
|
+
Ordinary history growth is append-only and preserves reusable entries. A surface replacement or compaction invalidates reuse from the first shadowed history token.
|
|
179
|
+
|
|
180
|
+
### Undispatched calls after cancellation
|
|
181
|
+
|
|
182
|
+
#### What the model sees
|
|
183
|
+
|
|
184
|
+
If a later request replays an aborted step, each tool call that cancellation prevented from dispatching has error code `ABORTED_BEFORE_DISPATCH` and result text `Error: tool call aborted before dispatch`.
|
|
185
|
+
|
|
186
|
+
#### Token effect
|
|
187
|
+
|
|
188
|
+
One fixed error result per skipped call remains in history until compaction shadows it.
|
|
189
|
+
|
|
190
|
+
#### KV Cache effect
|
|
191
|
+
|
|
192
|
+
Append-only; each synthetic result follows the reusable request prefix and does not invalidate existing KV Cache entries.
|
|
193
|
+
|
|
194
|
+
### Unanswered calls after step failure
|
|
195
|
+
|
|
196
|
+
#### What the model sees
|
|
197
|
+
|
|
198
|
+
Each unanswered tool call receives an error result in later history. For a recorded call, the result states `Its outcome is unknown.` and permits retries only for read-only or idempotent operations; possible side effects require checking external state or asking the user first. A call without a start record states `The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed.`
|
|
199
|
+
|
|
200
|
+
#### Token effect
|
|
201
|
+
|
|
202
|
+
One recovery result per unanswered call remains in history until compaction shadows it.
|
|
203
|
+
|
|
204
|
+
#### KV Cache effect
|
|
205
|
+
|
|
206
|
+
Recovery results append after the existing history and preserve its reusable prefix.
|
|
207
|
+
|
|
208
|
+
## Known Limitations and Deferred Work
|
|
209
|
+
|
|
210
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
These limits define when the loop needs special care. They are current package constraints, not a task backlog.
|
|
214
|
+
|
|
215
|
+
- **Classification is unary** — calls whose safety depends on comparing siblings or resources must remain exclusive ([rationale](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.md)).
|
|
216
|
+
- **Root shutdown persistence** — shutting down the root context with an active Agent can leave its final `turn/end` in memory but absent from the persisted log. Agent teardown completing does not guarantee that this event was saved.
|
|
217
|
+
- **Previously closed inconsistent history** — failed-step recovery does not rewrite unanswered calls in already-closed historical turns.
|
|
218
|
+
- **Config labels are fresh by default** — omitting `sessionId` creates a fresh `${id}-session-<uuid>` on every startup; exact resume-or-create behavior requires an explicit stable `sessionId`, while `resumeSessionId` requires existing persisted history.
|
|
219
|
+
- **Config agents have no per-agent persona field or setup hook** — they use the deployment persona; scoped persona and tool composition are available only through the programmatic `ctx.agents.create()` / `resume()` factory options.
|
|
220
|
+
- **No built-in turn budget** — tool calls or steering continue the current turn; a policy that bounds runaway turns must cancel from an existing lifecycle extension point such as `agent/turn-stopping`.
|
|
221
|
+
|
|
222
|
+
<a id="dev-note"></a>
|
|
223
|
+
### Dev Note
|
|
224
|
+
|
|
225
|
+
<details>
|
|
226
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
227
|
+
|
|
228
|
+
None.
|
|
229
|
+
|
|
230
|
+
</details>
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "面向用户与维护者的默认 agent(智能体)驱动器说明,用于选择、配置或调试 agent 的创建方式以及轮次与步骤的运行方式。"
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @deepseek-ai/dsh-agent-loop
|
|
7
|
+
|
|
8
|
+
[English](README.md) | 中文
|
|
9
|
+
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
`dsh-agent-loop` 创建全新 agent 或恢复持久化会话,随后通过模型请求、流式响应、工具执行和持久会话历史驱动每个轮次。标准 agent 组合应挂载本包;声明式条目会在启动时启动 agent,公开的 `ctx.agents` API 则支持以编程方式创建和恢复 agent。`maxParallelToolCalls` 限制同时运行的并行安全调用数量,独占调用保留顺序。取消会保留已经流式交付给用户的文本。只有标准的「调用模型、运行工具、重复」生命周期无法满足需求时,才应选择自定义 `Agent` 实现。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [使用本包](#use-this-package)
|
|
17
|
+
- [理解实现](#understand-the-implementation)
|
|
18
|
+
- [进一步探索](#further-exploration)
|
|
19
|
+
- [模型体验](#model-experience)
|
|
20
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
21
|
+
- [开发备注](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## 使用本包
|
|
27
|
+
|
|
28
|
+
在任何应运行 agent 的组合中挂载 `dsh-agent-loop`。它提供 `ctx.agents` 背后的驱动器,并启动你在配置中声明的 agent;[`dsh-base`](../../bundle/base/README.zh.md) 与 [`dsh-sdk-minimal`](../../bundle/sdk-minimal/README.zh.md) 都将它作为显式配置行挂载。
|
|
29
|
+
|
|
30
|
+
### 配置声明式 agent
|
|
31
|
+
|
|
32
|
+
配置中声明的 agent 会在插件加载时自动启动。每个条目需要一个 `id` 标签;模型调用还同时需要 `provider` 与 `model`(`agent/request` 可以在分发前补齐缺失的这一对值)。
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
- name: '@deepseek-ai/dsh-agent-loop'
|
|
36
|
+
config:
|
|
37
|
+
maxParallelToolCalls: 10
|
|
38
|
+
agents:
|
|
39
|
+
- id: 'main'
|
|
40
|
+
provider: deepseek
|
|
41
|
+
model: deepseek-chat
|
|
42
|
+
reasoningEffort: high
|
|
43
|
+
cwd: /workspace
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
| 字段 | 默认值 | 含义 |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `maxParallelToolCalls` | `10` | 每个步骤同时在途的并行安全工具调用数;`1` 为串行 |
|
|
49
|
+
| `agents[].id` | 必填 | 稳定标签;未设置 `sessionId` 时,全新会话会生成 `${id}-session-<uuid>` |
|
|
50
|
+
| `agents[].provider` / `agents[].model` | — | 模型路由;分发前两者都必须存在 |
|
|
51
|
+
| `agents[].reasoningEffort` | — | 非空的初始推理强度;`agent/request` 可以覆盖它 |
|
|
52
|
+
| `agents[].maxTokens` | — | 正数的逐请求输出 token 上限 |
|
|
53
|
+
| `agents[].cwd` | — | 全新会话的工作目录 |
|
|
54
|
+
| `agents[].sessionId` | — | 确切身份:首次使用创建,重新挂载时恢复已实体化的历史 |
|
|
55
|
+
| `agents[].resumeSessionId` | — | 加载这个持久化会话而不是创建新会话;与 `sessionId` 互斥 |
|
|
56
|
+
|
|
57
|
+
生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-agent-loop) 列出所有接受的字段。`maxParallelToolCalls` 是在下一个工具组开始时读取的 volatile Config 字段;`agents` 仍是启动配置。
|
|
58
|
+
|
|
59
|
+
### 以编程方式创建或恢复 agent
|
|
60
|
+
|
|
61
|
+
插件与宿主通过 `ctx.agents.create()` 创建 agent,通过 `ctx.agents.resume()` 恢复持久化会话;两者都返回 `AgentHandle`,其 `dispose()`(资源释放)负责精确拆除对应的 agent。循环会把每个创建的 agent 运行到完成——只有调用方需要自行拆除 agent 时才需要句柄。
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
const handle = await ctx.agents.create({
|
|
65
|
+
sessionId,
|
|
66
|
+
agentOptions: { provider: 'deepseek', model: 'deepseek-chat' },
|
|
67
|
+
setup: (agentCtx, agent) => { /* scoped registrations plus explicit unpublished Agent */ },
|
|
68
|
+
})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
每次 inbox 变更都会提交一条规范化的 `agent/inbox/spliced` 事件。投影注册表会同步折叠该事件,因此 `Session.append()` 返回时,实时投影已经反映该 splice。插入、编辑、移除、领取与取消都通过同一组标准 splice 坐标回放。普通删除携带 `outcome: 'canceled'` 并发出 `agent/inbox/discarded { message }`;领取使用不带 outcome 的纯删除,并发出 `agent/inbox/claimed`。每次插入都会发出 `agent/inbox/inserted { message }`。`MessageId` 在两个待处理列表之间保持唯一。需要被移除消息的消费方应使用 claimed 或 discarded 通知,而不依赖 splice 前的 `session/event` 投影视图。
|
|
72
|
+
|
|
73
|
+
### 一个步骤做什么
|
|
74
|
+
|
|
75
|
+
每个步骤都会发送会话的派生历史——最新的非空 `system/message` 节点是有效提示词,渲染提示词为空时则没有系统消息——及其可见工具 schema;模型的工具调用经过受守卫的工具流水线,每个被接纳的事实都会在下一步据此派生之前追加到会话日志。并行安全调用最多可重叠 `maxParallelToolCalls` 个;独占调用单独运行并构成排序屏障。取消是协作式的:`agent.cancel()` 中止当前活动,并在未设置 `keepInbox` 时清除待处理工作;被取消的流会终结已送达用户的文本。
|
|
76
|
+
|
|
77
|
+
-----
|
|
78
|
+
|
|
79
|
+
<a id="understand-the-implementation"></a>
|
|
80
|
+
## 理解实现
|
|
81
|
+
|
|
82
|
+
<details>
|
|
83
|
+
<summary>实现细节——点击展开</summary>
|
|
84
|
+
|
|
85
|
+
本节解释该包如何实现上述行为;可观察约定已在[使用本包](#use-this-package)中完整说明。
|
|
86
|
+
|
|
87
|
+
### 设计理念
|
|
88
|
+
|
|
89
|
+
该包是公开 `Agent` 约定的唯一具象实现。它在 `ctx.agents` 上把自身注册为 `AgentFactory`,因此消费方从不导入本包;每个创建 agent 的所有权归属于调用方 fiber 与循环提供方,并汇合到同一个记忆化的完全停稳边界。每个可观察效果都通过会话事件与 `agent/*` 分类体系发生——包内部实现绝不属于公开接口。
|
|
90
|
+
|
|
91
|
+
### 请求 header 与适配器默认值
|
|
92
|
+
|
|
93
|
+
`agent/request` 返回后,`ctx.llm.prepareCall()` 会在活跃轮次信号下校验适配器持有的字段,并解析推理强度和输出 token 默认值。循环会在解析、`request/header` 记录与分派期间保留同一个适配器。循环会为首次请求、变化的 envelope(配置或工具——提示词不属于 header)、显式消息序列起点、surface 替换或图片省略决定后的请求及恢复写入完整 header;同一序列内内容未变的步骤、重试与普通后续轮次继承最新 header,历史内追加提示词不是替换,因此紧随其后的请求同样继承 header。在 header 之外,循环还会记录 `request/context`——提供方、模型、`contextWindow` 以及来自 `prepareCall()` 的路由 `systemPromptUpdate` 模式——且仅在其中任何一项与最新快照不同时记录。下一次 waterfall 分发前,循环移除适配器默认字段,使当前路由重新解析它们;显式设置则保留。未处理的路由仍以 `NO_ADAPTER` 失败。
|
|
94
|
+
|
|
95
|
+
循环在每个派生消息对象首次进入请求时执行深冻结,并且仅在同一 agent 内复用该证明。恢复的消息保留对象身份;构造请求不会冻结包含消息的事件包装对象。每个请求都会冻结本地规范化 header、新消息数组和请求封装,同时保留取消信号的可变性。[已归档的请求冻结决策](../../../.agents/notes/archived/simplification/2026-09-06-agent-request-freeze-evidence.md)解释了所有权与测量依据。
|
|
96
|
+
|
|
97
|
+
### 源码地图
|
|
98
|
+
|
|
99
|
+
| 文件 | 职责 |
|
|
100
|
+
|---|---|
|
|
101
|
+
| [`src/index.ts`](src/index.ts) | 插件入口:`AgentLoop` 服务、配置 schema、声明式 agent 启动、工厂注册 |
|
|
102
|
+
| [`src/agent.ts`](src/agent.ts) | 具体 `ReactLoopAgent` 驱动器:收件箱、轮次/步骤状态机、取消 |
|
|
103
|
+
| [`src/inbox.ts`](src/inbox.ts) | 包内部的 `ReactLoopInbox`:持久投影、结构化命令与仅供循环使用的领取状态 |
|
|
104
|
+
| [`src/tool-calls.ts`](src/tool-calls.ts) | 工具调度:独占屏障与有界并行池 |
|
|
105
|
+
| [`src/runtime-context.ts`](src/runtime-context.ts) | 逐步骤运行时上下文快照处理 |
|
|
106
|
+
| [`src/constants.ts`](src/constants.ts) | `DEFAULT_MAX_PARALLEL_TOOL_CALLS` |
|
|
107
|
+
|
|
108
|
+
### 创建与拆除
|
|
109
|
+
|
|
110
|
+
创建是同一个受回滚保护的事务:构造私有会话、具象 agent 与带作用域上下文;等待可选 setup;进入两个注册表;宣告 `session/created`;等待串行 `agent/created` 监听器;随后释放已排队输入。创建运行时子 Agent 的调用方设置 `options.parentAgent`;调用方 Context 则单独拥有事务和存活句柄。Setup、commit、监听器失败或所有者 dispose 都会回滚已准备的资源。已送达的宣告仍可被观察,并有配对的销毁通知。Teardown 停止并排空驱动器、撤销作用域、关闭会话写路径、detach agent,再 detach 会话。每次 detach 都绑定到确切进入的对象,因此陈旧 disposer 无法移除之后出现的同 id 替代项。
|
|
111
|
+
|
|
112
|
+
工厂卸载时在整个作用域清理期间保留 inbox 投影与轮次边界投影,包括作用域内 abort 监听器发起取消的时刻,直到所有 Agent teardown 和启动任务结束。清理失败不会使这些投影提前释放。调用方卸载、工厂卸载和句柄释放等待同一次 teardown。
|
|
113
|
+
|
|
114
|
+
### 持久化集成
|
|
115
|
+
|
|
116
|
+
循环是会话写句柄在生产环境中的获取点。挂载 `ctx.sessionPersistence` 后,`create`/`createAgent` 调用 `persistence.create(header)`——在发布之前存储持久身份并取得写所有权——并通过句柄追加构造 seed;`resume` 先调用 `persistence.open(id, 'write')`(排除同 id 的并发恢复),通过句柄读取物理上有效的日志,并为在轮次中途崩溃的日志把 `interruptedTurnClosers` 作为普通批次追加——语义崩溃修复是 agent 层的职责,而非存储入口。发布前的最后一刻,`appendUnstoredSuffix` 存储 setup 窗口期间追加的事件(seed 标记、委派策略记录),它们绝不会经由 `session/event` 重新发出。发布之后,挂载的后端按会话 id 把该会话的 `session/event` 批次、`session/flush` 屏障与 `session/disposed` 退役路由进活跃写句柄;循环只通过它拥有的句柄触碰存储。在后端仍保持挂载时,teardown 会在驱动器提交收尾事件后关闭句柄;close 排空已路由的缓冲并释放写所有权。没有后端时,会话只存在于内存中,其余一切不变。
|
|
117
|
+
|
|
118
|
+
### 轮次与步骤流程
|
|
119
|
+
|
|
120
|
+
驱动器在其整个生命周期内拥有一个 agent,并在 `ctx.agents.withInitiator(agent, ...)` 内运行。`AgentLoop` 在服务生命周期内注册标准 `inbox` 投影,因此冷读取在没有 Agent 和所有 Agent 卸载后都可用。其包内部 `ReactLoopInbox` 使用该共享投影执行结构化命令与仅供 loop 使用的领取操作。在轮次边界,它先打开持久轮次,再原子领取待处理的 next-step 输入与一条排队提示词;在步骤之间则只领取 next-step 输入。驱动器组装提示词与工具,并运行 `agent/pre-step`;fallback 在监听器执行后投影运行时上下文。被拒绝的决定或空的首批输入不打开步骤。接纳后的首次尝试先记录 `step/start`,再运行 `agent/request` waterfall 与 `prepareCall()`;这两个异步阶段都看不到待提交的系统提示词与已接纳用户消息进入历史,在任一阶段取消都不会提交这两者。每次尝试时,循环随后依据已准备调用的能力,同步将渲染后的提示词与存活的 `system/message` 节点协调一致、刷新已注册运行时事实而不重新组装绑定的提示词或工具、仅在首次尝试追加已接纳的 `user/message` 批次并原位替换循环自有上下文候选、恢复被移除的运行时上下文、按需记录 header 与 context,再派生并冻结请求,通过该绑定的已准备调用发起流式请求。重试复用同一份已渲染组装结果,不重复组装、`agent/pre-step` 或用户消息准入。协调过程可见 pre-step 与重试中的压缩;序列中断时将提示词归并到头部,而非在已提交用户消息之后追加更新。请求由 `header.config`、`deriveMessages()` 与 `header.tools` 构成,不携带 `system` 字段。每次模型尝试会发出一个进程本地 `start`,仅在匹配的持久 assistant-frame 结算之后发出各个 `chunk`,并恰好发出一个终态 `end`;最终组装或消息追加失败时以 `aborted` 结算,`committed` 则出现在持久 `assistant/message` 之后。每次成功的模型调用都恰好追加一个 message 锚点,被取消的流则追加带 `interrupted: true` 的锚点并携带已交付前缀,使下一次请求包含用户看到的内容。在步骤内,独占调用形成屏障,并行安全调用使用有界滚动池;策略、持久结果与结果上下文保持模型顺序。
|
|
121
|
+
|
|
122
|
+
提示词准入依据实际的 `prepareCall()` 结果,而非先前的 `request/context`。没有系统节点时,即使提示词为空也追加(预留第 0 号节点,但不产生协议消息)。在不具备能力的路由上或新请求序列开始时,非空渲染文本归并到首个系统节点:每个非空的后续系统节点分别收到有日志记录的空内容替换,随后按需重写头节点。未生效的空尾节点无需替换,也不决定有效文本。即使最新有效文本未变,也执行归并。延续中的 `in-history` 序列在有效提示词不变时不产生事件,非空变更则追加。无论路由或序列状态如何,空渲染文本都会通过有日志记录的逐节点空内容替换清除每个非空的后续系统节点,再按需清空头节点。模型不会继续看到旧指令。空头节点且没有生效的后续系统节点表示没有提示词;重复清除与恢复会话都保持为空。重新提供的非空提示词遵循同一路由/序列规则:延续中的具备能力路由可以追加它,不具备能力的路由或新序列则重新填充头节点。以下情况开启序列:pre-step 决定声明 `startsRequestSeries`,或 `session.surface.contentGeneration` 自附接或上次请求以来发生变化(替换或图片省略决定)。仅当已准备路由未声明 `toolUpdate` 时,可见工具 schema 变化才强制归并提示词。同时支持工具和系统提示词更新的路由可以追加变更后的提示词并激活新增工具,无需改写此前消息。如果其他准入条件替换系统节点,请求会以当前工具声明开启新序列。恢复与单纯的提供方或模型切换都延续序列;准入仍由已准备的路由决定。逐节点的空内容替换保留其间历史,无需 surface 删除操作。
|
|
123
|
+
|
|
124
|
+
### 失败与取消
|
|
125
|
+
|
|
126
|
+
`turn/end` 声明的类型是 `TurnEndCancelCause`;取消时,循环在其中记录一份新的 `AgentCancelCause`,保留调用方的 `kind` 和 hook 的 `reason` 文本。实时 `AbortSignal.reason` 仍是调用方的那个对象,传输层可能向其添加属性——Node 的 fetch 会给它赋一个 `stack`——因此这份拷贝既让该调用栈不进入日志,也让结束事件保持可追加。
|
|
127
|
+
|
|
128
|
+
最终适配器选择、分发与迭代失败以终止结束的形式到达并进入 `agent/request-error`;处理该失败的监听器返回 `{ kind: 'retry' }` 且不调用 `next()`,未被处理的失败则是终态。Middleware、结果处理、工具及其他扩展失败仍会抛出并直接关闭轮次——插件失败结束的是轮次,不是循环。取消后未分发的模型工具调用会收到合成的 `tool/call` 加 `ABORTED_BEFORE_DISPATCH` 结果对。
|
|
129
|
+
|
|
130
|
+
关闭失败步骤之前,驱动器为每个尚无结果的 assistant 工具调用记录错误结果。已有 `tool/call` 记录但尚无已提交结果的调用获得 `TOOL_OUTCOME_UNKNOWN`;没有调用记录的请求获得 `TOOL_NOT_STARTED`。已提交的结果保持完整,已启动的派发先结算再恢复,轮次保留原始失败。这些结果让后续请求使用配对完整的工具历史,而不自动重试结果不明的操作。
|
|
131
|
+
|
|
132
|
+
</details>
|
|
133
|
+
|
|
134
|
+
-----
|
|
135
|
+
|
|
136
|
+
<a id="further-exploration"></a>
|
|
137
|
+
## 进一步探索
|
|
138
|
+
|
|
139
|
+
包级约定对大多数消费方已经足够;需要周边领域与设计原理时再阅读以下页面。
|
|
140
|
+
|
|
141
|
+
- [agent 包](../agent/README.zh.md)——本循环实现的 `Agent` 句柄、注册表与 `agent/*` 事件。
|
|
142
|
+
- [Core 子系统](../../../docs/subsystems/core.zh.md)——轮次流与拦截决策。
|
|
143
|
+
- [会话子系统](../../../docs/subsystems/session.zh.md)——循环写入并据此派生的持久日志。
|
|
144
|
+
- [工具子系统](../../../docs/subsystems/tools.zh.md)——循环分发所经过的流水线。
|
|
145
|
+
- [core 分组地图](../README.zh.md)——core 各包如何组合。
|
|
146
|
+
|
|
147
|
+
-----
|
|
148
|
+
|
|
149
|
+
<a id="model-experience"></a>
|
|
150
|
+
## 模型体验
|
|
151
|
+
|
|
152
|
+
### 完整对话请求
|
|
153
|
+
|
|
154
|
+
#### 模型看到什么
|
|
155
|
+
|
|
156
|
+
每个步骤中,循环会发送会话的派生消息与可见工具 schema。非空的 `system/message` 节点承载提示词,最新一条是有效版本;空渲染文本会从派生历史中清除所有提示词版本。它提供 `provider` 与 `model` 变量值,但不添加固定文案。工作目录服务通过用户上下文提供当前目录。 请求头始终记录当前有效工具。将其名称与前一请求头比较后,添加和移除合并为一条 `developer/message`,不依赖模型能力;添加记录引用新请求头。请求还携带 `Session.toolHistory()`,供 LLM 运行时构造提供方声明。
|
|
157
|
+
|
|
158
|
+
#### Token 影响
|
|
159
|
+
|
|
160
|
+
系统文本与 schema 在每个步骤都会再次计入,在 `in-history` 路由上,每个保留的提示词版本都会持续计入,直到压缩将其遮蔽或提示词协调将其清空。逐 agent 作用域决定贡献,而权威组装 waterfall 可以改变最终请求,并使其监听器负责保持协议连贯。
|
|
161
|
+
|
|
162
|
+
#### KV Cache 影响
|
|
163
|
+
|
|
164
|
+
前缀复用要求在同一提供方与模型路由下,此前消息和声明保持逐字节一致。渲染后的提示词未变时,缓存前缀得以保留,除非不具备能力的路由或新请求序列必须归并保留的历史内系统节点。原地替换某个系统节点的提示词变更会使请求从该节点的第一个 token 起就不同——该节点是第 0 号节点时则整个请求都不同——因此提供方前缀缓存从那里开始未命中;当已准备调用声明 `systemPromptUpdate: 'in-history'` 时,同一请求序列延续期间的非空提示词变更会追加到已缓存历史之后,因此直到该历史末尾的前缀仍可复用。更改保留工具的定义或重建声明,会从第一个改变的请求 token 起使复用失效。
|
|
165
|
+
|
|
166
|
+
### 保留的消息历史
|
|
167
|
+
|
|
168
|
+
#### 模型看到什么
|
|
169
|
+
|
|
170
|
+
已接纳的 user 消息、assistant 消息、工具调用与结果、注入上下文与 steering(中途引导)都会记录,并在后续步骤中发送。原始流分片、生命周期边界与其他仅写入日志的事件会被排除。
|
|
171
|
+
|
|
172
|
+
#### Token 影响
|
|
173
|
+
|
|
174
|
+
输入会随每条表层消息增长,直到压缩替换遮蔽较旧节点;包含多个步骤的工具轮次会在每个步骤重新发送累积的历史。
|
|
175
|
+
|
|
176
|
+
#### KV Cache 影响
|
|
177
|
+
|
|
178
|
+
普通历史增长仅追加,并保留可复用条目。表层替换或压缩会从第一个被遮蔽的历史 token 起使复用失效。
|
|
179
|
+
|
|
180
|
+
### 取消后未分发的调用
|
|
181
|
+
|
|
182
|
+
#### 模型看到什么
|
|
183
|
+
|
|
184
|
+
如果后续请求回放一个中止的步骤,取消所阻止分发的每个工具调用都有错误码 `ABORTED_BEFORE_DISPATCH`,结果文本为 `Error: tool call aborted before dispatch`。
|
|
185
|
+
|
|
186
|
+
#### Token 影响
|
|
187
|
+
|
|
188
|
+
每个跳过的调用都会在历史中保留一个固定错误结果,直到压缩将其遮蔽。
|
|
189
|
+
|
|
190
|
+
#### KV Cache 影响
|
|
191
|
+
|
|
192
|
+
仅追加;每个合成结果都位于可复用请求前缀之后,不会使现有 KV Cache 条目失效。
|
|
193
|
+
|
|
194
|
+
### 步骤失败后尚无结果的调用
|
|
195
|
+
|
|
196
|
+
#### 模型看到什么
|
|
197
|
+
|
|
198
|
+
每个尚无结果的工具调用都会在后续历史中获得错误结果。对于已有记录的调用,结果说明 `Its outcome is unknown.`,且仅允许重试只读或幂等操作;可能存在副作用时,必须先核验外部状态或询问用户。没有启动记录的调用说明 `The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed.`。
|
|
199
|
+
|
|
200
|
+
#### Token 影响
|
|
201
|
+
|
|
202
|
+
每个尚无结果的调用都会在历史中保留一个恢复结果,直到压缩将其遮蔽。
|
|
203
|
+
|
|
204
|
+
#### KV Cache 影响
|
|
205
|
+
|
|
206
|
+
恢复结果追加在既有历史之后,保留其可复用前缀。
|
|
207
|
+
|
|
208
|
+
## 已知限制与延期工作
|
|
209
|
+
|
|
210
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
这些限制说明循环何时需要特别留意。它们是当前包约束,不是任务积压。
|
|
214
|
+
|
|
215
|
+
- **分类是一元的**:安全性取决于比较同级调用或资源的调用必须保持独占([原理](../../../.agents/notes/implemented/feature/2026-07-10-parallel-tool-call-execution.zh.md))。
|
|
216
|
+
- **根上下文关停的持久化**:存在活跃 Agent 时,关停根上下文可能使最终的 `turn/end` 仅留在内存中,未写入持久化日志。Agent teardown 完成不保证该事件已保存。
|
|
217
|
+
- **此前已关闭的不一致历史**:失败步骤恢复不会改写已关闭历史轮次中尚无结果的调用。
|
|
218
|
+
- **配置标签默认对应新会话**:省略 `sessionId` 时,每次启动都会创建新的 `${id}-session-<uuid>`;如需确切的恢复或创建行为,必须显式提供稳定的 `sessionId`,而 `resumeSessionId` 要求已有持久化历史。
|
|
219
|
+
- **配置 agent 没有逐 agent persona 字段或 setup 钩子**:它们使用部署 persona;只有编程式 `ctx.agents.create()` / `resume()` 工厂选项支持带作用域的 persona 与工具组合。
|
|
220
|
+
- **没有内置轮次预算**:工具调用或 steering 会让当前轮次继续;限制失控轮次的策略必须从既有生命周期扩展点(如 `agent/turn-stopping`)执行取消。
|
|
221
|
+
|
|
222
|
+
<a id="dev-note"></a>
|
|
223
|
+
### 开发备注
|
|
224
|
+
|
|
225
|
+
<details>
|
|
226
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
227
|
+
|
|
228
|
+
无。
|
|
229
|
+
|
|
230
|
+
</details>
|