From cd6bd5c1882b455df0574f2d3dc260f3cdd67419 Mon Sep 17 00:00:00 2001 From: fz Date: Tue, 4 Aug 2026 11:48:34 +0800 Subject: [PATCH 01/25] fix(llm): honor DeepSeek SSE keep-alives --- ...-21-bounded-llm-request-recovery.i18n.yaml | 4 +-- ...2026-06-21-bounded-llm-request-recovery.md | 4 +-- ...6-06-21-bounded-llm-request-recovery.zh.md | 4 +-- .../fixtures/deepseek-defaults.cordis.yml | 1 + .../headless-agent/tests/headless.snapshot.ts | 23 +++++++++---- packages/llm/llm-deepseek/README.i18n.yaml | 4 +-- packages/llm/llm-deepseek/README.md | 2 +- packages/llm/llm-deepseek/README.zh.md | 2 +- packages/llm/llm-deepseek/src/adapter.ts | 11 ++++-- packages/llm/llm-deepseek/src/sse.ts | 19 +++++++---- .../llm/llm-deepseek/tests/adapter.spec.ts | 34 +++++++++++++++++++ packages/llm/llm-deepseek/tests/sse.spec.ts | 10 ++++++ packages/util/timeout/README.i18n.yaml | 4 +-- packages/util/timeout/README.md | 4 +-- packages/util/timeout/README.zh.md | 4 +-- packages/util/timeout/src/index.ts | 17 ++++++++-- packages/util/timeout/tests/timeout.spec.ts | 22 ++++++++++++ 17 files changed, 134 insertions(+), 35 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml index bf6f030683..d527bb9e7b 100644 --- a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md -2026-06-21-bounded-llm-request-recovery.md: 24725dcf300cf69e9cc72580d0c8afe937d4e2b9 -2026-06-21-bounded-llm-request-recovery.zh.md: 5f03a65b00be8d3349addce82e4f3faa2af1fe7e +2026-06-21-bounded-llm-request-recovery.md: 122f30118ebe3f213a887e56a9d3505e78b23070 +2026-06-21-bounded-llm-request-recovery.zh.md: 8af62cef952eba929dd770df15fdc0666f575666 diff --git a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md index 24725dcf30..122f30118e 100644 --- a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md +++ b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.md @@ -74,9 +74,9 @@ Adapters perform one provider request per `stream()` call. The pi-ai adapter rem ### Bound stalled streams where they can be stopped -Each adapter exposes a validated `streamIdleTimeoutMs` configuration field with the five-minute prior-art default cited above. The interval is capped at Node's maximum timer delay so it cannot be clamped to one millisecond. It covers each outstanding iterator `next()` from demand to the next valid `StreamChunk`; time a consumer spends between `next()` calls is not provider idle time. +Each adapter exposes a validated `streamIdleTimeoutMs` configuration field with the five-minute prior-art default cited above. The interval is capped at Node's maximum timer delay so it cannot be clamped to one millisecond. It covers each outstanding iterator `next()` from demand to adapter-recognized provider activity; time a consumer spends between `next()` calls is not provider idle time. DeepSeek SSE comments count as transport activity but never become `StreamChunk` values or session-log events. -`@deepseek-ai/dsh-timeout` exposes a rearmable idle-watchdog primitive. One stable local `AbortController` is fused with the caller signal and passed to the transport for the whole adapter call; each outstanding `next()` arms the watchdog, resolution disarms it, and the next demand rearms it. Timeout aborts that stable controller with a capability-owned `TimeoutReason`, and `finally` clears the timer. The adapter classifies its watchdog as `TIMEOUT` and an earlier upstream abort as `ABORTED`. The existing one-shot `deadline()` is not presented as a sliding timer. +`@deepseek-ai/dsh-timeout` exposes a rearmable idle-watchdog primitive. One stable local `AbortController` is fused with the caller signal and passed to the transport for the whole adapter call; each outstanding `next()` arms the watchdog, resolution disarms it, and the next demand rearms it. Out-of-band transport activity calls `pulse()` to rearm an outstanding demand without yielding a value. Timeout aborts that stable controller with a capability-owned `TimeoutReason`, and `finally` clears the timer. The adapter classifies its watchdog as `TIMEOUT` and an earlier upstream abort as `ABORTED`. The existing one-shot `deadline()` is not presented as a sliding timer. Boundary tests prove termination at both actual transports. The hand-written adapter aborts its fetch/reader, and the pi-ai adapter maps the stable signal through the SDK and proves the SDK closes the response. A timer that merely rejects a consumer promise while leaving the request running does not satisfy the contract. diff --git a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.zh.md b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.zh.md index 5f03a65b00..8af62cef95 100644 --- a/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.zh.md +++ b/.agents/notes/implemented/architecture/2026-06-21-bounded-llm-request-recovery.zh.md @@ -74,9 +74,9 @@ agent-spine 演示组合包加载该插件,因此共享的 stdio/TUI、一次 ### 在能够终止停滞流的位置施加边界 -每个适配器都公开一个经过验证的 `streamIdleTimeoutMs` 配置字段,默认值采用上文引用的五分钟先例。该间隔不超过 Node 的最大定时器延迟,因此不会被钳制为 1 毫秒。它覆盖每个尚未完成的迭代器 `next()`:从消费方请求下一项开始,到下一条有效 `StreamChunk` 到达为止;消费方在两次 `next()` 调用之间花费的时间不属于提供方空闲时间。 +每个适配器都公开一个经过验证的 `streamIdleTimeoutMs` 配置字段,默认值采用上文引用的五分钟先例。该间隔不超过 Node 的最大定时器延迟,因此不会被钳制为 1 毫秒。它覆盖每个尚未完成的迭代器 `next()`:从消费方请求下一项开始,到适配器识别到提供方活动为止;消费方在两次 `next()` 调用之间花费的时间不属于提供方空闲时间。DeepSeek SSE(Server-Sent Events)注释计为传输活动,但绝不会成为 `StreamChunk` 值或会话日志事件。 -`@deepseek-ai/dsh-timeout` 公开一个可重新布防的空闲看门狗原语。一个稳定的局部 `AbortController` 会与调用方信号融合,并在整个适配器调用期间传给传输层;每个尚未完成的 `next()` 都会布防看门狗,该调用完成时解除布防,下一次请求数据时再重新布防。超时会使用能力自身拥有的 `TimeoutReason` 中止这个稳定控制器,`finally` 则会清除定时器。适配器将自身看门狗归类为 `TIMEOUT`,将更早发生的上游中止归类为 `ABORTED`。现有的一次性 `deadline()` 不会被描述为滑动计时器。 +`@deepseek-ai/dsh-timeout` 公开一个可重新布防的空闲看门狗原语。一个稳定的局部 `AbortController` 会与调用方信号融合,并在整个适配器调用期间传给传输层;每个尚未完成的 `next()` 都会布防看门狗,该调用完成时解除布防,下一次请求数据时再重新布防。带外传输活动会调用 `pulse()`,在不产生值的情况下为尚未完成的需求重新布防。超时会使用能力自身拥有的 `TimeoutReason` 中止这个稳定控制器,`finally` 则会清除定时器。适配器将自身看门狗归类为 `TIMEOUT`,将更早发生的上游中止归类为 `ABORTED`。现有的一次性 `deadline()` 不会被描述为滑动计时器。 边界测试证明两个实际传输层都能终止。手写适配器会中止其 fetch/reader,pi-ai 适配器会把稳定信号映射到 SDK,并证明 SDK 会关闭响应。如果定时器只拒绝消费方 promise,却让请求继续运行,就不满足此契约。 diff --git a/examples/headless-agent/tests/fixtures/deepseek-defaults.cordis.yml b/examples/headless-agent/tests/fixtures/deepseek-defaults.cordis.yml index cd472f737d..829af9f813 100644 --- a/examples/headless-agent/tests/fixtures/deepseek-defaults.cordis.yml +++ b/examples/headless-agent/tests/fixtures/deepseek-defaults.cordis.yml @@ -8,6 +8,7 @@ apiKey: snapshot-key baseURL: !!js process.env.DSH_SNAPSHOT_BASE_URL thinking: disabled + streamIdleTimeoutMs: 100 - id: cli-agent config: provider: deepseek-official diff --git a/examples/headless-agent/tests/headless.snapshot.ts b/examples/headless-agent/tests/headless.snapshot.ts index 8b48165a83..fd1bbd3086 100644 --- a/examples/headless-agent/tests/headless.snapshot.ts +++ b/examples/headless-agent/tests/headless.snapshot.ts @@ -66,12 +66,21 @@ async function deepseekDefaultsServer(): Promise { request.on('end', () => { requests.push(JSON.parse(body) as JsonObject) response.writeHead(200, { 'content-type': 'text/event-stream' }) - response.end([ - 'data: {"choices":[{"delta":{"content":"DEFAULTS_OK"}}]}', - 'data: {"choices":[{"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":1}}', - 'data: [DONE]', - '', - ].join('\n\n')) + let keepAlives = 3 + const write = (): void => { + if (keepAlives-- > 0) { + response.write(': keep-alive\n\n') + setTimeout(write, 60) + return + } + response.end([ + 'data: {"choices":[{"delta":{"content":"DEFAULTS_OK"}}]}', + 'data: {"choices":[{"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":1}}', + 'data: [DONE]', + '', + ].join('\n\n')) + } + setTimeout(write, 60) }) }) await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) @@ -297,7 +306,7 @@ describe('headless stream-json snapshots', () => { `) }, LOADER_SMOKE_TEST_TIMEOUT_MS) - it('logs and sends the DeepSeek adapter maxTokens default through the one-shot app', async () => { + it('keeps provider comments alive and sends DeepSeek defaults through the one-shot app', async () => { const server = await deepseekDefaultsServer() try { const result = await runLoaderSmoke({ diff --git a/packages/llm/llm-deepseek/README.i18n.yaml b/packages/llm/llm-deepseek/README.i18n.yaml index 45d9cee054..cb18c83eff 100644 --- a/packages/llm/llm-deepseek/README.i18n.yaml +++ b/packages/llm/llm-deepseek/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/llm/llm-deepseek/README.md -README.md: 020aa65073495526be3f32912b7cd06667c52a2e -README.zh.md: 4c655e90ba00340c056f6ac16159621f7a8c1ddb +README.md: 2ecdb4330e5871bbaf0da6fc583083a06c935486 +README.zh.md: 63eb7be330806b668e12867ed906d0d88acaff1f diff --git a/packages/llm/llm-deepseek/README.md b/packages/llm/llm-deepseek/README.md index 020aa65073..2ecdb4330e 100644 --- a/packages/llm/llm-deepseek/README.md +++ b/packages/llm/llm-deepseek/README.md @@ -46,7 +46,7 @@ The same exact-model result exposes ordered `off`, `high`, and `max` efforts und `thinking: disabled` is a deployment lock that publishes only `off` with `off` as its default. Omitting `reasoningEffort` or configuring it as `off` is valid; configuring `high` or `max` fails plugin loading, and a direct per-request attempt to enable thinking fails before network I/O. A request with `GenerateOptions.purpose: 'session-title'` also forces thinking disabled and omits the already-resolved effort, reserving its bounded output for visible title text without changing conversation or compaction defaults. -`streamIdleTimeoutMs` bounds each outstanding provider read, including the initial `fetch`, without counting time the consumer spends between chunks. One stable abort signal reaches the request and body reader for the whole call; expiry stops the transport and throws `LlmError('TIMEOUT')`, while an earlier caller abort throws `LlmError('ABORTED')`. The adapter makes exactly one provider request per `stream()` call; it registers the configured policy as provider metadata, and `dsh-llm-retry` separately executes it at durable agent-step boundaries. +`streamIdleTimeoutMs` bounds each outstanding provider read, including the initial `fetch`, without counting time the consumer spends between chunks. DeepSeek SSE comments rearm an outstanding read as transport activity but never become `StreamChunk` values or session-log events. One stable abort signal reaches the request and body reader for the whole call; expiry stops the transport and throws `LlmError('TIMEOUT')`, while an earlier caller abort throws `LlmError('ABORTED')`. The adapter makes exactly one provider request per `stream()` call; it registers the configured policy as provider metadata, and `dsh-llm-retry` separately executes it at durable agent-step boundaries. ## Dynamic configuration (settings + credentials) diff --git a/packages/llm/llm-deepseek/README.zh.md b/packages/llm/llm-deepseek/README.zh.md index 4c655e90ba..63eb7be330 100644 --- a/packages/llm/llm-deepseek/README.zh.md +++ b/packages/llm/llm-deepseek/README.zh.md @@ -46,7 +46,7 @@ harness LLM(大语言模型)seam 的 DeepSeek chat-completions 适配器: `thinking: disabled` 是部署锁定:它只公布 `off`,并以 `off` 为默认值。省略 `reasoningEffort` 或将其配置为 `off` 均有效;配置 `high` 或 `max` 会使插件加载失败,直接按请求启用思考也会在网络 I/O 前失败。携带 `GenerateOptions.purpose: 'session-title'` 的请求也会强制禁用思考并省略已解析的推理强度,将有界输出保留给可见标题文本,不改变会话或压缩(compaction)默认值。 -`streamIdleTimeoutMs` 会限制每次未完成提供方读取,包括初始 `fetch`,但不计入消费方在分片间花费的时间。同一个稳定的 abort 信号会在整个调用期间传递给请求与 body reader;过期会停止传输并抛出 `LlmError('TIMEOUT')`,较早的调用方 abort 则抛出 `LlmError('ABORTED')`。适配器每次 `stream()` 调用恰好发起一次提供方请求;它把已配置策略注册为提供方元数据,再由 `dsh-llm-retry` 在持久化的 agent(智能体)步骤边界单独执行该策略。 +`streamIdleTimeoutMs` 会限制每次未完成提供方读取,包括初始 `fetch`,但不计入消费方在分片间花费的时间。DeepSeek SSE 注释会作为传输活动使尚未完成的读取重新布防,但绝不会成为 `StreamChunk` 值或会话日志事件。同一个稳定的 abort 信号会在整个调用期间传递给请求与 body reader;过期会停止传输并抛出 `LlmError('TIMEOUT')`,较早的调用方 abort 则抛出 `LlmError('ABORTED')`。适配器每次 `stream()` 调用恰好发起一次提供方请求;它把已配置策略注册为提供方元数据,再由 `dsh-llm-retry` 在持久化的 agent(智能体)步骤边界单独执行该策略。 ## 动态配置(settings + credentials) diff --git a/packages/llm/llm-deepseek/src/adapter.ts b/packages/llm/llm-deepseek/src/adapter.ts index 85985c41d8..c5f9655fa1 100644 --- a/packages/llm/llm-deepseek/src/adapter.ts +++ b/packages/llm/llm-deepseek/src/adapter.ts @@ -215,7 +215,13 @@ export class DeepSeekAdapter extends LlmAdapter { ? consumer.signal : AbortSignal.any([options.signal, consumer.signal]) using watchdog = idleWatchdog(upstream, connection.streamIdleTimeoutMs, STREAM_IDLE_TIMEOUT_CODE) - const iterator = this.request(options, watchdog.signal, connection, apiKey)[Symbol.asyncIterator]() + const iterator = this.request( + options, + watchdog.signal, + connection, + apiKey, + () => { watchdog.pulse() }, + )[Symbol.asyncIterator]() let exhausted = false try { while (true) { @@ -256,6 +262,7 @@ export class DeepSeekAdapter extends LlmAdapter { signal: AbortSignal, connection: DeepSeekConnectionOptions, apiKey: string, + onComment: () => void, ): AsyncIterable { const body = serializeRequest(options, connection.defaults) // Prepared outside the try so the TRANSPORT label below covers exactly the @@ -321,6 +328,6 @@ export class DeepSeekAdapter extends LlmAdapter { throw new LlmError('DeepSeek API returned no response body', 'EMPTY_RESPONSE') } - yield* translate(parseSse(response.body)) + yield* translate(parseSse(response.body, onComment)) } } diff --git a/packages/llm/llm-deepseek/src/sse.ts b/packages/llm/llm-deepseek/src/sse.ts index a8807a857c..9126a18940 100644 --- a/packages/llm/llm-deepseek/src/sse.ts +++ b/packages/llm/llm-deepseek/src/sse.ts @@ -1,11 +1,12 @@ /** * Decode an SSE byte stream into event `data` payloads. Framing — chunk * reassembly, UTF-8/CRLF/BOM handling, comment and non-data field skipping, - * multi-`data:` joining — is `eventsource-parser`'s; this module keeps only - * the DeepSeek protocol: the literal `[DONE]` is yielded so the caller owns - * final flushing, and EOF before it raises {@link LlmError}. Framing is - * spec-strict: an event dispatches only on its blank-line terminator, so an - * unterminated tail at EOF is truncation, not a flushable payload. + * multi-`data:` joining — is `eventsource-parser`'s. Comments are reported + * only through an optional transport-activity callback. This module keeps the + * DeepSeek protocol: the literal `[DONE]` is yielded so the caller owns final + * flushing, and EOF before it raises {@link LlmError}. Framing is spec-strict: + * an event dispatches only on its blank-line terminator, so an unterminated + * tail at EOF is truncation, not a flushable payload. * * @module dsh-llm-deepseek/sse */ @@ -21,12 +22,16 @@ export const DONE = '[DONE]' * value and returns; throws `LlmError('STREAM_CLOSED')` when the stream ends * without it (truncated response — the model call cannot be trusted). * @param stream - raw SSE bytes; reads may split anywhere, including mid-UTF-8 sequence. + * @param onComment - optional transport-activity callback; comments never enter the yielded payload stream. * @returns each event's data payload in arrival order, the `[DONE]` sentinel last. */ -export async function* parseSse(stream: ReadableStream): AsyncGenerator { +export async function* parseSse( + stream: ReadableStream, + onComment?: (comment: string) => void, +): AsyncGenerator { const events = stream .pipeThrough(new TextDecoderStream()) - .pipeThrough(new EventSourceParserStream()) + .pipeThrough(new EventSourceParserStream({ onComment })) for await (const { data } of events) { yield data if (data === DONE) return diff --git a/packages/llm/llm-deepseek/tests/adapter.spec.ts b/packages/llm/llm-deepseek/tests/adapter.spec.ts index ec4a271f15..8240dea7e6 100644 --- a/packages/llm/llm-deepseek/tests/adapter.spec.ts +++ b/packages/llm/llm-deepseek/tests/adapter.spec.ts @@ -545,6 +545,40 @@ describe('DeepSeekAdapter against a mock server', () => { fetchSpy.mockRestore() } }) + + it('keeps an idle provider read alive through SSE comments', async () => { + vi.useFakeTimers() + const encoder = new TextEncoder() + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(() => { + const body = new ReadableStream({ + start(controller) { + setTimeout(() => { controller.enqueue(encoder.encode(': keep-alive\n\n')) }, 75) + setTimeout(() => { controller.enqueue(encoder.encode(': keep-alive\n\n')) }, 150) + setTimeout(() => { + controller.enqueue(encoder.encode(textEvents.map(event => `data: ${event}\n\n`).join(''))) + controller.close() + }, 225) + }, + }) + return Promise.resolve(new Response(body, { status: 200 })) + }) + const adapter = adapterOf({ baseURL: 'https://example.invalid', streamIdleTimeoutMs: 100 }) + try { + const chunks: string[] = [] + const drain = (async () => { + for await (const chunk of adapter.stream({ provider: 'deepseek-official', model: 'm', messages: [] })) { + chunks.push(chunk.type) + } + })() + await vi.advanceTimersByTimeAsync(75) + await vi.advanceTimersByTimeAsync(75) + await vi.advanceTimersByTimeAsync(75) + await expect(drain).resolves.toBeUndefined() + expect(chunks).toEqual(['block-start', 'text-delta', 'block-end', 'usage', 'finish']) + } finally { + fetchSpy.mockRestore() + } + }) }) describe('plugin registration and config', () => { diff --git a/packages/llm/llm-deepseek/tests/sse.spec.ts b/packages/llm/llm-deepseek/tests/sse.spec.ts index 7ebb494a4d..67160d8079 100644 --- a/packages/llm/llm-deepseek/tests/sse.spec.ts +++ b/packages/llm/llm-deepseek/tests/sse.spec.ts @@ -31,6 +31,16 @@ describe('parseSse', () => { expect(events).toEqual(['{"a":1}', DONE]) }) + it('reports comments out of band without yielding them', async () => { + const comments: string[] = [] + const events = await collect(parseSse( + bytes(': keep-alive\n\ndata: {"a":1}\n\ndata: [DONE]\n\n'), + (comment) => { comments.push(comment) }, + )) + expect(comments).toEqual(['keep-alive']) + expect(events).toEqual(['{"a":1}', DONE]) + }) + it('stops yielding after DONE even when more data follows', async () => { const events = await collect(parseSse(bytes('data: [DONE]\n\ndata: {"late":1}\n\n'))) expect(events).toEqual([DONE]) diff --git a/packages/util/timeout/README.i18n.yaml b/packages/util/timeout/README.i18n.yaml index 0436cc4d34..a578a0ac4f 100644 --- a/packages/util/timeout/README.i18n.yaml +++ b/packages/util/timeout/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/util/timeout/README.md -README.md: 11c55a45a1255e14fb551e42ba3965453dbd94ae -README.zh.md: 8b63f00595139f9af2e9a31e8ab3c3494088e0ff +README.md: 8892b2dce53b5c315c088430ed3fcf386a0f3101 +README.zh.md: ec99f38ff92890c854a1f902e56b2278a42318b6 diff --git a/packages/util/timeout/README.md b/packages/util/timeout/README.md index 11c55a45a1..8892b2dce5 100644 --- a/packages/util/timeout/README.md +++ b/packages/util/timeout/README.md @@ -18,7 +18,7 @@ import { clampTimeout, deadline, idleWatchdog, MAX_TIMER_DELAY_MS, timeoutOf, Ti |---|---| | `clampTimeout(requested, def, max, name?)` | Validate the caller's optional positive-finite hint, fill from `def`, cap at `max`. Throws (with `name`) on a non-positive/non-finite hint. | | `deadline(upstream, timeoutMs, code)` | Fuse `upstream` cancellation with a timeout into one `AbortSignal` (`AbortSignal.any`); the timeout carries a `TimeoutReason`. `[Symbol.dispose]` clears the timer. | -| `idleWatchdog(upstream, timeoutMs, code)` | Keep one stable fused signal and arm only while its guarded async-iterator `next()` is outstanding. Resolution disarms; later demand rearms; disposal clears; concurrent demand rejects. | +| `idleWatchdog(upstream, timeoutMs, code)` | Keep one stable fused signal and arm only while its guarded async-iterator `next()` is outstanding. Resolution disarms; later demand or `pulse()` activity rearms; disposal clears; concurrent demand rejects. | | `MAX_TIMER_DELAY_MS` | Largest delay Node schedules without clamping it to one millisecond (`2_147_483_647`). Timer-owning config must not exceed it. | | `timeoutOf(signal \| { reason }, code?)` | Recover the `TimeoutReason` from an aborted signal/error, else `undefined` — the timeout-vs-cancel classifier. Pass `code` to match only THIS deadline's timer (see nesting below). | | `TimeoutReason` | The internal reason (`code` + `timeoutMs`) stamped on a timeout abort. Not a public error — providers translate it into their own error/field. | @@ -48,7 +48,7 @@ The signal only *notifies* — the caller MUST attach its own termination (`d.si Pass your own `code` to `timeoutOf` so classification composes under nesting: when the `upstream` you were handed is *itself* a deadline signal (a future `tools/execute` middleware arming a per-call deadline), `AbortSignal.any` preserves the outer `TimeoutReason` if the outer timer fires first. Scoping to your `code` makes a foreign timeout read as an ordinary upstream cancel — the correct classification from your capability's view — instead of your own timeout firing when your local timer never expired. -For a streamed transport, create one `idleWatchdog`, pass its stable `signal` into the transport, and call `watchdog.next(iterator)` for each provider read. The interval must be positive, finite, and no greater than `MAX_TIMER_DELAY_MS`; Node otherwise clamps it to one millisecond. It measures only outstanding demand, so no timer runs while downstream code renders or otherwise waits before asking for the next chunk. The primitive still only notifies, so the transport must observe the stable signal; the DeepSeek and pi-ai adapters prove that timeout closes their real response body or SDK request. +For a streamed transport, create one `idleWatchdog`, pass its stable `signal` into the transport, and call `watchdog.next(iterator)` for each provider read. Call `watchdog.pulse()` when transport activity does not yield an iterator value. The interval must be positive, finite, and no greater than `MAX_TIMER_DELAY_MS`; Node otherwise clamps it to one millisecond. It measures only outstanding demand, so no timer runs while downstream code renders or otherwise waits before asking for the next chunk. The primitive still only notifies, so the transport must observe the stable signal; the DeepSeek and pi-ai adapters prove that timeout closes their real response body or SDK request. ## What does NOT get a timeout diff --git a/packages/util/timeout/README.zh.md b/packages/util/timeout/README.zh.md index 8b63f00595..ec99f38ff9 100644 --- a/packages/util/timeout/README.zh.md +++ b/packages/util/timeout/README.zh.md @@ -18,7 +18,7 @@ import { clampTimeout, deadline, idleWatchdog, MAX_TIMER_DELAY_MS, timeoutOf, Ti |---|---| | `clampTimeout(requested, def, max, name?)` | 验证调用方可选的、值为正且有限的提示,从 `def` 填充,并限制在 `max` 以内。如果提示为非正数或非有限数,则抛出错误(包含 `name`)。 | | `deadline(upstream, timeoutMs, code)` | 将 `upstream` 取消与超时融合为一个 `AbortSignal`(`AbortSignal.any`);超时携带 `TimeoutReason`。`[Symbol.dispose]` 清除 timer。 | -| `idleWatchdog(upstream, timeoutMs, code)` | 保持一个稳定的融合信号,并且只在受保护的异步迭代器 `next()` 尚未完成时启动 timer。完成后停止 timer;后续需求重新启动 timer;dispose(资源释放)时清除;并发需求被拒绝。 | +| `idleWatchdog(upstream, timeoutMs, code)` | 保持一个稳定的融合信号,并且只在受保护的异步迭代器 `next()` 尚未完成时启动 timer。完成后停止 timer;后续需求或 `pulse()` 活动会重新启动 timer;dispose(资源释放)时清除;并发需求被拒绝。 | | `MAX_TIMER_DELAY_MS` | Node 在不将延迟限制为 1 毫秒时可调度的最大延迟(`2_147_483_647`)。负责 timer 的配置不得超过该值。 | | `timeoutOf(signal \| { reason }, code?)` | 从已中止的信号/错误中恢复 `TimeoutReason`,否则返回 `undefined`,即超时与取消的分类器。传入 `code` 可仅匹配这个 deadline 的 timer(见下文的嵌套)。 | | `TimeoutReason` | 标记在超时中止上的内部原因(`code` + `timeoutMs`)。它不是公开错误;提供方将其转换为自己的错误/字段。 | @@ -48,7 +48,7 @@ export async function runWithDeadline(upstream: AbortSignal | undefined, timeout 将你自己的 `code` 传给 `timeoutOf`,以便分类可在嵌套中组合:当你收到的 `upstream` *本身*就是 deadline 信号时(未来启动每次调用 deadline 的 `tools/execute` 中间件),如果外层 timer 首先触发,`AbortSignal.any` 会保留外层 `TimeoutReason`。将范围限定为你的 `code`,可将外部超时视为普通 upstream 取消,这才是你所属功能视角下的正确分类,而不会在本地 timer 尚未到期时就声称自己超时。 -对于流式传输,创建一个 `idleWatchdog`,将其稳定的 `signal` 传给传输层,并为提供方的每次读取调用 `watchdog.next(iterator)`。间隔必须为正有限数,且不得超过 `MAX_TIMER_DELAY_MS`;否则 Node 会将其限制为 1 毫秒。它只对尚未完成的读取请求计时,因此当下游代码进行渲染或在请求下一个分片前以其他方式等待时,timer 不会运行。该原语仍然只会通知,因此传输层必须观察稳定信号;DeepSeek 和 pi-ai 适配器证明,超时会关闭它们的真实响应正文或 SDK 请求。 +对于流式传输,创建一个 `idleWatchdog`,将其稳定的 `signal` 传给传输层,并为提供方的每次读取调用 `watchdog.next(iterator)`。当传输活动不产生迭代器值时,调用 `watchdog.pulse()`。间隔必须为正有限数,且不得超过 `MAX_TIMER_DELAY_MS`;否则 Node 会将其限制为 1 毫秒。它只对尚未完成的读取请求计时,因此当下游代码进行渲染或在请求下一个分片前以其他方式等待时,timer 不会运行。该原语仍然只会通知,因此传输层必须观察稳定信号;DeepSeek 和 pi-ai 适配器证明,超时会关闭它们的真实响应正文或 SDK 请求。 ## 哪些操作不设置超时 diff --git a/packages/util/timeout/src/index.ts b/packages/util/timeout/src/index.ts index a9bd47eb08..3fc4d2387b 100644 --- a/packages/util/timeout/src/index.ts +++ b/packages/util/timeout/src/index.ts @@ -72,6 +72,8 @@ export interface IdleWatchdog { * @returns the iterator's next result. */ next(iterator: AsyncIterator): Promise> + /** Rearm an outstanding demand after transport activity that yields no iterator value; otherwise a no-op. */ + pulse(): void /** Clear an armed timer; safe to call once at the owning stream's exit. */ [Symbol.dispose](): void } @@ -135,15 +137,20 @@ export function idleWatchdog( let outstanding = false let disposed = false + const arm = (): void => { + if (timer !== undefined) clearTimeout(timer) + timer = setTimeout(() => { + timeout.abort(new TimeoutReason(code, timeoutMs)) + }, timeoutMs) + } + return { signal, async next(iterator: AsyncIterator): Promise> { if (disposed) throw new Error('idleWatchdog is disposed') if (outstanding) throw new Error('idleWatchdog next is already outstanding') outstanding = true - timer = setTimeout(() => { - timeout.abort(new TimeoutReason(code, timeoutMs)) - }, timeoutMs) + arm() try { return await iterator.next() } finally { @@ -152,6 +159,10 @@ export function idleWatchdog( outstanding = false } }, + pulse(): void { + if (disposed || !outstanding) return + arm() + }, [Symbol.dispose](): void { if (disposed) return disposed = true diff --git a/packages/util/timeout/tests/timeout.spec.ts b/packages/util/timeout/tests/timeout.spec.ts index 11779c915f..af0389d3d7 100644 --- a/packages/util/timeout/tests/timeout.spec.ts +++ b/packages/util/timeout/tests/timeout.spec.ts @@ -229,6 +229,28 @@ describe('idleWatchdog', () => { await expect(secondNext).rejects.toBe(stableSignal.reason) }) + it('rearms outstanding demand on an out-of-band activity pulse', async () => { + vi.useFakeTimers() + const pending = Promise.withResolvers>() + const watchdog = idleWatchdog(undefined, 100, 'LLM_STREAM_IDLE_TIMEOUT') + watchdog.pulse() + await vi.advanceTimersByTimeAsync(1_000) + expect(watchdog.signal.aborted).toBe(false) + + const next = watchdog.next({ next: () => pending.promise }) + await vi.advanceTimersByTimeAsync(99) + watchdog.pulse() + await vi.advanceTimersByTimeAsync(99) + expect(watchdog.signal.aborted).toBe(false) + await vi.advanceTimersByTimeAsync(1) + expect(timeoutOf(watchdog.signal, 'LLM_STREAM_IDLE_TIMEOUT')).toMatchObject({ timeoutMs: 100 }) + pending.reject(watchdog.signal.reason) + await expect(next).rejects.toBe(watchdog.signal.reason) + + watchdog[Symbol.dispose]() + watchdog.pulse() + }) + it('keeps an earlier upstream abort distinct from its own timeout', async () => { vi.useFakeTimers() const upstream = new AbortController() From bf70345d81a35c1e821aa88a96b1dac7e3f38bb2 Mon Sep 17 00:00:00 2001 From: fz Date: Tue, 4 Aug 2026 12:00:14 +0800 Subject: [PATCH 02/25] test(snapshot): widen keep-alive timing margin --- .../headless-agent/tests/fixtures/deepseek-defaults.cordis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/headless-agent/tests/fixtures/deepseek-defaults.cordis.yml b/examples/headless-agent/tests/fixtures/deepseek-defaults.cordis.yml index 829af9f813..8a1ca177f4 100644 --- a/examples/headless-agent/tests/fixtures/deepseek-defaults.cordis.yml +++ b/examples/headless-agent/tests/fixtures/deepseek-defaults.cordis.yml @@ -8,7 +8,7 @@ apiKey: snapshot-key baseURL: !!js process.env.DSH_SNAPSHOT_BASE_URL thinking: disabled - streamIdleTimeoutMs: 100 + streamIdleTimeoutMs: 150 - id: cli-agent config: provider: deepseek-official From 7ff0cbcb7e37e95a865db9f4a2f0eb6389a57953 Mon Sep 17 00:00:00 2001 From: xjt Date: Sun, 9 Aug 2026 11:02:16 +0800 Subject: [PATCH 03/25] docs: complete Chinese proofreading and generated reference pairing --- ...026-06-11-runtime-arg-validation.i18n.yaml | 4 +- .../2026-06-11-runtime-arg-validation.zh.md | 4 +- .../2026-07-08-agent-scope-contexts.i18n.yaml | 2 +- .../2026-07-08-agent-scope-contexts.zh.md | 20 +- ...07-12-agent-scope-runtime-design.i18n.yaml | 2 +- ...026-07-12-agent-scope-runtime-design.zh.md | 66 +- ...19-gui-layering-and-rpc-protocol.i18n.yaml | 2 +- ...-07-19-gui-layering-and-rpc-protocol.zh.md | 64 +- ...7-19-gui-web-client-architecture.i18n.yaml | 2 +- ...26-07-19-gui-web-client-architecture.zh.md | 30 +- ...-19-zstandard-jsonl-session-logs.i18n.yaml | 2 +- ...6-07-19-zstandard-jsonl-session-logs.zh.md | 8 +- ...7-23-client-plugin-loading-model.i18n.yaml | 2 +- ...26-07-23-client-plugin-loading-model.zh.md | 18 +- ...ompiler-independent-typert-model.i18n.yaml | 2 +- ...27-compiler-independent-typert-model.zh.md | 24 +- ...directory-picker-capability-seam.i18n.yaml | 2 +- ...-28-directory-picker-capability-seam.zh.md | 46 +- ...-29-terminal-llm-stream-failures.i18n.yaml | 2 +- ...6-07-29-terminal-llm-stream-failures.zh.md | 20 +- ...adapter-owned-max-token-defaults.i18n.yaml | 2 +- ...-30-adapter-owned-max-token-defaults.zh.md | 2 +- ...07-30-client-locale-full-rollout.i18n.yaml | 2 +- ...026-07-30-client-locale-full-rollout.zh.md | 22 +- ...-07-30-command-row-copy-contract.i18n.yaml | 2 +- ...2026-07-30-command-row-copy-contract.zh.md | 20 +- ...-followup-enqueue-and-owned-runs.i18n.yaml | 2 +- ...7-30-followup-enqueue-and-owned-runs.zh.md | 4 +- ...-manager-native-repository-cache.i18n.yaml | 2 +- ...kage-manager-native-repository-cache.zh.md | 4 +- ...30-session-end-seed-log-boundary.i18n.yaml | 2 +- ...-07-30-session-end-seed-log-boundary.zh.md | 12 +- ...-static-repository-plugin-format.i18n.yaml | 2 +- ...7-30-static-repository-plugin-format.zh.md | 24 +- ...claimed-pre-step-inbox-lifecycle.i18n.yaml | 2 +- ...-31-claimed-pre-step-inbox-lifecycle.zh.md | 18 +- ...runtime-portable-identifier-seam.i18n.yaml | 2 +- ...ode-runtime-portable-identifier-seam.zh.md | 18 +- ...-07-31-goal-owned-durable-events.i18n.yaml | 2 +- ...2026-07-31-goal-owned-durable-events.zh.md | 6 +- ...26-08-01-packaged-ripgrep-search.i18n.yaml | 2 +- .../2026-08-01-packaged-ripgrep-search.zh.md | 14 +- ...08-02-typert-remote-method-calls.i18n.yaml | 2 +- ...026-08-02-typert-remote-method-calls.zh.md | 36 +- ...-pi-ai-declared-provider-catalog.i18n.yaml | 2 +- ...8-03-pi-ai-declared-provider-catalog.zh.md | 16 +- ...4-configuration-source-ownership.i18n.yaml | 2 +- ...08-04-configuration-source-ownership.zh.md | 14 +- ...-yaml-and-user-environment-layer.i18n.yaml | 2 +- ...ials-yaml-and-user-environment-layer.zh.md | 14 +- ...-a-provider-from-the-models-page.i18n.yaml | 2 +- ...ring-a-provider-from-the-models-page.zh.md | 2 +- ...-provider-endpoint-interrogation.i18n.yaml | 2 +- ...raft-provider-endpoint-interrogation.zh.md | 10 +- ...08-04-websocket-downlink-carrier.i18n.yaml | 2 +- ...026-08-04-websocket-downlink-carrier.zh.md | 12 +- ...e-session-jsonl-restore-pipeline.i18n.yaml | 2 +- ...large-session-jsonl-restore-pipeline.zh.md | 8 +- ...026-08-05-profile-plugin-bundles.i18n.yaml | 2 +- .../2026-08-05-profile-plugin-bundles.zh.md | 4 +- .../2026-08-05-session-preparation.i18n.yaml | 2 +- .../2026-08-05-session-preparation.zh.md | 6 +- ...08-05-slot-declaration-injection.i18n.yaml | 2 +- ...026-08-05-slot-declaration-injection.zh.md | 10 +- ...8-06-agent-event-payload-objects.i18n.yaml | 2 +- ...26-08-06-agent-event-payload-objects.zh.md | 6 +- ...ubagent-list-identity-projection.i18n.yaml | 2 +- ...06-subagent-list-identity-projection.zh.md | 20 +- ...arkdown-incremental-ast-renderer.i18n.yaml | 2 +- ...eb-markdown-incremental-ast-renderer.zh.md | 12 +- ...8-06-web-shell-dist-chunk-layout.i18n.yaml | 2 +- ...26-08-06-web-shell-dist-chunk-layout.zh.md | 22 +- ...ssion-persistence-write-batching.i18n.yaml | 2 +- ...d-session-persistence-write-batching.zh.md | 10 +- ...ient-tool-presentation-ownership.i18n.yaml | 2 +- ...8-client-tool-presentation-ownership.zh.md | 26 +- ...-20-config-hot-reload-resilience.i18n.yaml | 2 +- ...6-07-20-config-hot-reload-resilience.zh.md | 4 +- .../2026-07-27-glob-sampling.i18n.yaml | 2 +- .../bug-fix/2026-07-27-glob-sampling.zh.md | 16 +- ...d-scrollbars-and-reserved-gutter.i18n.yaml | 2 +- ...hemed-scrollbars-and-reserved-gutter.zh.md | 8 +- ...-07-28-web-agent-runtime-context.i18n.yaml | 2 +- ...2026-07-28-web-agent-runtime-context.zh.md | 12 +- ...2026-07-28-web-gui-feedback-loop.i18n.yaml | 2 +- .../2026-07-28-web-gui-feedback-loop.zh.md | 10 +- ...9-human-transcript-append-origin.i18n.yaml | 2 +- ...07-29-human-transcript-append-origin.zh.md | 38 +- ...7-29-pnpm-setup-runner-isolation.i18n.yaml | 2 +- ...26-07-29-pnpm-setup-runner-isolation.zh.md | 4 +- ...cky-composer-conversation-scroll.i18n.yaml | 2 +- ...-sticky-composer-conversation-scroll.zh.md | 16 +- ...29-web-details-session-lifecycle.i18n.yaml | 2 +- ...-07-29-web-details-session-lifecycle.zh.md | 4 +- ...07-30-approval-panel-command-cap.i18n.yaml | 2 +- ...026-07-30-approval-panel-command-cap.zh.md | 18 +- ...-30-composer-context-stack-order.i18n.yaml | 2 +- ...6-07-30-composer-context-stack-order.zh.md | 8 +- ...-07-30-hover-popup-pointer-grace.i18n.yaml | 2 +- ...2026-07-30-hover-popup-pointer-grace.zh.md | 8 +- ...select-custom-answer-composition.i18n.yaml | 2 +- ...lti-select-custom-answer-composition.zh.md | 4 +- ...rce-checkout-workdir-distinction.i18n.yaml | 2 +- ...-source-checkout-workdir-distinction.zh.md | 18 +- ...ranscript-log-ordered-projection.i18n.yaml | 2 +- ...eb-transcript-log-ordered-projection.zh.md | 40 +- ...text-layers-share-one-scrollport.i18n.yaml | 2 +- ...ser-text-layers-share-one-scrollport.zh.md | 12 +- ...1-english-compaction-checkpoints.i18n.yaml | 2 +- ...07-31-english-compaction-checkpoints.zh.md | 4 +- ...-fail-loud-releases-the-terminal.i18n.yaml | 2 +- ...7-31-fail-loud-releases-the-terminal.zh.md | 18 +- ...-fork-anchor-floors-to-event-seq.i18n.yaml | 2 +- ...7-31-fork-anchor-floors-to-event-seq.zh.md | 6 +- ...6-07-31-web-stop-preserves-queue.i18n.yaml | 2 +- .../2026-07-31-web-stop-preserves-queue.zh.md | 4 +- ...-ask-user-delegated-caller-guard.i18n.yaml | 2 +- ...8-01-ask-user-delegated-caller-guard.zh.md | 4 +- ...-08-02-goal-round-wrapup-message.i18n.yaml | 2 +- ...2026-08-02-goal-round-wrapup-message.zh.md | 20 +- ...ions-require-completed-turn-tail.i18n.yaml | 2 +- ...-actions-require-completed-turn-tail.zh.md | 2 +- ...odo-first-composer-context-order.i18n.yaml | 2 +- ...02-todo-first-composer-context-order.zh.md | 2 +- ...3-cli-signal-shutdown-escalation.i18n.yaml | 2 +- ...08-03-cli-signal-shutdown-escalation.zh.md | 6 +- ...3-hmr-initial-scan-boot-deadlock.i18n.yaml | 2 +- ...08-03-hmr-initial-scan-boot-deadlock.zh.md | 18 +- ...-composer-tab-gutter-reservation.i18n.yaml | 2 +- ...8-04-composer-tab-gutter-reservation.zh.md | 18 +- ...versation-column-one-axis-scroll.i18n.yaml | 2 +- ...-conversation-column-one-axis-scroll.zh.md | 12 +- ...-04-load-pre-react-loop-sessions.i18n.yaml | 2 +- ...6-08-04-load-pre-react-loop-sessions.zh.md | 10 +- ...ontext-meter-blind-to-compaction.i18n.yaml | 2 +- ...05-context-meter-blind-to-compaction.zh.md | 16 +- ...actions-require-a-completed-turn.i18n.yaml | 2 +- ...ail-actions-require-a-completed-turn.zh.md | 6 +- ...e-blank-session-reuse-membership.i18n.yaml | 2 +- ...space-blank-session-reuse-membership.zh.md | 16 +- ...-08-06-api-key-format-validation.i18n.yaml | 2 +- ...2026-08-06-api-key-format-validation.zh.md | 30 +- ...rding-step-owned-takeover-chrome.i18n.yaml | 2 +- ...nboarding-step-owned-takeover-chrome.zh.md | 22 +- ...06-provider-credential-lifecycle.i18n.yaml | 2 +- ...-08-06-provider-credential-lifecycle.zh.md | 10 +- ...-attribution-observed-top-ledger.i18n.yaml | 2 +- ...roll-attribution-observed-top-ledger.zh.md | 8 +- ...e-unpriced-replace-compatibility.i18n.yaml | 2 +- ...rface-unpriced-replace-compatibility.zh.md | 8 +- ...6-06-21-subagent-capability-seam.i18n.yaml | 2 +- .../2026-06-21-subagent-capability-seam.zh.md | 24 +- .../feature/2026-07-05-skill-system.i18n.yaml | 2 +- .../feature/2026-07-05-skill-system.zh.md | 22 +- ...-07-08-background-subagent-tasks.i18n.yaml | 2 +- ...2026-07-08-background-subagent-tasks.zh.md | 16 +- ...26-07-20-dsh-cli-personal-config.i18n.yaml | 2 +- .../2026-07-20-dsh-cli-personal-config.zh.md | 32 +- ...continuable-background-subagents.i18n.yaml | 2 +- ...-21-continuable-background-subagents.zh.md | 40 +- ...subagent-catalog-and-list-agents.i18n.yaml | 2 +- ...ble-subagent-catalog-and-list-agents.zh.md | 30 +- ...026-07-23-web-assistant-markdown.i18n.yaml | 2 +- .../2026-07-23-web-assistant-markdown.zh.md | 8 +- ...7-23-web-permission-and-approval.i18n.yaml | 2 +- ...26-07-23-web-permission-and-approval.zh.md | 16 +- ...model-facing-session-query-tools.i18n.yaml | 2 +- ...-24-model-facing-session-query-tools.zh.md | 20 +- ...n-list-browsing-and-manual-order.i18n.yaml | 2 +- ...ssion-list-browsing-and-manual-order.zh.md | 40 +- ...-07-25-workspace-ui-product-flow.i18n.yaml | 2 +- ...2026-07-25-workspace-ui-product-flow.zh.md | 36 +- ...-07-26-todo-parallel-in-progress.i18n.yaml | 2 +- ...2026-07-26-todo-parallel-in-progress.zh.md | 14 +- ...ative-workspace-directory-picker.i18n.yaml | 2 +- ...27-native-workspace-directory-picker.zh.md | 4 +- ...-07-27-skill-catalog-hot-refresh.i18n.yaml | 2 +- ...2026-07-27-skill-catalog-hot-refresh.zh.md | 12 +- ...-27-trajectory-inspection-ledger.i18n.yaml | 2 +- ...6-07-27-trajectory-inspection-ledger.zh.md | 16 +- ...6-07-27-web-session-fork-actions.i18n.yaml | 2 +- .../2026-07-27-web-session-fork-actions.zh.md | 20 +- .../2026-07-27-web-session-search.i18n.yaml | 2 +- .../2026-07-27-web-session-search.zh.md | 14 +- ...07-27-web-subagent-conversations.i18n.yaml | 2 +- ...026-07-27-web-subagent-conversations.zh.md | 26 +- ...ntinuable-subagent-conversations.i18n.yaml | 2 +- ...8-continuable-subagent-conversations.zh.md | 16 +- ...026-07-28-cross-workspace-resume.i18n.yaml | 2 +- .../2026-07-28-cross-workspace-resume.zh.md | 10 +- .../2026-07-28-feedback-command.i18n.yaml | 2 +- .../feature/2026-07-28-feedback-command.zh.md | 6 +- ...26-07-28-skill-invocation-policy.i18n.yaml | 2 +- .../2026-07-28-skill-invocation-policy.zh.md | 10 +- .../2026-07-28-web-terminal-card.i18n.yaml | 2 +- .../2026-07-28-web-terminal-card.zh.md | 14 +- ...0-config-only-repository-plugins.i18n.yaml | 2 +- ...07-30-config-only-repository-plugins.zh.md | 12 +- ...continuable-subagent-report-tool.i18n.yaml | 2 +- ...-30-continuable-subagent-report-tool.zh.md | 20 +- ...0-current-sandbox-policy-context.i18n.yaml | 2 +- ...07-30-current-sandbox-policy-context.zh.md | 10 +- ...seek-onboarding-credential-setup.i18n.yaml | 2 +- ...deepseek-onboarding-credential-setup.zh.md | 8 +- ...6-07-30-queued-manual-compaction.i18n.yaml | 2 +- .../2026-07-30-queued-manual-compaction.zh.md | 8 +- .../2026-07-30-search-render-card.i18n.yaml | 2 +- .../2026-07-30-search-render-card.zh.md | 10 +- ...versioned-gui-welcome-onboarding.i18n.yaml | 2 +- ...-30-versioned-gui-welcome-onboarding.zh.md | 8 +- .../2026-07-30-web-diff-card.i18n.yaml | 2 +- .../feature/2026-07-30-web-diff-card.zh.md | 24 +- ...026-07-30-web-queue-steer-action.i18n.yaml | 2 +- .../2026-07-30-web-queue-steer-action.zh.md | 18 +- .../2026-07-30-web-read-card.i18n.yaml | 2 +- .../feature/2026-07-30-web-read-card.zh.md | 18 +- ...6-07-30-web-result-card-frontend.i18n.yaml | 2 +- .../2026-07-30-web-result-card-frontend.zh.md | 24 +- .../2026-07-30-web-result-card.i18n.yaml | 2 +- .../feature/2026-07-30-web-result-card.zh.md | 16 +- ...l-row-unified-expand-and-inspect.i18n.yaml | 2 +- ...-tool-row-unified-expand-and-inspect.zh.md | 16 +- ...1-browser-derived-initial-locale.i18n.yaml | 2 +- ...07-31-browser-derived-initial-locale.zh.md | 8 +- ...7-31-code-mode-language-dispatch.i18n.yaml | 2 +- ...26-07-31-code-mode-language-dispatch.zh.md | 14 +- ...31-even-out-shipped-tool-rosters.i18n.yaml | 2 +- ...-07-31-even-out-shipped-tool-rosters.zh.md | 18 +- ...-31-gui-full-access-confirmation.i18n.yaml | 2 +- ...6-07-31-gui-full-access-confirmation.zh.md | 20 +- ...mission-default-for-new-sessions.i18n.yaml | 2 +- ...-permission-default-for-new-sessions.zh.md | 6 +- ...07-31-session-archive-global-set.i18n.yaml | 2 +- ...026-07-31-session-archive-global-set.zh.md | 18 +- ...7-31-telemetry-anonymous-user-id.i18n.yaml | 2 +- ...26-07-31-telemetry-anonymous-user-id.zh.md | 14 +- .../2026-07-31-web-default-search.i18n.yaml | 2 +- .../2026-07-31-web-default-search.zh.md | 2 +- ...7-31-web-telemetry-default-mount.i18n.yaml | 2 +- ...26-07-31-web-telemetry-default-mount.zh.md | 24 +- ...6-07-31-web-workspace-file-links.i18n.yaml | 2 +- .../2026-07-31-web-workspace-file-links.zh.md | 16 +- ...-workspace-write-surface-default.i18n.yaml | 2 +- ...7-31-workspace-write-surface-default.zh.md | 6 +- ...026-08-01-pwsh-tool-and-executor.i18n.yaml | 2 +- .../2026-08-01-pwsh-tool-and-executor.zh.md | 22 +- ...2026-08-02-pwsh-tool-bash-parity.i18n.yaml | 2 +- .../2026-08-02-pwsh-tool-bash-parity.zh.md | 20 +- ...ssion-search-not-shipped-default.i18n.yaml | 2 +- ...2-session-search-not-shipped-default.zh.md | 14 +- ...6-08-02-web-thinking-tail-scroll.i18n.yaml | 2 +- .../2026-08-02-web-thinking-tail-scroll.zh.md | 16 +- ...2-win32-in-process-folder-dialog.i18n.yaml | 2 +- ...08-02-win32-in-process-folder-dialog.zh.md | 18 +- .../2026-08-03-fs-tool-error-remedy.i18n.yaml | 2 +- .../2026-08-03-fs-tool-error-remedy.zh.md | 24 +- ...6-08-03-web-search-source-scroll.i18n.yaml | 2 +- .../2026-08-03-web-search-source-scroll.zh.md | 24 +- ...code-and-codex-subagent-backends.i18n.yaml | 2 +- ...ude-code-and-codex-subagent-backends.zh.md | 8 +- ...nter-revealed-sidebar-scrollbars.i18n.yaml | 2 +- ...-pointer-revealed-sidebar-scrollbars.zh.md | 30 +- ...4-web-composer-shared-width-axis.i18n.yaml | 2 +- ...08-04-web-composer-shared-width-axis.zh.md | 14 +- ...b-context-source-and-steer-marks.i18n.yaml | 2 +- ...4-web-context-source-and-steer-marks.zh.md | 14 +- ...4-web-latency-throughput-metrics.i18n.yaml | 2 +- ...08-04-web-latency-throughput-metrics.zh.md | 8 +- ...eb-slash-command-fuzzy-discovery.i18n.yaml | 2 +- ...04-web-slash-command-fuzzy-discovery.zh.md | 12 +- ...composer-context-meter-breakdown.i18n.yaml | 2 +- ...-05-composer-context-meter-breakdown.zh.md | 10 +- ...26-08-05-context-form-vocabulary.i18n.yaml | 2 +- .../2026-08-05-context-form-vocabulary.zh.md | 28 +- ...feedback-gated-session-telemetry.i18n.yaml | 2 +- ...-05-feedback-gated-session-telemetry.zh.md | 2 +- .../2026-08-05-pwsh-ui-bash-parity.i18n.yaml | 2 +- .../2026-08-05-pwsh-ui-bash-parity.zh.md | 10 +- ...-08-05-web-preview-product-badge.i18n.yaml | 2 +- ...2026-08-05-web-preview-product-badge.zh.md | 4 +- ...26-08-06-bundled-dsh-badge-skill.i18n.yaml | 2 +- .../2026-08-06-bundled-dsh-badge-skill.zh.md | 2 +- ...06-resolved-theme-color-metadata.i18n.yaml | 2 +- ...-08-06-resolved-theme-color-metadata.zh.md | 4 +- ...08-06-session-completed-done-dot.i18n.yaml | 2 +- ...026-08-06-session-completed-done-dot.zh.md | 12 +- .../2026-08-06-web-install-manifest.i18n.yaml | 2 +- .../2026-08-06-web-install-manifest.zh.md | 6 +- .../2026-08-06-web-skill-tool-row.i18n.yaml | 2 +- .../2026-08-06-web-skill-tool-row.zh.md | 8 +- ...default-model-follows-the-picker.i18n.yaml | 2 +- ...-07-default-model-follows-the-picker.zh.md | 14 +- ...6-08-08-dsh-run-headless-command.i18n.yaml | 2 +- .../2026-08-08-dsh-run-headless-command.zh.md | 2 +- ...8-user-explicit-skill-invocation.i18n.yaml | 2 +- ...08-08-user-explicit-skill-invocation.zh.md | 12 +- ...26-06-11-vendor-cordis-as-source.i18n.yaml | 2 +- .../2026-06-11-vendor-cordis-as-source.zh.md | 6 +- .../2026-06-17-ts-build-config.i18n.yaml | 2 +- .../process/2026-06-17-ts-build-config.zh.md | 10 +- ...-bilingual-docs-and-pairing-gate.i18n.yaml | 4 +- ...6-07-02-bilingual-docs-and-pairing-gate.md | 2 +- ...7-02-bilingual-docs-and-pairing-gate.zh.md | 2 +- ...fig-solution-root-two-aggregates.i18n.yaml | 2 +- ...sconfig-solution-root-two-aggregates.zh.md | 6 +- ...-incremental-pr-base-retargeting.i18n.yaml | 2 +- ...7-26-incremental-pr-base-retargeting.zh.md | 2 +- ...30-generated-third-party-notices.i18n.yaml | 2 +- ...-07-30-generated-third-party-notices.zh.md | 26 +- ...30-independent-ci-consumer-build.i18n.yaml | 2 +- ...-07-30-independent-ci-consumer-build.zh.md | 2 +- ...-31-coverage-exempt-heavy-suites.i18n.yaml | 2 +- ...6-07-31-coverage-exempt-heavy-suites.zh.md | 12 +- ...staller-adopts-existing-checkout.i18n.yaml | 2 +- ...1-installer-adopts-existing-checkout.zh.md | 22 +- ...thub-stacks-and-optional-rebases.i18n.yaml | 2 +- ...e-github-stacks-and-optional-rebases.zh.md | 2 +- ...-04-forward-only-pr-issue-status.i18n.yaml | 2 +- ...6-08-04-forward-only-pr-issue-status.zh.md | 4 +- ...-06-coverage-uncovered-locations.i18n.yaml | 2 +- ...6-08-06-coverage-uncovered-locations.zh.md | 24 +- ...8-06-doc-site-carries-its-images.i18n.yaml | 2 +- ...26-08-06-doc-site-carries-its-images.zh.md | 20 +- ...6-in-repository-landlock-release.i18n.yaml | 2 +- ...08-06-in-repository-landlock-release.zh.md | 4 +- ...remotes-generated-contract-build.i18n.yaml | 2 +- ...api-remotes-generated-contract-build.zh.md | 26 +- ...08-08-browser-gif-evidence-chain.i18n.yaml | 2 +- ...026-08-08-browser-gif-evidence-chain.zh.md | 10 +- ...-20-remove-stdio-and-echo-agents.i18n.yaml | 2 +- ...6-07-20-remove-stdio-and-echo-agents.zh.md | 16 +- ...6-merge-subagent-control-service.i18n.yaml | 2 +- ...07-26-merge-subagent-control-service.zh.md | 12 +- ...subagent-continuation-operations.i18n.yaml | 2 +- ...med-subagent-continuation-operations.zh.md | 2 +- ...-remove-synthetic-log-only-turns.i18n.yaml | 2 +- ...7-28-remove-synthetic-log-only-turns.zh.md | 6 +- ...7-29-shared-base-config-overlays.i18n.yaml | 2 +- ...26-07-29-shared-base-config-overlays.zh.md | 14 +- .../2026-07-30-private-agent-send.i18n.yaml | 2 +- .../2026-07-30-private-agent-send.zh.md | 6 +- ...y-neutral-sandbox-policy-context.i18n.yaml | 2 +- ...ility-neutral-sandbox-policy-context.zh.md | 4 +- ...7-31-drop-user-message-edit-stub.i18n.yaml | 2 +- ...26-07-31-drop-user-message-edit-stub.zh.md | 6 +- ...-31-one-route-to-add-a-workspace.i18n.yaml | 2 +- ...6-07-31-one-route-to-add-a-workspace.zh.md | 36 +- ...t-invariants-from-shipped-config.i18n.yaml | 2 +- ...-omit-invariants-from-shipped-config.zh.md | 2 +- ...ndows-powershell-picker-fallback.i18n.yaml | 2 +- ...p-windows-powershell-picker-fallback.zh.md | 30 +- .../2026-08-04-remove-tui-package.i18n.yaml | 2 +- .../2026-08-04-remove-tui-package.zh.md | 8 +- ...r-bubbles-drop-the-branch-action.i18n.yaml | 2 +- ...-user-bubbles-drop-the-branch-action.zh.md | 6 +- .../2026-08-08-remove-cli-demo.i18n.yaml | 2 +- .../2026-08-08-remove-cli-demo.zh.md | 6 +- ...6-07-24-web-gui-browser-e2e-lane.i18n.yaml | 2 +- .../2026-07-24-web-gui-browser-e2e-lane.zh.md | 40 +- ...itest-jsdom-webstorage-ownership.i18n.yaml | 2 +- ...30-vitest-jsdom-webstorage-ownership.zh.md | 4 +- ...-30-web-browser-snapshot-ci-gate.i18n.yaml | 2 +- ...6-07-30-web-browser-snapshot-ci-gate.zh.md | 12 +- ...n-reasoning-chunk-browser-stress.i18n.yaml | 2 +- ...pt-in-reasoning-chunk-browser-stress.zh.md | 12 +- ...7-29-durable-last-activity-index.i18n.yaml | 2 +- ...26-07-29-durable-last-activity-index.zh.md | 32 +- ...8-semantic-composer-chain-phases.i18n.yaml | 2 +- ...08-08-semantic-composer-chain-phases.zh.md | 8 +- ...07-17-sdk-follow-up-capabilities.i18n.yaml | 2 +- ...026-07-17-sdk-follow-up-capabilities.zh.md | 48 +- .../2026-08-01-windows-pwsh-default.i18n.yaml | 2 +- .../2026-08-01-windows-pwsh-default.zh.md | 18 +- ...t-first-npm-baseline-publication.i18n.yaml | 2 +- ...ifact-first-npm-baseline-publication.zh.md | 26 +- ...ency-swaps-rejected-by-nih-audit.i18n.yaml | 2 +- ...pendency-swaps-rejected-by-nih-audit.zh.md | 14 +- README.i18n.yaml | 2 +- README.zh.md | 6 +- apps/cli/README.i18n.yaml | 2 +- apps/cli/README.zh.md | 2 +- apps/cli/reference/README.i18n.yaml | 2 +- apps/cli/reference/README.zh.md | 12 +- docs/agent-lifecycle.i18n.yaml | 6 + docs/agent-lifecycle.zh.md | 84 + docs/api-gateway.i18n.yaml | 2 +- docs/api-gateway.zh.md | 50 +- docs/architecture.i18n.yaml | 2 +- docs/architecture.zh.md | 38 +- docs/capability-seams.i18n.yaml | 6 + docs/capability-seams.zh.md | 425 +++ docs/config-catalog.i18n.yaml | 6 + docs/config-catalog.zh.md | 2684 +++++++++++++++++ docs/cookbook/adding-a-package.i18n.yaml | 2 +- docs/cookbook/adding-a-package.zh.md | 20 +- docs/cookbook/adding-a-tool.i18n.yaml | 2 +- docs/cookbook/adding-a-tool.zh.md | 18 +- docs/cookbook/extension-cookbook.i18n.yaml | 2 +- docs/cookbook/extension-cookbook.zh.md | 28 +- ...sponding-to-pr-review-on-a-stack.i18n.yaml | 2 +- .../responding-to-pr-review-on-a-stack.zh.md | 4 +- docs/cordis-api/context.i18n.yaml | 6 + docs/cordis-api/context.zh.md | 366 +++ docs/cordis-api/events.i18n.yaml | 6 + docs/cordis-api/events.zh.md | 209 ++ docs/cordis-api/fiber.i18n.yaml | 6 + docs/cordis-api/fiber.zh.md | 377 +++ docs/cordis-api/registry.i18n.yaml | 6 + docs/cordis-api/registry.zh.md | 154 + docs/cordis-api/service.i18n.yaml | 6 + docs/cordis-api/service.zh.md | 104 + .../cordis-tutorial/01-first-plugin.i18n.yaml | 2 +- docs/cordis-tutorial/01-first-plugin.zh.md | 4 +- docs/development.i18n.yaml | 2 +- docs/development.zh.md | 28 +- docs/event-producer-consumer.i18n.yaml | 6 + docs/event-producer-consumer.zh.md | 83 + docs/graph-atlas.i18n.yaml | 6 + docs/graph-atlas.zh.md | 26 + docs/i18n/README.i18n.yaml | 4 +- docs/i18n/README.md | 4 +- docs/i18n/README.zh.md | 14 +- docs/i18n/terminology.md | 4 +- docs/i18n/translation-rules.i18n.yaml | 2 +- docs/module-graph.i18n.yaml | 6 + docs/module-graph.zh.md | 1370 +++++++++ docs/persistence-catalog.i18n.yaml | 6 + docs/persistence-catalog.zh.md | 775 +++++ ...0003-web-agent-gui-feedback-loop.i18n.yaml | 2 +- .../0003-web-agent-gui-feedback-loop.zh.md | 12 +- ...ice-misclassified-child-failures.i18n.yaml | 2 +- ...-notice-misclassified-child-failures.zh.md | 6 +- docs/subsystems/core.i18n.yaml | 2 +- docs/subsystems/core.zh.md | 12 +- docs/subsystems/credentials.i18n.yaml | 2 +- docs/subsystems/credentials.zh.md | 10 +- docs/subsystems/lsp.i18n.yaml | 2 +- docs/subsystems/persistence.i18n.yaml | 2 +- docs/subsystems/persistence.zh.md | 4 +- docs/subsystems/scope.i18n.yaml | 2 +- docs/subsystems/settings.i18n.yaml | 2 +- docs/subsystems/settings.zh.md | 14 +- docs/subsystems/subagent.i18n.yaml | 2 +- docs/subsystems/subagent.zh.md | 26 +- docs/subsystems/tools.i18n.yaml | 2 +- docs/subsystems/tools.zh.md | 6 +- docs/subsystems/typert.i18n.yaml | 2 +- docs/subsystems/typert.zh.md | 12 +- docs/subsystems/user-interaction.i18n.yaml | 2 +- docs/subsystems/user-interaction.zh.md | 4 +- docs/testing.i18n.yaml | 2 +- docs/testing.zh.md | 12 +- docs/tool-catalog.i18n.yaml | 6 + docs/tool-catalog.zh.md | 1553 ++++++++++ docs/tool-execution-pipeline.i18n.yaml | 6 + docs/tool-execution-pipeline.zh.md | 64 + docs/user/develop/basic/publish.i18n.yaml | 2 +- docs/user/develop/basic/publish.zh.md | 14 +- docs/user/guide/config.i18n.yaml | 2 +- docs/user/guide/config.zh.md | 4 +- docs/user/guide/index.i18n.yaml | 2 +- docs/user/guide/index.zh.md | 26 +- docs/user/guide/providers.i18n.yaml | 2 +- docs/user/guide/providers.zh.md | 8 +- docs/user/guide/quickstart.i18n.yaml | 2 +- docs/user/guide/quickstart.zh.md | 14 +- docs/web-styling.i18n.yaml | 2 +- docs/web-styling.zh.md | 6 +- examples/web-cordis/README.i18n.yaml | 2 +- examples/web-cordis/README.zh.md | 2 +- packages/README.i18n.yaml | 2 +- packages/README.zh.md | 78 +- packages/api/README.i18n.yaml | 2 +- packages/api/README.zh.md | 4 +- packages/api/gateway/README.i18n.yaml | 2 +- packages/api/gateway/README.zh.md | 4 +- packages/api/remotes/README.i18n.yaml | 2 +- packages/api/remotes/README.zh.md | 2 +- packages/bash/bash-env/README.i18n.yaml | 2 +- packages/bash/bash-env/README.zh.md | 24 +- packages/bash/pwsh-local/README.i18n.yaml | 2 +- packages/bash/pwsh-local/README.zh.md | 4 +- packages/bash/tool-bash/README.i18n.yaml | 2 +- packages/bash/tool-bash/README.zh.md | 10 +- packages/bash/tool-pwsh/README.i18n.yaml | 2 +- packages/bash/tool-pwsh/README.zh.md | 46 +- packages/boot/app-boot/README.i18n.yaml | 2 +- packages/boot/app-boot/README.zh.md | 30 +- packages/bundle/README.i18n.yaml | 2 +- packages/bundle/README.zh.md | 2 +- packages/bundle/headless/README.i18n.yaml | 2 +- packages/bundle/headless/README.zh.md | 2 +- packages/bundle/web-app/README.i18n.yaml | 2 +- packages/bundle/web-app/README.zh.md | 4 +- packages/client/README.i18n.yaml | 2 +- packages/client/README.zh.md | 10 +- packages/client/connection/README.i18n.yaml | 2 +- packages/client/connection/README.zh.md | 4 +- packages/client/test-runtime/README.i18n.yaml | 2 +- packages/client/test-runtime/README.zh.md | 10 +- .../client/ui-conversation/README.i18n.yaml | 2 +- packages/client/ui-conversation/README.zh.md | 12 +- .../client/ui-deliverables/README.i18n.yaml | 2 +- packages/client/ui-deliverables/README.zh.md | 10 +- .../ui-settings-general/README.i18n.yaml | 2 +- .../client/ui-settings-general/README.zh.md | 4 +- packages/client/ui-tool/README.i18n.yaml | 2 +- packages/client/ui-tool/README.zh.md | 32 +- packages/client/ui-workspace/README.i18n.yaml | 2 +- packages/client/ui-workspace/README.zh.md | 12 +- .../credentials/credentials/README.i18n.yaml | 2 +- packages/credentials/credentials/README.zh.md | 20 +- packages/feedback/README.i18n.yaml | 2 +- packages/feedback/README.zh.md | 4 +- .../command-feedback/README.i18n.yaml | 2 +- .../feedback/command-feedback/README.zh.md | 14 +- .../tool-str-replace-editor/README.i18n.yaml | 2 +- .../fs/tool-str-replace-editor/README.zh.md | 10 +- .../directory-picker-native/README.i18n.yaml | 2 +- .../host/directory-picker-native/README.zh.md | 6 +- .../host/frontend-static/README.i18n.yaml | 2 +- packages/host/frontend-static/README.zh.md | 2 +- packages/host/webserver/README.i18n.yaml | 2 +- packages/host/webserver/README.zh.md | 8 +- .../pty/tool-bash-persistent/README.i18n.yaml | 2 +- .../pty/tool-bash-persistent/README.zh.md | 14 +- .../README.i18n.yaml | 2 +- .../README.i18n.yaml | 2 +- .../README.i18n.yaml | 2 +- .../session-persistence/README.i18n.yaml | 2 +- .../session-projection-cache/README.i18n.yaml | 2 +- .../session-projection/README.i18n.yaml | 2 +- .../session-telemetry-otel/README.i18n.yaml | 2 +- .../session-telemetry/README.i18n.yaml | 2 +- .../README.i18n.yaml | 2 +- .../README.i18n.yaml | 2 +- .../session-title-llm/README.i18n.yaml | 2 +- packages/settings/README.i18n.yaml | 2 +- packages/settings/README.zh.md | 2 +- .../settings/settings-local/README.i18n.yaml | 2 +- packages/settings/settings-local/README.zh.md | 20 +- packages/settings/settings/README.i18n.yaml | 2 +- packages/settings/settings/README.zh.md | 16 +- packages/skill/skill-badge/README.i18n.yaml | 2 +- packages/skill/skill-badge/README.zh.md | 2 +- .../subagent-claude-code/README.i18n.yaml | 2 +- .../subagent-claude-code/README.zh.md | 10 +- .../subagent/subagent-codex/README.i18n.yaml | 2 +- packages/subagent/subagent-codex/README.zh.md | 14 +- .../support/acp-snapshot/README.i18n.yaml | 2 +- packages/support/acp-snapshot/README.zh.md | 12 +- packages/typert/type-meta/README.i18n.yaml | 2 +- packages/typert/type-meta/README.zh.md | 16 +- packages/util/atomic-write/README.i18n.yaml | 2 +- packages/util/atomic-write/README.zh.md | 8 +- packages/util/environment/README.i18n.yaml | 2 +- packages/util/environment/README.zh.md | 8 +- python/development.i18n.yaml | 2 +- python/development.zh.md | 12 +- scripts/project-doc-site.spec.ts | 42 +- scripts/translation-pairing.manifest.json | 13 +- scripts/translation-pairing.spec.ts | 11 + scripts/translation-pairing.ts | 21 + scripts/verify-translation-pairing.ts | 3 +- website/docs.ts | 53 +- 565 files changed, 10606 insertions(+), 2171 deletions(-) create mode 100644 docs/agent-lifecycle.i18n.yaml create mode 100644 docs/agent-lifecycle.zh.md create mode 100644 docs/capability-seams.i18n.yaml create mode 100644 docs/capability-seams.zh.md create mode 100644 docs/config-catalog.i18n.yaml create mode 100644 docs/config-catalog.zh.md create mode 100644 docs/cordis-api/context.i18n.yaml create mode 100644 docs/cordis-api/context.zh.md create mode 100644 docs/cordis-api/events.i18n.yaml create mode 100644 docs/cordis-api/events.zh.md create mode 100644 docs/cordis-api/fiber.i18n.yaml create mode 100644 docs/cordis-api/fiber.zh.md create mode 100644 docs/cordis-api/registry.i18n.yaml create mode 100644 docs/cordis-api/registry.zh.md create mode 100644 docs/cordis-api/service.i18n.yaml create mode 100644 docs/cordis-api/service.zh.md create mode 100644 docs/event-producer-consumer.i18n.yaml create mode 100644 docs/event-producer-consumer.zh.md create mode 100644 docs/graph-atlas.i18n.yaml create mode 100644 docs/graph-atlas.zh.md create mode 100644 docs/module-graph.i18n.yaml create mode 100644 docs/module-graph.zh.md create mode 100644 docs/persistence-catalog.i18n.yaml create mode 100644 docs/persistence-catalog.zh.md create mode 100644 docs/tool-catalog.i18n.yaml create mode 100644 docs/tool-catalog.zh.md create mode 100644 docs/tool-execution-pipeline.i18n.yaml create mode 100644 docs/tool-execution-pipeline.zh.md diff --git a/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.i18n.yaml b/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.i18n.yaml index 0697332171..b18ca244be 100644 --- a/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.i18n.yaml @@ -1,6 +1,6 @@ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.md 2026-06-11-runtime-arg-validation.md: e0bca0ff24c5adc7ca58007932dff6580694b01d -2026-06-11-runtime-arg-validation.zh.md: 09958147766b4015d6bebf786c4947b9d2941f74 +2026-06-11-runtime-arg-validation.zh.md: c98bde0ed2dd568a394aae74fb9f5549c3f61105 diff --git a/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.zh.md b/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.zh.md index 0995814776..c98bde0ed2 100644 --- a/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.zh.md +++ b/.agents/notes/implemented/architecture/2026-06-11-runtime-arg-validation.zh.md @@ -6,13 +6,13 @@ Status: implemented ## 问题 -`defineTool`([统一 schema DSL](2026-07-20-unified-json-value-schema-dsl.md))为工具作者的 `execute(args)` 提供了经 `InferArgs` 映射的类型化参数。但该类型只是对运行时值的编译期声明,而这个值实际上是模型生成的 JSON:没有任何机制强制模型遵守 schema,因此畸形调用(缺少必需键、声明为数字的位置传入字符串,或字面量超出声明的集合)会以「仅名义类型化」的状态到达 `execute`。工具函数体随后要么在错误形状上崩溃,要么静默地行为异常。 +`defineTool`([统一 schema DSL](2026-07-20-unified-json-value-schema-dsl.md))为工具作者的 `execute(args)` 提供了经 `InferArgs` 映射的类型化参数。但该类型只是对运行时值的编译期声明,而这个值实际上是模型生成的 JSON:没有任何机制强制模型遵守 schema,因此畸形调用(缺少必需键、声明为数字的位置传入字符串,或字面量超出声明的集合)会以「仅在名义上类型化」的状态到达 `execute`。工具函数体随后要么在错误形状上崩溃,要么静默地行为异常。 ## 决策 `validateArgs(spec, args): string[]` 编译 `ParameterSchemaSpec`,并委托共享的 `validateJsonSchemaValue()` 遍历器,对格式正确的声明返回可读的违规列表。`defineTool` 在定义时对编译后的参数 schema 创建快照,并在调用类型化函数体之前执行校验;存在违规时会抛出 `ToolArgsError`(`INVALID_ARGS`),注册表将其作为模型可据以修正的错误结果返回。 -校验器与编译器因此共享完全一致的语义:隐式参数根是开放对象;必需键仅来自 `required: true`;默认值仍是注解;显式嵌套对象遵循其声明的开放性;数组通过 `items` 递归;标量字面量约束保证类型正确;`oneOf` 仅在恰好一个分支匹配时才接受。原始注册的工具自行负责输入校验。 +校验器与编译器因此共享完全一致的语义:隐式参数根是开放对象;必需键仅来自 `required: true`;默认值仍是注解;显式嵌套对象遵循其声明的开放性;数组通过 `items` 递归;标量字面量约束保证类型正确;`oneOf` 仅在恰好一个分支匹配时才接受。直接注册的工具自行负责输入校验。 ## 后果 diff --git a/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.i18n.yaml index 215f612f5e..efed9ca5ea 100644 --- a/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.md 2026-07-08-agent-scope-contexts.md: eb3f6f247bac1a1d81aa2644132c7b9cc04d602c -2026-07-08-agent-scope-contexts.zh.md: 673dd8578bf545b1f14f3b7e7b89874d804e4ae9 +2026-07-08-agent-scope-contexts.zh.md: fe82ac18d07de97330e86461e6bdf86edc37d2ae diff --git a/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.zh.md b/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.zh.md index 673dd8578b..fe82ac18d0 100644 --- a/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-08-agent-scope-contexts.zh.md @@ -18,14 +18,14 @@ Status: implemented Cordis 是 SDK 底层的插件框架。Cordis **上下文**是插件用来访问服务和注册效果的对象,效果的清理跟随该上下文。[Cordis 入门](../../../../docs/cordis-primer.md)对该框架有更详细的说明。 -对大多数贡献者而言,完整契约是四条规则: +对大多数贡献者而言,完整约定是四条规则: | 问题 | 规则 | |---|---| | 在哪里为某个 agent 注册行为? | 通过 `agent.ctx` 调用普通注册 API | | 某个 agent 的操作能看到什么? | 部署全局加上该 agent 的层,按所属服务的合并规则 | | 哪些作用域监听器会运行? | 无作用域监听器加上为该操作所属 agent 注册的监听器 | -| 该层存在多久? | setup 在发布前完成;dispose 保留该层直到工作完全停稳 | +| 该层存在多久? | setup 在发布前完成;dispose(资源释放)保留该层直到工作完全停稳 | 作用域是扁平的。解析永远不会遍历父级或兄弟作用域,生命周期所有权也不意味着注册继承。 @@ -49,14 +49,14 @@ flowchart LR ### 注册来源决定可见性与清理 -通过普通插件上下文进行的注册是部署全局的,随该插件一起 dispose(资源释放)。同一方法通过 `agent.ctx` 调用则贡献给一个 agent,随该 agent 的作用域一起 dispose。 +通过普通插件上下文进行的注册是部署全局的,随该插件一起 dispose。同一方法通过 `agent.ctx` 调用则贡献给一个 agent,随该 agent 的作用域一起 dispose。 | 注册来源 | 默认可见性 | 随谁 dispose | |---|---|---| | 普通插件上下文 | 每个符合条件的 agent 视图 | 注册插件 | | `agent.ctx` | 仅该 agent 的视图 | agent 作用域 | -工具、提示词段落与变量、工具限制、守卫以及作用域事件监听器都遵循此契约。命名的本地值通常对该 agent 遮蔽同名全局值;各所属服务文档会说明例外与合并行为。 +工具、提示词段落与变量、工具限制、守卫以及作用域事件监听器都遵循此约定。命名的本地值通常对该 agent 遮蔽同名全局值;各所属服务文档会说明例外与合并行为。 普通贡献者的模式是在 agent setup 期间注册完整的本地世界: @@ -88,7 +88,7 @@ await handle.dispose() ctx.tools.get('review_summary', handle.agent) // undefined: scope is gone ``` -setup 接收一个完整的受信 Cordis 上下文,因此可以组合普通插件和服务。其契约仅限组合:不支持通过 cast 或内部注册表调用来驱动或发布正在构建中的 agent。 +setup 接收一个完整的受信 Cordis 上下文,因此可以组合普通插件和服务。其约定仅限组合:不支持通过 cast 或内部注册表调用来驱动或发布正在构建中的 agent。 ### 操作选择视图 @@ -96,7 +96,7 @@ setup 接收一个完整的受信 Cordis 上下文,因此可以组合普通插 工具查找与执行接收其所服务的 agent。提示词组装接收正在构建请求的 agent 的组装上下文。事件分发接收其领域主体。这使共享服务实例可在多个 agent 间复用,同时让每个操作的视图保持显式。 -只有采纳了作用域契约的服务才会解析 agent 层。`agent.ctx` 不会自动改变任意 Cordis 服务调用的行为。 +只有采纳了作用域约定的服务才会解析 agent 层。`agent.ctx` 不会自动改变任意 Cordis 服务调用的行为。 ### 作用域事件将路由与事件数据分离 @@ -108,13 +108,13 @@ setup 接收一个完整的受信 Cordis 上下文,因此可以组合普通插 ### 创建最后发布,dispose 最后撤销 -`ctx.agents.create()` 和 `resume()` 构建未发布的会话、作用域、agent 和驱动器。它们等待 `setup`,同步调用其可选的 `AgentSetupCommit`,准入最终的会话和 agent 条目,按序公告,启动循环,然后才返回 handle。该提交操作让可变的配置状态在所有 setup 的 await 均结算后,于确切的发布边界重新校验;若其抛出异常,则会在公告任何一个身份前回滚私有事务,而成功提交后的撤销属于普通的实时拆卸。 +`ctx.agents.create()` 和 `resume()` 构建未发布的会话、作用域、agent 和驱动器。它们等待 `setup`,同步调用其可选的 `AgentSetupCommit`,准入最终的会话和 agent 条目,按序公告,启动循环,然后才返回 handle。该提交操作让可变的配置状态在所有 setup 的 await 均结算后,于确切的发布边界重新校验;若其抛出异常,则会在公告任何一个身份前回滚私有事务,而成功提交后的撤销属于普通的存活期拆除。 可选的创建信号仅在创建或恢复挂起期间取消工作。promise resolve 后,返回的 `AgentHandle` 拥有显式 dispose 权。 如果加载、setup、可选的 setup 提交、准入或发布失败,私有事务回滚其准备的一切。使用同一个调用方提供的存活 ID 的并发操作可能都到达 setup,但最终注册表条目只准入一个;每个失败者拒绝并清理其私有资源。在等待 dispose 完成后的顺序复用仍然有效。 -`AgentHandle.dispose()` 反转边界。它停用创建或驱动,等待同步发布解除,停止并排空驱动器和最终会话刷写,分离 agent 和会话,最后 dispose 作用域。重复或竞争的 dispose 请求合并为一个完成 promise。 +`AgentHandle.dispose()` 反转边界。它停用创建或驱动,等待同步发布完成退栈,停止并排空驱动器和最终会话刷写,分离 agent 和会话,最后 dispose 作用域。重复或竞争的 dispose 请求合并为一个完成 promise。 调用方的 Cordis 上下文和具体的 AgentLoop 工厂是结构性共同所有者。卸载任一方都会 dispose 事务或存活 agent。 @@ -152,7 +152,7 @@ agent 作用域组合的是受信的同进程注册。它不沙箱化插件、 ### 向每个注册传递 agent 选项 -类似 `tools.register(definition, { agent })` 的 API 在每个注册表中重复作用域管道,且允许可见性所有权与清理所有权漂移。通过 `agent.ctx` 注册使两个事实跟随同一个 Cordis effect owner。 +类似 `tools.register(definition, { agent })` 的 API 在每个注册表中重复作用域传递逻辑,且允许可见性所有权与清理所有权漂移。通过 `agent.ctx` 注册使两个事实跟随同一个 Cordis effect owner。 ### 过滤事件但保持注册表全局 @@ -160,7 +160,7 @@ agent 作用域组合的是受信的同进程注册。它不沙箱化插件、 ### 为每个 agent 创建独立的服务图 -所需的视图是共享部署服务加上一个本地注册层。每 agent 一个图会重复适配器,并使共享持久化、提供方注册表和应用启动复杂化。 +所需的视图是共享部署服务加上一个本地注册层。每个 agent 一个服务图会重复适配器,并使共享持久化、提供方注册表和应用启动复杂化。 ### 继承父级注册作用域 diff --git a/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml index dbdc4347f6..527ac2d6b3 100644 --- a/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.md 2026-07-12-agent-scope-runtime-design.md: b93c291957fab98ab9d0d04eb816bb01a15a0b51 -2026-07-12-agent-scope-runtime-design.zh.md: 8b1332c23ce3b6c0332fcde40b4f4ac8dd20aa45 +2026-07-12-agent-scope-runtime-design.zh.md: 2dbdcab2ce3a7f92bf537d93a65dfe44374ec07c diff --git a/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.zh.md b/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.zh.md index 8b1332c23c..2dbdcab2ce 100644 --- a/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-12-agent-scope-runtime-design.zh.md @@ -1,4 +1,4 @@ -# Agent Note: Agent 作用域运行时设计与正确性 +# Agent Note: agent 作用域运行时设计与正确性 Status: implemented @@ -6,7 +6,7 @@ Status: implemented ## 问题 -[agent 作用域契约](2026-07-08-agent-scope-contexts.md)对贡献者而言很简单:通过 `agent.ctx` 注册,解析出一个全局加单 agent 的视图,仅在 setup 完成后发布,并保持作用域直到工作停止。运行时必须在协作式插件框架、异步创建、可重入监听器、持久化会话提交以及 worker 或进程故障等场景下维护这份契约。 +[agent(智能体)作用域约定](2026-07-08-agent-scope-contexts.md)对贡献者而言很简单:通过 `agent.ctx` 注册,解析出一个全局加单 agent 的视图,仅在 setup 完成后发布,并保持作用域直到工作停止。运行时必须在协作式插件框架、异步创建、可重入监听器、持久化会话提交以及 worker 或进程故障等场景下维护这份约定。 主要的设计风险是为每个竞态条件引入第二套机制。独立的预留、就绪哨兵、取消中继、快照层和保护注册表可能镜像同一个事实,直到没有读者能分辨哪个才是权威的。这些机制还会诱使运行时把可信的类型化调用当作敌对的序列化边界来处理。 @@ -23,18 +23,18 @@ Status: implemented | 选择全局加某个 agent 的注册 | 不透明作用域键、路由载体与共享 layer store | | 拥有一个活跃的 agent 或会话 | 由其 disposer 捕获的单条注册表条目 | | 协调创建/恢复 | 单个 `AgentCreationTransaction` | -| 保护持久化、队列、模型或协议格式数据 | 在该边界处一次性物化 | -| 在同一进程内传递类型化值 | Readonly 借用契约 | -| 组合模型可见的提示词与工具表面 | 单个共享工具视图加权威的 assembly-waterfall 结果 | +| 保护持久化、队列、模型或协议格式(wire format)数据 | 在该边界处一次性物化 | +| 在同一进程内传递类型化值 | Readonly 借用约定 | +| 组合模型可见的提示词与工具表面 | 单个共享工具视图加 assembly waterfall(瀑布式事件)的权威结果 | | 协调 subagent、worker 和进程关闭 | 单个取消信号加该边界独立的终止态/完全停稳态事实 | 本 Agent Note 余下部分按依赖顺序展开这些选择:Cordis 机制、作用域路由、创建与会话提交、工具与提示词、subagent 与工作流,最后是可执行检查。 -[7 月 8 日 Agent Note](2026-07-08-agent-scope-contexts.md)仍然是贡献者契约。独立的 [subagent 组合控制 Agent Note](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md)拥有 `persona`、`toolFilter` 和 `maxDepth`;本文仅讨论它们的 setup 如何融入生命周期。 +[7 月 8 日 Agent Note](2026-07-08-agent-scope-contexts.md)仍然是贡献者约定。独立的 [subagent 组合控制 Agent Note](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md)拥有 `persona`、`toolFilter` 和 `maxDepth`;本文仅讨论它们的 setup 如何融入生命周期。 ## Cordis 模型:上下文、fiber、effect、receiver 与 waterfall -理解实现需要五个 Cordis 概念。上下文选择服务和注册所有权;fiber 是一个活跃的插件或子生命周期;effect 将清理逻辑附加到 fiber;事件接收器选择监听器;waterfall(瀑布式事件)让监听器按顺序变换或否决一个操作。 +理解实现需要五个 Cordis 概念。上下文选择服务和注册所有权;fiber 是一个活跃的插件或子生命周期;effect 将清理逻辑附加到 fiber;事件接收器选择监听器;waterfall 让监听器按顺序变换或否决一个操作。 ### 上下文是贯穿单个服务图的所有权路径 @@ -56,7 +56,7 @@ Cordis 使用 dispatch receiver(`this`)过滤监听器,而 harness 的监 因此,产品辅助函数构造载体并单独传递领域主体。这防止监听器路由变成另一套对象模型,并使事件签名在不了解载体内部的情况下也可理解。 -Cordis waterfall 是中间件风格的 dispatch。每个监听器接收 `next()`:调用它则委托给剩余监听器和基础操作,不调用则否决或替换下游结果。Waterfall 驱动提示词组装和工具策略;普通 emit 事件同步通知,parallel 事件等待所有监听器但没有否决结果。 +Cordis waterfall 是中间件风格的 dispatch。每个监听器接收 `next()`:调用它则委托给剩余监听器和基础操作,不调用则否决或替换下游结果。waterfall 驱动提示词组装和工具策略;普通 emit 事件同步通知,parallel 事件等待所有监听器但没有否决结果。 ## 作用域路由:一个不透明键选择一层 @@ -82,7 +82,7 @@ Receiver 是一个小型载体而非领域对象的透明代理。需要 agent 类型标记拒绝普通的裸 receiver 误用,开发环境不变式覆盖直接 JavaScript 或强制转换的 dispatch。主体保持显式,因为路由正确性和有用的事件数据是不同的关注点。 -## Agent 创建:一个事务拥有完整操作 +## agent 创建:一个事务拥有完整操作 创建和恢复是一个具有多个阶段的异步生命周期,而非多个生命周期。`AgentCreationTransaction` 拥有调用方和工厂的活跃性、可选取消、私有资源、发布、回滚,以及每个所有者观察到的记忆化拆除。 @@ -104,7 +104,7 @@ detach 闭包捕获其确切注册表条目。它仅在映射仍指向该注册 ### Setup 是私有世界内的可信组合 -Setup 接收完整的子上下文,可以等待插件激活。它可以注册工具、提示词段、限制、监听器和其他 effect,但公开契约不支持通过强制转换或内部注册表调用来驱动或发布正在创建中的 agent。 +Setup 接收完整的子上下文,可以等待插件激活。它可以注册工具、提示词段、限制、监听器和其他 effect,但公开约定不支持通过强制转换或内部注册表调用来驱动或发布正在创建中的 agent。 事务将异步加载和 setup 与停用进行竞争,而非无限等待外部代码拥有的 promise。如果取消或所有者卸载获胜,即使外部 promise 永不结算,公开创建也会在事务拥有的清理之后拒绝。 @@ -120,7 +120,7 @@ Setup 接收完整的子上下文,可以等待插件激活。它可以注册 6. 发射 `agent/session-start`。 7. 启动 driver。 -Agent 在两个注册表和创建通知都达成一致之前绝不驱动。同步监听器可以否决或 dispose 一个所有者;事务记录发布进行中,并等待该回调栈展开后再继续拆除。每个已开始的创建宣告在回滚期间都有匹配的销毁宣告。 +agent 在两个注册表和创建通知都达成一致之前绝不驱动。同步监听器可以否决或 dispose 一个所有者;事务记录发布进行中,并等待该回调栈展开后再继续拆除。每个已开始的创建宣告在回滚期间都有匹配的销毁宣告。 以下序列图隔离了非显而易见的竞态:同步创建监听器可以在发布调用栈仍拥有两个注册表条目时请求 dispose。拆除必须立即停用,但要等待该栈展开后才停止和分离任何东西。 @@ -172,7 +172,7 @@ Session 头部、种子和追加的事件是无损 JSON 数据。Session 构造 追加遵循一个序列: -1. 物化持久化事件和表面意图。 +1. 物化持久化事件和呈现意图。 2. 取得 SessionEntry 的独占所有权,并拒绝该注册表条目上的重入追加。 3. 解析作用域回调并运行内部不变式验证。 4. 恰好推送一次;这是提交点。 @@ -185,7 +185,7 @@ Session 头部、种子和追加的事件是无损 JSON 数据。Session 构造 ## 信任边界:仅在所有权真正变更时复制 -运行时区分类型化的进程内契约与序列化及持久化边界。这是值和回调的主要简化规则。 +运行时区分类型化的进程内约定与序列化及持久化边界。这是值和回调的主要简化规则。 | 边界 | 所有权规则 | |---|---| @@ -196,9 +196,9 @@ Session 头部、种子和追加的事件是无损 JSON 数据。Session 构造 | 持久化会话或持久化数据 | 在提交前物化并验证 | | Worker、进程或协议格式消息 | 序列化、验证并拥有解码后的值 | -测试中构造恶意 getter、在交接后替换类型化回调、或强制转换伪造服务对象的做法本身不定义生产契约。运行时在数据跨越解析器、队列、模型、持久化、文件、worker、进程或协议格式(wire format)边界时保留检查,并在可信进程内依赖 readonly 类型加插件纪律。 +测试中构造恶意 getter、在交接后替换类型化回调、或强制转换伪造服务对象的做法本身不定义生产约定。运行时在数据跨越解析器、队列、模型、持久化、文件、worker、进程或协议格式边界时保留检查,并在可信进程内依赖 readonly 类型加插件纪律。 -回调隔离与数据所有权是分开的。监听器是任意扩展代码,即使其参数是可信的也可能抛出异常;发布和提交后路径仍按其事件契约隔离失败。 +回调隔离与数据所有权是分开的。监听器是任意扩展代码,即使其参数是可信的也可能抛出异常;发布和提交后路径仍按其事件约定隔离失败。 ## 工具与提示词:单一视图、权威组装、已提交的结果 @@ -206,7 +206,7 @@ Session 头部、种子和追加的事件是无损 JSON 数据。Session 构造 ### 一个解析器定义工具视图 -私有解析器应用当前展示模式、活跃的全局限制、精确的局部叠加和局部遮蔽。Schema、查找、执行、Code Mode SDK 生成和限制验证都使用该解析器或其限制前的全局名称视图。 +私有解析器应用当前展示模式、活跃的全局限制、精确的局部叠加和局部遮蔽。schema、查找、执行、Code Mode SDK 生成和限制验证都使用该解析器或其限制前的全局名称视图。 [subagent 组合控制 Agent Note](../feature/2026-07-12-subagent-persona-tool-filter-and-depth.md#tool-filtering-is-one-live-global-view-rule)拥有用户可见的 allow/deny 语义。实现要求是一致性:被过滤掉的全局工具不能通过另一条查找路径仍可执行,局部遮蔽的定义就是被展示和执行的同一个定义。 @@ -216,7 +216,7 @@ Session 头部、种子和追加的事件是无损 JSON 数据。Session 构造 注册表为每次执行分配一个新的带品牌的 `Symbol` token。嵌套的 Code Mode 调用将外层 token 作为 `parent` 携带,因此结构化输出可以通过标识将内层捕获与其外层 `run_code` 结果关联。 -注册表分配的新 Symbol 提供无碰撞的执行标识,无需 WeakSet 成员注册表。调用方无法通过 `ToolExecutionInput` 提供执行自身的 token;它们仅在注册表创建后接收流水线拥有的 `ToolExecution`。这是一个可信的类型化契约,而非针对任意强制转换或 JavaScript 调用方的运行时防御。 +注册表分配的新 Symbol 提供无碰撞的执行标识,无需 WeakSet 成员注册表。调用方无法通过 `ToolExecutionInput` 提供执行自身的 token;它们仅在注册表创建后接收流水线拥有的 `ToolExecution`。这是一个可信的类型化约定,而非针对任意强制转换或 JavaScript 调用方的运行时防御。 参数在模型/工具 JSON 进入流水线时一次性物化。Pre-、around- 和 post-execute 监听器操作类型化的 execution 和决策。Call ID 关联、审批、单调守卫和 Code Mode 嵌套仍然是显式的关系检查。 @@ -238,7 +238,7 @@ Scope 直接解决了真正的隔离问题。结构化输出贡献注册在子 对于 Code Mode SDK 调用,内层成功结果记录 `{ parentToken, value }` 而非提交。观察者等待 token 匹配 `parentToken` 的 `run_code` 执行,仅在该外层最终结果也成功时才提交。程序失败、运行时中止或外层 post-policy 拒绝会丢弃待定值。 -一旦值处于待定或已提交状态,作用域单调守卫拒绝后续工具调用。成功的结构化输出执行会调用 `exec.concludeTurn()`,因此其自身不可变结果携带 `concludesTurn: true`,循环在该步骤结束工具循环。Schema 验证失败仍然是普通的 `INVALID_ARGS` 工具错误,子级可以在同一轮次内重试。 +一旦值处于待定或已提交状态,作用域单调守卫拒绝后续工具调用。成功的结构化输出执行会调用 `exec.concludeTurn()`,因此其自身不可变结果携带 `concludesTurn: true`,循环在该步骤结束工具循环。schema 验证失败仍然是普通的 `INVALID_ARGS` 工具错误,子级可以在同一轮次内重试。 纯 Code Mode 的注册表贡献从原生 wire schema 中省略 `structured_output`,并通过生成的 SDK 暴露它。Assembly waterfall 可以有意改变该展示;执行仍然针对子作用域定义进行验证,监听器拥有其创建的任何替代模型可见路由的一致性。 @@ -252,21 +252,21 @@ Scope 直接解决了真正的隔离问题。结构化输出贡献注册在子 | 工具结果 | 观察不可变的已提交结果 | 结构化输出必须仅提交实际逃出流水线的结果 | | 轮次 continuation | 通过已提交工具结果终止 | 已提交的终端输出必须结束轮次 | -`ToolGuard` 是单调策略注册表。已提交的工具观察是上述被隔离的 `tools/result` 点。终端结构化输出在自身执行上标记 `concludesTurn`,因此终止性成为权威结果上的数据,而不是独立 hook 决策。 +`ToolGuard` 是单调策略注册表。已提交的工具观察是上述被隔离的 `tools/result` 点。终端结构化输出在自身执行上标记 `concludesTurn`,因此终止性成为权威结果上的数据,而不是独立钩子决策。 -### Skill 和 approval 服务信任类型化调用方 +### skill(技能)和 approval 服务信任类型化调用方 -Skill 注册表定义和 approval 策略是 readonly 的同进程契约。它们的服务不克隆回调对象,也不防御交接后的回调替换。 +skill 注册表定义和 approval 策略是 readonly 的同进程约定。它们的服务不克隆回调对象,也不防御交接后的回调替换。 -Skill 仍然验证外部 skill 文件和解析的提供方输出,通过调用 agent 的工具视图路由目录,并精确 dispose 注册。Approval 仍然解析策略、观察取消、按 `request.agent` 路由 `approval/request`、记录持久化审计对,并隔离应答者和提交后观察者的失败。 +skill 仍然验证外部 skill 文件和解析的提供方输出,通过调用 agent 的工具视图路由目录,并精确 dispose 注册。Approval 仍然解析策略、观察取消、按 `request.agent` 路由 `approval/request`、记录持久化审计对,并隔离应答者和提交后观察者的失败。 ## Subagent:发布即 start promise Subagent 启动有一次所有权转移。提供方拥有未发布资源,直到其 start promise 以一个已发布 run 兑现;调用方拥有返回的 run 并必须 dispose 它。 -### 服务契约有一个取消通道 +### 服务约定有一个取消通道 -`SubagentProvider.start()` 和 `SubagentService.start()` 返回 `Promise`。Promise 会在后端跨过发布边界后兑现,因此调用方和 `subagent/start` 观察者从不需要第二个 `run.started` promise。提供方工作如果在发布前失败,`start()` 就会被拒绝;发布后的提示词、轮次、取消与基础设施结果会通过 `SubagentRun.result` 结算,且不会隐藏 child id,这也是[持久化目录决策](../feature/2026-07-22-durable-subagent-catalog-and-list-agents.md)所要求的契约。 +`SubagentProvider.start()` 和 `SubagentService.start()` 返回 `Promise`。Promise 会在后端跨过发布边界后兑现,因此调用方和 `subagent/start` 观察者从不需要第二个 `run.started` promise。提供方工作如果在发布前失败,`start()` 就会被拒绝;发布后的提示词、轮次、取消与基础设施结果会通过 `SubagentRun.result` 结算,且不会隐藏 child id,这也是[持久化目录决策](../feature/2026-07-22-durable-subagent-catalog-and-list-agents.md)所要求的约定。 `SubagentStartRequest.signal` 是必需的。中止它会在启动期间,以及已发布 run 的剩余就绪或轮次工作中请求取消。`SubagentRun.dispose()` 也请求取消并等待完全停稳。没有单独的公开 `run.cancel()` 通道。 @@ -314,11 +314,11 @@ Worker 边界仍然序列化请求和结果。宿主保留首个终端结果仲 ## 正确性强制 -该设计通过类型、运行时逃逸点、生成的契约和行为测试来强制执行。没有哪一层被要求证明它无法观察到的东西。 +该设计通过类型、运行时逃逸点、生成的约定和行为测试来强制执行。没有哪一层被要求证明它无法观察到的东西。 ### 类型使常规路径难以误用 -Readonly 契约描述借用的同进程值。`Scoped` 标记事件接收器,`agentEvents()` 融合载体和主体,工具输入省略注册表拥有的 token,subagent 异步返回类型直接暴露发布与结算。 +Readonly 约定描述借用的同进程值。`Scoped` 标记事件接收器,`agentEvents()` 融合载体和主体,工具输入省略注册表拥有的 token,subagent 异步返回类型直接暴露发布与结算。 TypeScript 无法管控 JavaScript 强制转换、直接 Cordis dispatch、进程消息或持久化文件,因此运行时强制保留在这些逃逸点。 @@ -326,9 +326,9 @@ TypeScript 无法管控 JavaScript 强制转换、直接 Cordis dispatch、进 `dsh-scope/invariant` 配套插件在被选用时验证每个声明的作用域事件使用带标记的载体,以及暴露主体的事件族使用匹配的键。独立的 `dsh-session/invariant` 贡献在追加提交前暂存 trace 验证,并在同一事件提交后推进;二者都通过 `ctx.invariants` 注册。 -该插件不通过扫描注册表来管控可信 setup,也不拒绝通过强制转换构造的提示词 assembly 对象。这些检查会将组合契约变成推测性的运行时机制,却不保护真实的外部边界。 +该插件不通过扫描注册表来管控可信 setup,也不拒绝通过强制转换构造的提示词 assembly 对象。这些检查会将组合约定变成推测性的运行时机制,却不保护真实的外部边界。 -### 生成的产物使公开契约保持对齐 +### 生成的产物使公开约定保持对齐 事件目录、服务目录、生产者/消费方矩阵、配置目录、模块图、工具目录、type-equiv 块和作用域事件解析器映射都是从源码生成或受新鲜度门禁约束的。[TypeScript 语义门禁 Agent Note](../process/2026-07-14-typescript-program-backed-semantic-gates.md)拥有 Program 构造、语义事件发现和解析器生成规则。 @@ -336,7 +336,7 @@ TypeScript 无法管控 JavaScript 强制转换、直接 Cordis dispatch、进 ## 曾考虑的替代方案 -[7 月 8 日 Agent Note](2026-07-08-agent-scope-contexts.md#alternatives-considered)拥有公开扁平作用域契约的替代方案。此处的替代方案关注实现形态。 +[7 月 8 日 Agent Note](2026-07-08-agent-scope-contexts.md#alternatives-considered)拥有公开扁平作用域约定的替代方案。此处的替代方案关注实现形态。 ### 使用透明代理作为作用域载体 @@ -348,7 +348,7 @@ TypeScript 无法管控 JavaScript 强制转换、直接 Cordis dispatch、进 ### 对每个类型化的同进程参数做快照 -通用复制防御有状态 getter 和违反 readonly 契约的调用方,但增加分配、重复验证器和可能遗忘复制的路径。物化属于解析器、队列、模型、持久化、worker、进程和协议格式边界——即所有权真正变更的地方。 +通用复制防御有状态 getter 和违反 readonly 约定的调用方,但增加分配、重复验证器和可能遗忘复制的路径。物化属于解析器、队列、模型、持久化、worker、进程和协议格式边界——即所有权真正变更的地方。 ### 为就绪、取消和 dispose 提供独立控制器 @@ -375,10 +375,10 @@ Worker 消息、进程死亡和持久化输入确实跨越所有权和序列化 - 作用域贡献仅在其精确的 agent 视图中可见,并随该作用域一起 dispose。 - 创建和恢复不暴露部分配置的句柄;最终写入注册表时的失败者和发布失败清理每个已准备的资源。 - Dispose 在 driver 排空和最终会话工作期间保留作用域监听器和持久化,然后撤销作用域。 -- 持久化、队列、模型、worker、进程和协议格式的值在其真实边界处被拥有;类型化的同进程值遵循 readonly 契约。 +- 持久化、队列、模型、worker、进程和协议格式的值在其真实边界处被拥有;类型化的同进程值遵循 readonly 约定。 - ToolRegistry 的展示、查找和执行在专家 assembly 变换之前解析相同的活跃视图,已提交的结果有一个不可变的观察点。 - 注册表贡献是确定性输入,而可信的 assembly waterfall 拥有最终的模型可见组合。 -- Subagent start 仅返回已发布的 run,必需的 signal 取消待定或活跃的工作,dispose 到达后端的完全停稳契约。 +- Subagent start 仅返回已发布的 run,必需的 signal 取消待定或活跃的工作,dispose 到达后端的完全停稳约定。 - Worker/进程结果优先级和清理在死亡、迟到消息和有界拆除下保持正确。 ### 代价与局限 @@ -387,6 +387,6 @@ Worker 消息、进程死亡和持久化输入确实跨越所有权和序列化 可信的 `system-prompt/assemble` 监听器可以移除或替换 Code Mode 和结构化输出协议片段。这是有意为之:监听器拥有最终组合,必须保持部署期望仍可用的任何协议。 -该设计信任同进程中的类型化插件。它不防御任意强制转换、有状态 getter、违反 readonly 契约的修改,或插件有意在支持的组合 API 之外使用环境服务访问。 +该设计信任同进程中的类型化插件。它不防御任意强制转换、有状态 getter、违反 readonly 约定的修改,或插件有意在支持的组合 API 之外使用环境服务访问。 [安全与权限非目标](2026-07-08-agent-scope-contexts.md#security-and-authority-are-non-goals)仍然是根本性的。这些机制证明注册组合、发布和生命期所有权;它们不证明隔离或父到子的非升权。 diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml index 65cbe478ae..76877fd12c 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md 2026-07-19-gui-layering-and-rpc-protocol.md: 8e020e4fe9b60100671c0cf0e98e28532d850f94 -2026-07-19-gui-layering-and-rpc-protocol.zh.md: 55fa8084083aa83fbb4a38f8e41d2e5624b6e58e +2026-07-19-gui-layering-and-rpc-protocol.zh.md: 06b861ef61e83985b5c3640ff8eddfa5f6845c81 diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md index 55fa808408..06b861ef61 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md @@ -1,4 +1,4 @@ -# RFC: GUI 分层与 RPC 协议——host/client 按能力支持方分层、四象限消息模型与 fetch 载体 +# Agent Note: GUI 分层与 RPC 协议——host/client 按能力提供方分层、四象限消息模型与 fetch 载体 Status: implemented @@ -8,20 +8,20 @@ Status: implemented ## Problem -需要提供 UI 对接层,除已有 ACP/stdio基础版本外,还需要 Web(server) 、 Electron 、等其他产品 UI 形态。我们把这些形态统一称为 Client。希望有如下能力支持: -- 以 `dsh` 进程,同时支持 `dsh web`(启动) 和 `dsh run`(headless) ,一个进程两种模式(设计预留) +需要提供 UI 对接层,除已有 ACP(Agent Client Protocol)/stdio 基线外,还需要 Web(server)、Electron 等其他产品 UI 形态。我们把这些形态统一称为 Client。希望具备以下能力: +- 一个 `dsh` 进程同时支持 `dsh web`(启动)和 `dsh run`(headless),一个进程两种模式(设计预留) - 以与 `dsh web` 同构的 Web 技术形态,在 Electron 中启动 那么当前的工程代码需要稳定的分层职责模型,便于以后接入各类 client 形态。 -同时各消费端的物理通道不同(浏览器 HTTP/WebSocket、进程内 fetch/SSE、将来 IPC),还需要一个通道无关的消息模型和单一契约事实源,让「加一个方法」「换一种载体」互不牵连,且 wire 上的每条消息可类型校验、可观测、可对账。 +同时各消费方的物理通道不同(浏览器 HTTP/WebSocket、进程内 fetch/SSE(Server-Sent Events)、将来 IPC),还需要一个通道无关的消息模型和约定的单一真源,让「加一个方法」「换一种载体」互不牵连,且 wire 上的每条消息可类型校验、可观测、可对账。 ## Decision ### 分层 -目录按照如下分层: -- `packages/host/*`: 包只提供 Host 侧能力(代表了以现在 Harness 实体插件系统为主体的 Node.js 代码核心工程),除此之外,还包含 +目录按照如下分层: +- `packages/host/*`:包只提供 Host 侧能力(代表了基于现有 harness 插件系统的 Node.js 代码核心工程),除此之外,还包含 - 统一后端协议(fetch、HTTP、流式接口等)定义和支持,见本篇「消息协议」起各节 - `packages/client/*`:包只提供 Client 侧能力,每包单边不混。这里住三类包(两条轴归 [client 插件装载 RFC](2026-07-23-client-plugin-loading-model.md) 所有): - **纯库**(`ui-slots`、`web-react`、`ui-primitives`,外加内核包 `loader`):普通根入口包,静态打包进壳;前三者播种进模块表。 @@ -49,7 +49,7 @@ harness core packages ──────────────────┘ - `runtime → apiproxy` 单向;apiproxy 仅依赖类型定义。 - client 侧包**永不 import** host 侧包的运行时(只吃 `/api`、`/client` 两个浏览器安全子路径)。 -- `webserver` 不依赖 `runtime`:它提供 `{ fetch }` 特定实现 ——「webserver ← runtime」只是运行时注入关系,不是包依赖。 +- `webserver` 不依赖 `runtime`:它提供一个 `{ fetch }` 形状的实现 ——「webserver ← runtime」只是运行时注入关系,不是包依赖。 - client 侧跨包 import 插件包一律走 `/client` 子路径,且插件包之间只限类型 import——跨插件值 import 在 tsdown 纯度门禁处即构建错误(值层面的协作走 cordis 服务;边规则归 [client 插件装载 RFC](2026-07-23-client-plugin-loading-model.md) 所有)。 TypeScript 以 solution 根引用的**两个聚合 program** 检查(`tsconfig.json` = solution;`tsconfig.host.json` = host 侧 + 测试,排除 `packages/client`;`tsconfig.client.json` = client 各包及其测试):两侧在相同键(`sessions`、`loader`)下以不同服务合并 cordis `Context` 接口,单一 program 会同时看到两份声明合并而报冲突。共享叶子包(session/llm/tools/apiproxy 等)只构建一次,由两个 program 共同引用([拓扑](../process/2026-07-22-tsconfig-solution-root-two-aggregates.md))。 @@ -60,11 +60,11 @@ TypeScript 以 solution 根引用的**两个聚合 program** 检查(`tsconfig. | 层 | 包 | 职责 | 关键纪律 | |---|---|---|---| -| 前置层 | `dsh-host-apiproxy` | TS/zod 定义 (api/)+ fetch 抽象 (fetch/:handler + 客户端基类) | 做简单、所有接入方都要;Node/浏览器皆可 import;协议内容见下文「消息协议」起各节;client 不得经 ctx 绕开 api | +| 前置层 | `dsh-host-apiproxy` | TS/zod 定义 (api/)+ fetch 抽象 (fetch/:handler + 客户端基类) | 保持简单——每个消费方都需要它;Node/浏览器皆可 import;协议内容见下文「消息协议」起各节;client 不得经 ctx 绕开 api | | 装配层 | `dsh-host-runtime` | 插件组合 + ApiProxy 集成 + web UI 插件挂载(覆盖八个 dshClient 包的内存 Loader 树);host 级配置归属地(defaults/persistenceRoot,将来用户 profile) | 装什么插件、给什么默认值只在这里定;壳不得改装配 | | 承载层 | `dsh-host-webserver` | Web 形态 HTTP 与 upgrade:静态服务 + `/api/*`→handler 转发 + WebSocket upgrade route + close 语义;插件 bundle 端点 + `__DSH_BOOT__` manifest(元数据清单)注入(由 web 插件注册表供给) | Web(浏览器访问)专用;零 workspace 依赖(注册表经结构注入到达);Electron 不复用它 | | client 库 | `dsh-client-ui-slots` / `dsh-client-web-react` / `dsh-client-ui-primitives` | slot 注册表核心 / ctx↔React 胶合 / 纯 React 原子组件 | 组件零 cordis 运行时依赖;由壳播种进 loader 模块表 | -| client 插件 | `dsh-client-connection` / `dsh-client-runtime` / `dsh-client-ui-theme` / `dsh-client-i18n` / `dsh-client-ui-layout` / `dsh-client-ui-sidebar` / `dsh-client-ui-conversation` / `dsh-client-ui-trajectory` | 浏览器侧 cordis 插件树(wire 消费者、核心服务、主题、i18n、布局、侧栏、对话、轨迹)——见 Web 客户端架构 RFC | 双入口(node 半边=空 apply;实现在 `src/client/`);消费面唯一经 ApiProxy | +| client 插件 | `dsh-client-connection` / `dsh-client-runtime` / `dsh-client-ui-theme` / `dsh-client-i18n` / `dsh-client-ui-layout` / `dsh-client-ui-sidebar` / `dsh-client-ui-conversation` / `dsh-client-ui-trajectory` | 浏览器侧 cordis 插件树(wire 消费方、核心服务、主题、i18n、布局、侧栏、对话、轨迹)——见 Web 客户端架构 RFC | 双入口(node 半边=空 apply;实现在 `src/client/`);消费面唯一经 ApiProxy | | 应用态 | `@deepseek-ai/dsh`(apps/cli)+ `dsh-frontend`(apps/web,vite 应用) | bin 粗分发 + 每形态一个拼装模块(web.ts / headless.ts);vite 应用是 `dsh-client-web` 壳表面之上的薄 main | 形态间动态 import 互不加载;dist 定位等 workspace 知识留在 app | #### 命名规则 @@ -116,7 +116,7 @@ TypeScript 以 solution 根引用的**两个聚合 program** 检查(`tsconfig. `ClientResponse` 的 HTTP 应答体是 `RpcReceipt = { accepted: true } | { accepted: false; reason: 'not-pending' | 'bad-response' }`——载体层回执,**不是** RpcMessage(response 不再有 response);迟到/重复应答收 `not-pending`,逻辑收敛面是 `*/resolved` 帧。 -## 类型体系:函数签名即事实源 +## 类型体系:函数签名即真源 ### RpcMethodMap 与派生泛型(`api/rpc-map.ts`) @@ -151,7 +151,7 @@ export type ResponseValue = - **锚定**:schema 统一 `satisfies z.ZodType>`(`api/rpc.schema.ts`)。`Wire` 是深度「| undefined」宽化——仓库开 `exactOptionalPropertyTypes` 而 zod `.optional()` 输出 `T | undefined`,直接锚原类型全线不可用;JSON wire 上缺席与 undefined 同形,宽化不损失校验语义。透传宽分支(`SessionEvent`/`ContentBlock`/帧 union/`RpcError`)与 brand id schema 用显式 cast + 注释。 - brand cast 单点:每个 schema 文件的 id cast 收口一处(`rpcIdSchema` 是 rpc.schema.ts 唯一 cast 点)。 -## 契约面(ApiProxy) +## 约定面(ApiProxy) 根接口 `ApiProxy = { sessions, host, events, respond }`(`api/index.ts`)。新 client-request 域 = 新的一对文件(`<域>.ts` + `<域>.schema.ts`)+ 根接口一个字段 + map 加行。 @@ -163,7 +163,7 @@ export type ResponseValue = |---|---|---|---| | `session.list` | `{ cursor?: string }`(cursor 留座不实现) | `{ items: SessionSummary[] }` | 已持久化 session,updatedAt 倒序;v1 不建索引 | -其余方法(`session.create`/`session.history`/`session.rename`/`session.prompt`/`session.cancel`/`host.describe`)的参数与返回不在此复写——签名即事实源,见 `api/sessions.ts`、`api/host.ts` 与 `RpcMethodMap`。 +其余方法(`session.create`/`session.history`/`session.rename`/`session.prompt`/`session.cancel`/`host.describe`)的参数与返回不在此复写——签名即真源,见 `api/sessions.ts`、`api/host.ts` 与 `RpcMethodMap`。 ### 帧(server→client,具名 union) @@ -173,7 +173,7 @@ export type ResponseValue = |---|---|---| | `session/event` | `{ sessionId; event: SessionEvent }` | 核心透传:core 事件原样过,`assistant/chunk` 即 token 流,无独立 delta 帧 | -其余帧型不在此复写,union 全集见 `api/events.ts` 的 `MuxFrame`/`HostFrame`。语义上须知三点:`session/subscribed` 的 lastSeq 供 history 补缝竞态检测;`approval/question` 的 requested 帧可应答(rpcId 稳定)、resolved 帧是收敛面;`host/agent-error` 是无 turn 位置 live 失败的唯一出口。 +其余帧型不在此复写,union 全集见 `api/events.ts` 的 `MuxFrame`/`HostFrame`。语义上须知三点:`session/subscribed` 的 lastSeq 供 history 补缺口竞态检测;`approval/question` 的 requested 帧可应答(rpcId 稳定)、resolved 帧是收敛面;`host/agent-error` 是无 turn 位置 live 失败的唯一出口。 **透传纪律**:wire 上的事件/消息/内容块就是 core 类型(`SessionEvent`/`ContentBlock`),不造第二套 DTO;类型经 `import type` 依赖链直达浏览器。`SessionEventMap` merge-extensible:client 对未知 type documented-default(忽略),事件 schema 留「合法信封+未知类型」分支——信封仍严格,不是字段级 passthrough。 @@ -181,11 +181,11 @@ export type ResponseValue = - **历史 = 事件重放**:一套 fold(client 侧),历史分页与 live 增量同一条代码路径;server 不做物化快照第二套。history **页边界对齐消息边界**(绝不从消息中间截断;chunk 随定稿消息归组),尾页含进行中 partial 的 chunk。 - **prompt 关联**:prompt 的 rpcId 经 MessageSource(`'user-rpc'`)透传进 `user/message` 事件,client 以此把乐观回显转正。 -- **重连 = 重建**:不做续传 cursor(`mux` 的 `since` 签名留座、传了忽略);断线重开流 + 重拉 history;`subscribed.lastSeq` 与 history 尾 seq 比对,有缝再补拉一次。 -- **冷会话处理遵循所有权**:`session.history` 与 `session.fork` 的源端读取会在不获取 Agent 的情况下检查持久化存储,而绑定到 Agent 的普通会话方法(如 `prompt`)则通过在途表去重后恢复会话。由会话支撑的 subagent 会拒绝这条通用恢复路径,且附加状态不对客户端暴露(`running` 已经覆盖)。 -- **审批/问答**:requested 帧受理时 mint 稳定 rpcId;先到先赢,host 内存 pending 表(keyed by rpcId)是唯一裁判;mux 重开后在 subscribed 帧后重放仍 pending 的 requested 帧(rpcId 原样复用,刷新恢复)。审计事件 `approval/asked`/`decided` 照旧走 durable 日志——帧=live 控制面,事件=durable 审计。**现状**:契约与帧类型已 shipped,host 侧 pending 表/wire answerer 未实现(`api-proxy.ts` 的 `respond` 是 stub,恒回 `not-pending`);PendingCard v1 只展示。 +- **重连 = 重建**:不做续传 cursor(`mux` 的 `since` 签名留座、传了忽略);断线重开流 + 重拉 history;`subscribed.lastSeq` 与 history 尾 seq 比对,如有缺口,再补拉一次。 +- **冷会话处理遵循所有权**:`session.history` 与 `session.fork` 的源端读取会在不获取 agent(智能体) 的情况下检查持久化存储,而绑定到 agent 的普通会话方法(如 `prompt`)则通过在途表去重后恢复会话。由会话支撑的 subagent 会拒绝这条通用恢复路径,且附加状态不对客户端暴露(`running` 已经覆盖)。 +- **审批/问答**:requested 帧受理时 mint 稳定 rpcId;先到先赢,host 内存 pending 表(keyed by rpcId)是唯一裁判;mux 重开后在 subscribed 帧后重放仍 pending 的 requested 帧(rpcId 原样复用,刷新恢复)。审计事件 `approval/asked`/`decided` 照旧走 durable 日志——帧=live 控制面,事件=durable 审计。**现状**:约定与帧类型已 shipped,host 侧 pending 表/wire answerer 未实现(`api-proxy.ts` 的 `respond` 是 stub,恒回 `not-pending`);PendingCard v1 只展示。 - **不设协议版本**:client 与 host 绑定发布,`host.describe` 无 protocolVersion 字段;出现独立发布的 client 时再引入。 -- **预留接缝纪律**:map 只含已实现方法,未知 method 在信封 parse 即 fail loud(`bad-request`),不设 not-implemented 兜底码。预留清单(实现时把签名抄进域接口+map 加行+schema 加对即升格):`session.fork`、`prompt.mode` 加 `'inject'`、`task.list`、`host.listModels`、describe 加 `hostInstanceId`。(`session.rename` 已从本清单毕业:追加 user 来源的 `session/title` 事件。) +- **预留 seam纪律**:map 只含已实现方法,未知 method 在信封 parse 即 fail loud(`bad-request`),不设 not-implemented 兜底码。预留清单(实现时把签名抄进域接口+map 加行+schema 加对即升格):`session.fork`、`prompt.mode` 加 `'inject'`、`task.list`、`host.listModels`、describe 加 `hostInstanceId`。(`session.rename` 已从本清单毕业:追加 user 来源的 `session/title` 事件。) ## 客户端载体:AbstractApiClient 类体系(`fetch/client.ts`) @@ -193,7 +193,7 @@ export type ResponseValue = ### IApiClient:caller 视图 -与 `ApiProxy` 同域树,但 unary 方法**收业务 payload 直传**——载体 mint rpcId 并包信封,业务代码永不 mint;需要本次调用 rpcId 的从返回的 `RpcResponse` 回显里读。`ApiProxy` 是 impl 侧实现的窄形签名契约,`IApiClient` 是 client 侧消费的 payload 直传视图,`AbstractApiClient` 桥接两者。方法逐 key 从 `RpcMethodMap` 派生——map 加行即机械更新。 +与 `ApiProxy` 同域树,但 unary 方法**收业务 payload 直传**——载体 mint rpcId 并包信封,业务代码永不 mint;需要本次调用 rpcId 的从返回的 `RpcResponse` 回显里读。`ApiProxy` 是 impl 侧实现的窄形签名约定,`IApiClient` 是 client 侧消费的 payload 直传视图,`AbstractApiClient` 桥接两者。方法逐 key 从 `RpcMethodMap` 派生——map 加行即机械更新。 ### 基类持有的协议路径 @@ -207,47 +207,47 @@ export type ResponseValue = ### 实例级 envelope 观测切面 -四象限全形均过 `onEnvelope`;基类实现是**实例持有的微任务合批缓冲**(帧风暴不逐帧惊扰消费者;模块级状态会跨实例/测试泄漏,故实例持有)。观测者经 `subscribeEnvelopes(listener)` 订阅(收整批 `readonly RpcMessage[]`,返回退订函数);listener 抛异常被隔离(观测不得反噬载体)。无订阅者时零缓冲成本。当前没有任何现役消费者订阅——该切面是 wire 诊断的预留位(已退役的 RPC 调试面板是它的首个消费者,将来的诊断消费者接入时不动载体)。 +四象限全形均过 `onEnvelope`;基类实现是**实例持有的微任务合批缓冲**(帧风暴不逐帧惊扰消费方;模块级状态会跨实例/测试泄漏,故实例持有)。观测者经 `subscribeEnvelopes(listener)` 订阅(收整批 `readonly RpcMessage[]`,返回退订函数);listener 抛异常被隔离(观测不得反噬载体)。无订阅者时零缓冲成本。当前没有任何现役消费方订阅——该切面是 wire 诊断的预留位(已退役的 RPC 调试面板是它的首个消费方,将来的诊断消费方接入时不动载体)。 ### 子类表(传输承载) | 子类 | 所在包 | doFetch | 用途 | |---|---|---|---| -| `InProcessApiClient` | apiproxy 本包 | 注入的 `{ fetch }` handler | **同构点**:`new InProcessApiClient(toFetchHandler(api))` 全程不过网络但真跑 wire 序列化/zod/SSE 帧——`dsh run` headless 即协议第二真实消费者 | +| `InProcessApiClient` | apiproxy 本包 | 注入的 `{ fetch }` handler | **同构点**:`new InProcessApiClient(toFetchHandler(api))` 全程不过网络但真跑 wire 序列化/zod/SSE 帧——`dsh run` headless 即协议第二个真正的消费方 | | `WebApiClient` | dsh-client-connection | `globalThis.fetch` 上行 + 每逻辑流一条同源 WebSocket 下行 | 浏览器形态;物理边界见 [WebSocket 下行载体](2026-08-04-websocket-downlink-carrier.md) | | `FixtureApiClient` | dsh-client-connection | 不用(协议层覆写) | 无 server 的 UI 开发(`?fixture`):覆写 `callUnary`/`openMux`/`openHost`/`respond` 虚方法,自己就是假 server(帧 rpcId 由它 mint,语义自洽) | -| (将来)IPC 桥子类 | apps/electron | IPC 序列化往返 | 仅换 doFetch,契约/基类零改 | +| (将来)IPC 桥子类 | apps/electron | IPC 序列化往返 | 仅换 doFetch,约定/基类零改 | ## 怎么扩展(操作清单) -**加一个 unary 方法(5 步)**:①域接口加方法签名(参数/返回内联,这是唯一事实源);②`RpcMethodMap` 加一行;③`<域>.schema.ts` 加 request/value schema 对(锚 `Wire>`);④handler `UNARY_ROUTES` 加一行(handler 的 Web 承载见 Web 客户端架构 RFC);⑤impl 实现(回显 `request.rpcId`)。client 侧 `IApiClient`/`AbstractApiClient` 的域方法表同步加一行透传。 +**加一个 unary 方法(5 步)**:①域接口加方法签名(参数/返回内联,这是唯一真源);②`RpcMethodMap` 加一行;③`<域>.schema.ts` 加 request/value schema 对(锚 `Wire>`);④handler `UNARY_ROUTES` 加一行(handler 的 Web 承载见 Web 客户端架构 RFC);⑤impl 实现(回显 `request.rpcId`)。client 侧 `IApiClient`/`AbstractApiClient` 的域方法表同步加一行透传。 **加一个帧型(3 步)**:①`MuxFrame`/`HostFrame` union 加一支(可应答帧须注明 rpcId 稳定语义);②帧 schema 加一支;③消费端 fold/路由的 documented-default 已兜底未知型,按需加显式分支。 **加一个错误码(2 步)**:①`RpcErrorDetailsMap` 加一行(details 必填);②`rpcErrorSchema` discriminatedUnion 加一支。 -**接一种新载体**:继承 `AbstractApiClient` 只实现 `doFetch`;需要拦截协议层(如 fixture)再覆写 `callUnary`/`openMux`/`openHost` 虚方法。契约与基类零改。 +**接一种新载体**:继承 `AbstractApiClient` 只实现 `doFetch`;需要拦截协议层(如 fixture)再覆写 `callUnary`/`openMux`/`openHost` 虚方法。约定与基类零改。 -**升格一个预留接缝**:把预留签名抄进域接口 → map 加行 → schema 加对 → UNARY_ROUTES 加行 → impl 实现。 +**升格一个预留 seam**:把预留签名抄进域接口 → map 加行 → schema 加对 → UNARY_ROUTES 加行 → impl 实现。 ## Consequences -所有 client 形态消费同一契约:加一个 unary 方法是从单一签名辐射的五步机械改动,换载体只动一个 `doFetch` 子类,wire 上每条消息可 zod 校验、可经 envelope tap 观测、可按 rpcId 对账。普通 unary 调用仍受时限约束,而 `host.pickDirectory` 与 `command.execute` 可保持挂起,直到操作完成或调用方/连接取消到来;若由用户掌控节奏的操作不自行结束,请求可能一直挂起,这是为避免把合理的操作时长视为传输失败而接受的代价。其余接受的代价:两组包需要显式 tsconfig paths 条目;预留接缝(fork/inject/task.list/listModels/hostInstanceId)在真实消费者出现前保持休眠。 +所有 client 形态消费同一约定:加一个 unary 方法是从单一签名辐射的五步机械改动,换载体只动一个 `doFetch` 子类,wire 上每条消息可 zod 校验、可经 envelope tap 观测、可按 rpcId 对账。普通 unary 调用仍受时限约束,而 `host.pickDirectory` 与 `command.execute` 可保持挂起,直到操作完成或调用方/连接取消到来;若由用户掌控节奏的操作不自行结束,请求可能一直挂起,这是为避免把合理的操作时长视为传输失败而接受的代价。其余接受的代价:两组包需要显式 tsconfig paths 条目;预留 seam(fork/inject/task.list/listModels/hostInstanceId)在真正的消费方出现前保持休眠。 ## Alternatives considered | 放弃项 | 一句话理由 | |---|---| -| 按「产品形态」分包(web 一族、electron 一族) | 形态间共享的是 host/client 两侧能力而非形态本身;能力支持方分层让新形态零新包 | -| 混合体建包(如 headless 独立包) | 混合体只有一个消费者(它自己的 app),建包是无主抽象;拼装写在 app 里可读可弃 | -| 消费型 client 直连 ctx(省 apiproxy 一层) | 第二命令面绕开契约,wire 校验/观测/多端一致性全失;ctx 只留给前门与 headless 事件订阅两个正式用途 | +| 按「产品形态」分包(web 一族、electron 一族) | 形态间共享的是 host/client 两侧能力而非形态本身;能力提供方分层让新形态零新包 | +| 混合体建包(如 headless 独立包) | 混合体只有一个消费方(它自己的 app),建包是无主抽象;拼装写在 app 里可读可弃 | +| 作为消费方的 client 直连 ctx(省 apiproxy 一层) | 第二命令面绕开约定,wire 校验/观测/多端一致性全失;ctx 只留给前门与 headless 事件订阅两个正式用途 | | webserver 依赖 runtime(省 handler 注入) | 结构 typing 注入让 webserver 可被 sidecar/测试复用且零 workspace 依赖;包依赖会把装配知识拖进承载层 | | 包名不带组前缀(沿用 dsh-<尾段>) | `dsh-runtime`/`dsh-web-ui` 在扁平 npm 命名空间里失去归属信息;代价只是每包一条显式 paths | -| 复用仓内 JSON-RPC 2.0(dsh-jsonrpc) | 数字错误码退化成单码兜底、契约双份人肉对齐、命名无 convention 自然漂移 | +| 复用仓内 JSON-RPC 2.0(dsh-jsonrpc) | 数字错误码退化成单码兜底、约定双份人肉对齐、命名无 convention 自然漂移 | | 三信封模型(Request/Response/Frame 各一信封,签名不感知方向) | rpcId 是逻辑层关联,帧与应答的方向语义靠通道推断在换载体时即失效 | -| 具名 Request/Response 类型对为事实源(map 登记类型对) | 平铺具名类型是同一事实的第二个名字;签名 infer 反推让加方法只改一处 | -| REST 风格路径 | 消费者是自家 client,无第三方 REST 体验诉求;RPC 直映方法表更机械 | +| 具名 Request/Response 类型对为真源(map 登记类型对) | 平铺具名类型是同一事实的第二个名字;签名 infer 反推让加方法只改一处 | +| REST 风格路径 | 消费方是自家 client,无第三方 REST 体验诉求;RPC 直映方法表更机械 | | DTO 层(wire 专用第二套结构) | core 类型 type-only 直达浏览器零成本;DTO 是永久的双向同步税 | -| cursor 续传(mux since 实装) | 重连=重建(opencode 同款)覆盖 v1 全部需求;签名留座,实装等真实消费者 | +| cursor 续传(mux since 实装) | 重连=重建(opencode 同款)覆盖 v1 全部需求;签名留座,实装等真正的消费方 | | createApiClient 工厂函数(原实现) | 平台差异(传输/观测)是继承切面不是参数;类体系让 fixture 在协议层替换而不是包一层假信封 | | 对 `command.execute` 应用 30 秒传输时限 | 命令耗时属于操作本身,而非传输健康预算;该时限会终止本应继续运行的长时处理器,调用方/连接取消已提供所需的停止路径 | diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml index d3bbc7da9d..7a82202c72 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml @@ -3,4 +3,4 @@ # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md 2026-07-19-gui-web-client-architecture.md: e25d002b92df2016faf4073108aa905b65a1b115 -2026-07-19-gui-web-client-architecture.zh.md: cf31220aad61e4845f7fe90b73bf5da4432e53de +2026-07-19-gui-web-client-architecture.zh.md: 78a212dc18c1f553a6f782ed60822e36a622bda3 diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md index cf31220aad..78a212dc18 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md @@ -1,10 +1,10 @@ -# RFC: Web 客户端架构——client cordis 插件树、slot 体系与 React-free 对象层 +# Agent Note: Web 客户端架构——client cordis 插件树、slot 体系与 React-free 对象层 Status: implemented [English](2026-07-19-gui-web-client-architecture.md) | 中文 -> 分工线:通道无关的分层模型与 RPC 协议(消息模型/类型体系/契约面/客户端基类)见 [分层与 RPC 协议 RFC](2026-07-19-gui-layering-and-rpc-protocol.md);本篇 = 浏览器侧:client cordis 树如何装载、UI 插件如何经 slot 与服务组合、React-free 对象层如何以不可变快照供给 React。 +> 分工线:通道无关的分层模型与 RPC 协议(消息模型/类型体系/约定面/客户端基类)见 [分层与 RPC 协议 RFC](2026-07-19-gui-layering-and-rpc-protocol.md);本篇 = 浏览器侧:client cordis 树如何装载、UI 插件如何经 slot 与服务组合、React-free 对象层如何以不可变快照供给 React。 ## Problem @@ -30,27 +30,27 @@ Status: implemented ## client cordis 树与装载链 -装载链——两类包(普通包 vs dshClient 插件)、模块系统/插件治理器之分、host 独家撰写的带修订号 entry 图之上的双层 boot、热重载——归 [client 插件装载 RFC](2026-07-23-client-plugin-loading-model.md) 所有。本篇赖以立足的事实:浏览器启动与 host 相同的 vendored `@cordisjs/plugin-loader`,由 client 模块系统(`ctx.modules`,`packages/client/modules`)填上其 `internal` seam;凡带产品行为的单元都是 host 独家撰写的 `__DSH_BOOT__` 图里的 entry——每个生产插件包(含基础设施)都携带 `dshClient` 声明、以 fetch 到达的 `./client` tsdown 闭包 bundle 供给,`immediately` 行的差别仅在 boot 第一层预取,而普通包(react 家族、cordis、尚未升格的库)保持打进壳、已播种、对图不可见;bundle 执行 `window.__ModuleLoader__.load({ id, factory })`,其 `require` 由 lazy CJS 模块表应答(种子词条 + 已登记工厂,首次 require 时物化并记忆化——跨插件值 import 是构建错误,协作走 cordis 服务);插件 CSS 内联在 bundle 里、物化时注入为 `