From 953548cd59b9c5a8b7e8551465daecee675916d7 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Mon, 26 Oct 2026 17:50:30 +0800 Subject: [PATCH 01/65] feat(web): continue generation after transient request failures --- apps/web/tests/live-interactions.e2e.ts | 32 +++++++- .../client/ui-conversation/README.i18n.yaml | 4 +- packages/client/ui-conversation/README.md | 2 + packages/client/ui-conversation/README.zh.md | 2 + .../ui-conversation/src/client/apply.ts | 8 ++ .../src/client/contract/slots.ts | 9 ++- .../src/client/input/continuation.ts | 55 +++++++++++++ .../ui-conversation/src/client/input/hub.ts | 42 +++++++++- .../ui-conversation/src/client/locales.ts | 16 ++++ .../src/client/skeleton/InputBar.module.css | 5 ++ .../src/client/skeleton/InputBar.tsx | 29 +++++-- .../tests/apply-inject.client.spec.tsx | 46 +++++++++++ .../tests/continuation-fixture.client.ts | 28 +++++++ .../tests/continuation.client.spec.ts | 81 +++++++++++++++++++ .../tests/input-bar.client.spec.tsx | 40 +++++++-- .../tests/input-matrix.client.spec.tsx | 2 + .../tests/input-scenarios.client.spec.tsx | 2 + .../tests/skeleton.client.spec.tsx | 2 + .../live-interactions/continued.expected.md | 39 +++++++++ .../retry-exhausted.expected.md | 2 +- 20 files changed, 426 insertions(+), 20 deletions(-) create mode 100644 packages/client/ui-conversation/src/client/input/continuation.ts create mode 100644 packages/client/ui-conversation/tests/continuation-fixture.client.ts create mode 100644 packages/client/ui-conversation/tests/continuation.client.spec.ts create mode 100644 snapshots/web/live-interactions/continued.expected.md diff --git a/apps/web/tests/live-interactions.e2e.ts b/apps/web/tests/live-interactions.e2e.ts index 1eedb317fd..0a6c78c8fc 100644 --- a/apps/web/tests/live-interactions.e2e.ts +++ b/apps/web/tests/live-interactions.e2e.ts @@ -414,15 +414,16 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => { expect(tripwire.warnings).toEqual([]) }, 120_000) - it.skipIf(MODE === 'record')('surfaces the terminal turn error after transient retries exhaust', async () => { + it.skipIf(MODE === 'record')('continues after transient retries exhaust through a localized user message', async () => { // A whole-script replacement: three throw entries cover the first request // plus both budgeted retries (patches cannot reach past the one-call // derived script). The scenario-owned policy keeps exhaustion fast and // jitter-free instead of walking the shared default's five backed-off // attempts. const failure: ReplayEntry = { kind: 'throw', chunks: [], message: 'upstream 503', code: 'SERVER' } + const derived = deriveReplayScript(parseSessionLog(await readFile(FIXTURE, 'utf8'))) await launch( - () => [failure, failure, failure], + () => [failure, failure, failure, ...derived], { mode: 'normal', maxRetries: 2, retryableCodes: ['SERVER'], backoff: { initialDelayMs: 25, maxDelayMs: 50, jitterRatio: 0 } }, ) onTestFailed(() => saveFailureShot(page, 'web-e2e-retry-exhausted')) @@ -443,6 +444,31 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => { // context; the golden pins both. const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold!.workspaceCwd) await compareOrRefreshGolden(RETRY_EXHAUSTED_EXPECTED, snapshot, MODE) + await page.reload({ waitUntil: 'load' }) + const resume = page.getByRole('button', { name: 'Continue generating', exact: true }) + await resume.waitFor() + const input = page.locator('[data-composer-input]').first() + await input.fill('A different instruction') + expect(await resume.count()).toBe(0) + expect(await page.getByRole('button', { name: 'Send message', exact: true }).isEnabled()).toBe(true) + await input.fill('') + await resume.waitFor() + await page.setViewportSize({ width: 390, height: 844 }) + const bounds = await resume.boundingBox() + expect(bounds).not.toBeNull() + expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(390) + expect(await resume.isEnabled()).toBe(true) + const continued = scaffold!.whenTurnSettled() + await resume.click() + await continued + expect(turnEndReasons(sessionEvents)).toEqual(['error', 'completed']) + const userMessages = sessionEvents.flatMap(event => event.type === 'user/message' && event.data.source.kind === 'user' + ? [event.data.content] : []) + expect(userMessages).toEqual([[{ type: 'text', text: PROMPT }], [{ type: 'text', text: 'continue' }]]) + expect(await resume.count()).toBe(0) + await page.setViewportSize({ width: 1440, height: 900 }) + await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'continued.expected.md'), + await captureStableAria(page, '[class*="centerCol"]', scaffold!.workspaceCwd), MODE) expect(tripwire.pageErrors).toEqual([]) expect(tripwire.warnings).toEqual([]) }, 120_000) @@ -451,7 +477,7 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => { await assertFixtureInventory(SNAPSHOT_DIR, [ 'session.v3.jsonl', 'cancel.expected.md', 'cancel-expanded.expected.md', 'loading.expected.md', 'running-draft.expected.md', 'error-auth.expected.md', - 'retry.expected.md', 'retry-expanded.expected.md', 'retry-request.expected.md', 'retry-exhausted.expected.md', + 'retry.expected.md', 'retry-expanded.expected.md', 'retry-request.expected.md', 'retry-exhausted.expected.md', 'continued.expected.md', ]) }) }) diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index 09a0211fa7..ac94283c97 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/README.i18n.yaml @@ -21,8 +21,8 @@ en: cc4dafd1aae02468 zh: be6b67724b7ac4a3 /deepseek-ai-dsh-client-ui-conversation/shell-and-standard-props: - en: c62e2e19c9680567 - zh: e05ba69f967918c0 + en: 483f64e0a313ffaa + zh: 21a243552ac05038 /deepseek-ai-dsh-client-ui-conversation/temporary-composer-entries: en: bb1244310f5c0382 zh: 824b84077432b559 diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index 3b57654892..cfe8144cec 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -76,6 +76,8 @@ The composer measures its expanded control groups after size, content, visibilit The context meter follows the [statistics-control radius](../../../docs/ui-radius.md#radius-scale). +An empty, idle ordinary Session composer offers a play action after `RATE_LIMIT`, `SERVER`, `TIMEOUT`, `TRANSPORT`, `EMPTY_RESPONSE`, or `STREAM_CLOSED` ends its latest turn. The failed turn must include admitted user input, a settled model attempt, and a loaded start. Clicking submits a normal user message in the current interface language (`继续` in Chinese, `continue` in English); drafts and attachments keep the Send action. Continued requests retain committed history and use the existing submission lifecycle. + The composer uses the shared [text-input focus policy](../ui-primitives/README.md#use-this-package) on mount, Session switching, and unlock. Identified phones and tablets require tapping the input; desktop devices retain automatic focus. On these mobile devices, a submission that empties the focused draft moves focus to the non-text composer card, dismissing the keyboard while retaining its navigation position. Rejected submissions, retained newer draft text, and another control already holding focus preserve focus. The [Lexical patch](../../../patches/lexical@0.49.0.patch) keeps caret scrolling in the same coordinate system as DOM geometry when a mobile keyboard pans the page. The context-occupancy button shows a ring and percentage below the input card, after the Session statistics. Clicking it opens the token breakdown in a panel kept inside the viewport, including when no statistics are shown; the button stays hidden until context usage and capacity are available. diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index eebbbfca94..f8a1872c79 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -76,6 +76,8 @@ Queue 预览启用空白折叠和文档边缘 trim,保留 emoji,并使用 CS 上下文占用率入口遵循[统计控件圆角规则](../../../docs/ui-radius.zh.md#radius-scale)。 +普通 Session 的输入框在空草稿且空闲时,若最新一轮以 `RATE_LIMIT`、`SERVER`、`TIMEOUT`、`TRANSPORT`、`EMPTY_RESPONSE` 或 `STREAM_CLOSED` 结束,会提供播放图标按钮。失败轮次必须包含已提交的用户输入和已落盘的模型请求尝试,且起始事件已加载。点击后按当前界面语言提交一条普通用户消息(中文为“继续”,英文为“continue”);存在草稿或附件时保留发送按钮。继续请求保留已提交历史,并沿用现有提交流程。 + composer 在挂载、切换 Session 和解除锁定时使用共享的[文字输入框聚焦策略](../ui-primitives/README.zh.md#use-this-package)。识别出的手机和平板需要点击输入框;桌面设备保留自动聚焦。在这些移动设备上,提交清空正在聚焦的草稿后,焦点移到不接受文字输入的 composer 卡片,收起键盘并保留键盘导航位置。被拒绝的提交、保留的新草稿文字,以及已经聚焦的其他控件均保留原有焦点。移动端键盘平移页面时,[Lexical 补丁](../../../patches/lexical@0.49.0.patch)让光标滚动计算与 DOM 几何使用同一坐标系。 上下文占用按钮在输入卡片下方、会话统计右侧显示圆环和百分比。点击按钮可在视口内的面板查看 token 构成,没有统计项时面板也不会越界;上下文用量和容量尚不可用时,按钮保持隐藏。 diff --git a/packages/client/ui-conversation/src/client/apply.ts b/packages/client/ui-conversation/src/client/apply.ts index b3c0d3bcd0..08b136bf69 100644 --- a/packages/client/ui-conversation/src/client/apply.ts +++ b/packages/client/ui-conversation/src/client/apply.ts @@ -78,6 +78,10 @@ const ABSENT_NOTICES = { getSnapshot: (): InputNotice | null => null, subscribe: () => () => {}, } +const ABSENT_CONTINUATION = { + getSnapshot: () => undefined, + subscribe: () => () => {}, +} const ABSENT_BLOCK = { getSnapshot: (): ComposerBlock | undefined => undefined, subscribe: () => () => {}, @@ -482,7 +486,9 @@ export function apply(ctx: Context, config: Config = Config({})): void { retryFileUpload: undefined, toggleCommandMenu: undefined, stop: undefined, + continueGeneration: undefined, hooks: { + continuation: ABSENT_CONTINUATION, stopShortcut, busyEnter: submissionPolicy.busyEnter, fileUploads: ABSENT_FILE_UPLOADS, @@ -561,7 +567,9 @@ export function apply(ctx: Context, config: Config = Config({})): void { }) }, stop: () => { stop(sessionId) }, + continueGeneration: (ending) => { inputHub.continueGeneration(sessionId, ending) }, hooks: { + continuation: inputHub.continuation(sessionId), stopShortcut, busyEnter: submissionPolicy.busyEnter, fileUploads: conversation.fileUploads, diff --git a/packages/client/ui-conversation/src/client/contract/slots.ts b/packages/client/ui-conversation/src/client/contract/slots.ts index caec4849b4..c190443fb7 100644 --- a/packages/client/ui-conversation/src/client/contract/slots.ts +++ b/packages/client/ui-conversation/src/client/contract/slots.ts @@ -14,7 +14,7 @@ import type { } from '@deepseek-ai/dsh-client-ui-slots' import type { SessionPendingInteraction } from '@deepseek-ai/dsh-client-ui-session/client' import type {} from '@deepseek-ai/dsh-client-ui-layout/client' -import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type { SessionId, SessionSeq } from '@deepseek-ai/dsh-session/types' import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types' import type { ComposerBlock } from './composer-blocks.ts' import type { DraftAttachmentId, InputActions, InputNotice, InputState } from './input.ts' @@ -529,7 +529,14 @@ export interface ComposerBarInjected { retryFileUpload: ((id: DraftAttachmentId) => void) | undefined toggleCommandMenu: ((selection: EditSelection) => void) | undefined stop: (() => void) | undefined + /** + * Submit a localized continuation message for the displayed failure. + * @param ending - sequence of the displayed failed turn/end. + */ + continueGeneration: ((ending: SessionSeq) => void) | undefined hooks: { + /** Latest transient failure with admitted user input and a loaded turn start. */ + continuation: ObservableSnapshot /** Readable sequence while the fixed Stop command is registered. */ stopShortcut: ObservableSnapshot /** diff --git a/packages/client/ui-conversation/src/client/input/continuation.ts b/packages/client/ui-conversation/src/client/input/continuation.ts new file mode 100644 index 0000000000..261629c084 --- /dev/null +++ b/packages/client/ui-conversation/src/client/input/continuation.ts @@ -0,0 +1,55 @@ +/** Manual continuation of settled transient model-request failures. */ +import type { SessionEventSource } from '@deepseek-ai/dsh-api-session-controller/client' +import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store' +import type { SessionSeq } from '@deepseek-ai/dsh-session/types' + +const CONTINUABLE_FAILURES: ReadonlySet = new Set([ + 'RATE_LIMIT', 'SERVER', 'TIMEOUT', 'TRANSPORT', 'EMPTY_RESPONSE', 'STREAM_CLOSED', +]) + +/** + * Derive the latest eligible failure from the loaded history. A loaded turn + * start, admitted user message, and settled failed request are required; + * preparation failures may have consumed input without admitting it. + * @param feed - existing Session event source, including replacement and paging. + * @returns a stable source of the eligible turn/end sequence, absent otherwise. + */ +export function createContinuationSource(feed: SessionEventSource): ObservableSnapshot { + return { + getSnapshot: () => { + const { entries } = feed.getSnapshot() + let ending: SessionSeq | undefined + let admitted = false + let attempted = false + for (let index = entries.length - 1; index >= 0; index--) { + // oxlint-disable-next-line typescript/no-non-null-assertion -- index is bounded by this immutable window's length. + const entry = entries[index]! + if (entry.type === 'transient') return undefined + const event = entry.event + switch (event.type) { + case 'turn/end': + if (ending !== undefined || event.data.reason.kind !== 'error' + || !CONTINUABLE_FAILURES.has(event.data.reason.error.code)) return undefined + ending = event.seq + break + case 'turn/start': + return admitted && attempted ? ending : undefined + case 'assistant/attempt': + attempted = true + break + case 'step/start': + case 'assistant/message': + if (!attempted) return undefined + break + case 'user/message': + if (ending !== undefined && event.surfaceOp === 'append' && event.data.source.kind === 'user') admitted = true + break + default: + break + } + } + return undefined + }, + subscribe: listener => feed.subscribe(listener), + } +} diff --git a/packages/client/ui-conversation/src/client/input/hub.ts b/packages/client/ui-conversation/src/client/input/hub.ts index 02897a18c6..3c247d322e 100644 --- a/packages/client/ui-conversation/src/client/input/hub.ts +++ b/packages/client/ui-conversation/src/client/input/hub.ts @@ -11,7 +11,7 @@ import type { Context } from '@deepseek-ai/cordis' import type { ISessions, SessionBinding, SessionFace, } from '@deepseek-ai/dsh-api-session-controller/client' -import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type { SessionId, SessionSeq } from '@deepseek-ai/dsh-session/types' import type { TranslateNS } from '@deepseek-ai/dsh-client-locale/client' import type { InboxState } from '@deepseek-ai/dsh-agent/types' import type { ModelSelectionProjection } from '@deepseek-ai/dsh-agent/types' @@ -25,6 +25,7 @@ import type { ComposerBlocks } from '../contract/composer-blocks.ts' import type { InputSubmitMode } from '../contract/composer-submission.ts' import type { PopupDismissFace } from './facade.ts' import { SessionInputShell } from './facade.ts' +import { createContinuationSource } from './continuation.ts' import { reportMessageSubmission } from './submission-analytics.ts' import { readConversationDraft } from '../stores.ts' @@ -57,6 +58,7 @@ interface ConversationAttachmentFace { /** Session-addressed input facade registry (SessionInputResolver face + composer-layer extras). */ export class InputHub implements SessionInputResolver { private readonly shells = new WeakMap() + private readonly continuations = new WeakMap>() /** * @param ctx - client root context (services resolved lazily per call — boot order stays free). @@ -207,6 +209,44 @@ export class InputHub implements SessionInputResolver { return this.shell(id) } + /** + * Read manual-continuation eligibility from the retained Session history. + * @param id - retained ordinary Session identity. + * @returns the identity-stable eligibility source. + */ + continuation(id: SessionId): ObservableSnapshot { + const binding = this.sessions().binding(id) + if (binding === undefined) throw new Error(`conversation.input: session "${id}" resolved no binding`) + let source = this.continuations.get(binding) + if (source === undefined) { + source = createContinuationSource(binding.eventSource) + this.continuations.set(binding, source) + } + return source + } + + /** + * Submit localized continuation text through the ordinary composer. Stale + * failures, active work, and drafts keep their current input unchanged. + * @param id - retained ordinary Session identity. + * @param ending - failure the displayed button addresses. + */ + continueGeneration(id: SessionId, ending: SessionSeq): void { + const binding = this.sessions().binding(id) + if (binding === undefined || this.continuation(id).getSnapshot() !== ending) return + const session = binding.session.getSnapshot() + const shell = this.shellFor(binding) + const input = shell.snapshot + const inbox = binding.session.projections.faceOf('inbox').getSnapshot() as InboxState | undefined + if (session.running || session.removed || session.openState !== 'open' || session.subagent !== null + || session.pendingSubmissions.length > 0 || input.phase !== 'plain' + || input.draft.trim() !== '' || input.attachmentIds.length > 0 + || (inbox?.['next-turn'].length ?? 0) > 0 || (inbox?.['next-step'].length ?? 0) > 0 + || this.conversation().blocks.storeFor(id).getSnapshot() !== undefined) return + shell.setDraft(this.t('input.continueMessage')) + shell.submit('queue', 'click') + } + /** * Query file intake without creating a Session input. * @param id - target Session. diff --git a/packages/client/ui-conversation/src/client/locales.ts b/packages/client/ui-conversation/src/client/locales.ts index 5606842886..c9dc074e1c 100644 --- a/packages/client/ui-conversation/src/client/locales.ts +++ b/packages/client/ui-conversation/src/client/locales.ts @@ -28,6 +28,8 @@ export const zh = { 'input.file': '文件', 'input.stop': '停止生成', 'input.send': '发送消息', + 'input.continue': '继续生成', + 'input.continueMessage': '继续', 'input.sending': '正在发送消息', 'input.send.queue': '排队发送', 'input.send.steer': '插话发送', @@ -412,6 +414,8 @@ export const en = { 'input.file': 'File', 'input.stop': 'Stop generating', 'input.send': 'Send message', + 'input.continue': 'Continue generating', + 'input.continueMessage': 'continue', 'input.sending': 'Sending message', 'input.send.queue': 'Queue message', 'input.send.steer': 'Steer message', @@ -795,6 +799,8 @@ export const ja = { 'input.file': 'ファイル', 'input.stop': '生成を停止', 'input.send': 'メッセージを送信', + 'input.continue': '生成を続ける', + 'input.continueMessage': '続けて', 'input.sending': 'メッセージを送信中', 'input.send.queue': 'メッセージをキューに追加', 'input.send.steer': '実行中の指示にメッセージを追加', @@ -1176,6 +1182,8 @@ export const es = { 'input.file': 'Archivo', 'input.stop': 'Detener generación', 'input.send': 'Enviar mensaje', + 'input.continue': 'Continuar generando', + 'input.continueMessage': 'continúa', 'input.sending': 'Enviando mensaje', 'input.send.queue': 'mensaje de cola', 'input.send.steer': 'Redirigir mensaje', @@ -1559,6 +1567,8 @@ export const fr = { 'input.file': 'Fichier', 'input.stop': 'Arrêter la génération', 'input.send': 'Envoyer le message', + 'input.continue': 'Continuer la génération', + 'input.continueMessage': 'continue', 'input.sending': 'Envoi du message', 'input.send.queue': "Message de file d'attente", 'input.send.steer': 'Guider avec ce message', @@ -1942,6 +1952,8 @@ export const de = { 'input.file': 'Datei', 'input.stop': 'Generierung stoppen', 'input.send': 'Nachricht senden', + 'input.continue': 'Generierung fortsetzen', + 'input.continueMessage': 'weiter', 'input.sending': 'Nachricht wird gesendet', 'input.send.queue': 'Warteschlangennachricht', 'input.send.steer': 'Mit Nachricht lenken', @@ -2325,6 +2337,8 @@ export const ru = { 'input.file': 'Файл', 'input.stop': 'Остановить генерацию', 'input.send': 'Отправить сообщение', + 'input.continue': 'Продолжить генерацию', + 'input.continueMessage': 'продолжай', 'input.sending': 'Отправка сообщения', 'input.send.queue': 'Сообщение в очереди', 'input.send.steer': 'Направить сообщение', @@ -2708,6 +2722,8 @@ export const pt = { 'input.file': 'Arquivo', 'input.stop': 'Parar geração', 'input.send': 'Enviar mensagem', + 'input.continue': 'Continuar gerando', + 'input.continueMessage': 'continue', 'input.sending': 'Enviando mensagem', 'input.send.queue': 'Mensagem de fila', 'input.send.steer': 'Orientar com mensagem', diff --git a/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css b/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css index 7d6ae29adb..26203b9d11 100644 --- a/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css +++ b/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css @@ -407,6 +407,11 @@ transform: translateY(-2px); } +/* The triangle fills half its canvas; match the 14px arrow without widening the grid track. */ +.continueIcon { + transform: scale(1.75); +} + @media (hover: hover) { .primary:hover:not(:disabled) { background: var(--dsw-alias-button-info-hover); diff --git a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx index 09a43ec6fc..9d46a571d3 100644 --- a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx @@ -17,7 +17,7 @@ import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useStat import type { ChangeEvent, KeyboardEvent, MouseEvent } from 'react' import clsx from 'clsx' import { - focusWithoutRing, IconPlusOutlineMedium, IconWarningOutlineRegular, isMobileDevice, Toast, Tooltip, + focusWithoutRing, IconPlusOutlineMedium, IconTriangleRightFillRegular, IconWarningOutlineRegular, isMobileDevice, Toast, Tooltip, } from '@deepseek-ai/dsh-client-ui-primitives' // Type-only: the `plan` projection key merge (the TodoDock posture — the // composer reads a host-computed value; the domain owns the key). @@ -46,9 +46,9 @@ export type InputBarProps = ComposerBarProps export const InputBar = memo(function InputBar({ useSession, useInput, inputActions, keyboard, addFiles, removeAttachment, resolveDraftAttachments, retryFileUpload, - toggleCommandMenu, stop, t, + toggleCommandMenu, stop, continueGeneration, t, renderSlot, useBusyEnter, useFileUploads, useNotices, useLexicon, useMenuLauncher, useStopShortcut, - useAccess, + useAccess, useContinuation, useProjection, sessionId, variant, disabled: inert = false, blocked, workspacePickerOpen = false, onRequestWorkspace, placeholder, accessory, @@ -70,6 +70,9 @@ export const InputBar = memo(function InputBar({ useEffect(() => { setActivity(false) }, [sessionId]) const promptError = useSession(s => s.promptError) ?? null const running = useSession(s => s.running) ?? false + const continuation = useContinuation(value => value) + const historyOpen = useSession(s => s.openState === 'open') ?? false + const submissionPending = useSession(s => s.pendingSubmissions.length > 0) ?? false const subagent = useSession(s => s.subagent) ?? null const removed = useSession(s => s.removed) ?? false // Plan mode swaps the composer placeholder (the projection is the folded @@ -362,8 +365,12 @@ export const InputBar = memo(function InputBar({ // exposes Stop independently, and so does a running session while a // message waits for the Host. const primaryStops = running && subagent === null && !sending && (empty || blocked !== undefined) + const primaryContinues = !running && subagent === null && empty && continuation !== undefined + && historyOpen && !submissionPending && (input?.queue.length ?? 0) === 0 + && !disabled && input.phase === 'plain' && continueGeneration !== undefined // Disabled native buttons may omit mouseleave; their tooltip must close from state. - const primaryDisabled = primaryStops ? stopAction === undefined : empty || disabled || machineBusy || uploadsPending + const primaryDisabled = primaryStops ? stopAction === undefined + : (!primaryContinues && empty) || disabled || machineBusy || uploadsPending const interruptible = running && (continuable || (sending && subagent === null)) const primarySubmitMode = resolveSubmitMode(busyEnter, running, 'enter', steeringAvailable) const plainMessageDraft = !empty && input?.phase === 'plain' && !draft.trimStart().startsWith('/') @@ -371,14 +378,20 @@ export const InputBar = memo(function InputBar({ ? t('input.stop') : sending ? t('input.sending') - : running && steeringAvailable && !disabled && !uploadsPending && plainMessageDraft - ? t(primarySubmitMode === 'steer' ? 'input.send.steer' : 'input.send.queue') - : t('input.send') + : primaryContinues + ? t('input.continue') + : running && steeringAvailable && !disabled && !uploadsPending && plainMessageDraft + ? t(primarySubmitMode === 'steer' ? 'input.send.steer' : 'input.send.queue') + : t('input.send') const onPrimary = (): void => { if (primaryStops) { stopAction?.() return } + if (primaryContinues) { + if (!primaryDisabled) continueGeneration(continuation) + return + } if (keyboard === undefined) return // absent machine: the button is disabled /* v8 ignore next -- defensive: the primary button is disabled for empty, disabled, and pending-upload states. */ if (!empty && !disabled && !machineBusy && !uploadsPending) keyboard.submit(primarySubmitMode, 'click') @@ -560,6 +573,8 @@ export const InputBar = memo(function InputBar({ + ) : primaryContinues ? ( + ) : ( diff --git a/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx b/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx index 523fcffaae..dc69dad5bc 100644 --- a/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx +++ b/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx @@ -23,6 +23,8 @@ import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types' import { createConversationStore } from '../src/client/stores.ts' import type { SessionInputShell } from '../src/client/input/facade.ts' import { RemoteError } from '@deepseek-ai/dsh-client-test-runtime' +import { SessionSeq } from '@deepseek-ai/dsh-session/types' +import { continuationTurn } from './continuation-fixture.client.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { interface SlotMap { @@ -158,6 +160,50 @@ async function bench(renderQuote = false) { } describe('Conversation inject API', () => { + it.each([['zh', '继续'], ['en', 'continue']])('submits one continuation in the current %s locale', async (locale, message) => { + const b = await bench() + onTestFinished(() => b.runtime.dispose()) + await b.runtime.sessions.replaceEvents(ROOT, continuationTurn({ kind: 'error', error: { code: 'SERVER', message: 'busy' } })) + const composer = b.composerApi(ROOT) + expect(b.composerApi(ROOT).hooks.continuation).toBe(composer.hooks.continuation) + expect(composer.hooks.continuation.getSnapshot()).toBe(5) + b.runtime.ctx.locale.setLocale(locale) + const pending = Promise.withResolvers>>() + onTestFinished(() => { pending.resolve({ ok: true, value: { accepted: true } }) }) + b.sessionFake.prompt.mockImplementationOnce(() => pending.promise) + composer.continueGeneration!(SessionSeq(5)) + composer.continueGeneration!(SessionSeq(5)) + expect(composer.keyboard!.snapshot.phase).toBe('sending') + await vi.waitFor(() => { expect(b.sessionFake.prompt).toHaveBeenCalledOnce() }) + expect(b.sessionFake.prompt.mock.calls[0]?.[0]).toEqual([{ type: 'text', text: message }]) + expect(b.sessionFake.prompt.mock.calls[0]?.[1]).toBe('queue') + pending.resolve({ ok: true, value: { accepted: true } }) + await vi.waitFor(() => { expect(composer.keyboard!.snapshot.phase).toBe('plain') }) + expect(composer.keyboard!.snapshot.draft).toBe('') + }) + + it('rechecks the addressed failure, draft, and live activity before continuing', async () => { + const b = await bench() + onTestFinished(() => b.runtime.dispose()) + await b.runtime.sessions.replaceEvents(ROOT, continuationTurn({ kind: 'error', error: { code: 'TIMEOUT', message: 'timeout' } })) + const composer = b.composerApi(ROOT) + const input = b.inputApi(ROOT) + composer.continueGeneration!(SessionSeq(1)) + input.actions.setDraft('Keep my new instruction') + composer.continueGeneration!(SessionSeq(5)) + expect(input.state.getSnapshot().draft).toBe('Keep my new instruction') + input.actions.setDraft('') + await b.runtime.sessions.updateSessionSnapshot(ROOT, (state) => { state.running = true }) + composer.continueGeneration!(SessionSeq(5)) + await b.runtime.sessions.updateSessionSnapshot(ROOT, (state) => { state.running = false }) + await b.runtime.sessions.appendEvent(ROOT, { + type: 'event', event: { type: 'turn/start', seq: SessionSeq(6), time: 1, data: { turn: 2 } }, + }) + composer.continueGeneration!(SessionSeq(5)) + expect(input.state.getSnapshot().draft).toBe('') + expect(b.sessionFake.prompt).not.toHaveBeenCalled() + }) + it('renders literal quote text through the actual Factory and keyed node registration', async () => { const b = await bench(true) onTestFinished(() => b.runtime.dispose()) diff --git a/packages/client/ui-conversation/tests/continuation-fixture.client.ts b/packages/client/ui-conversation/tests/continuation-fixture.client.ts new file mode 100644 index 0000000000..20dc42fdc9 --- /dev/null +++ b/packages/client/ui-conversation/tests/continuation-fixture.client.ts @@ -0,0 +1,28 @@ +/** Admitted input and a settled turn for manual-continuation tests. */ +import type { SessionLiveEventEntry } from '@deepseek-ai/dsh-api-session-controller/client' +import { createUserMessage } from '@deepseek-ai/dsh-llm' +import { SessionSeq } from '@deepseek-ai/dsh-session/types' +import type { TurnEndReason } from '@deepseek-ai/dsh-session/types' + +/** + * Build one settled turn with ordinary user input. + * @param reason - recorded turn ending. + * @param number - turn number and corresponding event offset. + * @returns ordered durable entries. + */ +export function continuationTurn(reason: TurnEndReason, number = 1): SessionLiveEventEntry[] { + const offset = (number - 1) * 6 + return [ + { type: 'event', event: { type: 'turn/start', seq: SessionSeq(offset), time: 0, data: { turn: number } } }, + { type: 'event', event: { type: 'step/start', seq: SessionSeq(offset + 1), time: 0, data: { turn: number, step: 1 } } }, + { type: 'event', event: { + type: 'user/message', seq: SessionSeq(offset + 2), time: 0, surfaceOp: 'append', + data: createUserMessage({ content: [{ type: 'text', text: 'task' }], source: { kind: 'user' } }), + } }, + { type: 'event', event: { + type: 'assistant/attempt', seq: SessionSeq(offset + 3), time: 0, data: { turn: number, step: 1, stream: [] }, + } }, + { type: 'event', event: { type: 'step/end', seq: SessionSeq(offset + 4), time: 0, data: { turn: number, step: 1 } } }, + { type: 'event', event: { type: 'turn/end', seq: SessionSeq(offset + 5), time: 0, data: { turn: number, reason } } }, + ] +} diff --git a/packages/client/ui-conversation/tests/continuation.client.spec.ts b/packages/client/ui-conversation/tests/continuation.client.spec.ts new file mode 100644 index 0000000000..ba101e7737 --- /dev/null +++ b/packages/client/ui-conversation/tests/continuation.client.spec.ts @@ -0,0 +1,81 @@ +import { describe, expect, it, vi } from 'vitest' +import { MutableSessionEventSource } from '@deepseek-ai/dsh-api-session-controller/client' +import type { SessionLiveEventEntry } from '@deepseek-ai/dsh-api-session-controller/client' +import { SessionSeq } from '@deepseek-ai/dsh-session/types' +import type { TurnEndReason } from '@deepseek-ai/dsh-session/types' +import { createContinuationSource } from '../src/client/input/continuation.ts' +import { continuationTurn as turn } from './continuation-fixture.client.ts' + +const failed = (code: string): TurnEndReason => ({ kind: 'error', error: { code, message: code } }) + +describe('manual generation continuation', () => { + it.each(['RATE_LIMIT', 'SERVER', 'TIMEOUT', 'TRANSPORT', 'EMPTY_RESPONSE', 'STREAM_CLOSED'])( + 'offers continuation for an admitted request ending with %s', (code) => { + const feed = new MutableSessionEventSource() + feed.replace(turn(failed(code)), false) + expect(createContinuationSource(feed).getSnapshot()).toBe(5) + }, + ) + + it.each(['AUTH', 'QUOTA', 'ACCOUNT_QUOTA', 'MISSING_CREDENTIAL', 'INVALID_CREDENTIAL', 'UNKNOWN_MODEL', + 'INVALID_REQUEST', 'CONTEXT_WINDOW_EXCEEDED', 'TOOL_ACTIVATION_FAILED', 'MALFORMED_RESPONSE', 'PI_AI_ERROR', 'UNKNOWN'])( + 'leaves %s to its own recovery action', (code) => { + const feed = new MutableSessionEventSource() + feed.replace(turn(failed(code)), false) + expect(createContinuationSource(feed).getSnapshot()).toBeUndefined() + }, + ) + + it.each([ + { kind: 'completed' }, { kind: 'max-tokens' }, { kind: 'blocked' }, + { kind: 'interrupted' }, { kind: 'forked' }, { kind: 'aborted', reason: { kind: 'user' } }, + ])('leaves a $kind turn unchanged', (reason) => { + const feed = new MutableSessionEventSource() + feed.replace(turn(reason), false) + expect(createContinuationSource(feed).getSnapshot()).toBeUndefined() + }) + + it('requires user input from the failed turn to have entered model history', () => { + const feed = new MutableSessionEventSource() + const source = createContinuationSource(feed) + const entries = turn(failed('TIMEOUT'), 2) + feed.replace([...turn({ kind: 'completed' }), ...entries.filter(entry => entry.event.type !== 'user/message')], false) + expect(source.getSnapshot()).toBeUndefined() + const user = entries[2]! + if (user.event.type !== 'user/message') throw new Error('expected user input') + const replacement: SessionLiveEventEntry = { type: 'event', event: { + ...user.event, surfaceOp: { op: 'replace', startSeq: SessionSeq(0), endSeq: SessionSeq(1) }, + } } + feed.replace(entries.map(entry => entry === user ? replacement : entry), false) + expect(source.getSnapshot()).toBeUndefined() + }) + + it('keeps failures before request dispatch in their preparation flow', () => { + const feed = new MutableSessionEventSource() + const source = createContinuationSource(feed) + feed.replace(turn(failed('TIMEOUT')).filter(entry => entry.event.type !== 'assistant/attempt'), false) + expect(source.getSnapshot()).toBeUndefined() + }) + + it('restores eligibility from paging and replacement, and retires it when a later turn starts', () => { + const feed = new MutableSessionEventSource() + const source = createContinuationSource(feed) + const entries = turn(failed('SERVER')) + feed.replace(entries.slice(1), true) + expect(source.getSnapshot()).toBeUndefined() + feed.prepend(entries.slice(0, 1), false) + expect(source.getSnapshot()).toBe(5) + expect(createContinuationSource(feed).getSnapshot()).toBe(5) + const listener = vi.fn() + const unsubscribe = source.subscribe(listener) + feed.append({ type: 'event', event: { type: 'turn/start', seq: SessionSeq(6), time: 1, data: { turn: 2 } } }) + expect(listener).toHaveBeenCalledOnce() + expect(source.getSnapshot()).toBeUndefined() + unsubscribe() + feed.replace(turn({ kind: 'completed' }), false) + expect(listener).toHaveBeenCalledOnce() + expect(source.getSnapshot()).toBeUndefined() + feed.replace([], false) + expect(source.getSnapshot()).toBeUndefined() + }) +}) diff --git a/packages/client/ui-conversation/tests/input-bar.client.spec.tsx b/packages/client/ui-conversation/tests/input-bar.client.spec.tsx index a657abf806..0ba4d5f288 100644 --- a/packages/client/ui-conversation/tests/input-bar.client.spec.tsx +++ b/packages/client/ui-conversation/tests/input-bar.client.spec.tsx @@ -25,7 +25,7 @@ import type { SessionListState, SessionSnapshot } from '@deepseek-ai/dsh-api-ses import type { ContextPressureProjection } from '@deepseek-ai/dsh-token-meter/client' import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts' import type { Context } from '@deepseek-ai/cordis' -import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type { SessionId, SessionSeq } from '@deepseek-ai/dsh-session/types' import type { SubmitOutcome } from '../src/client/contract/input.ts' import { SessionInputShell } from '../src/client/input/facade.ts' import { $replaceDetectSpanWithText, $selectDetectSpan } from '../src/client/input/editor/span-map.ts' @@ -81,6 +81,7 @@ interface BenchOptions { } draft?: string running?: boolean + continuation?: SessionSeq subagent?: Exclude disabled?: boolean inert?: boolean @@ -161,6 +162,8 @@ function bench(over?: BenchOptions) { if (over?.draft !== undefined && over.draft !== '') shell.setDraft(over.draft) if (over?.attachments !== undefined) shell.addAttachments(over.attachments.map(attachment => attachment.id)) const stop = vi.fn() + const continueGeneration = vi.fn() + const continuation = createSnapshotStore(over?.continuation) const removeAttachment = vi.fn((id: DraftAttachmentId) => { shell.removeAttachment(id) }) const menuLauncher = createSnapshotStore(over?.commandMenuOpen === true ? 'command' : null) const busyEnter = createSnapshotStore<'queue' | 'steer'>(over?.busyEnter ?? 'queue') @@ -215,6 +218,8 @@ function bench(over?: BenchOptions) { }), toggleCommandMenu: over?.toggleCommandMenu ?? vi.fn(), useBusyEnter: bindSnapshotSelector(busyEnter), + continueGeneration, + useContinuation: bindSnapshotSelector(continuation), useStopShortcut: bindSnapshotSelector(stopShortcut), useNotices: bindSnapshotSelector(shell.notices), useLexicon: bindSnapshotSelector(shell.lexicon), @@ -252,20 +257,45 @@ function bench(over?: BenchOptions) { const plainMessageDraft = sendableDraft && !uploadsPending && !(over?.draft?.trimStart().startsWith('/') ?? false) const primaryLabel = primaryStops ? '停止生成' - : over?.running === true && steeringAvailable && !composerLocked && plainMessageDraft - ? (over.busyEnter === 'steer' ? '插话发送' : '排队发送') - : '发送消息' + : over?.continuation !== undefined && !sendableDraft && over.running !== true && over.subagent === undefined && !composerLocked + ? '继续生成' + : over?.running === true && steeringAvailable && !composerLocked && plainMessageDraft + ? (over.busyEnter === 'steer' ? '插话发送' : '排队发送') + : '发送消息' const button = view.container.querySelector(`button[aria-label="${primaryLabel}"]`)! const interruptButton = view.container.querySelector('button[aria-label="停止生成"]') return { view, textarea, button, interruptButton, props, sink, shell, wiring: shell, session, stop, removeAttachment, slotCalls, - menuLauncher, busyEnter, stopShortcut, + menuLauncher, busyEnter, stopShortcut, continuation, continueGeneration, steerQueue: over?.steerQueue, get placeholder() { return placeholderOf(view.container) }, get inputDisabled() { return textarea.getAttribute('aria-disabled') === 'true' }, } } +it('offers the play action only for an empty, idle composer with an eligible failure', async () => { + const b = bench({ continuation: 2 as SessionSeq }) + expect(b.button.disabled).toBe(false) + expect(b.button.querySelector('svg')).not.toBeNull() + fireEvent.click(b.button) + expect(b.continueGeneration).toHaveBeenCalledExactlyOnceWith(2) + expect(b.sink).not.toHaveBeenCalled() + await act(async () => { b.shell.setDraft('A new instruction') }) + const send = b.view.getByRole('button', { name: '发送消息' }) + fireEvent.click(send) + await vi.waitFor(() => { expect(b.sink).toHaveBeenCalledOnce() }) + expect(b.sink.mock.calls[0]?.[0]).toBe('A new instruction') + expect(b.continueGeneration).toHaveBeenCalledOnce() +}) + +it.each([ + { running: true }, { attachments: [{ kind: 'image' as const, id: 'image' as DraftAttachmentId, file: new File([], 'x.png'), previewUrl: 'blob:test' }] }, + { blocked: { reason: 'Select a model' } }, { subagent: CONTINUABLE_CHILD }, { deniedEndpoints: ['session/prompt'] }, +])('keeps the continue action unavailable with %j', (options) => { + const b = bench({ ...options, continuation: 2 as SessionSeq }) + expect(b.view.queryByRole('button', { name: '继续生成' })).toBeNull() +}) + function attachmentOwner(slotCalls: readonly { key: string; owner: unknown }[]): ComposerAttachmentsOwnerProps { for (let i = slotCalls.length - 1; i >= 0; i -= 1) { const call = slotCalls[i] diff --git a/packages/client/ui-conversation/tests/input-matrix.client.spec.tsx b/packages/client/ui-conversation/tests/input-matrix.client.spec.tsx index 6a18cede9a..524a9af703 100644 --- a/packages/client/ui-conversation/tests/input-matrix.client.spec.tsx +++ b/packages/client/ui-conversation/tests/input-matrix.client.spec.tsx @@ -84,6 +84,8 @@ function mountBar(shell: SessionInputShell, over?: { running?: boolean; disabled })), toggleCommandMenu: vi.fn(), useBusyEnter: bindSnapshotSelector(createSnapshotStore<'queue' | 'steer'>('queue')), + continueGeneration: undefined, + useContinuation: bindSnapshotSelector(createSnapshotStore(undefined)), useStopShortcut: bindSnapshotSelector(createSnapshotStore([])), useNotices: bindSnapshotSelector(shell.notices), useLexicon: bindSnapshotSelector(shell.lexicon), diff --git a/packages/client/ui-conversation/tests/input-scenarios.client.spec.tsx b/packages/client/ui-conversation/tests/input-scenarios.client.spec.tsx index 380097b5d1..ab9c460af8 100644 --- a/packages/client/ui-conversation/tests/input-scenarios.client.spec.tsx +++ b/packages/client/ui-conversation/tests/input-scenarios.client.spec.tsx @@ -200,6 +200,8 @@ async function scopedBench(register?: (inputTriggers: InputTriggerService) => vo useMenuLauncher: bindSnapshotSelector(controller.launcher), renderSlot: (() => null) as InputBarProps['renderSlot'], stop: vi.fn(), + continueGeneration: undefined, + useContinuation: bindSnapshotSelector(createSnapshotStore(undefined)), useStopShortcut: bindSnapshotSelector(createSnapshotStore([])), t: makeTranslate(zh, commonZh), variant: 'composer', diff --git a/packages/client/ui-conversation/tests/skeleton.client.spec.tsx b/packages/client/ui-conversation/tests/skeleton.client.spec.tsx index c061935f89..421da563ff 100644 --- a/packages/client/ui-conversation/tests/skeleton.client.spec.tsx +++ b/packages/client/ui-conversation/tests/skeleton.client.spec.tsx @@ -309,6 +309,8 @@ function mount( resolveDraftAttachments={() => []} toggleCommandMenu={vi.fn()} useBusyEnter={bindSnapshotSelector(createSnapshotStore<'queue' | 'steer'>('queue'))} + continueGeneration={undefined} + useContinuation={bindSnapshotSelector(createSnapshotStore(undefined))} useStopShortcut={bindSnapshotSelector(createSnapshotStore([]))} useNotices={bindSnapshotSelector(wiring.notices)} useLexicon={bindSnapshotSelector(wiring.lexicon)} diff --git a/snapshots/web/live-interactions/continued.expected.md b/snapshots/web/live-interactions/continued.expected.md new file mode 100644 index 0000000000..ab51db50fc --- /dev/null +++ b/snapshots/web/live-interactions/continued.expected.md @@ -0,0 +1,39 @@ +- banner: + - navigation "Session hierarchy": Reply with a one-sentence description + - text: Standard mode + - button "More actions" + - button "Open right sidebar" + - tablist: + - tab "Chat" [selected] + - tab "Trajectory" + - button "More conversation views" +- navigation "Turn navigation": + - button "Jump to turn 1" + - button "Jump to turn 2" +- text: Reply with a one-sentence description of event sourcing, then stop. {{clock}} +- button "Copy" +- status: Failed +- button "Failed" [disabled] [expanded] +- group: + - status: Retried model request (2/2) · {{duration}} +- status: + - text: This turn failedupstream 503 + - code: SERVER +- text: continue {{clock}} +- button "Copy" +- status: Completed +- button "Completed in {{duration}}" +- paragraph: Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures. +- button "Copy" +- button "Good response" +- button "Bad response" +- button "Branch into a new conversation" +- text: {{clock}} +- textbox "Message or run a task, / commands, @ files or sessions" +- button "Add files or run commands" +- 'button "Access mode, current: Workspace Write"': Workspace Write +- button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash +- button "Send message" [disabled] +- button "2 turns 2 steps · {{throughput}} tok/s": 2 turns 2 steps{{throughput}} tok/s +- button "7.9K tok · Cache hit 99%": 7.9K tokCache hit 99% +- button "6% of context used": 6% diff --git a/snapshots/web/live-interactions/retry-exhausted.expected.md b/snapshots/web/live-interactions/retry-exhausted.expected.md index 91ca93cf5e..a99b5a3cbb 100644 --- a/snapshots/web/live-interactions/retry-exhausted.expected.md +++ b/snapshots/web/live-interactions/retry-exhausted.expected.md @@ -20,5 +20,5 @@ - button "Add files or run commands" - 'button "Access mode, current: Workspace Write"': Workspace Write - button "Select model, current DeepSeek-V4-Flash": DeepSeek-V4-Flash -- button "Send message" [disabled] +- button "Continue generating" - text: 1 turns 1 steps From a8b1a9b4a8ccac657aa77c08cab5ad9c7d9a18ee Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Mon, 26 Oct 2026 18:14:51 +0800 Subject: [PATCH 02/65] fix(web): balance continue icon with composer controls --- apps/web/tests/live-interactions.e2e.ts | 8 +++++--- .../src/client/skeleton/InputBar.module.css | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/web/tests/live-interactions.e2e.ts b/apps/web/tests/live-interactions.e2e.ts index 0a6c78c8fc..0e8d7a04f0 100644 --- a/apps/web/tests/live-interactions.e2e.ts +++ b/apps/web/tests/live-interactions.e2e.ts @@ -454,9 +454,11 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => { await input.fill('') await resume.waitFor() await page.setViewportSize({ width: 390, height: 844 }) - const bounds = await resume.boundingBox() - expect(bounds).not.toBeNull() - expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(390) + // ResizeObserver collapses the model label after the viewport changes. + await expect.poll(async () => { + const bounds = await resume.boundingBox() + return bounds !== null && bounds.x + bounds.width <= 390 + }).toBe(true) expect(await resume.isEnabled()).toBe(true) const continued = scaffold!.whenTurnSettled() await resume.click() diff --git a/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css b/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css index 26203b9d11..a2dcaca5cc 100644 --- a/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css +++ b/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css @@ -407,9 +407,9 @@ transform: translateY(-2px); } -/* The triangle fills half its canvas; match the 14px arrow without widening the grid track. */ +/* Solid artwork uses a smaller footprint than the thin send arrow. */ .continueIcon { - transform: scale(1.75); + transform: scale(1.5); } @media (hover: hover) { From 38e5684d5c4b61502ca7f90fd3e08a7e0e3577ca Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Mon, 26 Oct 2026 19:59:54 +0800 Subject: [PATCH 03/65] fix(web): project continuation eligibility from complete session history --- ...6-manual-generation-continuation.i18n.yaml | 22 +++ ...26-10-26-manual-generation-continuation.md | 33 +++++ ...10-26-manual-generation-continuation.zh.md | 33 +++++ apps/web/tests/live-interactions.e2e.ts | 11 ++ docs/config-catalog.md | 2 +- docs/config-catalog.zh.md | 2 +- .../guide.i18n.yaml | 16 ++ .../guide.md | 25 ++++ .../guide.zh.md | 25 ++++ .../client/ui-conversation/README.i18n.yaml | 27 ++-- packages/client/ui-conversation/README.md | 17 ++- packages/client/ui-conversation/README.zh.md | 17 ++- packages/client/ui-conversation/package.json | 9 +- .../src/client/contract/slots.ts | 2 +- .../ui-conversation/src/client/index.ts | 1 + .../src/client/input/continuation.ts | 55 ------- .../ui-conversation/src/client/input/hub.ts | 21 ++- .../ui-conversation/src/client/locales.ts | 1 + .../src/client/skeleton/InputBar.tsx | 6 +- .../ui-conversation/src/continuation-types.ts | 9 ++ .../ui-conversation/src/host/continuation.ts | 55 +++++++ packages/client/ui-conversation/src/index.ts | 13 +- .../tests/apply-inject.client.spec.tsx | 140 +++++++++++++++++- .../ui-conversation/tests/config.host.spec.ts | 32 +++- ...ture.client.ts => continuation-fixture.ts} | 4 +- .../tests/continuation.client.spec.ts | 81 ---------- .../tests/continuation.host.spec.ts | 139 +++++++++++++++++ .../ui-conversation/tsconfig.client.json | 3 +- .../client/ui-conversation/tsconfig.host.json | 13 +- pnpm-lock.yaml | 15 +- .../verify-package-readme-model-experience.ts | 1 - .../live-interactions/continued.expected.md | 2 +- 32 files changed, 646 insertions(+), 186 deletions(-) create mode 100644 .agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.i18n.yaml create mode 100644 .agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.md create mode 100644 .agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.zh.md create mode 100644 docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.i18n.yaml create mode 100644 docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.md create mode 100644 docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.zh.md delete mode 100644 packages/client/ui-conversation/src/client/input/continuation.ts create mode 100644 packages/client/ui-conversation/src/continuation-types.ts create mode 100644 packages/client/ui-conversation/src/host/continuation.ts rename packages/client/ui-conversation/tests/{continuation-fixture.client.ts => continuation-fixture.ts} (92%) delete mode 100644 packages/client/ui-conversation/tests/continuation.client.spec.ts create mode 100644 packages/client/ui-conversation/tests/continuation.host.spec.ts diff --git a/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.i18n.yaml b/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.i18n.yaml new file mode 100644 index 0000000000..320476d704 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.i18n.yaml @@ -0,0 +1,22 @@ +# Bilingual-pair consistency record for 2026-10-26-manual-generation-continuation.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.md +/agent-note-manual-generation-continuation: + en: 327aaa1cdc63b1f5 + zh: 1147b4a23e51772c +/agent-note-manual-generation-continuation/problem: + en: 027e01e4586da32f + zh: dce2c6858ef709c1 +/agent-note-manual-generation-continuation/decision: + en: dc5f066e9c0bbb23 + zh: 6e54f59a34d8fca5 +/agent-note-manual-generation-continuation/alternatives-considered: + en: b32c1050bc0ca9de + zh: 5e950f842f944818 +/agent-note-manual-generation-continuation/consequences: + en: c825acfaae8f049d + zh: e4768dab9d8edde3 +/agent-note-manual-generation-continuation/verification: + en: bd59f8845a358dec + zh: cfcd30ca521e0111 diff --git a/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.md b/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.md new file mode 100644 index 0000000000..8060f1bde4 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.md @@ -0,0 +1,33 @@ +# Agent Note: Manual generation continuation + +Status: implemented + +English | [中文](2026-10-26-manual-generation-continuation.zh.md) + +## Problem + +Users need another turn after a model request fails. A browser page can omit the failed turn's start; streaming must not repeatedly copy loaded history. + +## Decision + +The play action submits localized ordinary user text through the existing composer, retaining admission, logging, failure drafts, and telemetry. No resume RPC, AgentLoop change, or Session event format is introduced. + +The fixed-size Host projection `conversationContinuation` recovers from complete Session history. Eligibility requires admitted ordinary input and a failed attempt, independently of the browser's 500-message page. + +The six-code manual allowlist remains independent of [automatic retry policy](../../../../packages/llm/llm-retry/README.md). Pending [injected context](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md) in `next-step` accompanies the new waking message; pending ordinary user submissions prevent duplicates. + +## Alternatives considered + +**Window scanning or entries caching.** Each revision can materialize a fresh window; caching cannot recover unloaded facts. + +**Use provider retryableCodes.** Deployment retry policies could expose authentication or other unsuitable errors as UI actions. + +**Add a resume RPC.** Ordinary submission already owns wakeup, admission, error reporting, and history. + +## Consequences + +Host activation requires `sessionProjections`; custom compositions follow the [upgrade guide](../../../../docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.md). Fixed-size state avoids Client history scans without introducing a second submission lifecycle. + +## Verification + +Tests cover checkpoint recovery, long-turn paging, injected context, refusal and retained drafts, and zero Client event-window reads. The recorded Web scenario exercises failure followed by continuation. diff --git a/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.zh.md b/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.zh.md new file mode 100644 index 0000000000..c7c0d9ee13 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.zh.md @@ -0,0 +1,33 @@ +# Agent Note: 手动继续生成 + +Status: implemented + +[English](2026-10-26-manual-generation-continuation.md) | 中文 + +## 问题 + +模型请求失败后,用户需要发起下一轮。浏览器分页可能不含失败轮次的起点;流式输出也不能反复复制已加载历史。 + +## 决策 + +播放按钮通过现有 composer 提交本地化的普通用户文本,沿用准入、日志、失败草稿和遥测语义。不引入 resume RPC,不修改 AgentLoop 或 Session 事件格式。 + +固定大小的 Host 投影 `conversationContinuation` 从完整 Session 历史恢复。资格要求已准入的普通用户输入和失败尝试,不依赖浏览器的 500 条消息分页。 + +六类手动继续白名单独立于[自动重试策略](../../../../packages/llm/llm-retry/README.zh.md)。`next-step` 中待处理的[注入上下文](../architecture/2026-07-24-separate-context-injection-from-turn-execution.zh.md) 随新的唤醒消息进入请求;待提交的普通用户消息仍阻止重复提交。 + +## 考虑过的替代方案 + +**窗口扫描或 entries 缓存。** 每个 revision 都可能物化新窗口;缓存无法恢复尚未加载的事实。 + +**采用提供方的 retryableCodes。** 部署重试策略可能把认证等不适合的错误也变成界面动作。 + +**新增 resume RPC。** 普通提交已经拥有唤醒、准入、错误报告和历史。 + +## 影响 + +Host 激活需要 `sessionProjections`;自定义组合按[升级指南](../../../../docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.zh.md) 调整。固定大小的状态避免 Client 历史扫描,也无需第二套提交生命周期。 + +## 验证 + +测试覆盖检查点恢复、长轮次分页、注入上下文、拒绝与草稿保留,以及不读取 Client 事件窗口。已录制的 Web 场景验证失败后的继续流程。 diff --git a/apps/web/tests/live-interactions.e2e.ts b/apps/web/tests/live-interactions.e2e.ts index 0e8d7a04f0..d1e90d0080 100644 --- a/apps/web/tests/live-interactions.e2e.ts +++ b/apps/web/tests/live-interactions.e2e.ts @@ -448,6 +448,16 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => { const resume = page.getByRole('button', { name: 'Continue generating', exact: true }) await resume.waitFor() const input = page.locator('[data-composer-input]').first() + await input.fill('/plan ') + await input.press('Enter') + const plan = page.getByRole('button', { name: 'Plan mode on, press to turn off', exact: true }) + await plan.waitFor() + await expect.poll(() => input.textContent()).toBe('') + await plan.click() + await plan.waitFor({ state: 'hidden' }) + await resume.waitFor() + expect(await resume.isEnabled()).toBe(true) + expect(turnEndReasons(sessionEvents)).toEqual(['error']) await input.fill('A different instruction') expect(await resume.count()).toBe(0) expect(await page.getByRole('button', { name: 'Send message', exact: true }).isEnabled()).toBe(true) @@ -464,6 +474,7 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => { await resume.click() await continued expect(turnEndReasons(sessionEvents)).toEqual(['error', 'completed']) + expect(sessionEvents.some(event => event.type === 'user/message' && event.data.source.kind === 'plan-mode')).toBe(true) const userMessages = sessionEvents.flatMap(event => event.type === 'user/message' && event.data.source.kind === 'user' ? [event.data.content] : []) expect(userMessages).toEqual([[{ type: 'text', text: PROMPT }], [{ type: 'text', text: 'continue' }]]) diff --git a/docs/config-catalog.md b/docs/config-catalog.md index be0d89caf2..43726adfb5 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -4713,7 +4713,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co | `@deepseek-ai/dsh-client-ui-brand-official` | — | [`packages/client/ui-brand-official/src/index.ts`](../packages/client/ui-brand-official/src/index.ts) | | `@deepseek-ai/dsh-client-ui-chat` | — | [`packages/client/ui-chat/src/index.ts`](../packages/client/ui-chat/src/index.ts) | | `@deepseek-ai/dsh-client-ui-commands` | — | [`packages/client/ui-commands/src/index.ts`](../packages/client/ui-commands/src/index.ts) | -| `@deepseek-ai/dsh-client-ui-conversation` | — | [`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts) | +| `@deepseek-ai/dsh-client-ui-conversation` | `sessionProjections` | [`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts) | | `@deepseek-ai/dsh-client-ui-cordis` | — | [`packages/extensions/ui-cordis/src/index.ts`](../packages/extensions/ui-cordis/src/index.ts) | | `@deepseek-ai/dsh-client-ui-creation-recommendations` | — | [`packages/experimental/client-ui-creation-recommendations/src/index.ts`](../packages/experimental/client-ui-creation-recommendations/src/index.ts) | | `@deepseek-ai/dsh-client-ui-deepseek-model-label-1017` | — | [`packages/client/ui-deepseek-model-label-1017/src/index.ts`](../packages/client/ui-deepseek-model-label-1017/src/index.ts) | diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 23165f5f85..470c40d5b0 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -4715,7 +4715,7 @@ export interface Config { | `@deepseek-ai/dsh-client-ui-brand-official` | — | [`packages/client/ui-brand-official/src/index.ts`](../packages/client/ui-brand-official/src/index.ts) | | `@deepseek-ai/dsh-client-ui-chat` | — | [`packages/client/ui-chat/src/index.ts`](../packages/client/ui-chat/src/index.ts) | | `@deepseek-ai/dsh-client-ui-commands` | — | [`packages/client/ui-commands/src/index.ts`](../packages/client/ui-commands/src/index.ts) | -| `@deepseek-ai/dsh-client-ui-conversation` | — | [`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts) | +| `@deepseek-ai/dsh-client-ui-conversation` | `sessionProjections` | [`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts) | | `@deepseek-ai/dsh-client-ui-cordis` | — | [`packages/extensions/ui-cordis/src/index.ts`](../packages/extensions/ui-cordis/src/index.ts) | | `@deepseek-ai/dsh-client-ui-creation-recommendations` | — | [`packages/experimental/client-ui-creation-recommendations/src/index.ts`](../packages/experimental/client-ui-creation-recommendations/src/index.ts) | | `@deepseek-ai/dsh-client-ui-deepseek-model-label-1017` | — | [`packages/client/ui-deepseek-model-label-1017/src/index.ts`](../packages/client/ui-deepseek-model-label-1017/src/index.ts) | diff --git a/docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.i18n.yaml new file mode 100644 index 0000000000..50094c2b2c --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.md +/: + en: cd293f657bb1ab7a + zh: 7d942448b488e22c +/provide-session-projections-for-conversation: + en: 6ec06cc57348d333 + zh: 86665b791e3b2e02 +/provide-session-projections-for-conversation/change: + en: b0048aafcb91d8a5 + zh: c913215a68c5ff5b +/provide-session-projections-for-conversation/migration: + en: e06d674bafc37abe + zh: b68409bfbc936d19 diff --git a/docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.md b/docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.md new file mode 100644 index 0000000000..52fa806c20 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.md @@ -0,0 +1,25 @@ +--- +kind: upgrade-guide +description: "Custom Conversation compositions must provide sessionProjections to activate the Host plugin." +--- +# Provide Session projections for Conversation + +English | [中文](guide.zh.md) + +## Change + +The Host entry of `@deepseek-ai/dsh-client-ui-conversation` requires the `sessionProjections` service to publish manual continuation eligibility. Custom compositions that mounted Conversation without a projection provider must add one. The shipped Web and Desktop compositions already provide it. Session logs do not require migration. + +## Migration + +1. Check the custom profile's bundles and `cordis.patch.yml` for an existing `@deepseek-ai/dsh-session-projection` entry. Keep that provider when present. +2. If the composition has none, add this row to the profile's `cordis.patch.yml`: + + ```yaml + - id: session-projection + name: '@deepseek-ai/dsh-session-projection' + ``` + +3. Restart the profile and verify that the Conversation Host entry activates. After a supported model-request failure, an empty, idle ordinary composer must offer the play action. + +See the [Conversation reference](../../../../packages/client/ui-conversation/README.md#shell-and-standard-props) for the supported failures and submission behavior. diff --git a/docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.zh.md new file mode 100644 index 0000000000..1277f31bf5 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.zh.md @@ -0,0 +1,25 @@ +--- +kind: upgrade-guide +description: "自定义 Conversation 组合必须提供 sessionProjections 才能激活 Host 插件。" +--- +# 为 Conversation 提供 Session 投影 + +[English](guide.md) | 中文 + +## 变更 + +`@deepseek-ai/dsh-client-ui-conversation` 的 Host 入口需要 `sessionProjections` 服务来发布手动继续资格。挂载 Conversation 却没有投影提供方的自定义组合必须补上提供方。随产品提供的 Web 和 Desktop 组合已经提供它。Session 日志不需要迁移。 + +## 迁移 + +1. 检查自定义 profile 的 bundle 和 `cordis.patch.yml` 中是否已有 `@deepseek-ai/dsh-session-projection` entry;存在时保留该提供方。 +2. 如果组合中没有该提供方,在 profile 的 `cordis.patch.yml` 中添加以下条目: + + ```yaml + - id: session-projection + name: '@deepseek-ai/dsh-session-projection' + ``` + +3. 重启 profile 并确认 Conversation Host 入口已激活。支持的模型请求失败后,空草稿且空闲的普通 composer 应提供播放按钮。 + +支持的失败类型和提交行为见 [Conversation 参考](../../../../packages/client/ui-conversation/README.zh.md#shell-and-standard-props)。 diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index ac94283c97..2b4fd0c5db 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/README.i18n.yaml @@ -21,20 +21,29 @@ en: cc4dafd1aae02468 zh: be6b67724b7ac4a3 /deepseek-ai-dsh-client-ui-conversation/shell-and-standard-props: - en: 483f64e0a313ffaa - zh: 21a243552ac05038 + en: ac170c9895e17840 + zh: c7603d365134ce89 /deepseek-ai-dsh-client-ui-conversation/temporary-composer-entries: en: bb1244310f5c0382 zh: 824b84077432b559 /deepseek-ai-dsh-client-ui-conversation/model-experience: - en: e0a05b73ba3e4318 - zh: dc85ab8433bb4456 -/deepseek-ai-dsh-client-ui-conversation/model-experience/kv-cache-effect: - en: 9f4e0da7c8addf88 - zh: 1a33d9981af75643 + en: 215e7ba838619b7b + zh: a311da3843709f90 +/deepseek-ai-dsh-client-ui-conversation/model-experience/manual-continuation-message: + en: 30f3836663c881ca + zh: ee3e1d1df8603fad +/deepseek-ai-dsh-client-ui-conversation/model-experience/manual-continuation-message/what-the-model-sees: + en: d566958ff2f23584 + zh: f6ebea015ba61872 +/deepseek-ai-dsh-client-ui-conversation/model-experience/manual-continuation-message/token-effect: + en: 0ad06a5163a62c50 + zh: d25b93fcb2783566 +/deepseek-ai-dsh-client-ui-conversation/model-experience/manual-continuation-message/kv-cache-effect: + en: 326e23f5913e3fc0 + zh: 865969505814e0af /deepseek-ai-dsh-client-ui-conversation/known-limitations-and-deferred-work: - en: 38d243630d0e4c38 - zh: a58cdc35b664020b + en: d76daa90b6d6f600 + zh: 37a9f7d402b2552b /deepseek-ai-dsh-client-ui-conversation/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: e88aeb617ffb74ea diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index cfe8144cec..eadcdf2467 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -76,7 +76,9 @@ The composer measures its expanded control groups after size, content, visibilit The context meter follows the [statistics-control radius](../../../docs/ui-radius.md#radius-scale). -An empty, idle ordinary Session composer offers a play action after `RATE_LIMIT`, `SERVER`, `TIMEOUT`, `TRANSPORT`, `EMPTY_RESPONSE`, or `STREAM_CLOSED` ends its latest turn. The failed turn must include admitted user input, a settled model attempt, and a loaded start. Clicking submits a normal user message in the current interface language (`继续` in Chinese, `continue` in English); drafts and attachments keep the Send action. Continued requests retain committed history and use the existing submission lifecycle. +An empty, idle ordinary Session composer offers a play action after `RATE_LIMIT`, `SERVER`, `TIMEOUT`, `TRANSPORT`, `EMPTY_RESPONSE`, or `STREAM_CLOSED` ends its latest turn. The failed turn must include admitted ordinary user input and a settled failed model attempt. Clicking submits a normal user message in the current interface language; drafts and attachments keep the Send action. Continued requests retain committed history and use the existing submission lifecycle. + +The Host requires `sessionProjections` and publishes `conversationContinuation`; eligibility is restored from Session history independently of the browser's loaded history pages. The composer uses the shared [text-input focus policy](../ui-primitives/README.md#use-this-package) on mount, Session switching, and unlock. Identified phones and tablets require tapping the input; desktop devices retain automatic focus. On these mobile devices, a submission that empties the focused draft moves focus to the non-text composer card, dismissing the keyboard while retaining its navigation position. Rejected submissions, retained newer draft text, and another control already holding focus preserve focus. The [Lexical patch](../../../patches/lexical@0.49.0.patch) keeps caret scrolling in the same coordinate system as DOM geometry when a mobile keyboard pans the page. @@ -187,11 +189,19 @@ The selector must be a pure function of the owner currency. Its non-null return ## Model Experience -None, as this package renders browser state and sends user-admitted inputs through Session Controller APIs without constructing model requests. +### Manual continuation message + +#### What the model sees + +The play action submits one ordinary user message using the interface language selected at the click: Chinese `继续`, English/French/Portuguese `continue`, Japanese `続けて`, Spanish `continúa`, German `weiter`, or Russian `продолжай`. The admitted message is stored in the Session log and included in subsequent model history; the action does not reconstruct text from the failed attempt. + +#### Token effect + +Each continuation admitted into model history adds its short localized text and the ordinary user-message overhead. The exact token count depends on the model tokenizer. A submission refused or failed before admission adds no continuation text to model history. #### KV Cache effect -None; Conversation assembly and browser input state do not alter provider-side prompt caching. +The continuation message extends retained history without replacing earlier messages or prompt sections. It preserves an already-reusable prefix; provider cache availability, reuse policy, and eviction remain outside this package's control. ## Known Limitations and Deferred Work @@ -199,6 +209,7 @@ None; Conversation assembly and browser input state do not alter provider-side p - **Only registered targets can render** — the shell deliberately has no implicit fallback target beyond the registered `chat` preference. - **Factory occurrences inherit their render-position Session** — `conversation.content` does not accept an independently addressed Session; that requires a separate Session-provider capability. +- **Manual continuation uses a fixed product allowlist** — the supported failure codes are independent of the provider's configurable `retryableCodes`; changing automatic retry policy does not expand or reduce this action. `STREAM_CLOSED` permits an explicit new user turn even when the default policy does not retry it. That code can also identify a protocol violation, so another request is not guaranteed to recover. diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index f8a1872c79..3573e9c7b4 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -76,7 +76,9 @@ Queue 预览启用空白折叠和文档边缘 trim,保留 emoji,并使用 CS 上下文占用率入口遵循[统计控件圆角规则](../../../docs/ui-radius.zh.md#radius-scale)。 -普通 Session 的输入框在空草稿且空闲时,若最新一轮以 `RATE_LIMIT`、`SERVER`、`TIMEOUT`、`TRANSPORT`、`EMPTY_RESPONSE` 或 `STREAM_CLOSED` 结束,会提供播放图标按钮。失败轮次必须包含已提交的用户输入和已落盘的模型请求尝试,且起始事件已加载。点击后按当前界面语言提交一条普通用户消息(中文为“继续”,英文为“continue”);存在草稿或附件时保留发送按钮。继续请求保留已提交历史,并沿用现有提交流程。 +普通 Session 的输入框在空草稿且空闲时,若最新一轮以 `RATE_LIMIT`、`SERVER`、`TIMEOUT`、`TRANSPORT`、`EMPTY_RESPONSE` 或 `STREAM_CLOSED` 结束,会提供播放图标按钮。失败轮次必须包含已进入模型历史的普通用户输入和已落盘的失败模型请求尝试。点击后按当前界面语言提交一条普通用户消息;存在草稿或附件时保留发送按钮。继续请求保留已提交历史,并沿用现有提交流程。 + +Host 需要 `sessionProjections`,并发布 `conversationContinuation`;继续资格从 Session 历史恢复,不受浏览器已加载历史分页的影响。 composer 在挂载、切换 Session 和解除锁定时使用共享的[文字输入框聚焦策略](../ui-primitives/README.zh.md#use-this-package)。识别出的手机和平板需要点击输入框;桌面设备保留自动聚焦。在这些移动设备上,提交清空正在聚焦的草稿后,焦点移到不接受文字输入的 composer 卡片,收起键盘并保留键盘导航位置。被拒绝的提交、保留的新草稿文字,以及已经聚焦的其他控件均保留原有焦点。移动端键盘平移页面时,[Lexical 补丁](../../../patches/lexical@0.49.0.patch)让光标滚动计算与 DOM 几何使用同一坐标系。 @@ -187,11 +189,19 @@ selector 必须是 owner currency 的纯函数。非 null 返回值作为 `match ## 模型体验 -无,因为本包渲染浏览器状态,并通过 Session Controller API 发送用户确认提交的输入,而不构造模型请求。 +### 手动继续消息 + +#### 模型看到什么 + +播放按钮按点击时选中的界面语言提交一条普通用户消息:中文为 `继续`,英语、法语和葡萄牙语为 `continue`,日语为 `続けて`,西班牙语为 `continúa`,德语为 `weiter`,俄语为 `продолжай`。已准入的消息写入 Session 日志并进入后续模型历史;该操作不从失败尝试中重建文本。 + +#### Token 影响 + +每条进入模型历史的继续消息增加其本地化短文本和普通用户消息的封装开销。准确 token 数取决于模型 tokenizer。在准入前被拒绝或失败的提交不会向模型历史添加继续文本。 #### KV Cache 影响 -无;Conversation 组装和浏览器输入状态不会改变提供方侧的 prompt cache。 +继续消息在保留历史后追加,不替换更早的消息或 prompt section。它保留原本可复用的前缀;提供方缓存的可用性、复用策略和淘汰规则不由本包控制。 ## 已知限制与暂缓事项 @@ -199,6 +209,7 @@ selector 必须是 owner currency 的纯函数。非 null 返回值作为 `match - **只有已注册 target 可以渲染**——除已注册的 `chat` 偏好外,shell 刻意不提供隐式 fallback target。 - **Factory occurrence 继承渲染位置的 Session**——`conversation.content` 不接受独立寻址的 Session;该能力需要单独的 Session provider。 +- **手动继续使用固定产品白名单**——支持的失败码独立于提供方可配置的 `retryableCodes`;修改自动重试策略不会扩展或缩小此操作的范围。即使默认策略不重试 `STREAM_CLOSED`,用户仍可明确发起新轮次。该错误码也可能表示协议违规,因此再次请求不保证能够恢复。 diff --git a/packages/client/ui-conversation/package.json b/packages/client/ui-conversation/package.json index 050319168e..c4b2317d94 100644 --- a/packages/client/ui-conversation/package.json +++ b/packages/client/ui-conversation/package.json @@ -47,7 +47,9 @@ }, "license": "MIT", "dependencies": { - "@deepseek-ai/schemastery": "workspace:~" + "@deepseek-ai/schemastery": "workspace:~", + "zod": "4.4.3", + "@deepseek-ai/dsh-brand": "workspace:*" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~" @@ -58,7 +60,6 @@ "@deepseek-ai/dsh-api-session-controller": "workspace:*", "@deepseek-ai/dsh-api-workspace-controller": "workspace:*", "@deepseek-ai/dsh-attachment": "workspace:*", - "@deepseek-ai/dsh-brand": "workspace:*", "@deepseek-ai/dsh-client-locale": "workspace:*", "@deepseek-ai/dsh-client-file-upload": "workspace:*", "@deepseek-ai/dsh-client-store": "workspace:*", @@ -86,7 +87,6 @@ "react": "^18.2.0", "@lexical/headless": "^0.49.0", "react-dom": "^18.2.0", - "zod": "4.4.3", "lexical": "^0.49.0", "clsx": "^2.0.0", "@lexical/utils": "^0.49.0", @@ -97,7 +97,8 @@ "@deepseek-ai/dsh-settings": "workspace:*", "@deepseek-ai/dsh-client-shortcuts": "workspace:*", "@deepseek-ai/dsh-client-product-analytics": "workspace:*", - "@deepseek-ai/dsh-message-quotes": "workspace:*" + "@deepseek-ai/dsh-message-quotes": "workspace:*", + "@deepseek-ai/dsh-session-projection": "workspace:*" }, "files": [ "locale/*.json", diff --git a/packages/client/ui-conversation/src/client/contract/slots.ts b/packages/client/ui-conversation/src/client/contract/slots.ts index c190443fb7..068fb0026e 100644 --- a/packages/client/ui-conversation/src/client/contract/slots.ts +++ b/packages/client/ui-conversation/src/client/contract/slots.ts @@ -535,7 +535,7 @@ export interface ComposerBarInjected { */ continueGeneration: ((ending: SessionSeq) => void) | undefined hooks: { - /** Latest transient failure with admitted user input and a loaded turn start. */ + /** Host-projected latest eligible failure, excluding pending human steering. */ continuation: ObservableSnapshot /** Readable sequence while the fixed Stop command is registered. */ stopShortcut: ObservableSnapshot diff --git a/packages/client/ui-conversation/src/client/index.ts b/packages/client/ui-conversation/src/client/index.ts index d4f06accc5..65d7edc6b2 100644 --- a/packages/client/ui-conversation/src/client/index.ts +++ b/packages/client/ui-conversation/src/client/index.ts @@ -1,5 +1,6 @@ /** Browser Conversation assemble core, React adapter, shell, input, and text projection plugin. */ export type {} from './user-text/session-reference-rule.ts' +export type {} from '../continuation-types.ts' /** Declaration-merged node payloads; IDs also identify the corresponding keyed renderer. */ export interface UserTextDataMap { diff --git a/packages/client/ui-conversation/src/client/input/continuation.ts b/packages/client/ui-conversation/src/client/input/continuation.ts deleted file mode 100644 index 261629c084..0000000000 --- a/packages/client/ui-conversation/src/client/input/continuation.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** Manual continuation of settled transient model-request failures. */ -import type { SessionEventSource } from '@deepseek-ai/dsh-api-session-controller/client' -import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store' -import type { SessionSeq } from '@deepseek-ai/dsh-session/types' - -const CONTINUABLE_FAILURES: ReadonlySet = new Set([ - 'RATE_LIMIT', 'SERVER', 'TIMEOUT', 'TRANSPORT', 'EMPTY_RESPONSE', 'STREAM_CLOSED', -]) - -/** - * Derive the latest eligible failure from the loaded history. A loaded turn - * start, admitted user message, and settled failed request are required; - * preparation failures may have consumed input without admitting it. - * @param feed - existing Session event source, including replacement and paging. - * @returns a stable source of the eligible turn/end sequence, absent otherwise. - */ -export function createContinuationSource(feed: SessionEventSource): ObservableSnapshot { - return { - getSnapshot: () => { - const { entries } = feed.getSnapshot() - let ending: SessionSeq | undefined - let admitted = false - let attempted = false - for (let index = entries.length - 1; index >= 0; index--) { - // oxlint-disable-next-line typescript/no-non-null-assertion -- index is bounded by this immutable window's length. - const entry = entries[index]! - if (entry.type === 'transient') return undefined - const event = entry.event - switch (event.type) { - case 'turn/end': - if (ending !== undefined || event.data.reason.kind !== 'error' - || !CONTINUABLE_FAILURES.has(event.data.reason.error.code)) return undefined - ending = event.seq - break - case 'turn/start': - return admitted && attempted ? ending : undefined - case 'assistant/attempt': - attempted = true - break - case 'step/start': - case 'assistant/message': - if (!attempted) return undefined - break - case 'user/message': - if (ending !== undefined && event.surfaceOp === 'append' && event.data.source.kind === 'user') admitted = true - break - default: - break - } - } - return undefined - }, - subscribe: listener => feed.subscribe(listener), - } -} diff --git a/packages/client/ui-conversation/src/client/input/hub.ts b/packages/client/ui-conversation/src/client/input/hub.ts index 3c247d322e..36ea1c6b4b 100644 --- a/packages/client/ui-conversation/src/client/input/hub.ts +++ b/packages/client/ui-conversation/src/client/input/hub.ts @@ -25,7 +25,7 @@ import type { ComposerBlocks } from '../contract/composer-blocks.ts' import type { InputSubmitMode } from '../contract/composer-submission.ts' import type { PopupDismissFace } from './facade.ts' import { SessionInputShell } from './facade.ts' -import { createContinuationSource } from './continuation.ts' +import type {} from '../../continuation-types.ts' import { reportMessageSubmission } from './submission-analytics.ts' import { readConversationDraft } from '../stores.ts' @@ -210,7 +210,7 @@ export class InputHub implements SessionInputResolver { } /** - * Read manual-continuation eligibility from the retained Session history. + * Read Host-projected eligibility while excluding pending human steering. * @param id - retained ordinary Session identity. * @returns the identity-stable eligibility source. */ @@ -219,7 +219,20 @@ export class InputHub implements SessionInputResolver { if (binding === undefined) throw new Error(`conversation.input: session "${id}" resolved no binding`) let source = this.continuations.get(binding) if (source === undefined) { - source = createContinuationSource(binding.eventSource) + const ending = binding.session.projections.faceOf('conversationContinuation') + const inbox = binding.session.projections.faceOf('inbox') + source = { + getSnapshot: () => { + const pending = inbox.getSnapshot() as InboxState | undefined + return pending?.['next-step'].some(message => message.source.kind === 'user') + ? undefined : (ending.getSnapshot() as SessionSeq | null | undefined) ?? undefined + }, + subscribe: (listener) => { + const unsubscribeEnding = ending.subscribe(listener) + const unsubscribeInbox = inbox.subscribe(listener) + return () => { unsubscribeInbox(); unsubscribeEnding() } + }, + } this.continuations.set(binding, source) } return source @@ -241,7 +254,7 @@ export class InputHub implements SessionInputResolver { if (session.running || session.removed || session.openState !== 'open' || session.subagent !== null || session.pendingSubmissions.length > 0 || input.phase !== 'plain' || input.draft.trim() !== '' || input.attachmentIds.length > 0 - || (inbox?.['next-turn'].length ?? 0) > 0 || (inbox?.['next-step'].length ?? 0) > 0 + || (inbox?.['next-turn'].length ?? 0) > 0 || this.conversation().blocks.storeFor(id).getSnapshot() !== undefined) return shell.setDraft(this.t('input.continueMessage')) shell.submit('queue', 'click') diff --git a/packages/client/ui-conversation/src/client/locales.ts b/packages/client/ui-conversation/src/client/locales.ts index c9dc074e1c..0ace610330 100644 --- a/packages/client/ui-conversation/src/client/locales.ts +++ b/packages/client/ui-conversation/src/client/locales.ts @@ -29,6 +29,7 @@ export const zh = { 'input.stop': '停止生成', 'input.send': '发送消息', 'input.continue': '继续生成', + // Model-visible text, durably logged when admitted as an ordinary user message. 'input.continueMessage': '继续', 'input.sending': '正在发送消息', 'input.send.queue': '排队发送', diff --git a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx index 9d46a571d3..962edd854f 100644 --- a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx @@ -366,8 +366,8 @@ export const InputBar = memo(function InputBar({ // message waits for the Host. const primaryStops = running && subagent === null && !sending && (empty || blocked !== undefined) const primaryContinues = !running && subagent === null && empty && continuation !== undefined - && historyOpen && !submissionPending && (input?.queue.length ?? 0) === 0 - && !disabled && input.phase === 'plain' && continueGeneration !== undefined + && historyOpen && !submissionPending && !disabled && input.queue.length === 0 + && input.phase === 'plain' && continueGeneration !== undefined // Disabled native buttons may omit mouseleave; their tooltip must close from state. const primaryDisabled = primaryStops ? stopAction === undefined : (!primaryContinues && empty) || disabled || machineBusy || uploadsPending @@ -389,7 +389,7 @@ export const InputBar = memo(function InputBar({ return } if (primaryContinues) { - if (!primaryDisabled) continueGeneration(continuation) + continueGeneration(continuation) return } if (keyboard === undefined) return // absent machine: the button is disabled diff --git a/packages/client/ui-conversation/src/continuation-types.ts b/packages/client/ui-conversation/src/continuation-types.ts new file mode 100644 index 0000000000..9acece7469 --- /dev/null +++ b/packages/client/ui-conversation/src/continuation-types.ts @@ -0,0 +1,9 @@ +/** Host-derived eligibility for an explicit generation-continuation message. */ +import type { SessionSeq } from '@deepseek-ai/dsh-session/types' + +declare module '@deepseek-ai/dsh-session-projection/types' { + interface SessionProjectionMap { + /** Eligible latest turn/end, or null while no settled request can be continued. */ + conversationContinuation: SessionSeq | null + } +} diff --git a/packages/client/ui-conversation/src/host/continuation.ts b/packages/client/ui-conversation/src/host/continuation.ts new file mode 100644 index 0000000000..ef2239338e --- /dev/null +++ b/packages/client/ui-conversation/src/host/continuation.ts @@ -0,0 +1,55 @@ +/** Constant-size continuation eligibility over the complete durable Session. */ +import type {} from '../continuation-types.ts' +import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection' +import type { SessionSeq } from '@deepseek-ai/dsh-session/types' +import { brandNumber } from '@deepseek-ai/dsh-brand' +import { z } from 'zod' + +// Explicit manual requests use a fixed product policy, independent of automatic retry configuration. +const CONTINUABLE_FAILURES: ReadonlySet = new Set([ + 'RATE_LIMIT', 'SERVER', 'TIMEOUT', 'TRANSPORT', 'EMPTY_RESPONSE', 'STREAM_CLOSED', +]) + +const endingSchema = z.number().int().nonnegative().transform(brandNumber).nullable() +const stateSchema = z.object({ + admitted: z.boolean(), + failedAttempt: z.boolean(), + ending: endingSchema, +}) + +type ContinuationState = z.infer + +const EMPTY: ContinuationState = { admitted: false, failedAttempt: false, ending: null } + +/** Eligibility requires admitted human input and a failed settlement in the last request step. */ +export const continuationProjectionDefinition = { + key: 'conversationContinuation', + stateVersion: 1, + stateSchema, + init: (): ContinuationState => EMPTY, + apply: (state, event) => { + switch (event.type) { + case 'turn/start': + return EMPTY + case 'step/start': + case 'assistant/message': + return state.failedAttempt ? { ...state, failedAttempt: false } : state + case 'user/message': + return !state.admitted && event.surfaceOp === 'append' && event.data.source.kind === 'user' + ? { ...state, admitted: true } : state + case 'assistant/attempt': + return state.failedAttempt ? state : { ...state, failedAttempt: true } + case 'turn/end': { + const ending = event.data.reason.kind === 'error' && CONTINUABLE_FAILURES.has(event.data.reason.error.code) + && state.admitted && state.failedAttempt ? event.seq : null + return ending === null ? EMPTY : { ...EMPTY, ending } + } + default: + return state + } + }, + wire: { + viewSchema: endingSchema, + view: (state: ContinuationState) => state.ending, + }, +} satisfies ProjectionDefinition<'conversationContinuation', ContinuationState> diff --git a/packages/client/ui-conversation/src/index.ts b/packages/client/ui-conversation/src/index.ts index afb38b5fe0..6c7515a448 100644 --- a/packages/client/ui-conversation/src/index.ts +++ b/packages/client/ui-conversation/src/index.ts @@ -1,5 +1,6 @@ -/** Host registration for browser conversation preferences. */ +/** Host continuation eligibility and browser conversation preferences. */ import type {} from '@deepseek-ai/dsh-settings' +export type {} from './continuation-types.ts' import type { Volatile, Context } from '@deepseek-ai/cordis' import type { BusyEnterBehavior } from './submission-settings.ts' @@ -7,6 +8,7 @@ import z from '@deepseek-ai/schemastery' import { BUSY_ENTER_FIELD } from './submission-settings.ts' import { ConversationSettingsFields } from './submission-settings.ts' +import { continuationProjectionDefinition } from './host/continuation.ts' export { BUSY_ENTER_BEHAVIORS, BUSY_ENTER_FIELD, CONVERSATION_SETTINGS_NAMESPACE, @@ -24,9 +26,14 @@ export const Config = z.object({ [BUSY_ENTER_FIELD]: ConversationSettingsFields[BUSY_ENTER_FIELD].volatile(), }) -/** Host preferences are consumed through the configuration form projection. +/** Required registry for the durable generation-continuation projection. */ +export const inject = ['sessionProjections'] + +/** Register generation-continuation eligibility and browser preferences. * @param ctx Plugin context used for optional settings presentation. + * @returns when continuation state is registered and available to Session carriers. */ -export function apply(ctx: Context): void { +export async function apply(ctx: Context): Promise { ctx.inject(['settings'], (child) => { child.effect(() => child.settings.configure({ auto: false }, ctx.fiber)) }) + await ctx.sessionProjections.register(continuationProjectionDefinition) } diff --git a/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx b/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx index dc69dad5bc..8f2b967e06 100644 --- a/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx +++ b/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx @@ -6,6 +6,7 @@ import { registerComposerKeymap } from '../src/client/input/editor/keymap.ts' import { createSnapshotStore } from '@deepseek-ai/dsh-client-store' import { describe, expect, it, onTestFinished, vi } from 'vitest' import type { CommandContribution, CommandUiContract } from '@deepseek-ai/dsh-client-ui-commands/client' +import type { SessionRequestId } from '@deepseek-ai/dsh-api-session-controller/types' import type { ISession, SessionReference } from '@deepseek-ai/dsh-api-session-controller/client' import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client' import * as messageQuotes from '@deepseek-ai/dsh-message-quotes/client' @@ -24,7 +25,11 @@ import { createConversationStore } from '../src/client/stores.ts' import type { SessionInputShell } from '../src/client/input/facade.ts' import { RemoteError } from '@deepseek-ai/dsh-client-test-runtime' import { SessionSeq } from '@deepseek-ai/dsh-session/types' -import { continuationTurn } from './continuation-fixture.client.ts' +import { createUserMessage, LlmAttemptId } from '@deepseek-ai/dsh-llm' +import { MutableSessionEventSource } from '@deepseek-ai/dsh-api-session-controller/client' +import type { SessionEventWindow } from '@deepseek-ai/dsh-api-session-controller/client' +import type { InboxWireState } from '@deepseek-ai/dsh-agent/types' +import { continuationTurn } from './continuation-fixture.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { interface SlotMap { @@ -164,6 +169,7 @@ describe('Conversation inject API', () => { const b = await bench() onTestFinished(() => b.runtime.dispose()) await b.runtime.sessions.replaceEvents(ROOT, continuationTurn({ kind: 'error', error: { code: 'SERVER', message: 'busy' } })) + await b.runtime.sessions.setProjection(ROOT, 'conversationContinuation', SessionSeq(5)) const composer = b.composerApi(ROOT) expect(b.composerApi(ROOT).hooks.continuation).toBe(composer.hooks.continuation) expect(composer.hooks.continuation.getSnapshot()).toBe(5) @@ -186,6 +192,7 @@ describe('Conversation inject API', () => { const b = await bench() onTestFinished(() => b.runtime.dispose()) await b.runtime.sessions.replaceEvents(ROOT, continuationTurn({ kind: 'error', error: { code: 'TIMEOUT', message: 'timeout' } })) + await b.runtime.sessions.setProjection(ROOT, 'conversationContinuation', SessionSeq(5)) const composer = b.composerApi(ROOT) const input = b.inputApi(ROOT) composer.continueGeneration!(SessionSeq(1)) @@ -199,11 +206,142 @@ describe('Conversation inject API', () => { await b.runtime.sessions.appendEvent(ROOT, { type: 'event', event: { type: 'turn/start', seq: SessionSeq(6), time: 1, data: { turn: 2 } }, }) + await b.runtime.sessions.setProjection(ROOT, 'conversationContinuation', null) composer.continueGeneration!(SessionSeq(5)) expect(input.state.getSnapshot().draft).toBe('') expect(b.sessionFake.prompt).not.toHaveBeenCalled() }) + it('keeps continuation available with injected mode context but excludes pending human steering', async () => { + const b = await bench() + onTestFinished(() => b.runtime.dispose()) + await b.runtime.sessions.setProjection(ROOT, 'conversationContinuation', SessionSeq(5)) + const composer = b.composerApi(ROOT) + const source = composer.hooks.continuation + const notified = vi.fn() + const unsubscribe = source.subscribe(notified) + onTestFinished(unsubscribe) + const context = { id: 'context', role: 'user', content: [{ type: 'text', text: 'approval policy context' }], source: { kind: 'user-approval' } } + const inbox: InboxWireState = { 'next-turn': [], 'next-step': [context] } + await b.runtime.sessions.setProjection(ROOT, 'inbox', inbox) + expect(source.getSnapshot()).toBe(5) + const user = createUserMessage({ content: [{ type: 'text', text: 'new instruction' }], source: { kind: 'user' } }) + await b.runtime.sessions.setProjection(ROOT, 'inbox', { ...inbox, 'next-step': [context, user] }) + expect(source.getSnapshot()).toBeUndefined() + composer.continueGeneration!(SessionSeq(5)) + expect(b.sessionFake.prompt).not.toHaveBeenCalled() + await b.runtime.sessions.setProjection(ROOT, 'inbox', inbox) + expect(source.getSnapshot()).toBe(5) + composer.continueGeneration!(SessionSeq(5)) + await vi.waitFor(() => { expect(b.sessionFake.prompt).toHaveBeenCalledOnce() }) + expect(b.sessionFake.prompt.mock.calls[0]?.[0]).toEqual([{ type: 'text', text: '继续' }]) + expect(notified).toHaveBeenCalledTimes(3) + }) + + it('uses the Host projection on partial pages and does no history materialization for streaming chunks', async () => { + const b = await bench() + onTestFinished(() => b.runtime.dispose()) + await b.runtime.sessions.replaceEvents(ROOT, continuationTurn({ kind: 'error', error: { code: 'SERVER', message: 'busy' } }).slice(1), true) + await b.runtime.sessions.setProjection(ROOT, 'conversationContinuation', SessionSeq(5)) + const source = b.composerApi(ROOT).hooks.continuation + expect(source.getSnapshot()).toBe(5) + const history = Array.from({ length: 1666 }, (_, index) => continuationTurn({ kind: 'completed' }, index + 1)).flat() + history.push(...continuationTurn({ kind: 'completed' }, 1667).slice(0, 4)) + await b.runtime.sessions.replaceEvents(ROOT, history) + await b.runtime.sessions.setProjection(ROOT, 'conversationContinuation', null) + const feed = b.runtime.sessions.binding(ROOT)!.eventSource + if (!(feed instanceof MutableSessionEventSource)) throw new Error('expected mutable fixture feed') + const rawSnapshot = feed.getSnapshot.bind(feed) + const windows = new WeakMap() + let materializations = 0 + const read = vi.spyOn(feed, 'getSnapshot').mockImplementation(() => { + const window = rawSnapshot() + let observed = windows.get(window) + if (observed === undefined) { + observed = { + get entries() { materializations++; return window.entries }, + hasMore: window.hasMore, revision: window.revision, change: window.change, + } + windows.set(window, observed) + } + return observed + }) + onTestFinished(() => { read.mockRestore() }) + const notified = vi.fn() + const unsubscribe = source.subscribe(notified) + onTestFinished(unsubscribe) + for (let index = 0; index < 2000; index++) { + feed.append({ type: 'transient', event: { type: 'assistant/live-chunk', seq: 9999.5, time: index, + data: { attemptId: LlmAttemptId('streaming'), turn: 1667, step: 1, chunk: { type: 'text-delta', index: 0, text: 'x' } }, + } }) + expect(source.getSnapshot()).toBeUndefined() + } + expect(materializations).toBe(0) + expect(notified).not.toHaveBeenCalled() + await b.runtime.sessions.setProjection(ROOT, 'conversationContinuation', SessionSeq(10001)) + expect(notified).toHaveBeenCalledOnce() + expect(source.getSnapshot()).toBe(10001) + unsubscribe() + await b.runtime.sessions.setProjection(ROOT, 'conversationContinuation', null) + expect(notified).toHaveBeenCalledOnce() + }) + + it.each(['next-turn', 'pending', 'block', 'attachment', 'subagent', 'removed', 'closed'] as const)( + 'rejects a stale continuation click after %s changes', async (condition) => { + const b = await bench() + onTestFinished(() => b.runtime.dispose()) + await b.runtime.sessions.setProjection(ROOT, 'conversationContinuation', SessionSeq(5)) + const composer = b.composerApi(ROOT) + switch (condition) { + case 'pending': + await b.runtime.sessions.updateSessionSnapshot(ROOT, (state) => { state.pendingSubmissions = [{ requestId: 'pending' as SessionRequestId, placement: 'transcript', time: 0, text: 'newer message', attachments: [] }] }) + break + case 'next-turn': + await b.runtime.sessions.setProjection(ROOT, 'inbox', { 'next-turn': [createUserMessage({ content: [{ type: 'text', text: 'queued' }], source: { kind: 'user' } })], 'next-step': [] }) + break + case 'block': + b.runtime.ctx.conversation.blocks.set(ROOT, { reason: 'Choose a model' }) + break + case 'attachment': + expect(composer.addFiles?.([new File([Uint8Array.of(1)], 'draft.pdf', { type: 'application/pdf' })])).toBeNull() + break + case 'subagent': + await b.runtime.sessions.updateSessionSnapshot(ROOT, (state) => { state.subagent = { address: { parentSessionId: ROOT, childSessionId: ROOT, mode: 'continuable' }, parentAvailable: true } }) + break + case 'removed': + await b.runtime.sessions.updateSessionSnapshot(ROOT, (state) => { state.removed = true }) + break + case 'closed': + await b.runtime.sessions.updateSessionSnapshot(ROOT, (state) => { state.openState = 'error' }) + break + } + const before = b.inputApi(ROOT).state.getSnapshot().draft + composer.continueGeneration!(SessionSeq(5)) + expect(b.inputApi(ROOT).state.getSnapshot().draft).toBe(before) + expect(b.sessionFake.prompt).not.toHaveBeenCalled() + }, + ) + + it.each(['refused', 'failed'] as const)('retains a visible editable continuation draft when submission is %s', async (outcome) => { + const b = await bench() + onTestFinished(() => b.runtime.dispose()) + await b.runtime.sessions.setProjection(ROOT, 'conversationContinuation', SessionSeq(5)) + const composer = b.composerApi(ROOT) + if (outcome === 'refused') b.sessionFake.prompt.mockImplementationOnce(async () => { + const error = new RemoteError('gateway/internal', 'request refused', {}) + await b.runtime.sessions.updateSessionSnapshot(ROOT, (state) => { state.promptError = { op: 'send', error } }) + return { ok: false, error } + }) + else b.sessionFake.prompt.mockRejectedValueOnce(new Error('connection lost')) + composer.continueGeneration!(SessionSeq(5)) + await vi.waitFor(() => { expect(b.sessionFake.prompt).toHaveBeenCalledOnce(); expect(composer.keyboard!.snapshot.phase).toBe('plain') }) + expect(composer.keyboard!.snapshot.draft).toBe('继续') + if (outcome === 'refused') expect(b.runtime.sessions.binding(ROOT)!.session.getSnapshot().promptError?.error.message).toBe('request refused') + else expect(composer.hooks.notices.getSnapshot()).toMatchObject({ level: 'error', text: 'connection lost' }) + composer.continueGeneration!(SessionSeq(5)) + expect(b.sessionFake.prompt).toHaveBeenCalledOnce() + }) + it('renders literal quote text through the actual Factory and keyed node registration', async () => { const b = await bench(true) onTestFinished(() => b.runtime.dispose()) diff --git a/packages/client/ui-conversation/tests/config.host.spec.ts b/packages/client/ui-conversation/tests/config.host.spec.ts index 7b87d23d59..402063cedc 100644 --- a/packages/client/ui-conversation/tests/config.host.spec.ts +++ b/packages/client/ui-conversation/tests/config.host.spec.ts @@ -1,17 +1,22 @@ -import { Context } from '@deepseek-ai/cordis' -import { describe, expect, it } from 'vitest' +import SessionStore from '@deepseek-ai/dsh-session' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' +import { Context, FiberState } from '@deepseek-ai/cordis' +import { describe, expect, it, onTestFinished } from 'vitest' import * as HostPlugin from '../src/index.ts' import { liveConfig, omitsGeneratedPage } from '../../../settings/settings/tests/live-config.ts' import { plainConfig } from '../../../settings/settings/src/schema.ts' import { - DEFAULT_BUSY_ENTER_BEHAVIOR, Config, apply, + DEFAULT_BUSY_ENTER_BEHAVIOR, } from '@deepseek-ai/dsh-client-ui-conversation' describe('ui-conversation host', () => { it('registers, validates, and disposes the durable busy-Enter preference', async () => { const ctx = new Context() - const configuration = await liveConfig(ctx, { Config, apply }) + onTestFinished(() => ctx.fiber.dispose()) + await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) + const configuration = await liveConfig(ctx, HostPlugin) const { fiber } = configuration expect(plainConfig(configuration.fiber.effectiveConfig)).toEqual({ busyEnter: DEFAULT_BUSY_ENTER_BEHAVIOR }) await configuration.update({ busyEnter: 'steer' }) @@ -21,4 +26,21 @@ describe('ui-conversation host', () => { }) }) -it('keeps its own instance off the generated Settings pages', () => omitsGeneratedPage(ctx => ctx.plugin(HostPlugin))) +it('keeps its own instance off the generated Settings pages', () => omitsGeneratedPage(async (ctx) => { + onTestFinished(() => ctx.fiber.dispose()) + await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) + return ctx.plugin(HostPlugin) +})) + +it('waits for the required projection registry before activation', async () => { + const ctx = new Context() + onTestFinished(() => ctx.fiber.dispose()) + await ctx.plugin(SessionStore) + const fiber = ctx.plugin(HostPlugin) + await ctx.fiber.await() + expect(fiber.state).toBe(FiberState.PENDING) + await ctx.plugin(SessionProjectionRegistry) + await fiber + expect(fiber.state).toBe(FiberState.ACTIVE) +}) diff --git a/packages/client/ui-conversation/tests/continuation-fixture.client.ts b/packages/client/ui-conversation/tests/continuation-fixture.ts similarity index 92% rename from packages/client/ui-conversation/tests/continuation-fixture.client.ts rename to packages/client/ui-conversation/tests/continuation-fixture.ts index 20dc42fdc9..392f01d39a 100644 --- a/packages/client/ui-conversation/tests/continuation-fixture.client.ts +++ b/packages/client/ui-conversation/tests/continuation-fixture.ts @@ -1,5 +1,5 @@ /** Admitted input and a settled turn for manual-continuation tests. */ -import type { SessionLiveEventEntry } from '@deepseek-ai/dsh-api-session-controller/client' +import type { SessionEvent } from '@deepseek-ai/dsh-session/types' import { createUserMessage } from '@deepseek-ai/dsh-llm' import { SessionSeq } from '@deepseek-ai/dsh-session/types' import type { TurnEndReason } from '@deepseek-ai/dsh-session/types' @@ -10,7 +10,7 @@ import type { TurnEndReason } from '@deepseek-ai/dsh-session/types' * @param number - turn number and corresponding event offset. * @returns ordered durable entries. */ -export function continuationTurn(reason: TurnEndReason, number = 1): SessionLiveEventEntry[] { +export function continuationTurn(reason: TurnEndReason, number = 1): { type: 'event'; event: SessionEvent }[] { const offset = (number - 1) * 6 return [ { type: 'event', event: { type: 'turn/start', seq: SessionSeq(offset), time: 0, data: { turn: number } } }, diff --git a/packages/client/ui-conversation/tests/continuation.client.spec.ts b/packages/client/ui-conversation/tests/continuation.client.spec.ts deleted file mode 100644 index ba101e7737..0000000000 --- a/packages/client/ui-conversation/tests/continuation.client.spec.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { MutableSessionEventSource } from '@deepseek-ai/dsh-api-session-controller/client' -import type { SessionLiveEventEntry } from '@deepseek-ai/dsh-api-session-controller/client' -import { SessionSeq } from '@deepseek-ai/dsh-session/types' -import type { TurnEndReason } from '@deepseek-ai/dsh-session/types' -import { createContinuationSource } from '../src/client/input/continuation.ts' -import { continuationTurn as turn } from './continuation-fixture.client.ts' - -const failed = (code: string): TurnEndReason => ({ kind: 'error', error: { code, message: code } }) - -describe('manual generation continuation', () => { - it.each(['RATE_LIMIT', 'SERVER', 'TIMEOUT', 'TRANSPORT', 'EMPTY_RESPONSE', 'STREAM_CLOSED'])( - 'offers continuation for an admitted request ending with %s', (code) => { - const feed = new MutableSessionEventSource() - feed.replace(turn(failed(code)), false) - expect(createContinuationSource(feed).getSnapshot()).toBe(5) - }, - ) - - it.each(['AUTH', 'QUOTA', 'ACCOUNT_QUOTA', 'MISSING_CREDENTIAL', 'INVALID_CREDENTIAL', 'UNKNOWN_MODEL', - 'INVALID_REQUEST', 'CONTEXT_WINDOW_EXCEEDED', 'TOOL_ACTIVATION_FAILED', 'MALFORMED_RESPONSE', 'PI_AI_ERROR', 'UNKNOWN'])( - 'leaves %s to its own recovery action', (code) => { - const feed = new MutableSessionEventSource() - feed.replace(turn(failed(code)), false) - expect(createContinuationSource(feed).getSnapshot()).toBeUndefined() - }, - ) - - it.each([ - { kind: 'completed' }, { kind: 'max-tokens' }, { kind: 'blocked' }, - { kind: 'interrupted' }, { kind: 'forked' }, { kind: 'aborted', reason: { kind: 'user' } }, - ])('leaves a $kind turn unchanged', (reason) => { - const feed = new MutableSessionEventSource() - feed.replace(turn(reason), false) - expect(createContinuationSource(feed).getSnapshot()).toBeUndefined() - }) - - it('requires user input from the failed turn to have entered model history', () => { - const feed = new MutableSessionEventSource() - const source = createContinuationSource(feed) - const entries = turn(failed('TIMEOUT'), 2) - feed.replace([...turn({ kind: 'completed' }), ...entries.filter(entry => entry.event.type !== 'user/message')], false) - expect(source.getSnapshot()).toBeUndefined() - const user = entries[2]! - if (user.event.type !== 'user/message') throw new Error('expected user input') - const replacement: SessionLiveEventEntry = { type: 'event', event: { - ...user.event, surfaceOp: { op: 'replace', startSeq: SessionSeq(0), endSeq: SessionSeq(1) }, - } } - feed.replace(entries.map(entry => entry === user ? replacement : entry), false) - expect(source.getSnapshot()).toBeUndefined() - }) - - it('keeps failures before request dispatch in their preparation flow', () => { - const feed = new MutableSessionEventSource() - const source = createContinuationSource(feed) - feed.replace(turn(failed('TIMEOUT')).filter(entry => entry.event.type !== 'assistant/attempt'), false) - expect(source.getSnapshot()).toBeUndefined() - }) - - it('restores eligibility from paging and replacement, and retires it when a later turn starts', () => { - const feed = new MutableSessionEventSource() - const source = createContinuationSource(feed) - const entries = turn(failed('SERVER')) - feed.replace(entries.slice(1), true) - expect(source.getSnapshot()).toBeUndefined() - feed.prepend(entries.slice(0, 1), false) - expect(source.getSnapshot()).toBe(5) - expect(createContinuationSource(feed).getSnapshot()).toBe(5) - const listener = vi.fn() - const unsubscribe = source.subscribe(listener) - feed.append({ type: 'event', event: { type: 'turn/start', seq: SessionSeq(6), time: 1, data: { turn: 2 } } }) - expect(listener).toHaveBeenCalledOnce() - expect(source.getSnapshot()).toBeUndefined() - unsubscribe() - feed.replace(turn({ kind: 'completed' }), false) - expect(listener).toHaveBeenCalledOnce() - expect(source.getSnapshot()).toBeUndefined() - feed.replace([], false) - expect(source.getSnapshot()).toBeUndefined() - }) -}) diff --git a/packages/client/ui-conversation/tests/continuation.host.spec.ts b/packages/client/ui-conversation/tests/continuation.host.spec.ts new file mode 100644 index 0000000000..1b0dd1c7b3 --- /dev/null +++ b/packages/client/ui-conversation/tests/continuation.host.spec.ts @@ -0,0 +1,139 @@ +/** Complete-log continuation projection, including recovery and publication. */ +import { Context } from '@deepseek-ai/cordis' +import SessionStore, { SessionId, SessionSeq, SessionLogOffset } from '@deepseek-ai/dsh-session' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' +import { createAssistantMessage, createUserMessage } from '@deepseek-ai/dsh-llm' +import type { SessionEvent, TurnEndReason } from '@deepseek-ai/dsh-session/types' +import { expect, it, onTestFinished, vi } from 'vitest' +import * as plugin from '../src/index.ts' +import { continuationProjectionDefinition as definition } from '../src/host/continuation.ts' +import { paginate } from '@deepseek-ai/dsh-api-session-controller/src/history.ts' +import { continuationTurn as turn } from './continuation-fixture.ts' + +const failed = (code: string): TurnEndReason => ({ kind: 'error', error: { code, message: code } }) +function fold(events: readonly SessionEvent[]) { + return events.reduce((state, event) => definition.apply(state, event), definition.init()) +} +const events = (reason: TurnEndReason) => turn(reason).map(entry => entry.event) + +it.each(['RATE_LIMIT', 'SERVER', 'TIMEOUT', 'TRANSPORT', 'EMPTY_RESPONSE', 'STREAM_CLOSED'])( + 'offers a manual new turn after %s independently of automatic retries', (code) => { + expect(definition.wire.view(fold(events(failed(code))))).toBe(5) + }, +) +it.each(['AUTH', 'QUOTA', 'ACCOUNT_QUOTA', 'MISSING_CREDENTIAL', 'INVALID_CREDENTIAL', 'UNKNOWN_MODEL', + 'INVALID_REQUEST', 'CONTEXT_WINDOW_EXCEEDED', 'TOOL_ACTIVATION_FAILED', 'MALFORMED_RESPONSE', 'PI_AI_ERROR', 'UNKNOWN'])( + 'leaves %s to its own recovery action', (code) => { + expect(definition.wire.view(fold(events(failed(code))))).toBeNull() + }, +) +it.each([ + { kind: 'completed' }, { kind: 'max-tokens' }, { kind: 'blocked' }, + { kind: 'interrupted' }, { kind: 'forked' }, { kind: 'aborted', reason: { kind: 'user' } }, +])('leaves a $kind turn unchanged', (reason) => { + expect(definition.wire.view(fold(events(reason)))).toBeNull() +}) + +it('requires admitted human input and a failed settlement in the last step', () => { + const base = events(failed('TIMEOUT')) + expect(fold(base.filter(event => event.type !== 'user/message')).ending).toBeNull() + expect(fold(base.filter(event => event.type !== 'assistant/attempt')).ending).toBeNull() + const user = base.find(event => event.type === 'user/message')! + const replacement: SessionEvent<'user/message'> = { ...user, + surfaceOp: { op: 'replace', startSeq: SessionSeq(0), endSeq: SessionSeq(1) }, + } + expect(fold(base.map(event => event === user ? replacement : event)).ending).toBeNull() + const injected: SessionEvent<'user/message'> = { ...user, data: createUserMessage({ + content: [{ type: 'text', text: 'mode context' }], source: { kind: 'user-approval' }, + }) } + expect(fold(base.map(event => event === user ? injected : event)).ending).toBeNull() + const prefix = base.slice(0, -1) + const step: SessionEvent<'step/start'> = { type: 'step/start', seq: SessionSeq(5), time: 0, data: { turn: 1, step: 2 } } + expect(fold([...prefix, step, base.at(-1)!]).ending).toBeNull() + const success: SessionEvent<'assistant/message'> = { type: 'assistant/message', seq: SessionSeq(5), time: 0, + surfaceOp: 'append', data: { turn: 1, step: 1, stream: [], message: createAssistantMessage({ + content: [{ type: 'text', text: 'done' }], source: { provider: 'test', model: 'test' }, + }) }, + } + expect(fold([...prefix, success, base.at(-1)!]).ending).toBeNull() +}) + +it('keeps unrelated events and repeated settlements reference-stable', () => { + const base = events(failed('SERVER')) + const state = fold(base.slice(0, -1)) + expect(definition.apply(state, base[2]!)).toBe(state) + expect(definition.apply(state, base[3]!)).toBe(state) + expect(definition.apply(state, base[4]!)).toBe(state) + expect(definition.apply(definition.init(), base[1]!)).toBe(definition.init()) + const reset = definition.apply(state, base[1]!) + expect(reset.failedAttempt).toBe(false) +}) + +it('restores a checkpoint plus suffix identically to full replay and retires on a new turn', () => { + const base = events(failed('SERVER')) + const prefix = fold(base.slice(0, 3)) + const restored = definition.stateSchema.parse(JSON.parse(JSON.stringify(prefix))) + expect(base.slice(3).reduce((state, event) => definition.apply(state, event), restored)).toEqual(fold(base)) + const ended = fold(base) + expect(definition.apply(ended, { type: 'turn/start', seq: SessionSeq(6), time: 1, data: { turn: 2 } }).ending).toBeNull() + expect(definition.wire.viewSchema.parse(ended.ending)).toBe(5) + expect(() => definition.stateSchema.parse({ ...ended, ending: -1 })).toThrow() + expect(() => definition.wire.viewSchema.parse('5')).toThrow() +}) + +it('publishes a complete-log result independently of the visible page and releases registration', async () => { + const ctx = new Context() + onTestFinished(() => ctx.fiber.dispose()) + await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) + const fiber = await ctx.plugin(plugin) + const session = ctx.sessions.create(SessionId('continued')) + expect(ctx.sessionProjections.snapshot(session).values.conversationContinuation).toBeNull() + const history: SessionEvent[] = [] + ctx.on('session/event', (_session, event) => { history.push(event) }) + const changed = vi.fn[0]>() + ctx.sessionProjections.onChanged(changed) + session.append('turn/start', { turn: 1 }) + expect(changed).not.toHaveBeenCalled() + session.append('step/start', { turn: 1, step: 1 }) + session.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'task' }], source: { kind: 'user' } }), { surfaceOp: 'append' }) + // A long failed turn exceeds the browser's 500-message page budget. + for (let index = 0; index < 600; index++) { + session.append('assistant/message', { turn: 1, step: index + 1, stream: [], message: createAssistantMessage({ content: [{ type: 'text', text: 'progress' }], source: { provider: 'test', model: 'test' } }) }, { surfaceOp: 'append' }) + session.append('step/end', { turn: 1, step: index + 1 }) + session.append('step/start', { turn: 1, step: index + 2 }) + } + session.append('assistant/attempt', { turn: 1, step: 601, stream: [] }) + const ending = session.append('turn/end', { turn: 1, reason: failed('SERVER') }).seq + const page = paginate(history, undefined, 500, ending, { minMessages: 50, minTurns: 2 }) + expect(page.hasMore).toBe(true) + expect(page.events.some(event => event.type === 'turn/start')).toBe(false) + expect(ctx.sessionProjections.snapshot(session).values.conversationContinuation).toBe(ending) + expect(changed.mock.calls.map(([, key, value]) => [key, value])).toEqual([['conversationContinuation', null], ['conversationContinuation', ending]]) + expect(ctx.sessionProjections.checkpoint(session).conversationContinuation) + .toEqual({ ver: 1, seq: ending, val: { admitted: false, failedAttempt: false, ending } }) + session.append('turn/start', { turn: 2 }) + expect(ctx.sessionProjections.snapshot(session).values.conversationContinuation).toBeNull() + expect(changed).toHaveBeenCalledTimes(3) + await fiber.dispose() + expect(ctx.sessionProjections.snapshot(session).values).not.toHaveProperty('conversationContinuation') +}) + +it('restores valid, absent and outdated checkpoints through the registry', async () => { + const ctx = new Context() + onTestFinished(() => ctx.fiber.dispose()) + await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(plugin) + const session = ctx.sessions.create(SessionId('restored')) + const log = events(failed('SERVER')) + const checkpoint = { conversationContinuation: { ver: 1, seq: SessionSeq(2), val: fold(log.slice(0, 3)) } } + const restore = (rows: typeof checkpoint | Record, tail: readonly SessionEvent[], base: number) => + ctx.sessionProjections.restore(rows, tail, SessionLogOffset(base), session.header, session.inheritedEventCount) + expect(restore(checkpoint, log.slice(3), 3).snapshot.values.conversationContinuation).toBe(5) + expect(restore({}, log, 0).snapshot.values.conversationContinuation).toBe(5) + const outdated = { conversationContinuation: { ...checkpoint.conversationContinuation, ver: 0 } } + expect(restore(outdated, log, 0).snapshot.values.conversationContinuation).toBe(5) + expect(() => restore(outdated, log.slice(3), 3)).toThrow('re-read from seq 0') + expect(() => restore({}, log.slice(3), 3)).toThrow('re-read from seq 0') +}) diff --git a/packages/client/ui-conversation/tsconfig.client.json b/packages/client/ui-conversation/tsconfig.client.json index 0339eebbe6..cc0d628ef0 100644 --- a/packages/client/ui-conversation/tsconfig.client.json +++ b/packages/client/ui-conversation/tsconfig.client.json @@ -107,6 +107,7 @@ } ], "exclude": [ - "src/index.ts" + "src/index.ts", + "src/host" ] } diff --git a/packages/client/ui-conversation/tsconfig.host.json b/packages/client/ui-conversation/tsconfig.host.json index ec30b21bfe..cc278423fc 100644 --- a/packages/client/ui-conversation/tsconfig.host.json +++ b/packages/client/ui-conversation/tsconfig.host.json @@ -7,7 +7,9 @@ }, "files": [ "src/submission-settings.ts", - "src/index.ts" + "src/index.ts", + "src/continuation-types.ts", + "src/host/continuation.ts" ], "references": [ { @@ -18,6 +20,15 @@ }, { "path": "../../settings/settings" + }, + { + "path": "../../core/session" + }, + { + "path": "../../session/session-projection" + }, + { + "path": "../../util/brand" } ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 281ea5d111..489c15f56e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4001,9 +4001,15 @@ importers: packages/client/ui-conversation: dependencies: + '@deepseek-ai/dsh-brand': + specifier: workspace:* + version: link:../../util/brand '@deepseek-ai/schemastery': specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery + zod: + specifier: 4.4.3 + version: 4.4.3 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -4023,9 +4029,6 @@ importers: '@deepseek-ai/dsh-attachment': specifier: workspace:* version: link:../../attachment/attachment - '@deepseek-ai/dsh-brand': - specifier: workspace:* - version: link:../../util/brand '@deepseek-ai/dsh-client-file-upload': specifier: workspace:* version: link:../file-upload @@ -4089,6 +4092,9 @@ importers: '@deepseek-ai/dsh-session': specifier: workspace:* version: link:../../core/session + '@deepseek-ai/dsh-session-projection': + specifier: workspace:* + version: link:../../session/session-projection '@deepseek-ai/dsh-settings': specifier: workspace:* version: link:../../settings/settings @@ -4137,9 +4143,6 @@ importers: react-dom: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) - zod: - specifier: 4.4.3 - version: 4.4.3 packages/client/ui-deepseek-model-label-1017: devDependencies: diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts index aed0e5a121..e236f4e02b 100644 --- a/scripts/verify-package-readme-model-experience.ts +++ b/scripts/verify-package-readme-model-experience.ts @@ -138,7 +138,6 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly> = { 'packages/client/ui-sidebar-right': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' }, 'packages/client/ui-sidebar': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' }, 'packages/client/ui-brand-official': { kind: 'none', reason: 'Browser-side presentation occupants; registers nothing model-facing.' }, - 'packages/client/ui-conversation': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' }, 'packages/client/ui-approval': { kind: 'none', reason: 'Browser-side approval presentation; registers nothing model-facing.' }, 'packages/client/ui-chat': { kind: 'none', reason: 'Browser-side Chat presentation; registers nothing model-facing.' }, 'packages/client/ui-message-feedback': { kind: 'none', reason: 'Browser-side controls over log-only message feedback; ratings and notes never enter model context or change model-input tokens.' }, diff --git a/snapshots/web/live-interactions/continued.expected.md b/snapshots/web/live-interactions/continued.expected.md index ab51db50fc..1eb97847c2 100644 --- a/snapshots/web/live-interactions/continued.expected.md +++ b/snapshots/web/live-interactions/continued.expected.md @@ -19,7 +19,7 @@ - status: - text: This turn failedupstream 503 - code: SERVER -- text: continue {{clock}} +- text: plan Plan mode on. Use /plan off to leave. plan Plan mode off. continue {{clock}} - button "Copy" - status: Completed - button "Completed in {{duration}}" From d8254627feb3d4d1017819cc9e3bc0da31926b80 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Mon, 26 Oct 2026 22:12:13 +0800 Subject: [PATCH 04/65] feat(core): continue failed turns through empty prompt input --- ...6-manual-generation-continuation.i18n.yaml | 20 ++--- ...26-10-26-manual-generation-continuation.md | 18 ++-- ...10-26-manual-generation-continuation.zh.md | 20 ++--- apps/web/tests/live-interactions.e2e.ts | 6 +- docs/architecture.i18n.yaml | 4 +- docs/architecture.md | 2 + docs/architecture.zh.md | 2 + docs/config-catalog.md | 2 +- docs/config-catalog.zh.md | 2 +- .../guide.i18n.yaml | 16 ---- .../guide.md | 25 ------ .../guide.zh.md | 25 ------ .../empty-prompt-continuation/guide.i18n.yaml | 16 ++++ .../empty-prompt-continuation/guide.md | 19 +++++ .../empty-prompt-continuation/guide.zh.md | 19 +++++ .../api/session-controller/README.i18n.yaml | 4 +- packages/api/session-controller/README.md | 2 + packages/api/session-controller/README.zh.md | 2 + .../api/session-controller/src/commands.ts | 29 +++++++ .../session-controller/src}/continuation.ts | 22 +++-- packages/api/session-controller/src/index.ts | 4 +- packages/api/session-controller/src/types.ts | 8 +- .../tests/continuation-fixture.ts | 0 .../tests/continuation.host.spec.ts | 43 ++++++++-- .../tests/empty-prompt.host.spec.ts | 82 +++++++++++++++++++ .../tests/session-cold.host.spec.ts | 1 - .../api/session-controller/tsconfig.host.json | 6 +- .../client/ui-conversation/README.i18n.yaml | 28 +++---- packages/client/ui-conversation/README.md | 12 +-- packages/client/ui-conversation/README.zh.md | 12 +-- packages/client/ui-conversation/package.json | 8 +- .../src/client/contract/input.ts | 2 + .../src/client/contract/slots.ts | 2 +- .../ui-conversation/src/client/index.ts | 1 - .../src/client/input/facade.ts | 7 +- .../ui-conversation/src/client/input/hub.ts | 10 +-- .../src/client/input/machine.ts | 2 +- .../ui-conversation/src/client/locales.ts | 9 -- .../ui-conversation/src/continuation-types.ts | 9 -- packages/client/ui-conversation/src/index.ts | 14 ++-- .../tests/apply-inject.client.spec.tsx | 12 +-- .../ui-conversation/tests/config.host.spec.ts | 14 +--- .../client/ui-conversation/tsconfig.host.json | 13 +-- packages/core/agent-loop/README.i18n.yaml | 4 +- packages/core/agent-loop/README.md | 2 + packages/core/agent-loop/README.zh.md | 2 + packages/core/agent-loop/src/agent.ts | 2 +- .../agent-loop/tests/request-error.spec.ts | 27 +++++- packages/core/agent/README.i18n.yaml | 4 +- packages/core/agent/README.md | 2 + packages/core/agent/README.zh.md | 2 + packages/core/agent/src/runtime-types.ts | 4 +- pnpm-lock.yaml | 12 +-- scripts/smoke-python-runtime.py | 9 +- .../scheduler-recovery/requests.json | 4 - .../scheduler-recovery/result.json | 2 - .../scheduler-recovery/session.v4.jsonl | 3 +- snapshots/sdk/sdk.snapshot.ts | 19 ++++- .../notifications.expected.jsonl | 9 +- .../tool-scheduler-recovery/session.v4.jsonl | 3 +- .../live-interactions/continued.expected.md | 2 +- 61 files changed, 411 insertions(+), 255 deletions(-) delete mode 100644 docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.i18n.yaml delete mode 100644 docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.md delete mode 100644 docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.zh.md create mode 100644 docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.i18n.yaml create mode 100644 docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.md create mode 100644 docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.zh.md rename packages/{client/ui-conversation/src/host => api/session-controller/src}/continuation.ts (65%) rename packages/{client/ui-conversation => api/session-controller}/tests/continuation-fixture.ts (100%) rename packages/{client/ui-conversation => api/session-controller}/tests/continuation.host.spec.ts (78%) create mode 100644 packages/api/session-controller/tests/empty-prompt.host.spec.ts delete mode 100644 packages/client/ui-conversation/src/continuation-types.ts diff --git a/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.i18n.yaml b/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.i18n.yaml index 320476d704..0b66ec2f1d 100644 --- a/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.i18n.yaml @@ -6,17 +6,17 @@ en: 327aaa1cdc63b1f5 zh: 1147b4a23e51772c /agent-note-manual-generation-continuation/problem: - en: 027e01e4586da32f - zh: dce2c6858ef709c1 + en: f118844946afdbdc + zh: f1fe37d042f88b24 /agent-note-manual-generation-continuation/decision: - en: dc5f066e9c0bbb23 - zh: 6e54f59a34d8fca5 + en: 32e75f0c2a35bfec + zh: 343fedcd8f53a6a8 /agent-note-manual-generation-continuation/alternatives-considered: - en: b32c1050bc0ca9de - zh: 5e950f842f944818 + en: 58bb948d210d42ea + zh: 5dc920d222d71218 /agent-note-manual-generation-continuation/consequences: - en: c825acfaae8f049d - zh: e4768dab9d8edde3 + en: e86aee3a7da7649f + zh: d58e8cc94e5008bf /agent-note-manual-generation-continuation/verification: - en: bd59f8845a358dec - zh: cfcd30ca521e0111 + en: e31a5bd5826c689f + zh: 02ac700146246664 diff --git a/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.md b/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.md index 8060f1bde4..ab91eb5f7b 100644 --- a/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.md +++ b/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.md @@ -6,28 +6,28 @@ English | [中文](2026-10-26-manual-generation-continuation.zh.md) ## Problem -Users need another turn after a model request fails. A browser page can omit the failed turn's start; streaming must not repeatedly copy loaded history. +Users need another request after a model failure without inserting an artificial instruction. Browser paging can omit the failed turn's start, and streaming must not repeatedly copy history. ## Decision -The play action submits localized ordinary user text through the existing composer, retaining admission, logging, failure drafts, and telemetry. No resume RPC, AgentLoop change, or Session event format is introduced. +The play action submits `content: []` through the existing Session prompt API. AgentLoop retains the identified inbox input and starts a new balanced turn, but omits the empty ordinary input from model-visible `user/message` events. The original failed turn remains closed. -The fixed-size Host projection `conversationContinuation` recovers from complete Session history. Eligibility requires admitted ordinary input and a failed attempt, independently of the browser's 500-message page. +The Session API owns the fixed-size `conversationContinuation` projection and shares its value with the browser. Admission requires an eligible failure, true idle ownership, and no pending human input. An accepted empty input carries the admitted work into its next turn, preserving eligibility if that request also fails. -The six-code manual allowlist remains independent of [automatic retry policy](../../../../packages/llm/llm-retry/README.md). Pending [injected context](../architecture/2026-07-24-separate-context-injection-from-turn-execution.md) in `next-step` accompanies the new waking message; pending ordinary user submissions prevent duplicates. +The six-code manual allowlist stays independent of provider retry policy. Non-waking injected context accompanies the request. Durable inbox receipts retain request-id deduplication after the empty input is claimed; no new Remote operation or Session event format is required. ## Alternatives considered -**Window scanning or entries caching.** Each revision can materialize a fresh window; caching cannot recover unloaded facts. +**Localized or hidden continuation text.** Adds model-visible instructions and changes retained history. -**Use provider retryableCodes.** Deployment retry policies could expose authentication or other unsuitable errors as UI actions. +**A separate resume operation.** The existing prompt and inbox already carry identified control input and own wakeup. -**Add a resume RPC.** Ordinary submission already owns wakeup, admission, error reporting, and history. +**Keep the failed turn waiting for a click.** Changes failure settlement, idle status, cancellation, and model-selection semantics. ## Consequences -Host activation requires `sessionProjections`; custom compositions follow the [upgrade guide](../../../../docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.md). Fixed-size state avoids Client history scans without introducing a second submission lifecycle. +Ordinary empty-draft Enter remains inert. The explicit action reuses submission state and error reporting without an echo or generated draft. Pre-step rejection and cleared batches still prevent model calls. ## Verification -Tests cover checkpoint recovery, long-turn paging, injected context, refusal and retained drafts, and zero Client event-window reads. The recorded Web scenario exercises failure followed by continuation. +Unit and assembled scenarios cover repeated failure, request deduplication, injected context, balanced turns, retained tools, one original user message, and zero Client event-window reads. diff --git a/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.zh.md b/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.zh.md index c7c0d9ee13..21ff2cf50a 100644 --- a/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.zh.md +++ b/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.zh.md @@ -6,28 +6,28 @@ Status: implemented ## 问题 -模型请求失败后,用户需要发起下一轮。浏览器分页可能不含失败轮次的起点;流式输出也不能反复复制已加载历史。 +模型请求失败后,用户需要再次请求,但不应插入人为构造的指令。浏览器分页可能缺少失败轮次起点,流式输出也不能反复复制历史。 ## 决策 -播放按钮通过现有 composer 提交本地化的普通用户文本,沿用准入、日志、失败草稿和遥测语义。不引入 resume RPC,不修改 AgentLoop 或 Session 事件格式。 +播放按钮通过现有 Session prompt API 提交 `content: []`。AgentLoop 保留带身份的 inbox 输入并启动正常闭合的新轮次,但不将空普通输入写成模型可见的 `user/message`。原失败轮次保持闭合。 -固定大小的 Host 投影 `conversationContinuation` 从完整 Session 历史恢复。资格要求已准入的普通用户输入和失败尝试,不依赖浏览器的 500 条消息分页。 +Session API 拥有固定大小的 `conversationContinuation` 投影,向浏览器发布相同的值。准入要求失败符合条件、持有真正的空闲状态且没有待消费的用户输入。已接受的空输入将原先准入的工作带入新轮次,因此请求再次失败后仍保留继续资格。 -六类手动继续白名单独立于[自动重试策略](../../../../packages/llm/llm-retry/README.zh.md)。`next-step` 中待处理的[注入上下文](../architecture/2026-07-24-separate-context-injection-from-turn-execution.zh.md) 随新的唤醒消息进入请求;待提交的普通用户消息仍阻止重复提交。 +六码手动白名单独立于提供方重试策略。非唤醒的注入上下文随请求进入。空输入被领取后,持久 inbox 回执仍支持 request-id 去重;不需要新增 Remote 操作或 Session 事件格式。 ## 考虑过的替代方案 -**窗口扫描或 entries 缓存。** 每个 revision 都可能物化新窗口;缓存无法恢复尚未加载的事实。 +**本地化或隐藏的继续文本。** 增加模型可见指令并改变保留历史。 -**采用提供方的 retryableCodes。** 部署重试策略可能把认证等不适合的错误也变成界面动作。 +**单独的 resume 操作。** 现有 prompt 和 inbox 已可承载带身份的控制输入并拥有唤醒流程。 -**新增 resume RPC。** 普通提交已经拥有唤醒、准入、错误报告和历史。 +**保持失败轮次等待点击。** 改变失败结算、空闲状态、取消和模型选择语义。 -## 影响 +## 后果 -Host 激活需要 `sessionProjections`;自定义组合按[升级指南](../../../../docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.zh.md) 调整。固定大小的状态避免 Client 历史扫描,也无需第二套提交生命周期。 +普通空草稿回车仍不执行。显式继续沿用提交状态与错误报告,不创建消息回显或生成草稿。pre-step 拒绝和被清空的批次仍阻止模型调用。 ## 验证 -测试覆盖检查点恢复、长轮次分页、注入上下文、拒绝与草稿保留,以及不读取 Client 事件窗口。已录制的 Web 场景验证失败后的继续流程。 +单元和组装场景覆盖重复失败、请求去重、注入上下文、轮次闭合、工具结果保留、仅一条原始用户消息,以及客户端不读取完整事件窗口。 diff --git a/apps/web/tests/live-interactions.e2e.ts b/apps/web/tests/live-interactions.e2e.ts index d1e90d0080..eda193847c 100644 --- a/apps/web/tests/live-interactions.e2e.ts +++ b/apps/web/tests/live-interactions.e2e.ts @@ -414,7 +414,7 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => { expect(tripwire.warnings).toEqual([]) }, 120_000) - it.skipIf(MODE === 'record')('continues after transient retries exhaust through a localized user message', async () => { + it.skipIf(MODE === 'record')('continues after transient retries exhaust without another user message', async () => { // A whole-script replacement: three throw entries cover the first request // plus both budgeted retries (patches cannot reach past the one-call // derived script). The scenario-owned policy keeps exhaustion fast and @@ -477,7 +477,9 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => { expect(sessionEvents.some(event => event.type === 'user/message' && event.data.source.kind === 'plan-mode')).toBe(true) const userMessages = sessionEvents.flatMap(event => event.type === 'user/message' && event.data.source.kind === 'user' ? [event.data.content] : []) - expect(userMessages).toEqual([[{ type: 'text', text: PROMPT }], [{ type: 'text', text: 'continue' }]]) + expect(userMessages).toEqual([[{ type: 'text', text: PROMPT }]]) + expect(sessionEvents.filter(event => event.type === 'turn/start')).toHaveLength(2) + expect(sessionEvents.filter(event => event.type === 'turn/end')).toHaveLength(2) expect(await resume.count()).toBe(0) await page.setViewportSize({ width: 1440, height: 900 }) await compareOrRefreshGolden(join(SNAPSHOT_DIR, 'continued.expected.md'), diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index c8543cdc36..d1e20ef6b6 100644 --- a/docs/architecture.i18n.yaml +++ b/docs/architecture.i18n.yaml @@ -19,12 +19,12 @@ zh: 70d87a12c43e042a /deepseek-harness-architecture/core-packages: en: 20ef882e44b06a10 - zh: c10c3934ed5570e9 + zh: 0d69c1500650868f /deepseek-harness-architecture/events: en: 693407efb233f961 zh: 37b9dd0677d4405c /deepseek-harness-architecture/turn-flow: - en: 1a79a70b62c04cdb + en: 22964b8327adc3f6 zh: c86d765e21430405 /deepseek-harness-architecture/session-log: en: 8a7f19b95d426c99 diff --git a/docs/architecture.md b/docs/architecture.md index e6bede8d0e..ed35ab94af 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -119,6 +119,8 @@ Details: the [sequence diagram](agent-lifecycle.md), the [tool pipeline](tool-ex [`ctx.tools.execute()`](subsystems/tools.md) receives raw batches and concurrency limits. +An identified empty ordinary input starts a new turn from committed history without creating a model-visible user message. It remains logged in the inbox, passes normal pre-step admission, and closes through the same turn and step lifecycle. + ## Session log The session log is the source of the context the model sees. `deriveMessages()` projects model history from it. Each `assistant/message` embeds the exact compact timed stream that produced its assembled content; `assistant/attempt` retains settled failed, retried, cancelled, and stream-error attempts without adding model history. Fork, resume, transcripts, telemetry, and persistence all derive from these durable settlements, while live UI incrementality comes from `agent/assistant-stream`; a hard process loss before settlement leaves no durable attempt stream ([decision](../.agents/notes/implemented/architecture/2026-09-01-v2-embedded-assistant-streams.md)). diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 8863c8047c..64c3be1ddd 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -66,6 +66,8 @@ Electron 使用 Electron Node 模式启动私有 Desktop Host。Host 调用共 +带身份的空普通输入基于已提交历史启动新轮次,不生成模型可见的用户消息。它仍记录在 inbox 中,经过正常 pre-step 准入,并通过相同的轮次和步骤生命周期闭合。 + ## 事件 事件就是扩展点,而选对事件域是大多数改动的第一个决定。 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 43726adfb5..be0d89caf2 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -4713,7 +4713,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co | `@deepseek-ai/dsh-client-ui-brand-official` | — | [`packages/client/ui-brand-official/src/index.ts`](../packages/client/ui-brand-official/src/index.ts) | | `@deepseek-ai/dsh-client-ui-chat` | — | [`packages/client/ui-chat/src/index.ts`](../packages/client/ui-chat/src/index.ts) | | `@deepseek-ai/dsh-client-ui-commands` | — | [`packages/client/ui-commands/src/index.ts`](../packages/client/ui-commands/src/index.ts) | -| `@deepseek-ai/dsh-client-ui-conversation` | `sessionProjections` | [`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts) | +| `@deepseek-ai/dsh-client-ui-conversation` | — | [`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts) | | `@deepseek-ai/dsh-client-ui-cordis` | — | [`packages/extensions/ui-cordis/src/index.ts`](../packages/extensions/ui-cordis/src/index.ts) | | `@deepseek-ai/dsh-client-ui-creation-recommendations` | — | [`packages/experimental/client-ui-creation-recommendations/src/index.ts`](../packages/experimental/client-ui-creation-recommendations/src/index.ts) | | `@deepseek-ai/dsh-client-ui-deepseek-model-label-1017` | — | [`packages/client/ui-deepseek-model-label-1017/src/index.ts`](../packages/client/ui-deepseek-model-label-1017/src/index.ts) | diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 470c40d5b0..23165f5f85 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -4715,7 +4715,7 @@ export interface Config { | `@deepseek-ai/dsh-client-ui-brand-official` | — | [`packages/client/ui-brand-official/src/index.ts`](../packages/client/ui-brand-official/src/index.ts) | | `@deepseek-ai/dsh-client-ui-chat` | — | [`packages/client/ui-chat/src/index.ts`](../packages/client/ui-chat/src/index.ts) | | `@deepseek-ai/dsh-client-ui-commands` | — | [`packages/client/ui-commands/src/index.ts`](../packages/client/ui-commands/src/index.ts) | -| `@deepseek-ai/dsh-client-ui-conversation` | `sessionProjections` | [`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts) | +| `@deepseek-ai/dsh-client-ui-conversation` | — | [`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts) | | `@deepseek-ai/dsh-client-ui-cordis` | — | [`packages/extensions/ui-cordis/src/index.ts`](../packages/extensions/ui-cordis/src/index.ts) | | `@deepseek-ai/dsh-client-ui-creation-recommendations` | — | [`packages/experimental/client-ui-creation-recommendations/src/index.ts`](../packages/experimental/client-ui-creation-recommendations/src/index.ts) | | `@deepseek-ai/dsh-client-ui-deepseek-model-label-1017` | — | [`packages/client/ui-deepseek-model-label-1017/src/index.ts`](../packages/client/ui-deepseek-model-label-1017/src/index.ts) | diff --git a/docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.i18n.yaml deleted file mode 100644 index 50094c2b2c..0000000000 --- a/docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.i18n.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading -# section, a hash of its English and Chinese blocks outside code blocks and generated regions. -# After editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.md -/: - en: cd293f657bb1ab7a - zh: 7d942448b488e22c -/provide-session-projections-for-conversation: - en: 6ec06cc57348d333 - zh: 86665b791e3b2e02 -/provide-session-projections-for-conversation/change: - en: b0048aafcb91d8a5 - zh: c913215a68c5ff5b -/provide-session-projections-for-conversation/migration: - en: e06d674bafc37abe - zh: b68409bfbc936d19 diff --git a/docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.md b/docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.md deleted file mode 100644 index 52fa806c20..0000000000 --- a/docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -kind: upgrade-guide -description: "Custom Conversation compositions must provide sessionProjections to activate the Host plugin." ---- -# Provide Session projections for Conversation - -English | [中文](guide.zh.md) - -## Change - -The Host entry of `@deepseek-ai/dsh-client-ui-conversation` requires the `sessionProjections` service to publish manual continuation eligibility. Custom compositions that mounted Conversation without a projection provider must add one. The shipped Web and Desktop compositions already provide it. Session logs do not require migration. - -## Migration - -1. Check the custom profile's bundles and `cordis.patch.yml` for an existing `@deepseek-ai/dsh-session-projection` entry. Keep that provider when present. -2. If the composition has none, add this row to the profile's `cordis.patch.yml`: - - ```yaml - - id: session-projection - name: '@deepseek-ai/dsh-session-projection' - ``` - -3. Restart the profile and verify that the Conversation Host entry activates. After a supported model-request failure, an empty, idle ordinary composer must offer the play action. - -See the [Conversation reference](../../../../packages/client/ui-conversation/README.md#shell-and-standard-props) for the supported failures and submission behavior. diff --git a/docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.zh.md deleted file mode 100644 index 1277f31bf5..0000000000 --- a/docs/upgrade-guide/v0.2.1-rc.3/conversation-continuation-projection/guide.zh.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -kind: upgrade-guide -description: "自定义 Conversation 组合必须提供 sessionProjections 才能激活 Host 插件。" ---- -# 为 Conversation 提供 Session 投影 - -[English](guide.md) | 中文 - -## 变更 - -`@deepseek-ai/dsh-client-ui-conversation` 的 Host 入口需要 `sessionProjections` 服务来发布手动继续资格。挂载 Conversation 却没有投影提供方的自定义组合必须补上提供方。随产品提供的 Web 和 Desktop 组合已经提供它。Session 日志不需要迁移。 - -## 迁移 - -1. 检查自定义 profile 的 bundle 和 `cordis.patch.yml` 中是否已有 `@deepseek-ai/dsh-session-projection` entry;存在时保留该提供方。 -2. 如果组合中没有该提供方,在 profile 的 `cordis.patch.yml` 中添加以下条目: - - ```yaml - - id: session-projection - name: '@deepseek-ai/dsh-session-projection' - ``` - -3. 重启 profile 并确认 Conversation Host 入口已激活。支持的模型请求失败后,空草稿且空闲的普通 composer 应提供播放按钮。 - -支持的失败类型和提交行为见 [Conversation 参考](../../../../packages/client/ui-conversation/README.zh.md#shell-and-standard-props)。 diff --git a/docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.i18n.yaml new file mode 100644 index 0000000000..cbda06d0c8 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.md +/: + en: 7a194a68f49ac09a + zh: 4d7bdff82c571c62 +/observe-empty-prompt-admission-through-the-inbox: + en: 450277479989809e + zh: 4b01f252faeae19c +/observe-empty-prompt-admission-through-the-inbox/change: + en: e1c081d5321f3256 + zh: a5dd5585fe4300b3 +/observe-empty-prompt-admission-through-the-inbox/migration: + en: 36e80226df9dcf54 + zh: 81a9c0bd5c667470 diff --git a/docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.md b/docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.md new file mode 100644 index 0000000000..16876dd26c --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.md @@ -0,0 +1,19 @@ +--- +kind: upgrade-guide +description: "Empty ordinary Agent inputs start requests without producing user/message events." +--- +# Observe empty prompt admission through the inbox + +English | [中文](guide.zh.md) + +## Change + +An ordinary Agent input with `content: []` starts a new turn from committed history without appending an empty `user/message`. The identified input remains in durable inbox events. Custom consumers that wait for every accepted input to produce a user message must observe its inbox receipt instead. Session logs need no migration; existing recorded empty user messages keep their historical interpretation. + +The Web Session prompt endpoint accepts an empty array only for an eligible failed, idle ordinary Session in queue mode. Whitespace-only text parts remain invalid. + +## Migration + +1. Keep ordinary text and attachment inputs unchanged. Use an empty array only when requesting another model call without new user text. +2. Match acceptance by the input identity in `agent/inbox/spliced`, then observe the normal turn/status lifecycle. Do not wait for an empty `user/message`. +3. Verify that continuation adds a balanced turn without adding a user message. The shipped TypeScript and Python SDK clients already observe inbox receipts. diff --git a/docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.zh.md new file mode 100644 index 0000000000..9c879cc2aa --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.zh.md @@ -0,0 +1,19 @@ +--- +kind: upgrade-guide +description: "空的普通 Agent 输入发起请求,但不产生 user/message 事件。" +--- +# 通过 inbox 观察空 prompt 准入 + +[English](guide.md) | 中文 + +## 变更 + +普通 Agent 输入的 `content: []` 基于已提交历史启动新轮次,不再追加空的 `user/message`。带身份的输入仍保留在持久 inbox 事件中。若自定义消费者等待每条已接受输入产生用户消息,应改为观察对应 inbox 回执。Session 日志无需迁移,历史空用户消息的解释保持不变。 + +Web Session prompt 端点仅在普通 Session 空闲、最新失败符合继续条件且使用 queue 模式时接受空数组。仅含空白的文本块仍无效。 + +## 迁移 + +1. 普通文本和附件输入保持不变。仅在需要再次请求模型而不添加用户文本时使用空数组。 +2. 通过 `agent/inbox/spliced` 中的输入身份匹配准入,然后观察正常轮次与状态生命周期。不要等待空的 `user/message`。 +3. 验证继续操作增加正常闭合的轮次,但不增加用户消息。内置 TypeScript 和 Python SDK 客户端已观察 inbox 回执。 diff --git a/packages/api/session-controller/README.i18n.yaml b/packages/api/session-controller/README.i18n.yaml index 010c41bf94..78f64d3acc 100644 --- a/packages/api/session-controller/README.i18n.yaml +++ b/packages/api/session-controller/README.i18n.yaml @@ -24,8 +24,8 @@ en: 954923654eebe2dc zh: 6bd9a7d2ea965aed /session-controller/configuration: - en: c92590fbaf75e45e - zh: fcac67017bb0e3ab + en: 2a91d9f904e90756 + zh: 8affb15ccc571829 /session-controller/model-experience: en: 30b917bf822bb4df zh: b0b023b5b526c317 diff --git a/packages/api/session-controller/README.md b/packages/api/session-controller/README.md index 6fd7b24561..1b11d04106 100644 --- a/packages/api/session-controller/README.md +++ b/packages/api/session-controller/README.md @@ -111,6 +111,8 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a Blockquotes travel as ordinary prompt text without quote metadata. Local submission echoes and durable messages use the same text rendering rules; queue edits replace content while retaining prompt correlation. +An empty `prompt.content` array requests a new turn only for an eligible failed, idle ordinary Session in queue mode. Pending human input prevents admission; non-waking injected context is retained. `conversationContinuation` supplies the shared six-code manual policy, independently of provider retry configuration. The input is logged through the inbox for request-id deduplication, but creates no model-visible user message. Whitespace-only text parts remain invalid. + ## Model Experience None, as invoked Agent commands own any model-visible effect. diff --git a/packages/api/session-controller/README.zh.md b/packages/api/session-controller/README.zh.md index 8729f555ba..cb8a1ba818 100644 --- a/packages/api/session-controller/README.zh.md +++ b/packages/api/session-controller/README.zh.md @@ -111,6 +111,8 @@ GUI 模型选择要求确切提供方/模型对出现在可用目录中;不 Blockquote 作为普通 prompt 文本传递,不携带 quote 元数据。本地提交回显与持久消息使用同一文本渲染规则;队列编辑替换 content,同时保留 prompt 关联信息。 +空的 `prompt.content` 数组仅在普通 Session 空闲、最新失败符合继续条件且使用 queue 模式时请求新轮次。待消费的用户输入阻止准入,非唤醒的注入上下文保留。`conversationContinuation` 提供共用的六码手动策略,独立于提供方重试配置。输入通过 inbox 持久记录,用于 request-id 去重,但不生成模型可见的用户消息。仅含空白的文本块仍无效。 + ## 模型体验 无;任何模型可见效果都由被调用的 Agent 命令负责。 diff --git a/packages/api/session-controller/src/commands.ts b/packages/api/session-controller/src/commands.ts index 5309872331..0b62ff724a 100644 --- a/packages/api/session-controller/src/commands.ts +++ b/packages/api/session-controller/src/commands.ts @@ -1,5 +1,8 @@ /** Session commands whose activation policy is explicit at each Remote method. */ +import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection' +import { continuationProjectionDefinition, type ContinuationState } from './continuation.ts' + import { modelAvailable } from './catalog.ts' import { randomUUID } from 'node:crypto' import type { Context } from '@deepseek-ai/cordis' @@ -83,6 +86,13 @@ function latestCompletedPrefixBoundary(events: readonly SessionEvent[]): Session /** Implements Session business commands delegated by the Session Controller Remote service. */ export class SessionCommandController { + private continuation!: ProjectionHandle + + /** Register complete-history empty-prompt eligibility. @returns when command admission is ready. */ + async initialize(): Promise { + this.continuation = await this.ctx.sessionProjections.register(continuationProjectionDefinition) + } + /** * @param ctx - Host context carrying Agent, model, attachment, title, and Workspace services. * @param agents - sole owner of create, resume, and Agent composition. @@ -304,6 +314,22 @@ export class SessionCommandController { async prompt(request: SessionPromptRequest, source: MessageSource): Promise { const agent = await this.resolveAgent(request.sessionId) if (hasPromptRequest(agent, request.requestId)) return { accepted: true } + if (request.content.length === 0) { + try { + return await agent.runMaintenance(async () => { + const inbox = agent.inbox + if (request.mode !== 'queue' || this.continuation.stateOf(agent.session).ending === null + || inbox.nextTurn.length > 0 || inbox.nextStep.some(message => message.source.kind === 'user')) { + throw new RemoteError('session/agent-busy', 'generation continuation is unavailable', {}) + } + agent.followup(createUserMessage({ content: [], source })) + return { accepted: true as const } + }) + } catch (error: unknown) { + if (remoteErrorOf(error) !== undefined) throw error + throw new RemoteError('session/agent-busy', 'generation continuation was not accepted', { reason: String(error) }) + } + } const admission = resolvePromptFileReceipts( request.content, receiptId => this.ctx.fileUploads.resolve(agent, receiptId), @@ -547,6 +573,9 @@ function hasPromptRequest(agent: Agent, requestId: SessionRequestId): boolean { if (agent.inbox.nextTurn.some(matches) || agent.inbox.nextStep.some(matches)) return true // oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred. return agent.session.snapshotEvents().some((event) => { + if (event.type === 'agent/inbox/spliced') { + return event.data.inserted.some(message => message.content.length === 0 && matches(message)) + } if (event.type !== 'user/message') return false const source = event.data.source return source.kind === 'user' && 'rpcId' in source && source.rpcId === requestId diff --git a/packages/client/ui-conversation/src/host/continuation.ts b/packages/api/session-controller/src/continuation.ts similarity index 65% rename from packages/client/ui-conversation/src/host/continuation.ts rename to packages/api/session-controller/src/continuation.ts index ef2239338e..8673cfc731 100644 --- a/packages/client/ui-conversation/src/host/continuation.ts +++ b/packages/api/session-controller/src/continuation.ts @@ -1,5 +1,5 @@ /** Constant-size continuation eligibility over the complete durable Session. */ -import type {} from '../continuation-types.ts' +import type {} from './types.ts' import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection' import type { SessionSeq } from '@deepseek-ai/dsh-session/types' import { brandNumber } from '@deepseek-ai/dsh-brand' @@ -15,27 +15,35 @@ const stateSchema = z.object({ admitted: z.boolean(), failedAttempt: z.boolean(), ending: endingSchema, + continuing: z.boolean(), }) -type ContinuationState = z.infer +/** Recoverable manual-continuation facts, including an accepted empty input awaiting its turn. */ +export type ContinuationState = z.infer -const EMPTY: ContinuationState = { admitted: false, failedAttempt: false, ending: null } +const EMPTY: ContinuationState = { admitted: false, failedAttempt: false, ending: null, continuing: false } -/** Eligibility requires admitted human input and a failed settlement in the last request step. */ +/** Eligibility requires admitted human work and a failed settlement in the last request step. */ export const continuationProjectionDefinition = { key: 'conversationContinuation', - stateVersion: 1, + stateVersion: 2, stateSchema, init: (): ContinuationState => EMPTY, apply: (state, event) => { switch (event.type) { + case 'agent/inbox/spliced': + if (state.continuing && event.data.target === 'next-turn' && event.data.outcome === 'canceled' + && event.data.start === 0 && (event.data.removedCount ?? 0) > 0) return { ...state, continuing: false } + return state.ending !== null && !state.continuing && event.data.target === 'next-turn' + && event.data.inserted.some(message => message.source.kind === 'user' && message.content.length === 0) + ? { ...state, continuing: true } : state case 'turn/start': - return EMPTY + return state.continuing ? { ...EMPTY, admitted: true } : EMPTY case 'step/start': case 'assistant/message': return state.failedAttempt ? { ...state, failedAttempt: false } : state case 'user/message': - return !state.admitted && event.surfaceOp === 'append' && event.data.source.kind === 'user' + return !state.admitted && event.surfaceOp === 'append' && event.data.source.kind === 'user' && event.data.content.length > 0 ? { ...state, admitted: true } : state case 'assistant/attempt': return state.failedAttempt ? state : { ...state, failedAttempt: true } diff --git a/packages/api/session-controller/src/index.ts b/packages/api/session-controller/src/index.ts index 94d08c77e3..f87c4f0b0f 100644 --- a/packages/api/session-controller/src/index.ts +++ b/packages/api/session-controller/src/index.ts @@ -192,6 +192,8 @@ export class SessionController extends TypertRemoteService { this.agents = new ApiSessionAgentController(ctx) this.agents.initialize() this.commands = new SessionCommandController(ctx, this.agents, process.cwd()) + await this.commands.initialize() + ctx.fiber.assertActive() ctx.effect(() => ctx.fileUploads.registerAgentResolver(async (sessionId) => { const result = await this.agents.resolveAgent(sessionId) if ('error' in result) throw result.error @@ -464,7 +466,7 @@ export class SessionController extends TypertRemoteService { @Remote('prompt') async prompt(request: SessionPromptRequest, signal: AbortSignal): Promise { signal.throwIfAborted() - if (!hasPromptContent(request.content)) { + if (request.content.length > 0 && !hasPromptContent(request.content)) { throw new RemoteError('gateway/bad-request', 'prompt content must include non-whitespace text or an attachment', {}) } const clientTimeZone = request.clientTimeZone === undefined diff --git a/packages/api/session-controller/src/types.ts b/packages/api/session-controller/src/types.ts index e3e2ea375a..798ca0e04c 100644 --- a/packages/api/session-controller/src/types.ts +++ b/packages/api/session-controller/src/types.ts @@ -8,7 +8,7 @@ import type { import type { Branded } from '@deepseek-ai/dsh-brand' import type { LlmAttemptId, MessageId } from '@deepseek-ai/dsh-llm/brand' import type { TextBlock } from '@deepseek-ai/dsh-llm' -import type { SessionId, SessionSeqCursor } from '@deepseek-ai/dsh-session/types' +import type { SessionId, SessionSeq, SessionSeqCursor } from '@deepseek-ai/dsh-session/types' import type { SessionProjectionMap, SessionProjectionSummaryExcluded, SessionProjectionSummaryValues } from '@deepseek-ai/dsh-session-projection/types' import type { Principal } from '@deepseek-ai/dsh-typert-protocol' import type { JsonValue } from '@deepseek-ai/dsh-util-values' @@ -16,6 +16,8 @@ import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types' declare module '@deepseek-ai/dsh-session-projection/types' { interface SessionProjectionMap { + /** Eligible latest failed turn/end for an empty prompt, or null. */ + conversationContinuation: SessionSeq | null /** Persisted facts used to summarize a Session without activating it. */ sessionListMetadata: SessionListMetadata /** @@ -317,11 +319,11 @@ export interface SessionForkValue { /** Session prompt request. */ export interface SessionPromptRequest { - /** Client-minted identity persisted on the exact accepted user message. */ + /** Client-minted identity persisted on the accepted inbox input, including empty continuation. */ readonly requestId: SessionRequestId readonly sessionId: SessionId readonly mode: 'queue' | 'steer' - /** At least one non-whitespace text part or attachment. */ + /** Non-whitespace text or attachments; an empty array continues an eligible failed idle Session. */ readonly content: readonly PromptContentPart[] readonly clientTimeZone?: string } diff --git a/packages/client/ui-conversation/tests/continuation-fixture.ts b/packages/api/session-controller/tests/continuation-fixture.ts similarity index 100% rename from packages/client/ui-conversation/tests/continuation-fixture.ts rename to packages/api/session-controller/tests/continuation-fixture.ts diff --git a/packages/client/ui-conversation/tests/continuation.host.spec.ts b/packages/api/session-controller/tests/continuation.host.spec.ts similarity index 78% rename from packages/client/ui-conversation/tests/continuation.host.spec.ts rename to packages/api/session-controller/tests/continuation.host.spec.ts index 1b0dd1c7b3..f87b29809f 100644 --- a/packages/client/ui-conversation/tests/continuation.host.spec.ts +++ b/packages/api/session-controller/tests/continuation.host.spec.ts @@ -5,9 +5,9 @@ import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import { createAssistantMessage, createUserMessage } from '@deepseek-ai/dsh-llm' import type { SessionEvent, TurnEndReason } from '@deepseek-ai/dsh-session/types' import { expect, it, onTestFinished, vi } from 'vitest' -import * as plugin from '../src/index.ts' -import { continuationProjectionDefinition as definition } from '../src/host/continuation.ts' -import { paginate } from '@deepseek-ai/dsh-api-session-controller/src/history.ts' + +import { continuationProjectionDefinition as definition } from '../src/continuation.ts' +import { paginate } from '../src/history.ts' import { continuationTurn as turn } from './continuation-fixture.ts' const failed = (code: string): TurnEndReason => ({ kind: 'error', error: { code, message: code } }) @@ -86,7 +86,7 @@ it('publishes a complete-log result independently of the visible page and releas onTestFinished(() => ctx.fiber.dispose()) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - const fiber = await ctx.plugin(plugin) + const fiber = await ctx.sessionProjections.register(definition) const session = ctx.sessions.create(SessionId('continued')) expect(ctx.sessionProjections.snapshot(session).values.conversationContinuation).toBeNull() const history: SessionEvent[] = [] @@ -111,7 +111,7 @@ it('publishes a complete-log result independently of the visible page and releas expect(ctx.sessionProjections.snapshot(session).values.conversationContinuation).toBe(ending) expect(changed.mock.calls.map(([, key, value]) => [key, value])).toEqual([['conversationContinuation', null], ['conversationContinuation', ending]]) expect(ctx.sessionProjections.checkpoint(session).conversationContinuation) - .toEqual({ ver: 1, seq: ending, val: { admitted: false, failedAttempt: false, ending } }) + .toEqual({ ver: 2, seq: ending, val: { admitted: false, failedAttempt: false, ending, continuing: false } }) session.append('turn/start', { turn: 2 }) expect(ctx.sessionProjections.snapshot(session).values.conversationContinuation).toBeNull() expect(changed).toHaveBeenCalledTimes(3) @@ -124,10 +124,10 @@ it('restores valid, absent and outdated checkpoints through the registry', async onTestFinished(() => ctx.fiber.dispose()) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - await ctx.plugin(plugin) + await ctx.sessionProjections.register(definition) const session = ctx.sessions.create(SessionId('restored')) const log = events(failed('SERVER')) - const checkpoint = { conversationContinuation: { ver: 1, seq: SessionSeq(2), val: fold(log.slice(0, 3)) } } + const checkpoint = { conversationContinuation: { ver: 2, seq: SessionSeq(2), val: fold(log.slice(0, 3)) } } const restore = (rows: typeof checkpoint | Record, tail: readonly SessionEvent[], base: number) => ctx.sessionProjections.restore(rows, tail, SessionLogOffset(base), session.header, session.inheritedEventCount) expect(restore(checkpoint, log.slice(3), 3).snapshot.values.conversationContinuation).toBe(5) @@ -137,3 +137,32 @@ it('restores valid, absent and outdated checkpoints through the registry', async expect(() => restore(outdated, log.slice(3), 3)).toThrow('re-read from seq 0') expect(() => restore({}, log.slice(3), 3)).toThrow('re-read from seq 0') }) + +it('carries admitted work through an empty continuation and retires a canceled queued control', async () => { + const ctx = new Context() + onTestFinished(() => ctx.fiber.dispose()) + await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) + const handle = await ctx.sessionProjections.register(definition) + const session = ctx.sessions.create(SessionId('empty-repeat')) + for (const event of events(failed('SERVER'))) { + if (event.type === 'user/message') session.append(event.type, event.data, { surfaceOp: 'append' }) + else if (event.type === 'turn/start') session.append(event.type, event.data) + else if (event.type === 'step/start' || event.type === 'step/end') session.append(event.type, event.data) + else if (event.type === 'assistant/attempt') session.append(event.type, event.data) + else if (event.type === 'turn/end') session.append(event.type, event.data) + } + const input = createUserMessage({ content: [], source: { kind: 'user' } }) + session.append('agent/inbox/spliced', { target: 'next-turn', start: 0, inserted: [input] }) + expect(handle.stateOf(session).continuing).toBe(true) + session.append('agent/inbox/spliced', { target: 'next-turn', start: 0, removedCount: 1, inserted: [], outcome: 'canceled' }) + expect(handle.stateOf(session).continuing).toBe(false) + session.append('agent/inbox/spliced', { target: 'next-turn', start: 0, inserted: [input] }) + session.append('turn/start', { turn: 2 }) + expect(handle.stateOf(session).admitted).toBe(true) + session.append('step/start', { turn: 2, step: 1 }) + session.append('assistant/attempt', { turn: 2, step: 1, stream: [] }) + session.append('step/end', { turn: 2, step: 1 }) + const ending = session.append('turn/end', { turn: 2, reason: failed('SERVER') }).seq + expect(handle.stateOf(session).ending).toBe(ending) +}) diff --git a/packages/api/session-controller/tests/empty-prompt.host.spec.ts b/packages/api/session-controller/tests/empty-prompt.host.spec.ts new file mode 100644 index 0000000000..5f8d913f40 --- /dev/null +++ b/packages/api/session-controller/tests/empty-prompt.host.spec.ts @@ -0,0 +1,82 @@ +/** Empty prompt admission over the real AgentLoop and the Session Remote owner. */ +import { Context } from '@deepseek-ai/cordis' +import AgentLoop from '@deepseek-ai/dsh-agent-loop' +import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' +import { createUserMessage, LlmError } from '@deepseek-ai/dsh-llm' +import { SessionId } from '@deepseek-ai/dsh-session' +import { expect, it, onTestFinished } from 'vitest' +import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' +import type { SessionPromptRequest, SessionRequestId } from '../src/types.ts' +import { createSessionTestRemote } from './test-remote.ts' + +async function setup() { + const ctx = new Context() + onTestFinished(() => ctx.fiber.dispose()) + await mountAgentLoopTestDependencies(ctx) + await ctx.plugin(AgentLoop, { agents: [] }) + const fail = (): never => { throw new LlmError('busy', 'SERVER') } + const adapter = new MockAdapter([fail, fail, textResponse('done')]) + ctx.effect(() => ctx.llm.registerAdapter(['mock'], adapter)) + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'mock', model: 'mock' }), cwd: '/workspace' }) + const agent = await ctx.agentLoop.create(SessionId('empty-prompt'), { provider: 'mock', model: 'mock' }) + const request = (id: string, mode: SessionPromptRequest['mode'] = 'queue'): SessionPromptRequest => ({ + sessionId: agent.id, requestId: id as SessionRequestId, content: [], mode, + }) + return { ctx, remote, agent, adapter, request } +} + +it('admits repeated empty prompts after failures without extra user messages or duplicate requests', async () => { + const { remote, agent, adapter, request } = await setup() + agent.followup(createUserMessage({ content: [{ type: 'text', text: 'original task' }], source: { kind: 'user' } })) + await agent.whenIdle() + const first = request('continue-1') + expect((await remote.prompt(first)).ok).toBe(true) + await agent.whenIdle() + expect(adapter.requests).toHaveLength(2) + expect((await remote.prompt(first)).ok).toBe(true) + await agent.whenIdle() + expect(adapter.requests).toHaveLength(2) + expect((await remote.prompt(request('continue-2'))).ok).toBe(true) + await agent.whenIdle() + expect(adapter.requests).toHaveLength(3) + const events = agent.session.snapshotEvents() + expect(events.filter(event => event.type === 'user/message' && event.data.source.kind === 'user')).toHaveLength(1) + expect(events.flatMap(event => event.type === 'turn/end' ? [event.data.reason.kind] : [])).toEqual(['error', 'error', 'completed']) + expect(events.filter(event => event.type === 'turn/start')).toHaveLength(3) + expect((await remote.prompt(request('after-completion'))).ok).toBe(false) +}) + +it('refuses empty fresh sessions, steering, queued human input and maintenance without waking work', async () => { + const { remote, agent, adapter, request } = await setup() + expect((await remote.prompt(request('empty-fresh'))).ok).toBe(false) + expect(adapter.requests).toHaveLength(0) + agent.followup(createUserMessage({ content: [{ type: 'text', text: 'original task' }], source: { kind: 'user' } })) + await agent.whenIdle() + expect((await remote.prompt(request('empty-steer', 'steer'))).ok).toBe(false) + const pending = createUserMessage({ content: [{ type: 'text', text: 'queued' }], source: { kind: 'user' } }) + agent.inbox.append('next-turn', pending) + expect((await remote.prompt(request('empty-queued'))).ok).toBe(false) + agent.inbox.clear() + agent.inject(pending) + expect((await remote.prompt(request('empty-steering'))).ok).toBe(false) + agent.inbox.clear() + await agent.runMaintenance(async () => { + expect((await remote.prompt(request('empty-maintenance'))).ok).toBe(false) + }) + expect(adapter.requests).toHaveLength(1) +}) + +it('consumes injected context with the empty prompt', async () => { + const { ctx, remote, agent, adapter, request } = await setup() + agent.followup(createUserMessage({ content: [{ type: 'text', text: 'original task' }], source: { kind: 'user' } })) + await agent.whenIdle() + agent.inject(createUserMessage({ content: [{ type: 'text', text: 'approval context' }], source: { kind: 'user-approval' } })) + agent.model.select({ provider: 'mock', model: 'changed' }) + expect((await remote.prompt(request('empty-context'))).ok).toBe(true) + await agent.whenIdle() + expect(adapter.requests).toHaveLength(2) + expect(JSON.stringify(adapter.requests[1]!.messages)).toContain('approval context') + expect(adapter.requests[1]!.model).toBe('changed') + expect(agent.inbox.nextStep).toHaveLength(0) + expect(ctx.sessionProjections.snapshot(agent.session).values.conversationContinuation).not.toBeNull() +}) diff --git a/packages/api/session-controller/tests/session-cold.host.spec.ts b/packages/api/session-controller/tests/session-cold.host.spec.ts index 560c05514f..5139baf644 100644 --- a/packages/api/session-controller/tests/session-cold.host.spec.ts +++ b/packages/api/session-controller/tests/session-cold.host.spec.ts @@ -924,7 +924,6 @@ describe('sessions.prompt synchronous rejection', () => { }) const initialEvents = session.snapshotEvents() const rejectedContent: readonly SessionPromptRequest['content'][] = [ - [], [{ type: 'text', text: '' }], [{ type: 'text', text: ' \t\n' }, { type: 'text', text: '' }], ] diff --git a/packages/api/session-controller/tsconfig.host.json b/packages/api/session-controller/tsconfig.host.json index acadf82099..168fd5a0c7 100644 --- a/packages/api/session-controller/tsconfig.host.json +++ b/packages/api/session-controller/tsconfig.host.json @@ -25,7 +25,8 @@ "src/list.ts", "src/media-references.ts", "src/archived-session-gate.ts", - "src/skill-catalog.ts" + "src/skill-catalog.ts", + "src/continuation.ts" ], "references": [ { @@ -126,6 +127,9 @@ }, { "path": "../../session/working-directory" + }, + { + "path": "../../util/brand" } ] } diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index 2b4fd0c5db..6ee1b33613 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/README.i18n.yaml @@ -21,26 +21,26 @@ en: cc4dafd1aae02468 zh: be6b67724b7ac4a3 /deepseek-ai-dsh-client-ui-conversation/shell-and-standard-props: - en: ac170c9895e17840 - zh: c7603d365134ce89 + en: 8f3eb5f46b48df30 + zh: a1a1da12767f4e92 /deepseek-ai-dsh-client-ui-conversation/temporary-composer-entries: en: bb1244310f5c0382 zh: 824b84077432b559 /deepseek-ai-dsh-client-ui-conversation/model-experience: en: 215e7ba838619b7b zh: a311da3843709f90 -/deepseek-ai-dsh-client-ui-conversation/model-experience/manual-continuation-message: - en: 30f3836663c881ca - zh: ee3e1d1df8603fad -/deepseek-ai-dsh-client-ui-conversation/model-experience/manual-continuation-message/what-the-model-sees: - en: d566958ff2f23584 - zh: f6ebea015ba61872 -/deepseek-ai-dsh-client-ui-conversation/model-experience/manual-continuation-message/token-effect: - en: 0ad06a5163a62c50 - zh: d25b93fcb2783566 -/deepseek-ai-dsh-client-ui-conversation/model-experience/manual-continuation-message/kv-cache-effect: - en: 326e23f5913e3fc0 - zh: 865969505814e0af +/deepseek-ai-dsh-client-ui-conversation/model-experience/manual-continuation-request: + en: 6a7ae99570c9c007 + zh: 26e86063e97d82c7 +/deepseek-ai-dsh-client-ui-conversation/model-experience/manual-continuation-request/what-the-model-sees: + en: b1376c710fa71ce3 + zh: e751e1a490c36bd8 +/deepseek-ai-dsh-client-ui-conversation/model-experience/manual-continuation-request/token-effect: + en: a5bbf55d50568fa2 + zh: 6c43af211055db93 +/deepseek-ai-dsh-client-ui-conversation/model-experience/manual-continuation-request/kv-cache-effect: + en: bbb4b8186c85f62d + zh: 222397b869a60df2 /deepseek-ai-dsh-client-ui-conversation/known-limitations-and-deferred-work: en: d76daa90b6d6f600 zh: 37a9f7d402b2552b diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index eadcdf2467..4a408c5950 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -76,9 +76,9 @@ The composer measures its expanded control groups after size, content, visibilit The context meter follows the [statistics-control radius](../../../docs/ui-radius.md#radius-scale). -An empty, idle ordinary Session composer offers a play action after `RATE_LIMIT`, `SERVER`, `TIMEOUT`, `TRANSPORT`, `EMPTY_RESPONSE`, or `STREAM_CLOSED` ends its latest turn. The failed turn must include admitted ordinary user input and a settled failed model attempt. Clicking submits a normal user message in the current interface language; drafts and attachments keep the Send action. Continued requests retain committed history and use the existing submission lifecycle. +An empty, idle ordinary Session composer offers a play action after `RATE_LIMIT`, `SERVER`, `TIMEOUT`, `TRANSPORT`, `EMPTY_RESPONSE`, or `STREAM_CLOSED` ends its latest turn. The failed turn must include admitted ordinary user input and a settled failed model attempt. Clicking submits `content: []` through the existing Session prompt API; drafts and attachments keep the Send action. A new balanced turn uses committed history without adding a user message. Refusal or transport failure leaves the draft empty and reports the error through the ordinary submission lifecycle. -The Host requires `sessionProjections` and publishes `conversationContinuation`; eligibility is restored from Session history independently of the browser's loaded history pages. +The Session API owns `conversationContinuation`, shared by Host admission and browser presentation. It restores eligibility from complete Session history and preserves it across repeated failed empty continuations, independently of loaded browser pages. The composer uses the shared [text-input focus policy](../ui-primitives/README.md#use-this-package) on mount, Session switching, and unlock. Identified phones and tablets require tapping the input; desktop devices retain automatic focus. On these mobile devices, a submission that empties the focused draft moves focus to the non-text composer card, dismissing the keyboard while retaining its navigation position. Rejected submissions, retained newer draft text, and another control already holding focus preserve focus. The [Lexical patch](../../../patches/lexical@0.49.0.patch) keeps caret scrolling in the same coordinate system as DOM geometry when a mobile keyboard pans the page. @@ -189,19 +189,19 @@ The selector must be a pure function of the owner currency. Its non-null return ## Model Experience -### Manual continuation message +### Manual continuation request #### What the model sees -The play action submits one ordinary user message using the interface language selected at the click: Chinese `继续`, English/French/Portuguese `continue`, Japanese `続けて`, Spanish `continúa`, German `weiter`, or Russian `продолжай`. The admitted message is stored in the Session log and included in subsequent model history; the action does not reconstruct text from the failed attempt. +The play action starts a new request from committed Session history. Its empty input is a durable inbox command, not a model-visible user message; it adds no localized or hidden continuation instruction. Pending injected context follows ordinary admission. #### Token effect -Each continuation admitted into model history adds its short localized text and the ordinary user-message overhead. The exact token count depends on the model tokenizer. A submission refused or failed before admission adds no continuation text to model history. +The action adds no continuation text or empty user-message overhead. The request still incurs the ordinary cost of retained history, current prompt sections, tool declarations, and any newly admitted context. #### KV Cache effect -The continuation message extends retained history without replacing earlier messages or prompt sections. It preserves an already-reusable prefix; provider cache availability, reuse policy, and eviction remain outside this package's control. +The control input does not alter model history. Prefix reuse still depends on the current model, prompt, tools, injected context, and provider cache policy. ## Known Limitations and Deferred Work diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index 3573e9c7b4..85df62d84b 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -76,9 +76,9 @@ Queue 预览启用空白折叠和文档边缘 trim,保留 emoji,并使用 CS 上下文占用率入口遵循[统计控件圆角规则](../../../docs/ui-radius.zh.md#radius-scale)。 -普通 Session 的输入框在空草稿且空闲时,若最新一轮以 `RATE_LIMIT`、`SERVER`、`TIMEOUT`、`TRANSPORT`、`EMPTY_RESPONSE` 或 `STREAM_CLOSED` 结束,会提供播放图标按钮。失败轮次必须包含已进入模型历史的普通用户输入和已落盘的失败模型请求尝试。点击后按当前界面语言提交一条普通用户消息;存在草稿或附件时保留发送按钮。继续请求保留已提交历史,并沿用现有提交流程。 +空闲且草稿为空的普通 Session,在最新轮次以 `RATE_LIMIT`、`SERVER`、`TIMEOUT`、`TRANSPORT`、`EMPTY_RESPONSE` 或 `STREAM_CLOSED` 结束后显示播放按钮。失败工作必须有已准入的普通用户输入和失败请求尝试。点击通过现有 Session prompt API 提交 `content: []`;已有草稿或附件仍使用发送按钮。新轮次正常闭合,使用已提交历史而不新增用户消息。请求被拒绝或传输失败时,草稿保持为空,错误沿用普通提交流程展示。 -Host 需要 `sessionProjections`,并发布 `conversationContinuation`;继续资格从 Session 历史恢复,不受浏览器已加载历史分页的影响。 +Session API 拥有 `conversationContinuation`,Host 准入和浏览器展示共用该投影。它从完整 Session 历史恢复资格,并在空输入继续再次失败时保留资格,不依赖浏览器已加载的历史分页。 composer 在挂载、切换 Session 和解除锁定时使用共享的[文字输入框聚焦策略](../ui-primitives/README.zh.md#use-this-package)。识别出的手机和平板需要点击输入框;桌面设备保留自动聚焦。在这些移动设备上,提交清空正在聚焦的草稿后,焦点移到不接受文字输入的 composer 卡片,收起键盘并保留键盘导航位置。被拒绝的提交、保留的新草稿文字,以及已经聚焦的其他控件均保留原有焦点。移动端键盘平移页面时,[Lexical 补丁](../../../patches/lexical@0.49.0.patch)让光标滚动计算与 DOM 几何使用同一坐标系。 @@ -189,19 +189,19 @@ selector 必须是 owner currency 的纯函数。非 null 返回值作为 `match ## 模型体验 -### 手动继续消息 +### 手动继续请求 #### 模型看到什么 -播放按钮按点击时选中的界面语言提交一条普通用户消息:中文为 `继续`,英语、法语和葡萄牙语为 `continue`,日语为 `続けて`,西班牙语为 `continúa`,德语为 `weiter`,俄语为 `продолжай`。已准入的消息写入 Session 日志并进入后续模型历史;该操作不从失败尝试中重建文本。 +播放按钮基于已提交的 Session 历史发起新请求。空输入作为持久 inbox 控制记录,不成为模型可见的用户消息;不会添加本地化或隐藏的继续指令。待注入上下文遵循普通准入流程。 #### Token 影响 -每条进入模型历史的继续消息增加其本地化短文本和普通用户消息的封装开销。准确 token 数取决于模型 tokenizer。在准入前被拒绝或失败的提交不会向模型历史添加继续文本。 +该操作不增加继续文本或空用户消息的封装开销。请求仍承担保留历史、当前提示词、工具声明以及新准入上下文的正常输入成本。 #### KV Cache 影响 -继续消息在保留历史后追加,不替换更早的消息或 prompt section。它保留原本可复用的前缀;提供方缓存的可用性、复用策略和淘汰规则不由本包控制。 +控制输入不改变模型历史。前缀复用仍取决于当前模型、提示词、工具、注入上下文及提供方缓存策略。 ## 已知限制与暂缓事项 diff --git a/packages/client/ui-conversation/package.json b/packages/client/ui-conversation/package.json index c4b2317d94..d38d3d7ebe 100644 --- a/packages/client/ui-conversation/package.json +++ b/packages/client/ui-conversation/package.json @@ -47,9 +47,7 @@ }, "license": "MIT", "dependencies": { - "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3", - "@deepseek-ai/dsh-brand": "workspace:*" + "@deepseek-ai/schemastery": "workspace:~" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~" @@ -98,7 +96,9 @@ "@deepseek-ai/dsh-client-shortcuts": "workspace:*", "@deepseek-ai/dsh-client-product-analytics": "workspace:*", "@deepseek-ai/dsh-message-quotes": "workspace:*", - "@deepseek-ai/dsh-session-projection": "workspace:*" + "@deepseek-ai/dsh-session-projection": "workspace:*", + "@deepseek-ai/dsh-brand": "workspace:*", + "zod": "4.4.3" }, "files": [ "locale/*.json", diff --git a/packages/client/ui-conversation/src/client/contract/input.ts b/packages/client/ui-conversation/src/client/contract/input.ts index 82f030c775..9cc7e4b566 100644 --- a/packages/client/ui-conversation/src/client/contract/input.ts +++ b/packages/client/ui-conversation/src/client/contract/input.ts @@ -352,6 +352,8 @@ export type InputEvent = /** Enter submission with the current clipboard projection, its reference chips, its quotes, and its attachments. */ | { readonly type: 'enter' + /** Explicit continuation may submit an empty draft through the default sink. */ + readonly allowEmpty?: boolean readonly mode: InputSubmitMode readonly draft: string readonly occurrences: readonly Occurrence[] diff --git a/packages/client/ui-conversation/src/client/contract/slots.ts b/packages/client/ui-conversation/src/client/contract/slots.ts index 068fb0026e..4d701b2af8 100644 --- a/packages/client/ui-conversation/src/client/contract/slots.ts +++ b/packages/client/ui-conversation/src/client/contract/slots.ts @@ -530,7 +530,7 @@ export interface ComposerBarInjected { toggleCommandMenu: ((selection: EditSelection) => void) | undefined stop: (() => void) | undefined /** - * Submit a localized continuation message for the displayed failure. + * Submit empty continuation input for the displayed failure. * @param ending - sequence of the displayed failed turn/end. */ continueGeneration: ((ending: SessionSeq) => void) | undefined diff --git a/packages/client/ui-conversation/src/client/index.ts b/packages/client/ui-conversation/src/client/index.ts index 65d7edc6b2..d4f06accc5 100644 --- a/packages/client/ui-conversation/src/client/index.ts +++ b/packages/client/ui-conversation/src/client/index.ts @@ -1,6 +1,5 @@ /** Browser Conversation assemble core, React adapter, shell, input, and text projection plugin. */ export type {} from './user-text/session-reference-rule.ts' -export type {} from '../continuation-types.ts' /** Declaration-merged node payloads; IDs also identify the corresponding keyed renderer. */ export interface UserTextDataMap { diff --git a/packages/client/ui-conversation/src/client/input/facade.ts b/packages/client/ui-conversation/src/client/input/facade.ts index 6306216b1c..e82acfdd35 100644 --- a/packages/client/ui-conversation/src/client/input/facade.ts +++ b/packages/client/ui-conversation/src/client/input/facade.ts @@ -388,7 +388,7 @@ export class SessionInputShell implements SessionInput { * (adjudicating/submitting/sending) force-closes the transient layers: the * popup dismisses and the menu tracks frozen. */ - submit(mode: InputSubmitMode = 'queue', source?: 'click' | 'enter'): void { + submit(mode: InputSubmitMode = 'queue', source?: 'click' | 'enter', allowEmpty = false): void { if (this.disposed) return const timestamp = Date.now() let state: MessageSubmissionState | undefined @@ -409,6 +409,7 @@ export class SessionInputShell implements SessionInput { } this.dispatchRun({ type: 'enter', + ...allowEmpty ? { allowEmpty: true } : {}, mode, draft: this.projection.clipboardText, occurrences: [...this.projection.occurrences], @@ -726,6 +727,10 @@ export class SessionInputShell implements SessionInput { }) const send = (text: string): void => { if (this.dead(attempt)) return + if (text === '' && draft !== '' && attachmentIds.length === 0) { + this.settleSink(attempt, Promise.resolve({ kind: 'success' })) + return + } this.settleSink(attempt, this.deps.defaultSink(text, attachmentIds, mode, attempt.signal, () => { this.acceptSink(attempt) })) } if (!(prompt instanceof Promise)) { send(prompt); return } diff --git a/packages/client/ui-conversation/src/client/input/hub.ts b/packages/client/ui-conversation/src/client/input/hub.ts index 36ea1c6b4b..d1454d29d7 100644 --- a/packages/client/ui-conversation/src/client/input/hub.ts +++ b/packages/client/ui-conversation/src/client/input/hub.ts @@ -25,7 +25,6 @@ import type { ComposerBlocks } from '../contract/composer-blocks.ts' import type { InputSubmitMode } from '../contract/composer-submission.ts' import type { PopupDismissFace } from './facade.ts' import { SessionInputShell } from './facade.ts' -import type {} from '../../continuation-types.ts' import { reportMessageSubmission } from './submission-analytics.ts' import { readConversationDraft } from '../stores.ts' @@ -239,7 +238,7 @@ export class InputHub implements SessionInputResolver { } /** - * Submit localized continuation text through the ordinary composer. Stale + * Submit empty continuation input through the ordinary composer. Stale * failures, active work, and drafts keep their current input unchanged. * @param id - retained ordinary Session identity. * @param ending - failure the displayed button addresses. @@ -256,8 +255,7 @@ export class InputHub implements SessionInputResolver { || input.draft.trim() !== '' || input.attachmentIds.length > 0 || (inbox?.['next-turn'].length ?? 0) > 0 || this.conversation().blocks.storeFor(id).getSnapshot() !== undefined) return - shell.setDraft(this.t('input.continueMessage')) - shell.submit('queue', 'click') + shell.submit('queue', 'click', true) } /** @@ -304,7 +302,9 @@ export class InputHub implements SessionInputResolver { signal: AbortSignal, accept: () => void, ): Promise { - if (text === '' && attachmentIds.length === 0) return Promise.resolve({ kind: 'success' }) + if (text === '' && attachmentIds.length === 0) { + return session.prompt([], mode, signal).then(result => result.ok ? { kind: 'success' } : { kind: 'error' }) + } return this.conversation().sendSession(session, text, attachmentIds, mode, signal, accept) } diff --git a/packages/client/ui-conversation/src/client/input/machine.ts b/packages/client/ui-conversation/src/client/input/machine.ts index 16fe7dd034..8545f964e5 100644 --- a/packages/client/ui-conversation/src/client/input/machine.ts +++ b/packages/client/ui-conversation/src/client/input/machine.ts @@ -144,7 +144,7 @@ export class SubmitMachine { return [{ type: 'begin-submit', attempt, claim: this.claim, args: argsAfter(draft, this.claim.token) }] } const trimmed = draft.trim() - if (trimmed === '' && attachmentIds.length === 0) return [] + if (trimmed === '' && attachmentIds.length === 0 && ev.allowEmpty !== true) return [] const attempt = this.beginAttempt(mode, draft, occurrences, quotes, attachmentIds, submission) if (trimmed.startsWith('/')) { this.phase = 'adjudicating' diff --git a/packages/client/ui-conversation/src/client/locales.ts b/packages/client/ui-conversation/src/client/locales.ts index 0ace610330..0598cee7d9 100644 --- a/packages/client/ui-conversation/src/client/locales.ts +++ b/packages/client/ui-conversation/src/client/locales.ts @@ -29,8 +29,6 @@ export const zh = { 'input.stop': '停止生成', 'input.send': '发送消息', 'input.continue': '继续生成', - // Model-visible text, durably logged when admitted as an ordinary user message. - 'input.continueMessage': '继续', 'input.sending': '正在发送消息', 'input.send.queue': '排队发送', 'input.send.steer': '插话发送', @@ -416,7 +414,6 @@ export const en = { 'input.stop': 'Stop generating', 'input.send': 'Send message', 'input.continue': 'Continue generating', - 'input.continueMessage': 'continue', 'input.sending': 'Sending message', 'input.send.queue': 'Queue message', 'input.send.steer': 'Steer message', @@ -801,7 +798,6 @@ export const ja = { 'input.stop': '生成を停止', 'input.send': 'メッセージを送信', 'input.continue': '生成を続ける', - 'input.continueMessage': '続けて', 'input.sending': 'メッセージを送信中', 'input.send.queue': 'メッセージをキューに追加', 'input.send.steer': '実行中の指示にメッセージを追加', @@ -1184,7 +1180,6 @@ export const es = { 'input.stop': 'Detener generación', 'input.send': 'Enviar mensaje', 'input.continue': 'Continuar generando', - 'input.continueMessage': 'continúa', 'input.sending': 'Enviando mensaje', 'input.send.queue': 'mensaje de cola', 'input.send.steer': 'Redirigir mensaje', @@ -1569,7 +1564,6 @@ export const fr = { 'input.stop': 'Arrêter la génération', 'input.send': 'Envoyer le message', 'input.continue': 'Continuer la génération', - 'input.continueMessage': 'continue', 'input.sending': 'Envoi du message', 'input.send.queue': "Message de file d'attente", 'input.send.steer': 'Guider avec ce message', @@ -1954,7 +1948,6 @@ export const de = { 'input.stop': 'Generierung stoppen', 'input.send': 'Nachricht senden', 'input.continue': 'Generierung fortsetzen', - 'input.continueMessage': 'weiter', 'input.sending': 'Nachricht wird gesendet', 'input.send.queue': 'Warteschlangennachricht', 'input.send.steer': 'Mit Nachricht lenken', @@ -2339,7 +2332,6 @@ export const ru = { 'input.stop': 'Остановить генерацию', 'input.send': 'Отправить сообщение', 'input.continue': 'Продолжить генерацию', - 'input.continueMessage': 'продолжай', 'input.sending': 'Отправка сообщения', 'input.send.queue': 'Сообщение в очереди', 'input.send.steer': 'Направить сообщение', @@ -2724,7 +2716,6 @@ export const pt = { 'input.stop': 'Parar geração', 'input.send': 'Enviar mensagem', 'input.continue': 'Continuar gerando', - 'input.continueMessage': 'continue', 'input.sending': 'Enviando mensagem', 'input.send.queue': 'Mensagem de fila', 'input.send.steer': 'Orientar com mensagem', diff --git a/packages/client/ui-conversation/src/continuation-types.ts b/packages/client/ui-conversation/src/continuation-types.ts deleted file mode 100644 index 9acece7469..0000000000 --- a/packages/client/ui-conversation/src/continuation-types.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** Host-derived eligibility for an explicit generation-continuation message. */ -import type { SessionSeq } from '@deepseek-ai/dsh-session/types' - -declare module '@deepseek-ai/dsh-session-projection/types' { - interface SessionProjectionMap { - /** Eligible latest turn/end, or null while no settled request can be continued. */ - conversationContinuation: SessionSeq | null - } -} diff --git a/packages/client/ui-conversation/src/index.ts b/packages/client/ui-conversation/src/index.ts index 6c7515a448..6ef779d42b 100644 --- a/packages/client/ui-conversation/src/index.ts +++ b/packages/client/ui-conversation/src/index.ts @@ -1,6 +1,5 @@ -/** Host continuation eligibility and browser conversation preferences. */ +/** Browser conversation preferences. */ import type {} from '@deepseek-ai/dsh-settings' -export type {} from './continuation-types.ts' import type { Volatile, Context } from '@deepseek-ai/cordis' import type { BusyEnterBehavior } from './submission-settings.ts' @@ -8,7 +7,6 @@ import z from '@deepseek-ai/schemastery' import { BUSY_ENTER_FIELD } from './submission-settings.ts' import { ConversationSettingsFields } from './submission-settings.ts' -import { continuationProjectionDefinition } from './host/continuation.ts' export { BUSY_ENTER_BEHAVIORS, BUSY_ENTER_FIELD, CONVERSATION_SETTINGS_NAMESPACE, @@ -26,14 +24,12 @@ export const Config = z.object({ [BUSY_ENTER_FIELD]: ConversationSettingsFields[BUSY_ENTER_FIELD].volatile(), }) -/** Required registry for the durable generation-continuation projection. */ -export const inject = ['sessionProjections'] +/** Settings integration is optional and does not block activation. */ +export const inject = [] -/** Register generation-continuation eligibility and browser preferences. +/** Register browser conversation preferences. * @param ctx Plugin context used for optional settings presentation. - * @returns when continuation state is registered and available to Session carriers. */ -export async function apply(ctx: Context): Promise { +export function apply(ctx: Context): void { ctx.inject(['settings'], (child) => { child.effect(() => child.settings.configure({ auto: false }, ctx.fiber)) }) - await ctx.sessionProjections.register(continuationProjectionDefinition) } diff --git a/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx b/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx index 8f2b967e06..a1cf51452a 100644 --- a/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx +++ b/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx @@ -29,7 +29,7 @@ import { createUserMessage, LlmAttemptId } from '@deepseek-ai/dsh-llm' import { MutableSessionEventSource } from '@deepseek-ai/dsh-api-session-controller/client' import type { SessionEventWindow } from '@deepseek-ai/dsh-api-session-controller/client' import type { InboxWireState } from '@deepseek-ai/dsh-agent/types' -import { continuationTurn } from './continuation-fixture.ts' +import { continuationTurn } from '../../../api/session-controller/tests/continuation-fixture.ts' declare module '@deepseek-ai/dsh-client-ui-slots' { interface SlotMap { @@ -165,7 +165,7 @@ async function bench(renderQuote = false) { } describe('Conversation inject API', () => { - it.each([['zh', '继续'], ['en', 'continue']])('submits one continuation in the current %s locale', async (locale, message) => { + it.each(['zh', 'en'])('submits empty continuation input in the %s locale', async (locale) => { const b = await bench() onTestFinished(() => b.runtime.dispose()) await b.runtime.sessions.replaceEvents(ROOT, continuationTurn({ kind: 'error', error: { code: 'SERVER', message: 'busy' } })) @@ -181,7 +181,7 @@ describe('Conversation inject API', () => { composer.continueGeneration!(SessionSeq(5)) expect(composer.keyboard!.snapshot.phase).toBe('sending') await vi.waitFor(() => { expect(b.sessionFake.prompt).toHaveBeenCalledOnce() }) - expect(b.sessionFake.prompt.mock.calls[0]?.[0]).toEqual([{ type: 'text', text: message }]) + expect(b.sessionFake.prompt.mock.calls[0]?.[0]).toEqual([]) expect(b.sessionFake.prompt.mock.calls[0]?.[1]).toBe('queue') pending.resolve({ ok: true, value: { accepted: true } }) await vi.waitFor(() => { expect(composer.keyboard!.snapshot.phase).toBe('plain') }) @@ -322,7 +322,7 @@ describe('Conversation inject API', () => { }, ) - it.each(['refused', 'failed'] as const)('retains a visible editable continuation draft when submission is %s', async (outcome) => { + it.each(['refused', 'failed'] as const)('keeps continuation input empty and exposes submission failure when %s', async (outcome) => { const b = await bench() onTestFinished(() => b.runtime.dispose()) await b.runtime.sessions.setProjection(ROOT, 'conversationContinuation', SessionSeq(5)) @@ -335,11 +335,11 @@ describe('Conversation inject API', () => { else b.sessionFake.prompt.mockRejectedValueOnce(new Error('connection lost')) composer.continueGeneration!(SessionSeq(5)) await vi.waitFor(() => { expect(b.sessionFake.prompt).toHaveBeenCalledOnce(); expect(composer.keyboard!.snapshot.phase).toBe('plain') }) - expect(composer.keyboard!.snapshot.draft).toBe('继续') + expect(composer.keyboard!.snapshot.draft).toBe('') if (outcome === 'refused') expect(b.runtime.sessions.binding(ROOT)!.session.getSnapshot().promptError?.error.message).toBe('request refused') else expect(composer.hooks.notices.getSnapshot()).toMatchObject({ level: 'error', text: 'connection lost' }) composer.continueGeneration!(SessionSeq(5)) - expect(b.sessionFake.prompt).toHaveBeenCalledOnce() + await vi.waitFor(() => { expect(b.sessionFake.prompt).toHaveBeenCalledTimes(2) }) }) it('renders literal quote text through the actual Factory and keyed node registration', async () => { diff --git a/packages/client/ui-conversation/tests/config.host.spec.ts b/packages/client/ui-conversation/tests/config.host.spec.ts index 402063cedc..25420eff00 100644 --- a/packages/client/ui-conversation/tests/config.host.spec.ts +++ b/packages/client/ui-conversation/tests/config.host.spec.ts @@ -1,6 +1,6 @@ import SessionStore from '@deepseek-ai/dsh-session' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' -import { Context, FiberState } from '@deepseek-ai/cordis' +import { Context } from '@deepseek-ai/cordis' import { describe, expect, it, onTestFinished } from 'vitest' import * as HostPlugin from '../src/index.ts' import { liveConfig, omitsGeneratedPage } from '../../../settings/settings/tests/live-config.ts' @@ -32,15 +32,3 @@ it('keeps its own instance off the generated Settings pages', () => omitsGenerat await ctx.plugin(SessionProjectionRegistry) return ctx.plugin(HostPlugin) })) - -it('waits for the required projection registry before activation', async () => { - const ctx = new Context() - onTestFinished(() => ctx.fiber.dispose()) - await ctx.plugin(SessionStore) - const fiber = ctx.plugin(HostPlugin) - await ctx.fiber.await() - expect(fiber.state).toBe(FiberState.PENDING) - await ctx.plugin(SessionProjectionRegistry) - await fiber - expect(fiber.state).toBe(FiberState.ACTIVE) -}) diff --git a/packages/client/ui-conversation/tsconfig.host.json b/packages/client/ui-conversation/tsconfig.host.json index cc278423fc..ec30b21bfe 100644 --- a/packages/client/ui-conversation/tsconfig.host.json +++ b/packages/client/ui-conversation/tsconfig.host.json @@ -7,9 +7,7 @@ }, "files": [ "src/submission-settings.ts", - "src/index.ts", - "src/continuation-types.ts", - "src/host/continuation.ts" + "src/index.ts" ], "references": [ { @@ -20,15 +18,6 @@ }, { "path": "../../settings/settings" - }, - { - "path": "../../core/session" - }, - { - "path": "../../session/session-projection" - }, - { - "path": "../../util/brand" } ] } diff --git a/packages/core/agent-loop/README.i18n.yaml b/packages/core/agent-loop/README.i18n.yaml index 33c53afffa..2a3ecdf694 100644 --- a/packages/core/agent-loop/README.i18n.yaml +++ b/packages/core/agent-loop/README.i18n.yaml @@ -51,8 +51,8 @@ en: eea56903c4ff82f5 zh: c0d5abb5767780ed /deepseek-ai-dsh-agent-loop/further-exploration: - en: 7ac519f8e635acbc - zh: 975e514915dc6c9f + en: 687e42aa91c15be5 + zh: 0e2d860391a9ce89 /deepseek-ai-dsh-agent-loop/model-experience: en: 215e7ba838619b7b zh: a311da3843709f90 diff --git a/packages/core/agent-loop/README.md b/packages/core/agent-loop/README.md index 28b862ff9d..9beebb6543 100644 --- a/packages/core/agent-loop/README.md +++ b/packages/core/agent-loop/README.md @@ -147,6 +147,8 @@ The package-level contract is enough for most consumers; read these when you nee ----- +Empty ordinary input remains an identified inbox command, so it can wake a new turn while adding no `user/message` to the model surface. It passes normal pre-step admission; a rejected or removed input does not authorize a model request. Turn and step endings retain their normal success, failure, and cancellation handling. + ## Model Experience ### Complete conversation request diff --git a/packages/core/agent-loop/README.zh.md b/packages/core/agent-loop/README.zh.md index f872d4289e..8bbbd145c5 100644 --- a/packages/core/agent-loop/README.zh.md +++ b/packages/core/agent-loop/README.zh.md @@ -147,6 +147,8 @@ loop 在发布工厂前等待 inbox 与轮次边界注册完成,并将句柄 ----- +空的普通输入仍是带身份的 inbox 控制记录,可以唤醒新轮次,但不向模型消息面添加 `user/message`。它经过正常 pre-step 准入;被拒绝或移除的输入不授权模型请求。轮次和步骤结束沿用正常的成功、失败与取消处理。 + ## 模型体验 ### 完整对话请求 diff --git a/packages/core/agent-loop/src/agent.ts b/packages/core/agent-loop/src/agent.ts index 03c4c0c2bf..619cca96ba 100644 --- a/packages/core/agent-loop/src/agent.ts +++ b/packages/core/agent-loop/src/agent.ts @@ -460,7 +460,7 @@ export class ReactLoopAgent implements Agent { this.session.append('user/message', context, { surfaceOp: 'append' }) contextAdmitted = true } - } else { + } else if (message.source.kind !== 'user' || message.content.length > 0) { this.session.append('user/message', message, { surfaceOp: 'append' }) } } diff --git a/packages/core/agent-loop/tests/request-error.spec.ts b/packages/core/agent-loop/tests/request-error.spec.ts index 5287cb4deb..16c07fcf4e 100644 --- a/packages/core/agent-loop/tests/request-error.spec.ts +++ b/packages/core/agent-loop/tests/request-error.spec.ts @@ -1,5 +1,5 @@ import * as NativeToolExecutor from '@deepseek-ai/dsh-tool-executor-native' -import { describe, expect, it } from 'vitest' +import { describe, expect, it, onTestFinished } from 'vitest' import { Context } from '@deepseek-ai/cordis' import AgentRegistry from '@deepseek-ai/dsh-agent' import AgentLoop from '@deepseek-ai/dsh-agent-loop' @@ -32,6 +32,31 @@ function fail(message: string, code: string): () => never { } describe('agent/request-error', () => { + it('continues repeated failures through empty followups with balanced turns and one admitted user message', async () => { + const adapter = new MockAdapter([fail('busy', 'SERVER'), fail('still busy', 'SERVER'), textResponse('done')]) + const ctx = await harness(adapter) + onTestFinished(() => ctx.fiber.dispose()) + const agent = await ctx.agentLoop.create(SessionId('empty-continuation'), { provider: 'mock', model: 'mock' }) + agent.followup(createUserMessage({ content: [{ type: 'text', text: 'original task' }], source: { kind: 'user' } })) + await agent.whenIdle() + for (let index = 0; index < 2; index++) { + agent.followup(createUserMessage({ content: [], source: { kind: 'user' } })) + await agent.whenIdle() + } + expect(adapter.requests).toHaveLength(3) + for (const request of adapter.requests) { + expect(request.messages.filter(message => message.role === 'user').map(message => message.content)) + .toEqual([[{ type: 'text', text: 'original task' }]]) + } + const events = agent.session.snapshotEvents() + expect(events.filter(event => event.type === 'user/message')).toHaveLength(1) + expect(events.flatMap(event => event.type === 'turn/start' || event.type === 'turn/end' + ? [`${event.type}:${event.data.turn}`] : [])) + .toEqual(['turn/start:1', 'turn/end:1', 'turn/start:2', 'turn/end:2', 'turn/start:3', 'turn/end:3']) + expect(events.flatMap(event => event.type === 'turn/end' ? [event.data.reason.kind] : [])) + .toEqual(['error', 'error', 'completed']) + }) + it('does not offer middleware failures to request recovery', async () => { const adapter = new MockAdapter([textResponse('unused')]) const ctx = await harness(adapter) diff --git a/packages/core/agent/README.i18n.yaml b/packages/core/agent/README.i18n.yaml index 53a219df1a..3aeeb9a406 100644 --- a/packages/core/agent/README.i18n.yaml +++ b/packages/core/agent/README.i18n.yaml @@ -60,8 +60,8 @@ en: e48eae6c7de88cf8 zh: 1f5b37bdb0e53788 /deepseek-ai-dsh-agent/further-exploration: - en: 4ef57cfafa344581 - zh: 73575876bfdc7a18 + en: d9d9bda2357ef857 + zh: 5fa1368d5f8912e2 /deepseek-ai-dsh-agent/model-experience: en: 215e7ba838619b7b zh: a311da3843709f90 diff --git a/packages/core/agent/README.md b/packages/core/agent/README.md index a7433347f8..2f7c830c7c 100644 --- a/packages/core/agent/README.md +++ b/packages/core/agent/README.md @@ -151,6 +151,8 @@ The package-level contract is enough for most consumers; read these when you nee ----- +An ordinary `followup` with `content: []` is an explicit request to start a new turn from committed history. It retains the identified inbox receipt and normal lifecycle without creating a model-visible user message. A pre-step rejection or a decision rewritten to an empty batch still prevents the request. + ## Model Experience ### User, steering, and injected messages diff --git a/packages/core/agent/README.zh.md b/packages/core/agent/README.zh.md index a92b8338e0..1eb86a7a5e 100644 --- a/packages/core/agent/README.zh.md +++ b/packages/core/agent/README.zh.md @@ -151,6 +151,8 @@ await handle.agent.whenIdle() ----- +普通 `followup` 的 `content: []` 明确请求基于已提交历史启动新轮次。它保留带身份的 inbox 回执和正常生命周期,不生成模型可见的用户消息。pre-step 拒绝或改写为空批次的决策仍会阻止请求。 + ## 模型体验 ### 用户、steering 与注入消息 diff --git a/packages/core/agent/src/runtime-types.ts b/packages/core/agent/src/runtime-types.ts index 2b1698e39c..58d883721c 100644 --- a/packages/core/agent/src/runtime-types.ts +++ b/packages/core/agent/src/runtime-types.ts @@ -232,6 +232,7 @@ declare module './types.ts' { /** * Route identified input to an inbox boundary and optionally wake the driver. + * Empty ordinary input drives work without becoming a model-visible message. * Waking input submitted after active cancellation is queued for the next * turn and runs when the aborted activity converges to idle; a `disposed` * cancel leaves it parked. A wake submitted while already idle always opens @@ -245,7 +246,8 @@ declare module './types.ts' { /** * Queue an ordinary follow-up turn and wake the driver. The item becomes the - * sole ordinary message of its own turn. + * sole ordinary message of its own turn. An empty ordinary input starts a + * turn from committed history without appending a model-visible user message. * @param message - identified prompt content and the source that supplied it. */ followup(message: UserMessage): void diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 489c15f56e..a8056e3e2f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4001,15 +4001,9 @@ importers: packages/client/ui-conversation: dependencies: - '@deepseek-ai/dsh-brand': - specifier: workspace:* - version: link:../../util/brand '@deepseek-ai/schemastery': specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery - zod: - specifier: 4.4.3 - version: 4.4.3 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -4029,6 +4023,9 @@ importers: '@deepseek-ai/dsh-attachment': specifier: workspace:* version: link:../../attachment/attachment + '@deepseek-ai/dsh-brand': + specifier: workspace:* + version: link:../../util/brand '@deepseek-ai/dsh-client-file-upload': specifier: workspace:* version: link:../file-upload @@ -4143,6 +4140,9 @@ importers: react-dom: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) + zod: + specifier: 4.4.3 + version: 4.4.3 packages/client/ui-deepseek-model-label-1017: devDependencies: diff --git a/scripts/smoke-python-runtime.py b/scripts/smoke-python-runtime.py index 3141b7bb24..fe17f6f952 100644 --- a/scripts/smoke-python-runtime.py +++ b/scripts/smoke-python-runtime.py @@ -113,7 +113,6 @@ RESTART_SECOND_SESSION_ID = "process-two" RESUME_PROMPT = "Continue the first Python SDK session after restarting the runtime." RESUME_TEXT = "RESUMED_PROCESS_OK" RECOVERY_PROMPT = "Run three todo updates; the scheduler will reject the second call." -RECOVERY_CONTINUE_PROMPT = "Continue after the scheduler failure without repeating any tools." RECOVERY_TEXT = "SCHEDULER_RECOVERY_OK" RECOVERY_SESSION_ID = "scheduler-recovery" RECOVERY_CALL_IDS = ("scheduler-complete", "scheduler-fail", "scheduler-unstarted") @@ -348,7 +347,7 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]: block.get("text") for block in latest.get("content", []) if isinstance(block, dict) and block.get("type") == "text" - and block.get("text") in {RECOVERY_PROMPT, RECOVERY_CONTINUE_PROMPT} + and block.get("text") == RECOVERY_PROMPT ), None) if recovery_prompt == RECOVERY_PROMPT: assert_advertised_tool(body, "todo_write") @@ -358,7 +357,9 @@ def completion_chunks(body: dict[str, object]) -> list[dict[str, object]]: chunks.extend({**chunk, "index": index} for chunk in call[1:-2]) chunks.extend(call[-2:]) return chunks - if recovery_prompt == RECOVERY_CONTINUE_PROMPT: + if any(block.get("tool_use_id") == RECOVERY_CALL_IDS[-1] + for message in messages for block in message.get("content", []) + if isinstance(block, dict) and block.get("type") == "tool_result"): results = [ block for message in messages for block in message.get("content", []) @@ -1909,7 +1910,7 @@ def smoke_sdk_scheduler_recovery(base_url: str, executable: Path, update_snapsho with connect() as harness: failed = harness.run(RECOVERY_PROMPT, session_id=RECOVERY_SESSION_ID) - continued = harness.run(RECOVERY_CONTINUE_PROMPT, session_id=RECOVERY_SESSION_ID) + continued = harness.run([], session_id=RECOVERY_SESSION_ID) results = [failed, continued] if [result.finish_reason for result in results] != ["error", "completed"]: outcomes = [ diff --git a/scripts/snapshots/python-sdk-single-exe/scheduler-recovery/requests.json b/scripts/snapshots/python-sdk-single-exe/scheduler-recovery/requests.json index f83fee366e..41cd02a726 100644 --- a/scripts/snapshots/python-sdk-single-exe/scheduler-recovery/requests.json +++ b/scripts/snapshots/python-sdk-single-exe/scheduler-recovery/requests.json @@ -97,10 +97,6 @@ } ], "is_error": true - }, - { - "type": "text", - "text": "Continue after the scheduler failure without repeating any tools." } ] } diff --git a/scripts/snapshots/python-sdk-single-exe/scheduler-recovery/result.json b/scripts/snapshots/python-sdk-single-exe/scheduler-recovery/result.json index 46704a4ab7..7172a0edd1 100644 --- a/scripts/snapshots/python-sdk-single-exe/scheduler-recovery/result.json +++ b/scripts/snapshots/python-sdk-single-exe/scheduler-recovery/result.json @@ -57,7 +57,6 @@ "turn/start", "agent/inbox/spliced", "step/start", - "user/message", "assistant/message", "step/end", "turn/end" @@ -71,7 +70,6 @@ "session.event", "session.event", "session.event", - "session.event", "session.status" ] } diff --git a/scripts/snapshots/python-sdk-single-exe/scheduler-recovery/session.v4.jsonl b/scripts/snapshots/python-sdk-single-exe/scheduler-recovery/session.v4.jsonl index c679ff36b7..37ccfaa185 100644 --- a/scripts/snapshots/python-sdk-single-exe/scheduler-recovery/session.v4.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/scheduler-recovery/session.v4.jsonl @@ -21,11 +21,10 @@ {"type":"tool/result","data":{"turn":1,"step":1,"message":{"id":"interrupted-tool-result-scheduler-unstarted-19","role":"tool","toolCallId":"scheduler-unstarted","isError":true,"source":{"kind":"tool","callId":"scheduler-unstarted"},"content":[{"type":"text","text":"The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed."}]},"error":{"name":"ToolNotStartedError","code":"TOOL_NOT_STARTED"}},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"error","error":{"message":"Snapshot scheduler preparation failed","code":"UNKNOWN"}}}} -{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Continue after the scheduler failure without repeating any tools."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}} {"type":"turn/start","data":{"turn":2}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":2,"step":1}} -{"type":"user/message","data":{"content":[{"type":"text","text":"Continue after the scheduler failure without repeating any tools."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SCHEDULER_RECOVERY_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"text"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["SCHEDULER_RECOVERY_OK"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SCHEDULER_RECOVERY_OK"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"text"}]}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} diff --git a/snapshots/sdk/sdk.snapshot.ts b/snapshots/sdk/sdk.snapshot.ts index 1a687411fc..ddd859bd38 100644 --- a/snapshots/sdk/sdk.snapshot.ts +++ b/snapshots/sdk/sdk.snapshot.ts @@ -485,11 +485,23 @@ function modelFromSession(log: string): { provider: string; model: string } { function turnActions(log: string): TurnAction[] { const actions: TurnAction[] = [] let current: TurnAction | undefined + let emptyQueued = false for (const record of records(log)) { + if (record.type === 'agent/inbox/spliced') { + const data = record.data as JsonObject | undefined + if (data?.target === 'next-turn' && Array.isArray(data.inserted)) { + emptyQueued ||= data.inserted.some(value => { + const message = value as JsonObject + return (message.source as JsonObject | undefined)?.kind === 'user' + && Array.isArray(message.content) && message.content.length === 0 + }) + } + } if (record.type === 'turn/start') { const data = record.data as JsonObject | undefined if (typeof data?.turn !== 'number') throw new Error('SDK snapshot turn/start has no turn') - current = { turn: data.turn } + current = { turn: data.turn, ...emptyQueued ? { content: [] } : {} } + emptyQueued = false continue } if (record.type === 'user/message' && current !== undefined && current.content === undefined) { @@ -768,7 +780,8 @@ async function runScenario(scenario: CorpusScenario): Promise<{ session = await harness.resumeSession(sessionId) expect(session.id).toBe(sessionId) } - const result = await session.run(materializeInput(action.content, scenario, cwd, liveSessions), { + const input = materializeInput(action.content, scenario, cwd, liveSessions) + const result = await session.run(input, { onNotification: (notification) => { notifications.push(notification) observe(notification) @@ -1117,6 +1130,8 @@ describe('TypeScript SDK snapshots over the jsonrpc runtime', () => { } } }) } if (scenario.name === 'tool-scheduler-recovery') { + expect(records(ordered[0]!.content).filter(event => event.type === 'user/message' + && (event.data['source'] as { kind: string }).kind === 'user')).toHaveLength(1) expect(results).toHaveLength(2) const events = results.flatMap(result => result.events) expect(results[1]?.finalResponse, JSON.stringify(events.filter(event => event.type === 'turn/end'))) diff --git a/snapshots/sdk/tool-scheduler-recovery/notifications.expected.jsonl b/snapshots/sdk/tool-scheduler-recovery/notifications.expected.jsonl index 885df800f9..cfe4388d42 100644 --- a/snapshots/sdk/tool-scheduler-recovery/notifications.expected.jsonl +++ b/snapshots/sdk/tool-scheduler-recovery/notifications.expected.jsonl @@ -19,13 +19,12 @@ {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":20,"time":0,"data":{"turn":1,"step":1}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":21,"time":0,"data":{"turn":1,"reason":{"kind":"error","error":{"message":"Snapshot scheduler preparation failed","code":"UNKNOWN"}}}}}} {"method":"session.status","params":{"sessionId":"{{sessionId}}","status":"idle"}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":22,"time":0,"data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Continue after the scheduler failure without repeating any tools."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":22,"time":0,"data":{"target":"next-turn","start":0,"inserted":[{"content":[],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}}} {"method":"session.status","params":{"sessionId":"{{sessionId}}","status":"running"}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/start","seq":23,"time":0,"data":{"turn":2}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":24,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":25,"time":0,"data":{"turn":2,"step":1}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":26,"time":0,"data":{"content":[{"type":"text","text":"Continue after the scheduler failure without repeating any tools."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":27,"time":0,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SCHEDULER_RECOVERY_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":64,"outputTokens":28},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["SCHEDULER_RECOVERY_OK"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SCHEDULER_RECOVERY_OK"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":64,"outputTokens":28}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":28,"time":0,"data":{"turn":2,"step":1}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":29,"time":0,"data":{"turn":2,"reason":{"kind":"completed"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":26,"time":0,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SCHEDULER_RECOVERY_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":64,"outputTokens":28},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["SCHEDULER_RECOVERY_OK"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SCHEDULER_RECOVERY_OK"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":64,"outputTokens":28}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":27,"time":0,"data":{"turn":2,"step":1}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":28,"time":0,"data":{"turn":2,"reason":{"kind":"completed"}}}}} {"method":"session.status","params":{"sessionId":"{{sessionId}}","status":"idle"}} diff --git a/snapshots/sdk/tool-scheduler-recovery/session.v4.jsonl b/snapshots/sdk/tool-scheduler-recovery/session.v4.jsonl index 1eb28fe0c1..31eb0f7d38 100644 --- a/snapshots/sdk/tool-scheduler-recovery/session.v4.jsonl +++ b/snapshots/sdk/tool-scheduler-recovery/session.v4.jsonl @@ -21,11 +21,10 @@ {"type":"tool/result","data":{"turn":1,"step":1,"message":{"id":"interrupted-tool-result-scheduler-unstarted-19","role":"tool","toolCallId":"scheduler-unstarted","isError":true,"source":{"kind":"tool","callId":"scheduler-unstarted"},"content":[{"type":"text","text":"The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed."}]},"error":{"name":"ToolNotStartedError","code":"TOOL_NOT_STARTED"}},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"error","error":{"message":"Snapshot scheduler preparation failed","code":"UNKNOWN"}}}} -{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"Continue after the scheduler failure without repeating any tools."}],"source":{"kind":"user"},"role":"user","id":"{{message:6}}"}]}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[],"source":{"kind":"user"},"role":"user","id":"{{message:6}}"}]}} {"type":"turn/start","data":{"turn":2}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":2,"step":1}} -{"type":"user/message","data":{"content":[{"type":"text","text":"Continue after the scheduler failure without repeating any tools."}],"source":{"kind":"user"},"role":"user","id":"{{message:6}}"},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SCHEDULER_RECOVERY_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":64,"outputTokens":28},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["SCHEDULER_RECOVERY_OK"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SCHEDULER_RECOVERY_OK"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":64,"outputTokens":28}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} diff --git a/snapshots/web/live-interactions/continued.expected.md b/snapshots/web/live-interactions/continued.expected.md index 1eb97847c2..0b4bcd0205 100644 --- a/snapshots/web/live-interactions/continued.expected.md +++ b/snapshots/web/live-interactions/continued.expected.md @@ -19,7 +19,7 @@ - status: - text: This turn failedupstream 503 - code: SERVER -- text: plan Plan mode on. Use /plan off to leave. plan Plan mode off. continue {{clock}} +- text: plan Plan mode on. Use /plan off to leave. plan Plan mode off. - button "Copy" - status: Completed - button "Completed in {{duration}}" From 4a4aabaa57dd3203d1424f97d91fb7361d77dcea Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Mon, 26 Oct 2026 22:16:24 +0800 Subject: [PATCH 05/65] fix(core): align empty continuation checks and generated references --- docs/architecture.i18n.yaml | 6 ++-- docs/architecture.md | 4 +-- docs/architecture.zh.md | 4 +-- docs/event-producer-consumer.md | 32 +++++++++---------- docs/event-producer-consumer.zh.md | 32 +++++++++---------- docs/persistence-catalog.i18n.yaml | 8 ++--- docs/persistence-catalog.md | 4 +-- docs/persistence-catalog.zh.md | 4 +-- docs/persistence-schema.json | 4 +-- docs/subsystems/core.md | 4 ++- docs/subsystems/core.zh.md | 4 ++- .../api/session-controller/README.i18n.yaml | 4 +-- packages/api/session-controller/README.md | 2 +- packages/api/session-controller/README.zh.md | 2 +- .../api/session-controller/src/commands.ts | 2 +- .../client/ui-conversation/README.i18n.yaml | 4 +-- packages/client/ui-conversation/README.md | 2 +- packages/client/ui-conversation/README.zh.md | 2 +- .../src/client/input/facade.ts | 1 + .../tests/apply-inject.client.spec.tsx | 2 +- packages/core/agent-loop/README.i18n.yaml | 4 +-- packages/core/agent-loop/README.md | 2 +- packages/core/agent-loop/README.zh.md | 2 +- packages/core/agent/README.i18n.yaml | 4 +-- packages/core/agent/README.md | 2 +- packages/core/agent/README.zh.md | 2 +- 26 files changed, 72 insertions(+), 71 deletions(-) diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index d1e20ef6b6..e9bb6328e2 100644 --- a/docs/architecture.i18n.yaml +++ b/docs/architecture.i18n.yaml @@ -19,13 +19,13 @@ zh: 70d87a12c43e042a /deepseek-harness-architecture/core-packages: en: 20ef882e44b06a10 - zh: 0d69c1500650868f + zh: c10c3934ed5570e9 /deepseek-harness-architecture/events: en: 693407efb233f961 zh: 37b9dd0677d4405c /deepseek-harness-architecture/turn-flow: - en: 22964b8327adc3f6 - zh: c86d765e21430405 + en: adffae46f8181328 + zh: 1009d3a5934cd1ba /deepseek-harness-architecture/session-log: en: 8a7f19b95d426c99 zh: d5bb0538404e20ea diff --git a/docs/architecture.md b/docs/architecture.md index ed35ab94af..cd561ac353 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -107,7 +107,7 @@ turn/end Factory setup follows SessionStore entry and `session/created`, preceding `agent/created`; closing events commit before removal. -One inbox feeds the driver; injected context waits for a waking message. AgentLoop’s durable `inbox` projection exposes pending input without live Agents. +The durable `inbox` projection exposes pending input. Empty ordinary input starts a turn without a model-visible message; injected context waits for waking input. Before assembly, the loop snapshots Session model selection in `AssembleContext.modelSelection` and `agent/pre-step.modelSelection`; retries retain it; omitted reasoning effort uses adapter defaults. `agent/pre-step` listeners accept, rewrite, or reject claimed messages; a rejected or empty first claim closes a durable turn without a step. An enter decision may set `startsRequestSeries`: the loop logs a fresh `request/header` (reason `series`, or `change` with `startsSeries: true` when the envelope also changed). Wrapping listeners preserve that declaration with `{ ...decision, messages }`. After assembly and `step/start`, the deprecated `agent/request` hook adjusts controls without changing the provider, model, or effort selection, and `prepareCall()` binds the adapter before the system prompt and accepted users are committed; cancellation during either async phase commits neither. Prepared call capabilities govern prompt admission, not preceding `request/context`. Each attempt refreshes registered runtime facts, reconciles the bound prompt, admits users once, and restores removed runtime context, logs header/context as needed, and freezes the request before streaming the bound call. Retries do not repeat assembly or `agent/pre-step`. Surface replacements and image-offload decisions after attachment start a new request series, including during the first resumed pre-step; unchanged resume continues the series. The system prompt travels as `system/message` history: empty text clears it, capable routes append updates, and other routes consolidate it at the first system node. [Agent-loop](../packages/core/agent-loop/README.md#understand-the-implementation) owns prompt admission and replacement details. @@ -119,8 +119,6 @@ Details: the [sequence diagram](agent-lifecycle.md), the [tool pipeline](tool-ex [`ctx.tools.execute()`](subsystems/tools.md) receives raw batches and concurrency limits. -An identified empty ordinary input starts a new turn from committed history without creating a model-visible user message. It remains logged in the inbox, passes normal pre-step admission, and closes through the same turn and step lifecycle. - ## Session log The session log is the source of the context the model sees. `deriveMessages()` projects model history from it. Each `assistant/message` embeds the exact compact timed stream that produced its assembled content; `assistant/attempt` retains settled failed, retried, cancelled, and stream-error attempts without adding model history. Fork, resume, transcripts, telemetry, and persistence all derive from these durable settlements, while live UI incrementality comes from `agent/assistant-stream`; a hard process loss before settlement leaves no durable attempt stream ([decision](../.agents/notes/implemented/architecture/2026-09-01-v2-embedded-assistant-streams.md)). diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 64c3be1ddd..02c7423e56 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -66,8 +66,6 @@ Electron 使用 Electron Node 模式启动私有 Desktop Host。Host 调用共 -带身份的空普通输入基于已提交历史启动新轮次,不生成模型可见的用户消息。它仍记录在 inbox 中,经过正常 pre-step 准入,并通过相同的轮次和步骤生命周期闭合。 - ## 事件 事件就是扩展点,而选对事件域是大多数改动的第一个决定。 @@ -113,7 +111,7 @@ turn/end 工厂 setup 在 Session 进入 SessionStore 并完成 `session/created` 之后、`agent/created` 之前运行;收尾事件在移除之前提交。 -输入通过同一个 inbox 到达驱动器;注入的上下文等待一条唤醒消息。AgentLoop 的持久 `inbox` 投影使待处理输入在没有活跃 Agent 时仍可读取。 +持久 `inbox` 投影公开待消费输入。空普通输入启动轮次而不新增模型可见消息;注入上下文等待唤醒输入。 循环在组装前将 Session 模型选择快照到 `AssembleContext.modelSelection` 与 `agent/pre-step.modelSelection`;重试保留该选择,省略推理强度时使用适配器默认值。`agent/pre-step` 监听器接纳、改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。组装与 `step/start` 之后,已弃用的 `agent/request` hook 在不改变提供方、模型或推理强度选择的前提下调整控制参数,`prepareCall()` 绑定适配器,再提交系统提示词与已接纳用户消息;在任一异步阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试刷新已注册运行时事实、协调绑定的提示词、仅接纳一次用户消息并恢复被移除的运行时上下文、按需记录 header/context,再冻结请求并通过绑定调用发起流式请求。重试不重复组装或 `agent/pre-step`。附接后的 surface 替换和图片省略决定开启新请求序列,包括恢复后的首次 pre-step 中发生的替换;未变化的恢复延续序列。系统提示词通过 `system/message` 历史传递:空文本清除提示词,具备能力的路由追加更新,其他路由将其归并到首个系统节点。[Agent-loop](../packages/core/agent-loop/README.zh.md#understand-the-implementation) 说明提示词准入与替换的细节。 diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 4ae036085f..c2f38b3416 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -12,22 +12,22 @@ Application listeners are outside this package matrix. Desktop Host subscribes t | --- | --- | --- | --- | --- | | `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:199`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | | `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:66`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | [`cua`](../packages/experimental/cua), `remotes` | -| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:399`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` | -| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | -| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team` | -| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:429`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) | -| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:328`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) | -| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:336`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) | -| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:317`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) | -| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:350`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`action-cards`](../packages/experimental/action-cards), [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | -| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:389`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) | -| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:309`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` | -| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:417`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) | -| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:689`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:669`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:696`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:675`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:682`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:401`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` | +| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:292`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:301`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team` | +| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:431`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) | +| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:330`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) | +| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:338`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) | +| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:319`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) | +| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:352`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`action-cards`](../packages/experimental/action-cards), [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | +| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:391`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) | +| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:311`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` | +| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:419`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:691`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:671`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:698`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:677`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:684`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:55`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) | | `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:87`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes`, `server` | | `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | - | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index a385c1f64a..ee937cf3b9 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -14,22 +14,22 @@ | --- | --- | --- | --- | --- | | `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:199`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | | `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:66`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | [`cua`](../packages/experimental/cua), `remotes` | -| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:399`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` | -| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | -| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:299`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team` | -| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:429`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) | -| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:328`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) | -| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:336`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) | -| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:317`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) | -| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:350`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`action-cards`](../packages/experimental/action-cards), [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | -| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:389`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) | -| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:309`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` | -| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:417`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) | -| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:689`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:669`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:696`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:675`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:682`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:401`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`headless`](../packages/bundle/headless), `session-controller` | +| `agent/created` | `serial` | [`packages/core/agent/src/runtime-types.ts:292`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`cua`](../packages/experimental/cua), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`loader-smoke`](../packages/test-support/loader-smoke), [`permission-presets`](../packages/interaction/permission-presets), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:301`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`action-cards`](../packages/experimental/action-cards), [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`creation-recommendations`](../packages/experimental/creation-recommendations), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`subagent`](../packages/subagent/subagent), `tool-agent-team` | +| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:431`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`deepseek-account`](../packages/credentials/deepseek-account), [`goal-round-driver`](../packages/goal/goal-round-driver), [`headless`](../packages/bundle/headless), `server`, `session-controller`, [`session-telemetry`](../packages/session/session-telemetry), [`subagent`](../packages/subagent/subagent) | +| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:330`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) | +| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:338`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) | +| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:319`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) | +| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:352`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`action-cards`](../packages/experimental/action-cards), [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), `claude-code-mods`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `session-controller`, [`session-reference`](../packages/context/session-reference), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-skill`](../packages/skill/tool-skill) | +| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:391`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) | +| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:311`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` | +| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:419`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) | +| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:691`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:671`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:698`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:677`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:684`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:55`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) | | `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:87`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes`, `server` | | `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | - | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index 280eda500e..0774d434a7 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -1350,8 +1350,8 @@ en: 2786fef560851bb4 zh: 5d787475356b2606 /session-persistence-event-catalog/resolved-persistence-types/string: - en: 1e12d380c4988d40 - zh: 14a781104e00f360 + en: 1e484bd12747db9f + zh: 1e6fc6aa9066a702 /session-persistence-event-catalog/resolved-persistence-types/string~2: en: 0089620bf4638263 zh: 1922bef1713094bb @@ -1857,8 +1857,8 @@ en: 5a0bbf4c76266ac5 zh: 0b20854f90f9d74d /session-persistence-event-catalog/resolved-persistence-types/kind-user: - en: e98b716228feb157 - zh: 2bb593ae9cd0ce0d + en: 3d3ae20cba45bae8 + zh: 04ad200556cd8ced /session-persistence-event-catalog/resolved-persistence-types/kind-user-approval: en: a52d2dfbaf844127 zh: 4c5306ca40f20873 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 97cfa4bd63..a498e62e2e 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -5601,7 +5601,7 @@ Array of `number`. SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b` -Sources: [`packages/api/session-controller/src/types.ts:386`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:21`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:33`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:9`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts) +Sources: [`packages/api/session-controller/src/types.ts:388`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:21`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:33`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:9`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts) `string` @@ -7657,7 +7657,7 @@ Sources: [`packages/core/tools/src/index.ts:28`](../packages/core/tools/src/inde SHA-256: `6dacb1584f2bfbe209dc6b6f578d51da6d55595f64d0e63c727c98f359278def` -Sources: [`packages/api/session-controller/src/types.ts:395`](../packages/api/session-controller/src/types.ts) +Sources: [`packages/api/session-controller/src/types.ts:397`](../packages/api/session-controller/src/types.ts) | Property | Presence | Type | |---|---|---| diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index c1dc641ae7..d75ff6f2b1 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -5603,7 +5603,7 @@ SHA-256: `5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78` SHA-256: `4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b` -来源:[`packages/api/session-controller/src/types.ts:386`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:21`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:33`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:9`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts) +来源:[`packages/api/session-controller/src/types.ts:388`](../packages/api/session-controller/src/types.ts) · [`packages/attachment/attachment/src/brand.ts:6`](../packages/attachment/attachment/src/brand.ts) · [`packages/compaction/compaction/src/brand.ts:4`](../packages/compaction/compaction/src/brand.ts) · [`packages/core/session/src/types.ts:21`](../packages/core/session/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:21`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:33`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/agent-team/src/types.ts:9`](../packages/experimental/agent-team/src/types.ts) · [`packages/experimental/webhook/src/brand.ts:12`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:6`](../packages/experimental/webhook/src/brand.ts) · [`packages/experimental/webhook/src/brand.ts:9`](../packages/experimental/webhook/src/brand.ts) · [`packages/feedback/message-feedback/src/types.ts:14`](../packages/feedback/message-feedback/src/types.ts) · [`packages/goal/goal/src/types.ts:17`](../packages/goal/goal/src/types.ts) · [`packages/interaction/commands/src/brand.ts:31`](../packages/interaction/commands/src/brand.ts) · [`packages/interaction/user-approval/src/types.ts:17`](../packages/interaction/user-approval/src/types.ts) · [`packages/jobs/jobs/src/brand.ts:19`](../packages/jobs/jobs/src/brand.ts) · [`packages/llm/llm-retry/src/brand.ts:4`](../packages/llm/llm-retry/src/brand.ts) · [`packages/llm/llm/src/brand.ts:16`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:31`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:43`](../packages/llm/llm/src/brand.ts) · [`packages/llm/llm/src/brand.ts:67`](../packages/llm/llm/src/brand.ts) · [`packages/schedule/schedule/src/types.ts:14`](../packages/schedule/schedule/src/types.ts) · [`packages/session/session-title/src/index.ts:42`](../packages/session/session-title/src/index.ts) · [`packages/session/session-title/src/types.ts:17`](../packages/session/session-title/src/types.ts) · [`packages/workflow/workflow/src/types.ts:13`](../packages/workflow/workflow/src/types.ts) `string` @@ -7659,7 +7659,7 @@ SHA-256: `8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543` SHA-256: `6dacb1584f2bfbe209dc6b6f578d51da6d55595f64d0e63c727c98f359278def` -来源:[`packages/api/session-controller/src/types.ts:395`](../packages/api/session-controller/src/types.ts) +来源:[`packages/api/session-controller/src/types.ts:397`](../packages/api/session-controller/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| diff --git a/docs/persistence-schema.json b/docs/persistence-schema.json index c6c15a7c90..70f64325e4 100644 --- a/docs/persistence-schema.json +++ b/docs/persistence-schema.json @@ -251,7 +251,7 @@ {"digest":"4b259f7f8f0a44ad5e90b70a73553ced1df27012d126921d397ff403b6c86731","names":["packages/llm/llm/src/message.ts#ToolResultMessage"],"sources":["packages/llm/llm/src/message.ts:173"]}, {"digest":"4b7ddaf3710b88ff9b8df66304f5f97bf03688fd19730eedc2e9535203c820ea","names":[],"sources":[]}, {"digest":"4bb451330ff699f36b45539ceb0eb2606ead72084e2c5bb637c36cc80e2252db","names":[],"sources":["packages/core/session/src/types.ts:198"]}, - {"digest":"4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b","names":["packages/api/session-controller/src/types.ts#SessionRequestId","packages/attachment/attachment/src/brand.ts#AttachmentId","packages/compaction/compaction/src/brand.ts#CompactionId","packages/core/session/src/types.ts#SessionId","packages/experimental/agent-team/src/types.ts#TeamId","packages/experimental/agent-team/src/types.ts#TeamMessageId","packages/experimental/agent-team/src/types.ts#TeamTaskId","packages/experimental/webhook/src/brand.ts#WebhookDeliveryId","packages/experimental/webhook/src/brand.ts#WebhookRuleId","packages/experimental/webhook/src/brand.ts#WebhookSourceId","packages/feedback/message-feedback/src/types.ts#MessageFeedbackVersion","packages/goal/goal/src/types.ts#GoalId","packages/interaction/commands/src/brand.ts#CommandId","packages/interaction/user-approval/src/types.ts#ApprovalRequestId","packages/jobs/jobs/src/brand.ts#JobId","packages/llm/llm-retry/src/brand.ts#RetryId","packages/llm/llm/src/brand.ts#MessageId","packages/llm/llm/src/brand.ts#ProviderRequestId","packages/llm/llm/src/brand.ts#ReasoningEffortId","packages/llm/llm/src/brand.ts#ToolCallId","packages/schedule/schedule/src/types.ts#ScheduleId","packages/session/session-title/src/index.ts#SessionTitleProviderId","packages/session/session-title/src/types.ts#SessionTitleProviderId","packages/workflow/workflow/src/types.ts#WorkflowRunId"],"sources":["packages/api/session-controller/src/types.ts:386","packages/attachment/attachment/src/brand.ts:6","packages/compaction/compaction/src/brand.ts:4","packages/core/session/src/types.ts:21","packages/experimental/agent-team/src/types.ts:21","packages/experimental/agent-team/src/types.ts:33","packages/experimental/agent-team/src/types.ts:9","packages/experimental/webhook/src/brand.ts:12","packages/experimental/webhook/src/brand.ts:6","packages/experimental/webhook/src/brand.ts:9","packages/feedback/message-feedback/src/types.ts:14","packages/goal/goal/src/types.ts:17","packages/interaction/commands/src/brand.ts:31","packages/interaction/user-approval/src/types.ts:17","packages/jobs/jobs/src/brand.ts:19","packages/llm/llm-retry/src/brand.ts:4","packages/llm/llm/src/brand.ts:16","packages/llm/llm/src/brand.ts:31","packages/llm/llm/src/brand.ts:43","packages/llm/llm/src/brand.ts:67","packages/schedule/schedule/src/types.ts:14","packages/session/session-title/src/index.ts:42","packages/session/session-title/src/types.ts:17","packages/workflow/workflow/src/types.ts:13"]}, + {"digest":"4cd48f3e5108bd6ebedf7301c3638839b8b2b9db8dbd493974177fbdeb91a01b","names":["packages/api/session-controller/src/types.ts#SessionRequestId","packages/attachment/attachment/src/brand.ts#AttachmentId","packages/compaction/compaction/src/brand.ts#CompactionId","packages/core/session/src/types.ts#SessionId","packages/experimental/agent-team/src/types.ts#TeamId","packages/experimental/agent-team/src/types.ts#TeamMessageId","packages/experimental/agent-team/src/types.ts#TeamTaskId","packages/experimental/webhook/src/brand.ts#WebhookDeliveryId","packages/experimental/webhook/src/brand.ts#WebhookRuleId","packages/experimental/webhook/src/brand.ts#WebhookSourceId","packages/feedback/message-feedback/src/types.ts#MessageFeedbackVersion","packages/goal/goal/src/types.ts#GoalId","packages/interaction/commands/src/brand.ts#CommandId","packages/interaction/user-approval/src/types.ts#ApprovalRequestId","packages/jobs/jobs/src/brand.ts#JobId","packages/llm/llm-retry/src/brand.ts#RetryId","packages/llm/llm/src/brand.ts#MessageId","packages/llm/llm/src/brand.ts#ProviderRequestId","packages/llm/llm/src/brand.ts#ReasoningEffortId","packages/llm/llm/src/brand.ts#ToolCallId","packages/schedule/schedule/src/types.ts#ScheduleId","packages/session/session-title/src/index.ts#SessionTitleProviderId","packages/session/session-title/src/types.ts#SessionTitleProviderId","packages/workflow/workflow/src/types.ts#WorkflowRunId"],"sources":["packages/api/session-controller/src/types.ts:388","packages/attachment/attachment/src/brand.ts:6","packages/compaction/compaction/src/brand.ts:4","packages/core/session/src/types.ts:21","packages/experimental/agent-team/src/types.ts:21","packages/experimental/agent-team/src/types.ts:33","packages/experimental/agent-team/src/types.ts:9","packages/experimental/webhook/src/brand.ts:12","packages/experimental/webhook/src/brand.ts:6","packages/experimental/webhook/src/brand.ts:9","packages/feedback/message-feedback/src/types.ts:14","packages/goal/goal/src/types.ts:17","packages/interaction/commands/src/brand.ts:31","packages/interaction/user-approval/src/types.ts:17","packages/jobs/jobs/src/brand.ts:19","packages/llm/llm-retry/src/brand.ts:4","packages/llm/llm/src/brand.ts:16","packages/llm/llm/src/brand.ts:31","packages/llm/llm/src/brand.ts:43","packages/llm/llm/src/brand.ts:67","packages/schedule/schedule/src/types.ts:14","packages/session/session-title/src/index.ts:42","packages/session/session-title/src/types.ts:17","packages/workflow/workflow/src/types.ts:13"]}, {"digest":"4e011753f8263f436efaabfbb3b4db53a98fc55342c5ded2daf7208b217be159","names":[],"sources":[]}, {"digest":"4e44c9118152f6829634bfd508e47669ab0497caffa380c94b4be91f9390d35d","names":[],"sources":[]}, {"digest":"4e887768586528565381dadbddee6cef555874148089f4579e0b4b1ad096fc9c","names":["packages/llm/llm/src/types.ts#TextBlock"],"sources":["packages/llm/llm/src/types.ts:62","packages/web/web-search-deepseek/src/provider.ts:66"]}, @@ -334,7 +334,7 @@ {"digest":"6ccb6bc423fb3952458abf9c1b1c3284208b7e660344b612cc55da773d61afe4","names":[],"sources":[]}, {"digest":"6cda0d6d3b4336d83998fe580628426df81c2afbab5d768b8813b56d569120e8","names":[],"sources":[]}, {"digest":"6cf082d0fb6034dea61636787ff0ddf43b6e59aad71e644435a479bc9b83ad65","names":[],"sources":[]}, - {"digest":"6dacb1584f2bfbe209dc6b6f578d51da6d55595f64d0e63c727c98f359278def","names":[],"sources":["packages/api/session-controller/src/types.ts:395"]}, + {"digest":"6dacb1584f2bfbe209dc6b6f578d51da6d55595f64d0e63c727c98f359278def","names":[],"sources":["packages/api/session-controller/src/types.ts:397"]}, {"digest":"6e52dd55a1d48954ed5464ec46fe34d9eb65f709baabc9561e7a279910b0c406","names":[],"sources":["packages/preset/agent-preset-registry/src/session.ts:31"]}, {"digest":"6ea608bdc8a7a63d3d23b84c49721325094db88d136482e43a820945295eee3e","names":[],"sources":[]}, {"digest":"6ebcc2dbec9f2dabca9f668ae6d39e877899b180740570f453409db339221df3","names":[],"sources":[]}, diff --git a/docs/subsystems/core.md b/docs/subsystems/core.md index 86df19aa94..d11e03488a 100644 --- a/docs/subsystems/core.md +++ b/docs/subsystems/core.md @@ -104,6 +104,7 @@ interface Agent { /** * Route identified input to an inbox boundary and optionally wake the driver. + * Empty ordinary input drives work without becoming a model-visible message. * Waking input submitted after active cancellation is queued for the next * turn and runs when the aborted activity converges to idle; a `disposed` * cancel leaves it parked. A wake submitted while already idle always opens @@ -117,7 +118,8 @@ interface Agent { /** * Queue an ordinary follow-up turn and wake the driver. The item becomes the - * sole ordinary message of its own turn. + * sole ordinary message of its own turn. An empty ordinary input starts a + * turn from committed history without appending a model-visible user message. * @param message - identified prompt content and the source that supplied it. */ followup(message: UserMessage): void diff --git a/docs/subsystems/core.zh.md b/docs/subsystems/core.zh.md index db4bb206f5..7e4c682f69 100644 --- a/docs/subsystems/core.zh.md +++ b/docs/subsystems/core.zh.md @@ -108,6 +108,7 @@ interface Agent { /** * Route identified input to an inbox boundary and optionally wake the driver. + * Empty ordinary input drives work without becoming a model-visible message. * Waking input submitted after active cancellation is queued for the next * turn and runs when the aborted activity converges to idle; a `disposed` * cancel leaves it parked. A wake submitted while already idle always opens @@ -121,7 +122,8 @@ interface Agent { /** * Queue an ordinary follow-up turn and wake the driver. The item becomes the - * sole ordinary message of its own turn. + * sole ordinary message of its own turn. An empty ordinary input starts a + * turn from committed history without appending a model-visible user message. * @param message - identified prompt content and the source that supplied it. */ followup(message: UserMessage): void diff --git a/packages/api/session-controller/README.i18n.yaml b/packages/api/session-controller/README.i18n.yaml index 78f64d3acc..a1daccc8f0 100644 --- a/packages/api/session-controller/README.i18n.yaml +++ b/packages/api/session-controller/README.i18n.yaml @@ -24,8 +24,8 @@ en: 954923654eebe2dc zh: 6bd9a7d2ea965aed /session-controller/configuration: - en: 2a91d9f904e90756 - zh: 8affb15ccc571829 + en: 94cb7b73b385b320 + zh: 1f3bc1e6552a01a3 /session-controller/model-experience: en: 30b917bf822bb4df zh: b0b023b5b526c317 diff --git a/packages/api/session-controller/README.md b/packages/api/session-controller/README.md index 1b11d04106..371ee3c13e 100644 --- a/packages/api/session-controller/README.md +++ b/packages/api/session-controller/README.md @@ -110,9 +110,9 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a Blockquotes travel as ordinary prompt text without quote metadata. Local submission echoes and durable messages use the same text rendering rules; queue edits replace content while retaining prompt correlation. - An empty `prompt.content` array requests a new turn only for an eligible failed, idle ordinary Session in queue mode. Pending human input prevents admission; non-waking injected context is retained. `conversationContinuation` supplies the shared six-code manual policy, independently of provider retry configuration. The input is logged through the inbox for request-id deduplication, but creates no model-visible user message. Whitespace-only text parts remain invalid. + ## Model Experience None, as invoked Agent commands own any model-visible effect. diff --git a/packages/api/session-controller/README.zh.md b/packages/api/session-controller/README.zh.md index cb8a1ba818..9e698a26e7 100644 --- a/packages/api/session-controller/README.zh.md +++ b/packages/api/session-controller/README.zh.md @@ -110,9 +110,9 @@ GUI 模型选择要求确切提供方/模型对出现在可用目录中;不 Blockquote 作为普通 prompt 文本传递,不携带 quote 元数据。本地提交回显与持久消息使用同一文本渲染规则;队列编辑替换 content,同时保留 prompt 关联信息。 - 空的 `prompt.content` 数组仅在普通 Session 空闲、最新失败符合继续条件且使用 queue 模式时请求新轮次。待消费的用户输入阻止准入,非唤醒的注入上下文保留。`conversationContinuation` 提供共用的六码手动策略,独立于提供方重试配置。输入通过 inbox 持久记录,用于 request-id 去重,但不生成模型可见的用户消息。仅含空白的文本块仍无效。 + ## 模型体验 无;任何模型可见效果都由被调用的 Agent 命令负责。 diff --git a/packages/api/session-controller/src/commands.ts b/packages/api/session-controller/src/commands.ts index 0b62ff724a..b43333ebc0 100644 --- a/packages/api/session-controller/src/commands.ts +++ b/packages/api/session-controller/src/commands.ts @@ -320,7 +320,7 @@ export class SessionCommandController { const inbox = agent.inbox if (request.mode !== 'queue' || this.continuation.stateOf(agent.session).ending === null || inbox.nextTurn.length > 0 || inbox.nextStep.some(message => message.source.kind === 'user')) { - throw new RemoteError('session/agent-busy', 'generation continuation is unavailable', {}) + throw new RemoteError('session/agent-busy', 'generation continuation is unavailable', { reason: 'NO_CONTINUABLE_FAILURE_OR_PENDING_INPUT' }) } agent.followup(createUserMessage({ content: [], source })) return { accepted: true as const } diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index 6ee1b33613..8507f1dd99 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/README.i18n.yaml @@ -33,8 +33,8 @@ en: 6a7ae99570c9c007 zh: 26e86063e97d82c7 /deepseek-ai-dsh-client-ui-conversation/model-experience/manual-continuation-request/what-the-model-sees: - en: b1376c710fa71ce3 - zh: e751e1a490c36bd8 + en: cfd91cdd0132732b + zh: 298d1ce1b973e7b0 /deepseek-ai-dsh-client-ui-conversation/model-experience/manual-continuation-request/token-effect: en: a5bbf55d50568fa2 zh: 6c43af211055db93 diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index 4a408c5950..8955b71b31 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -193,7 +193,7 @@ The selector must be a pure function of the owner currency. Its non-null return #### What the model sees -The play action starts a new request from committed Session history. Its empty input is a durable inbox command, not a model-visible user message; it adds no localized or hidden continuation instruction. Pending injected context follows ordinary admission. +The play action starts a new request from committed Session history. Its `content: []` input is a durable inbox command, not a model-visible user message; it adds no localized or hidden continuation instruction. Pending injected context follows ordinary admission. #### Token effect diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index 85df62d84b..65c9feda93 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -193,7 +193,7 @@ selector 必须是 owner currency 的纯函数。非 null 返回值作为 `match #### 模型看到什么 -播放按钮基于已提交的 Session 历史发起新请求。空输入作为持久 inbox 控制记录,不成为模型可见的用户消息;不会添加本地化或隐藏的继续指令。待注入上下文遵循普通准入流程。 +播放按钮基于已提交的 Session 历史发起新请求。`content: []` 作为持久 inbox 控制记录,不成为模型可见的用户消息;不会添加本地化或隐藏的继续指令。待注入上下文遵循普通准入流程。 #### Token 影响 diff --git a/packages/client/ui-conversation/src/client/input/facade.ts b/packages/client/ui-conversation/src/client/input/facade.ts index e82acfdd35..cb8472f746 100644 --- a/packages/client/ui-conversation/src/client/input/facade.ts +++ b/packages/client/ui-conversation/src/client/input/facade.ts @@ -387,6 +387,7 @@ export class SessionInputShell implements SessionInput { * out from the machine; this method only feeds the event. Lock entry * (adjudicating/submitting/sending) force-closes the transient layers: the * popup dismisses and the menu tracks frozen. + * @param allowEmpty - explicitly admit empty continuation input through the default sink. */ submit(mode: InputSubmitMode = 'queue', source?: 'click' | 'enter', allowEmpty = false): void { if (this.disposed) return diff --git a/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx b/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx index a1cf51452a..58dff85859 100644 --- a/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx +++ b/packages/client/ui-conversation/tests/apply-inject.client.spec.tsx @@ -234,7 +234,7 @@ describe('Conversation inject API', () => { expect(source.getSnapshot()).toBe(5) composer.continueGeneration!(SessionSeq(5)) await vi.waitFor(() => { expect(b.sessionFake.prompt).toHaveBeenCalledOnce() }) - expect(b.sessionFake.prompt.mock.calls[0]?.[0]).toEqual([{ type: 'text', text: '继续' }]) + expect(b.sessionFake.prompt.mock.calls[0]?.[0]).toEqual([]) expect(notified).toHaveBeenCalledTimes(3) }) diff --git a/packages/core/agent-loop/README.i18n.yaml b/packages/core/agent-loop/README.i18n.yaml index 2a3ecdf694..5b88c245a3 100644 --- a/packages/core/agent-loop/README.i18n.yaml +++ b/packages/core/agent-loop/README.i18n.yaml @@ -51,8 +51,8 @@ en: eea56903c4ff82f5 zh: c0d5abb5767780ed /deepseek-ai-dsh-agent-loop/further-exploration: - en: 687e42aa91c15be5 - zh: 0e2d860391a9ce89 + en: bd0b5d56ebef7e23 + zh: 52911b993bc41afb /deepseek-ai-dsh-agent-loop/model-experience: en: 215e7ba838619b7b zh: a311da3843709f90 diff --git a/packages/core/agent-loop/README.md b/packages/core/agent-loop/README.md index 9beebb6543..b18484ad82 100644 --- a/packages/core/agent-loop/README.md +++ b/packages/core/agent-loop/README.md @@ -146,9 +146,9 @@ The package-level contract is enough for most consumers; read these when you nee ----- - Empty ordinary input remains an identified inbox command, so it can wake a new turn while adding no `user/message` to the model surface. It passes normal pre-step admission; a rejected or removed input does not authorize a model request. Turn and step endings retain their normal success, failure, and cancellation handling. + ## Model Experience ### Complete conversation request diff --git a/packages/core/agent-loop/README.zh.md b/packages/core/agent-loop/README.zh.md index 8bbbd145c5..54395d27dc 100644 --- a/packages/core/agent-loop/README.zh.md +++ b/packages/core/agent-loop/README.zh.md @@ -146,9 +146,9 @@ loop 在发布工厂前等待 inbox 与轮次边界注册完成,并将句柄 ----- - 空的普通输入仍是带身份的 inbox 控制记录,可以唤醒新轮次,但不向模型消息面添加 `user/message`。它经过正常 pre-step 准入;被拒绝或移除的输入不授权模型请求。轮次和步骤结束沿用正常的成功、失败与取消处理。 + ## 模型体验 ### 完整对话请求 diff --git a/packages/core/agent/README.i18n.yaml b/packages/core/agent/README.i18n.yaml index 3aeeb9a406..a7bdc11ed0 100644 --- a/packages/core/agent/README.i18n.yaml +++ b/packages/core/agent/README.i18n.yaml @@ -60,8 +60,8 @@ en: e48eae6c7de88cf8 zh: 1f5b37bdb0e53788 /deepseek-ai-dsh-agent/further-exploration: - en: d9d9bda2357ef857 - zh: 5fa1368d5f8912e2 + en: faf384f89b18b671 + zh: 42400f79f5ab84b0 /deepseek-ai-dsh-agent/model-experience: en: 215e7ba838619b7b zh: a311da3843709f90 diff --git a/packages/core/agent/README.md b/packages/core/agent/README.md index 2f7c830c7c..aba28e301e 100644 --- a/packages/core/agent/README.md +++ b/packages/core/agent/README.md @@ -150,9 +150,9 @@ The package-level contract is enough for most consumers; read these when you nee ----- - An ordinary `followup` with `content: []` is an explicit request to start a new turn from committed history. It retains the identified inbox receipt and normal lifecycle without creating a model-visible user message. A pre-step rejection or a decision rewritten to an empty batch still prevents the request. + ## Model Experience ### User, steering, and injected messages diff --git a/packages/core/agent/README.zh.md b/packages/core/agent/README.zh.md index 1eb86a7a5e..c317176430 100644 --- a/packages/core/agent/README.zh.md +++ b/packages/core/agent/README.zh.md @@ -150,9 +150,9 @@ await handle.agent.whenIdle() ----- - 普通 `followup` 的 `content: []` 明确请求基于已提交历史启动新轮次。它保留带身份的 inbox 回执和正常生命周期,不生成模型可见的用户消息。pre-step 拒绝或改写为空批次的决策仍会阻止请求。 + ## 模型体验 ### 用户、steering 与注入消息 From 4daabfd629bbcdaeab1e2c21304f4b3ea6bcbf0f Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Mon, 26 Oct 2026 22:20:41 +0800 Subject: [PATCH 06/65] fix(test): include shared continuation events in client checks --- tsconfig.client.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tsconfig.client.json b/tsconfig.client.json index 514c1f9deb..36babf0686 100644 --- a/tsconfig.client.json +++ b/tsconfig.client.json @@ -29,6 +29,8 @@ "packages/experimental/*/src/css-modules.d.ts", "packages/client/*/tests/**/*.ts", "packages/client/*/tests/**/*.tsx", + // Shared Session event data has no Host Context augmentation. + "packages/api/session-controller/tests/continuation-fixture.ts", "benchmarks/**/*.client.ts", "benchmarks/**/*.client.tsx", "benchmarks/support/**/*.ts", From 1b907b26aacdf43d07d9046868badd80bbfae2a4 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Mon, 26 Oct 2026 22:25:05 +0800 Subject: [PATCH 07/65] test(web): pin continuation without a user message bubble --- packages/api/session-controller/src/commands.ts | 2 +- packages/api/session-controller/tests/session-cold.host.spec.ts | 2 +- snapshots/web/live-interactions/continued.expected.md | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/api/session-controller/src/commands.ts b/packages/api/session-controller/src/commands.ts index b43333ebc0..a258825c66 100644 --- a/packages/api/session-controller/src/commands.ts +++ b/packages/api/session-controller/src/commands.ts @@ -62,7 +62,7 @@ type PromptContentCandidate = /** * Whether prompt content carries non-whitespace text or an attachment. * @param content - submitted or edited prompt parts. - * @returns false for content the Agent must not receive. + * @returns whether any part supplies ordinary prompt content. */ export function hasPromptContent(content: readonly PromptContentCandidate[]): boolean { return content.some(part => part.type !== 'text' || part.text.trim().length > 0) diff --git a/packages/api/session-controller/tests/session-cold.host.spec.ts b/packages/api/session-controller/tests/session-cold.host.spec.ts index 5139baf644..7e58d39aca 100644 --- a/packages/api/session-controller/tests/session-cold.host.spec.ts +++ b/packages/api/session-controller/tests/session-cold.host.spec.ts @@ -874,7 +874,7 @@ async function modelFor(ctx: Context, session: Session): Promise { } describe('sessions.prompt synchronous rejection', () => { - it('rejects content without non-whitespace text or an attachment before delivery or Session events', async () => { + it('rejects whitespace-only text parts before delivery or Session events', async () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) diff --git a/snapshots/web/live-interactions/continued.expected.md b/snapshots/web/live-interactions/continued.expected.md index 0b4bcd0205..16c21a89f1 100644 --- a/snapshots/web/live-interactions/continued.expected.md +++ b/snapshots/web/live-interactions/continued.expected.md @@ -20,7 +20,6 @@ - text: This turn failedupstream 503 - code: SERVER - text: plan Plan mode on. Use /plan off to leave. plan Plan mode off. -- button "Copy" - status: Completed - button "Completed in {{duration}}" - paragraph: Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures. From 53d4ed8f217ea157081a6ad8de7bf99e6e3a4c95 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Mon, 26 Oct 2026 22:40:07 +0800 Subject: [PATCH 08/65] refactor(api): use canceled inbox splice semantics --- packages/api/session-controller/src/continuation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/session-controller/src/continuation.ts b/packages/api/session-controller/src/continuation.ts index 8673cfc731..5d8e97553e 100644 --- a/packages/api/session-controller/src/continuation.ts +++ b/packages/api/session-controller/src/continuation.ts @@ -33,7 +33,7 @@ export const continuationProjectionDefinition = { switch (event.type) { case 'agent/inbox/spliced': if (state.continuing && event.data.target === 'next-turn' && event.data.outcome === 'canceled' - && event.data.start === 0 && (event.data.removedCount ?? 0) > 0) return { ...state, continuing: false } + && event.data.start === 0) return { ...state, continuing: false } return state.ending !== null && !state.continuing && event.data.target === 'next-turn' && event.data.inserted.some(message => message.source.kind === 'user' && message.content.length === 0) ? { ...state, continuing: true } : state From 04f1a699ee54d7210632a58252fa713e791ccd93 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Mon, 26 Oct 2026 22:49:03 +0800 Subject: [PATCH 09/65] fix(api): return the synchronous continuation admission promise --- packages/api/session-controller/src/commands.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api/session-controller/src/commands.ts b/packages/api/session-controller/src/commands.ts index a258825c66..88d7486cbe 100644 --- a/packages/api/session-controller/src/commands.ts +++ b/packages/api/session-controller/src/commands.ts @@ -316,14 +316,14 @@ export class SessionCommandController { if (hasPromptRequest(agent, request.requestId)) return { accepted: true } if (request.content.length === 0) { try { - return await agent.runMaintenance(async () => { + return await agent.runMaintenance(() => { const inbox = agent.inbox if (request.mode !== 'queue' || this.continuation.stateOf(agent.session).ending === null || inbox.nextTurn.length > 0 || inbox.nextStep.some(message => message.source.kind === 'user')) { throw new RemoteError('session/agent-busy', 'generation continuation is unavailable', { reason: 'NO_CONTINUABLE_FAILURE_OR_PENDING_INPUT' }) } agent.followup(createUserMessage({ content: [], source })) - return { accepted: true as const } + return Promise.resolve({ accepted: true as const }) }) } catch (error: unknown) { if (remoteErrorOf(error) !== undefined) throw error From 2e0c73a89599a50102448b8f67199565f04b45c6 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Tue, 27 Oct 2026 12:07:24 +0800 Subject: [PATCH 10/65] refactor(agent): record continuation as retry control input --- ...6-manual-generation-continuation.i18n.yaml | 4 +- ...26-10-26-manual-generation-continuation.md | 6 +- ...10-26-manual-generation-continuation.zh.md | 6 +- apps/web/tests/live-interactions.e2e.ts | 9 +- docs/architecture.i18n.yaml | 4 +- docs/architecture.md | 4 +- docs/architecture.zh.md | 4 +- docs/event-producer-consumer.md | 10 +- docs/event-producer-consumer.zh.md | 10 +- docs/persistence-catalog.i18n.yaml | 160 ++++++----- docs/persistence-catalog.md | 199 ++++++++++---- docs/persistence-catalog.zh.md | 199 ++++++++++---- .../2026-10-27-retry-control-input.i18n.yaml | 28 ++ .../2026-10-27-retry-control-input.md | 64 +++++ ...2026-10-27-retry-control-input.schema.json | 257 ++++++++++++++++++ .../2026-10-27-retry-control-input.zh.md | 64 +++++ docs/persistence-schema.json | 79 +++--- docs/subsystems/core.md | 8 +- docs/subsystems/core.zh.md | 8 +- .../empty-prompt-continuation/guide.i18n.yaml | 22 +- .../empty-prompt-continuation/guide.md | 15 +- .../empty-prompt-continuation/guide.zh.md | 15 +- .../api/session-controller/README.i18n.yaml | 4 +- packages/api/session-controller/README.md | 2 +- packages/api/session-controller/README.zh.md | 2 +- .../api/session-controller/src/commands.ts | 7 +- .../session-controller/src/continuation.ts | 17 +- packages/api/session-controller/src/index.ts | 2 +- packages/api/session-controller/src/types.ts | 2 + .../tests/continuation.host.spec.ts | 44 +-- .../tests/empty-prompt.host.spec.ts | 17 +- .../client/ui-conversation/README.i18n.yaml | 12 +- packages/client/ui-conversation/README.md | 6 +- packages/client/ui-conversation/README.zh.md | 6 +- packages/core/agent-loop/README.i18n.yaml | 8 +- packages/core/agent-loop/README.md | 4 +- packages/core/agent-loop/README.zh.md | 4 +- packages/core/agent-loop/src/agent.ts | 7 +- .../agent-loop/tests/request-error.spec.ts | 32 ++- packages/core/agent/README.i18n.yaml | 8 +- packages/core/agent/README.md | 4 +- packages/core/agent/README.zh.md | 4 +- packages/core/agent/src/runtime-types.ts | 8 +- packages/core/agent/src/types.ts | 18 +- .../core/session/src/known-event-types.ts | 1 + packages/sdk/client/src/client.ts | 2 +- packages/sdk/protocol/README.i18n.yaml | 4 +- packages/sdk/protocol/README.md | 2 +- packages/sdk/protocol/README.zh.md | 2 +- packages/sdk/protocol/src/types.ts | 4 +- packages/sdk/server/README.i18n.yaml | 8 +- packages/sdk/server/README.md | 4 +- packages/sdk/server/README.zh.md | 4 +- packages/sdk/server/src/server.ts | 4 +- scripts/smoke-python-runtime.py | 5 + .../scheduler-recovery/result.json | 2 + .../scheduler-recovery/session.v4.jsonl | 3 +- snapshots/sdk/sdk.snapshot.ts | 19 +- .../notifications.expected.jsonl | 9 +- .../tool-scheduler-recovery/session.v4.jsonl | 3 +- 60 files changed, 1083 insertions(+), 386 deletions(-) create mode 100644 docs/persistence-changes/2026-10-27-retry-control-input.i18n.yaml create mode 100644 docs/persistence-changes/2026-10-27-retry-control-input.md create mode 100644 docs/persistence-changes/2026-10-27-retry-control-input.schema.json create mode 100644 docs/persistence-changes/2026-10-27-retry-control-input.zh.md diff --git a/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.i18n.yaml b/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.i18n.yaml index 0b66ec2f1d..8c9fc03d74 100644 --- a/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.i18n.yaml @@ -9,8 +9,8 @@ en: f118844946afdbdc zh: f1fe37d042f88b24 /agent-note-manual-generation-continuation/decision: - en: 32e75f0c2a35bfec - zh: 343fedcd8f53a6a8 + en: d384d0ea6c502908 + zh: 90e0cb41a5a0d125 /agent-note-manual-generation-continuation/alternatives-considered: en: 58bb948d210d42ea zh: 5dc920d222d71218 diff --git a/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.md b/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.md index ab91eb5f7b..ededd88ffb 100644 --- a/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.md +++ b/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.md @@ -10,11 +10,11 @@ Users need another request after a model failure without inserting an artificial ## Decision -The play action submits `content: []` through the existing Session prompt API. AgentLoop retains the identified inbox input and starts a new balanced turn, but omits the empty ordinary input from model-visible `user/message` events. The original failed turn remains closed. +The play action submits `content: []` through the existing Session prompt API. The API assigns `source.kind: 'retry'`. AgentLoop retains the identified inbox input and starts a new balanced turn; acceptance records `agent/retry` instead of a model-visible `user/message`. Ordinary empty user-message behavior is unchanged. The original failed turn remains closed. -The Session API owns the fixed-size `conversationContinuation` projection and shares its value with the browser. Admission requires an eligible failure, true idle ownership, and no pending human input. An accepted empty input carries the admitted work into its next turn, preserving eligibility if that request also fails. +The Session API owns the fixed-size `conversationContinuation` projection and shares its value with the browser. Admission requires an eligible failure, true idle ownership, and no pending human input. An accepted retry event admits the next turn, preserving eligibility if that request also fails. -The six-code manual allowlist stays independent of provider retry policy. Non-waking injected context accompanies the request. Durable inbox receipts retain request-id deduplication after the empty input is claimed; no new Remote operation or Session event format is required. +The six-code manual allowlist stays independent of provider retry policy. Non-waking injected context accompanies the request. The retry event retains request-id deduplication after the inbox input is claimed; no new Remote operation is required. An explicit control source prevents unrelated empty user inputs from changing behavior, and a separate event keeps chat rendering free of retry-specific filtering. ## Alternatives considered diff --git a/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.zh.md b/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.zh.md index 21ff2cf50a..b3d659edf2 100644 --- a/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.zh.md +++ b/.agents/notes/implemented/feature/2026-10-26-manual-generation-continuation.zh.md @@ -10,11 +10,11 @@ Status: implemented ## 决策 -播放按钮通过现有 Session prompt API 提交 `content: []`。AgentLoop 保留带身份的 inbox 输入并启动正常闭合的新轮次,但不将空普通输入写成模型可见的 `user/message`。原失败轮次保持闭合。 +播放按钮通过现有 Session prompt API 提交 `content: []`。API 指定 `source.kind: 'retry'`。AgentLoop 保留带身份的 inbox 输入并启动正常闭合的新轮次;接受后记录 `agent/retry`,而不写入模型可见的 `user/message`。普通空用户消息的行为保持不变。原失败轮次保持闭合。 -Session API 拥有固定大小的 `conversationContinuation` 投影,向浏览器发布相同的值。准入要求失败符合条件、持有真正的空闲状态且没有待消费的用户输入。已接受的空输入将原先准入的工作带入新轮次,因此请求再次失败后仍保留继续资格。 +Session API 拥有固定大小的 `conversationContinuation` 投影,向浏览器发布相同的值。准入要求失败符合条件、持有真正的空闲状态且没有待消费的用户输入。已接受的重试事件使新轮次获得准入,因此请求再次失败后仍保留继续资格。 -六码手动白名单独立于提供方重试策略。非唤醒的注入上下文随请求进入。空输入被领取后,持久 inbox 回执仍支持 request-id 去重;不需要新增 Remote 操作或 Session 事件格式。 +六码手动白名单独立于提供方重试策略。非唤醒的注入上下文随请求进入。inbox 输入被领取后,重试事件仍支持 request-id 去重;不需要新增 Remote 操作。显式控制来源避免改变无关空用户输入的行为,独立事件则使聊天渲染无需针对重试过滤。 ## 考虑过的替代方案 diff --git a/apps/web/tests/live-interactions.e2e.ts b/apps/web/tests/live-interactions.e2e.ts index eda193847c..bde84fe2d1 100644 --- a/apps/web/tests/live-interactions.e2e.ts +++ b/apps/web/tests/live-interactions.e2e.ts @@ -414,7 +414,7 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => { expect(tripwire.warnings).toEqual([]) }, 120_000) - it.skipIf(MODE === 'record')('continues after transient retries exhaust without another user message', async () => { + it.skipIf(MODE === 'record')('continues after transient retries exhaust without another user bubble', async () => { // A whole-script replacement: three throw entries cover the first request // plus both budgeted retries (patches cannot reach past the one-call // derived script). The scenario-owned policy keeps exhaustion fast and @@ -476,8 +476,11 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => { expect(turnEndReasons(sessionEvents)).toEqual(['error', 'completed']) expect(sessionEvents.some(event => event.type === 'user/message' && event.data.source.kind === 'plan-mode')).toBe(true) const userMessages = sessionEvents.flatMap(event => event.type === 'user/message' && event.data.source.kind === 'user' - ? [event.data.content] : []) - expect(userMessages).toEqual([[{ type: 'text', text: PROMPT }]]) + ? [event.data] : []) + expect(userMessages.map(message => message.content)).toEqual([[{ type: 'text', text: PROMPT }]]) + expect(sessionEvents.filter(event => event.type === 'agent/retry')).toEqual([ + expect.objectContaining({ data: expect.objectContaining({ turn: 2, step: 1, source: expect.objectContaining({ kind: 'retry' }) }) }), + ]) expect(sessionEvents.filter(event => event.type === 'turn/start')).toHaveLength(2) expect(sessionEvents.filter(event => event.type === 'turn/end')).toHaveLength(2) expect(await resume.count()).toBe(0) diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index e9bb6328e2..5dfa2c2995 100644 --- a/docs/architecture.i18n.yaml +++ b/docs/architecture.i18n.yaml @@ -24,8 +24,8 @@ en: 693407efb233f961 zh: 37b9dd0677d4405c /deepseek-harness-architecture/turn-flow: - en: adffae46f8181328 - zh: 1009d3a5934cd1ba + en: 3770eacd9c3bae28 + zh: bd7b8a0374ba0ff8 /deepseek-harness-architecture/session-log: en: 8a7f19b95d426c99 zh: d5bb0538404e20ea diff --git a/docs/architecture.md b/docs/architecture.md index cd561ac353..064026d4d2 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -107,9 +107,9 @@ turn/end Factory setup follows SessionStore entry and `session/created`, preceding `agent/created`; closing events commit before removal. -The durable `inbox` projection exposes pending input. Empty ordinary input starts a turn without a model-visible message; injected context waits for waking input. +One inbox feeds the driver; injected context waits for a waking message. AgentLoop’s durable `inbox` projection exposes pending input without live Agents. -Before assembly, the loop snapshots Session model selection in `AssembleContext.modelSelection` and `agent/pre-step.modelSelection`; retries retain it; omitted reasoning effort uses adapter defaults. `agent/pre-step` listeners accept, rewrite, or reject claimed messages; a rejected or empty first claim closes a durable turn without a step. An enter decision may set `startsRequestSeries`: the loop logs a fresh `request/header` (reason `series`, or `change` with `startsSeries: true` when the envelope also changed). Wrapping listeners preserve that declaration with `{ ...decision, messages }`. After assembly and `step/start`, the deprecated `agent/request` hook adjusts controls without changing the provider, model, or effort selection, and `prepareCall()` binds the adapter before the system prompt and accepted users are committed; cancellation during either async phase commits neither. Prepared call capabilities govern prompt admission, not preceding `request/context`. Each attempt refreshes registered runtime facts, reconciles the bound prompt, admits users once, and restores removed runtime context, logs header/context as needed, and freezes the request before streaming the bound call. Retries do not repeat assembly or `agent/pre-step`. Surface replacements and image-offload decisions after attachment start a new request series, including during the first resumed pre-step; unchanged resume continues the series. The system prompt travels as `system/message` history: empty text clears it, capable routes append updates, and other routes consolidate it at the first system node. [Agent-loop](../packages/core/agent-loop/README.md#understand-the-implementation) owns prompt admission and replacement details. +Before assembly, the loop snapshots Session model selection in `AssembleContext.modelSelection` and `agent/pre-step.modelSelection`; retries retain it; omitted reasoning effort uses adapter defaults. `agent/pre-step` listeners accept, rewrite, or reject claimed messages; a rejected or empty first claim closes a durable turn without a step. An enter decision may set `startsRequestSeries`: the loop logs a fresh `request/header` (reason `series`, or `change` with `startsSeries: true` when the envelope also changed). Wrapping listeners preserve that declaration with `{ ...decision, messages }`. After assembly and `step/start`, the deprecated `agent/request` hook adjusts controls without changing the provider, model, or effort selection, and `prepareCall()` binds the adapter before the system prompt and accepted users are committed; cancellation during either async phase commits neither. Prepared call capabilities govern prompt admission, not preceding `request/context`. Empty retry-source controls record `agent/retry` instead of entering model history; they retain ordinary inbox and turn lifecycle. Each attempt refreshes registered runtime facts, reconciles the bound prompt, admits users once, and restores removed runtime context, logs header/context as needed, and freezes the request before streaming the bound call. Retries do not repeat assembly or `agent/pre-step`. Surface replacements and image-offload decisions after attachment start a new request series, including during the first resumed pre-step; unchanged resume continues the series. The system prompt travels as `system/message` history: empty text clears it, capable routes append updates, and other routes consolidate it at the first system node. [Agent-loop](../packages/core/agent-loop/README.md#understand-the-implementation) owns prompt admission and replacement details. The loop sends immutable requests with live cancellation and reuses freeze evidence for frozen identities; [agent-loop](../packages/core/agent-loop/README.md) owns construction and cancellation causes. diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 02c7423e56..e836ab7452 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -111,9 +111,9 @@ turn/end 工厂 setup 在 Session 进入 SessionStore 并完成 `session/created` 之后、`agent/created` 之前运行;收尾事件在移除之前提交。 -持久 `inbox` 投影公开待消费输入。空普通输入启动轮次而不新增模型可见消息;注入上下文等待唤醒输入。 +输入通过同一个 inbox 到达驱动器;注入的上下文等待一条唤醒消息。AgentLoop 的持久 `inbox` 投影使待处理输入在没有活跃 Agent 时仍可读取。 -循环在组装前将 Session 模型选择快照到 `AssembleContext.modelSelection` 与 `agent/pre-step.modelSelection`;重试保留该选择,省略推理强度时使用适配器默认值。`agent/pre-step` 监听器接纳、改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。组装与 `step/start` 之后,已弃用的 `agent/request` hook 在不改变提供方、模型或推理强度选择的前提下调整控制参数,`prepareCall()` 绑定适配器,再提交系统提示词与已接纳用户消息;在任一异步阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试刷新已注册运行时事实、协调绑定的提示词、仅接纳一次用户消息并恢复被移除的运行时上下文、按需记录 header/context,再冻结请求并通过绑定调用发起流式请求。重试不重复组装或 `agent/pre-step`。附接后的 surface 替换和图片省略决定开启新请求序列,包括恢复后的首次 pre-step 中发生的替换;未变化的恢复延续序列。系统提示词通过 `system/message` 历史传递:空文本清除提示词,具备能力的路由追加更新,其他路由将其归并到首个系统节点。[Agent-loop](../packages/core/agent-loop/README.zh.md#understand-the-implementation) 说明提示词准入与替换的细节。 +循环在组装前将 Session 模型选择快照到 `AssembleContext.modelSelection` 与 `agent/pre-step.modelSelection`;重试保留该选择,省略推理强度时使用适配器默认值。`agent/pre-step` 监听器接纳、改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。组装与 `step/start` 之后,已弃用的 `agent/request` hook 在不改变提供方、模型或推理强度选择的前提下调整控制参数,`prepareCall()` 绑定适配器,再提交系统提示词与已接纳用户消息;在任一异步阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试刷新已注册运行时事实、协调绑定的提示词、仅接纳一次用户消息并恢复被移除的运行时上下文、按需记录 header/context,再冻结请求并通过绑定调用发起流式请求。重试不重复组装或 `agent/pre-step`。附接后的 surface 替换和图片省略决定开启新请求序列,包括恢复后的首次 pre-step 中发生的替换;未变化的恢复延续序列。系统提示词通过 `system/message` 历史传递:空文本清除提示词,具备能力的路由追加更新,其他路由将其归并到首个系统节点。[Agent-loop](../packages/core/agent-loop/README.zh.md#understand-the-implementation) 说明提示词准入与替换的细节。 空的 retry 来源控制输入记录 `agent/retry`,不进入模型历史;inbox 与轮次生命周期仍按普通流程处理。 循环发送不可变请求,同时保持取消有效,仅对完全冻结的对象复用冻结证据;[agent-loop](../packages/core/agent-loop/README.zh.md) 负责请求构建与取消原因。 diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 1c1bdb2d05..fbe751ca65 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -23,11 +23,11 @@ Application listeners are outside this package matrix. Desktop Host subscribes t | `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:391`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) | | `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:311`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` | | `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:419`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) | -| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:691`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:671`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:698`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:677`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:684`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:693`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:673`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:700`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:679`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:686`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:55`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) | | `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:87`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes`, `server` | | `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | - | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index f793641ff3..30163a2e5f 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -25,11 +25,11 @@ | `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:391`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`compaction-image-offload`](../packages/compaction/compaction-image-offload), [`llm-retry`](../packages/llm/llm-retry) | | `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:311`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), `server`, `session-controller` | | `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:419`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`workspace-changes`](../packages/deliverables/workspace-changes) | -| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:691`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:671`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:698`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:677`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | -| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:684`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:693`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:673`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:700`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:679`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | +| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:686`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` | | `app-boot/config-reload` | `emit` | [`packages/boot/app-boot/src/index.ts:55`](../packages/boot/app-boot/src/index.ts) | [`app-boot`](../packages/boot/app-boot) (`emit`) | [`settings`](../packages/settings/settings) | | `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:87`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes`, `server` | | `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | - | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index 0774d434a7..3214b08f10 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -9,8 +9,8 @@ en: 1387c4505dde2faf zh: 48570fa1c13d6910 /session-persistence-event-catalog/persistence-type-fingerprints: - en: 519d46d6078d3f68 - zh: ec9aec262312c573 + en: 84b66494d8162dc6 + zh: f89855f2a8e9a367 /session-persistence-event-catalog/event-envelope: en: 56414ffd00cdc32a zh: 6dc11eb013e95e41 @@ -21,8 +21,11 @@ en: 9188862058c6986b zh: 9188862058c6986b /session-persistence-event-catalog/events/agent/agent-inbox-spliced-log-only: - en: 481727894895b43b - zh: 098bc9788bf88968 + en: 9ae614c041a27169 + zh: 8d669dd582b88f79 +/session-persistence-event-catalog/events/agent/agent-retry-log-only: + en: 559e0484e8ae8904 + zh: 8967a336e5aef50c /session-persistence-event-catalog/events/agent-preset: en: 49891ca9be5ac123 zh: 49891ca9be5ac123 @@ -141,8 +144,8 @@ en: 9edf4ef31e60e880 zh: 9edf4ef31e60e880 /session-persistence-event-catalog/events/model/model-selection-log-only: - en: c2a60ec7d297ed79 - zh: a2f6b2d3c733ce96 + en: 404c54b08c088438 + zh: eb86ed4e7d0e9228 /session-persistence-event-catalog/events/permission: en: 19917bc5438e60af zh: 19917bc5438e60af @@ -330,8 +333,11 @@ en: b3b528acadd04168 zh: b3b528acadd04168 /session-persistence-event-catalog/resolved-persistence-types/agent-inbox-spliced: - en: 8f6d91375141d923 - zh: 8f6d91375141d923 + en: 8719b8f3a0388f64 + zh: 8719b8f3a0388f64 +/session-persistence-event-catalog/resolved-persistence-types/agent-retry: + en: f634a360f6f4a5a7 + zh: f634a360f6f4a5a7 /session-persistence-event-catalog/resolved-persistence-types/allowed-once: en: 6c3effe2abc1aa53 zh: 6c3effe2abc1aa53 @@ -711,8 +717,11 @@ en: ade80ed58d3b63b2 zh: ade80ed58d3b63b2 /session-persistence-event-catalog/resolved-persistence-types/resume: - en: e27772872766bd6d - zh: e27772872766bd6d + en: f9a34b26963fc22d + zh: f9a34b26963fc22d +/session-persistence-event-catalog/resolved-persistence-types/retry: + en: 68c90ebe83c80004 + zh: 68c90ebe83c80004 /session-persistence-event-catalog/resolved-persistence-types/runtime-context: en: 784f0d4afdaaa472 zh: 784f0d4afdaaa472 @@ -1008,11 +1017,11 @@ en: a6a2e38fdd6d127a zh: 1fe82356d89f5769 /session-persistence-event-catalog/resolved-persistence-types/deepseeksearchllmrequest: - en: cf3979c698e2b2ac - zh: 6252c8b668faa706 + en: 96cd6b26224ff94f + zh: cf340bff9692f438 /session-persistence-event-catalog/resolved-persistence-types/developermessage: - en: 16992572a6919e52 - zh: 853f4873fd520444 + en: 6a4c81ce3ec34434 + zh: 96bced8b8cad4b1a /session-persistence-event-catalog/resolved-persistence-types/epochheader: en: 3bdc8ecd0bdf0c9c zh: 0fe5b75c174ebab4 @@ -1077,8 +1086,8 @@ en: a3087ab7bebe9371 zh: abf6afc2f9f5b692 /session-persistence-event-catalog/resolved-persistence-types/inboxtarget: - en: b3b7b8a3414bcb6b - zh: 06be5b8e56bacedf + en: f17ed3bd806e279f + zh: 72920ace94922d45 /session-persistence-event-catalog/resolved-persistence-types/jobregistereddata: en: f604eebb973fe2cf zh: 0c0aba1e17ecf66e @@ -1122,11 +1131,11 @@ en: 6dbe099c62f7c226 zh: 6fd788f63c038c91 /session-persistence-event-catalog/resolved-persistence-types/llmretrystartedeventdata: - en: 5d61649043e317aa - zh: bdf9b9d553d291cf + en: dc14ea1c81e3d226 + zh: 21a2b974bdc708d2 /session-persistence-event-catalog/resolved-persistence-types/message: - en: 56953b055da0dc2b - zh: dcaa193ddb2fece0 + en: 7b11edaf7aa790ae + zh: 514e2826b20dd7d3 /session-persistence-event-catalog/resolved-persistence-types/messagefeedbackdelete: en: c582953270d4ca44 zh: 966ccfe2e7660a2a @@ -1137,20 +1146,20 @@ en: f58575a1fc96df42 zh: 9f0c6ed9033e5c9d /session-persistence-event-catalog/resolved-persistence-types/messagefeedbackrating: - en: 70ade19513d0070f - zh: ff9acb50974a32ae + en: 9897f3cb7a6c7233 + zh: 30e377f7c302a32b /session-persistence-event-catalog/resolved-persistence-types/messagesource: - en: b1bd7c8bd2309d45 - zh: d434e41517c3620d + en: 91e395f09fd30c1a + zh: 5efbc52f4e72e049 /session-persistence-event-catalog/resolved-persistence-types/message~2: - en: dcd71b3190faff96 - zh: b2c07224705aa52e + en: 11eafb2bccde2b25 + zh: 681c0cdb27f6a1c0 /session-persistence-event-catalog/resolved-persistence-types/modelmessagesource: en: 5f9154ca34235ab9 zh: a833105910ebfa82 /session-persistence-event-catalog/resolved-persistence-types/modelselection: - en: 6e301c0ea35d9e0e - zh: 6fbf1d318db9a4e0 + en: 970930c4a8b1bad8 + zh: 6c596aba5e3c5020 /session-persistence-event-catalog/resolved-persistence-types/oneshotscheduledispatchchange: en: a3300babe57b6b67 zh: 4aaf06a149b399c3 @@ -1206,11 +1215,11 @@ en: 9a54c4478ad3b620 zh: 02c4c1fd7e252770 /session-persistence-event-catalog/resolved-persistence-types/sessiontitleeventdata: - en: 46baac9dcdfb1b63 - zh: 30a357bb8405c147 + en: c8e30c2e9886e459 + zh: d7b53b79e0849e64 /session-persistence-event-catalog/resolved-persistence-types/sessiontitlellmrequesteventdata: - en: 035b656ba6500174 - zh: fc9d4d7cd154ca78 + en: aefe8cd3c2b273b2 + zh: b2e21ce4ca05f3fb /session-persistence-event-catalog/resolved-persistence-types/sessiontitlemodelidentity: en: 9531468264499498 zh: 9da23c0ad2776563 @@ -1317,14 +1326,14 @@ en: 778debd61e0284a2 zh: 2cc82ad9e80f18b1 /session-persistence-event-catalog/resolved-persistence-types/turnendreason: - en: 3cbc1b5ae251451f - zh: a95ff0555e368432 + en: 2ef8e1ebf9e02934 + zh: 986c170add1fb987 /session-persistence-event-catalog/resolved-persistence-types/usermessage: - en: 1b30ae0450f156bd - zh: 00bb0efae7ac741e + en: cf92679783fc00c6 + zh: 7a6ba95081ca4ace /session-persistence-event-catalog/resolved-persistence-types/usermessage~2: - en: 1f242f48a3c8c230 - zh: 2516be50ce573b0c + en: f2f335cf1b45d3f9 + zh: 4df02f25e9a324dd /session-persistence-event-catalog/resolved-persistence-types/workflowagentoutcome: en: d0a3f9aab18c3de5 zh: 76ac69a835ca4e29 @@ -1371,9 +1380,12 @@ en: f692844b29923b2e zh: 72f12bca08d63267 /session-persistence-event-catalog/resolved-persistence-types/union-2-variants~2: - en: 69d360cf5a1ae6b4 - zh: f23faf39a2a0e6f1 + en: 2453bf81cd9cd1f6 + zh: b1cef542e013c238 /session-persistence-event-catalog/resolved-persistence-types/union-2-variants~3: + en: 1137a9a48ddad4b3 + zh: 222c1545cdb19d76 +/session-persistence-event-catalog/resolved-persistence-types/union-2-variants~4: en: 5a1af9849b0967ea zh: 1085d793bbc47360 /session-persistence-event-catalog/resolved-persistence-types/union-3-variants: @@ -1443,8 +1455,11 @@ en: 43cf5cd666b591a0 zh: 7c7a3011f8681f12 /session-persistence-event-catalog/resolved-persistence-types/childcreatedat-childid-label-mode~6: - en: a1f188c9058cf621 - zh: eb43c0f045fb9afb + en: 959fc569699d21f0 + zh: b48a0a3b1639957d +/session-persistence-event-catalog/resolved-persistence-types/clienttimezone-kind-retry-principal-rpcid: + en: 1c523784a95eff23 + zh: e19a26cb9ae14e12 /session-persistence-event-catalog/resolved-persistence-types/code-name-reason: en: 25df2af19bc9c46d zh: ebb6ecbeee25c9be @@ -1503,11 +1518,11 @@ en: 51af619ed8532a43 zh: a5ad878c63d0e046 /session-persistence-event-catalog/resolved-persistence-types/header-reason-startsseries: - en: 27f5b76003ac1ead - zh: 2a5a0490cc22512b + en: 616424f7d89b2617 + zh: 1a90b1c57ecb4f88 /session-persistence-event-catalog/resolved-persistence-types/headerseq-message-step-turn: - en: 598021cdfe2a8687 - zh: 02e63a8b453f2f4e + en: bcdb5e4a5a9b0aac + zh: a214e6242694bfdd /session-persistence-event-catalog/resolved-persistence-types/height-width: en: a5df50710083ac25 zh: aad3ccf6f6de70e4 @@ -1524,11 +1539,14 @@ en: 41f73c58a77dfa28 zh: af77f4aee3cb4a40 /session-persistence-event-catalog/resolved-persistence-types/inherited: - en: 7e64cae3bd1b71e9 - zh: 7451c0639044c59a + en: 7b92563703daff7b + zh: c91b356048b96141 +/session-persistence-event-catalog/resolved-persistence-types/inputid-source-step-turn: + en: 930d9c64fe39857d + zh: 65cd07dc3c7789db /session-persistence-event-catalog/resolved-persistence-types/inserted-outcome-removedcount-start: - en: df2dd98c35b34bd2 - zh: ee25992a3f29d561 + en: 4edbaf2329a66619 + zh: 7d29428252d9d84f /session-persistence-event-catalog/resolved-persistence-types/interrupted-message-step-stream: en: 8e86cd838e3e4a49 zh: f5fec2a8adf1f3cc @@ -1719,8 +1737,11 @@ en: 1f6dbdbf5d923df0 zh: aea576e5ec34c0fc /session-persistence-event-catalog/resolved-persistence-types/kind-repeat-tool-reminder-form-snapshot: - en: e9c88cceef2d8432 - zh: 14fb0897543b6960 + en: fb3160150710d86b + zh: 27a71fa774531620 +/session-persistence-event-catalog/resolved-persistence-types/kind-retry: + en: db4c8e67a5176225 + zh: 762719a4d2ab1f73 /session-persistence-event-catalog/resolved-persistence-types/kind-runtime-context: en: 68b5598e21cf8ba2 zh: 95ef1685af64a5f4 @@ -1944,11 +1965,14 @@ en: 374b8adee8d571fa zh: b21057efc58e6490 /session-persistence-event-catalog/resolved-persistence-types/type-agent-preset-selected: - en: a990181189cf77ac - zh: 70198727356d1ad3 + en: 7626d618ab37dec4 + zh: 662b0be4c51feebc /session-persistence-event-catalog/resolved-persistence-types/type-agent-inbox-spliced: - en: f3b491e65c79b3a0 - zh: 2d3e53ceb67a55a3 + en: 60466c7cafae41b5 + zh: 9dee130ddd7b0fd1 +/session-persistence-event-catalog/resolved-persistence-types/type-agent-retry: + en: 7a5e1c92d8760186 + zh: ae4173989281facd /session-persistence-event-catalog/resolved-persistence-types/type-approval-asked: en: 9c77f18b0dad22f0 zh: 8894cf2878fe90db @@ -1992,11 +2016,11 @@ en: ba73fb10b7e2ffc9 zh: 8d5c97e63e5f16da /session-persistence-event-catalog/resolved-persistence-types/type-deliverables-presented: - en: 17956f440c284ea3 - zh: 5dbfd68d50483047 + en: 7fbea1c722f8c2f3 + zh: d732349d783b9b01 /session-persistence-event-catalog/resolved-persistence-types/type-developer-message: - en: ded2f3b02f776800 - zh: 4d6619c283a18e3c + en: f231622830903402 + zh: 03457dc27539fafd /session-persistence-event-catalog/resolved-persistence-types/type-disabled: en: 585627a50ece4e5f zh: 157837aee3da0ac3 @@ -2070,11 +2094,11 @@ en: acf7e21e4d21ebff zh: fb83a853634d204a /session-persistence-event-catalog/resolved-persistence-types/type-session-title: - en: f0acf1c873cd9e2a - zh: b8e175e14306afd4 + en: a642705c8e44a3c9 + zh: 10aa1aff0d3766d3 /session-persistence-event-catalog/resolved-persistence-types/type-session-title-llm-request: - en: ab4f20d890eed5e5 - zh: 6e4035a166821157 + en: dd94dea63f118b12 + zh: e7feae9219037e9a /session-persistence-event-catalog/resolved-persistence-types/type-step-end: en: 7fc233c4a569dae2 zh: bc2b3fed4a397c00 @@ -2151,11 +2175,11 @@ en: 1b045de96172be4b zh: 8a15c6f35b576b37 /session-persistence-event-catalog/resolved-persistence-types/type-usage: - en: 4ed36e4498f5f203 - zh: 7b3fcddec14c306b + en: 311b42bd63735e87 + zh: 37ea452a672fca21 /session-persistence-event-catalog/resolved-persistence-types/type-user-message: - en: 5b8341a673284ec1 - zh: e3264a827babffeb + en: 07e60b23dc23e0a3 + zh: dc74747ec207e1bb /session-persistence-event-catalog/resolved-persistence-types/type-web-deepseek-search-llm-request: en: 99c8837344178a47 zh: bf746f5678346596 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index a498e62e2e..70e16e7ce7 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -23,7 +23,8 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `JsonlHeaderLine` | header | `18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f` | [`HeaderLine`](#persistence-type-sha256-18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f) | | `SessionEventEnvelope` | envelope | `5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc` | [`union (3 variants)`](#persistence-type-sha256-5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc) | | `event:agent-preset/selected` | event | `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | [`{ type: "agent-preset/selected" }`](#persistence-type-sha256-a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b) | -| `event:agent/inbox/spliced` | event | `9e8043098fb89db6d7c92a92c70146c5c8d24fa88f733c687fa29e5e7a58ab30` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-9e8043098fb89db6d7c92a92c70146c5c8d24fa88f733c687fa29e5e7a58ab30) | +| `event:agent/inbox/spliced` | event | `a6d8c25f4b2ca2d137672a8a278ee1398df3f3cb60f5d32d8e3007c68299b664` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-a6d8c25f4b2ca2d137672a8a278ee1398df3f3cb60f5d32d8e3007c68299b664) | +| `event:agent/retry` | event | `d8dde9629b7323b03c1c3fb0db75d6b72c73666dc0240ab7d3e41b9c0c40fc7c` | [`{ type: "agent/retry" }`](#persistence-type-sha256-d8dde9629b7323b03c1c3fb0db75d6b72c73666dc0240ab7d3e41b9c0c40fc7c) | | `event:approval/asked` | event | `3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1` | [`{ type: "approval/asked" }`](#persistence-type-sha256-3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1) | | `event:approval/decided` | event | `bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3` | [`{ type: "approval/decided" }`](#persistence-type-sha256-bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3) | | `event:approval/policy` | event | `26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78` | [`{ type: "approval/policy" }`](#persistence-type-sha256-26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78) | @@ -36,7 +37,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `event:compaction/start` | event | `db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965` | [`{ type: "compaction/start" }`](#persistence-type-sha256-db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965) | | `event:compaction/summary` | event | `e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602` | [`{ type: "compaction/summary" }`](#persistence-type-sha256-e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602) | | `event:deliverables/presented` | event | `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | [`{ type: "deliverables/presented" }`](#persistence-type-sha256-13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176) | -| `event:developer/message` | event | `95acdbd066cf445d37092e5fdfe2e8717ef6ca1aed50f70e2e6037a6989a1f2f` | [`{ type: "developer/message" }`](#persistence-type-sha256-95acdbd066cf445d37092e5fdfe2e8717ef6ca1aed50f70e2e6037a6989a1f2f) | +| `event:developer/message` | event | `92b82e8b7bbb38b3adeb2c07eb76d0f0e0d18c1729808def7004ac2bc9baa583` | [`{ type: "developer/message" }`](#persistence-type-sha256-92b82e8b7bbb38b3adeb2c07eb76d0f0e0d18c1729808def7004ac2bc9baa583) | | `event:feedback/message-delete` | event | `3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796` | [`{ type: "feedback/message-delete" }`](#persistence-type-sha256-3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796) | | `event:feedback/message-put` | event | `b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf` | [`{ type: "feedback/message-put" }`](#persistence-type-sha256-b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf) | | `event:feedback/record` | event | `b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8` | [`{ type: "feedback/record" }`](#persistence-type-sha256-b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8) | @@ -58,7 +59,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `event:session-log-deepseek/delivery-accepted` | event | `d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9` | [`{ type: "session-log-deepseek/delivery-accepted" }`](#persistence-type-sha256-d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9) | | `event:session/end-seed` | event | `5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e` | [`{ type: "session/end-seed" }`](#persistence-type-sha256-5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e) | | `event:session/title` | event | `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | [`{ type: "session/title" }`](#persistence-type-sha256-1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5) | -| `event:session/title-llm-request` | event | `eaa9125aec0fb28478096c46e73e21bf512ab0d84d826bfb41e7308707734cb5` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-eaa9125aec0fb28478096c46e73e21bf512ab0d84d826bfb41e7308707734cb5) | +| `event:session/title-llm-request` | event | `fb28ad7d7aabceb5bda505cf027e0e22354d7f31757337e7e51a4b592feac172` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-fb28ad7d7aabceb5bda505cf027e0e22354d7f31757337e7e51a4b592feac172) | | `event:step/end` | event | `e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05` | [`{ type: "step/end" }`](#persistence-type-sha256-e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05) | | `event:step/start` | event | `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | [`{ type: "step/start" }`](#persistence-type-sha256-4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986) | | `event:subagent/catalog` | event | `53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac) | @@ -80,7 +81,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `event:tool/result` | event | `7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397` | [`{ type: "tool/result" }`](#persistence-type-sha256-7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397) | | `event:turn/end` | event | `0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce` | [`{ type: "turn/end" }`](#persistence-type-sha256-0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce) | | `event:turn/start` | event | `aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8` | [`{ type: "turn/start" }`](#persistence-type-sha256-aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8) | -| `event:user/message` | event | `39adb7fce8475af6402acd925ea79697b3341c22422ac32833d71217d84e987f` | [`{ type: "user/message" }`](#persistence-type-sha256-39adb7fce8475af6402acd925ea79697b3341c22422ac32833d71217d84e987f) | +| `event:user/message` | event | `c5bbcb3435908af2036df7105883e0a49c30ecc1fae4420aa5c4cf944066eff5` | [`{ type: "user/message" }`](#persistence-type-sha256-c5bbcb3435908af2036df7105883e0a49c30ecc1fae4420aa5c4cf944066eff5) | | `event:web/deepseek-search-llm-request` | event | `0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52` | [`{ type: "web/deepseek-search-llm-request" }`](#persistence-type-sha256-0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52) | | `event:working-directory/change` | event | `124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3` | [`{ type: "working-directory/change" }`](#persistence-type-sha256-124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3) | | `event:workspace/changes` | event | `e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72` | [`{ type: "workspace/changes" }`](#persistence-type-sha256-e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72) | @@ -185,7 +186,20 @@ Sources: [`packages/core/session/src/types.ts:439`](../packages/core/session/src } ``` -Source: [`packages/core/agent/src/types.ts:110`](../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:119`](../packages/core/agent/src/types.ts) + + + +#### `agent/retry` — log-only + +```ts persistence-catalog +/** An admitted retry control, correlated with its durable inbox input; never enters model history. */ +'agent/retry': { turn: number; step: number; inputId: MessageId; source: Extract } +``` + +Types: [MessageSource](subsystems/core.md) + +Source: [`packages/core/agent/src/types.ts:113`](../packages/core/agent/src/types.ts) ### `agent-preset/*` @@ -700,7 +714,7 @@ Source: [`packages/llm/llm-retry/src/types.ts:11`](../packages/llm/llm-retry/src 'model/selection': ModelSelection ``` -Source: [`packages/core/agent/src/types.ts:121`](../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:130`](../packages/core/agent/src/types.ts) ### `permission/*` @@ -1405,6 +1419,14 @@ SHA-256: `829a2fb3be6efd4ba36aef8ed2d381a4c6fb2a0aaf50c823a6f3f8290786b809` `"agent/inbox/spliced"` + + +### `"agent/retry"` + +SHA-256: `30093f9f63a120cb932f07577594c04afcec3f0bd02859154a6e5b0e79a46359` + +`"agent/retry"` + ### `"allowed-once"` @@ -2427,6 +2449,14 @@ SHA-256: `34d1ab5c5df378186d6054b1a1beea9a41e41965d6f336f0bc0025c441a82e66` `"resume"` + + +### `"retry"` + +SHA-256: `63ba8895cc09e4693cf0819fdcd18c084bef195febda5b6a23af279b3852be36` + +`"retry"` + ### `"runtime-context"` @@ -3375,7 +3405,7 @@ Sources: [`packages/web/web-search-deepseek/src/provider.ts:55`](../packages/web | `body` | required | [`{ max_tokens, messages, model, thinking?, … }`](#persistence-type-sha256-9c001ecb5e1d3c3894410db2a1e383842e5f8454fb52182338686158abc1b3ee) | | `endpoint` | required | `string` | - + @@ -3383,7 +3413,7 @@ Sources: [`packages/web/web-search-deepseek/src/provider.ts:55`](../packages/web ### `DeveloperMessage` -SHA-256: `cebb7e9f3f663e660a531d9764ecfb9fa7fc73c22bf8d628f888e9ecf18c5cf0` +SHA-256: `0ef3072084202b084b2a25678fe9014ec339ee3797bc0bb164538ff3fa5e3fcc` Sources: [`packages/llm/llm/src/message.ts:157`](../packages/llm/llm/src/message.ts) @@ -3392,7 +3422,7 @@ Sources: [`packages/llm/llm/src/message.ts:157`](../packages/llm/llm/src/message | `content` | required | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) | | `id` | required | `string` | | `role` | required | `"developer"` | -| `source` | required | [`MessageSource`](#persistence-type-sha256-b112459dcb0686091c31ce6d213b2a4934b5ad19d37cc5b599e46f5a9d715591) | +| `source` | required | [`MessageSource`](#persistence-type-sha256-e3e9f4d1e245b470cba77c3e6b94ea141408604d693b2a8437278abc8cfd8e2c) | Source compatibility: `source` — `session-source-attribution` v1; `session.developer-message.source`; `kind`; `preserve`. @@ -3798,7 +3828,7 @@ Array of [`ImageOffloadTarget`](#persistence-type-sha256-1cde1bedde79c5e8a56aee9 SHA-256: `96f4a9c81fc0f940ef71528a8cc66731ae4cf1f40d79680c5fe3c39f96d7723c` -Sources: [`packages/core/agent/src/types.ts:40`](../packages/core/agent/src/types.ts) +Sources: [`packages/core/agent/src/types.ts:47`](../packages/core/agent/src/types.ts) One of: @@ -4091,7 +4121,7 @@ Sources: [`packages/llm/llm-retry/src/types.ts:43`](../packages/llm/llm-retry/sr | `step` | required | `number` | | `turn` | required | `number` | - + @@ -4099,15 +4129,15 @@ Sources: [`packages/llm/llm-retry/src/types.ts:43`](../packages/llm/llm-retry/sr ### `Message` -SHA-256: `4f9125fa028a56286fac70176ecc670d12f4402d8cbe112e14f0639f5e8e5c9d` +SHA-256: `7cf091467eb288875ea4e88397003a7e43d95ab75afaf1b42554a8cc766799f2` Sources: [`packages/llm/llm/src/message.ts:196`](../packages/llm/llm/src/message.ts) One of: - [`ToolResultMessage`](#persistence-type-sha256-4b259f7f8f0a44ad5e90b70a73553ced1df27012d126921d397ff403b6c86731) -- [`DeveloperMessage`](#persistence-type-sha256-cebb7e9f3f663e660a531d9764ecfb9fa7fc73c22bf8d628f888e9ecf18c5cf0) -- [`UserMessage`](#persistence-type-sha256-8bca2b4f53ec7745b0484c6e7d3a6beb2ddd6cea82fc81786eaa3c0d065bbc14) +- [`DeveloperMessage`](#persistence-type-sha256-0ef3072084202b084b2a25678fe9014ec339ee3797bc0bb164538ff3fa5e3fcc) +- [`UserMessage`](#persistence-type-sha256-9632024d168f8169ad83fe3639da51c864ebb5444c9fc965ac9342611b2d5263) - [`SystemMessage`](#persistence-type-sha256-09f3f8dc417f278257202714132f98ba7b52957d7b558c4a04456603890eb363) - [`AssistantMessage`](#persistence-type-sha256-fa87537f4a9c1e25602990a8bf686b383e9e00352ec8946e16ae24ba7c1625be) @@ -4184,7 +4214,7 @@ One of: - `"negative"` - `"positive"` - + @@ -4192,7 +4222,7 @@ One of: ### `MessageSource` -SHA-256: `b112459dcb0686091c31ce6d213b2a4934b5ad19d37cc5b599e46f5a9d715591` +SHA-256: `e3e9f4d1e245b470cba77c3e6b94ea141408604d693b2a8437278abc8cfd8e2c` Sources: [`packages/llm/llm/src/message.ts:136`](../packages/llm/llm/src/message.ts) @@ -4254,6 +4284,8 @@ Sources: [`packages/llm/llm/src/message.ts:136`](../packages/llm/llm/src/message | `repeat-tool-reminder` | `"relay"` | none | [`{ kind: "repeat-tool-reminder", form: "relay" }`](#persistence-type-sha256-4991dfc415c9efebc36fa6969e1798c7d2e42396cc1e6e0ac7d7c89c5c5c4aea) | | `repeat-tool-reminder` | `"snapshot"` | `sections`: [`ContextSnapshotSection[]`](#persistence-type-sha256-c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262) | [`{ kind: "repeat-tool-reminder", form: "snapshot" }`](#persistence-type-sha256-db06ef35b27a38425046b2228fa6bce9582abe140d781d363137eb2f4d233a1c) | | `repeat-tool-reminder` | not declared | none | [`{ kind: "repeat-tool-reminder" }`](#persistence-type-sha256-b062078e899958f99ae464514578b8c093924dfe34bd9a835932b420ff2150b1) | +| `retry` | not declared | none | [`{ kind: "retry" }`](#persistence-type-sha256-414a44a96f06339f04657b0e441a2baed954f7e31aa1552f37dbe7107f0df8e9) | +| `retry` | not declared | `rpcId`: `string` | [`{ clientTimeZone?, kind: "retry", principal?, rpcId }`](#persistence-type-sha256-e103228e063b4762f2fb51ee5e50acaddd53a513de107efae09bdebf2958a664) | | `runtime-context` | `"catalog"` | none | [`{ kind: "runtime-context", form: "catalog" }`](#persistence-type-sha256-58015da1c1950a66daf66ae24913d6a1fc6d1d9ac441b155f8847f7f8584ba8e) | | `runtime-context` | `"instructions"` | none | [`{ kind: "runtime-context", form: "instructions" }`](#persistence-type-sha256-0eeb17e1df89b1ebc2902116fe7145e0d321ef0db26e6bdebc802cb8513a01c1) | | `runtime-context` | `"notice"` | `summary`: `string` | [`{ kind: "runtime-context", form: "notice" }`](#persistence-type-sha256-cda2345a9be9d139df49ccbd16dcba716f3440178bf20ff8b801fda1f5eb77dc) | @@ -4317,13 +4349,13 @@ Sources: [`packages/llm/llm/src/message.ts:136`](../packages/llm/llm/src/message | `webhook` | `"notice"` | `deliveryId`: `string`; `provider`: `string`; `ruleId`: `string`; `source`: `string`; `summary`: `string` | [`{ kind: "webhook", form: "notice" }`](#persistence-type-sha256-9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94) | | `working-directory` | not declared | none | [`{ kind: "working-directory" }`](#persistence-type-sha256-2235d957bf4b7c6cf959231d480f0ba1f3a6d72d82981493fb6c7c963021d3d5) | - + ### `Message[]` -SHA-256: `d2b617d9dfcc4e0a4b60732d8ba1effc632e3b75124b93b064c07814a0a716bc` +SHA-256: `2f5b14bc2ccbe8810568df6392f1a8e0c3a2be59440051b60666266519e403ff` -Array of [`Message`](#persistence-type-sha256-4f9125fa028a56286fac70176ecc670d12f4402d8cbe112e14f0639f5e8e5c9d). +Array of [`Message`](#persistence-type-sha256-7cf091467eb288875ea4e88397003a7e43d95ab75afaf1b42554a8cc766799f2). @@ -4354,7 +4386,7 @@ Sources: [`packages/llm/llm/src/message.ts:24`](../packages/llm/llm/src/message. SHA-256: `f09e5e7acf5ff5ccd193ff2eeec015fa2ff7c29d98bc465f8c3302058d47e29b` -Sources: [`packages/core/agent/src/types.ts:79`](../packages/core/agent/src/types.ts) +Sources: [`packages/core/agent/src/types.ts:86`](../packages/core/agent/src/types.ts) | Property | Presence | Type | |---|---|---| @@ -4707,7 +4739,7 @@ Sources: [`packages/session/session-title/src/types.ts:41`](../packages/session/ | `source` | required | [`SessionTitleSource`](#persistence-type-sha256-30d5a537b296685770b1241c4b4bbde19d5a31ff81aa23df73fab5e90b8fc65f) | | `title` | required | `string` | - + @@ -4715,7 +4747,7 @@ Sources: [`packages/session/session-title/src/types.ts:41`](../packages/session/ ### `SessionTitleLlmRequestEventData` -SHA-256: `9808c3d0f0a1be8fe313b4148ba22c691c4d59bd2c67aef3c63cfe8043148c5f` +SHA-256: `8eda0195dd1d4a3fee80f5e0b9f702fdf0f342c27ece60d44d459899b6a30b60` Sources: [`packages/session/session-title-llm/src/index.ts:37`](../packages/session/session-title-llm/src/index.ts) @@ -4723,7 +4755,7 @@ Sources: [`packages/session/session-title-llm/src/index.ts:37`](../packages/sess |---|---|---| | `maxTokens` | required | `number` | | `messageSeqs` | required | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | -| `messages` | required | [`Message[]`](#persistence-type-sha256-d2b617d9dfcc4e0a4b60732d8ba1effc632e3b75124b93b064c07814a0a716bc) | +| `messages` | required | [`Message[]`](#persistence-type-sha256-2f5b14bc2ccbe8810568df6392f1a8e0c3a2be59440051b60666266519e403ff) | | `reasoningEffort` | optional | `string` | | `route` | required | [`SessionTitleModelIdentity`](#persistence-type-sha256-07e1a58c58b593f507b8e7f25723bb0aa42baae6e00f5ecfddf28cb1e64d3235) | | `system` | required | `string` | @@ -5380,7 +5412,7 @@ Sources: [`packages/core/session/src/types.ts:238`](../packages/core/session/src | `interrupted` | not declared | none | [`{ kind: "interrupted" }`](#persistence-type-sha256-56b7c047694875a843ba020dfea3c8380d54ae4ff88ab349e72004dc21e07c23) | | `max-tokens` | not declared | none | [`{ kind: "max-tokens" }`](#persistence-type-sha256-ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7) | - + @@ -5388,7 +5420,7 @@ Sources: [`packages/core/session/src/types.ts:238`](../packages/core/session/src ### `UserMessage` -SHA-256: `8bca2b4f53ec7745b0484c6e7d3a6beb2ddd6cea82fc81786eaa3c0d065bbc14` +SHA-256: `9632024d168f8169ad83fe3639da51c864ebb5444c9fc965ac9342611b2d5263` Sources: [`packages/llm/llm/src/message.ts:162`](../packages/llm/llm/src/message.ts) @@ -5397,19 +5429,19 @@ Sources: [`packages/llm/llm/src/message.ts:162`](../packages/llm/llm/src/message | `content` | required | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) | | `id` | required | `string` | | `role` | required | `"user"` | -| `source` | required | [`MessageSource`](#persistence-type-sha256-b112459dcb0686091c31ce6d213b2a4934b5ad19d37cc5b599e46f5a9d715591) | +| `source` | required | [`MessageSource`](#persistence-type-sha256-e3e9f4d1e245b470cba77c3e6b94ea141408604d693b2a8437278abc8cfd8e2c) | Source compatibility: `source` — `session-source-attribution` v1; `session.user-message.source`; `kind`; `preserve`. Attribution-only additions: `action-cards`, `tmux-context`, `user-question-reply`, `working-directory`. - + ### `UserMessage[]` -SHA-256: `aada2771a250136cc3a839b0507e9c6502622a2897716903942297bcb6661292` +SHA-256: `347c1718600bbb3814ae6b4df06e6aceae35e8a8e8cb3c39ac28b2e932c5abd6` -Array of [`UserMessage`](#persistence-type-sha256-8bca2b4f53ec7745b0484c6e7d3a6beb2ddd6cea82fc81786eaa3c0d065bbc14). +Array of [`UserMessage`](#persistence-type-sha256-9632024d168f8169ad83fe3639da51c864ebb5444c9fc965ac9342611b2d5263). @@ -5673,6 +5705,17 @@ One of: - `"fork"` - `"fresh"` + + +### `union (2 variants)` + +SHA-256: `671e72b8036a3afb018b38ca5ae9ee3aabe6a314647411655b4b9497b75010b1` + +| kind | Form property | Other required fields | Full definition | +|---|---|---|---| +| `retry` | not declared | none | [`{ kind: "retry" }`](#persistence-type-sha256-414a44a96f06339f04657b0e441a2baed954f7e31aa1552f37dbe7107f0df8e9) | +| `retry` | not declared | `rpcId`: `string` | [`{ clientTimeZone?, kind: "retry", principal?, rpcId }`](#persistence-type-sha256-e103228e063b4762f2fb51ee5e50acaddd53a513de107efae09bdebf2958a664) | + ### `union (2 variants)` @@ -5991,6 +6034,21 @@ SHA-256: `cbbe8377ee744964537dbafd83607219e58ba79a13f6db0d0645c7ad3d5e8a09` | `mode` | required | `"external"` | | `version` | required | `2` | + + +### `{ clientTimeZone?, kind: "retry", principal?, rpcId }` + +SHA-256: `e103228e063b4762f2fb51ee5e50acaddd53a513de107efae09bdebf2958a664` + +Sources: [`packages/api/session-controller/src/types.ts:399`](../packages/api/session-controller/src/types.ts) + +| Property | Presence | Type | +|---|---|---| +| `clientTimeZone` | optional | `string` | +| `kind` | required | `"retry"` | +| `principal` | optional | [`Principal`](#persistence-type-sha256-deebfc7c62177ea840f23c6362b8615bc78ffa93377dd5d243c04e541c413af5) | +| `rpcId` | required | `string` | + ### `{ code, name, reason? }` @@ -6294,18 +6352,18 @@ Sources: [`packages/core/session/src/types.ts:398`](../packages/core/session/src | `reason` | required | [`RequestHeaderReason`](#persistence-type-sha256-530da6c0267813bb4511af72883cc518834e624e3b3220a7062955fd57a788a6) | | `startsSeries` | optional | `true` | - + ### `{ headerSeq?, message, step, turn }` -SHA-256: `12f9a5a7506bca46d6ef8e3c71826c4bf40e1389bfe0236cfffaea0c3fb367d2` +SHA-256: `d582ddae3f21edf55b6f50454e3015ef78ed3788fee4670827f2c3c4bd0b47e2` Sources: [`packages/core/session/src/types.ts:317`](../packages/core/session/src/types.ts) | Property | Presence | Type | |---|---|---| | `headerSeq` | optional | `number` | -| `message` | required | [`DeveloperMessage`](#persistence-type-sha256-cebb7e9f3f663e660a531d9764ecfb9fa7fc73c22bf8d628f888e9ecf18c5cf0) | +| `message` | required | [`DeveloperMessage`](#persistence-type-sha256-0ef3072084202b084b2a25678fe9014ec339ee3797bc0bb164538ff3fa5e3fcc) | | `step` | required | `number` | | `turn` | required | `number` | @@ -6389,17 +6447,32 @@ Sources: [`packages/core/session/src/types.ts:435`](../packages/core/session/src |---|---|---| | `inherited` | optional | `true` | - + -### `{ inserted, outcome?, removedCount?, start, … }` +### `{ inputId, source, step, turn }` -SHA-256: `c433ef580a5b229bbe68274a7164aa3a8e8040d7217f010b610ca0696a84253c` +SHA-256: `2a668a48dec1739752123d67c3bc254412d6743dbe3347365e0ed83a542f2bc3` -Sources: [`packages/core/agent/src/types.ts:110`](../packages/core/agent/src/types.ts) +Sources: [`packages/core/agent/src/types.ts:113`](../packages/core/agent/src/types.ts) | Property | Presence | Type | |---|---|---| -| `inserted` | required | [`UserMessage[]`](#persistence-type-sha256-aada2771a250136cc3a839b0507e9c6502622a2897716903942297bcb6661292) | +| `inputId` | required | `string` | +| `source` | required | [`union (2 variants)`](#persistence-type-sha256-671e72b8036a3afb018b38ca5ae9ee3aabe6a314647411655b4b9497b75010b1) | +| `step` | required | `number` | +| `turn` | required | `number` | + + + +### `{ inserted, outcome?, removedCount?, start, … }` + +SHA-256: `c66c6c3b411abf46c1f9b99e0019c10c1b6d13ee716a4b5e88f7e1b65feb3039` + +Sources: [`packages/core/agent/src/types.ts:119`](../packages/core/agent/src/types.ts) + +| Property | Presence | Type | +|---|---|---| +| `inserted` | required | [`UserMessage[]`](#persistence-type-sha256-347c1718600bbb3814ae6b4df06e6aceae35e8a8e8cb3c39ac28b2e932c5abd6) | | `outcome` | optional | `"canceled"` | | `removedCount` | optional | `number` | | `start` | required | `number` | @@ -7147,6 +7220,18 @@ SHA-256: `db06ef35b27a38425046b2228fa6bce9582abe140d781d363137eb2f4d233a1c` | `kind` | required | `"repeat-tool-reminder"` | | `sections` | required | [`ContextSnapshotSection[]`](#persistence-type-sha256-c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262) | + + +### `{ kind: "retry" }` + +SHA-256: `414a44a96f06339f04657b0e441a2baed954f7e31aa1552f37dbe7107f0df8e9` + +Sources: [`packages/core/agent/src/types.ts:24`](../packages/core/agent/src/types.ts) + +| Property | Presence | Type | +|---|---|---| +| `kind` | required | `"retry"` | + ### `{ kind: "runtime-context" }` @@ -8046,22 +8131,38 @@ SHA-256: `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | `time` | required | `number` | | `type` | required | `"agent-preset/selected"` | - + ### `{ type: "agent/inbox/spliced" }` -SHA-256: `9e8043098fb89db6d7c92a92c70146c5c8d24fa88f733c687fa29e5e7a58ab30` +SHA-256: `a6d8c25f4b2ca2d137672a8a278ee1398df3f3cb60f5d32d8e3007c68299b664` | Property | Presence | Type | |---|---|---| -| `data` | required | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-c433ef580a5b229bbe68274a7164aa3a8e8040d7217f010b610ca0696a84253c) | +| `data` | required | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-c66c6c3b411abf46c1f9b99e0019c10c1b6d13ee716a4b5e88f7e1b65feb3039) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `time` | required | `number` | | `type` | required | `"agent/inbox/spliced"` | + + + + +### `{ type: "agent/retry" }` + +SHA-256: `d8dde9629b7323b03c1c3fb0db75d6b72c73666dc0240ab7d3e41b9c0c40fc7c` + +| Property | Presence | Type | +|---|---|---| +| `data` | required | [`{ inputId, source, step, turn }`](#persistence-type-sha256-2a668a48dec1739752123d67c3bc254412d6743dbe3347365e0ed83a542f2bc3) | +| `ignorable` | optional | `true` | +| `seq` | required | `number` | +| `time` | required | `number` | +| `type` | required | `"agent/retry"` | + @@ -8297,17 +8398,17 @@ SHA-256: `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | `time` | required | `number` | | `type` | required | `"deliverables/presented"` | - + ### `{ type: "developer/message" }` -SHA-256: `95acdbd066cf445d37092e5fdfe2e8717ef6ca1aed50f70e2e6037a6989a1f2f` +SHA-256: `92b82e8b7bbb38b3adeb2c07eb76d0f0e0d18c1729808def7004ac2bc9baa583` | Property | Presence | Type | |---|---|---| -| `data` | required | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-12f9a5a7506bca46d6ef8e3c71826c4bf40e1389bfe0236cfffaea0c3fb367d2) | +| `data` | required | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-d582ddae3f21edf55b6f50454e3015ef78ed3788fee4670827f2c3c4bd0b47e2) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `sourceEventSeqs` | optional | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | @@ -8707,17 +8808,17 @@ SHA-256: `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | `time` | required | `number` | | `type` | required | `"session/title"` | - + ### `{ type: "session/title-llm-request" }` -SHA-256: `eaa9125aec0fb28478096c46e73e21bf512ab0d84d826bfb41e7308707734cb5` +SHA-256: `fb28ad7d7aabceb5bda505cf027e0e22354d7f31757337e7e51a4b592feac172` | Property | Presence | Type | |---|---|---| -| `data` | required | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-9808c3d0f0a1be8fe313b4148ba22c691c4d59bd2c67aef3c63cfe8043148c5f) | +| `data` | required | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-8eda0195dd1d4a3fee80f5e0b9f702fdf0f342c27ece60d44d459899b6a30b60) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `time` | required | `number` | @@ -9140,17 +9241,17 @@ Sources: [`packages/llm/llm/src/types.ts:476`](../packages/llm/llm/src/types.ts) | `type` | required | `"usage"` | | `usage` | required | [`TokenUsage`](#persistence-type-sha256-fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394) | - + ### `{ type: "user/message" }` -SHA-256: `39adb7fce8475af6402acd925ea79697b3341c22422ac32833d71217d84e987f` +SHA-256: `c5bbcb3435908af2036df7105883e0a49c30ecc1fae4420aa5c4cf944066eff5` | Property | Presence | Type | |---|---|---| -| `data` | required | [`UserMessage`](#persistence-type-sha256-8bca2b4f53ec7745b0484c6e7d3a6beb2ddd6cea82fc81786eaa3c0d065bbc14) | +| `data` | required | [`UserMessage`](#persistence-type-sha256-9632024d168f8169ad83fe3639da51c864ebb5444c9fc965ac9342611b2d5263) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `sourceEventSeqs` | optional | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index d75ff6f2b1..47f1c30ed9 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -25,7 +25,8 @@ | `JsonlHeaderLine` | header | `18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f` | [`HeaderLine`](#persistence-type-sha256-18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f) | | `SessionEventEnvelope` | envelope | `5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc` | [`union (3 variants)`](#persistence-type-sha256-5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc) | | `event:agent-preset/selected` | event | `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | [`{ type: "agent-preset/selected" }`](#persistence-type-sha256-a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b) | -| `event:agent/inbox/spliced` | event | `9e8043098fb89db6d7c92a92c70146c5c8d24fa88f733c687fa29e5e7a58ab30` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-9e8043098fb89db6d7c92a92c70146c5c8d24fa88f733c687fa29e5e7a58ab30) | +| `event:agent/inbox/spliced` | event | `a6d8c25f4b2ca2d137672a8a278ee1398df3f3cb60f5d32d8e3007c68299b664` | [`{ type: "agent/inbox/spliced" }`](#persistence-type-sha256-a6d8c25f4b2ca2d137672a8a278ee1398df3f3cb60f5d32d8e3007c68299b664) | +| `event:agent/retry` | event | `d8dde9629b7323b03c1c3fb0db75d6b72c73666dc0240ab7d3e41b9c0c40fc7c` | [`{ type: "agent/retry" }`](#persistence-type-sha256-d8dde9629b7323b03c1c3fb0db75d6b72c73666dc0240ab7d3e41b9c0c40fc7c) | | `event:approval/asked` | event | `3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1` | [`{ type: "approval/asked" }`](#persistence-type-sha256-3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1) | | `event:approval/decided` | event | `bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3` | [`{ type: "approval/decided" }`](#persistence-type-sha256-bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3) | | `event:approval/policy` | event | `26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78` | [`{ type: "approval/policy" }`](#persistence-type-sha256-26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78) | @@ -38,7 +39,7 @@ | `event:compaction/start` | event | `db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965` | [`{ type: "compaction/start" }`](#persistence-type-sha256-db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965) | | `event:compaction/summary` | event | `e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602` | [`{ type: "compaction/summary" }`](#persistence-type-sha256-e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602) | | `event:deliverables/presented` | event | `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | [`{ type: "deliverables/presented" }`](#persistence-type-sha256-13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176) | -| `event:developer/message` | event | `95acdbd066cf445d37092e5fdfe2e8717ef6ca1aed50f70e2e6037a6989a1f2f` | [`{ type: "developer/message" }`](#persistence-type-sha256-95acdbd066cf445d37092e5fdfe2e8717ef6ca1aed50f70e2e6037a6989a1f2f) | +| `event:developer/message` | event | `92b82e8b7bbb38b3adeb2c07eb76d0f0e0d18c1729808def7004ac2bc9baa583` | [`{ type: "developer/message" }`](#persistence-type-sha256-92b82e8b7bbb38b3adeb2c07eb76d0f0e0d18c1729808def7004ac2bc9baa583) | | `event:feedback/message-delete` | event | `3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796` | [`{ type: "feedback/message-delete" }`](#persistence-type-sha256-3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796) | | `event:feedback/message-put` | event | `b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf` | [`{ type: "feedback/message-put" }`](#persistence-type-sha256-b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf) | | `event:feedback/record` | event | `b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8` | [`{ type: "feedback/record" }`](#persistence-type-sha256-b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8) | @@ -60,7 +61,7 @@ | `event:session-log-deepseek/delivery-accepted` | event | `d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9` | [`{ type: "session-log-deepseek/delivery-accepted" }`](#persistence-type-sha256-d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9) | | `event:session/end-seed` | event | `5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e` | [`{ type: "session/end-seed" }`](#persistence-type-sha256-5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e) | | `event:session/title` | event | `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | [`{ type: "session/title" }`](#persistence-type-sha256-1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5) | -| `event:session/title-llm-request` | event | `eaa9125aec0fb28478096c46e73e21bf512ab0d84d826bfb41e7308707734cb5` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-eaa9125aec0fb28478096c46e73e21bf512ab0d84d826bfb41e7308707734cb5) | +| `event:session/title-llm-request` | event | `fb28ad7d7aabceb5bda505cf027e0e22354d7f31757337e7e51a4b592feac172` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-fb28ad7d7aabceb5bda505cf027e0e22354d7f31757337e7e51a4b592feac172) | | `event:step/end` | event | `e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05` | [`{ type: "step/end" }`](#persistence-type-sha256-e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05) | | `event:step/start` | event | `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | [`{ type: "step/start" }`](#persistence-type-sha256-4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986) | | `event:subagent/catalog` | event | `53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac) | @@ -82,7 +83,7 @@ | `event:tool/result` | event | `7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397` | [`{ type: "tool/result" }`](#persistence-type-sha256-7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397) | | `event:turn/end` | event | `0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce` | [`{ type: "turn/end" }`](#persistence-type-sha256-0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce) | | `event:turn/start` | event | `aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8` | [`{ type: "turn/start" }`](#persistence-type-sha256-aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8) | -| `event:user/message` | event | `39adb7fce8475af6402acd925ea79697b3341c22422ac32833d71217d84e987f` | [`{ type: "user/message" }`](#persistence-type-sha256-39adb7fce8475af6402acd925ea79697b3341c22422ac32833d71217d84e987f) | +| `event:user/message` | event | `c5bbcb3435908af2036df7105883e0a49c30ecc1fae4420aa5c4cf944066eff5` | [`{ type: "user/message" }`](#persistence-type-sha256-c5bbcb3435908af2036df7105883e0a49c30ecc1fae4420aa5c4cf944066eff5) | | `event:web/deepseek-search-llm-request` | event | `0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52` | [`{ type: "web/deepseek-search-llm-request" }`](#persistence-type-sha256-0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52) | | `event:working-directory/change` | event | `124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3` | [`{ type: "working-directory/change" }`](#persistence-type-sha256-124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3) | | `event:workspace/changes` | event | `e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72` | [`{ type: "workspace/changes" }`](#persistence-type-sha256-e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72) | @@ -187,7 +188,20 @@ export type SessionEvent = { } ``` -来源:[`packages/core/agent/src/types.ts:110`](../packages/core/agent/src/types.ts) +来源:[`packages/core/agent/src/types.ts:119`](../packages/core/agent/src/types.ts) + + + +#### `agent/retry` — log-only + +```ts persistence-catalog +/** An admitted retry control, correlated with its durable inbox input; never enters model history. */ +'agent/retry': { turn: number; step: number; inputId: MessageId; source: Extract } +``` + +类型:[MessageSource](subsystems/core.zh.md) + +来源:[`packages/core/agent/src/types.ts:113`](../packages/core/agent/src/types.ts) ### `agent-preset/*` @@ -702,7 +716,7 @@ export type SessionEvent = { 'model/selection': ModelSelection ``` -来源:[`packages/core/agent/src/types.ts:121`](../packages/core/agent/src/types.ts) +来源:[`packages/core/agent/src/types.ts:130`](../packages/core/agent/src/types.ts) ### `permission/*` @@ -1407,6 +1421,14 @@ SHA-256: `829a2fb3be6efd4ba36aef8ed2d381a4c6fb2a0aaf50c823a6f3f8290786b809` `"agent/inbox/spliced"` + + +### `"agent/retry"` + +SHA-256: `30093f9f63a120cb932f07577594c04afcec3f0bd02859154a6e5b0e79a46359` + +`"agent/retry"` + ### `"allowed-once"` @@ -2429,6 +2451,14 @@ SHA-256: `34d1ab5c5df378186d6054b1a1beea9a41e41965d6f336f0bc0025c441a82e66` `"resume"` + + +### `"retry"` + +SHA-256: `63ba8895cc09e4693cf0819fdcd18c084bef195febda5b6a23af279b3852be36` + +`"retry"` + ### `"runtime-context"` @@ -3377,7 +3407,7 @@ SHA-256: `abd6ca62a5ab166553848324a23fef90eed7f59c8021bd63e7ea5bc6fd666b0c` | `body` | 必需 | [`{ max_tokens, messages, model, thinking?, … }`](#persistence-type-sha256-9c001ecb5e1d3c3894410db2a1e383842e5f8454fb52182338686158abc1b3ee) | | `endpoint` | 必需 | `string` | - + @@ -3385,7 +3415,7 @@ SHA-256: `abd6ca62a5ab166553848324a23fef90eed7f59c8021bd63e7ea5bc6fd666b0c` ### `DeveloperMessage` -SHA-256: `cebb7e9f3f663e660a531d9764ecfb9fa7fc73c22bf8d628f888e9ecf18c5cf0` +SHA-256: `0ef3072084202b084b2a25678fe9014ec339ee3797bc0bb164538ff3fa5e3fcc` 来源:[`packages/llm/llm/src/message.ts:157`](../packages/llm/llm/src/message.ts) @@ -3394,7 +3424,7 @@ SHA-256: `cebb7e9f3f663e660a531d9764ecfb9fa7fc73c22bf8d628f888e9ecf18c5cf0` | `content` | 必需 | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) | | `id` | 必需 | `string` | | `role` | 必需 | `"developer"` | -| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-b112459dcb0686091c31ce6d213b2a4934b5ad19d37cc5b599e46f5a9d715591) | +| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-e3e9f4d1e245b470cba77c3e6b94ea141408604d693b2a8437278abc8cfd8e2c) | 消息源兼容策略:`source` — `session-source-attribution` v1; `session.developer-message.source`; `kind`; `preserve`. @@ -3800,7 +3830,7 @@ SHA-256: `9e2c06d8653585b33d72d5c1a80da79c8a24a88c284409bca15cc0f0ead5ebfb` SHA-256: `96f4a9c81fc0f940ef71528a8cc66731ae4cf1f40d79680c5fe3c39f96d7723c` -来源:[`packages/core/agent/src/types.ts:40`](../packages/core/agent/src/types.ts) +来源:[`packages/core/agent/src/types.ts:47`](../packages/core/agent/src/types.ts) 以下类型之一: @@ -4093,7 +4123,7 @@ SHA-256: `6f72ece8f317cd080c4f20a93f1bf709965f5b81ac4669528f5a023191cfcf20` | `step` | 必需 | `number` | | `turn` | 必需 | `number` | - + @@ -4101,15 +4131,15 @@ SHA-256: `6f72ece8f317cd080c4f20a93f1bf709965f5b81ac4669528f5a023191cfcf20` ### `Message` -SHA-256: `4f9125fa028a56286fac70176ecc670d12f4402d8cbe112e14f0639f5e8e5c9d` +SHA-256: `7cf091467eb288875ea4e88397003a7e43d95ab75afaf1b42554a8cc766799f2` 来源:[`packages/llm/llm/src/message.ts:196`](../packages/llm/llm/src/message.ts) 以下类型之一: - [`ToolResultMessage`](#persistence-type-sha256-4b259f7f8f0a44ad5e90b70a73553ced1df27012d126921d397ff403b6c86731) -- [`DeveloperMessage`](#persistence-type-sha256-cebb7e9f3f663e660a531d9764ecfb9fa7fc73c22bf8d628f888e9ecf18c5cf0) -- [`UserMessage`](#persistence-type-sha256-8bca2b4f53ec7745b0484c6e7d3a6beb2ddd6cea82fc81786eaa3c0d065bbc14) +- [`DeveloperMessage`](#persistence-type-sha256-0ef3072084202b084b2a25678fe9014ec339ee3797bc0bb164538ff3fa5e3fcc) +- [`UserMessage`](#persistence-type-sha256-9632024d168f8169ad83fe3639da51c864ebb5444c9fc965ac9342611b2d5263) - [`SystemMessage`](#persistence-type-sha256-09f3f8dc417f278257202714132f98ba7b52957d7b558c4a04456603890eb363) - [`AssistantMessage`](#persistence-type-sha256-fa87537f4a9c1e25602990a8bf686b383e9e00352ec8946e16ae24ba7c1625be) @@ -4186,7 +4216,7 @@ SHA-256: `390eed1028aba02774f14408647acec10ec5516e97d4f15ecde2b9989d5a69f2` - `"negative"` - `"positive"` - + @@ -4194,7 +4224,7 @@ SHA-256: `390eed1028aba02774f14408647acec10ec5516e97d4f15ecde2b9989d5a69f2` ### `MessageSource` -SHA-256: `b112459dcb0686091c31ce6d213b2a4934b5ad19d37cc5b599e46f5a9d715591` +SHA-256: `e3e9f4d1e245b470cba77c3e6b94ea141408604d693b2a8437278abc8cfd8e2c` 来源:[`packages/llm/llm/src/message.ts:136`](../packages/llm/llm/src/message.ts) @@ -4256,6 +4286,8 @@ SHA-256: `b112459dcb0686091c31ce6d213b2a4934b5ad19d37cc5b599e46f5a9d715591` | `repeat-tool-reminder` | `"relay"` | 无 | [`{ kind: "repeat-tool-reminder", form: "relay" }`](#persistence-type-sha256-4991dfc415c9efebc36fa6969e1798c7d2e42396cc1e6e0ac7d7c89c5c5c4aea) | | `repeat-tool-reminder` | `"snapshot"` | `sections`: [`ContextSnapshotSection[]`](#persistence-type-sha256-c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262) | [`{ kind: "repeat-tool-reminder", form: "snapshot" }`](#persistence-type-sha256-db06ef35b27a38425046b2228fa6bce9582abe140d781d363137eb2f4d233a1c) | | `repeat-tool-reminder` | 未声明 | 无 | [`{ kind: "repeat-tool-reminder" }`](#persistence-type-sha256-b062078e899958f99ae464514578b8c093924dfe34bd9a835932b420ff2150b1) | +| `retry` | 未声明 | 无 | [`{ kind: "retry" }`](#persistence-type-sha256-414a44a96f06339f04657b0e441a2baed954f7e31aa1552f37dbe7107f0df8e9) | +| `retry` | 未声明 | `rpcId`: `string` | [`{ clientTimeZone?, kind: "retry", principal?, rpcId }`](#persistence-type-sha256-e103228e063b4762f2fb51ee5e50acaddd53a513de107efae09bdebf2958a664) | | `runtime-context` | `"catalog"` | 无 | [`{ kind: "runtime-context", form: "catalog" }`](#persistence-type-sha256-58015da1c1950a66daf66ae24913d6a1fc6d1d9ac441b155f8847f7f8584ba8e) | | `runtime-context` | `"instructions"` | 无 | [`{ kind: "runtime-context", form: "instructions" }`](#persistence-type-sha256-0eeb17e1df89b1ebc2902116fe7145e0d321ef0db26e6bdebc802cb8513a01c1) | | `runtime-context` | `"notice"` | `summary`: `string` | [`{ kind: "runtime-context", form: "notice" }`](#persistence-type-sha256-cda2345a9be9d139df49ccbd16dcba716f3440178bf20ff8b801fda1f5eb77dc) | @@ -4319,13 +4351,13 @@ SHA-256: `b112459dcb0686091c31ce6d213b2a4934b5ad19d37cc5b599e46f5a9d715591` | `webhook` | `"notice"` | `deliveryId`: `string`; `provider`: `string`; `ruleId`: `string`; `source`: `string`; `summary`: `string` | [`{ kind: "webhook", form: "notice" }`](#persistence-type-sha256-9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94) | | `working-directory` | 未声明 | 无 | [`{ kind: "working-directory" }`](#persistence-type-sha256-2235d957bf4b7c6cf959231d480f0ba1f3a6d72d82981493fb6c7c963021d3d5) | - + ### `Message[]` -SHA-256: `d2b617d9dfcc4e0a4b60732d8ba1effc632e3b75124b93b064c07814a0a716bc` +SHA-256: `2f5b14bc2ccbe8810568df6392f1a8e0c3a2be59440051b60666266519e403ff` -[`Message`](#persistence-type-sha256-4f9125fa028a56286fac70176ecc670d12f4402d8cbe112e14f0639f5e8e5c9d) 的数组。 +[`Message`](#persistence-type-sha256-7cf091467eb288875ea4e88397003a7e43d95ab75afaf1b42554a8cc766799f2) 的数组。 @@ -4356,7 +4388,7 @@ SHA-256: `719c51f67074f97034a6c94297fe1f4b5d3c8d392c57586dab732aa5398c7a91` SHA-256: `f09e5e7acf5ff5ccd193ff2eeec015fa2ff7c29d98bc465f8c3302058d47e29b` -来源:[`packages/core/agent/src/types.ts:79`](../packages/core/agent/src/types.ts) +来源:[`packages/core/agent/src/types.ts:86`](../packages/core/agent/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -4709,7 +4741,7 @@ SHA-256: `b56f6a885da3dc394c69a3e3cc5c0cfc4da15601b63fda4989eee8f40efc5be4` | `source` | 必需 | [`SessionTitleSource`](#persistence-type-sha256-30d5a537b296685770b1241c4b4bbde19d5a31ff81aa23df73fab5e90b8fc65f) | | `title` | 必需 | `string` | - + @@ -4717,7 +4749,7 @@ SHA-256: `b56f6a885da3dc394c69a3e3cc5c0cfc4da15601b63fda4989eee8f40efc5be4` ### `SessionTitleLlmRequestEventData` -SHA-256: `9808c3d0f0a1be8fe313b4148ba22c691c4d59bd2c67aef3c63cfe8043148c5f` +SHA-256: `8eda0195dd1d4a3fee80f5e0b9f702fdf0f342c27ece60d44d459899b6a30b60` 来源:[`packages/session/session-title-llm/src/index.ts:37`](../packages/session/session-title-llm/src/index.ts) @@ -4725,7 +4757,7 @@ SHA-256: `9808c3d0f0a1be8fe313b4148ba22c691c4d59bd2c67aef3c63cfe8043148c5f` |---|---|---| | `maxTokens` | 必需 | `number` | | `messageSeqs` | 必需 | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | -| `messages` | 必需 | [`Message[]`](#persistence-type-sha256-d2b617d9dfcc4e0a4b60732d8ba1effc632e3b75124b93b064c07814a0a716bc) | +| `messages` | 必需 | [`Message[]`](#persistence-type-sha256-2f5b14bc2ccbe8810568df6392f1a8e0c3a2be59440051b60666266519e403ff) | | `reasoningEffort` | 可选 | `string` | | `route` | 必需 | [`SessionTitleModelIdentity`](#persistence-type-sha256-07e1a58c58b593f507b8e7f25723bb0aa42baae6e00f5ecfddf28cb1e64d3235) | | `system` | 必需 | `string` | @@ -5382,7 +5414,7 @@ SHA-256: `f1687f982fa3d41ef29e159feed154ecd13af4dd209acaee16038f223f2e9402` | `interrupted` | 未声明 | 无 | [`{ kind: "interrupted" }`](#persistence-type-sha256-56b7c047694875a843ba020dfea3c8380d54ae4ff88ab349e72004dc21e07c23) | | `max-tokens` | 未声明 | 无 | [`{ kind: "max-tokens" }`](#persistence-type-sha256-ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7) | - + @@ -5390,7 +5422,7 @@ SHA-256: `f1687f982fa3d41ef29e159feed154ecd13af4dd209acaee16038f223f2e9402` ### `UserMessage` -SHA-256: `8bca2b4f53ec7745b0484c6e7d3a6beb2ddd6cea82fc81786eaa3c0d065bbc14` +SHA-256: `9632024d168f8169ad83fe3639da51c864ebb5444c9fc965ac9342611b2d5263` 来源:[`packages/llm/llm/src/message.ts:162`](../packages/llm/llm/src/message.ts) @@ -5399,19 +5431,19 @@ SHA-256: `8bca2b4f53ec7745b0484c6e7d3a6beb2ddd6cea82fc81786eaa3c0d065bbc14` | `content` | 必需 | [`ContentBlock[]`](#persistence-type-sha256-b52f215a6cd691058a3413c12f161fa3b563a2f4895adb3df6f744ec4b0a6ef1) | | `id` | 必需 | `string` | | `role` | 必需 | `"user"` | -| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-b112459dcb0686091c31ce6d213b2a4934b5ad19d37cc5b599e46f5a9d715591) | +| `source` | 必需 | [`MessageSource`](#persistence-type-sha256-e3e9f4d1e245b470cba77c3e6b94ea141408604d693b2a8437278abc8cfd8e2c) | 消息源兼容策略:`source` — `session-source-attribution` v1; `session.user-message.source`; `kind`; `preserve`. 仅表示归属的新增 kind:`action-cards`, `tmux-context`, `user-question-reply`, `working-directory`. - + ### `UserMessage[]` -SHA-256: `aada2771a250136cc3a839b0507e9c6502622a2897716903942297bcb6661292` +SHA-256: `347c1718600bbb3814ae6b4df06e6aceae35e8a8e8cb3c39ac28b2e932c5abd6` -[`UserMessage`](#persistence-type-sha256-8bca2b4f53ec7745b0484c6e7d3a6beb2ddd6cea82fc81786eaa3c0d065bbc14) 的数组。 +[`UserMessage`](#persistence-type-sha256-9632024d168f8169ad83fe3639da51c864ebb5444c9fc965ac9342611b2d5263) 的数组。 @@ -5675,6 +5707,17 @@ SHA-256: `50cf02d875f8a1f1411f4a6fbcffab9120ccef5f74a47331fbf29480e5f68f48` - `"fork"` - `"fresh"` + + +### `union (2 variants)` + +SHA-256: `671e72b8036a3afb018b38ca5ae9ee3aabe6a314647411655b4b9497b75010b1` + +| kind | form 属性 | 其他必需字段 | 完整定义 | +|---|---|---|---| +| `retry` | 未声明 | 无 | [`{ kind: "retry" }`](#persistence-type-sha256-414a44a96f06339f04657b0e441a2baed954f7e31aa1552f37dbe7107f0df8e9) | +| `retry` | 未声明 | `rpcId`: `string` | [`{ clientTimeZone?, kind: "retry", principal?, rpcId }`](#persistence-type-sha256-e103228e063b4762f2fb51ee5e50acaddd53a513de107efae09bdebf2958a664) | + ### `union (2 variants)` @@ -5993,6 +6036,21 @@ SHA-256: `cbbe8377ee744964537dbafd83607219e58ba79a13f6db0d0645c7ad3d5e8a09` | `mode` | 必需 | `"external"` | | `version` | 必需 | `2` | + + +### `{ clientTimeZone?, kind: "retry", principal?, rpcId }` + +SHA-256: `e103228e063b4762f2fb51ee5e50acaddd53a513de107efae09bdebf2958a664` + +来源:[`packages/api/session-controller/src/types.ts:399`](../packages/api/session-controller/src/types.ts) + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `clientTimeZone` | 可选 | `string` | +| `kind` | 必需 | `"retry"` | +| `principal` | 可选 | [`Principal`](#persistence-type-sha256-deebfc7c62177ea840f23c6362b8615bc78ffa93377dd5d243c04e541c413af5) | +| `rpcId` | 必需 | `string` | + ### `{ code, name, reason? }` @@ -6296,18 +6354,18 @@ SHA-256: `ad0c38a0adb82a94c925456df41586d9b5f9202fca18593eae2213d4445b4585` | `reason` | 必需 | [`RequestHeaderReason`](#persistence-type-sha256-530da6c0267813bb4511af72883cc518834e624e3b3220a7062955fd57a788a6) | | `startsSeries` | 可选 | `true` | - + ### `{ headerSeq?, message, step, turn }` -SHA-256: `12f9a5a7506bca46d6ef8e3c71826c4bf40e1389bfe0236cfffaea0c3fb367d2` +SHA-256: `d582ddae3f21edf55b6f50454e3015ef78ed3788fee4670827f2c3c4bd0b47e2` 来源:[`packages/core/session/src/types.ts:317`](../packages/core/session/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| | `headerSeq` | 可选 | `number` | -| `message` | 必需 | [`DeveloperMessage`](#persistence-type-sha256-cebb7e9f3f663e660a531d9764ecfb9fa7fc73c22bf8d628f888e9ecf18c5cf0) | +| `message` | 必需 | [`DeveloperMessage`](#persistence-type-sha256-0ef3072084202b084b2a25678fe9014ec339ee3797bc0bb164538ff3fa5e3fcc) | | `step` | 必需 | `number` | | `turn` | 必需 | `number` | @@ -6391,17 +6449,32 @@ SHA-256: `17d1afb770d9941936130996da00dc86782cfef731d8d6526162c301256a4ac3` |---|---|---| | `inherited` | 可选 | `true` | - + -### `{ inserted, outcome?, removedCount?, start, … }` +### `{ inputId, source, step, turn }` -SHA-256: `c433ef580a5b229bbe68274a7164aa3a8e8040d7217f010b610ca0696a84253c` +SHA-256: `2a668a48dec1739752123d67c3bc254412d6743dbe3347365e0ed83a542f2bc3` -来源:[`packages/core/agent/src/types.ts:110`](../packages/core/agent/src/types.ts) +来源:[`packages/core/agent/src/types.ts:113`](../packages/core/agent/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| -| `inserted` | 必需 | [`UserMessage[]`](#persistence-type-sha256-aada2771a250136cc3a839b0507e9c6502622a2897716903942297bcb6661292) | +| `inputId` | 必需 | `string` | +| `source` | 必需 | [`union (2 variants)`](#persistence-type-sha256-671e72b8036a3afb018b38ca5ae9ee3aabe6a314647411655b4b9497b75010b1) | +| `step` | 必需 | `number` | +| `turn` | 必需 | `number` | + + + +### `{ inserted, outcome?, removedCount?, start, … }` + +SHA-256: `c66c6c3b411abf46c1f9b99e0019c10c1b6d13ee716a4b5e88f7e1b65feb3039` + +来源:[`packages/core/agent/src/types.ts:119`](../packages/core/agent/src/types.ts) + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `inserted` | 必需 | [`UserMessage[]`](#persistence-type-sha256-347c1718600bbb3814ae6b4df06e6aceae35e8a8e8cb3c39ac28b2e932c5abd6) | | `outcome` | 可选 | `"canceled"` | | `removedCount` | 可选 | `number` | | `start` | 必需 | `number` | @@ -7149,6 +7222,18 @@ SHA-256: `db06ef35b27a38425046b2228fa6bce9582abe140d781d363137eb2f4d233a1c` | `kind` | 必需 | `"repeat-tool-reminder"` | | `sections` | 必需 | [`ContextSnapshotSection[]`](#persistence-type-sha256-c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262) | + + +### `{ kind: "retry" }` + +SHA-256: `414a44a96f06339f04657b0e441a2baed954f7e31aa1552f37dbe7107f0df8e9` + +来源:[`packages/core/agent/src/types.ts:24`](../packages/core/agent/src/types.ts) + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `kind` | 必需 | `"retry"` | + ### `{ kind: "runtime-context" }` @@ -8048,22 +8133,38 @@ SHA-256: `a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b` | `time` | 必需 | `number` | | `type` | 必需 | `"agent-preset/selected"` | - + ### `{ type: "agent/inbox/spliced" }` -SHA-256: `9e8043098fb89db6d7c92a92c70146c5c8d24fa88f733c687fa29e5e7a58ab30` +SHA-256: `a6d8c25f4b2ca2d137672a8a278ee1398df3f3cb60f5d32d8e3007c68299b664` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-c433ef580a5b229bbe68274a7164aa3a8e8040d7217f010b610ca0696a84253c) | +| `data` | 必需 | [`{ inserted, outcome?, removedCount?, start, … }`](#persistence-type-sha256-c66c6c3b411abf46c1f9b99e0019c10c1b6d13ee716a4b5e88f7e1b65feb3039) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `time` | 必需 | `number` | | `type` | 必需 | `"agent/inbox/spliced"` | + + + + +### `{ type: "agent/retry" }` + +SHA-256: `d8dde9629b7323b03c1c3fb0db75d6b72c73666dc0240ab7d3e41b9c0c40fc7c` + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `data` | 必需 | [`{ inputId, source, step, turn }`](#persistence-type-sha256-2a668a48dec1739752123d67c3bc254412d6743dbe3347365e0ed83a542f2bc3) | +| `ignorable` | 可选 | `true` | +| `seq` | 必需 | `number` | +| `time` | 必需 | `number` | +| `type` | 必需 | `"agent/retry"` | + @@ -8299,17 +8400,17 @@ SHA-256: `13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176` | `time` | 必需 | `number` | | `type` | 必需 | `"deliverables/presented"` | - + ### `{ type: "developer/message" }` -SHA-256: `95acdbd066cf445d37092e5fdfe2e8717ef6ca1aed50f70e2e6037a6989a1f2f` +SHA-256: `92b82e8b7bbb38b3adeb2c07eb76d0f0e0d18c1729808def7004ac2bc9baa583` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-12f9a5a7506bca46d6ef8e3c71826c4bf40e1389bfe0236cfffaea0c3fb367d2) | +| `data` | 必需 | [`{ headerSeq?, message, step, turn }`](#persistence-type-sha256-d582ddae3f21edf55b6f50454e3015ef78ed3788fee4670827f2c3c4bd0b47e2) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `sourceEventSeqs` | 可选 | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | @@ -8709,17 +8810,17 @@ SHA-256: `1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5` | `time` | 必需 | `number` | | `type` | 必需 | `"session/title"` | - + ### `{ type: "session/title-llm-request" }` -SHA-256: `eaa9125aec0fb28478096c46e73e21bf512ab0d84d826bfb41e7308707734cb5` +SHA-256: `fb28ad7d7aabceb5bda505cf027e0e22354d7f31757337e7e51a4b592feac172` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-9808c3d0f0a1be8fe313b4148ba22c691c4d59bd2c67aef3c63cfe8043148c5f) | +| `data` | 必需 | [`SessionTitleLlmRequestEventData`](#persistence-type-sha256-8eda0195dd1d4a3fee80f5e0b9f702fdf0f342c27ece60d44d459899b6a30b60) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `time` | 必需 | `number` | @@ -9142,17 +9243,17 @@ SHA-256: `f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515` | `type` | 必需 | `"usage"` | | `usage` | 必需 | [`TokenUsage`](#persistence-type-sha256-fec5442b9ee4122e10514d81d5a43648003fe8bfb7ee2ba50c0cdbdd383ed394) | - + ### `{ type: "user/message" }` -SHA-256: `39adb7fce8475af6402acd925ea79697b3341c22422ac32833d71217d84e987f` +SHA-256: `c5bbcb3435908af2036df7105883e0a49c30ecc1fae4420aa5c4cf944066eff5` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`UserMessage`](#persistence-type-sha256-8bca2b4f53ec7745b0484c6e7d3a6beb2ddd6cea82fc81786eaa3c0d065bbc14) | +| `data` | 必需 | [`UserMessage`](#persistence-type-sha256-9632024d168f8169ad83fe3639da51c864ebb5444c9fc965ac9342611b2d5263) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `sourceEventSeqs` | 可选 | [`number[]`](#persistence-type-sha256-5d03ba38734809bcbd2a55221bd938b3b3fc34b49bb686c6113e9e4931e3aa78) | diff --git a/docs/persistence-changes/2026-10-27-retry-control-input.i18n.yaml b/docs/persistence-changes/2026-10-27-retry-control-input.i18n.yaml new file mode 100644 index 0000000000..3513ba5aaa --- /dev/null +++ b/docs/persistence-changes/2026-10-27-retry-control-input.i18n.yaml @@ -0,0 +1,28 @@ +# Bilingual-pair consistency record for 2026-10-27-retry-control-input.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/persistence-changes/2026-10-27-retry-control-input.md +/: + en: 3d5a424ae7d7e46b + zh: c22a8e4a62d8cc1a +/2026-10-27-retry-control-input: + en: 32c7e9cc4a9f86b4 + zh: f1cb7169df15e8e4 +/2026-10-27-retry-control-input/summary: + en: e3a11c473fcdc452 + zh: 02ce33e05c10de71 +/2026-10-27-retry-control-input/table-of-contents: + en: 6cf314a069ceb451 + zh: 9adff678e9ecab3b +/2026-10-27-retry-control-input/declaration: + en: 6907136f28e48c90 + zh: 02f8298485f3d8e7 +/2026-10-27-retry-control-input/compatibility: + en: 4487a595853a65d6 + zh: d9b356c77ccab559 +/2026-10-27-retry-control-input/verification: + en: da37e5d4769c40fe + zh: 992f66b1a2c66fb2 +/2026-10-27-retry-control-input/dev-note: + en: b75c8458928b72b9 + zh: 003ba4e17b4b5269 diff --git a/docs/persistence-changes/2026-10-27-retry-control-input.md b/docs/persistence-changes/2026-10-27-retry-control-input.md new file mode 100644 index 0000000000..3faecc4503 --- /dev/null +++ b/docs/persistence-changes/2026-10-27-retry-control-input.md @@ -0,0 +1,64 @@ +--- +description: "Records a persistence type transition and its compatibility acknowledgement." +kind: persistence-change +--- + +# 2026-10-27-retry-control-input + +English | [中文](2026-10-27-retry-control-input.zh.md) + +## Summary + +Add an explicit retry control source and the required agent/retry acceptance event, correlated with durable inbox identity, turn, and step. + +## Table of Contents + +- [Declaration](#declaration) +- [Compatibility](#compatibility) +- [Verification](#verification) +- [Dev Note](#dev-note) + + +## Declaration + +```yaml persistence-change +schemaVersion: 1 +id: 2026-10-27-retry-control-input +baseline: false +changes: + - root: "event:agent/inbox/spliced" + previous: "2026-10-25-skill-catalog-incomplete" + after: "a6d8c25f4b2ca2d137672a8a278ee1398df3f3cb60f5d32d8e3007c68299b664" + decision: same-version + - root: "event:agent/retry" + previous: null + after: "d8dde9629b7323b03c1c3fb0db75d6b72c73666dc0240ab7d3e41b9c0c40fc7c" + decision: same-version + - root: "event:developer/message" + previous: "2026-10-25-skill-catalog-incomplete" + after: "92b82e8b7bbb38b3adeb2c07eb76d0f0e0d18c1729808def7004ac2bc9baa583" + decision: same-version + - root: "event:session/title-llm-request" + previous: "2026-10-25-skill-catalog-incomplete" + after: "fb28ad7d7aabceb5bda505cf027e0e22354d7f31757337e7e51a4b592feac172" + decision: same-version + - root: "event:user/message" + previous: "2026-10-25-skill-catalog-incomplete" + after: "c5bbcb3435908af2036df7105883e0a49c30ecc1fae4420aa5c4cf944066eff5" + decision: same-version +``` + + +## Compatibility + +Updated readers continue accepting older logs with no retry controls. Older readers reject the unknown required agent/retry event before treating a retry as ordinary user content. The retry discriminator also distinguishes queued controls from ordinary empty user messages. API and both SDK consumers are updated together; the current Session format version is retained because event and source discriminators prevent unsafe interpretation. + + +## Verification + +Source review confirms controls retain existing inbox and turn lifecycle while agent/retry has no model-surface operation. Regression coverage covers repeated retry, input-id correlation, balanced turns, ordinary empty messages, non-empty retry rejection, deduplication, checkpoint replay, browser behavior, and both SDK recovery recordings. Execution is pending post-push validation. + + +## Dev Note + +None. diff --git a/docs/persistence-changes/2026-10-27-retry-control-input.schema.json b/docs/persistence-changes/2026-10-27-retry-control-input.schema.json new file mode 100644 index 0000000000..54eea6ef36 --- /dev/null +++ b/docs/persistence-changes/2026-10-27-retry-control-input.schema.json @@ -0,0 +1,257 @@ +{ + "formatVersion": 2, + "roots": [ + {"key":"event:agent/inbox/spliced","kind":"event","event":"agent/inbox/spliced","surface":false,"digest":"a6d8c25f4b2ca2d137672a8a278ee1398df3f3cb60f5d32d8e3007c68299b664"}, + {"key":"event:agent/retry","kind":"event","event":"agent/retry","surface":false,"digest":"d8dde9629b7323b03c1c3fb0db75d6b72c73666dc0240ab7d3e41b9c0c40fc7c"}, + {"key":"event:developer/message","kind":"event","event":"developer/message","surface":true,"digest":"92b82e8b7bbb38b3adeb2c07eb76d0f0e0d18c1729808def7004ac2bc9baa583"}, + {"key":"event:session/title-llm-request","kind":"event","event":"session/title-llm-request","surface":false,"digest":"fb28ad7d7aabceb5bda505cf027e0e22354d7f31757337e7e51a4b592feac172"}, + {"key":"event:user/message","kind":"event","event":"user/message","surface":true,"digest":"c5bbcb3435908af2036df7105883e0a49c30ecc1fae4420aa5c4cf944066eff5"} + ], + "types": [], + "nodes": { + "0203243b72fc867a": {"kind":"object","properties":[{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, + "022623b9ef98ec47": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, + "02b2c23f2d94b028": {"kind":"object","properties":[{"name":"kind","type":"c5687b1313206a6f","optional":false}],"indices":[]}, + "05205074b24d8340": {"kind":"literal","value":"action-cards"}, + "0657d3ca5ac4e7bf": {"kind":"primitive","type":"number"}, + "07e1a58c58b593f5": {"kind":"object","properties":[{"name":"model","type":"4cd48f3e5108bd6e","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "09655b34d7f3cf1e": {"kind":"literal","value":"system-prompt"}, + "09f3f8dc417f2782": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"8af841fc9b265ade","optional":false},{"name":"source","type":"65a8400e451e667e","optional":false}],"indices":[]}, + "0ed8d1108fe7a5e1": {"kind":"object","properties":[{"name":"kind","type":"a9059a81430b3207","optional":false}],"indices":[]}, + "0eeb17e1df89b1eb": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, + "0ef3072084202b08": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"589d0d91239681f8","optional":false},{"name":"source","type":"e3e9f4d1e245b470","optional":false,"compatibility":{"version":1,"policy":"session-source-attribution","binding":"session.developer-message.source","discriminator":"kind","unknownKinds":"preserve","attributionKinds":["action-cards","tmux-context","user-question-reply","working-directory"]}}],"indices":[]}, + "0f46153645c29784": {"kind":"object","properties":[{"name":"height","type":"0657d3ca5ac4e7bf","optional":false},{"name":"width","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "0fe52387f6505270": {"kind":"literal","value":"plan-mode"}, + "11638dc58a31496f": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "12d5808fac1996b9": {"kind":"object","properties":[{"name":"attachment","type":"b0b0eb1b16490f6b","optional":false},{"name":"offloaded","type":"dacf3d5f3e4ff619","optional":true},{"name":"type","type":"fb8b6a2e53cb9cf4","optional":false}],"indices":[]}, + "1379f0b72275994d": {"kind":"literal","value":"developer/message"}, + "16d0507cb94b4cfc": {"kind":"literal","value":"relay"}, + "1922cd79a70806c1": {"kind":"literal","value":"skill-invocation"}, + "1938c8c20f1cba7c": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false}],"indices":[]}, + "1e709346005826eb": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false}],"indices":[]}, + "1f0133217eeeffd8": {"kind":"literal","value":"tool-call"}, + "1f1385dbc3e875c0": {"kind":"literal","value":"repeat-tool-reminder"}, + "1fdad624334d3548": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false}],"indices":[]}, + "2152caf1a71e588e": {"kind":"object","properties":[{"name":"kind","type":"2e4debca12f2c449","optional":false},{"name":"messageId","type":"4cd48f3e5108bd6e","optional":false},{"name":"senderId","type":"4cd48f3e5108bd6e","optional":false},{"name":"senderName","type":"4cd48f3e5108bd6e","optional":false},{"name":"teamId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "2235d957bf4b7c6c": {"kind":"object","properties":[{"name":"kind","type":"7b1ce60193fa8b69","optional":false}],"indices":[]}, + "224aa11d4d5f88c4": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, + "23af3759678a30de": {"kind":"literal","value":"dsh-session-title-llm"}, + "24b10f7cdae64928": {"kind":"literal","value":"runtime-context"}, + "24ce31aa528272e5": {"kind":"object","properties":[{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, + "260523b412c19ad8": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false}],"indices":[]}, + "2765e9119edf5816": {"kind":"literal","value":"model"}, + "277b61f9c6f81704": {"kind":"literal","value":"file"}, + "28936c614bd4e7e2": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "290f32bfd81ff85a": {"kind":"object","properties":[{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, + "297fbd8067d7815a": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false}],"indices":[]}, + "2a668a48dec17397": {"kind":"object","properties":[{"name":"inputId","type":"4cd48f3e5108bd6e","optional":false},{"name":"source","type":"671e72b8036a3afb","optional":false},{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "2aad1179f9576314": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "2ab12a0e345658f9": {"kind":"array","element":"83da72c6857601c7"}, + "2e4debca12f2c449": {"kind":"literal","value":"team-message"}, + "2f1e6a7ebace43d7": {"kind":"union","types":["3f74ba74acd10644","58f94390b982f8a5","de30b1fa12c75f81","ebb768d85ec87fb6"]}, + "2f3f201c8846e19c": {"kind":"literal","value":"tool-goal"}, + "2f5b14bc2ccbe881": {"kind":"array","element":"7cf091467eb28887"}, + "30093f9f63a120cb": {"kind":"literal","value":"agent/retry"}, + "312aee2f0c42aabb": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "31907de956375182": {"kind":"object","properties":[{"name":"compactionId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"6b23ed9ce1b397e6","optional":false},{"name":"sourceCommandId","type":"4cd48f3e5108bd6e","optional":true}],"indices":[]}, + "3247e566f4340029": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "335e242de1fcc17b": {"kind":"union","types":["bcf0caf62d964b2f","e438a9d4fb883b3e"]}, + "347c1718600bbb38": {"kind":"array","element":"9632024d168f8169"}, + "383cf73ae4748e5b": {"kind":"literal","value":"recall"}, + "3b04790e08b35241": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, + "3b4f2c9ef2c1217e": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false}],"indices":[]}, + "3bd652ebfa8726b3": {"kind":"union","types":["0657d3ca5ac4e7bf","938f7c7357bc403e"]}, + "3bdb8dc6989655b0": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "3c2535ca0a2062f1": {"kind":"literal","value":"reasoning"}, + "3e2ece4967cfebe9": {"kind":"literal","value":"next-step"}, + "3f74ba74acd10644": {"kind":"literal","value":"image/jpeg"}, + "408927267422a7b2": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, + "414a44a96f06339f": {"kind":"object","properties":[{"name":"kind","type":"63ba8895cc09e469","optional":false}],"indices":[]}, + "43e311235aa537df": {"kind":"object","properties":[{"name":"toolName","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"83d6916021eede8f","optional":false}],"indices":[]}, + "45f005e6bbb76452": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "4991dfc415c9efeb": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, + "49dab019d00e8c26": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "4a9bef9f27a94252": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"82470020b5bfcb1a","optional":false},{"name":"senderSessionId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "4b259f7f8f0a44ad": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"isError","type":"aa93010d0e521ad7","optional":true},{"name":"role","type":"a13d8f379222ece8","optional":false},{"name":"source","type":"d9c462b9f62fdd3e","optional":false},{"name":"toolCallId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "4cd48f3e5108bd6e": {"kind":"primitive","type":"string"}, + "4e44c9118152f682": {"kind":"literal","value":"text"}, + "4e88776858652856": {"kind":"object","properties":[{"name":"text","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"4e44c9118152f682","optional":false}],"indices":[]}, + "4e97c3d85c0fc817": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"bf16d2e98a7f1389","optional":false},{"name":"senderSessionId","type":"4cd48f3e5108bd6e","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "4edec1b770e8ec4c": {"kind":"literal","value":"ptc-mode"}, + "4f91f187baeabef5": {"kind":"object","properties":[{"name":"capturedFormatVersion","type":"0657d3ca5ac4e7bf","optional":true},{"name":"capturedThroughSeq","type":"3bd652ebfa8726b3","optional":false},{"name":"compacted","type":"aa93010d0e521ad7","optional":false},{"name":"inputIndex","type":"0657d3ca5ac4e7bf","optional":false},{"name":"label","type":"4cd48f3e5108bd6e","optional":false},{"name":"omittedBytes","type":"0657d3ca5ac4e7bf","optional":false},{"name":"omittedMessages","type":"0657d3ca5ac4e7bf","optional":false},{"name":"originalMessages","type":"0657d3ca5ac4e7bf","optional":false},{"name":"retainedMessages","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sessionId","type":"4cd48f3e5108bd6e","optional":false},{"name":"truncated","type":"aa93010d0e521ad7","optional":false}],"indices":[]}, + "52c6113cb0e53f34": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "53cf1704ae0709c7": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, + "5654685d7557ec74": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "58015da1c1950a66": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, + "587bb11da730e06a": {"kind":"literal","value":"session/title-llm-request"}, + "589d0d91239681f8": {"kind":"literal","value":"developer"}, + "58f94390b982f8a5": {"kind":"literal","value":"image/png"}, + "5bb69b88517f6214": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "5bc66269cfaf614c": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false}],"indices":[]}, + "5cdbf342b7748d15": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, + "5d03ba38734809bc": {"kind":"array","element":"0657d3ca5ac4e7bf"}, + "5d7a64ed6aac5551": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false}],"indices":[]}, + "6085488d223aeffd": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false}],"indices":[]}, + "6292bcbedaa97fea": {"kind":"object","properties":[{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, + "62ecb62dc0fe1b5e": {"kind":"literal","value":"next-turn"}, + "63ba8895cc09e469": {"kind":"literal","value":"retry"}, + "65a8400e451e667e": {"kind":"object","properties":[{"name":"kind","type":"09655b34d7f3cf1e","optional":false}],"indices":[]}, + "65d583e66518c523": {"kind":"object","properties":[{"name":"entries","type":"aba175fb20ffc711","optional":false},{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"incomplete","type":"dacf3d5f3e4ff619","optional":true},{"name":"kind","type":"6ebcc2dbec9f2dab","optional":false},{"name":"update","type":"dacf3d5f3e4ff619","optional":true}],"indices":[]}, + "671e72b8036a3afb": {"kind":"union","types":["414a44a96f06339f","e103228e063b4762"]}, + "672c4aa2a9b736e3": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"1922cd79a70806c1","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "68f44f59e91c4139": {"kind":"literal","value":"user"}, + "6b23ed9ce1b397e6": {"kind":"literal","value":"compact-checkpoint"}, + "6ccb6bc423fb3952": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "6dacb1584f2bfbe2": {"kind":"object","properties":[{"name":"clientTimeZone","type":"4cd48f3e5108bd6e","optional":true},{"name":"kind","type":"68f44f59e91c4139","optional":false},{"name":"principal","type":"deebfc7c62177ea8","optional":true},{"name":"rpcId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "6ea608bdc8a7a63d": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false}],"indices":[]}, + "6ebcc2dbec9f2dab": {"kind":"literal","value":"skill-catalog"}, + "6f22a9d02b9a1f11": {"kind":"object","properties":[{"name":"baseline","type":"dacf3d5f3e4ff619","optional":true},{"name":"baselineIdentity","type":"4cd48f3e5108bd6e","optional":true},{"name":"changes","type":"2ab12a0e345658f9","optional":false},{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"fbd2c74b346a1595","optional":false}],"indices":[]}, + "6f44f32be9bcd9a8": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, + "6f9e00383836275e": {"kind":"literal","value":"snapshot"}, + "719c51f67074f970": {"kind":"object","properties":[{"name":"kind","type":"2765e9119edf5816","optional":false},{"name":"model","type":"4cd48f3e5108bd6e","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"replayState","type":"bbc5696e3f13c758","optional":true}],"indices":[]}, + "7382c4fc95fe8c5d": {"kind":"literal","value":"remove"}, + "7554565be416b034": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false}],"indices":[]}, + "756b5e23a5cb2c7f": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false}],"indices":[]}, + "78a9be340090b735": {"kind":"literal","value":"tmux-context"}, + "7b1ce60193fa8b69": {"kind":"literal","value":"working-directory"}, + "7cf091467eb28887": {"kind":"union","types":["09f3f8dc417f2782","0ef3072084202b08","4b259f7f8f0a44ad","9632024d168f8169","fa87537f4a9c1e25"]}, + "7df62978a2b7de85": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false}],"indices":[]}, + "7eaef7a763ba5b94": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, + "7f13860ee89a2d35": {"kind":"literal","value":"session-reference"}, + "7f959241c24128b4": {"kind":"object","properties":[{"name":"goalId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"b815d7b91cdd9fc6","optional":false},{"name":"revision","type":"0657d3ca5ac4e7bf","optional":false},{"name":"round","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "8064c03c87c357fd": {"kind":"union","types":["12d5808fac1996b9","43e311235aa537df","4e88776858652856","814d9434e1ddf907","8e611bf46821e500","c1f0f17b6493b85d","f2da0f27c1e6add5"]}, + "80677c8c8b804189": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, + "8067b0d232c350f1": {"kind":"literal","value":"time-context"}, + "814d9434e1ddf907": {"kind":"object","properties":[{"name":"text","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"3c2535ca0a2062f1","optional":false}],"indices":[]}, + "82470020b5bfcb1a": {"kind":"literal","value":"agent-message"}, + "825ca23416b1ea9d": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, + "829a2fb3be6efd4b": {"kind":"literal","value":"agent/inbox/spliced"}, + "83079c8a3f733ac3": {"kind":"object","properties":[{"name":"description","type":"4cd48f3e5108bd6e","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "8338ee303e2bd448": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "83d6916021eede8f": {"kind":"literal","value":"tool-removal"}, + "83da72c6857601c7": {"kind":"object","properties":[{"name":"action","type":"904bf499d98be4a3","optional":false},{"name":"digest","type":"4cd48f3e5108bd6e","optional":true},{"name":"path","type":"4cd48f3e5108bd6e","optional":false},{"name":"scope","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "854db3efdfb0cd39": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "85d6d96ff8842974": {"kind":"literal","value":"answered"}, + "85f9b505b7bdff1f": {"kind":"literal","value":"tool-addition"}, + "85fe96bf15d5c755": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false}],"indices":[]}, + "8620189c8e335827": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, + "86e1e384495ee09a": {"kind":"object","properties":[{"name":"name","type":"4cd48f3e5108bd6e","optional":false},{"name":"text","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "87b1a33564b9a4d6": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false}],"indices":[]}, + "88841202942c7683": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "8a046b52331959aa": {"kind":"object","properties":[{"name":"kind","type":"78a9be340090b735","optional":false}],"indices":[]}, + "8af841fc9b265ade": {"kind":"literal","value":"system"}, + "8c1c4f72755a78ea": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "8c627d7a45fa406c": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "8e611bf46821e500": {"kind":"object","properties":[{"name":"tool","type":"fcf427290c6213c2","optional":true},{"name":"toolName","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"85f9b505b7bdff1f","optional":false}],"indices":[]}, + "8eda0195dd1d4a3f": {"kind":"object","properties":[{"name":"maxTokens","type":"0657d3ca5ac4e7bf","optional":false},{"name":"messageSeqs","type":"5d03ba38734809bc","optional":false},{"name":"messages","type":"2f5b14bc2ccbe881","optional":false},{"name":"reasoningEffort","type":"4cd48f3e5108bd6e","optional":true},{"name":"route","type":"07e1a58c58b593f5","optional":false},{"name":"system","type":"4cd48f3e5108bd6e","optional":false},{"name":"titleProvider","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "8f324cea3916c051": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false}],"indices":[]}, + "8fcb0b44a575e998": {"kind":"object","properties":[{"name":"kind","type":"a720757f704a2512","optional":false}],"indices":[]}, + "904bf499d98be4a3": {"kind":"union","types":["7382c4fc95fe8c5d","b765ee31033cea46","f6cb462f808309e3"]}, + "92b82e8b7bbb38b3": {"kind":"object","properties":[{"name":"data","type":"d582ddae3f21edf5","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"1379f0b72275994d","optional":false}],"indices":[]}, + "938f7c7357bc403e": {"kind":"primitive","type":"null"}, + "95a3ffccbf3c8afc": {"kind":"literal","value":"owner"}, + "95c0e88fcf0bafb2": {"kind":"literal","value":"assistant"}, + "9632024d168f8169": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"68f44f59e91c4139","optional":false},{"name":"source","type":"e3e9f4d1e245b470","optional":false,"compatibility":{"version":1,"policy":"session-source-attribution","binding":"session.user-message.source","discriminator":"kind","unknownKinds":"preserve","attributionKinds":["action-cards","tmux-context","user-question-reply","working-directory"]}}],"indices":[]}, + "9645cf93769a6dfb": {"kind":"literal","value":"user-question-reply"}, + "96bfb916a727cd44": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, + "96f4a9c81fc0f940": {"kind":"union","types":["3e2ece4967cfebe9","62ecb62dc0fe1b5e"]}, + "99135d8a39379779": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, + "99e7e7d7fed55ae6": {"kind":"literal","value":"tool-jobs"}, + "99f0e98184709ad1": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false}],"indices":[]}, + "9ac52f0f84fec2eb": {"kind":"literal","value":"model-selection"}, + "9edc162949abf93b": {"kind":"object","properties":[{"name":"deliveryId","type":"4cd48f3e5108bd6e","optional":false},{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"db6c5246911d47a7","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"ruleId","type":"4cd48f3e5108bd6e","optional":false},{"name":"source","type":"4cd48f3e5108bd6e","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "9f65c43e340bba31": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false}],"indices":[]}, + "a13d8f379222ece8": {"kind":"literal","value":"tool"}, + "a15a19ef63cf5d1b": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, + "a16e567c8cd2c55c": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false}],"indices":[]}, + "a2066bd52f61218b": {"kind":"object","properties":[{"name":"kind","type":"2f3f201c8846e19c","optional":false}],"indices":[]}, + "a3c6f1ea38e9a61d": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "a6d8c25f4b2ca2d1": {"kind":"object","properties":[{"name":"data","type":"c66c6c3b411abf46","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"829a2fb3be6efd4b","optional":false}],"indices":[]}, + "a720757f704a2512": {"kind":"literal","value":"tool-registry"}, + "a9059a81430b3207": {"kind":"literal","value":"hooks-claude-code"}, + "aa2bf9e82b9fdeb9": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, + "aa93010d0e521ad7": {"kind":"primitive","type":"boolean"}, + "aae343bc3d663c83": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, + "aba175fb20ffc711": {"kind":"array","element":"83079c8a3f733ac3"}, + "ada310bf0bdb8fed": {"kind":"object","properties":[{"name":"kind","type":"68f44f59e91c4139","optional":false}],"indices":[]}, + "ade057f4fdc387ae": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, + "aec150797b655c5f": {"kind":"literal","value":"catalog"}, + "b062078e899958f9": {"kind":"object","properties":[{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, + "b0b0eb1b16490f6b": {"kind":"object","properties":[{"name":"attachmentId","type":"4cd48f3e5108bd6e","optional":false},{"name":"bytes","type":"0657d3ca5ac4e7bf","optional":false},{"name":"height","type":"0657d3ca5ac4e7bf","optional":false},{"name":"mediaType","type":"2f1e6a7ebace43d7","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":true},{"name":"originalDimensions","type":"0f46153645c29784","optional":true},{"name":"width","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "b2a5c8c008a31e3d": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, + "b3681c026a0b98e9": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "b4ec0af39fd52ed9": {"kind":"object","properties":[{"name":"kind","type":"bb94f3217aa56c88","optional":false}],"indices":[]}, + "b52f215a6cd69105": {"kind":"array","element":"8064c03c87c357fd"}, + "b613f83572fe2fbf": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "b6f94c17dd9bbd7c": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"bb94f3217aa56c88","optional":false}],"indices":[]}, + "b70f3e81fe11b7a5": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false}],"indices":[]}, + "b765ee31033cea46": {"kind":"literal","value":"set"}, + "b79641a9d25e7566": {"kind":"object","properties":[{"name":"kind","type":"8067b0d232c350f1","optional":false}],"indices":[]}, + "b815d7b91cdd9fc6": {"kind":"literal","value":"goal"}, + "ba2a1a8f194b1bb2": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "ba4c40b4381e0dcd": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"23af3759678a30de","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "baccc97ec34be6c4": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "bb94f3217aa56c88": {"kind":"literal","value":"hooks-codex"}, + "bbc5696e3f13c758": {"kind":"opaque","reason":"unknown"}, + "bcf0caf62d964b2f": {"kind":"object","properties":[{"name":"endSeq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"op","type":"f6cb462f808309e3","optional":false},{"name":"startSeq","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "bf16d2e98a7f1389": {"kind":"literal","value":"subagent-settled"}, + "c1f0f17b6493b85d": {"kind":"object","properties":[{"name":"arguments","type":"4cd48f3e5108bd6e","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"1f0133217eeeffd8","optional":false}],"indices":[]}, + "c5228b7a55498d84": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "c55827fba7018198": {"kind":"object","properties":[{"name":"callId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"9645cf93769a6dfb","optional":false},{"name":"outcome","type":"85d6d96ff8842974","optional":false}],"indices":[]}, + "c5687b1313206a6f": {"kind":"literal","value":"cordis-host-runner"}, + "c5bbcb3435908af2": {"kind":"object","properties":[{"name":"data","type":"9632024d168f8169","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"de25352d2dc41963","optional":false}],"indices":[]}, + "c66c6c3b411abf46": {"kind":"object","properties":[{"name":"inserted","type":"347c1718600bbb38","optional":false},{"name":"outcome","type":"d4e4d575abeb4b72","optional":true},{"name":"removedCount","type":"0657d3ca5ac4e7bf","optional":true},{"name":"start","type":"0657d3ca5ac4e7bf","optional":false},{"name":"target","type":"96f4a9c81fc0f940","optional":false}],"indices":[]}, + "c748fb42c80a1200": {"kind":"array","element":"86e1e384495ee09a"}, + "cb1c8959e1726f82": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, + "cda2345a9be9d139": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "d4e4d575abeb4b72": {"kind":"literal","value":"canceled"}, + "d582ddae3f21edf5": {"kind":"object","properties":[{"name":"headerSeq","type":"0657d3ca5ac4e7bf","optional":true},{"name":"message","type":"0ef3072084202b08","optional":false},{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "d85590a5675c6e2f": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, + "d8dde9629b7323b0": {"kind":"object","properties":[{"name":"data","type":"2a668a48dec17397","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"30093f9f63a120cb","optional":false}],"indices":[]}, + "d9c462b9f62fdd3e": {"kind":"object","properties":[{"name":"callId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"a13d8f379222ece8","optional":false}],"indices":[]}, + "d9edf1862e4471d6": {"kind":"literal","value":"schedule"}, + "dacf3d5f3e4ff619": {"kind":"literal","value":true}, + "db06ef35b27a3842": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "db6c5246911d47a7": {"kind":"literal","value":"webhook"}, + "dba54c09c6e7aafc": {"kind":"object","properties":[{"name":"kind","type":"4edec1b770e8ec4c","optional":false}],"indices":[]}, + "dd00cdccd0ba487b": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "dd5ee8762bd8a14b": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, + "dd6cc2bb2a8f2824": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, + "dde0cdf9d426768b": {"kind":"object","properties":[{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "de25352d2dc41963": {"kind":"literal","value":"user/message"}, + "de30b1fa12c75f81": {"kind":"literal","value":"image/gif"}, + "deaae5cd30987d4f": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, + "deebfc7c62177ea8": {"kind":"object","properties":[{"name":"kind","type":"95a3ffccbf3c8afc","optional":false}],"indices":[]}, + "df843efd87c78cf8": {"kind":"array","element":"4f91f187baeabef5"}, + "e103228e063b4762": {"kind":"object","properties":[{"name":"clientTimeZone","type":"4cd48f3e5108bd6e","optional":true},{"name":"kind","type":"63ba8895cc09e469","optional":false},{"name":"principal","type":"deebfc7c62177ea8","optional":true},{"name":"rpcId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "e3e9f4d1e245b470": {"kind":"union","types":["0203243b72fc867a","022623b9ef98ec47","02b2c23f2d94b028","0ed8d1108fe7a5e1","0eeb17e1df89b1eb","11638dc58a31496f","1938c8c20f1cba7c","1e709346005826eb","1fdad624334d3548","2152caf1a71e588e","2235d957bf4b7c6c","224aa11d4d5f88c4","24ce31aa528272e5","260523b412c19ad8","28936c614bd4e7e2","290f32bfd81ff85a","297fbd8067d7815a","2aad1179f9576314","312aee2f0c42aabb","31907de956375182","3247e566f4340029","3b04790e08b35241","3b4f2c9ef2c1217e","3bdb8dc6989655b0","408927267422a7b2","414a44a96f06339f","45f005e6bbb76452","4991dfc415c9efeb","49dab019d00e8c26","4a9bef9f27a94252","4e97c3d85c0fc817","52c6113cb0e53f34","53cf1704ae0709c7","5654685d7557ec74","58015da1c1950a66","5bb69b88517f6214","5bc66269cfaf614c","5cdbf342b7748d15","5d7a64ed6aac5551","6085488d223aeffd","6292bcbedaa97fea","65a8400e451e667e","65d583e66518c523","672c4aa2a9b736e3","6ccb6bc423fb3952","6dacb1584f2bfbe2","6ea608bdc8a7a63d","6f22a9d02b9a1f11","6f44f32be9bcd9a8","719c51f67074f970","7554565be416b034","756b5e23a5cb2c7f","7df62978a2b7de85","7eaef7a763ba5b94","7f959241c24128b4","80677c8c8b804189","825ca23416b1ea9d","8338ee303e2bd448","854db3efdfb0cd39","85fe96bf15d5c755","8620189c8e335827","87b1a33564b9a4d6","88841202942c7683","8a046b52331959aa","8c1c4f72755a78ea","8c627d7a45fa406c","8f324cea3916c051","8fcb0b44a575e998","96bfb916a727cd44","99135d8a39379779","99f0e98184709ad1","9edc162949abf93b","9f65c43e340bba31","a15a19ef63cf5d1b","a16e567c8cd2c55c","a2066bd52f61218b","a3c6f1ea38e9a61d","aa2bf9e82b9fdeb9","aae343bc3d663c83","ada310bf0bdb8fed","ade057f4fdc387ae","b062078e899958f9","b2a5c8c008a31e3d","b3681c026a0b98e9","b4ec0af39fd52ed9","b613f83572fe2fbf","b6f94c17dd9bbd7c","b70f3e81fe11b7a5","b79641a9d25e7566","ba2a1a8f194b1bb2","ba4c40b4381e0dcd","baccc97ec34be6c4","c5228b7a55498d84","c55827fba7018198","cb1c8959e1726f82","cda2345a9be9d139","d85590a5675c6e2f","d9c462b9f62fdd3e","db06ef35b27a3842","dba54c09c6e7aafc","dd00cdccd0ba487b","dd5ee8762bd8a14b","dd6cc2bb2a8f2824","dde0cdf9d426768b","deaae5cd30987d4f","e103228e063b4762","e7f4cbb3a57f4cbd","edbbfff1128099fd","ee577a0cc18664c9","f230c0b583e23b25","f29d63336e8266d8","f4a66f654996c093","f56c5b59ca016131","f6329fb30c7ea801","f6605d6c57933aea","f6afe41e3926f422","f7283223aacdc39b","f97f47b0eb87905e","fcf81d3c58c13593","fe1a7acdeed4d0f5"]}, + "e438a9d4fb883b3e": {"kind":"literal","value":"append"}, + "e7f4cbb3a57f4cbd": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"05205074b24d8340","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false},{"name":"through","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, + "e8de0f457fed5b9f": {"kind":"literal","value":"notice"}, + "ebb768d85ec87fb6": {"kind":"literal","value":"image/webp"}, + "edbbfff1128099fd": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "ee577a0cc18664c9": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "f19bee84bf3d2090": {"kind":"object","properties":[{"name":"attachmentId","type":"4cd48f3e5108bd6e","optional":false},{"name":"bytes","type":"0657d3ca5ac4e7bf","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "f230c0b583e23b25": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, + "f29d63336e8266d8": {"kind":"object","properties":[{"name":"kind","type":"d9edf1862e4471d6","optional":false}],"indices":[]}, + "f2da0f27c1e6add5": {"kind":"object","properties":[{"name":"attachment","type":"f19bee84bf3d2090","optional":false},{"name":"type","type":"277b61f9c6f81704","optional":false}],"indices":[]}, + "f4a66f654996c093": {"kind":"object","properties":[{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, + "f56c5b59ca016131": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"7f13860ee89a2d35","optional":false},{"name":"references","type":"df843efd87c78cf8","optional":false},{"name":"version","type":"f88f1b7b6bb733f6","optional":false}],"indices":[]}, + "f6329fb30c7ea801": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, + "f6605d6c57933aea": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, + "f68c335306535a00": {"kind":"literal","value":"user-approval"}, + "f6afe41e3926f422": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false}],"indices":[]}, + "f6cb462f808309e3": {"kind":"literal","value":"replace"}, + "f7283223aacdc39b": {"kind":"object","properties":[{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, + "f88f1b7b6bb733f6": {"kind":"literal","value":1}, + "f97f47b0eb87905e": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false}],"indices":[]}, + "fa87537f4a9c1e25": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"95c0e88fcf0bafb2","optional":false},{"name":"source","type":"719c51f67074f970","optional":false}],"indices":[]}, + "fb28ad7d7aabceb5": {"kind":"object","properties":[{"name":"data","type":"8eda0195dd1d4a3f","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"587bb11da730e06a","optional":false}],"indices":[]}, + "fb8b6a2e53cb9cf4": {"kind":"literal","value":"image"}, + "fbd2c74b346a1595": {"kind":"literal","value":"agent-instructions"}, + "fcf427290c6213c2": {"kind":"primitive","type":"never"}, + "fcf81d3c58c13593": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false}],"indices":[]}, + "fe1a7acdeed4d0f5": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "fee1caf8ab6a65c1": {"kind":"literal","value":"instructions"} + } +} diff --git a/docs/persistence-changes/2026-10-27-retry-control-input.zh.md b/docs/persistence-changes/2026-10-27-retry-control-input.zh.md new file mode 100644 index 0000000000..672f1d032c --- /dev/null +++ b/docs/persistence-changes/2026-10-27-retry-control-input.zh.md @@ -0,0 +1,64 @@ +--- +description: "记录持久化类型更改及其兼容性确认。" +kind: persistence-change +--- + +# 2026-10-27-retry-control-input + +[English](2026-10-27-retry-control-input.md) | 中文 + +## 概述 + +新增显式 retry 控制来源与必读 agent/retry 接受事件,关联持久 inbox 身份、轮次和步骤。 + +## 目录 + +- [声明](#declaration) +- [兼容性](#compatibility) +- [验证](#verification) +- [开发备注](#dev-note) + + +## 声明 + +```yaml persistence-change +schemaVersion: 1 +id: 2026-10-27-retry-control-input +baseline: false +changes: + - root: "event:agent/inbox/spliced" + previous: "2026-10-25-skill-catalog-incomplete" + after: "a6d8c25f4b2ca2d137672a8a278ee1398df3f3cb60f5d32d8e3007c68299b664" + decision: same-version + - root: "event:agent/retry" + previous: null + after: "d8dde9629b7323b03c1c3fb0db75d6b72c73666dc0240ab7d3e41b9c0c40fc7c" + decision: same-version + - root: "event:developer/message" + previous: "2026-10-25-skill-catalog-incomplete" + after: "92b82e8b7bbb38b3adeb2c07eb76d0f0e0d18c1729808def7004ac2bc9baa583" + decision: same-version + - root: "event:session/title-llm-request" + previous: "2026-10-25-skill-catalog-incomplete" + after: "fb28ad7d7aabceb5bda505cf027e0e22354d7f31757337e7e51a4b592feac172" + decision: same-version + - root: "event:user/message" + previous: "2026-10-25-skill-catalog-incomplete" + after: "c5bbcb3435908af2036df7105883e0a49c30ecc1fae4420aa5c4cf944066eff5" + decision: same-version +``` + + +## 兼容性 + +更新后的读取方继续接受没有重试控制输入的旧日志。旧读取方会拒绝未知必读 agent/retry 事件,不会将重试当作普通用户内容。retry 判别标签也将排队控制输入与普通空用户消息区分开。API 与两套 SDK 消费方同步更新;事件及来源判别标签可阻止不安全解释,因此保留当前 Session 格式版本。 + + +## 验证 + +源码审查确认控制输入保留现有 inbox 和轮次生命周期,agent/retry 不携带模型表面操作。回归用例覆盖连续重试、输入身份关联、轮次闭合、普通空消息、拒绝非空重试、去重、检查点回放、浏览器行为及两套 SDK 恢复录制。执行验证将在 push 后进行。 + + +## 开发备注 + +无。 diff --git a/docs/persistence-schema.json b/docs/persistence-schema.json index 70f64325e4..07e2ce9415 100644 --- a/docs/persistence-schema.json +++ b/docs/persistence-schema.json @@ -5,7 +5,8 @@ {"key":"JsonlHeaderLine","kind":"header","digest":"18ee62b8900a4c3d046700f05d7a4d49d6cab2a660a020c87481d1603dd8bd4f"}, {"key":"SessionEventEnvelope","kind":"envelope","digest":"5776e5553ff2dfe3f5bc202dbb1e7c9f93e35a531aebb7764c23b2b6153b2ccc"}, {"key":"event:agent-preset/selected","kind":"event","event":"agent-preset/selected","surface":false,"digest":"a10c17474eaf2ddab7095a099e0fe3d046fc18e56c3e344fc8894c05ff9ef97b"}, - {"key":"event:agent/inbox/spliced","kind":"event","event":"agent/inbox/spliced","surface":false,"digest":"9e8043098fb89db6d7c92a92c70146c5c8d24fa88f733c687fa29e5e7a58ab30"}, + {"key":"event:agent/inbox/spliced","kind":"event","event":"agent/inbox/spliced","surface":false,"digest":"a6d8c25f4b2ca2d137672a8a278ee1398df3f3cb60f5d32d8e3007c68299b664"}, + {"key":"event:agent/retry","kind":"event","event":"agent/retry","surface":false,"digest":"d8dde9629b7323b03c1c3fb0db75d6b72c73666dc0240ab7d3e41b9c0c40fc7c"}, {"key":"event:approval/asked","kind":"event","event":"approval/asked","surface":false,"digest":"3bfeb47b58606f4661904bc723da612782214c463d01e6d61cd6d6193d7374e1"}, {"key":"event:approval/decided","kind":"event","event":"approval/decided","surface":false,"digest":"bb1ab3d08f49a9f3b265f844cd78d5c49813062a7b34b54904b426f85d0ff6e3"}, {"key":"event:approval/policy","kind":"event","event":"approval/policy","surface":false,"digest":"26718e15e7e395bce9642dba5bbe09b3b1a4ce2213d20d566cd9207d7fc5fb78"}, @@ -18,7 +19,7 @@ {"key":"event:compaction/start","kind":"event","event":"compaction/start","surface":false,"digest":"db874d463b0fdec77e9da1c4568f37cb70bd6596781eb93800db44fb8a116965"}, {"key":"event:compaction/summary","kind":"event","event":"compaction/summary","surface":false,"digest":"e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602"}, {"key":"event:deliverables/presented","kind":"event","event":"deliverables/presented","surface":false,"digest":"13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176"}, - {"key":"event:developer/message","kind":"event","event":"developer/message","surface":true,"digest":"95acdbd066cf445d37092e5fdfe2e8717ef6ca1aed50f70e2e6037a6989a1f2f"}, + {"key":"event:developer/message","kind":"event","event":"developer/message","surface":true,"digest":"92b82e8b7bbb38b3adeb2c07eb76d0f0e0d18c1729808def7004ac2bc9baa583"}, {"key":"event:feedback/message-delete","kind":"event","event":"feedback/message-delete","surface":false,"digest":"3ee93b06f3a125850337602bcdf155d2538c43a5c944ec55b1b3c365152d6796"}, {"key":"event:feedback/message-put","kind":"event","event":"feedback/message-put","surface":false,"digest":"b5086d249e8502e9ead1d39156bb8d559bde7951cac0f14ce150345b4e42a2bf"}, {"key":"event:feedback/record","kind":"event","event":"feedback/record","surface":false,"digest":"b54940ff095c17e874c5be03815f4c2145a256cf3a1d34dae4ab2f7769dfffe8"}, @@ -40,7 +41,7 @@ {"key":"event:session-log-deepseek/delivery-accepted","kind":"event","event":"session-log-deepseek/delivery-accepted","surface":false,"digest":"d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9"}, {"key":"event:session/end-seed","kind":"event","event":"session/end-seed","surface":false,"digest":"5e6db6e24948d4a853c71cb9fabd252ad051ce93d4672c1266cf837c1c17b84e"}, {"key":"event:session/title","kind":"event","event":"session/title","surface":false,"digest":"1b912703e2d64f91c99c675b8f805b01076c8325b905c1218ad81ef0b24909d5"}, - {"key":"event:session/title-llm-request","kind":"event","event":"session/title-llm-request","surface":false,"digest":"eaa9125aec0fb28478096c46e73e21bf512ab0d84d826bfb41e7308707734cb5"}, + {"key":"event:session/title-llm-request","kind":"event","event":"session/title-llm-request","surface":false,"digest":"fb28ad7d7aabceb5bda505cf027e0e22354d7f31757337e7e51a4b592feac172"}, {"key":"event:step/end","kind":"event","event":"step/end","surface":false,"digest":"e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05"}, {"key":"event:step/start","kind":"event","event":"step/start","surface":false,"digest":"4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986"}, {"key":"event:subagent/catalog","kind":"event","event":"subagent/catalog","surface":false,"digest":"53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac"}, @@ -62,7 +63,7 @@ {"key":"event:tool/result","kind":"event","event":"tool/result","surface":true,"digest":"7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397"}, {"key":"event:turn/end","kind":"event","event":"turn/end","surface":false,"digest":"0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce"}, {"key":"event:turn/start","kind":"event","event":"turn/start","surface":false,"digest":"aa0957eca50aeb28bcd2e6930b95809926edacb550c8c340ba526ba6b861b3d8"}, - {"key":"event:user/message","kind":"event","event":"user/message","surface":true,"digest":"39adb7fce8475af6402acd925ea79697b3341c22422ac32833d71217d84e987f"}, + {"key":"event:user/message","kind":"event","event":"user/message","surface":true,"digest":"c5bbcb3435908af2036df7105883e0a49c30ecc1fae4420aa5c4cf944066eff5"}, {"key":"event:web/deepseek-search-llm-request","kind":"event","event":"web/deepseek-search-llm-request","surface":false,"digest":"0c556c9b9f2a96238a6b9bbb99e748206cbff2ed6203041c9571f04ae5802f52"}, {"key":"event:working-directory/change","kind":"event","event":"working-directory/change","surface":false,"digest":"124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3"}, {"key":"event:workspace/changes","kind":"event","event":"workspace/changes","surface":false,"digest":"e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72"} @@ -98,6 +99,7 @@ {"digest":"0e7b2a97f54f1afdb8cdfad1635abe9145327db539bfb69b4045c19d8c17755d","names":[],"sources":[]}, {"digest":"0ed8d1108fe7a5e1c3078ef1faea12199a56b6b1216ccaceb0f6103b0c9a7691","names":[],"sources":[]}, {"digest":"0eeb17e1df89b1ebc2902116fe7145e0d321ef0db26e6bdebc802cb8513a01c1","names":[],"sources":[]}, + {"digest":"0ef3072084202b084b2a25678fe9014ec339ee3797bc0bb164538ff3fa5e3fcc","names":["packages/llm/llm/src/message.ts#DeveloperMessage"],"sources":["packages/llm/llm/src/message.ts:157"]}, {"digest":"0f46153645c297846a3fad3911636659ca4717a5e7d71c9f50bffacd312115ef","names":[],"sources":["packages/attachment/attachment/src/types.ts:28"]}, {"digest":"0f8512903d94f57a4748fa1a2092e64342856796684e6b8343db685b192745ce","names":[],"sources":[]}, {"digest":"0fe52387f65052702ed9d7cd59db826468936595d1b3eec207e7a823adc719db","names":[],"sources":[]}, @@ -109,7 +111,6 @@ {"digest":"124aa62078ca3311353b39cc3bf6c75da281338be6bbf672f8b361d828cddca3","names":[],"sources":[]}, {"digest":"12a0cfa619ac4a5fc6c3e8e359c8dae15769edc61497a575935c78d5842f8e42","names":[],"sources":[]}, {"digest":"12d5808fac1996b9b3c2afa806e03022252ba2d7b88f835862ae1896591f60c7","names":["packages/llm/llm/src/types.ts#ImageBlock"],"sources":["packages/llm/llm/src/types.ts:79"]}, - {"digest":"12f9a5a7506bca46d6ef8e3c71826c4bf40e1389bfe0236cfffaea0c3fb367d2","names":[],"sources":["packages/core/session/src/types.ts:317"]}, {"digest":"1379f0b72275994d20f1ddca2d60a96ab16ec80397ca9f1af21bdc67ba5c269c","names":[],"sources":[]}, {"digest":"13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176","names":[],"sources":[]}, {"digest":"14230f9b8b5797bb7e8c621fa636531cfbde2ffba1ef5f1fd058216ff21952a0","names":[],"sources":[]}, @@ -173,6 +174,7 @@ {"digest":"28936c614bd4e7e2774806edcbb3cbfd3ee643ac1cbf0bbdff43d915505a8fe3","names":[],"sources":[]}, {"digest":"290f32bfd81ff85a8095886a8066fae06e885942b63a8bcd6a8d66d328445808","names":[],"sources":[]}, {"digest":"297fbd8067d7815adae02b89790ec6ff5fb7c2cb0d0eb026e1746c24ec857761","names":[],"sources":[]}, + {"digest":"2a668a48dec1739752123d67c3bc254412d6743dbe3347365e0ed83a542f2bc3","names":[],"sources":["packages/core/agent/src/types.ts:113"]}, {"digest":"2aad1179f957631477b582e57cd8f538cb82618fc3fb816bde1171154235b065","names":[],"sources":[]}, {"digest":"2ab12a0e345658f9342816d773fe610f049982bcad82631f43b9205abe43e5ee","names":[],"sources":[]}, {"digest":"2abb711460e6238d851c30f3e4bdd87a2aa1887ee70575b3d3b7b25610673095","names":[],"sources":["packages/llm/llm/src/types.ts:473"]}, @@ -186,7 +188,9 @@ {"digest":"2eafefdc6f79a4922d68a787a09eaf4dd2b2369c3777fba4c05e800b5678ac8e","names":["packages/experimental/agent-team/src/types.ts#TeamMessageSnapshot"],"sources":["packages/experimental/agent-team/src/types.ts:129"]}, {"digest":"2f1e6a7ebace43d727ae138ddcd352eb3813960009e5805b8c5ac84300c97716","names":["packages/attachment/attachment/src/types.ts#ImageMediaType"],"sources":["packages/attachment/attachment/src/types.ts:8"]}, {"digest":"2f3f201c8846e19c20bb75c02280e3d4eb3c57509e4715f0c0a4d1d4f91a7a06","names":[],"sources":[]}, + {"digest":"2f5b14bc2ccbe8810568df6392f1a8e0c3a2be59440051b60666266519e403ff","names":[],"sources":[]}, {"digest":"2fe5026daf14bd4ff2b7bfb7cf9791c302d9cbba06802178b6dfec00b892698f","names":["packages/core/tool-executor-ptc/src/types.ts#PtcDispatchStartEventData"],"sources":["packages/core/tool-executor-ptc/src/types.ts:12"]}, + {"digest":"30093f9f63a120cb932f07577594c04afcec3f0bd02859154a6e5b0e79a46359","names":[],"sources":[]}, {"digest":"30d5a537b296685770b1241c4b4bbde19d5a31ff81aa23df73fab5e90b8fc65f","names":["packages/session/session-title/src/types.ts#SessionTitleSource"],"sources":["packages/session/session-title/src/types.ts:28"]}, {"digest":"312aee2f0c42aabbae6cc60d92d596d8453c0aeb061c5868d1755a69ef49e69a","names":[],"sources":[]}, {"digest":"31907de9563751822af6d9be02bafaf26faa098bbd1cb412524134d577cc1648","names":["packages/compaction/compaction/src/checkpoint.ts#CompactionCheckpointSource"],"sources":["packages/compaction/compaction/src/checkpoint.ts:22"]}, @@ -195,6 +199,7 @@ {"digest":"3305a4bb6b6c29b482ba91d59c60b47e8f98282d3dacc8c29e4a4c2712209bdd","names":[],"sources":[]}, {"digest":"33341aac09312300de5a178e6cc6f6544a4876f7f710cfea55eda473c8d329ef","names":["packages/llm/llm/src/assistant-stream.ts#AssistantStreamRecord"],"sources":["packages/llm/llm/src/assistant-stream.ts:20"]}, {"digest":"335e242de1fcc17b6ca920fc420d71bec2d76e53e37955c00948b65ab77f05c5","names":["packages/core/session/src/types.ts#SurfaceOp"],"sources":["packages/core/session/src/types.ts:470"]}, + {"digest":"347c1718600bbb3814ae6b4df06e6aceae35e8a8e8cb3c39ac28b2e932c5abd6","names":[],"sources":[]}, {"digest":"347de3380f2da308d0c74e08821f9e85559f55af4c5010795b110d00f7fc9dbd","names":[],"sources":[]}, {"digest":"34d1ab5c5df378186d6054b1a1beea9a41e41965d6f336f0bc0025c441a82e66","names":[],"sources":[]}, {"digest":"35203ba7ad5ef6f97d556b85df20ae98f04f09c65748cecdf8eefdb8b6405ffc","names":[],"sources":[]}, @@ -207,7 +212,6 @@ {"digest":"383cf73ae4748e5beb281ae74857ba1ffb92ae01075b9fbf51af78b666007a8f","names":[],"sources":[]}, {"digest":"3860b4757d512a2869be833acb0faf834335b1b056bf3f83df5474dce95f8fa6","names":[],"sources":["packages/llm/llm/src/assistant-stream.ts:35"]}, {"digest":"390eed1028aba02774f14408647acec10ec5516e97d4f15ecde2b9989d5a69f2","names":["packages/feedback/message-feedback/src/types.ts#MessageFeedbackRating"],"sources":["packages/feedback/message-feedback/src/types.ts:17"]}, - {"digest":"39adb7fce8475af6402acd925ea79697b3341c22422ac32833d71217d84e987f","names":[],"sources":[]}, {"digest":"39cf2d3ff8bac81c62d49ac2ba38a095b18e80f3ad700819fa1f650a4e60197f","names":["packages/jobs/jobs/src/types.ts#JobToolCall"],"sources":["packages/jobs/jobs/src/types.ts:16"]}, {"digest":"3b04790e08b35241ffa0d38f6d7e4f8788aa921341f6c0d7574a329397c59408","names":[],"sources":[]}, {"digest":"3b1be838223869fe0a08210db85bf773796ed3f2373ac16555dff227cade0c48","names":[],"sources":[]}, @@ -227,6 +231,7 @@ {"digest":"3f74ba74acd10644c1ed6947a411d8088bd2cfac74f61bb8540f96d68e31f004","names":[],"sources":[]}, {"digest":"3fdf9582c3b5597df104d32d97dab6c6ee3a93794f3034ec6ada6272404ea27f","names":["packages/schedule/schedule/src/types.ts#LegacyAfterScheduleRecord"],"sources":["packages/schedule/schedule/src/types.ts:161"]}, {"digest":"408927267422a7b2beb6df354ff7c1406d929a5646554f48adf85eb8372f811c","names":[],"sources":[]}, + {"digest":"414a44a96f06339f04657b0e441a2baed954f7e31aa1552f37dbe7107f0df8e9","names":[],"sources":["packages/core/agent/src/types.ts:24"]}, {"digest":"4208123b50df5006b181481ab45fcf1cde807b88d3fd4d340090bc2e202fac41","names":[],"sources":[]}, {"digest":"42e0916e0dda5f6d1e7bb05d8514717147c036a79c9f36085683469516c1fd3f","names":[],"sources":[]}, {"digest":"4312134fc6f9cad507c31390bd3d147535492dd77621ec517c5e85f853d53a03","names":[],"sources":[]}, @@ -258,7 +263,6 @@ {"digest":"4e97c3d85c0fc817ee58873c38c25a2474abaaa0af4839311b50b68db3b8cf1a","names":["packages/subagent/subagent/src/continuation-messages.ts#SubagentSettledMessageSource"],"sources":["packages/subagent/subagent/src/continuation-messages.ts:29"]}, {"digest":"4edec1b770e8ec4ce1220bf42edc5ac46713a1b48d4caa778dcfc98bad1f28a3","names":[],"sources":[]}, {"digest":"4f507598cf476214aa7646ce7841a7064a9dcb05d0a1eaaefaf79c2b738f160f","names":[],"sources":[]}, - {"digest":"4f9125fa028a56286fac70176ecc670d12f4402d8cbe112e14f0639f5e8e5c9d","names":["packages/llm/llm/src/message.ts#Message"],"sources":["packages/llm/llm/src/message.ts:196"]}, {"digest":"4f91f187baeabef5c7950c4780d31bbe39f3345dff591a6cf26ac7e135ac1cf4","names":[],"sources":["packages/context/session-reference/src/types.ts:18"]}, {"digest":"4fb59762612c3e3ac3a3bd4f84c9c148d3c3893bd422ba2b201cc039fabd49bc","names":[],"sources":[]}, {"digest":"50cf02d875f8a1f1411f4a6fbcffab9120ccef5f74a47331fbf29480e5f68f48","names":[],"sources":[]}, @@ -311,11 +315,13 @@ {"digest":"6292bcbedaa97feaec5b48a03bb8144b581b93309d9d6c905f26da7a6a7a0a4f","names":[],"sources":[]}, {"digest":"62e6a429ff3f390a46ac39fb468692032f42936ca0db19b53454184189401379","names":[],"sources":[]}, {"digest":"62ecb62dc0fe1b5e70217f8a80c39256a40ee3a1917be14e5ca899d20c35f7fd","names":[],"sources":[]}, + {"digest":"63ba8895cc09e4693cf0819fdcd18c084bef195febda5b6a23af279b3852be36","names":[],"sources":[]}, {"digest":"6487b10ba7542f3c447b1e41a1c6a590b7bfafe0b9b66b49f34028da85bb970f","names":["packages/subagent/subagent/src/descriptor.ts#ContinuableSubagentDescriptorData"],"sources":["packages/subagent/subagent/src/descriptor.ts:72"]}, {"digest":"6598cd6da253948d6b207c6374a45335804c79b576866d6215649c2d1bc85e25","names":["packages/experimental/agent-team/src/types.ts#TeamMemberSnapshot"],"sources":["packages/experimental/agent-team/src/types.ts:48"]}, {"digest":"65a8400e451e667e5b6e34df72ceb81fca8c12330330fa72083ad40279b189ad","names":["packages/llm/llm/src/message.ts#SystemPromptMessageSource"],"sources":["packages/llm/llm/src/message.ts:35"]}, {"digest":"65d583e66518c5236015e0846470fb32efcf92375ed306aef14e50a9d2cd2099","names":["packages/skill/tool-skill/src/index.ts#SkillCatalogSource"],"sources":["packages/skill/tool-skill/src/index.ts:35"]}, {"digest":"66ba70f6f068da443f969a0b947a013c3addd776b9b60a0a393759c19825e9a2","names":["packages/llm/llm/src/types.ts#ToolSchema"],"sources":["packages/llm/llm/src/types.ts:491"]}, + {"digest":"671e72b8036a3afb018b38ca5ae9ee3aabe6a314647411655b4b9497b75010b1","names":[],"sources":[]}, {"digest":"672c4aa2a9b736e3d6e5da6346d2e2ed5e3573f2d0baafa18e7407c9f3fee3db","names":["packages/skill/skill/src/index.ts#SkillInvocationSource"],"sources":["packages/skill/skill/src/index.ts:146"]}, {"digest":"67d5f73ceae82f5265258142ad6cf172de2c1fef93ba551b7f0454e0a93bffe0","names":["packages/llm/llm/src/call-config.ts#LlmCallConfig"],"sources":["packages/llm/llm/src/call-config.ts:31"]}, {"digest":"68141cbe3994a5ffcd064c07c75996538437fd9dbf77ce8a2927e89ec577fabe","names":[],"sources":[]}, @@ -364,6 +370,7 @@ {"digest":"7b1ce60193fa8b69292e5843858d9cee7dc565dcb40d33c136e5d7e8b8d4296a","names":[],"sources":[]}, {"digest":"7c6129b6961cf4b2a030f36c78d2b0a5be38617651a9b81322e054ee512bee06","names":["packages/feedback/message-feedback/src/types.ts#MessageFeedbackDelete"],"sources":["packages/feedback/message-feedback/src/types.ts:46"]}, {"digest":"7c9f44e90a0058f4cc532ae20dad0c10afa6eba22e70a6c79fc79490bad64397","names":[],"sources":[]}, + {"digest":"7cf091467eb288875ea4e88397003a7e43d95ab75afaf1b42554a8cc766799f2","names":["packages/llm/llm/src/message.ts#Message"],"sources":["packages/llm/llm/src/message.ts:196"]}, {"digest":"7d226b9fd95b4ba213d4237c22e930a91c3c769c95ca7f92977b9d625ba13438","names":["packages/goal/goal/src/domain.ts#GoalChangeMeta"],"sources":["packages/goal/goal/src/domain.ts:44"]}, {"digest":"7d54a5a5e629e2e7783bfac72683bcc5e969e9f43426b9289826aaf294ff6344","names":["packages/feedback/command-feedback/src/types.ts#FeedbackRecord"],"sources":["packages/feedback/command-feedback/src/types.ts:27"]}, {"digest":"7d65acf0a3a43a8f445ce5d3320b42eca492739fc931b2a7516bfb8368dc2c68","names":["packages/llm/llm/src/types.ts#ContentBlockType"],"sources":["packages/llm/llm/src/types.ts:148"]}, @@ -405,7 +412,6 @@ {"digest":"8ad45ad053b4d89f8865c17cc9ff3b8748e86045f3f0fb816269a34af2619141","names":[],"sources":["packages/experimental/hook-protocol/src/types.ts:31"]}, {"digest":"8af841fc9b265ade3441caf9fe40dd24db24226befba48150e6d0eeccc1389c3","names":[],"sources":[]}, {"digest":"8bb8c3751a6ce64b61aba1ec7ae798c00a5803c4d868a0430873216c6569ed52","names":[],"sources":["packages/session/session-log-deepseek/src/types.ts:81"]}, - {"digest":"8bca2b4f53ec7745b0484c6e7d3a6beb2ddd6cea82fc81786eaa3c0d065bbc14","names":["packages/llm/llm/src/message.ts#UserMessage"],"sources":["packages/llm/llm/src/message.ts:162"]}, {"digest":"8c1c4f72755a78ea163523a46565316e36e9b644382eb48e6635c71a6f412dd2","names":[],"sources":[]}, {"digest":"8c627d7a45fa406cea879a3922105e27ed4c3c7966e264afaf11ea7a330f2dd1","names":[],"sources":[]}, {"digest":"8cdd3fdd295cee53035de16a1b23411716f75e9f994fe9510cac5d7fef605c76","names":[],"sources":[]}, @@ -413,10 +419,12 @@ {"digest":"8e611bf46821e5003a1605a73560a6f56388295149d449dfe2142d5ec335f29c","names":["packages/llm/llm/src/types.ts#ToolAdditionBlock"],"sources":["packages/llm/llm/src/types.ts:115"]}, {"digest":"8e8155594e07812356b67d732196dda894b0f9c91a5969d51612cf490b0a016d","names":[],"sources":[]}, {"digest":"8e8e91646a5cae3fc78f7532a0013ff2088ae47077642c2edd507a508209b8af","names":["packages/schedule/schedule/src/types.ts#OneShotScheduleDispatchChange"],"sources":["packages/schedule/schedule/src/types.ts:237"]}, + {"digest":"8eda0195dd1d4a3fee80f5e0b9f702fdf0f342c27ece60d44d459899b6a30b60","names":["packages/session/session-title-llm/src/index.ts#SessionTitleLlmRequestEventData"],"sources":["packages/session/session-title-llm/src/index.ts:37"]}, {"digest":"8f324cea3916c0517261e88167def8efb3a9994c1135e23d40ed9fd48f8c7bd4","names":[],"sources":[]}, {"digest":"8f6d609794bf5afc01d7ccf3e03811d32f4b8047f58a2c9a98db0154c2ad7cb9","names":[],"sources":[]}, {"digest":"8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543","names":[],"sources":["packages/core/tools/src/index.ts:28"]}, {"digest":"904bf499d98be4a3a5f836fcac533b6c4899569b018071e6604e1d32657d00fd","names":[],"sources":[]}, + {"digest":"92b82e8b7bbb38b3adeb2c07eb76d0f0e0d18c1729808def7004ac2bc9baa583","names":[],"sources":[]}, {"digest":"930c1cd289def119d443e19dd90008916bf035f0a4d23e07241e7f050f2b89b7","names":["packages/interaction/user-approval/src/types.ts#ApprovalOutcome"],"sources":["packages/interaction/user-approval/src/types.ts:32"]}, {"digest":"935c8d486292b13c87b57ca280eb8c58b5b2f5239be86fcd4334c33fdeb87682","names":[],"sources":[]}, {"digest":"938f7c7357bc403ea3b281a9dd016939df7a0880fc8db12612585be574d0d615","names":[],"sources":[]}, @@ -426,12 +434,11 @@ {"digest":"94c56aeb1564ad735eb773d88636b40761d063efa1610f1308004d32923f5378","names":[],"sources":[]}, {"digest":"95080295115fbd83544520ba247b68a03790131f6a2eac323d388758cb542f65","names":[],"sources":["packages/llm/llm/src/types.ts:472"]}, {"digest":"95a3ffccbf3c8afcf9a2f3b355f724da8e07e98dc47ee741166dde7f9ea058bd","names":[],"sources":[]}, - {"digest":"95acdbd066cf445d37092e5fdfe2e8717ef6ca1aed50f70e2e6037a6989a1f2f","names":[],"sources":[]}, {"digest":"95c0e88fcf0bafb2087f912ad9e2369de6ae6530cc0c4c2772c96205b57f7f60","names":[],"sources":[]}, + {"digest":"9632024d168f8169ad83fe3639da51c864ebb5444c9fc965ac9342611b2d5263","names":["packages/llm/llm/src/message.ts#UserMessage"],"sources":["packages/llm/llm/src/message.ts:162"]}, {"digest":"9645cf93769a6dfb227bd400bd6cb9fa7bab9140f4a851e4ae92514ade6e3114","names":[],"sources":[]}, {"digest":"96bfb916a727cd4445a594df264c523fc039749a31bfed924b8cc7bb308a5bb0","names":[],"sources":[]}, - {"digest":"96f4a9c81fc0f940ef71528a8cc66731ae4cf1f40d79680c5fe3c39f96d7723c","names":["packages/core/agent/src/types.ts#InboxTarget"],"sources":["packages/core/agent/src/types.ts:40"]}, - {"digest":"9808c3d0f0a1be8fe313b4148ba22c691c4d59bd2c67aef3c63cfe8043148c5f","names":["packages/session/session-title-llm/src/index.ts#SessionTitleLlmRequestEventData"],"sources":["packages/session/session-title-llm/src/index.ts:37"]}, + {"digest":"96f4a9c81fc0f940ef71528a8cc66731ae4cf1f40d79680c5fe3c39f96d7723c","names":["packages/core/agent/src/types.ts#InboxTarget"],"sources":["packages/core/agent/src/types.ts:47"]}, {"digest":"9890113e97dd3541787bfe6704f2b223d3a890b5325ca0acae2b0398909b413e","names":[],"sources":["packages/sandbox/sandbox-policy/src/session-mode.ts:33"]}, {"digest":"99135d8a3937977955eaac5f8f04f930dcd1562f8450a54a51cde9b921dd59a6","names":[],"sources":[]}, {"digest":"9947aa5938fe19a895298da412917acbf9ffb5093a73ab8baa257e6ae9e63a07","names":[],"sources":[]}, @@ -449,7 +456,6 @@ {"digest":"9e07693fd7cd9babe0688c22c986e4d519ae1c0e40eeea13b2dc583bed2abc86","names":[],"sources":[]}, {"digest":"9e2c06d8653585b33d72d5c1a80da79c8a24a88c284409bca15cc0f0ead5ebfb","names":[],"sources":[]}, {"digest":"9e41386b3a0c9572b0d63078492ebb3997da7d3a830d44e0259418bd02f4bcb2","names":[],"sources":["packages/compaction/compaction/src/types.ts:38","packages/compaction/compaction/src/types.ts:84"]}, - {"digest":"9e8043098fb89db6d7c92a92c70146c5c8d24fa88f733c687fa29e5e7a58ab30","names":[],"sources":[]}, {"digest":"9edc162949abf93bd5ff049191b345454ba104f583937ce5580fb50f067d2b94","names":[],"sources":["packages/experimental/webhook/src/types.ts:74"]}, {"digest":"9f65c43e340bba31cdc0fc37bd93ccb68d5e9b36f47a68c207e95a8e68620a5d","names":[],"sources":[]}, {"digest":"a0a2e5c42e1c929445ecd1cd70f49be6b66441894ec72c08e8ae332821d4a3cb","names":[],"sources":[]}, @@ -469,6 +475,7 @@ {"digest":"a40d12070f6f4a124f32fb3cc86e7857554702e32f0eda3080a25c4b9ac9b18c","names":[],"sources":["packages/experimental/agent-team/src/types.ts:232"]}, {"digest":"a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc","names":[],"sources":[]}, {"digest":"a6b41f5bede8094d7bf635be5d69b1bd7a25e006ca9cf9415bceb9322f1a7feb","names":["packages/experimental/hook-protocol/src/types.ts#HookDialect"],"sources":["packages/experimental/hook-protocol/src/types.ts:48"]}, + {"digest":"a6d8c25f4b2ca2d137672a8a278ee1398df3f3cb60f5d32d8e3007c68299b664","names":[],"sources":[]}, {"digest":"a6da9ad5d42ae83a6833a8288b076688e8e0801633ea36cbdd8a30a6d9f3160c","names":["packages/workflow/tool-workflow/src/types.ts#ToolWorkflowRunStartData"],"sources":["packages/workflow/tool-workflow/src/types.ts:14"]}, {"digest":"a720757f704a2512e9d2f20da4a093e582472176236a75ab8b8eb7aaff17d85a","names":[],"sources":[]}, {"digest":"a7cf43ce7c2a4c038feed1885cd7a00d5c6ee2d90a7e0d56b46f78a3e1ca327f","names":[],"sources":[]}, @@ -484,7 +491,6 @@ {"digest":"aa7bc2817014ca6c45719b1c193c5c131be718a0921f34345098ff250f444626","names":["packages/feedback/message-feedback/src/types.ts#MessageFeedbackPut"],"sources":["packages/feedback/message-feedback/src/types.ts:38"]}, {"digest":"aa93010d0e521ad7c42273f58d5c9b8bf1dfc94edf1b805a3971b364a35c1877","names":[],"sources":[]}, {"digest":"aacda84a69739238aa782eea902d3073017d9d74b9808de2bf5a83c9738cbdc6","names":[],"sources":[]}, - {"digest":"aada2771a250136cc3a839b0507e9c6502622a2897716903942297bcb6661292","names":[],"sources":[]}, {"digest":"aae343bc3d663c83f2e8b5e1c7eea5d509dc7a5361bc8bd8b5ac3bb8d3b1ed56","names":[],"sources":[]}, {"digest":"ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7","names":[],"sources":["packages/core/session/src/types.ts:220","packages/llm/llm/src/types.ts:159"]}, {"digest":"aba175fb20ffc71112c55efe70458ff5d7dbfb83501892b4ba8d077f0ea5d7ba","names":[],"sources":[]}, @@ -503,7 +509,6 @@ {"digest":"b0127044ab31a702bddfd785d345f5abd7a70876746e895ce443afa3e60ddf2d","names":[],"sources":[]}, {"digest":"b062078e899958f99ae464514578b8c093924dfe34bd9a835932b420ff2150b1","names":[],"sources":[]}, {"digest":"b0b0eb1b16490f6bee135b2377e8f8c39227974c799ad7d49ff418af23ecbc35","names":["packages/attachment/attachment/src/types.ts#ImageAttachmentRef"],"sources":["packages/attachment/attachment/src/types.ts:11"]}, - {"digest":"b112459dcb0686091c31ce6d213b2a4934b5ad19d37cc5b599e46f5a9d715591","names":["packages/llm/llm/src/message.ts#MessageSource"],"sources":["packages/llm/llm/src/message.ts:136"]}, {"digest":"b158f3ff6923714807c0d73882f0b05dc87f8fa85c9e90814852d48bbc866081","names":["packages/core/session/src/types.ts#RequestContext"],"sources":["packages/core/session/src/types.ts:260"]}, {"digest":"b1664fa89f8eab49a14e7e3168d8bc4da4aa9fd76332f6c4a053d839e5276ae7","names":[],"sources":[]}, {"digest":"b1ebd5cda47d90d980bb05bffc3f970c51537b9aff15b3bc3d639811727741b1","names":[],"sources":["packages/experimental/agent-team/src/types.ts:230"]}, @@ -556,13 +561,14 @@ {"digest":"c219efbdfd53b836786202ab4532913d555798f0a8615a4e9629736e3799fb70","names":[],"sources":[]}, {"digest":"c2deb7c9183735d4e0fffddf5c623f4e34daa1c8e96b111982e957dfcbbcf896","names":["packages/workflow/workflow/src/types.ts#WorkflowAgentOutcome"],"sources":["packages/workflow/workflow/src/types.ts:110"]}, {"digest":"c2f49004d9ae38a964e80850c87bc6849d6a872091a1d43e99eedf00c641e2a3","names":[],"sources":[]}, - {"digest":"c433ef580a5b229bbe68274a7164aa3a8e8040d7217f010b610ca0696a84253c","names":[],"sources":["packages/core/agent/src/types.ts:110"]}, {"digest":"c43b119ce579b3e9983a0d3adc4ba402ebb9ba773d89738a2dab01ecf5813943","names":[],"sources":[]}, {"digest":"c5228b7a55498d84f529591744a0ab26e0b93ade18a70a26db1377a1ecfa6b49","names":[],"sources":[]}, {"digest":"c55827fba7018198772c50e37cb7d3f60a3d8bb6e94b07de72dee0d9f8968005","names":[],"sources":["packages/interaction/user-questions/src/types.ts:134"]}, {"digest":"c5687b1313206a6ff334066047b6a0681611467dc4df8c5a8e71d0b6df39311f","names":[],"sources":[]}, {"digest":"c589e938cc0e4989df8b82abef6339de621f0dc4fe62c230d145b94996af7f92","names":[],"sources":[]}, {"digest":"c592ce75aab73fcab19c1d7845684c72cf402b78d2e1f2833a58ecf9f3598ed6","names":["packages/util/values/src/index.ts#JsonValue"],"sources":["packages/util/values/src/index.ts:4"]}, + {"digest":"c5bbcb3435908af2036df7105883e0a49c30ecc1fae4420aa5c4cf944066eff5","names":[],"sources":[]}, + {"digest":"c66c6c3b411abf46c1f9b99e0019c10c1b6d13ee716a4b5e88f7e1b65feb3039","names":[],"sources":["packages/core/agent/src/types.ts:119"]}, {"digest":"c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262","names":[],"sources":[]}, {"digest":"c8e6f48878cc2a2f0dc59682280d81f407943fb503f32c64bf9ff3d9393c3eff","names":[],"sources":[]}, {"digest":"c99e20441bf15341e9db54385e558e5c03d21dff2eb1ac08ccbf47c2d8747954","names":[],"sources":[]}, @@ -575,20 +581,20 @@ {"digest":"cda2345a9be9d139df49ccbd16dcba716f3440178bf20ff8b801fda1f5eb77dc","names":[],"sources":[]}, {"digest":"cdca80b8dad24d4e7b1d5a288a8475ba8d8d46be385bd2da85f122ee836f1670","names":[],"sources":[]}, {"digest":"cdee10e87dbed8440d8b6935888624884878efc7af95402e94171b29f173dced","names":[],"sources":["packages/llm/llm-retry/src/types.ts:17"]}, - {"digest":"cebb7e9f3f663e660a531d9764ecfb9fa7fc73c22bf8d628f888e9ecf18c5cf0","names":["packages/llm/llm/src/message.ts#DeveloperMessage"],"sources":["packages/llm/llm/src/message.ts:157"]}, {"digest":"cef171adf021a1867d244618a3dbbaafc22beb17c7f3df3ee203393271c8c6f6","names":["packages/core/tool-executor-ptc/src/types.ts#PtcDispatchEventData"],"sources":["packages/core/tool-executor-ptc/src/types.ts:21"]}, {"digest":"cffcdc19881ab61ec0a9ce18743996f7ac44ac8889f34dfac36ae745574aa22f","names":["packages/goal/goal/src/domain.ts#GoalClearChangeMeta"],"sources":["packages/goal/goal/src/domain.ts:35"]}, {"digest":"d0fbb0ca1bbba059830826f33f801da1aced35a2504376d61e020bdfec8d50c0","names":[],"sources":[]}, {"digest":"d28765f87d6b6ab408e0b2f3e7e297791b004b806d8e302c49903ebae815feb2","names":[],"sources":[]}, - {"digest":"d2b617d9dfcc4e0a4b60732d8ba1effc632e3b75124b93b064c07814a0a716bc","names":[],"sources":[]}, {"digest":"d4b739491689cc6bb13d07ffac1a32239875abda364c0d5edd8f74faf5e9c00d","names":[],"sources":["packages/llm/llm/src/types.ts:475"]}, {"digest":"d4c799e3d51cae1040362ba4968efc484739e14416c9c1df20c2f108b4ef2eb5","names":[],"sources":[]}, {"digest":"d4e4d575abeb4b72d616dc65c17ac0eaa943cc135609395044c38d5afcb10e31","names":[],"sources":[]}, + {"digest":"d582ddae3f21edf55b6f50454e3015ef78ed3788fee4670827f2c3c4bd0b47e2","names":[],"sources":["packages/core/session/src/types.ts:317"]}, {"digest":"d595ec73b32b016a6055333c67a5d646032b22e672da1d4a09c5b1ae398a093a","names":[],"sources":[]}, {"digest":"d63b8b8ffad9c02fd80c43a17df4f240c1fe8118ecca9de34f9d5871838ab5b9","names":[],"sources":[]}, {"digest":"d66e11dddf5dfa3c71547e2cbd7b0608824cd8d6243fc16f532017ff28e8fba5","names":[],"sources":[]}, {"digest":"d6e654b88b584fcdf290a56802b1730bc7b9847ae86baa4265ab7769eb48990c","names":[],"sources":[]}, {"digest":"d85590a5675c6e2fd8164e5792a6cceac714605b8ac228adae5ee668facb1490","names":[],"sources":[]}, + {"digest":"d8dde9629b7323b03c1c3fb0db75d6b72c73666dc0240ab7d3e41b9c0c40fc7c","names":[],"sources":[]}, {"digest":"d9196086e41e4dd5c2ce87f5c25529c8112a1731d258cf0728747d41a9ee3241","names":[],"sources":[]}, {"digest":"d9c462b9f62fdd3efc76a44b9524dc8781f6b91cd9c7e584b17d971defafebab","names":["packages/llm/llm/src/message.ts#ToolMessageSource"],"sources":["packages/llm/llm/src/message.ts:29"]}, {"digest":"d9edf1862e4471d6ad8a106a3f4160e0643874368707d50ecd8e27e5ee7a2567","names":[],"sources":[]}, @@ -612,6 +618,7 @@ {"digest":"df843efd87c78cf8de98f8e0d0216956cd568d0d0e56a22c853663e274aa1a18","names":[],"sources":[]}, {"digest":"dfce5db4aa42b7ebe0bb79eca92553587287e18c06b9eba85cbfb9d332e26acd","names":["packages/experimental/agent-team/src/types.ts#TeamMemberPhase"],"sources":["packages/experimental/agent-team/src/types.ts:45"]}, {"digest":"e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05","names":[],"sources":[]}, + {"digest":"e103228e063b4762f2fb51ee5e50acaddd53a513de107efae09bdebf2958a664","names":[],"sources":["packages/api/session-controller/src/types.ts:399"]}, {"digest":"e2ad8f5bcd813f268ffef1b10a12d7ddd2b8ced8b9b6d0275b3f6b7ed094a187","names":[],"sources":[]}, {"digest":"e2d0367e5716bc188435e51718ebff8ce873190625fa93b1c5331102d98758fa","names":[],"sources":[]}, {"digest":"e2f9a41e0989f54ed8cee80f8db2bcf9d60a5c810dc9d45b83fa050b9dce7602","names":[],"sources":[]}, @@ -619,6 +626,7 @@ {"digest":"e308ccf867a5398e316e0af8cb6ce238a8d33a63b9b384c8250a686786285f72","names":[],"sources":[]}, {"digest":"e31572180d6b28b73f073ad30ae19d5d82982d3daf984723984e23da1e7c459a","names":[],"sources":[]}, {"digest":"e3e77b26f0148755a505f5b8ea843a3be117827d4703bb5754d9dc8ddbef521f","names":[],"sources":[]}, + {"digest":"e3e9f4d1e245b470cba77c3e6b94ea141408604d693b2a8437278abc8cfd8e2c","names":["packages/llm/llm/src/message.ts#MessageSource"],"sources":["packages/llm/llm/src/message.ts:136"]}, {"digest":"e41bdebe246abe17154de7e7a1d81035437eee8d20e7f1edff0d91e2598ad828","names":[],"sources":[]}, {"digest":"e42b0f40b5d0b628c6e1c44a8fb9eda0780037a81959e1b93d2dfb47b46b0e87","names":[],"sources":[]}, {"digest":"e438a9d4fb883b3e4a8d9fc8913aa1550ed50eb384bfbf943100a5605b537498","names":[],"sources":[]}, @@ -638,7 +646,6 @@ {"digest":"e95f957154b2b1b58692af53d4131180b6dd19e2264869f876dbaa959991494e","names":[],"sources":[]}, {"digest":"ea08ec1df0c33108fd03345b9af199cf04dea450aa045d3c683f216311403403","names":[],"sources":["packages/experimental/agent-team/src/types.ts:234"]}, {"digest":"eaa71be0fbf0f702df8ea6c11d0aa2f9782263d45a10d0fb14eda662f4bfe04c","names":[],"sources":["packages/core/session/src/types.ts:201"]}, - {"digest":"eaa9125aec0fb28478096c46e73e21bf512ab0d84d826bfb41e7308707734cb5","names":[],"sources":[]}, {"digest":"ebb768d85ec87fb68e42fc14fd7d8535b5ac11791b14d04c6770180f0a719bf8","names":[],"sources":[]}, {"digest":"ec38b5949af8eacaf00df002f4acbe344f934f8a061e9cdc65a52a48e5f6dd93","names":[],"sources":[]}, {"digest":"ec8b5ff7c77ca6893ac7ba5a0c5b2aefeb52f12c64450b7359660c2a7e8a53d7","names":[],"sources":["packages/core/session/src/types.ts:234"]}, @@ -651,7 +658,7 @@ {"digest":"ef226a90ddac6cd8e631a3a337a3a3e27f9494be92befb7c718b404681d75e21","names":[],"sources":[]}, {"digest":"ef4f00923b9edbae15c7ef05320280bf61147ca17146cf59690359bfd5e5da45","names":[],"sources":[]}, {"digest":"f05fb85e715af4ab4de0b6ba111166dd0435bd8bb74a0fa0f32b9d174113fc7c","names":[],"sources":[]}, - {"digest":"f09e5e7acf5ff5ccd193ff2eeec015fa2ff7c29d98bc465f8c3302058d47e29b","names":["packages/core/agent/src/types.ts#ModelSelection"],"sources":["packages/core/agent/src/types.ts:79"]}, + {"digest":"f09e5e7acf5ff5ccd193ff2eeec015fa2ff7c29d98bc465f8c3302058d47e29b","names":["packages/core/agent/src/types.ts#ModelSelection"],"sources":["packages/core/agent/src/types.ts:86"]}, {"digest":"f1687f982fa3d41ef29e159feed154ecd13af4dd209acaee16038f223f2e9402","names":["packages/core/session/src/types.ts#TurnEndReason"],"sources":["packages/core/session/src/types.ts:238"]}, {"digest":"f1996af2602e6f62b5ab179ca5c4af00d8753938b74cc6ef102de09e99e74a7e","names":[],"sources":[]}, {"digest":"f19bee84bf3d20904553cdcd57465232bc1c2a194f54c4d195292ba0487223a6","names":["packages/attachment/attachment/src/types.ts#FileAttachmentRef"],"sources":["packages/attachment/attachment/src/types.ts:39"]}, @@ -679,6 +686,7 @@ {"digest":"fa87537f4a9c1e25602990a8bf686b383e9e00352ec8946e16ae24ba7c1625be","names":["packages/llm/llm/src/message.ts#AssistantMessage"],"sources":["packages/llm/llm/src/message.ts:167"]}, {"digest":"fab2bb09bc5e3b9db0685e8f5500b029af1a2b33e70bc703c1d5d0f596fbc1fd","names":["packages/sandbox/sandbox/src/index.ts#SandboxMode"],"sources":["packages/sandbox/sandbox/src/index.ts:30"]}, {"digest":"fab35c4520883c0d69e42d079c9f543fc5ac7a2d15b537b5572fcd99059c7018","names":[],"sources":[]}, + {"digest":"fb28ad7d7aabceb5bda505cf027e0e22354d7f31757337e7e51a4b592feac172","names":[],"sources":[]}, {"digest":"fb5e0517050d2c6d40e5b8145db1f7c5ca7044d5d50fada6ff80e0b22c6ca973","names":[],"sources":[]}, {"digest":"fb8b6a2e53cb9cf44c551176de0fb77d1ddc60037b99a891ff8a860b6d5c29a0","names":[],"sources":[]}, {"digest":"fbd2c74b346a1595b376139c587fadd23e4a21acc9c0abce342975b6cfc60986","names":[],"sources":[]}, @@ -722,6 +730,7 @@ "0e7b2a97f54f1afd": {"kind":"literal","value":"schedule/change"}, "0ed8d1108fe7a5e1": {"kind":"object","properties":[{"name":"kind","type":"a9059a81430b3207","optional":false}],"indices":[]}, "0eeb17e1df89b1eb": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, + "0ef3072084202b08": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"589d0d91239681f8","optional":false},{"name":"source","type":"e3e9f4d1e245b470","optional":false,"compatibility":{"version":1,"policy":"session-source-attribution","binding":"session.developer-message.source","discriminator":"kind","unknownKinds":"preserve","attributionKinds":["action-cards","tmux-context","user-question-reply","working-directory"]}}],"indices":[]}, "0f46153645c29784": {"kind":"object","properties":[{"name":"height","type":"0657d3ca5ac4e7bf","optional":false},{"name":"width","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "0f8512903d94f57a": {"kind":"object","properties":[{"name":"data","type":"e481c46af3813e00","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"4312134fc6f9cad5","optional":false}],"indices":[]}, "0fe52387f6505270": {"kind":"literal","value":"plan-mode"}, @@ -733,7 +742,6 @@ "124aa62078ca3311": {"kind":"object","properties":[{"name":"data","type":"da60ed58a8fa23d7","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"d4c799e3d51cae10","optional":false}],"indices":[]}, "12a0cfa619ac4a5f": {"kind":"literal","value":"read-only"}, "12d5808fac1996b9": {"kind":"object","properties":[{"name":"attachment","type":"b0b0eb1b16490f6b","optional":false},{"name":"offloaded","type":"dacf3d5f3e4ff619","optional":true},{"name":"type","type":"fb8b6a2e53cb9cf4","optional":false}],"indices":[]}, - "12f9a5a7506bca46": {"kind":"object","properties":[{"name":"headerSeq","type":"0657d3ca5ac4e7bf","optional":true},{"name":"message","type":"cebb7e9f3f663e66","optional":false},{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "1379f0b72275994d": {"kind":"literal","value":"developer/message"}, "13d3d180f977bf78": {"kind":"object","properties":[{"name":"data","type":"1528539c63db8b23","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"1a2718b00937b236","optional":false}],"indices":[]}, "14230f9b8b5797bb": {"kind":"literal","value":"chunk"}, @@ -797,6 +805,7 @@ "28936c614bd4e7e2": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, "290f32bfd81ff85a": {"kind":"object","properties":[{"name":"kind","type":"23af3759678a30de","optional":false}],"indices":[]}, "297fbd8067d7815a": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"a9059a81430b3207","optional":false}],"indices":[]}, + "2a668a48dec17397": {"kind":"object","properties":[{"name":"inputId","type":"4cd48f3e5108bd6e","optional":false},{"name":"source","type":"671e72b8036a3afb","optional":false},{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "2aad1179f9576314": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, "2ab12a0e345658f9": {"kind":"array","element":"83da72c6857601c7"}, "2abb711460e6238d": {"kind":"object","properties":[{"name":"index","type":"0657d3ca5ac4e7bf","optional":false},{"name":"text","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"6f091fe32ab24407","optional":false}],"indices":[]}, @@ -810,7 +819,9 @@ "2eafefdc6f79a492": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"senderId","type":"4cd48f3e5108bd6e","optional":false},{"name":"senderName","type":"4cd48f3e5108bd6e","optional":false},{"name":"targetId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "2f1e6a7ebace43d7": {"kind":"union","types":["3f74ba74acd10644","58f94390b982f8a5","de30b1fa12c75f81","ebb768d85ec87fb6"]}, "2f3f201c8846e19c": {"kind":"literal","value":"tool-goal"}, + "2f5b14bc2ccbe881": {"kind":"array","element":"7cf091467eb28887"}, "2fe5026daf14bd4f": {"kind":"object","properties":[{"name":"arguments","type":"bbc5696e3f13c758","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false},{"name":"parentCallId","type":"4cd48f3e5108bd6e","optional":false},{"name":"rootCallId","type":"4cd48f3e5108bd6e","optional":false},{"name":"subCallId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "30093f9f63a120cb": {"kind":"literal","value":"agent/retry"}, "30d5a537b2966857": {"kind":"union","types":["1b5649238876e288","ada310bf0bdb8fed","b81e9de81a481b24"]}, "312aee2f0c42aabb": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"d9edf1862e4471d6","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "31907de956375182": {"kind":"object","properties":[{"name":"compactionId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"6b23ed9ce1b397e6","optional":false},{"name":"sourceCommandId","type":"4cd48f3e5108bd6e","optional":true}],"indices":[]}, @@ -819,6 +830,7 @@ "3305a4bb6b6c29b4": {"kind":"union","types":["0341d4c3dfcd5325","07eccc9ea2f9b543","5bd310104cfe448e","7180844e1066230c","9d80129e13edfb6f","e4cac694f8b6e1d5","f05fb85e715af4ab"]}, "33341aac09312300": {"kind":"union","types":["3860b4757d512a28","487b65c37bfedb8b","71bb3254ec8f1e49","e72269f931559e90"]}, "335e242de1fcc17b": {"kind":"union","types":["bcf0caf62d964b2f","e438a9d4fb883b3e"]}, + "347c1718600bbb38": {"kind":"array","element":"9632024d168f8169"}, "347de3380f2da308": {"kind":"literal","value":"job/settled"}, "34d1ab5c5df37818": {"kind":"literal","value":"resume"}, "35203ba7ad5ef6f9": {"kind":"object","properties":[{"name":"data","type":"f09e5e7acf5ff5cc","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"1d5f04a03ea75fc8","optional":false}],"indices":[]}, @@ -831,7 +843,6 @@ "383cf73ae4748e5b": {"kind":"literal","value":"recall"}, "3860b4757d512a28": {"kind":"object","properties":[{"name":"args","type":"93c33d9687613293","optional":false},{"name":"dt","type":"5d03ba38734809bc","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"index","type":"0657d3ca5ac4e7bf","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":true},{"name":"time0","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"616f93a5047de668","optional":false}],"indices":[]}, "390eed1028aba027": {"kind":"union","types":["b1f62380f6746fed","df5d9709b7db2a72"]}, - "39adb7fce8475af6": {"kind":"object","properties":[{"name":"data","type":"8bca2b4f53ec7745","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"de25352d2dc41963","optional":false}],"indices":[]}, "39cf2d3ff8bac81c": {"kind":"object","properties":[{"name":"callId","type":"4cd48f3e5108bd6e","optional":false},{"name":"rootCallId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "3b04790e08b35241": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, "3b1be838223869fe": {"kind":"object","properties":[{"name":"data","type":"e4c18e294232c3ba","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"c589e938cc0e4989","optional":false}],"indices":[]}, @@ -851,6 +862,7 @@ "3f74ba74acd10644": {"kind":"literal","value":"image/jpeg"}, "3fdf9582c3b5597d": {"kind":"object","properties":[{"name":"afterSeconds","type":"0657d3ca5ac4e7bf","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"5b232a9b9bfcc0fc","optional":false},{"name":"prompt","type":"4cd48f3e5108bd6e","optional":false},{"name":"scheduledAt","type":"4cd48f3e5108bd6e","optional":false},{"name":"title","type":"4cd48f3e5108bd6e","optional":true}],"indices":[]}, "408927267422a7b2": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false}],"indices":[]}, + "414a44a96f06339f": {"kind":"object","properties":[{"name":"kind","type":"63ba8895cc09e469","optional":false}],"indices":[]}, "4208123b50df5006": {"kind":"object","properties":[{"name":"data","type":"ad0c38a0adb82a94","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"3e0eff5946255ff0","optional":false}],"indices":[]}, "42e0916e0dda5f6d": {"kind":"object","properties":[{"name":"data","type":"7f9365bae9fcb73e","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"fe792a6cd4304945","optional":false}],"indices":[]}, "4312134fc6f9cad5": {"kind":"literal","value":"turn/end"}, @@ -882,7 +894,6 @@ "4e97c3d85c0fc817": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"bf16d2e98a7f1389","optional":false},{"name":"senderSessionId","type":"4cd48f3e5108bd6e","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "4edec1b770e8ec4c": {"kind":"literal","value":"ptc-mode"}, "4f507598cf476214": {"kind":"literal","value":"series"}, - "4f9125fa028a5628": {"kind":"union","types":["09f3f8dc417f2782","4b259f7f8f0a44ad","8bca2b4f53ec7745","cebb7e9f3f663e66","fa87537f4a9c1e25"]}, "4f91f187baeabef5": {"kind":"object","properties":[{"name":"capturedFormatVersion","type":"0657d3ca5ac4e7bf","optional":true},{"name":"capturedThroughSeq","type":"3bd652ebfa8726b3","optional":false},{"name":"compacted","type":"aa93010d0e521ad7","optional":false},{"name":"inputIndex","type":"0657d3ca5ac4e7bf","optional":false},{"name":"label","type":"4cd48f3e5108bd6e","optional":false},{"name":"omittedBytes","type":"0657d3ca5ac4e7bf","optional":false},{"name":"omittedMessages","type":"0657d3ca5ac4e7bf","optional":false},{"name":"originalMessages","type":"0657d3ca5ac4e7bf","optional":false},{"name":"retainedMessages","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sessionId","type":"4cd48f3e5108bd6e","optional":false},{"name":"truncated","type":"aa93010d0e521ad7","optional":false}],"indices":[]}, "4fb59762612c3e3a": {"kind":"object","properties":[{"name":"data","type":"b1ebd5cda47d90d9","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"18fcdd273a2f2f9d","optional":false}],"indices":[]}, "50cf02d875f8a1f1": {"kind":"union","types":["2da725e554cd8930","dd2780e7e0768433"]}, @@ -935,11 +946,13 @@ "6292bcbedaa97fea": {"kind":"object","properties":[{"name":"kind","type":"f68c335306535a00","optional":false}],"indices":[]}, "62e6a429ff3f390a": {"kind":"union","types":["1d8798b1a5e8c545","1e346446e183ee76","34d1ab5c5df37818","7624ca28c61ca0ea","ad22a5ca5bbaab2e","d9196086e41e4dd5"]}, "62ecb62dc0fe1b5e": {"kind":"literal","value":"next-turn"}, + "63ba8895cc09e469": {"kind":"literal","value":"retry"}, "6487b10ba7542f3c": {"kind":"object","properties":[{"name":"agentModel","type":"4cd48f3e5108bd6e","optional":true},{"name":"agentProvider","type":"4cd48f3e5108bd6e","optional":true},{"name":"agentReasoningEffort","type":"4cd48f3e5108bd6e","optional":true},{"name":"label","type":"4cd48f3e5108bd6e","optional":false},{"name":"mode","type":"e5d3828df1ec3e2a","optional":false},{"name":"persona","type":"4cd48f3e5108bd6e","optional":true},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"toolFilter","type":"3cfc3a56502da1f8","optional":true},{"name":"version","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "6598cd6da253948d": {"kind":"object","properties":[{"name":"context","type":"50cf02d875f8a1f1","optional":false},{"name":"description","type":"4cd48f3e5108bd6e","optional":false},{"name":"error","type":"4cd48f3e5108bd6e","optional":true},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false},{"name":"phase","type":"dfce5db4aa42b7eb","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "65a8400e451e667e": {"kind":"object","properties":[{"name":"kind","type":"09655b34d7f3cf1e","optional":false}],"indices":[]}, "65d583e66518c523": {"kind":"object","properties":[{"name":"entries","type":"aba175fb20ffc711","optional":false},{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"incomplete","type":"dacf3d5f3e4ff619","optional":true},{"name":"kind","type":"6ebcc2dbec9f2dab","optional":false},{"name":"update","type":"dacf3d5f3e4ff619","optional":true}],"indices":[]}, "66ba70f6f068da44": {"kind":"object","properties":[{"name":"deferLoading","type":"dacf3d5f3e4ff619","optional":true},{"name":"description","type":"4cd48f3e5108bd6e","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false},{"name":"parameters","type":"0aa62ab51b36c0e6","optional":false}],"indices":[]}, + "671e72b8036a3afb": {"kind":"union","types":["414a44a96f06339f","e103228e063b4762"]}, "672c4aa2a9b736e3": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"1922cd79a70806c1","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "67d5f73ceae82f52": {"kind":"object","properties":[{"name":"maxTokens","type":"0657d3ca5ac4e7bf","optional":true},{"name":"model","type":"4cd48f3e5108bd6e","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"reasoningEffort","type":"4cd48f3e5108bd6e","optional":true},{"name":"stop","type":"93c33d9687613293","optional":true},{"name":"temperature","type":"0657d3ca5ac4e7bf","optional":true}],"indices":[]}, "68141cbe3994a5ff": {"kind":"literal","value":"max-tokens"}, @@ -988,6 +1001,7 @@ "7b1ce60193fa8b69": {"kind":"literal","value":"working-directory"}, "7c6129b6961cf4b2": {"kind":"object","properties":[{"name":"messageId","type":"4cd48f3e5108bd6e","optional":false},{"name":"sessionId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "7c9f44e90a0058f4": {"kind":"object","properties":[{"name":"data","type":"5302e76315127761","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"570e850224346133","optional":false}],"indices":[]}, + "7cf091467eb28887": {"kind":"union","types":["09f3f8dc417f2782","0ef3072084202b08","4b259f7f8f0a44ad","9632024d168f8169","fa87537f4a9c1e25"]}, "7d226b9fd95b4ba2": {"kind":"union","types":["1df34a6c086866ff","cffcdc19881ab61e"]}, "7d54a5a5e629e2e7": {"kind":"object","properties":[{"name":"category","type":"3305a4bb6b6c29b4","optional":true},{"name":"text","type":"4cd48f3e5108bd6e","optional":true}],"indices":[]}, "7d65acf0a3a43a8f": {"kind":"union","types":["1f0133217eeeffd8","277b61f9c6f81704","3c2535ca0a2062f1","4e44c9118152f682","83d6916021eede8f","85f9b505b7bdff1f","fb8b6a2e53cb9cf4"]}, @@ -1029,7 +1043,6 @@ "8ad45ad053b4d89f": {"kind":"object","properties":[{"name":"decision","type":"4cd48f3e5108bd6e","optional":false},{"name":"durationMs","type":"0657d3ca5ac4e7bf","optional":false},{"name":"exitCode","type":"0657d3ca5ac4e7bf","optional":true},{"name":"handlerId","type":"4cd48f3e5108bd6e","optional":false},{"name":"point","type":"4cd48f3e5108bd6e","optional":false},{"name":"stderrSummary","type":"4cd48f3e5108bd6e","optional":true},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "8af841fc9b265ade": {"kind":"literal","value":"system"}, "8bb8c3751a6ce64b": {"kind":"object","properties":[{"name":"sessionFormatVersion","type":"0657d3ca5ac4e7bf","optional":true},{"name":"sessionId","type":"4cd48f3e5108bd6e","optional":false},{"name":"throughSeq","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, - "8bca2b4f53ec7745": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"68f44f59e91c4139","optional":false},{"name":"source","type":"b112459dcb068609","optional":false,"compatibility":{"version":1,"policy":"session-source-attribution","binding":"session.user-message.source","discriminator":"kind","unknownKinds":"preserve","attributionKinds":["action-cards","tmux-context","user-question-reply","working-directory"]}}],"indices":[]}, "8c1c4f72755a78ea": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"0fe52387f6505270","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, "8c627d7a45fa406c": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "8cdd3fdd295cee53": {"kind":"literal","value":"command/done"}, @@ -1037,10 +1050,12 @@ "8e611bf46821e500": {"kind":"object","properties":[{"name":"tool","type":"fcf427290c6213c2","optional":true},{"name":"toolName","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"85f9b505b7bdff1f","optional":false}],"indices":[]}, "8e8155594e078123": {"kind":"literal","value":"workspace/changes"}, "8e8e91646a5cae3f": {"kind":"object","properties":[{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"operation","type":"26c879bdd130e8b8","optional":false},{"name":"version","type":"f88f1b7b6bb733f6","optional":false}],"indices":[]}, + "8eda0195dd1d4a3f": {"kind":"object","properties":[{"name":"maxTokens","type":"0657d3ca5ac4e7bf","optional":false},{"name":"messageSeqs","type":"5d03ba38734809bc","optional":false},{"name":"messages","type":"2f5b14bc2ccbe881","optional":false},{"name":"reasoningEffort","type":"4cd48f3e5108bd6e","optional":true},{"name":"route","type":"07e1a58c58b593f5","optional":false},{"name":"system","type":"4cd48f3e5108bd6e","optional":false},{"name":"titleProvider","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "8f324cea3916c051": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false}],"indices":[]}, "8f6d609794bf5afc": {"kind":"tuple","elements":[{"type":"4e88776858652856","optional":false,"rest":false}]}, "8fcb0b44a575e998": {"kind":"object","properties":[{"name":"kind","type":"a720757f704a2512","optional":false}],"indices":[]}, "904bf499d98be4a3": {"kind":"union","types":["7382c4fc95fe8c5d","b765ee31033cea46","f6cb462f808309e3"]}, + "92b82e8b7bbb38b3": {"kind":"object","properties":[{"name":"data","type":"d582ddae3f21edf5","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"1379f0b72275994d","optional":false}],"indices":[]}, "930c1cd289def119": {"kind":"union","types":["2142e391b9ccb60d","2c4c21f19f426a58","811229eedcec5797","fab35c4520883c0d"]}, "935c8d486292b13c": {"kind":"literal","value":"web_search"}, "938f7c7357bc403e": {"kind":"primitive","type":"null"}, @@ -1050,12 +1065,11 @@ "94c56aeb1564ad73": {"kind":"literal","value":"in_progress"}, "95080295115fbd83": {"kind":"object","properties":[{"name":"index","type":"0657d3ca5ac4e7bf","optional":false},{"name":"text","type":"4cd48f3e5108bd6e","optional":false},{"name":"type","type":"5abab3a27fd27366","optional":false}],"indices":[]}, "95a3ffccbf3c8afc": {"kind":"literal","value":"owner"}, - "95acdbd066cf445d": {"kind":"object","properties":[{"name":"data","type":"12f9a5a7506bca46","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"1379f0b72275994d","optional":false}],"indices":[]}, "95c0e88fcf0bafb2": {"kind":"literal","value":"assistant"}, + "9632024d168f8169": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"68f44f59e91c4139","optional":false},{"name":"source","type":"e3e9f4d1e245b470","optional":false,"compatibility":{"version":1,"policy":"session-source-attribution","binding":"session.user-message.source","discriminator":"kind","unknownKinds":"preserve","attributionKinds":["action-cards","tmux-context","user-question-reply","working-directory"]}}],"indices":[]}, "9645cf93769a6dfb": {"kind":"literal","value":"user-question-reply"}, "96bfb916a727cd44": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false}],"indices":[]}, "96f4a9c81fc0f940": {"kind":"union","types":["3e2ece4967cfebe9","62ecb62dc0fe1b5e"]}, - "9808c3d0f0a1be8f": {"kind":"object","properties":[{"name":"maxTokens","type":"0657d3ca5ac4e7bf","optional":false},{"name":"messageSeqs","type":"5d03ba38734809bc","optional":false},{"name":"messages","type":"d2b617d9dfcc4e0a","optional":false},{"name":"reasoningEffort","type":"4cd48f3e5108bd6e","optional":true},{"name":"route","type":"07e1a58c58b593f5","optional":false},{"name":"system","type":"4cd48f3e5108bd6e","optional":false},{"name":"titleProvider","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "9890113e97dd3541": {"kind":"object","properties":[{"name":"mode","type":"fab2bb09bc5e3b9d","optional":false},{"name":"source","type":"76632533f67879dc","optional":true}],"indices":[]}, "99135d8a39379779": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, "9947aa5938fe19a8": {"kind":"union","types":["576a2de8355eb098","a99ec0993c62c058"]}, @@ -1073,7 +1087,6 @@ "9e07693fd7cd9bab": {"kind":"literal","value":"ask"}, "9e2c06d8653585b3": {"kind":"array","element":"1cde1bedde79c5e8"}, "9e41386b3a0c9572": {"kind":"object","properties":[{"name":"end","type":"0657d3ca5ac4e7bf","optional":false},{"name":"start","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, - "9e8043098fb89db6": {"kind":"object","properties":[{"name":"data","type":"c433ef580a5b229b","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"829a2fb3be6efd4b","optional":false}],"indices":[]}, "9edc162949abf93b": {"kind":"object","properties":[{"name":"deliveryId","type":"4cd48f3e5108bd6e","optional":false},{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"db6c5246911d47a7","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"ruleId","type":"4cd48f3e5108bd6e","optional":false},{"name":"source","type":"4cd48f3e5108bd6e","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "9f65c43e340bba31": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"78a9be340090b735","optional":false}],"indices":[]}, "a0a2e5c42e1c9294": {"kind":"object","properties":[{"name":"data","type":"2e3523541e942ef6","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"0e7b2a97f54f1afd","optional":false}],"indices":[]}, @@ -1093,6 +1106,7 @@ "a40d12070f6f4a12": {"kind":"object","properties":[{"name":"task","type":"51edec100659da3a","optional":false},{"name":"teamId","type":"4cd48f3e5108bd6e","optional":false},{"name":"version","type":"a0f906856fc06f1c","optional":false}],"indices":[]}, "a6567ccb2e530606": {"kind":"object","properties":[{"name":"data","type":"0d47a8f3d847243c","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"0a8a062bc9c3c0d9","optional":false}],"indices":[]}, "a6b41f5bede8094d": {"kind":"union","types":["15495614ccfa9cbd","aff60ef947c18b01"]}, + "a6d8c25f4b2ca2d1": {"kind":"object","properties":[{"name":"data","type":"c66c6c3b411abf46","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"829a2fb3be6efd4b","optional":false}],"indices":[]}, "a6da9ad5d42ae83a": {"kind":"object","properties":[{"name":"name","type":"4cd48f3e5108bd6e","optional":false},{"name":"runId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "a720757f704a2512": {"kind":"literal","value":"tool-registry"}, "a7cf43ce7c2a4c03": {"kind":"object","properties":[{"name":"data","type":"20a57544bbf204da","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"f634871d1e18b1eb","optional":false}],"indices":[]}, @@ -1108,7 +1122,6 @@ "aa7bc2817014ca6c": {"kind":"object","properties":[{"name":"item","type":"3d7d07060a7be779","optional":false},{"name":"sessionId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "aa93010d0e521ad7": {"kind":"primitive","type":"boolean"}, "aacda84a69739238": {"kind":"literal","value":"subagent"}, - "aada2771a250136c": {"kind":"array","element":"8bca2b4f53ec7745"}, "aae343bc3d663c83": {"kind":"object","properties":[{"name":"form","type":"383cf73ae4748e5b","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, "ab7ec5f9eb2c6dfa": {"kind":"object","properties":[{"name":"kind","type":"68141cbe3994a5ff","optional":false}],"indices":[]}, "aba175fb20ffc711": {"kind":"array","element":"83079c8a3f733ac3"}, @@ -1127,7 +1140,6 @@ "b0127044ab31a702": {"kind":"object","properties":[{"name":"data","type":"ca59530d1a4f3e19","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"1986c2664009c991","optional":false}],"indices":[]}, "b062078e899958f9": {"kind":"object","properties":[{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, "b0b0eb1b16490f6b": {"kind":"object","properties":[{"name":"attachmentId","type":"4cd48f3e5108bd6e","optional":false},{"name":"bytes","type":"0657d3ca5ac4e7bf","optional":false},{"name":"height","type":"0657d3ca5ac4e7bf","optional":false},{"name":"mediaType","type":"2f1e6a7ebace43d7","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":true},{"name":"originalDimensions","type":"0f46153645c29784","optional":true},{"name":"width","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, - "b112459dcb068609": {"kind":"union","types":["0203243b72fc867a","022623b9ef98ec47","02b2c23f2d94b028","0ed8d1108fe7a5e1","0eeb17e1df89b1eb","11638dc58a31496f","1938c8c20f1cba7c","1e709346005826eb","1fdad624334d3548","2152caf1a71e588e","2235d957bf4b7c6c","224aa11d4d5f88c4","24ce31aa528272e5","260523b412c19ad8","28936c614bd4e7e2","290f32bfd81ff85a","297fbd8067d7815a","2aad1179f9576314","312aee2f0c42aabb","31907de956375182","3247e566f4340029","3b04790e08b35241","3b4f2c9ef2c1217e","3bdb8dc6989655b0","408927267422a7b2","45f005e6bbb76452","4991dfc415c9efeb","49dab019d00e8c26","4a9bef9f27a94252","4e97c3d85c0fc817","52c6113cb0e53f34","53cf1704ae0709c7","5654685d7557ec74","58015da1c1950a66","5bb69b88517f6214","5bc66269cfaf614c","5cdbf342b7748d15","5d7a64ed6aac5551","6085488d223aeffd","6292bcbedaa97fea","65a8400e451e667e","65d583e66518c523","672c4aa2a9b736e3","6ccb6bc423fb3952","6dacb1584f2bfbe2","6ea608bdc8a7a63d","6f22a9d02b9a1f11","6f44f32be9bcd9a8","719c51f67074f970","7554565be416b034","756b5e23a5cb2c7f","7df62978a2b7de85","7eaef7a763ba5b94","7f959241c24128b4","80677c8c8b804189","825ca23416b1ea9d","8338ee303e2bd448","854db3efdfb0cd39","85fe96bf15d5c755","8620189c8e335827","87b1a33564b9a4d6","88841202942c7683","8a046b52331959aa","8c1c4f72755a78ea","8c627d7a45fa406c","8f324cea3916c051","8fcb0b44a575e998","96bfb916a727cd44","99135d8a39379779","99f0e98184709ad1","9edc162949abf93b","9f65c43e340bba31","a15a19ef63cf5d1b","a16e567c8cd2c55c","a2066bd52f61218b","a3c6f1ea38e9a61d","aa2bf9e82b9fdeb9","aae343bc3d663c83","ada310bf0bdb8fed","ade057f4fdc387ae","b062078e899958f9","b2a5c8c008a31e3d","b3681c026a0b98e9","b4ec0af39fd52ed9","b613f83572fe2fbf","b6f94c17dd9bbd7c","b70f3e81fe11b7a5","b79641a9d25e7566","ba2a1a8f194b1bb2","ba4c40b4381e0dcd","baccc97ec34be6c4","c5228b7a55498d84","c55827fba7018198","cb1c8959e1726f82","cda2345a9be9d139","d85590a5675c6e2f","d9c462b9f62fdd3e","db06ef35b27a3842","dba54c09c6e7aafc","dd00cdccd0ba487b","dd5ee8762bd8a14b","dd6cc2bb2a8f2824","dde0cdf9d426768b","deaae5cd30987d4f","e7f4cbb3a57f4cbd","edbbfff1128099fd","ee577a0cc18664c9","f230c0b583e23b25","f29d63336e8266d8","f4a66f654996c093","f56c5b59ca016131","f6329fb30c7ea801","f6605d6c57933aea","f6afe41e3926f422","f7283223aacdc39b","f97f47b0eb87905e","fcf81d3c58c13593","fe1a7acdeed4d0f5"]}, "b158f3ff69237148": {"kind":"object","properties":[{"name":"contextWindow","type":"0657d3ca5ac4e7bf","optional":true},{"name":"model","type":"4cd48f3e5108bd6e","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"systemPromptUpdate","type":"f311671e07bc7fa6","optional":true}],"indices":[]}, "b1664fa89f8eab49": {"kind":"array","element":"bbc5696e3f13c758"}, "b1ebd5cda47d90d9": {"kind":"object","properties":[{"name":"member","type":"6598cd6da253948d","optional":false},{"name":"teamId","type":"4cd48f3e5108bd6e","optional":false},{"name":"version","type":"a0f906856fc06f1c","optional":false}],"indices":[]}, @@ -1180,13 +1192,14 @@ "c219efbdfd53b836": {"kind":"literal","value":"step/end"}, "c2deb7c9183735d4": {"kind":"union","types":["1ebc6f9ff3aa9e3a","2142e391b9ccb60d","b57a2889408de36e"]}, "c2f49004d9ae38a9": {"kind":"union","types":["1ebc6f9ff3aa9e3a","b57a2889408de36e","ef226a90ddac6cd8"]}, - "c433ef580a5b229b": {"kind":"object","properties":[{"name":"inserted","type":"aada2771a250136c","optional":false},{"name":"outcome","type":"d4e4d575abeb4b72","optional":true},{"name":"removedCount","type":"0657d3ca5ac4e7bf","optional":true},{"name":"start","type":"0657d3ca5ac4e7bf","optional":false},{"name":"target","type":"96f4a9c81fc0f940","optional":false}],"indices":[]}, "c43b119ce579b3e9": {"kind":"literal","value":"paused"}, "c5228b7a55498d84": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, "c55827fba7018198": {"kind":"object","properties":[{"name":"callId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"9645cf93769a6dfb","optional":false},{"name":"outcome","type":"85d6d96ff8842974","optional":false}],"indices":[]}, "c5687b1313206a6f": {"kind":"literal","value":"cordis-host-runner"}, "c589e938cc0e4989": {"kind":"literal","value":"tool/call"}, "c592ce75aab73fca": {"kind":"union","types":["0657d3ca5ac4e7bf","4cd48f3e5108bd6e","529e0ccd0e34079a","72ec79127a9c0d02","938f7c7357bc403e","aa93010d0e521ad7"]}, + "c5bbcb3435908af2": {"kind":"object","properties":[{"name":"data","type":"9632024d168f8169","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"sourceEventSeqs","type":"5d03ba38734809bc","optional":true},{"name":"surfaceOp","type":"335e242de1fcc17b","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"de25352d2dc41963","optional":false}],"indices":[]}, + "c66c6c3b411abf46": {"kind":"object","properties":[{"name":"inserted","type":"347c1718600bbb38","optional":false},{"name":"outcome","type":"d4e4d575abeb4b72","optional":true},{"name":"removedCount","type":"0657d3ca5ac4e7bf","optional":true},{"name":"start","type":"0657d3ca5ac4e7bf","optional":false},{"name":"target","type":"96f4a9c81fc0f940","optional":false}],"indices":[]}, "c748fb42c80a1200": {"kind":"array","element":"86e1e384495ee09a"}, "c8e6f48878cc2a2f": {"kind":"literal","value":"active"}, "c99e20441bf15341": {"kind":"literal","value":"feedback/record"}, @@ -1199,20 +1212,20 @@ "cda2345a9be9d139": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"24b10f7cdae64928","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "cdca80b8dad24d4e": {"kind":"literal","value":"tool-workflow/run-start"}, "cdee10e87dbed844": {"kind":"object","properties":[{"name":"delayMs","type":"0657d3ca5ac4e7bf","optional":false},{"name":"failure","type":"5f2117d1856a5ab4","optional":false},{"name":"maxRetries","type":"0657d3ca5ac4e7bf","optional":false},{"name":"mode","type":"3bc287e765d58dc3","optional":false},{"name":"policyKey","type":"4cd48f3e5108bd6e","optional":false},{"name":"provider","type":"4cd48f3e5108bd6e","optional":false},{"name":"retry","type":"0657d3ca5ac4e7bf","optional":false},{"name":"retryId","type":"4cd48f3e5108bd6e","optional":false},{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, - "cebb7e9f3f663e66": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"589d0d91239681f8","optional":false},{"name":"source","type":"b112459dcb068609","optional":false,"compatibility":{"version":1,"policy":"session-source-attribution","binding":"session.developer-message.source","discriminator":"kind","unknownKinds":"preserve","attributionKinds":["action-cards","tmux-context","user-question-reply","working-directory"]}}],"indices":[]}, "cef171adf021a186": {"kind":"object","properties":[{"name":"arguments","type":"bbc5696e3f13c758","optional":false},{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"error","type":"a81059ca06999da1","optional":true},{"name":"isError","type":"aa93010d0e521ad7","optional":false},{"name":"meta","type":"c592ce75aab73fca","optional":true},{"name":"name","type":"4cd48f3e5108bd6e","optional":false},{"name":"parentCallId","type":"4cd48f3e5108bd6e","optional":false},{"name":"rootCallId","type":"4cd48f3e5108bd6e","optional":false},{"name":"subCallId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "cffcdc19881ab61e": {"kind":"object","properties":[{"name":"cleared","type":"0b6d718c693fdf7f","optional":false},{"name":"clearedAt","type":"0657d3ca5ac4e7bf","optional":false},{"name":"kind","type":"237c004c73b6b288","optional":false},{"name":"operation","type":"e7e154d47d5fbc53","optional":false},{"name":"version","type":"f88f1b7b6bb733f6","optional":false}],"indices":[]}, "d0fbb0ca1bbba059": {"kind":"literal","value":"delete"}, "d28765f87d6b6ab4": {"kind":"literal","value":"one-shot"}, - "d2b617d9dfcc4e0a": {"kind":"array","element":"4f9125fa028a5628"}, "d4b739491689cc6b": {"kind":"object","properties":[{"name":"block","type":"8064c03c87c357fd","optional":false},{"name":"index","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"eefbea819caeeea9","optional":false}],"indices":[]}, "d4c799e3d51cae10": {"kind":"literal","value":"working-directory/change"}, "d4e4d575abeb4b72": {"kind":"literal","value":"canceled"}, + "d582ddae3f21edf5": {"kind":"object","properties":[{"name":"headerSeq","type":"0657d3ca5ac4e7bf","optional":true},{"name":"message","type":"0ef3072084202b08","optional":false},{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "d595ec73b32b016a": {"kind":"object","properties":[{"name":"data","type":"a40d12070f6f4a12","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"b57ae1b37e52bee0","optional":false}],"indices":[]}, "d63b8b8ffad9c02f": {"kind":"object","properties":[{"name":"data","type":"8bb8c3751a6ce64b","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"47e5d84f1a93450a","optional":false}],"indices":[]}, "d66e11dddf5dfa3c": {"kind":"object","properties":[{"name":"data","type":"cef171adf021a186","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"fb5e0517050d2c6d","optional":false}],"indices":[]}, "d6e654b88b584fcd": {"kind":"literal","value":"fallback"}, "d85590a5675c6e2f": {"kind":"object","properties":[{"name":"form","type":"aec150797b655c5f","optional":false},{"name":"kind","type":"1f1385dbc3e875c0","optional":false}],"indices":[]}, + "d8dde9629b7323b0": {"kind":"object","properties":[{"name":"data","type":"2a668a48dec17397","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"30093f9f63a120cb","optional":false}],"indices":[]}, "d9196086e41e4dd5": {"kind":"literal","value":"complete"}, "d9c462b9f62fdd3e": {"kind":"object","properties":[{"name":"callId","type":"4cd48f3e5108bd6e","optional":false},{"name":"kind","type":"a13d8f379222ece8","optional":false}],"indices":[]}, "d9edf1862e4471d6": {"kind":"literal","value":"schedule"}, @@ -1236,6 +1249,7 @@ "df843efd87c78cf8": {"kind":"array","element":"4f91f187baeabef5"}, "dfce5db4aa42b7eb": {"kind":"union","types":["871bfd6f7bb91061","b57a2889408de36e","c8e6f48878cc2a2f"]}, "e0a787e6ec76c7c9": {"kind":"object","properties":[{"name":"data","type":"ef11a3b7fa2033ae","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"c219efbdfd53b836","optional":false}],"indices":[]}, + "e103228e063b4762": {"kind":"object","properties":[{"name":"clientTimeZone","type":"4cd48f3e5108bd6e","optional":true},{"name":"kind","type":"63ba8895cc09e469","optional":false},{"name":"principal","type":"deebfc7c62177ea8","optional":true},{"name":"rpcId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "e2ad8f5bcd813f26": {"kind":"object","properties":[{"name":"childCreatedAt","type":"0657d3ca5ac4e7bf","optional":false},{"name":"childId","type":"4cd48f3e5108bd6e","optional":false},{"name":"label","type":"4cd48f3e5108bd6e","optional":false},{"name":"mode","type":"e5d3828df1ec3e2a","optional":false},{"name":"version","type":"f88f1b7b6bb733f6","optional":false}],"indices":[]}, "e2d0367e5716bc18": {"kind":"literal","value":"compaction/start"}, "e2f9a41e0989f54e": {"kind":"object","properties":[{"name":"data","type":"9947aa5938fe19a8","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"ef4f00923b9edbae","optional":false}],"indices":[]}, @@ -1243,6 +1257,7 @@ "e308ccf867a5398e": {"kind":"object","properties":[{"name":"data","type":"f90eb4ab2d3897bc","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"8e8155594e078123","optional":false}],"indices":[]}, "e31572180d6b28b7": {"kind":"literal","value":"provider"}, "e3e77b26f0148755": {"kind":"object","properties":[{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, + "e3e9f4d1e245b470": {"kind":"union","types":["0203243b72fc867a","022623b9ef98ec47","02b2c23f2d94b028","0ed8d1108fe7a5e1","0eeb17e1df89b1eb","11638dc58a31496f","1938c8c20f1cba7c","1e709346005826eb","1fdad624334d3548","2152caf1a71e588e","2235d957bf4b7c6c","224aa11d4d5f88c4","24ce31aa528272e5","260523b412c19ad8","28936c614bd4e7e2","290f32bfd81ff85a","297fbd8067d7815a","2aad1179f9576314","312aee2f0c42aabb","31907de956375182","3247e566f4340029","3b04790e08b35241","3b4f2c9ef2c1217e","3bdb8dc6989655b0","408927267422a7b2","414a44a96f06339f","45f005e6bbb76452","4991dfc415c9efeb","49dab019d00e8c26","4a9bef9f27a94252","4e97c3d85c0fc817","52c6113cb0e53f34","53cf1704ae0709c7","5654685d7557ec74","58015da1c1950a66","5bb69b88517f6214","5bc66269cfaf614c","5cdbf342b7748d15","5d7a64ed6aac5551","6085488d223aeffd","6292bcbedaa97fea","65a8400e451e667e","65d583e66518c523","672c4aa2a9b736e3","6ccb6bc423fb3952","6dacb1584f2bfbe2","6ea608bdc8a7a63d","6f22a9d02b9a1f11","6f44f32be9bcd9a8","719c51f67074f970","7554565be416b034","756b5e23a5cb2c7f","7df62978a2b7de85","7eaef7a763ba5b94","7f959241c24128b4","80677c8c8b804189","825ca23416b1ea9d","8338ee303e2bd448","854db3efdfb0cd39","85fe96bf15d5c755","8620189c8e335827","87b1a33564b9a4d6","88841202942c7683","8a046b52331959aa","8c1c4f72755a78ea","8c627d7a45fa406c","8f324cea3916c051","8fcb0b44a575e998","96bfb916a727cd44","99135d8a39379779","99f0e98184709ad1","9edc162949abf93b","9f65c43e340bba31","a15a19ef63cf5d1b","a16e567c8cd2c55c","a2066bd52f61218b","a3c6f1ea38e9a61d","aa2bf9e82b9fdeb9","aae343bc3d663c83","ada310bf0bdb8fed","ade057f4fdc387ae","b062078e899958f9","b2a5c8c008a31e3d","b3681c026a0b98e9","b4ec0af39fd52ed9","b613f83572fe2fbf","b6f94c17dd9bbd7c","b70f3e81fe11b7a5","b79641a9d25e7566","ba2a1a8f194b1bb2","ba4c40b4381e0dcd","baccc97ec34be6c4","c5228b7a55498d84","c55827fba7018198","cb1c8959e1726f82","cda2345a9be9d139","d85590a5675c6e2f","d9c462b9f62fdd3e","db06ef35b27a3842","dba54c09c6e7aafc","dd00cdccd0ba487b","dd5ee8762bd8a14b","dd6cc2bb2a8f2824","dde0cdf9d426768b","deaae5cd30987d4f","e103228e063b4762","e7f4cbb3a57f4cbd","edbbfff1128099fd","ee577a0cc18664c9","f230c0b583e23b25","f29d63336e8266d8","f4a66f654996c093","f56c5b59ca016131","f6329fb30c7ea801","f6605d6c57933aea","f6afe41e3926f422","f7283223aacdc39b","f97f47b0eb87905e","fcf81d3c58c13593","fe1a7acdeed4d0f5"]}, "e41bdebe246abe17": {"kind":"literal","value":"session"}, "e42b0f40b5d0b628": {"kind":"literal","value":"every"}, "e438a9d4fb883b3e": {"kind":"literal","value":"append"}, @@ -1262,7 +1277,6 @@ "e95f957154b2b1b5": {"kind":"literal","value":"assistant/attempt"}, "ea08ec1df0c33108": {"kind":"object","properties":[{"name":"message","type":"2eafefdc6f79a492","optional":false},{"name":"teamId","type":"4cd48f3e5108bd6e","optional":false},{"name":"version","type":"a0f906856fc06f1c","optional":false}],"indices":[]}, "eaa71be0fbf0f702": {"kind":"object","properties":[{"name":"kind","type":"ad3b56aa2fc1ad42","optional":false}],"indices":[]}, - "eaa9125aec0fb284": {"kind":"object","properties":[{"name":"data","type":"9808c3d0f0a1be8f","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"587bb11da730e06a","optional":false}],"indices":[]}, "ebb768d85ec87fb6": {"kind":"literal","value":"image/webp"}, "ec38b5949af8eaca": {"kind":"object","properties":[{"name":"data","type":"2fe5026daf14bd4f","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"2aed07493b1c65b6","optional":false}],"indices":[]}, "ec8b5ff7c77ca689": {"kind":"object","properties":[{"name":"kind","type":"b2245a906e13aa94","optional":false}],"indices":[]}, @@ -1303,6 +1317,7 @@ "fa87537f4a9c1e25": {"kind":"object","properties":[{"name":"content","type":"b52f215a6cd69105","optional":false},{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"role","type":"95c0e88fcf0bafb2","optional":false},{"name":"source","type":"719c51f67074f970","optional":false}],"indices":[]}, "fab2bb09bc5e3b9d": {"kind":"union","types":["12a0cfa619ac4a5f","18fce9540bb20897","a3edd1efd1fef21b"]}, "fab35c4520883c0d": {"kind":"literal","value":"rejected"}, + "fb28ad7d7aabceb5": {"kind":"object","properties":[{"name":"data","type":"8eda0195dd1d4a3f","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"587bb11da730e06a","optional":false}],"indices":[]}, "fb5e0517050d2c6d": {"kind":"literal","value":"tool/ptc-dispatch"}, "fb8b6a2e53cb9cf4": {"kind":"literal","value":"image"}, "fbd2c74b346a1595": {"kind":"literal","value":"agent-instructions"}, diff --git a/docs/subsystems/core.md b/docs/subsystems/core.md index d11e03488a..d709735217 100644 --- a/docs/subsystems/core.md +++ b/docs/subsystems/core.md @@ -104,7 +104,8 @@ interface Agent { /** * Route identified input to an inbox boundary and optionally wake the driver. - * Empty ordinary input drives work without becoming a model-visible message. + * A retry-source input must be empty; admission records `agent/retry` + * without adding a model-visible message. * Waking input submitted after active cancellation is queued for the next * turn and runs when the aborted activity converges to idle; a `disposed` * cancel leaves it parked. A wake submitted while already idle always opens @@ -117,9 +118,8 @@ interface Agent { send(message: UserMessage, target: InboxTarget, wakeup: boolean): void /** - * Queue an ordinary follow-up turn and wake the driver. The item becomes the - * sole ordinary message of its own turn. An empty ordinary input starts a - * turn from committed history without appending a model-visible user message. + * Queue a follow-up turn and wake the driver. The item is its sole ordinary + * prompt, or an empty retry-source control with no model-visible message. * @param message - identified prompt content and the source that supplied it. */ followup(message: UserMessage): void diff --git a/docs/subsystems/core.zh.md b/docs/subsystems/core.zh.md index 7e4c682f69..11fa2a6ffe 100644 --- a/docs/subsystems/core.zh.md +++ b/docs/subsystems/core.zh.md @@ -108,7 +108,8 @@ interface Agent { /** * Route identified input to an inbox boundary and optionally wake the driver. - * Empty ordinary input drives work without becoming a model-visible message. + * A retry-source input must be empty; admission records `agent/retry` + * without adding a model-visible message. * Waking input submitted after active cancellation is queued for the next * turn and runs when the aborted activity converges to idle; a `disposed` * cancel leaves it parked. A wake submitted while already idle always opens @@ -121,9 +122,8 @@ interface Agent { send(message: UserMessage, target: InboxTarget, wakeup: boolean): void /** - * Queue an ordinary follow-up turn and wake the driver. The item becomes the - * sole ordinary message of its own turn. An empty ordinary input starts a - * turn from committed history without appending a model-visible user message. + * Queue a follow-up turn and wake the driver. The item is its sole ordinary + * prompt, or an empty retry-source control with no model-visible message. * @param message - identified prompt content and the source that supplied it. */ followup(message: UserMessage): void diff --git a/docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.i18n.yaml index cbda06d0c8..9573c050b1 100644 --- a/docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.i18n.yaml +++ b/docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.i18n.yaml @@ -3,14 +3,14 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.md /: - en: 7a194a68f49ac09a - zh: 4d7bdff82c571c62 -/observe-empty-prompt-admission-through-the-inbox: - en: 450277479989809e - zh: 4b01f252faeae19c -/observe-empty-prompt-admission-through-the-inbox/change: - en: e1c081d5321f3256 - zh: a5dd5585fe4300b3 -/observe-empty-prompt-admission-through-the-inbox/migration: - en: 36e80226df9dcf54 - zh: 81a9c0bd5c667470 + en: f5e2d8120bcf97bd + zh: 64b692f69ec7b0d4 +/empty-prompts-and-retry-controls: + en: e26b58650aa8e914 + zh: c0e5a824ec3bea40 +/empty-prompts-and-retry-controls/change: + en: bf69985ccd8cc89a + zh: b1046f8c98cbdb43 +/empty-prompts-and-retry-controls/migration: + en: 028f7fd6db73a36b + zh: b72f840db5301d6c diff --git a/docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.md b/docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.md index 16876dd26c..c74f3c2047 100644 --- a/docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.md +++ b/docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.md @@ -1,19 +1,20 @@ --- kind: upgrade-guide -description: "Empty ordinary Agent inputs start requests without producing user/message events." +description: Empty API and SDK prompts become retry controls whose required Session event needs an updated reader. --- -# Observe empty prompt admission through the inbox + +# Empty prompts and retry controls English | [中文](guide.zh.md) ## Change -An ordinary Agent input with `content: []` starts a new turn from committed history without appending an empty `user/message`. The identified input remains in durable inbox events. Custom consumers that wait for every accepted input to produce a user message must observe its inbox receipt instead. Session logs need no migration; existing recorded empty user messages keep their historical interpretation. +The Session prompt API accepts `content: []` to continue eligible failed, idle sessions in queue mode. SDK `session/prompt` maps empty `contentBlocks` to the same retry-source control. Acceptance records `agent/retry` with `turn`, `step`, `inputId`, and `source`, instead of a `user/message`. A new turn still starts and closes normally. Direct Agent callers using ordinary empty user messages retain their existing behavior. -The Web Session prompt endpoint accepts an empty array only for an eligible failed, idle ordinary Session in queue mode. Whitespace-only text parts remain invalid. +The new required event and retry source keep the current Session format version. Updated readers accept older logs; older readers reject logs containing the unknown required event. See the [persistence acknowledgement](../../../persistence-changes/2026-10-27-retry-control-input.md). ## Migration -1. Keep ordinary text and attachment inputs unchanged. Use an empty array only when requesting another model call without new user text. -2. Match acceptance by the input identity in `agent/inbox/spliced`, then observe the normal turn/status lifecycle. Do not wait for an empty `user/message`. -3. Verify that continuation adds a balanced turn without adding a user message. The shipped TypeScript and Python SDK clients already observe inbox receipts. +1. Upgrade Session readers and SDK runtimes together before opening logs containing retry controls. +2. Consumers correlating empty API or SDK prompts with `user/message` must use the durable inbox identity and `agent/retry.inputId`. Handle the retry source without treating it as model-visible user content. +3. Verify that a continued failure produces a balanced new turn and `agent/retry`, while derived model history contains no additional user message. diff --git a/docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.zh.md index 9c879cc2aa..66f436fae9 100644 --- a/docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.zh.md +++ b/docs/upgrade-guide/v0.2.1-rc.3/empty-prompt-continuation/guide.zh.md @@ -1,19 +1,20 @@ --- kind: upgrade-guide -description: "空的普通 Agent 输入发起请求,但不产生 user/message 事件。" +description: 空 API 和 SDK 提示词变为重试控制输入,其必读 Session 事件需要更新后的读取方。 --- -# 通过 inbox 观察空 prompt 准入 + +# 空提示词与重试控制输入 [English](guide.md) | 中文 ## 变更 -普通 Agent 输入的 `content: []` 基于已提交历史启动新轮次,不再追加空的 `user/message`。带身份的输入仍保留在持久 inbox 事件中。若自定义消费者等待每条已接受输入产生用户消息,应改为观察对应 inbox 回执。Session 日志无需迁移,历史空用户消息的解释保持不变。 +Session prompt API 接受 `content: []`,在 queue 模式下继续符合条件且空闲的失败会话。SDK `session/prompt` 将空 `contentBlocks` 映射到同样的 retry 来源控制输入。接受后记录带 `turn`、`step`、`inputId` 和 `source` 的 `agent/retry`,而非 `user/message`。新轮次仍正常开启和闭合。直接使用普通空用户消息的 Agent 调用方保留原有行为。 -Web Session prompt 端点仅在普通 Session 空闲、最新失败符合继续条件且使用 queue 模式时接受空数组。仅含空白的文本块仍无效。 +新增必读事件及 retry 来源保留当前 Session 格式版本。更新后的读取方接受旧日志;旧读取方会拒绝包含未知必读事件的日志。见[持久化确认记录](../../../persistence-changes/2026-10-27-retry-control-input.zh.md)。 ## 迁移 -1. 普通文本和附件输入保持不变。仅在需要再次请求模型而不添加用户文本时使用空数组。 -2. 通过 `agent/inbox/spliced` 中的输入身份匹配准入,然后观察正常轮次与状态生命周期。不要等待空的 `user/message`。 -3. 验证继续操作增加正常闭合的轮次,但不增加用户消息。内置 TypeScript 和 Python SDK 客户端已观察 inbox 回执。 +1. 打开包含重试控制输入的日志前,同时升级 Session 读取方与 SDK 运行时。 +2. 通过 `user/message` 关联空 API 或 SDK 提示词的消费方,改用持久 inbox 身份与 `agent/retry.inputId`。处理 retry 来源时,不应将其视为模型可见用户内容。 +3. 确认继续失败请求会产生正常开启并闭合的新轮次和 `agent/retry`,而派生模型历史中没有额外用户消息。 diff --git a/packages/api/session-controller/README.i18n.yaml b/packages/api/session-controller/README.i18n.yaml index a1daccc8f0..99568c2d0c 100644 --- a/packages/api/session-controller/README.i18n.yaml +++ b/packages/api/session-controller/README.i18n.yaml @@ -24,8 +24,8 @@ en: 954923654eebe2dc zh: 6bd9a7d2ea965aed /session-controller/configuration: - en: 94cb7b73b385b320 - zh: 1f3bc1e6552a01a3 + en: c1b6d81775d0b56e + zh: 67c3ef16ff39c6c3 /session-controller/model-experience: en: 30b917bf822bb4df zh: b0b023b5b526c317 diff --git a/packages/api/session-controller/README.md b/packages/api/session-controller/README.md index 371ee3c13e..77b6f9eb1b 100644 --- a/packages/api/session-controller/README.md +++ b/packages/api/session-controller/README.md @@ -110,7 +110,7 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a Blockquotes travel as ordinary prompt text without quote metadata. Local submission echoes and durable messages use the same text rendering rules; queue edits replace content while retaining prompt correlation. -An empty `prompt.content` array requests a new turn only for an eligible failed, idle ordinary Session in queue mode. Pending human input prevents admission; non-waking injected context is retained. `conversationContinuation` supplies the shared six-code manual policy, independently of provider retry configuration. The input is logged through the inbox for request-id deduplication, but creates no model-visible user message. Whitespace-only text parts remain invalid. +An empty `prompt.content` array requests a new turn only for an eligible failed, idle ordinary Session in queue mode. Pending human input prevents admission; non-waking injected context is retained. `conversationContinuation` supplies the shared six-code manual policy, independently of provider retry configuration. The input uses `source.kind: 'retry'` and ordinary inbox admission; Loop acceptance records `agent/retry` with the input identity and caller metadata for deduplication and audit, without adding a model-visible message. Whitespace-only text parts remain invalid. ## Model Experience diff --git a/packages/api/session-controller/README.zh.md b/packages/api/session-controller/README.zh.md index 9e698a26e7..53387989ad 100644 --- a/packages/api/session-controller/README.zh.md +++ b/packages/api/session-controller/README.zh.md @@ -110,7 +110,7 @@ GUI 模型选择要求确切提供方/模型对出现在可用目录中;不 Blockquote 作为普通 prompt 文本传递,不携带 quote 元数据。本地提交回显与持久消息使用同一文本渲染规则;队列编辑替换 content,同时保留 prompt 关联信息。 -空的 `prompt.content` 数组仅在普通 Session 空闲、最新失败符合继续条件且使用 queue 模式时请求新轮次。待消费的用户输入阻止准入,非唤醒的注入上下文保留。`conversationContinuation` 提供共用的六码手动策略,独立于提供方重试配置。输入通过 inbox 持久记录,用于 request-id 去重,但不生成模型可见的用户消息。仅含空白的文本块仍无效。 +空的 `prompt.content` 数组仅在普通 Session 空闲、最新失败符合继续条件且使用 queue 模式时请求新轮次。待消费的用户输入阻止准入,非唤醒的注入上下文保留。`conversationContinuation` 提供共用的六码手动策略,独立于提供方重试配置。输入使用 `source.kind: 'retry'` 并沿用普通 inbox 准入;Loop 接受后记录带输入身份和调用方元数据的 `agent/retry`,供去重和审计使用,不增加模型可见消息。仅含空白的文本块仍无效。 ## 模型体验 diff --git a/packages/api/session-controller/src/commands.ts b/packages/api/session-controller/src/commands.ts index 88d7486cbe..15b110c8ac 100644 --- a/packages/api/session-controller/src/commands.ts +++ b/packages/api/session-controller/src/commands.ts @@ -568,13 +568,14 @@ function resolvePromptFileReceipts( function hasPromptRequest(agent: Agent, requestId: SessionRequestId): boolean { const matches = (message: UserMessage): boolean => { const source = message.source - return source.kind === 'user' && 'rpcId' in source && source.rpcId === requestId + return (source.kind === 'user' || source.kind === 'retry') && 'rpcId' in source && source.rpcId === requestId } if (agent.inbox.nextTurn.some(matches) || agent.inbox.nextStep.some(matches)) return true // oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred. return agent.session.snapshotEvents().some((event) => { - if (event.type === 'agent/inbox/spliced') { - return event.data.inserted.some(message => message.content.length === 0 && matches(message)) + if (event.type === 'agent/retry') { + const source = event.data.source + return 'rpcId' in source && source.rpcId === requestId } if (event.type !== 'user/message') return false const source = event.data.source diff --git a/packages/api/session-controller/src/continuation.ts b/packages/api/session-controller/src/continuation.ts index 5d8e97553e..0e8247688a 100644 --- a/packages/api/session-controller/src/continuation.ts +++ b/packages/api/session-controller/src/continuation.ts @@ -15,30 +15,25 @@ const stateSchema = z.object({ admitted: z.boolean(), failedAttempt: z.boolean(), ending: endingSchema, - continuing: z.boolean(), }) -/** Recoverable manual-continuation facts, including an accepted empty input awaiting its turn. */ +/** Recoverable manual-continuation facts over admitted user input and retry controls. */ export type ContinuationState = z.infer -const EMPTY: ContinuationState = { admitted: false, failedAttempt: false, ending: null, continuing: false } +const EMPTY: ContinuationState = { admitted: false, failedAttempt: false, ending: null } /** Eligibility requires admitted human work and a failed settlement in the last request step. */ export const continuationProjectionDefinition = { key: 'conversationContinuation', - stateVersion: 2, + stateVersion: 3, stateSchema, init: (): ContinuationState => EMPTY, apply: (state, event) => { switch (event.type) { - case 'agent/inbox/spliced': - if (state.continuing && event.data.target === 'next-turn' && event.data.outcome === 'canceled' - && event.data.start === 0) return { ...state, continuing: false } - return state.ending !== null && !state.continuing && event.data.target === 'next-turn' - && event.data.inserted.some(message => message.source.kind === 'user' && message.content.length === 0) - ? { ...state, continuing: true } : state case 'turn/start': - return state.continuing ? { ...EMPTY, admitted: true } : EMPTY + return EMPTY + case 'agent/retry': + return state.admitted ? state : { ...state, admitted: true } case 'step/start': case 'assistant/message': return state.failedAttempt ? { ...state, failedAttempt: false } : state diff --git a/packages/api/session-controller/src/index.ts b/packages/api/session-controller/src/index.ts index b5599f3a22..a10dcf5578 100644 --- a/packages/api/session-controller/src/index.ts +++ b/packages/api/session-controller/src/index.ts @@ -483,7 +483,7 @@ export class SessionController extends TypertRemoteService { // The caller is recorded only when it is not the owner, so owner messages keep their exact source. const principal = principalOf(this.ctx.invocation) const source: MessageSource = { - kind: 'user', + kind: request.content.length === 0 ? 'retry' : 'user', rpcId: request.requestId, ...(clientTimeZone === undefined ? {} : { clientTimeZone }), ...(principal === undefined ? {} : { principal }), diff --git a/packages/api/session-controller/src/types.ts b/packages/api/session-controller/src/types.ts index 798ca0e04c..91818f9748 100644 --- a/packages/api/session-controller/src/types.ts +++ b/packages/api/session-controller/src/types.ts @@ -395,6 +395,8 @@ declare module '@deepseek-ai/dsh-llm' { * caller other than the owner, so an absent `principal` means the owner. */ 'user-rpc': { kind: 'user'; rpcId: SessionRequestId; clientTimeZone?: string; principal?: Principal } + /** Browser retry correlation with the same caller metadata as an ordinary prompt. */ + 'retry-rpc': { kind: 'retry'; rpcId: SessionRequestId; clientTimeZone?: string; principal?: Principal } } } diff --git a/packages/api/session-controller/tests/continuation.host.spec.ts b/packages/api/session-controller/tests/continuation.host.spec.ts index f87b29809f..f2ae6ecf35 100644 --- a/packages/api/session-controller/tests/continuation.host.spec.ts +++ b/packages/api/session-controller/tests/continuation.host.spec.ts @@ -111,7 +111,7 @@ it('publishes a complete-log result independently of the visible page and releas expect(ctx.sessionProjections.snapshot(session).values.conversationContinuation).toBe(ending) expect(changed.mock.calls.map(([, key, value]) => [key, value])).toEqual([['conversationContinuation', null], ['conversationContinuation', ending]]) expect(ctx.sessionProjections.checkpoint(session).conversationContinuation) - .toEqual({ ver: 2, seq: ending, val: { admitted: false, failedAttempt: false, ending, continuing: false } }) + .toEqual({ ver: 3, seq: ending, val: { admitted: false, failedAttempt: false, ending } }) session.append('turn/start', { turn: 2 }) expect(ctx.sessionProjections.snapshot(session).values.conversationContinuation).toBeNull() expect(changed).toHaveBeenCalledTimes(3) @@ -127,42 +127,28 @@ it('restores valid, absent and outdated checkpoints through the registry', async await ctx.sessionProjections.register(definition) const session = ctx.sessions.create(SessionId('restored')) const log = events(failed('SERVER')) - const checkpoint = { conversationContinuation: { ver: 2, seq: SessionSeq(2), val: fold(log.slice(0, 3)) } } + const checkpoint = { conversationContinuation: { ver: 3, seq: SessionSeq(2), val: fold(log.slice(0, 3)) } } const restore = (rows: typeof checkpoint | Record, tail: readonly SessionEvent[], base: number) => ctx.sessionProjections.restore(rows, tail, SessionLogOffset(base), session.header, session.inheritedEventCount) expect(restore(checkpoint, log.slice(3), 3).snapshot.values.conversationContinuation).toBe(5) expect(restore({}, log, 0).snapshot.values.conversationContinuation).toBe(5) - const outdated = { conversationContinuation: { ...checkpoint.conversationContinuation, ver: 0 } } + const outdated = { conversationContinuation: { ...checkpoint.conversationContinuation, ver: 2 } } expect(restore(outdated, log, 0).snapshot.values.conversationContinuation).toBe(5) expect(() => restore(outdated, log.slice(3), 3)).toThrow('re-read from seq 0') expect(() => restore({}, log.slice(3), 3)).toThrow('re-read from seq 0') }) -it('carries admitted work through an empty continuation and retires a canceled queued control', async () => { - const ctx = new Context() - onTestFinished(() => ctx.fiber.dispose()) - await ctx.plugin(SessionStore) - await ctx.plugin(SessionProjectionRegistry) - const handle = await ctx.sessionProjections.register(definition) - const session = ctx.sessions.create(SessionId('empty-repeat')) - for (const event of events(failed('SERVER'))) { - if (event.type === 'user/message') session.append(event.type, event.data, { surfaceOp: 'append' }) - else if (event.type === 'turn/start') session.append(event.type, event.data) - else if (event.type === 'step/start' || event.type === 'step/end') session.append(event.type, event.data) - else if (event.type === 'assistant/attempt') session.append(event.type, event.data) - else if (event.type === 'turn/end') session.append(event.type, event.data) +it('admits retry controls independently of empty ordinary user messages', () => { + const base = events(failed('SERVER')) + const input = base.find(event => event.type === 'user/message')! + const empty: SessionEvent<'user/message'> = { ...input, type: 'user/message', data: createUserMessage({ + content: [], source: { kind: 'user' }, + }) } + expect(fold(base.map(event => event === input ? empty : event)).ending).toBeNull() + const retry: SessionEvent<'agent/retry'> = { type: 'agent/retry', seq: empty.seq, time: empty.time, + data: { turn: 1, step: 1, inputId: empty.data.id, source: { kind: 'retry' } }, } - const input = createUserMessage({ content: [], source: { kind: 'user' } }) - session.append('agent/inbox/spliced', { target: 'next-turn', start: 0, inserted: [input] }) - expect(handle.stateOf(session).continuing).toBe(true) - session.append('agent/inbox/spliced', { target: 'next-turn', start: 0, removedCount: 1, inserted: [], outcome: 'canceled' }) - expect(handle.stateOf(session).continuing).toBe(false) - session.append('agent/inbox/spliced', { target: 'next-turn', start: 0, inserted: [input] }) - session.append('turn/start', { turn: 2 }) - expect(handle.stateOf(session).admitted).toBe(true) - session.append('step/start', { turn: 2, step: 1 }) - session.append('assistant/attempt', { turn: 2, step: 1, stream: [] }) - session.append('step/end', { turn: 2, step: 1 }) - const ending = session.append('turn/end', { turn: 2, reason: failed('SERVER') }).seq - expect(handle.stateOf(session).ending).toBe(ending) + expect(fold(base.map(event => event === input ? retry : event)).ending).toBe(5) + const admitted = definition.apply(definition.init(), retry) + expect(definition.apply(admitted, retry)).toBe(admitted) }) diff --git a/packages/api/session-controller/tests/empty-prompt.host.spec.ts b/packages/api/session-controller/tests/empty-prompt.host.spec.ts index 5f8d913f40..835d7ae442 100644 --- a/packages/api/session-controller/tests/empty-prompt.host.spec.ts +++ b/packages/api/session-controller/tests/empty-prompt.host.spec.ts @@ -25,7 +25,7 @@ async function setup() { return { ctx, remote, agent, adapter, request } } -it('admits repeated empty prompts after failures without extra user messages or duplicate requests', async () => { +it('records retry controls without user messages or duplicate requests', async () => { const { remote, agent, adapter, request } = await setup() agent.followup(createUserMessage({ content: [{ type: 'text', text: 'original task' }], source: { kind: 'user' } })) await agent.whenIdle() @@ -40,7 +40,20 @@ it('admits repeated empty prompts after failures without extra user messages or await agent.whenIdle() expect(adapter.requests).toHaveLength(3) const events = agent.session.snapshotEvents() - expect(events.filter(event => event.type === 'user/message' && event.data.source.kind === 'user')).toHaveLength(1) + const messages = events.flatMap(event => event.type === 'user/message' && event.data.source.kind === 'user' ? [event.data] : []) + expect(messages).toHaveLength(1) + const retries = events.flatMap(event => event.type === 'agent/retry' ? [event.data] : []) + expect(retries).toEqual([ + { turn: 2, step: 1, inputId: expect.any(String), source: { kind: 'retry', rpcId: 'continue-1' } }, + { turn: 3, step: 1, inputId: expect.any(String), source: { kind: 'retry', rpcId: 'continue-2' } }, + ]) + const controls = events.flatMap(event => event.type === 'agent/inbox/spliced' ? event.data.inserted : []) + .filter(message => message.source.kind === 'retry') + expect(retries.map(retry => retry.inputId)).toEqual(controls.map(control => control.id)) + for (const call of adapter.requests) { + expect(call.messages.filter(message => message.role === 'user').map(message => message.content)) + .toEqual([[{ type: 'text', text: 'original task' }]]) + } expect(events.flatMap(event => event.type === 'turn/end' ? [event.data.reason.kind] : [])).toEqual(['error', 'error', 'completed']) expect(events.filter(event => event.type === 'turn/start')).toHaveLength(3) expect((await remote.prompt(request('after-completion'))).ok).toBe(false) diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index 8507f1dd99..5ec07de08b 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/README.i18n.yaml @@ -33,14 +33,14 @@ en: 6a7ae99570c9c007 zh: 26e86063e97d82c7 /deepseek-ai-dsh-client-ui-conversation/model-experience/manual-continuation-request/what-the-model-sees: - en: cfd91cdd0132732b - zh: 298d1ce1b973e7b0 + en: 5c81348e39a58220 + zh: d61ecf2b51b2d583 /deepseek-ai-dsh-client-ui-conversation/model-experience/manual-continuation-request/token-effect: - en: a5bbf55d50568fa2 - zh: 6c43af211055db93 + en: 777a5c51789aba0c + zh: 9a710c83d1765fba /deepseek-ai-dsh-client-ui-conversation/model-experience/manual-continuation-request/kv-cache-effect: - en: bbb4b8186c85f62d - zh: 222397b869a60df2 + en: 4889ad2beba4cc52 + zh: 778e8d7803481cd3 /deepseek-ai-dsh-client-ui-conversation/known-limitations-and-deferred-work: en: d76daa90b6d6f600 zh: 37a9f7d402b2552b diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index 8955b71b31..9e06885423 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -193,15 +193,15 @@ The selector must be a pure function of the owner currency. Its non-null return #### What the model sees -The play action starts a new request from committed Session history. Its `content: []` input is a durable inbox command, not a model-visible user message; it adds no localized or hidden continuation instruction. Pending injected context follows ordinary admission. +The play action submits `content: []` through the existing Session prompt API as a retry control. Loop acceptance records `agent/retry` without adding a `user/message` or a chat bubble. Pending injected context follows ordinary admission; the action adds no localized or hidden continuation text. #### Token effect -The action adds no continuation text or empty user-message overhead. The request still incurs the ordinary cost of retained history, current prompt sections, tool declarations, and any newly admitted context. +The control adds no model tokens. The request still incurs the ordinary cost of retained history, current prompt sections, tool declarations, and any newly admitted context. #### KV Cache effect -The control input does not alter model history. Prefix reuse still depends on the current model, prompt, tools, injected context, and provider cache policy. +The control leaves model history unchanged. Prefix reuse depends on the current model, prompt, tools, injected context, and provider cache policy. ## Known Limitations and Deferred Work diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index 65c9feda93..152417372c 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -193,15 +193,15 @@ selector 必须是 owner currency 的纯函数。非 null 返回值作为 `match #### 模型看到什么 -播放按钮基于已提交的 Session 历史发起新请求。`content: []` 作为持久 inbox 控制记录,不成为模型可见的用户消息;不会添加本地化或隐藏的继续指令。待注入上下文遵循普通准入流程。 +播放按钮通过现有 Session prompt API 将 `content: []` 提交为重试控制输入。Loop 接受后记录 `agent/retry`,不增加 `user/message` 或聊天气泡。待注入上下文遵循普通准入流程;该操作不添加本地化或隐藏的继续文本。 #### Token 影响 -该操作不增加继续文本或空用户消息的封装开销。请求仍承担保留历史、当前提示词、工具声明以及新准入上下文的正常输入成本。 +控制输入不增加模型 token。请求仍承担保留历史、当前提示词、工具声明以及新准入上下文的正常输入成本。 #### KV Cache 影响 -控制输入不改变模型历史。前缀复用仍取决于当前模型、提示词、工具、注入上下文及提供方缓存策略。 +控制输入不改变模型历史。前缀复用取决于当前模型、提示词、工具、注入上下文及提供方缓存策略。 ## 已知限制与暂缓事项 diff --git a/packages/core/agent-loop/README.i18n.yaml b/packages/core/agent-loop/README.i18n.yaml index 5b88c245a3..2d3b453c44 100644 --- a/packages/core/agent-loop/README.i18n.yaml +++ b/packages/core/agent-loop/README.i18n.yaml @@ -45,14 +45,14 @@ en: 3b48a480036bd5df zh: 28cea2036b0dccee /deepseek-ai-dsh-agent-loop/understand-the-implementation/turn-and-step-flow: - en: ad92c751515520ff - zh: ca6b24db5ab4e471 + en: c9f75ebd07873e6a + zh: 863bc96addeb8eb9 /deepseek-ai-dsh-agent-loop/understand-the-implementation/failure-and-cancellation: en: eea56903c4ff82f5 zh: c0d5abb5767780ed /deepseek-ai-dsh-agent-loop/further-exploration: - en: bd0b5d56ebef7e23 - zh: 52911b993bc41afb + en: 7ac519f8e635acbc + zh: 975e514915dc6c9f /deepseek-ai-dsh-agent-loop/model-experience: en: 215e7ba838619b7b zh: a311da3843709f90 diff --git a/packages/core/agent-loop/README.md b/packages/core/agent-loop/README.md index b18484ad82..021401061a 100644 --- a/packages/core/agent-loop/README.md +++ b/packages/core/agent-loop/README.md @@ -117,6 +117,8 @@ The loop is the production acquisition point for session write handles. When `ct ### Turn and step flow +An empty input with `source.kind: 'retry'` is a control: on first-attempt admission, the Loop records `agent/retry` with its inbox identity, turn, step, and source instead of a model-visible `user/message`. A retry control containing content fails with `INVALID_REQUEST`. Queue ownership, pre-step decisions, and turn settlement remain the same as for ordinary input; ordinary empty user messages are still committed. + The driver owns one agent for its lifetime and runs inside `ctx.agents.withInitiator(agent, ...)`. `AgentLoop` registers the standard `inbox` projection for its service lifetime, so cold reads work before any Agent exists and after all Agents unload. It also owns `modelSelection` and constructs an `AgentModel` for each Agent, with the public [Model API](../agent/README.md#model-selection). Its package-internal `ReactLoopInbox` uses that shared projection for structural commands and loop-only claims. At a turn boundary it opens the durable turn, then atomically claims pending next-step input plus one queued prompt; between steps it claims only next-step input. The driver assembles the prompt and tools and runs `agent/pre-step`; the fallback projects runtime context after listeners run. A rejected decision or empty first batch opens no step. On the first attempt after acceptance, `step/start` precedes the `agent/request` waterfall and `prepareCall()`; neither async phase sees the pending system prompt or accepted users committed to history, and cancellation during either commits neither. For every attempt, the loop then synchronously reconciles the rendered prompt against the surviving `system/message` nodes using the prepared call capability, refreshes registered runtime facts without reassembling the bound prompt or tools, appends the accepted `user/message` batch only on the first attempt with its owned context candidate replaced in place, and restores removed runtime context, logs the header and context as needed, and derives and freezes the request before streaming through that bound prepared call. Retries reuse the same rendered assembly without repeating assembly, `agent/pre-step`, or user admission. Reconciliation sees pre-step and retry compaction; a broken series consolidates the prompt at the head rather than appending an update after already-committed users. The request is `header.config`, `deriveMessages()`, and `header.tools`; it carries no `system` field. Each model attempt emits one process-local `start`, emits every `chunk` only after the matching durable assistant-frame settlement, and emits exactly one terminal `end`; final assembly or message-append failure settles it as `aborted`, while `committed` follows the durable `assistant/message`. Each successful model call appends one message anchor, and a cancelled stream appends an `interrupted: true` anchor with the delivered prefix so the next request contains what the user saw. Within a step, exclusive calls form barriers and parallel-safe calls use the bounded rolling pool; policy, durable results... (line truncated to 2000 chars) Prompt admission uses the actual `prepareCall()` result, not the preceding `request/context`. With no system node, even an empty prompt is appended (reserving node 0 without a wire message). On an incapable route, or at a new request series whose effective text differs, a non-empty rendering is consolidated at the first system node: each non-empty later system node receives a logged per-node empty replacement, then the head is rewritten if needed. Dormant empty tails need no replacement and do not determine the effective text. A series start whose latest surviving node already holds the rendering produces no system event, so the request keeps the prefix it had. On a continuing `in-history` series, an unchanged effective prompt produces no event and a non-empty change appends. An empty rendering clears every non-empty later system node through a logged per-node empty replacement, then empties the head if needed, regardless of route or series state. No older instructions remain model-visible. An empty head with no active later system node means no prompt; repeated clears and resume leave it empty. A restored non-empty prompt follows the same route/series rule: a continuing capable route may append it, while an incapable route or a new series whose effective text differs refills the head. A step starts a series when the pre-step decision declares `startsRequestSeries` or `session.surface.contentGeneration` changed since attachment or the last request (a replacement or image-offload decision). Visible tool-schema changes force prompt consolidation only when the prepared route lacks `toolUpdate`. Routes supporting both tool and system-prompt updates can append a changed prompt and activate added tools without rewriting earlier messages. If another admission condition replaces a system node, the request starts a new series with current tool declarations. Resume and a provider or model swap alone continue the series; the prepared route still governs admission. Per-node empty replacements preserve intervening history without a surface delete operation. @@ -146,8 +148,6 @@ The package-level contract is enough for most consumers; read these when you nee ----- -Empty ordinary input remains an identified inbox command, so it can wake a new turn while adding no `user/message` to the model surface. It passes normal pre-step admission; a rejected or removed input does not authorize a model request. Turn and step endings retain their normal success, failure, and cancellation handling. - ## Model Experience diff --git a/packages/core/agent-loop/README.zh.md b/packages/core/agent-loop/README.zh.md index 54395d27dc..1937358731 100644 --- a/packages/core/agent-loop/README.zh.md +++ b/packages/core/agent-loop/README.zh.md @@ -117,6 +117,8 @@ loop 在发布工厂前等待 inbox 与轮次边界注册完成,并将句柄 ### 轮次与步骤流程 +带 `source.kind: 'retry'` 的空输入属于控制输入:首次尝试准入时,Loop 记录带 inbox 身份、轮次、步骤和来源的 `agent/retry`,而不写入模型可见的 `user/message`。包含内容的重试控制输入以 `INVALID_REQUEST` 失败。队列所有权、pre-step 决策和轮次结算与普通输入一致;普通空用户消息仍会提交。 + 驱动器在其整个生命周期内拥有一个 agent,并在 `ctx.agents.withInitiator(agent, ...)` 内运行。`AgentLoop` 在服务生命周期内注册标准 `inbox` 投影,因此冷读取在没有 Agent 和所有 Agent 卸载后都可用。它还拥有 `modelSelection`,并为每个 Agent 构建 `AgentModel`,实现公开的 [Model API](../agent/README.zh.md#model-selection)。其包内部 `ReactLoopInbox` 使用该共享投影执行结构化命令与仅供 loop 使用的领取操作。在轮次边界,它先打开持久轮次,再原子领取待处理的 next-step 输入与一条排队提示词;在步骤之间则只领取 next-step 输入。驱动器组装提示词与工具,并运行 `agent/pre-step`;fallback 在监听器执行后投影运行时上下文。被拒绝的决定或空的首批输入不打开步骤。接纳后的首次尝试先记录 `step/start`,再运行 `agent/request` waterfall 与 `prepareCall()`;这两个异步阶段都看不到待提交的系统提示词与已接纳用户消息进入历史,在任一阶段取消都不会提交这两者。每次尝试时,循环随后依据已准备调用的能力,同步将渲染后的提示词与存活的 `system/message` 节点协调一致、刷新已注册运行时事实而不重新组装绑定的提示词或工具、仅在首次尝试追加已接纳的 `user/message` 批次并原位替换循环自有上下文候选、恢复被移除的运行时上下文、按需记录 header 与 context,再派生并冻结请求,通过该绑定的已准备调用发起流式请求。重试复用同一份已渲染组装结果,不重复组装、`agent/pre-step` 或用户消息准入。协调过程可见 pre-step 与重试中的压缩;序列中断时将提示词归并到头部,而非在已提交用户消息之后追加更新。请求由 `header.config`、`deriveMessages()` 与 `header.tools` 构成,不携带 `system` 字段。每次模型尝试会发出一个进程本地 `start`,仅在匹配的持久 assistant-frame 结算之后发出各个 `chunk`,并恰好发出一个终态 `end`;最终组装或消息追加失败时以 `aborted` 结算,`committed` 则出现在持久 `assistant/message` 之后。每次成功的模型调用都恰好追加一个 message 锚点,被取消的流则追加带 `interrupted: true` 的锚点并携带已交付前缀,使下一次请求包含用户看到的内容。在步骤内,独占调用形成屏障,并行安全调用使用有界滚动池;策略、持久结果与结果上下文保持模型顺序。 提示词准入依据实际的 `prepareCall()` 结果,而非先前的 `request/context`。没有系统节点时,即使提示词为空也追加(预留第 0 号节点,但不产生协议消息)。在不具备能力的路由上,或有效文本不同的新请求序列开始时,非空渲染文本归并到首个系统节点:每个非空的后续系统节点分别收到有日志记录的空内容替换,随后按需重写头节点。未生效的空尾节点无需替换,也不决定有效文本。新序列开始时若最新存活节点已经持有该渲染文本,则不产生系统事件,请求保留原有前缀。延续中的 `in-history` 序列在有效提示词不变时不产生事件,非空变更则追加。无论路由或序列状态如何,空渲染文本都会通过有日志记录的逐节点空内容替换清除每个非空的后续系统节点,再按需清空头节点。模型不会继续看到旧指令。空头节点且没有生效的后续系统节点表示没有提示词;重复清除与恢复会话都保持为空。重新提供的非空提示词遵循同一路由/序列规则:延续中的具备能力路由可以追加它,不具备能力的路由或有效文本不同的新序列则重新填充头节点。以下情况开启序列:pre-step 决定声明 `startsRequestSeries`,或 `session.surface.contentGeneration` 自附接或上次请求以来发生变化(替换或图片省略决定)。仅当已准备路由未声明 `toolUpdate` 时,可见工具 schema 变化才强制归并提示词。同时支持工具和系统提示词更新的路由可以追加变更后的提示词并激活新增工具,无需改写此前消息。如果其他准入条件替换系统节点,请求会以当前工具声明开启新序列。恢复与单纯的提供方或模型切换都延续序列;准入仍由已准备的路由决定。逐节点的空内容替换保留其间历史,无需 surface 删除操作。 @@ -146,8 +148,6 @@ loop 在发布工厂前等待 inbox 与轮次边界注册完成,并将句柄 ----- -空的普通输入仍是带身份的 inbox 控制记录,可以唤醒新轮次,但不向模型消息面添加 `user/message`。它经过正常 pre-step 准入;被拒绝或移除的输入不授权模型请求。轮次和步骤结束沿用正常的成功、失败与取消处理。 - ## 模型体验 diff --git a/packages/core/agent-loop/src/agent.ts b/packages/core/agent-loop/src/agent.ts index 619cca96ba..8150c334cc 100644 --- a/packages/core/agent-loop/src/agent.ts +++ b/packages/core/agent-loop/src/agent.ts @@ -455,12 +455,15 @@ export class ReactLoopAgent implements Agent { let contextAdmitted = false if (firstAttempt) { for (const message of decision.messages) { - if (message.source.kind === 'runtime-context') { + if (message.source.kind === 'retry') { + if (message.content.length > 0) throw new LlmError('Retry control input must be empty', 'INVALID_REQUEST') + this.session.append('agent/retry', { turn, step, inputId: message.id, source: message.source }) + } else if (message.source.kind === 'runtime-context') { if (context !== undefined && !contextAdmitted) { this.session.append('user/message', context, { surfaceOp: 'append' }) contextAdmitted = true } - } else if (message.source.kind !== 'user' || message.content.length > 0) { + } else { this.session.append('user/message', message, { surfaceOp: 'append' }) } } diff --git a/packages/core/agent-loop/tests/request-error.spec.ts b/packages/core/agent-loop/tests/request-error.spec.ts index 16c07fcf4e..ed95db70b3 100644 --- a/packages/core/agent-loop/tests/request-error.spec.ts +++ b/packages/core/agent-loop/tests/request-error.spec.ts @@ -32,15 +32,16 @@ function fail(message: string, code: string): () => never { } describe('agent/request-error', () => { - it('continues repeated failures through empty followups with balanced turns and one admitted user message', async () => { + it('records repeated retry controls with balanced turns and no additional model messages', async () => { const adapter = new MockAdapter([fail('busy', 'SERVER'), fail('still busy', 'SERVER'), textResponse('done')]) const ctx = await harness(adapter) onTestFinished(() => ctx.fiber.dispose()) - const agent = await ctx.agentLoop.create(SessionId('empty-continuation'), { provider: 'mock', model: 'mock' }) + const agent = await ctx.agentLoop.create(SessionId('retry-controls'), { provider: 'mock', model: 'mock' }) agent.followup(createUserMessage({ content: [{ type: 'text', text: 'original task' }], source: { kind: 'user' } })) await agent.whenIdle() - for (let index = 0; index < 2; index++) { - agent.followup(createUserMessage({ content: [], source: { kind: 'user' } })) + const controls = Array.from({ length: 2 }, () => createUserMessage({ content: [], source: { kind: 'retry' } })) + for (const control of controls) { + agent.followup(control) await agent.whenIdle() } expect(adapter.requests).toHaveLength(3) @@ -50,6 +51,9 @@ describe('agent/request-error', () => { } const events = agent.session.snapshotEvents() expect(events.filter(event => event.type === 'user/message')).toHaveLength(1) + expect(events.flatMap(event => event.type === 'agent/retry' ? [event.data] : [])).toEqual( + controls.map((control, index) => ({ turn: index + 2, step: 1, inputId: control.id, source: { kind: 'retry' } })), + ) expect(events.flatMap(event => event.type === 'turn/start' || event.type === 'turn/end' ? [`${event.type}:${event.data.turn}`] : [])) .toEqual(['turn/start:1', 'turn/end:1', 'turn/start:2', 'turn/end:2', 'turn/start:3', 'turn/end:3']) @@ -57,6 +61,26 @@ describe('agent/request-error', () => { .toEqual(['error', 'error', 'completed']) }) + it('preserves ordinary empty user input and rejects retry controls containing content', async () => { + const adapter = new MockAdapter([textResponse('done')]) + const ctx = await harness(adapter) + onTestFinished(() => ctx.fiber.dispose()) + const agent = await ctx.agentLoop.create(SessionId('retry-validation'), { provider: 'mock', model: 'mock' }) + const empty = createUserMessage({ content: [], source: { kind: 'user' } }) + agent.followup(empty) + await agent.whenIdle() + expect(adapter.requests[0]?.messages.filter(message => message.role === 'user')).toEqual([empty]) + agent.followup(createUserMessage({ content: [{ type: 'text', text: 'must not disappear' }], source: { kind: 'retry' } })) + await agent.whenIdle() + expect(adapter.requests).toHaveLength(1) + const events = agent.session.snapshotEvents() + expect(events.filter(event => event.type === 'agent/retry')).toHaveLength(0) + expect(events.filter(event => event.type === 'user/message')).toMatchObject([{ data: empty }]) + expect(events.filter(event => event.type === 'turn/end').at(-1)).toMatchObject({ + data: { turn: 2, reason: { kind: 'error', error: { code: 'INVALID_REQUEST' } } }, + }) + }) + it('does not offer middleware failures to request recovery', async () => { const adapter = new MockAdapter([textResponse('unused')]) const ctx = await harness(adapter) diff --git a/packages/core/agent/README.i18n.yaml b/packages/core/agent/README.i18n.yaml index a7bdc11ed0..44fd486abc 100644 --- a/packages/core/agent/README.i18n.yaml +++ b/packages/core/agent/README.i18n.yaml @@ -24,8 +24,8 @@ en: 85b0ff72a187110f zh: 3f61c1568d27b8bc /deepseek-ai-dsh-agent/use-this-package/drive-an-agent-s-conversation: - en: 71e258897e2e7d73 - zh: abf0991012faa17e + en: 09f6d66dddba9ae7 + zh: f3cd42db2375b705 /deepseek-ai-dsh-agent/use-this-package/scope-registrations-to-one-agent: en: 2594a1e02adea801 zh: cae8860369728efb @@ -60,8 +60,8 @@ en: e48eae6c7de88cf8 zh: 1f5b37bdb0e53788 /deepseek-ai-dsh-agent/further-exploration: - en: faf384f89b18b671 - zh: 42400f79f5ab84b0 + en: 4ef57cfafa344581 + zh: 73575876bfdc7a18 /deepseek-ai-dsh-agent/model-experience: en: 215e7ba838619b7b zh: a311da3843709f90 diff --git a/packages/core/agent/README.md b/packages/core/agent/README.md index aba28e301e..441d453f7f 100644 --- a/packages/core/agent/README.md +++ b/packages/core/agent/README.md @@ -55,7 +55,7 @@ For scoped switch notices, call `installModelSelection(agentCtx)` from `dsh-agen ### Drive an agent's conversation -The handle's methods route identified user-role messages into the agent's inbox. `followup()` queues an ordinary next-turn prompt and wakes the driver; `steer()` submits next-step input and wakes it; `inject()` adds model-facing context without waking the driver, so it lands in the next admitted step. `cancel(cause)` aborts the active activity and, unless `keepInbox` is set, clears pending work; `whenIdle()` resolves after the whole agent reaches quiescence. +The handle's methods route identified user-role messages into the agent's inbox. `followup()` queues an ordinary next-turn prompt and wakes the driver; `steer()` submits next-step input and wakes it; `inject()` adds model-facing context without waking the driver, so it lands in the next admitted step. An empty input with `source.kind: 'retry'` requests another model call through the same inbox; the shipped Loop records its acceptance as `agent/retry` without adding model-visible content. Ordinary empty user inputs retain ordinary message admission. `cancel(cause)` aborts the active activity and, unless `keepInbox` is set, clears pending work; `whenIdle()` resolves after the whole agent reaches quiescence. ```text handle.agent.followup({ @@ -150,8 +150,6 @@ The package-level contract is enough for most consumers; read these when you nee ----- -An ordinary `followup` with `content: []` is an explicit request to start a new turn from committed history. It retains the identified inbox receipt and normal lifecycle without creating a model-visible user message. A pre-step rejection or a decision rewritten to an empty batch still prevents the request. - ## Model Experience diff --git a/packages/core/agent/README.zh.md b/packages/core/agent/README.zh.md index c317176430..66367a5f6e 100644 --- a/packages/core/agent/README.zh.md +++ b/packages/core/agent/README.zh.md @@ -55,7 +55,7 @@ await handle.dispose() // stops the loop, unregisters, removes the session, un ### 驱动 agent 的对话 -句柄的方法把带标识的 user 角色消息路由进 agent 的收件箱。`followup()` 排队一条普通的下一个轮次提示词并唤醒驱动器;`steer()` 提交下一步输入并唤醒它;`inject()` 添加面向模型的上下文但不唤醒驱动器,因此它落在下一个被接纳的步骤中。`cancel(cause)` 中止当前活动,并在未设置 `keepInbox` 时清除待处理工作;`whenIdle()` 会在整个 agent 达到完全停稳后完成。 +句柄的方法把带标识的 user 角色消息路由进 agent 的收件箱。`followup()` 排队一条普通的下一个轮次提示词并唤醒驱动器;`steer()` 提交下一步输入并唤醒它;`inject()` 添加面向模型的上下文但不唤醒驱动器,因此它落在下一个被接纳的步骤中。带 `source.kind: 'retry'` 的空输入通过同一 inbox 请求再次调用模型;内置 Loop 将接受记录为 `agent/retry`,不增加模型可见内容。普通空用户输入仍遵循普通消息准入。`cancel(cause)` 中止当前活动,并在未设置 `keepInbox` 时清除待处理工作;`whenIdle()` 会在整个 agent 达到完全停稳后完成。 ```text handle.agent.followup({ @@ -150,8 +150,6 @@ await handle.agent.whenIdle() ----- -普通 `followup` 的 `content: []` 明确请求基于已提交历史启动新轮次。它保留带身份的 inbox 回执和正常生命周期,不生成模型可见的用户消息。pre-step 拒绝或改写为空批次的决策仍会阻止请求。 - ## 模型体验 diff --git a/packages/core/agent/src/runtime-types.ts b/packages/core/agent/src/runtime-types.ts index 58d883721c..5ae5c35aff 100644 --- a/packages/core/agent/src/runtime-types.ts +++ b/packages/core/agent/src/runtime-types.ts @@ -232,7 +232,8 @@ declare module './types.ts' { /** * Route identified input to an inbox boundary and optionally wake the driver. - * Empty ordinary input drives work without becoming a model-visible message. + * A retry-source input must be empty; admission records `agent/retry` + * without adding a model-visible message. * Waking input submitted after active cancellation is queued for the next * turn and runs when the aborted activity converges to idle; a `disposed` * cancel leaves it parked. A wake submitted while already idle always opens @@ -245,9 +246,8 @@ declare module './types.ts' { send(message: UserMessage, target: InboxTarget, wakeup: boolean): void /** - * Queue an ordinary follow-up turn and wake the driver. The item becomes the - * sole ordinary message of its own turn. An empty ordinary input starts a - * turn from committed history without appending a model-visible user message. + * Queue a follow-up turn and wake the driver. The item is its sole ordinary + * prompt, or an empty retry-source control with no model-visible message. * @param message - identified prompt content and the source that supplied it. */ followup(message: UserMessage): void diff --git a/packages/core/agent/src/types.ts b/packages/core/agent/src/types.ts index 06d2c88ab1..defbd94bec 100644 --- a/packages/core/agent/src/types.ts +++ b/packages/core/agent/src/types.ts @@ -4,8 +4,8 @@ * @module @deepseek-ai/dsh-agent/types */ -import type { ReasoningEffortId } from '@deepseek-ai/dsh-llm/brand' -import type { UserMessage } from '@deepseek-ai/dsh-llm/types' +import type { MessageId, ReasoningEffortId } from '@deepseek-ai/dsh-llm/brand' +import type { MessageSource, UserMessage } from '@deepseek-ai/dsh-llm/types' // Type-only: the Workspace registry's archive-admission family map this registry merges `turn` into. import type {} from '@deepseek-ai/dsh-workspace/types' import type { OptionalSessionSeq, SessionId, SessionSeq } from '@deepseek-ai/dsh-session/types' @@ -18,6 +18,13 @@ export interface Agent { readonly id: SessionId } +declare module '@deepseek-ai/dsh-llm' { + interface MessageSourceMap { + /** Empty control input requesting a model call without adding user text. */ + retry: { kind: 'retry' } + } +} + declare module '@deepseek-ai/dsh-workspace/types' { interface SessionActivityKindMap { /** The session's own Agent is inside a turn, including one waiting for an approval or an answer. */ @@ -102,6 +109,13 @@ declare module '@deepseek-ai/dsh-session-projection/types' { declare module '@deepseek-ai/dsh-session/types' { interface SessionEventMap { + /** An admitted retry control, correlated with its durable inbox input; never enters model history. */ + 'agent/retry': { + turn: number + step: number + inputId: MessageId + source: Extract + } /** * One normalized mutation of an agent's durable pending-message lists. * The session-projection registry applies the committed event before diff --git a/packages/core/session/src/known-event-types.ts b/packages/core/session/src/known-event-types.ts index a0f5c17795..71a61005df 100644 --- a/packages/core/session/src/known-event-types.ts +++ b/packages/core/session/src/known-event-types.ts @@ -22,6 +22,7 @@ export const KNOWN_SESSION_EVENT_TYPES: ReadonlySet = new Set([ 'agent-preset/selected', 'agent/inbox/spliced', + 'agent/retry', 'approval/asked', 'approval/decided', 'approval/policy', diff --git a/packages/sdk/client/src/client.ts b/packages/sdk/client/src/client.ts index 4bc8140c02..56f7401b7a 100644 --- a/packages/sdk/client/src/client.ts +++ b/packages/sdk/client/src/client.ts @@ -344,7 +344,7 @@ export class HarnessClient { /** * Queue one prompt and return its durable inbox identity. * @param sessionId - target session; an unknown id creates it. - * @param contentBlocks - the user message, sent verbatim. + * @param contentBlocks - user content, or an empty array to retry without adding a model-visible message. * @returns the queued message id. */ async prompt(sessionId: string, contentBlocks: SdkPromptContentBlock[]): Promise { diff --git a/packages/sdk/protocol/README.i18n.yaml b/packages/sdk/protocol/README.i18n.yaml index 2aed1dc438..c29b0abca2 100644 --- a/packages/sdk/protocol/README.i18n.yaml +++ b/packages/sdk/protocol/README.i18n.yaml @@ -27,8 +27,8 @@ en: 9dcd999ae8c66b71 zh: 174c5fe0cbb0a9e4 /deepseek-ai-dsh-sdk-protocol/use-this-package/payload-semantics: - en: 3cf5d4ffbaf37f69 - zh: 23c4450e31c91d67 + en: f5f0ff9090bdd076 + zh: 83e43639a892879a /deepseek-ai-dsh-sdk-protocol/understand-the-implementation: en: a11b136d8296deda zh: 85081da5be53c9e1 diff --git a/packages/sdk/protocol/README.md b/packages/sdk/protocol/README.md index 93a34454af..2240c78cf7 100644 --- a/packages/sdk/protocol/README.md +++ b/packages/sdk/protocol/README.md @@ -69,7 +69,7 @@ An answering host sets `initialize.capabilities.userQuestions: true`. The runtim ### Payload semantics -`SessionPromptResult.messageId` identifies the queued user message; it does not identify a later assistant message, turn ending, or prompt result. `SdkPromptContentBlock` accepts ordinary durable content plus `SdkEncodedImageBlock { type: "image", data, mimeType }`; the server converts encoded images to durable references before enqueue. `InitializeParams.reasoningEffort` is an optional non-empty adapter-owned identifier for the selected provider/model route; omission preserves that model's default. `InitializeParams.maxTokens` is an optional positive safe integer that caps each conversation-model output for SDK-created agents and their in-process descendants; omission lets the selected adapter's exact-model default apply. The server resolves the exact route during initialization and rejects `session/prompt` until that handshake succeeds, so a missing adapter, unavailable model, or unsupported effort cannot fall back to constructor defaults. `SubagentFinishedNotification.lastAssistantMessage` carries the child's last non-empty assistant message, or its accumulated assistant text when no such message exists; the field is absent when the child produced neither. `serverInfo.name` stays the wire-stable `deepseek-harness-sdk-runtime`. Notification payloads depend on `SessionEvent` (`dsh-session`), `ContentBlock` (`dsh-llm`), and `SubagentStopReason` (`dsh-subagent`), so the session vocabulary is part of the wire contract. +`SessionPromptResult.messageId` identifies the queued prompt or retry control; it does not identify a later assistant message, turn ending, or prompt result. An empty `contentBlocks` array queues an empty retry-source control; admission records `agent/retry` rather than `user/message`. `SdkPromptContentBlock` accepts ordinary durable content plus `SdkEncodedImageBlock { type: "image", data, mimeType }`; the server converts encoded images to durable references before enqueue. `InitializeParams.reasoningEffort` is an optional non-empty adapter-owned identifier for the selected provider/model route; omission preserves that model's default. `InitializeParams.maxTokens` is an optional positive safe integer that caps each conversation-model output for SDK-created agents and their in-process descendants; omission lets the selected adapter's exact-model default apply. The server resolves the exact route during initialization and rejects `session/prompt` until that handshake succeeds, so a missing adapter, unavailable model, or unsupported effort cannot fall back to constructor defaults. `SubagentFinishedNotification.lastAssistantMessage` carries the child's last non-empty assistant message, or its accumulated assistant text when no such message exists; the field is absent when the child produced neither. `serverInfo.name` stays the wire-stable `deepseek-harness-sdk-runtime`. Notification payloads depend on `SessionEvent` (`dsh-session`), `ContentBlock` (`dsh-llm`), and `SubagentStopReason` (`dsh-subagent`), so the session vocabulary is part of the wire contract. ----- diff --git a/packages/sdk/protocol/README.zh.md b/packages/sdk/protocol/README.zh.md index f81f58c223..f5339e55c9 100644 --- a/packages/sdk/protocol/README.zh.md +++ b/packages/sdk/protocol/README.zh.md @@ -69,7 +69,7 @@ kind: "package-library" ### 载荷语义 -`SessionPromptResult.messageId` 标识已排队的用户消息;它不标识后续的助手消息、轮次结束或提示词结果。`SdkPromptContentBlock` 接受普通持久内容以及 `SdkEncodedImageBlock { type: "image", data, mimeType }`;服务器在入队前把编码图像转换为持久引用。`InitializeParams.reasoningEffort` 是所选提供方/模型路由可选的非空适配器自有标识符;省略时保留该模型的默认值。`InitializeParams.maxTokens` 是可选的正安全整数,用于限制 SDK 创建的 agent 及其进程内后代的每次对话模型输出;省略时应用所选适配器的确切模型默认值。服务器会在初始化期间解析确切路由,并在握手成功前拒绝 `session/prompt`,因此缺少适配器、模型不可用或推理强度不受支持时,不会回退到构造期默认值。`SubagentFinishedNotification.lastAssistantMessage` 携带子 agent 最后一条非空 assistant 消息;若不存在这类消息,则携带其累积的 assistant 文本;子 agent 两种输出均未产生时,该字段缺省。`serverInfo.name` 的协议值固定为 `deepseek-harness-sdk-runtime`。通知载荷依赖 `SessionEvent`(`dsh-session`)、`ContentBlock`(`dsh-llm`)与 `SubagentStopReason`(`dsh-subagent`),因此会话词汇是协议格式约定的一部分。 +`SessionPromptResult.messageId` 标识已排队的提示词或重试控制输入;它不标识后续的助手消息、轮次结束或提示词结果。空 `contentBlocks` 数组将空的 retry 来源控制输入排入队列;准入记录 `agent/retry`,而非 `user/message`。`SdkPromptContentBlock` 接受普通持久内容以及 `SdkEncodedImageBlock { type: "image", data, mimeType }`;服务器在入队前把编码图像转换为持久引用。`InitializeParams.reasoningEffort` 是所选提供方/模型路由可选的非空适配器自有标识符;省略时保留该模型的默认值。`InitializeParams.maxTokens` 是可选的正安全整数,用于限制 SDK 创建的 agent 及其进程内后代的每次对话模型输出;省略时应用所选适配器的确切模型默认值。服务器会在初始化期间解析确切路由,并在握手成功前拒绝 `session/prompt`,因此缺少适配器、模型不可用或推理强度不受支持时,不会回退到构造期默认值。`SubagentFinishedNotification.lastAssistantMessage` 携带子 agent 最后一条非空 assistant 消息;若不存在这类消息,则携带其累积的 assistant 文本;子 agent 两种输出均未产生时,该字段缺省。`serverInfo.name` 的协议值固定为 `deepseek-harness-sdk-runtime`。通知载荷依赖 `SessionEvent`(`dsh-session`)、`ContentBlock`(`dsh-llm`)与 `SubagentStopReason`(`dsh-subagent`),因此会话词汇是协议格式约定的一部分。 ----- diff --git a/packages/sdk/protocol/src/types.ts b/packages/sdk/protocol/src/types.ts index cfa746cac2..dd3c86a930 100644 --- a/packages/sdk/protocol/src/types.ts +++ b/packages/sdk/protocol/src/types.ts @@ -67,7 +67,7 @@ export interface InitializeResult { export interface SessionPromptParams { /** The SDK-side session id; an unknown id lazily creates the agent+session pair. */ sessionId: string - /** The prompt content blocks, sent verbatim as the user message. */ + /** Prompt content; an empty array requests a retry control without a model-visible user message. */ contentBlocks: SdkPromptContentBlock[] } @@ -85,7 +85,7 @@ export type SdkPromptContentBlock = ContentBlock | SdkEncodedImageBlock /** Durable enqueue receipt for one prompt. */ export interface SessionPromptResult { - /** Identity of the queued user message. */ + /** Identity of the queued prompt or retry control. */ messageId: string } diff --git a/packages/sdk/server/README.i18n.yaml b/packages/sdk/server/README.i18n.yaml index b56838412f..7ad56c42a9 100644 --- a/packages/sdk/server/README.i18n.yaml +++ b/packages/sdk/server/README.i18n.yaml @@ -27,8 +27,8 @@ en: 784b68b5a498f2d5 zh: 2c3c5a1490845179 /deepseek-ai-dsh-sdk-jsonrpc-server/use-this-package/what-sdk-clients-can-do: - en: 3e657fa4c9586454 - zh: 26d572f91e124cc5 + en: 0936bb4ac5f59851 + zh: fb91df93242fc80e /deepseek-ai-dsh-sdk-jsonrpc-server/use-this-package/native-user-questions: en: 5100c311c8476f93 zh: 60d4bc3165731246 @@ -60,8 +60,8 @@ en: 04d1b73cd8e06dfe zh: cd67fbbfdf39f776 /deepseek-ai-dsh-sdk-jsonrpc-server/model-experience/sdk-user-message/what-the-model-sees: - en: 2b5a80ec4b4df11e - zh: e459fe77cc14d9b8 + en: 57ed296ea4f0ec42 + zh: a0bf3746ce9bdf01 /deepseek-ai-dsh-sdk-jsonrpc-server/model-experience/sdk-user-message/token-effect: en: 372ed851986b76d2 zh: 075b94a565275368 diff --git a/packages/sdk/server/README.md b/packages/sdk/server/README.md index e9007e593c..f3dcb00c29 100644 --- a/packages/sdk/server/README.md +++ b/packages/sdk/server/README.md @@ -53,7 +53,7 @@ Stdout carries only JSON-RPC frames, so clients can parse every byte; diagnostic ### What SDK clients can do -`initialize` is the runtime-readiness boundary: when the server is mounted by a Loader composition, it waits for the current plugin tree to settle before replying, so async sibling capabilities such as initial MCP tool discovery are visible to the first prompt. The handshake returns the wire-stable identity `deepseek-harness-sdk-runtime`. The server validates the provider/model route and optional non-empty `reasoningEffort` through the selected adapter before it stores them; omission stores no effort, so the model retains its own default. An optional positive `maxTokens` becomes the request output cap of each SDK-created agent and its in-process descendants, while omission applies the selected adapter or provider route default. JSON-RPC requests may dispatch concurrently, so `session/prompt` rejects until one `initialize` has completed successfully; clients must await the handshake before sending prompts. An accepted prompt queues one identified user message and immediately returns `{ messageId }`; the server then streams every durable fact as `session.event` and every whole-agent lifecycle transition as `session.status`. It does not assign an assistant message or `turn/end` to a prompt, and independent requests may enqueue more work on the same session. Persistence roots and persona come from the surrounding composition. +`initialize` is the runtime-readiness boundary: when the server is mounted by a Loader composition, it waits for the current plugin tree to settle before replying, so async sibling capabilities such as initial MCP tool discovery are visible to the first prompt. The handshake returns the wire-stable identity `deepseek-harness-sdk-runtime`. The server validates the provider/model route and optional non-empty `reasoningEffort` through the selected adapter before it stores them; omission stores no effort, so the model retains its own default. An optional positive `maxTokens` becomes the request output cap of each SDK-created agent and its in-process descendants, while omission applies the selected adapter or provider route default. JSON-RPC requests may dispatch concurrently, so `session/prompt` rejects until one `initialize` has completed successfully; clients must await the handshake before sending prompts. An accepted prompt queues one identified input and immediately returns `{ messageId }`; the server then streams every durable fact as `session.event` and every whole-agent lifecycle transition as `session.status`. It does not assign an assistant message or `turn/end` to a prompt, and independent requests may enqueue more work on the same session. Persistence roots and persona come from the surrounding composition. `session/wait` observes an existing SDK-owned session through managed descendant completion and any resulting root turns. It rejects unknown or retired roots, never creates a session, and responds after the root stays idle across the descendant check. Idle children with parked inbox messages remain resident but do not delay this response; later waking input can resume them. Session notifications emitted before completion precede the response on the stdio transport. A live Agent failure remains associated with that exact session until a later committed `turn/end` accounts for the outcome; otherwise the wait rejects, including when the failure preceded the wait request. Errors already represented by a terminal remain in Session events, and later successful activity clears an earlier live failure. @@ -120,7 +120,7 @@ Read these pages when the plugin contract is not enough. They move from the wire #### What the model sees -For each accepted `session/prompt`, text and durable content references enter one user message verbatim. Inline `SdkEncodedImageBlock` values are validated and committed through the composition's attachment store first, so the session log retains content-addressed image references rather than base64 bytes. This package adds no system-prompt prose or tool schema; those come from the other plugins in the composition. +For each non-empty accepted `session/prompt`, text and durable content references enter one user message verbatim. An empty content array queues a retry-source control; Loop admission records `agent/retry` without adding model-visible content. Inline `SdkEncodedImageBlock` values are validated and committed through the composition's attachment store first, so the session log retains content-addressed image references rather than base64 bytes. This package adds no system-prompt prose or tool schema; those come from the other plugins in the composition. #### Token effect diff --git a/packages/sdk/server/README.zh.md b/packages/sdk/server/README.zh.md index 56ae2f8bdf..4d56722a7f 100644 --- a/packages/sdk/server/README.zh.md +++ b/packages/sdk/server/README.zh.md @@ -53,7 +53,7 @@ Stdout 只承载 JSON-RPC 帧,客户端可以逐字节解析;诊断信息应 ### SDK 客户端可以做什么 -`initialize` 是运行时就绪边界:服务器由 Loader 组合挂载时,会等待当前插件树完成所有加载任务后再响应,因此首次提示词能够看到 MCP 初始工具发现等异步同级能力。握手返回协议稳定标识 `deepseek-harness-sdk-runtime`。服务器会通过所选适配器校验提供方/模型路由与可选的非空 `reasoningEffort`,再保存这些值;省略时不会保存推理强度,因此模型保留自身默认值。可选的正整数 `maxTokens` 会成为每个 SDK 创建的 agent 及其进程内后代的请求输出上限,省略时则应用所选适配器或提供方路由的默认值。JSON-RPC 请求可能并发分派,因此在一次 `initialize` 成功完成之前,`session/prompt` 会拒绝;客户端必须等待握手完成后再发送提示词。已接受的提示词会把一条带标识的用户消息排入队列,并立即返回 `{ messageId }`;服务器随后把每个持久事实作为 `session.event`、把整个 agent 生命周期的每次状态转换作为 `session.status` 流式发出。它不会把某条助手消息或 `turn/end` 归属于某个提示词,同一会话上的独立请求可以继续排入更多工作。持久化根目录与 persona 来自外围组合。 +`initialize` 是运行时就绪边界:服务器由 Loader 组合挂载时,会等待当前插件树完成所有加载任务后再响应,因此首次提示词能够看到 MCP 初始工具发现等异步同级能力。握手返回协议稳定标识 `deepseek-harness-sdk-runtime`。服务器会通过所选适配器校验提供方/模型路由与可选的非空 `reasoningEffort`,再保存这些值;省略时不会保存推理强度,因此模型保留自身默认值。可选的正整数 `maxTokens` 会成为每个 SDK 创建的 agent 及其进程内后代的请求输出上限,省略时则应用所选适配器或提供方路由的默认值。JSON-RPC 请求可能并发分派,因此在一次 `initialize` 成功完成之前,`session/prompt` 会拒绝;客户端必须等待握手完成后再发送提示词。已接受的提示词会把一条带标识的输入排入队列,并立即返回 `{ messageId }`;服务器随后把每个持久事实作为 `session.event`、把整个 agent 生命周期的每次状态转换作为 `session.status` 流式发出。它不会把某条助手消息或 `turn/end` 归属于某个提示词,同一会话上的独立请求可以继续排入更多工作。持久化根目录与 persona 来自外围组合。 `session/wait` 观察已有 SDK 会话,直到受管理后代及其触发的根 Agent 后续轮次完成。它拒绝未知或已释放的根 Agent,不会创建会话,并在后代检查期间根 Agent 持续空闲后响应。收件箱消息处于停放状态的空闲子级继续驻留,但不延迟此响应;后续唤醒输入可使其继续运行。完成前发出的会话通知在 stdio 传输上先于响应到达。Agent 的运行时错误会保留在对应的确切会话中,直到后续提交的 `turn/end` 记录结果;否则等待请求会失败,即使错误发生在等待请求之前。终止事件已经记录的错误仍通过会话事件表达,后续成功的活动会清除较早的运行时错误。 @@ -120,7 +120,7 @@ Stdout 只承载 JSON-RPC 帧,客户端可以逐字节解析;诊断信息应 #### 模型看到什么 -对于每个已接受的 `session/prompt`,文本和持久内容引用会原样进入一条用户消息。内联 `SdkEncodedImageBlock` 会先通过组合中的附件存储完成校验与提交,因此会话日志保留内容寻址的图片引用而不是 base64 字节。此包不会添加系统提示词文本或工具 schema;这些内容来自组合中的其他插件。 +对于每个非空且已接受的 `session/prompt`,文本和持久内容引用会原样进入一条用户消息。空内容数组将 retry 来源控制输入排入队列;Loop 准入记录 `agent/retry`,不增加模型可见内容。内联 `SdkEncodedImageBlock` 会先通过组合中的附件存储完成校验与提交,因此会话日志保留内容寻址的图片引用而不是 base64 字节。此包不会添加系统提示词文本或工具 schema;这些内容来自组合中的其他插件。 #### Token 影响 diff --git a/packages/sdk/server/src/server.ts b/packages/sdk/server/src/server.ts index 0dceccc59c..8bb25cb1db 100644 --- a/packages/sdk/server/src/server.ts +++ b/packages/sdk/server/src/server.ts @@ -247,7 +247,7 @@ export class HarnessSdkJsonRpcServer { /** * Queue one identified prompt without assigning later activity to it. - * @param params - target session and user content. + * @param params - target session and user content; an empty array queues a retry control. * @returns the durable message identity. */ async prompt(params: SessionPromptParams): Promise { @@ -263,7 +263,7 @@ export class HarnessSdkJsonRpcServer { this.assertLiveAgent(rec, params.sessionId) const message = createUserMessage({ content, - source: { kind: 'user' }, + source: { kind: content.length === 0 ? 'retry' : 'user' }, }) rec.handle.agent.followup(message) return { messageId: message.id } diff --git a/scripts/smoke-python-runtime.py b/scripts/smoke-python-runtime.py index fe17f6f952..0ea6f3831c 100644 --- a/scripts/smoke-python-runtime.py +++ b/scripts/smoke-python-runtime.py @@ -1925,6 +1925,11 @@ def smoke_sdk_scheduler_recovery(base_url: str, executable: Path, update_snapsho raise AssertionError(f"scheduler recovery expected two serialized model requests: {requests}") logs = read_session_logs(sessions) records = logs[RECOVERY_SESSION_ID] + prompts = [record for record in records if record.get("type") == "user/message" + and record["data"]["source"].get("kind") == "user"] + retries = [record for record in records if record.get("type") == "agent/retry"] + if len(prompts) != 1 or len(retries) != 1: + raise AssertionError("scheduler recovery must record one original prompt and one retry control") calls = [record["data"]["callId"] for record in records if record.get("type") == "tool/call"] if calls != list(RECOVERY_CALL_IDS[:2]): raise AssertionError(f"scheduler recovery dispatched the unstarted call: {calls}") diff --git a/scripts/snapshots/python-sdk-single-exe/scheduler-recovery/result.json b/scripts/snapshots/python-sdk-single-exe/scheduler-recovery/result.json index 7172a0edd1..e023466dea 100644 --- a/scripts/snapshots/python-sdk-single-exe/scheduler-recovery/result.json +++ b/scripts/snapshots/python-sdk-single-exe/scheduler-recovery/result.json @@ -57,6 +57,7 @@ "turn/start", "agent/inbox/spliced", "step/start", + "agent/retry", "assistant/message", "step/end", "turn/end" @@ -70,6 +71,7 @@ "session.event", "session.event", "session.event", + "session.event", "session.status" ] } diff --git a/scripts/snapshots/python-sdk-single-exe/scheduler-recovery/session.v4.jsonl b/scripts/snapshots/python-sdk-single-exe/scheduler-recovery/session.v4.jsonl index 37ccfaa185..acab2bcdb6 100644 --- a/scripts/snapshots/python-sdk-single-exe/scheduler-recovery/session.v4.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/scheduler-recovery/session.v4.jsonl @@ -21,10 +21,11 @@ {"type":"tool/result","data":{"turn":1,"step":1,"message":{"id":"interrupted-tool-result-scheduler-unstarted-19","role":"tool","toolCallId":"scheduler-unstarted","isError":true,"source":{"kind":"tool","callId":"scheduler-unstarted"},"content":[{"type":"text","text":"The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed."}]},"error":{"name":"ToolNotStartedError","code":"TOOL_NOT_STARTED"}},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"error","error":{"message":"Snapshot scheduler preparation failed","code":"UNKNOWN"}}}} -{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"}]}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[],"source":{"kind":"retry"},"role":"user","id":"{{messageId}}"}]}} {"type":"turn/start","data":{"turn":2}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":2,"step":1}} +{"type":"agent/retry","data":{"turn":2,"step":1,"inputId":"{{messageId}}","source":{"kind":"retry"}}} {"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SCHEDULER_RECOVERY_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"text"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["SCHEDULER_RECOVERY_OK"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SCHEDULER_RECOVERY_OK"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"text"}]}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} diff --git a/snapshots/sdk/sdk.snapshot.ts b/snapshots/sdk/sdk.snapshot.ts index ddd859bd38..c4c5a27aa7 100644 --- a/snapshots/sdk/sdk.snapshot.ts +++ b/snapshots/sdk/sdk.snapshot.ts @@ -485,23 +485,15 @@ function modelFromSession(log: string): { provider: string; model: string } { function turnActions(log: string): TurnAction[] { const actions: TurnAction[] = [] let current: TurnAction | undefined - let emptyQueued = false for (const record of records(log)) { - if (record.type === 'agent/inbox/spliced') { - const data = record.data as JsonObject | undefined - if (data?.target === 'next-turn' && Array.isArray(data.inserted)) { - emptyQueued ||= data.inserted.some(value => { - const message = value as JsonObject - return (message.source as JsonObject | undefined)?.kind === 'user' - && Array.isArray(message.content) && message.content.length === 0 - }) - } - } if (record.type === 'turn/start') { const data = record.data as JsonObject | undefined if (typeof data?.turn !== 'number') throw new Error('SDK snapshot turn/start has no turn') - current = { turn: data.turn, ...emptyQueued ? { content: [] } : {} } - emptyQueued = false + current = { turn: data.turn } + continue + } + if (record.type === 'agent/retry' && current !== undefined) { + current = { turn: current.turn, content: [] } continue } if (record.type === 'user/message' && current !== undefined && current.content === undefined) { @@ -1132,6 +1124,7 @@ describe('TypeScript SDK snapshots over the jsonrpc runtime', () => { if (scenario.name === 'tool-scheduler-recovery') { expect(records(ordered[0]!.content).filter(event => event.type === 'user/message' && (event.data['source'] as { kind: string }).kind === 'user')).toHaveLength(1) + expect(records(ordered[0]!.content).filter(event => event.type === 'agent/retry')).toHaveLength(1) expect(results).toHaveLength(2) const events = results.flatMap(result => result.events) expect(results[1]?.finalResponse, JSON.stringify(events.filter(event => event.type === 'turn/end'))) diff --git a/snapshots/sdk/tool-scheduler-recovery/notifications.expected.jsonl b/snapshots/sdk/tool-scheduler-recovery/notifications.expected.jsonl index cfe4388d42..01f324070c 100644 --- a/snapshots/sdk/tool-scheduler-recovery/notifications.expected.jsonl +++ b/snapshots/sdk/tool-scheduler-recovery/notifications.expected.jsonl @@ -19,12 +19,13 @@ {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":20,"time":0,"data":{"turn":1,"step":1}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":21,"time":0,"data":{"turn":1,"reason":{"kind":"error","error":{"message":"Snapshot scheduler preparation failed","code":"UNKNOWN"}}}}}} {"method":"session.status","params":{"sessionId":"{{sessionId}}","status":"idle"}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":22,"time":0,"data":{"target":"next-turn","start":0,"inserted":[{"content":[],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"}]}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":22,"time":0,"data":{"target":"next-turn","start":0,"inserted":[{"content":[],"source":{"kind":"retry"},"role":"user","id":"{{sessionId}}"}]}}}} {"method":"session.status","params":{"sessionId":"{{sessionId}}","status":"running"}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/start","seq":23,"time":0,"data":{"turn":2}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":24,"time":0,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":25,"time":0,"data":{"turn":2,"step":1}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":26,"time":0,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SCHEDULER_RECOVERY_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":64,"outputTokens":28},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["SCHEDULER_RECOVERY_OK"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SCHEDULER_RECOVERY_OK"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":64,"outputTokens":28}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":27,"time":0,"data":{"turn":2,"step":1}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":28,"time":0,"data":{"turn":2,"reason":{"kind":"completed"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"agent/retry","seq":26,"time":0,"data":{"turn":2,"step":1,"inputId":"{{sessionId}}","source":{"kind":"retry"}}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":27,"time":0,"data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SCHEDULER_RECOVERY_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":64,"outputTokens":28},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["SCHEDULER_RECOVERY_OK"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SCHEDULER_RECOVERY_OK"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":64,"outputTokens":28}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":28,"time":0,"data":{"turn":2,"step":1}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":29,"time":0,"data":{"turn":2,"reason":{"kind":"completed"}}}}} {"method":"session.status","params":{"sessionId":"{{sessionId}}","status":"idle"}} diff --git a/snapshots/sdk/tool-scheduler-recovery/session.v4.jsonl b/snapshots/sdk/tool-scheduler-recovery/session.v4.jsonl index 31eb0f7d38..704d83c721 100644 --- a/snapshots/sdk/tool-scheduler-recovery/session.v4.jsonl +++ b/snapshots/sdk/tool-scheduler-recovery/session.v4.jsonl @@ -21,10 +21,11 @@ {"type":"tool/result","data":{"turn":1,"step":1,"message":{"id":"interrupted-tool-result-scheduler-unstarted-19","role":"tool","toolCallId":"scheduler-unstarted","isError":true,"source":{"kind":"tool","callId":"scheduler-unstarted"},"content":[{"type":"text","text":"The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed."}]},"error":{"name":"ToolNotStartedError","code":"TOOL_NOT_STARTED"}},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"turn/end","data":{"turn":1,"reason":{"kind":"error","error":{"message":"Snapshot scheduler preparation failed","code":"UNKNOWN"}}}} -{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[],"source":{"kind":"user"},"role":"user","id":"{{message:6}}"}]}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[],"source":{"kind":"retry"},"role":"user","id":"{{message:6}}"}]}} {"type":"turn/start","data":{"turn":2}} {"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":2,"step":1}} +{"type":"agent/retry","data":{"turn":2,"step":1,"inputId":"{{message:6}}","source":{"kind":"retry"}}} {"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"text","text":"SCHEDULER_RECOVERY_OK"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"usage":{"inputTokens":64,"outputTokens":28},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["SCHEDULER_RECOVERY_OK"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"SCHEDULER_RECOVERY_OK"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":64,"outputTokens":28}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} From 40599db7c85fb030b2086bd4da81c32c3170ac41 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Tue, 27 Oct 2026 12:11:22 +0800 Subject: [PATCH 11/65] test(agent): verify retry control identity across SDK recovery --- apps/web/tests/live-interactions.e2e.ts | 6 +++--- docs/architecture.i18n.yaml | 4 ++-- docs/architecture.md | 4 ++-- docs/architecture.zh.md | 4 ++-- docs/persistence-catalog.i18n.yaml | 12 ++++++------ docs/persistence-catalog.md | 13 +++++++++---- docs/persistence-catalog.zh.md | 13 +++++++++---- .../historical-formats/README.i18n.yaml | 4 ++-- .../historical-formats/README.md | 2 +- .../historical-formats/README.zh.md | 2 +- docs/persistence-schema.json | 2 +- .../tests/empty-prompt.host.spec.ts | 7 ++++--- scripts/smoke-python-runtime.py | 8 ++++++++ 13 files changed, 50 insertions(+), 31 deletions(-) diff --git a/apps/web/tests/live-interactions.e2e.ts b/apps/web/tests/live-interactions.e2e.ts index bde84fe2d1..394da150c3 100644 --- a/apps/web/tests/live-interactions.e2e.ts +++ b/apps/web/tests/live-interactions.e2e.ts @@ -478,9 +478,9 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => { const userMessages = sessionEvents.flatMap(event => event.type === 'user/message' && event.data.source.kind === 'user' ? [event.data] : []) expect(userMessages.map(message => message.content)).toEqual([[{ type: 'text', text: PROMPT }]]) - expect(sessionEvents.filter(event => event.type === 'agent/retry')).toEqual([ - expect.objectContaining({ data: expect.objectContaining({ turn: 2, step: 1, source: expect.objectContaining({ kind: 'retry' }) }) }), - ]) + const retries = sessionEvents.filter(event => event.type === 'agent/retry') + expect(retries).toHaveLength(1) + expect(retries[0]?.data).toMatchObject({ turn: 2, step: 1, source: { kind: 'retry' } }) expect(sessionEvents.filter(event => event.type === 'turn/start')).toHaveLength(2) expect(sessionEvents.filter(event => event.type === 'turn/end')).toHaveLength(2) expect(await resume.count()).toBe(0) diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index 5dfa2c2995..ca357a4a9c 100644 --- a/docs/architecture.i18n.yaml +++ b/docs/architecture.i18n.yaml @@ -24,8 +24,8 @@ en: 693407efb233f961 zh: 37b9dd0677d4405c /deepseek-harness-architecture/turn-flow: - en: 3770eacd9c3bae28 - zh: bd7b8a0374ba0ff8 + en: d67fe476bec779ea + zh: 774a0913a8fb03d0 /deepseek-harness-architecture/session-log: en: 8a7f19b95d426c99 zh: d5bb0538404e20ea diff --git a/docs/architecture.md b/docs/architecture.md index 064026d4d2..1ffee7bf2f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -107,9 +107,9 @@ turn/end Factory setup follows SessionStore entry and `session/created`, preceding `agent/created`; closing events commit before removal. -One inbox feeds the driver; injected context waits for a waking message. AgentLoop’s durable `inbox` projection exposes pending input without live Agents. +`inbox` persists pending input; injected context waits for waking input. Retry controls record `agent/retry` without changing turn settlement. -Before assembly, the loop snapshots Session model selection in `AssembleContext.modelSelection` and `agent/pre-step.modelSelection`; retries retain it; omitted reasoning effort uses adapter defaults. `agent/pre-step` listeners accept, rewrite, or reject claimed messages; a rejected or empty first claim closes a durable turn without a step. An enter decision may set `startsRequestSeries`: the loop logs a fresh `request/header` (reason `series`, or `change` with `startsSeries: true` when the envelope also changed). Wrapping listeners preserve that declaration with `{ ...decision, messages }`. After assembly and `step/start`, the deprecated `agent/request` hook adjusts controls without changing the provider, model, or effort selection, and `prepareCall()` binds the adapter before the system prompt and accepted users are committed; cancellation during either async phase commits neither. Prepared call capabilities govern prompt admission, not preceding `request/context`. Empty retry-source controls record `agent/retry` instead of entering model history; they retain ordinary inbox and turn lifecycle. Each attempt refreshes registered runtime facts, reconciles the bound prompt, admits users once, and restores removed runtime context, logs header/context as needed, and freezes the request before streaming the bound call. Retries do not repeat assembly or `agent/pre-step`. Surface replacements and image-offload decisions after attachment start a new request series, including during the first resumed pre-step; unchanged resume continues the series. The system prompt travels as `system/message` history: empty text clears it, capable routes append updates, and other routes consolidate it at the first system node. [Agent-loop](../packages/core/agent-loop/README.md#understand-the-implementation) owns prompt admission and replacement details. +Before assembly, the loop snapshots Session model selection in `AssembleContext.modelSelection` and `agent/pre-step.modelSelection`; retries retain it; omitted reasoning effort uses adapter defaults. `agent/pre-step` listeners accept, rewrite, or reject claimed messages; a rejected or empty first claim closes a durable turn without a step. An enter decision may set `startsRequestSeries`: the loop logs a fresh `request/header` (reason `series`, or `change` with `startsSeries: true` when the envelope also changed). Wrapping listeners preserve that declaration with `{ ...decision, messages }`. After assembly and `step/start`, the deprecated `agent/request` hook adjusts controls without changing the provider, model, or effort selection, and `prepareCall()` binds the adapter before the system prompt and accepted users are committed; cancellation during either async phase commits neither. Prepared call capabilities govern prompt admission, not preceding `request/context`. Each attempt refreshes registered runtime facts, reconciles the bound prompt, admits users once, and restores removed runtime context, logs header/context as needed, and freezes the request before streaming the bound call. Retries do not repeat assembly or `agent/pre-step`. Surface replacements and image-offload decisions after attachment start a new request series, including during the first resumed pre-step; unchanged resume continues the series. The system prompt travels as `system/message` history: empty text clears it, capable routes append updates, and other routes consolidate it at the first system node. [Agent-loop](../packages/core/agent-loop/README.md#understand-the-implementation) owns prompt admission and replacement details. The loop sends immutable requests with live cancellation and reuses freeze evidence for frozen identities; [agent-loop](../packages/core/agent-loop/README.md) owns construction and cancellation causes. diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index e836ab7452..42297aca67 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -111,9 +111,9 @@ turn/end 工厂 setup 在 Session 进入 SessionStore 并完成 `session/created` 之后、`agent/created` 之前运行;收尾事件在移除之前提交。 -输入通过同一个 inbox 到达驱动器;注入的上下文等待一条唤醒消息。AgentLoop 的持久 `inbox` 投影使待处理输入在没有活跃 Agent 时仍可读取。 +`inbox` 持久化待消费输入;注入上下文等待唤醒输入。重试控制输入记录 `agent/retry`,不改变轮次结算。 -循环在组装前将 Session 模型选择快照到 `AssembleContext.modelSelection` 与 `agent/pre-step.modelSelection`;重试保留该选择,省略推理强度时使用适配器默认值。`agent/pre-step` 监听器接纳、改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。组装与 `step/start` 之后,已弃用的 `agent/request` hook 在不改变提供方、模型或推理强度选择的前提下调整控制参数,`prepareCall()` 绑定适配器,再提交系统提示词与已接纳用户消息;在任一异步阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试刷新已注册运行时事实、协调绑定的提示词、仅接纳一次用户消息并恢复被移除的运行时上下文、按需记录 header/context,再冻结请求并通过绑定调用发起流式请求。重试不重复组装或 `agent/pre-step`。附接后的 surface 替换和图片省略决定开启新请求序列,包括恢复后的首次 pre-step 中发生的替换;未变化的恢复延续序列。系统提示词通过 `system/message` 历史传递:空文本清除提示词,具备能力的路由追加更新,其他路由将其归并到首个系统节点。[Agent-loop](../packages/core/agent-loop/README.zh.md#understand-the-implementation) 说明提示词准入与替换的细节。 空的 retry 来源控制输入记录 `agent/retry`,不进入模型历史;inbox 与轮次生命周期仍按普通流程处理。 +循环在组装前将 Session 模型选择快照到 `AssembleContext.modelSelection` 与 `agent/pre-step.modelSelection`;重试保留该选择,省略推理强度时使用适配器默认值。`agent/pre-step` 监听器接纳、改写或拒绝已领取消息;首次领取被拒绝或为空时,关闭不含步骤的持久轮次。enter 决策可设置 `startsRequestSeries`:循环记录新的 `request/header`(原因为 `series`,或在封装同时变化时为携带 `startsSeries: true` 的 `change`)。包装监听器通过 `{ ...decision, messages }` 保留该声明。组装与 `step/start` 之后,已弃用的 `agent/request` hook 在不改变提供方、模型或推理强度选择的前提下调整控制参数,`prepareCall()` 绑定适配器,再提交系统提示词与已接纳用户消息;在任一异步阶段取消都不会提交这两者。提示词准入依据已准备调用的能力,而非先前的 `request/context`。每次尝试刷新已注册运行时事实、协调绑定的提示词、仅接纳一次用户消息并恢复被移除的运行时上下文、按需记录 header/context,再冻结请求并通过绑定调用发起流式请求。重试不重复组装或 `agent/pre-step`。附接后的 surface 替换和图片省略决定开启新请求序列,包括恢复后的首次 pre-step 中发生的替换;未变化的恢复延续序列。系统提示词通过 `system/message` 历史传递:空文本清除提示词,具备能力的路由追加更新,其他路由将其归并到首个系统节点。[Agent-loop](../packages/core/agent-loop/README.zh.md#understand-the-implementation) 说明提示词准入与替换的细节。 循环发送不可变请求,同时保持取消有效,仅对完全冻结的对象复用冻结证据;[agent-loop](../packages/core/agent-loop/README.zh.md) 负责请求构建与取消原因。 diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index 3214b08f10..546008ca82 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -21,8 +21,8 @@ en: 9188862058c6986b zh: 9188862058c6986b /session-persistence-event-catalog/events/agent/agent-inbox-spliced-log-only: - en: 9ae614c041a27169 - zh: 8d669dd582b88f79 + en: b647f3f80ec8b407 + zh: d5e43856cfd85ddb /session-persistence-event-catalog/events/agent/agent-retry-log-only: en: 559e0484e8ae8904 zh: 8967a336e5aef50c @@ -144,8 +144,8 @@ en: 9edf4ef31e60e880 zh: 9edf4ef31e60e880 /session-persistence-event-catalog/events/model/model-selection-log-only: - en: 404c54b08c088438 - zh: eb86ed4e7d0e9228 + en: 608081c311c1bf13 + zh: bcd14effec5f6c78 /session-persistence-event-catalog/events/permission: en: 19917bc5438e60af zh: 19917bc5438e60af @@ -1545,8 +1545,8 @@ en: 930d9c64fe39857d zh: 65cd07dc3c7789db /session-persistence-event-catalog/resolved-persistence-types/inserted-outcome-removedcount-start: - en: 4edbaf2329a66619 - zh: 7d29428252d9d84f + en: 473f9e6ab2b776dc + zh: ce701e3ff6ab63ab /session-persistence-event-catalog/resolved-persistence-types/interrupted-message-step-stream: en: 8e86cd838e3e4a49 zh: f5fec2a8adf1f3cc diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 70e16e7ce7..f3cfff9e7f 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -186,7 +186,7 @@ Sources: [`packages/core/session/src/types.ts:439`](../packages/core/session/src } ``` -Source: [`packages/core/agent/src/types.ts:119`](../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:124`](../packages/core/agent/src/types.ts) @@ -194,7 +194,12 @@ Source: [`packages/core/agent/src/types.ts:119`](../packages/core/agent/src/type ```ts persistence-catalog /** An admitted retry control, correlated with its durable inbox input; never enters model history. */ -'agent/retry': { turn: number; step: number; inputId: MessageId; source: Extract } +'agent/retry': { + turn: number + step: number + inputId: MessageId + source: Extract +} ``` Types: [MessageSource](subsystems/core.md) @@ -714,7 +719,7 @@ Source: [`packages/llm/llm-retry/src/types.ts:11`](../packages/llm/llm-retry/src 'model/selection': ModelSelection ``` -Source: [`packages/core/agent/src/types.ts:130`](../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:135`](../packages/core/agent/src/types.ts) ### `permission/*` @@ -6468,7 +6473,7 @@ Sources: [`packages/core/agent/src/types.ts:113`](../packages/core/agent/src/typ SHA-256: `c66c6c3b411abf46c1f9b99e0019c10c1b6d13ee716a4b5e88f7e1b65feb3039` -Sources: [`packages/core/agent/src/types.ts:119`](../packages/core/agent/src/types.ts) +Sources: [`packages/core/agent/src/types.ts:124`](../packages/core/agent/src/types.ts) | Property | Presence | Type | |---|---|---| diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 47f1c30ed9..05543778cb 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -188,7 +188,7 @@ export type SessionEvent = { } ``` -来源:[`packages/core/agent/src/types.ts:119`](../packages/core/agent/src/types.ts) +来源:[`packages/core/agent/src/types.ts:124`](../packages/core/agent/src/types.ts) @@ -196,7 +196,12 @@ export type SessionEvent = { ```ts persistence-catalog /** An admitted retry control, correlated with its durable inbox input; never enters model history. */ -'agent/retry': { turn: number; step: number; inputId: MessageId; source: Extract } +'agent/retry': { + turn: number + step: number + inputId: MessageId + source: Extract +} ``` 类型:[MessageSource](subsystems/core.zh.md) @@ -716,7 +721,7 @@ export type SessionEvent = { 'model/selection': ModelSelection ``` -来源:[`packages/core/agent/src/types.ts:130`](../packages/core/agent/src/types.ts) +来源:[`packages/core/agent/src/types.ts:135`](../packages/core/agent/src/types.ts) ### `permission/*` @@ -6470,7 +6475,7 @@ SHA-256: `2a668a48dec1739752123d67c3bc254412d6743dbe3347365e0ed83a542f2bc3` SHA-256: `c66c6c3b411abf46c1f9b99e0019c10c1b6d13ee716a4b5e88f7e1b65feb3039` -来源:[`packages/core/agent/src/types.ts:119`](../packages/core/agent/src/types.ts) +来源:[`packages/core/agent/src/types.ts:124`](../packages/core/agent/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| diff --git a/docs/persistence-changes/historical-formats/README.i18n.yaml b/docs/persistence-changes/historical-formats/README.i18n.yaml index 40d842e063..cae6902733 100644 --- a/docs/persistence-changes/historical-formats/README.i18n.yaml +++ b/docs/persistence-changes/historical-formats/README.i18n.yaml @@ -15,8 +15,8 @@ en: 14c57f30e676f6e3 zh: b1a0ccf26fab3727 /session-persistence-formats/format-references: - en: e3511a4ac3bc4457 - zh: 828f3c39f80d5370 + en: 83fd4c4276a2600d + zh: 4f3431c960a7f8da /session-persistence-formats/scope-and-evidence: en: b220062139ddac1e zh: 7c4c0efe05dafa9c diff --git a/docs/persistence-changes/historical-formats/README.md b/docs/persistence-changes/historical-formats/README.md index 74c575b300..da1a2d629e 100644 --- a/docs/persistence-changes/historical-formats/README.md +++ b/docs/persistence-changes/historical-formats/README.md @@ -32,7 +32,7 @@ The index is generated from validated snapshots and the current writer constant. | 1 | PR #3349 | [V1](v1.md) | [JSON](v1.schema.json) | 54 / 415 | | 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.md) | [JSON](v2.schema.json) | 56 / 435 | | 3 | PR #4320 | [V3](v3.md) | [JSON](v3.schema.json) | 60 / 467 | -| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 65 / 622 | +| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 66 / 629 | diff --git a/docs/persistence-changes/historical-formats/README.zh.md b/docs/persistence-changes/historical-formats/README.zh.md index 12eb600771..8a2460c46c 100644 --- a/docs/persistence-changes/historical-formats/README.zh.md +++ b/docs/persistence-changes/historical-formats/README.zh.md @@ -32,7 +32,7 @@ description: "查找从 V0 到工作区写入器版本的每个 Session 格式 | 1 | PR #3349 | [V1](v1.zh.md) | [JSON](v1.schema.json) | 54 / 415 | | 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.zh.md) | [JSON](v2.schema.json) | 56 / 435 | | 3 | PR #4320 | [V3](v3.zh.md) | [JSON](v3.schema.json) | 60 / 467 | -| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 65 / 622 | +| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 66 / 629 | diff --git a/docs/persistence-schema.json b/docs/persistence-schema.json index 07e2ce9415..f7630fdb61 100644 --- a/docs/persistence-schema.json +++ b/docs/persistence-schema.json @@ -568,7 +568,7 @@ {"digest":"c589e938cc0e4989df8b82abef6339de621f0dc4fe62c230d145b94996af7f92","names":[],"sources":[]}, {"digest":"c592ce75aab73fcab19c1d7845684c72cf402b78d2e1f2833a58ecf9f3598ed6","names":["packages/util/values/src/index.ts#JsonValue"],"sources":["packages/util/values/src/index.ts:4"]}, {"digest":"c5bbcb3435908af2036df7105883e0a49c30ecc1fae4420aa5c4cf944066eff5","names":[],"sources":[]}, - {"digest":"c66c6c3b411abf46c1f9b99e0019c10c1b6d13ee716a4b5e88f7e1b65feb3039","names":[],"sources":["packages/core/agent/src/types.ts:119"]}, + {"digest":"c66c6c3b411abf46c1f9b99e0019c10c1b6d13ee716a4b5e88f7e1b65feb3039","names":[],"sources":["packages/core/agent/src/types.ts:124"]}, {"digest":"c748fb42c80a120038a8c587af80ec39924803c401364db11c8974d5bf6d0262","names":[],"sources":[]}, {"digest":"c8e6f48878cc2a2f0dc59682280d81f407943fb503f32c64bf9ff3d9393c3eff","names":[],"sources":[]}, {"digest":"c99e20441bf15341e9db54385e558e5c03d21dff2eb1ac08ccbf47c2d8747954","names":[],"sources":[]}, diff --git a/packages/api/session-controller/tests/empty-prompt.host.spec.ts b/packages/api/session-controller/tests/empty-prompt.host.spec.ts index 835d7ae442..50d4871ee8 100644 --- a/packages/api/session-controller/tests/empty-prompt.host.spec.ts +++ b/packages/api/session-controller/tests/empty-prompt.host.spec.ts @@ -43,9 +43,10 @@ it('records retry controls without user messages or duplicate requests', async ( const messages = events.flatMap(event => event.type === 'user/message' && event.data.source.kind === 'user' ? [event.data] : []) expect(messages).toHaveLength(1) const retries = events.flatMap(event => event.type === 'agent/retry' ? [event.data] : []) - expect(retries).toEqual([ - { turn: 2, step: 1, inputId: expect.any(String), source: { kind: 'retry', rpcId: 'continue-1' } }, - { turn: 3, step: 1, inputId: expect.any(String), source: { kind: 'retry', rpcId: 'continue-2' } }, + expect(retries).toHaveLength(2) + expect(retries).toMatchObject([ + { turn: 2, step: 1, source: { kind: 'retry', rpcId: 'continue-1' } }, + { turn: 3, step: 1, source: { kind: 'retry', rpcId: 'continue-2' } }, ]) const controls = events.flatMap(event => event.type === 'agent/inbox/spliced' ? event.data.inserted : []) .filter(message => message.source.kind === 'retry') diff --git a/scripts/smoke-python-runtime.py b/scripts/smoke-python-runtime.py index 0ea6f3831c..24bc9f15f1 100644 --- a/scripts/smoke-python-runtime.py +++ b/scripts/smoke-python-runtime.py @@ -1930,6 +1930,13 @@ def smoke_sdk_scheduler_recovery(base_url: str, executable: Path, update_snapsho retries = [record for record in records if record.get("type") == "agent/retry"] if len(prompts) != 1 or len(retries) != 1: raise AssertionError("scheduler recovery must record one original prompt and one retry control") + retry_id = retries[0]["data"]["inputId"] + queued_retry_ids = [message.get("id") for record in records + if record.get("type") == "agent/inbox/spliced" + for message in record["data"]["inserted"] + if message.get("source", {}).get("kind") == "retry"] + if not isinstance(retry_id, str) or queued_retry_ids != [retry_id]: + raise AssertionError("scheduler recovery lost the retry control's durable inbox identity") calls = [record["data"]["callId"] for record in records if record.get("type") == "tool/call"] if calls != list(RECOVERY_CALL_IDS[:2]): raise AssertionError(f"scheduler recovery dispatched the unstarted call: {calls}") @@ -1946,6 +1953,7 @@ def smoke_sdk_scheduler_recovery(base_url: str, executable: Path, update_snapsho replacements = [ *snapshot_directory_replacements(root, "{{cwd}}"), (RECOVERY_SESSION_ID, "{{parent}}"), + (retry_id, "{{messageId}}"), ] result_value = [{ "session_id": result.session_id, From 22db5af493661e9da3c37d3915ae58029052dd39 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Tue, 27 Oct 2026 12:15:57 +0800 Subject: [PATCH 12/65] docs(agent): record retry control compatibility and validation --- .../2026-10-27-retry-control-input.i18n.yaml | 8 ++++---- .../persistence-changes/2026-10-27-retry-control-input.md | 4 ++-- .../2026-10-27-retry-control-input.zh.md | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/persistence-changes/2026-10-27-retry-control-input.i18n.yaml b/docs/persistence-changes/2026-10-27-retry-control-input.i18n.yaml index 3513ba5aaa..502105b258 100644 --- a/docs/persistence-changes/2026-10-27-retry-control-input.i18n.yaml +++ b/docs/persistence-changes/2026-10-27-retry-control-input.i18n.yaml @@ -18,11 +18,11 @@ en: 6907136f28e48c90 zh: 02f8298485f3d8e7 /2026-10-27-retry-control-input/compatibility: - en: 4487a595853a65d6 - zh: d9b356c77ccab559 + en: ffc72b74956201d6 + zh: 78cfe9d2353ca739 /2026-10-27-retry-control-input/verification: - en: da37e5d4769c40fe - zh: 992f66b1a2c66fb2 + en: fbd40f2d4da56e1e + zh: 82546f33ad58b08b /2026-10-27-retry-control-input/dev-note: en: b75c8458928b72b9 zh: 003ba4e17b4b5269 diff --git a/docs/persistence-changes/2026-10-27-retry-control-input.md b/docs/persistence-changes/2026-10-27-retry-control-input.md index 3faecc4503..10ebe9fe04 100644 --- a/docs/persistence-changes/2026-10-27-retry-control-input.md +++ b/docs/persistence-changes/2026-10-27-retry-control-input.md @@ -51,12 +51,12 @@ changes: ## Compatibility -Updated readers continue accepting older logs with no retry controls. Older readers reject the unknown required agent/retry event before treating a retry as ordinary user content. The retry discriminator also distinguishes queued controls from ordinary empty user messages. API and both SDK consumers are updated together; the current Session format version is retained because event and source discriminators prevent unsafe interpretation. +Updated readers preserve older logs and ordinary empty user-message admission. Older readers refuse the new required agent/retry event before append or tail repair. A queued control predating acceptance still has the existing identified empty UserMessage representation; an older Loop can consume it using its ordinary empty-input behavior, retaining inbox identity and adding no user text. API and both SDK consumers recognize retry controls and the acceptance event together. The current Session format version is retained; mixed ordinary messages and retry events remain readable by the updated reader. ## Verification -Source review confirms controls retain existing inbox and turn lifecycle while agent/retry has no model-surface operation. Regression coverage covers repeated retry, input-id correlation, balanced turns, ordinary empty messages, non-empty retry rejection, deduplication, checkpoint replay, browser behavior, and both SDK recovery recordings. Execution is pending post-push validation. +Focused AgentLoop, Agent, Session API, and SDK-server tests passed: 91 files, 1806 tests, with 100% statements, branches, functions, and lines for agent.ts and continuation.ts. Browser continuation replay and TypeScript/Python SDK scheduler-recovery replay passed; assertions retain one original user message, correlate retry input identity, preserve tool results, and close new turns normally. Current-event and native-source admission tests cover unknown required-event refusal without mutation and preservation of extensible source metadata. Build, aggregate checks, and typed lint passed. doc-sync passed 42 gates; its remaining repository-reference failure is the unchanged python/sdk/uv.lock:113 commit hash. ## Dev Note diff --git a/docs/persistence-changes/2026-10-27-retry-control-input.zh.md b/docs/persistence-changes/2026-10-27-retry-control-input.zh.md index 672f1d032c..5729a666e7 100644 --- a/docs/persistence-changes/2026-10-27-retry-control-input.zh.md +++ b/docs/persistence-changes/2026-10-27-retry-control-input.zh.md @@ -51,12 +51,12 @@ changes: ## 兼容性 -更新后的读取方继续接受没有重试控制输入的旧日志。旧读取方会拒绝未知必读 agent/retry 事件,不会将重试当作普通用户内容。retry 判别标签也将排队控制输入与普通空用户消息区分开。API 与两套 SDK 消费方同步更新;事件及来源判别标签可阻止不安全解释,因此保留当前 Session 格式版本。 +更新后的读取方保留旧日志含义和普通空用户消息准入。旧读取方会在追加或尾部修复前拒绝新的必读 agent/retry 事件。接受前的排队控制输入仍采用现有带身份的空 UserMessage 表示;旧 Loop 可按其普通空输入行为消费,保留 inbox 身份且不增加用户文本。API 与两套 SDK 消费方同步识别重试控制输入及接受事件。保留当前 Session 格式版本;更新后的读取方可读取普通消息与重试事件混合的日志。 ## 验证 -源码审查确认控制输入保留现有 inbox 和轮次生命周期,agent/retry 不携带模型表面操作。回归用例覆盖连续重试、输入身份关联、轮次闭合、普通空消息、拒绝非空重试、去重、检查点回放、浏览器行为及两套 SDK 恢复录制。执行验证将在 push 后进行。 +AgentLoop、Agent、Session API 与 SDK 服务端定向测试通过:91 个文件、1806 个测试,agent.ts 和 continuation.ts 的语句、分支、函数及行覆盖率均为 100%。浏览器继续回放与 TypeScript/Python SDK scheduler-recovery 回放通过;断言保留一条原始用户消息、关联重试输入身份、保留工具结果且新轮次正常闭合。当前事件与原生来源准入测试覆盖未知必读事件在不修改数据的情况下拒绝,以及保留可扩展来源元数据。构建、聚合检查和类型感知 lint 通过。doc-sync 有 42 项通过;剩余仓库引用失败来自未改动的 python/sdk/uv.lock:113 提交哈希。 ## 开发备注 From 3be7d56c6d27f7a71447262a85a6c1bcfb7053f2 Mon Sep 17 00:00:00 2001 From: Turtle Date: Wed, 28 Oct 2026 15:22:54 +0800 Subject: [PATCH 13/65] fix(session-controller): give repeated forks distinct titles Fixes #7471 Co-authored-by: imccyu <276526105+imccyu@users.noreply.github.com> --- .../api/session-controller/README.i18n.yaml | 4 ++-- packages/api/session-controller/README.md | 2 +- packages/api/session-controller/README.zh.md | 2 +- .../src/client/contract/sessions.ts | 3 ++- .../src/client/sessions/service.ts | 14 ++++++++++-- .../tests/sessions-service.client.spec.ts | 22 +++++++++++++++++++ .../src/client/api-catalog.ts | 2 +- 7 files changed, 41 insertions(+), 8 deletions(-) diff --git a/packages/api/session-controller/README.i18n.yaml b/packages/api/session-controller/README.i18n.yaml index 010c41bf94..bb4a228804 100644 --- a/packages/api/session-controller/README.i18n.yaml +++ b/packages/api/session-controller/README.i18n.yaml @@ -15,8 +15,8 @@ en: 1fdf707322fb6996 zh: 0204f7e9d4aa3cce /session-controller/use-this-package: - en: d3d53e899960d96e - zh: 16b2bc7fe74215d8 + en: 4e5e6899044ebd61 + zh: c6da8d03641e56e9 /session-controller/client-references: en: 41bdda1ac32df558 zh: aa5d0e325924066b diff --git a/packages/api/session-controller/README.md b/packages/api/session-controller/README.md index 6fd7b24561..2e1bd3e6df 100644 --- a/packages/api/session-controller/README.md +++ b/packages/api/session-controller/README.md @@ -64,7 +64,7 @@ During uninterrupted following, transcript and steering echoes stay in Chat thro The user-invocable `skills/list` metadata includes the winning provider’s optional instruction-file `path`. The composer can preview that file without loading every skill body or activating a cold Agent. -Fork copies the exact inclusive event prefix selected by `atSeq`, including a cut inside an open turn, into an independent Session without `parentSession`. Client placeholders likewise omit `parentSessionId`; list indentation applies only to subagent-origin rows, including when historical ordinary forks retain a parent id. Workspace attachment and optional title increments use the source selected for the operation. The child records its inherited marker before synthetic fork results and closing events. Omitting `atSeq` selects the latest completed turn and its standalone tail, stopping before the next turn or queued input; a nonexistent event is rejected. The chat action selects a completed turn. +Fork copies the exact inclusive event prefix selected by `atSeq`, including a cut inside an open turn, into an independent Session without `parentSession`. Client placeholders likewise omit `parentSessionId`; list indentation applies only to subagent-origin rows, including when historical ordinary forks retain a parent id. Workspace attachment and optional title increments use the source selected for the operation; an increment skips numbers that other listed titles already use. The child records its inherited marker before synthetic fork results and closing events. Omitting `atSeq` selects the latest completed turn and its standalone tail, stopping before the next turn or queued input; a nonexistent event is rejected. The chat action selects a completed turn. A resume blocked by an existing write handle returns `session/writer-held` with the Session id; other resume failures retain `gateway/internal`. diff --git a/packages/api/session-controller/README.zh.md b/packages/api/session-controller/README.zh.md index 8729f555ba..d4a946d8ce 100644 --- a/packages/api/session-controller/README.zh.md +++ b/packages/api/session-controller/README.zh.md @@ -64,7 +64,7 @@ Session 对象还承载本地提交回显:`session.beginSubmission` 在调用 面向用户调用的 `skills/list` 元数据包含胜出提供方可选的指令文件 `path`。输入框可据此预览文件,无需加载每个 skill 的正文或激活冷态 Agent。 -Fork 将 `atSeq` 选定的含边界事件前缀(包括开放轮次内的截点)复制到不带 `parentSession` 的独立 Session。客户端占位项同样省略 `parentSessionId`;列表缩进仅适用于 subagent 来源的行,历史普通 fork 即使保留父 ID 也不缩进。工作区绑定及可选标题递增使用本次操作选定的源会话。子会话在合成的 fork 结果和结束事件之前记录继承标记。省略 `atSeq` 时选择最近已结束轮次及其独立尾部,在下一轮次或排队输入之前停止;不存在的事件会被拒绝。聊天操作选择已结束轮次。 +Fork 将 `atSeq` 选定的含边界事件前缀(包括开放轮次内的截点)复制到不带 `parentSession` 的独立 Session。客户端占位项同样省略 `parentSessionId`;列表缩进仅适用于 subagent 来源的行,历史普通 fork 即使保留父 ID 也不缩进。工作区绑定及可选标题递增使用本次操作选定的源会话;递增时会跳过其他已列出标题已使用的编号。子会话在合成的 fork 结果和结束事件之前记录继承标记。省略 `atSeq` 时选择最近已结束轮次及其独立尾部,在下一轮次或排队输入之前停止;不存在的事件会被拒绝。聊天操作选择已结束轮次。 恢复会话时若已有写句柄占用,返回 `session/writer-held`,并携带会话 id;其他恢复失败仍返回 `gateway/internal`。 diff --git a/packages/api/session-controller/src/client/contract/sessions.ts b/packages/api/session-controller/src/client/contract/sessions.ts index a52d2c155a..52764f83a0 100644 --- a/packages/api/session-controller/src/client/contract/sessions.ts +++ b/packages/api/session-controller/src/client/contract/sessions.ts @@ -128,7 +128,8 @@ export interface ISessions { * seq (a real event seq the caller already knows; a cut inside an open * turn is balanced Host-side with synthetic closers, and omission selects * the latest completed-turn prefix), and whether to increment an - * inherited durable title before resolving. `onCreated` observes the catalogued child before that optional rename. + * inherited durable title, skipping numbers listed titles already use, + * before resolving. `onCreated` observes the catalogued child before that optional rename. * @returns the child session id. * @throws when the fork fails, or when a requested child-title rename fails after creation. */ diff --git a/packages/api/session-controller/src/client/sessions/service.ts b/packages/api/session-controller/src/client/sessions/service.ts index 4b70888468..282086b7de 100644 --- a/packages/api/session-controller/src/client/sessions/service.ts +++ b/packages/api/session-controller/src/client/sessions/service.ts @@ -132,10 +132,18 @@ function displayTitleOf(title: string | undefined, cwd: string | undefined, id: /** * Increment a trailing fork number while preserving its half-width or * full-width parentheses; an unnumbered title starts with ` (1)`. + * The number keeps increasing while the candidate is already taken. * @param title - source session's durable title. + * @param taken - durable titles of listed sessions. * @returns the title assigned to the fork child. */ -function increasedForkTitle(title: string): string { +function increasedForkTitle(title: string, taken: ReadonlySet): string { + let next = incrementedTitle(title) + while (taken.has(next)) next = incrementedTitle(next) + return next +} + +function incrementedTitle(title: string): string { const ascii = /^(.*?)\((\d+)\)$/u.exec(title) if (ascii?.[1] !== undefined && ascii[2] !== undefined) { return `${ascii[1]}(${BigInt(ascii[2]) + 1n})` @@ -480,7 +488,9 @@ export class ClientSessions implements ISessions { const childId = result.value.sessionId opts.onCreated?.(childId) if (sourceTitle !== undefined) { - const renamed = await this.manager.rename(childId, increasedForkTitle(sourceTitle)) + const list = this.list.getSnapshot() + const taken = new Set(list.ids.flatMap(id => list.byId[id]?.title ?? [])) + const renamed = await this.manager.rename(childId, increasedForkTitle(sourceTitle, taken)) if (!renamed.ok) throw new Error(`fork child rename failed: ${renamed.error.code}: ${renamed.error.message}`) } return childId diff --git a/packages/api/session-controller/tests/sessions-service.client.spec.ts b/packages/api/session-controller/tests/sessions-service.client.spec.ts index 212320fdf6..3043be5b2a 100644 --- a/packages/api/session-controller/tests/sessions-service.client.spec.ts +++ b/packages/api/session-controller/tests/sessions-service.client.spec.ts @@ -1010,6 +1010,28 @@ describe('fork', () => { }) }) + it('gives repeated forks distinct titles past numbers listed sessions use', async ({ bench }) => { + const b = bench() + for (const [id, value] of [['source', 'Roadmap'], ['sibling', 'Roadmap (2)']] as const) { + b.svc.handleControlFrame({ type: 'projection', sessionId: sid(id), key: 'title', value, seq: 2 }) + } + await feedList(b, [{ id: 'source' }, { id: 'sibling' }]) + b.mock.remote.session.rename.mockImplementation((payload) => { + const { title } = payload as { title: string } + return Promise.resolve(ok({ title, seq: 3 })) + }) + + for (const child of ['child-1', 'child-2']) { + b.mock.remote.session.fork.mockResolvedValueOnce(ok({ sessionId: sid(child) })) + await b.svc.fork({ sessionId: sid('source'), increaseTitle: true }) + } + + expect(b.mock.remote.session.rename.mock.calls).toEqual([ + [{ sessionId: 'child-1', title: 'Roadmap (1)' }], + [{ sessionId: 'child-2', title: 'Roadmap (3)' }], + ]) + }) + it('sends the exact boundary seq verbatim: callers pass real event seqs', async ({ bench }) => { const b = bench() await feedList(b, [{ id: 'source', cwd: '/work' }]) diff --git a/packages/extensions/cordis-client-runner/src/client/api-catalog.ts b/packages/extensions/cordis-client-runner/src/client/api-catalog.ts index 8777befdb0..3662cd2eb5 100644 --- a/packages/extensions/cordis-client-runner/src/client/api-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/api-catalog.ts @@ -215,7 +215,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ { signature: 'fork(opts: { sessionId: SessionId atSeq?: number increaseTitle?: boolean onCreated?: (childId: SessionId) => void /** False refuses source migration; omission preserves the Host default. */ allowMigration?: boolean }): Promise', description: 'Fork a session from an exact inclusive prefix of the source; on resolution the child is catalogued and can be explicitly retained.', - parameters: [{ name: 'opts', description: 'source session id, the optional exact inclusive boundary seq (a real event seq the caller already knows; a cut inside an open turn is balanced Host-side with synthetic closers, and omission selects the latest completed-turn prefix), and whether to increment an inherited durable title before resolving. `onCreated` observes the catalogued child before that optional rename.' }], + parameters: [{ name: 'opts', description: 'source session id, the optional exact inclusive boundary seq (a real event seq the caller already knows; a cut inside an open turn is balanced Host-side with synthetic closers, and omission selects the latest completed-turn prefix), and whether to increment an inherited durable title, skipping numbers listed titles already use, before resolving. `onCreated` observes the catalogued child before that optional rename.' }], returns: 'the child session id.', throws: ['when the fork fails, or when a requested child-title rename fails after creation.'], }, From 8818fdd2e0a073847eae48335fb76830df3b3289 Mon Sep 17 00:00:00 2001 From: Turtle Date: Wed, 28 Oct 2026 15:35:43 +0800 Subject: [PATCH 14/65] docs(session-controller): document fork title limits Co-authored-by: imccyu <276526105+imccyu@users.noreply.github.com> --- packages/api/session-controller/README.i18n.yaml | 8 ++++---- packages/api/session-controller/README.md | 3 ++- packages/api/session-controller/README.zh.md | 3 ++- .../src/client/contract/sessions.ts | 2 +- .../src/client/sessions/service.ts | 15 ++++++++++----- .../tests/sessions-service.client.spec.ts | 1 + .../src/client/api-catalog.ts | 2 +- 7 files changed, 21 insertions(+), 13 deletions(-) diff --git a/packages/api/session-controller/README.i18n.yaml b/packages/api/session-controller/README.i18n.yaml index bb4a228804..2df5bf6abc 100644 --- a/packages/api/session-controller/README.i18n.yaml +++ b/packages/api/session-controller/README.i18n.yaml @@ -15,8 +15,8 @@ en: 1fdf707322fb6996 zh: 0204f7e9d4aa3cce /session-controller/use-this-package: - en: 4e5e6899044ebd61 - zh: c6da8d03641e56e9 + en: eb59401ac9f8489f + zh: 7ab1650c6df67461 /session-controller/client-references: en: 41bdda1ac32df558 zh: aa5d0e325924066b @@ -33,8 +33,8 @@ en: 22fc0c9dbedd3bb6 zh: fad040ca55fd0e0a /session-controller/known-limitations-and-deferred-work: - en: 5db0f9f69a8be926 - zh: 6ba1cc4a3850fcbc + en: 23bc3db43e84daa6 + zh: 0242338ac7bd24fa /session-controller/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: e88aeb617ffb74ea diff --git a/packages/api/session-controller/README.md b/packages/api/session-controller/README.md index 2e1bd3e6df..9f7878c3ba 100644 --- a/packages/api/session-controller/README.md +++ b/packages/api/session-controller/README.md @@ -64,7 +64,7 @@ During uninterrupted following, transcript and steering echoes stay in Chat thro The user-invocable `skills/list` metadata includes the winning provider’s optional instruction-file `path`. The composer can preview that file without loading every skill body or activating a cold Agent. -Fork copies the exact inclusive event prefix selected by `atSeq`, including a cut inside an open turn, into an independent Session without `parentSession`. Client placeholders likewise omit `parentSessionId`; list indentation applies only to subagent-origin rows, including when historical ordinary forks retain a parent id. Workspace attachment and optional title increments use the source selected for the operation; an increment skips numbers that other listed titles already use. The child records its inherited marker before synthetic fork results and closing events. Omitting `atSeq` selects the latest completed turn and its standalone tail, stopping before the next turn or queued input; a nonexistent event is rejected. The chat action selects a completed turn. +Fork copies the exact inclusive event prefix selected by `atSeq`, including a cut inside an open turn, into an independent Session without `parentSession`. Client placeholders likewise omit `parentSessionId`; list indentation applies only to subagent-origin rows, including when historical ordinary forks retain a parent id. Workspace attachment and optional title increments use the source selected for the operation; an increment skips numbers that other Host-listed titles already use. The child records its inherited marker before synthetic fork results and closing events. Omitting `atSeq` selects the latest completed turn and its standalone tail, stopping before the next turn or queued input; a nonexistent event is rejected. The chat action selects a completed turn. A resume blocked by an existing write handle returns `session/writer-held` with the Session id; other resume failures retain `gateway/internal`. @@ -127,6 +127,7 @@ No direct effect; model requests remain owned by the Agent and LLM packages. - The image byte cap does not validate decoded dimensions or pixel count. - A failed follow resumption remains visible to the caller instead of retrying indefinitely. - The raw browser upload is one streaming HTTP request without resumable offsets; a retry sends the file again from byte zero. +- Fork title increments compare candidates with the Client list before the Host normalizes them: concurrent forks of one source, or a title the Host truncates at its byte limit, can still produce duplicate titles. - File-reference completion uses the shared Agent lookup and can resume a cold Session; the `skills/list` catalog is the non-activating alternative for skill metadata. - Accepted/running display memory lives only in Client memory and is lost on page reload. - That memory is not shared between tabs: one tab can show a converted row while another still shows `New Session` for the same Session. diff --git a/packages/api/session-controller/README.zh.md b/packages/api/session-controller/README.zh.md index d4a946d8ce..0799531e63 100644 --- a/packages/api/session-controller/README.zh.md +++ b/packages/api/session-controller/README.zh.md @@ -64,7 +64,7 @@ Session 对象还承载本地提交回显:`session.beginSubmission` 在调用 面向用户调用的 `skills/list` 元数据包含胜出提供方可选的指令文件 `path`。输入框可据此预览文件,无需加载每个 skill 的正文或激活冷态 Agent。 -Fork 将 `atSeq` 选定的含边界事件前缀(包括开放轮次内的截点)复制到不带 `parentSession` 的独立 Session。客户端占位项同样省略 `parentSessionId`;列表缩进仅适用于 subagent 来源的行,历史普通 fork 即使保留父 ID 也不缩进。工作区绑定及可选标题递增使用本次操作选定的源会话;递增时会跳过其他已列出标题已使用的编号。子会话在合成的 fork 结果和结束事件之前记录继承标记。省略 `atSeq` 时选择最近已结束轮次及其独立尾部,在下一轮次或排队输入之前停止;不存在的事件会被拒绝。聊天操作选择已结束轮次。 +Fork 将 `atSeq` 选定的含边界事件前缀(包括开放轮次内的截点)复制到不带 `parentSession` 的独立 Session。客户端占位项同样省略 `parentSessionId`;列表缩进仅适用于 subagent 来源的行,历史普通 fork 即使保留父 ID 也不缩进。工作区绑定及可选标题递增使用本次操作选定的源会话;递增时会跳过其他由 Host 列出的标题已使用的编号。子会话在合成的 fork 结果和结束事件之前记录继承标记。省略 `atSeq` 时选择最近已结束轮次及其独立尾部,在下一轮次或排队输入之前停止;不存在的事件会被拒绝。聊天操作选择已结束轮次。 恢复会话时若已有写句柄占用,返回 `session/writer-held`,并携带会话 id;其他恢复失败仍返回 `gateway/internal`。 @@ -127,6 +127,7 @@ Blockquote 作为普通 prompt 文本传递,不携带 quote 元数据。本地 - 图片字节上限不校验解码后的尺寸或像素数。 - follow 恢复失败会对调用方可见,而不会无限重试。 - 浏览器原始字节上传使用一次不带断点续传偏移的流式 HTTP 请求;重试会从第零字节重新传输整个文件。 +- Fork 标题递增会在 Host 规范化候选标题之前,先将其与客户端列表比较:同一来源的并发 Fork,或被 Host 按字节上限截断的标题,仍可能产生重复标题。 - 文件引用补全使用共享 Agent lookup,因此可能恢复冷 Session;`skills/list` 目录是不激活 Agent 的 skill 元数据读取路径。 - 受理/运行的展示记忆只存在于客户端内存,页面重载后即丢失。 - 该记忆不在 tab 之间共享:同一会话可能在一个 tab 中已转正,在另一个 tab 中仍显示为 `New Session`。 diff --git a/packages/api/session-controller/src/client/contract/sessions.ts b/packages/api/session-controller/src/client/contract/sessions.ts index 52764f83a0..c1fede0937 100644 --- a/packages/api/session-controller/src/client/contract/sessions.ts +++ b/packages/api/session-controller/src/client/contract/sessions.ts @@ -128,7 +128,7 @@ export interface ISessions { * seq (a real event seq the caller already knows; a cut inside an open * turn is balanced Host-side with synthetic closers, and omission selects * the latest completed-turn prefix), and whether to increment an - * inherited durable title, skipping numbers listed titles already use, + * inherited durable title, skipping numbers Host-listed titles already use, * before resolving. `onCreated` observes the catalogued child before that optional rename. * @returns the child session id. * @throws when the fork fails, or when a requested child-title rename fails after creation. diff --git a/packages/api/session-controller/src/client/sessions/service.ts b/packages/api/session-controller/src/client/sessions/service.ts index 282086b7de..86b1ad4757 100644 --- a/packages/api/session-controller/src/client/sessions/service.ts +++ b/packages/api/session-controller/src/client/sessions/service.ts @@ -130,11 +130,9 @@ function displayTitleOf(title: string | undefined, cwd: string | undefined, id: } /** - * Increment a trailing fork number while preserving its half-width or - * full-width parentheses; an unnumbered title starts with ` (1)`. - * The number keeps increasing while the candidate is already taken. + * Increment the fork number until the candidate is not taken. * @param title - source session's durable title. - * @param taken - durable titles of listed sessions. + * @param taken - durable titles of Host-listed sessions. * @returns the title assigned to the fork child. */ function increasedForkTitle(title: string, taken: ReadonlySet): string { @@ -143,6 +141,12 @@ function increasedForkTitle(title: string, taken: ReadonlySet): string { return next } +/** + * Increment a trailing fork number while preserving its half-width or + * full-width parentheses; an unnumbered title starts with ` (1)`. + * @param title - title to increment. + * @returns the title with the next fork number. + */ function incrementedTitle(title: string): string { const ascii = /^(.*?)\((\d+)\)$/u.exec(title) if (ascii?.[1] !== undefined && ascii[2] !== undefined) { @@ -463,7 +467,8 @@ export class ClientSessions implements ISessions { * seq (a real event seq the caller already knows; a cut inside an open * turn is balanced Host-side with synthetic closers, and omission selects * the latest completed-turn prefix), and whether to increment an - * inherited durable title before resolving, plus permission to start source migration. + * inherited durable title, skipping numbers Host-listed titles already use, + * before resolving, plus permission to start source migration. * @returns the child session id. * @throws {SessionForkError} with the source id. * @throws {Error} when a requested child-title rename fails after creation. diff --git a/packages/api/session-controller/tests/sessions-service.client.spec.ts b/packages/api/session-controller/tests/sessions-service.client.spec.ts index 3043be5b2a..effb9ee1b6 100644 --- a/packages/api/session-controller/tests/sessions-service.client.spec.ts +++ b/packages/api/session-controller/tests/sessions-service.client.spec.ts @@ -1030,6 +1030,7 @@ describe('fork', () => { [{ sessionId: 'child-1', title: 'Roadmap (1)' }], [{ sessionId: 'child-2', title: 'Roadmap (3)' }], ]) + expect(b.svc.list.getSnapshot().byId[sid('child-2')]?.title).toBe('Roadmap (3)') }) it('sends the exact boundary seq verbatim: callers pass real event seqs', async ({ bench }) => { diff --git a/packages/extensions/cordis-client-runner/src/client/api-catalog.ts b/packages/extensions/cordis-client-runner/src/client/api-catalog.ts index 3662cd2eb5..56780cfe72 100644 --- a/packages/extensions/cordis-client-runner/src/client/api-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/api-catalog.ts @@ -215,7 +215,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ { signature: 'fork(opts: { sessionId: SessionId atSeq?: number increaseTitle?: boolean onCreated?: (childId: SessionId) => void /** False refuses source migration; omission preserves the Host default. */ allowMigration?: boolean }): Promise', description: 'Fork a session from an exact inclusive prefix of the source; on resolution the child is catalogued and can be explicitly retained.', - parameters: [{ name: 'opts', description: 'source session id, the optional exact inclusive boundary seq (a real event seq the caller already knows; a cut inside an open turn is balanced Host-side with synthetic closers, and omission selects the latest completed-turn prefix), and whether to increment an inherited durable title, skipping numbers listed titles already use, before resolving. `onCreated` observes the catalogued child before that optional rename.' }], + parameters: [{ name: 'opts', description: 'source session id, the optional exact inclusive boundary seq (a real event seq the caller already knows; a cut inside an open turn is balanced Host-side with synthetic closers, and omission selects the latest completed-turn prefix), and whether to increment an inherited durable title, skipping numbers Host-listed titles already use, before resolving. `onCreated` observes the catalogued child before that optional rename.' }], returns: 'the child session id.', throws: ['when the fork fails, or when a requested child-title rename fails after creation.'], }, From ff563d07e07e78a882b376c674254a7aedfbbc51 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Mon, 2 Nov 2026 14:58:32 +0800 Subject: [PATCH 15/65] feat(subagent): name subagents in their creator catalog Models coordinate subagents through send_message, interrupt_agent, list_agents, and Team task owners, but every one of those addressed a member by a 36-character Session id. Each new child now has a name, unique among its creator's children, recorded as a field of the subagent/catalog entry the creator already appends (payload version 3). The Session id stays the only execution identity for routing, authorization, persistence, task ownership, and navigation. A name is a model-facing reference: 1-32 lower-case letters, digits, or hyphens starting with a letter, never `lead`, so a UUID never parses as one. Omission selects the first free `agent-N`; an explicit duplicate or invalid name fails before startup. The creator reserves the name synchronously at admission and releases it when the catalog entry commits or startup fails, so no second write, rollback protocol, or optional naming service exists. The root is `lead`, and a descendant's full name is its path from the root without a `lead/` prefix, such as `reviewer/checker`. Sibling-unique segments make full names tree-unique without a root registry, and cold resume rebuilds a member's path from the catalogs along its creation chain. A member below an unnamed historical creator keeps its Session id as its name. resolveName() reads only the catalogs along the path and grants no authority. Tools, runtime-authored text, and the Web client consume names: delegation takes an optional name and returns { kind, subagentId, name }; send_message and interrupt_agent take `agent`; list_agents rows carry name, status, and description; Team task results report the owner's name while storage keeps ownerId; identity runtime context, message framing, and settlement notices use full names. Recorded sessions, CLI and Web expectations, the Python runtime snapshots, bilingual docs, an upgrade guide, and an Agent Note follow. --- ...7-adjacent-agent-steer-messaging.i18n.yaml | 8 +- ...26-08-27-adjacent-agent-steer-messaging.md | 6 +- ...08-27-adjacent-agent-steer-messaging.zh.md | 6 +- ...01-parent-owned-subagent-catalog.i18n.yaml | 4 +- ...026-09-01-parent-owned-subagent-catalog.md | 4 +- ...-09-01-parent-owned-subagent-catalog.zh.md | 4 +- ...13-team-identity-runtime-context.i18n.yaml | 4 +- ...026-10-13-team-identity-runtime-context.md | 2 + ...-10-13-team-identity-runtime-context.zh.md | 2 + ...6-10-15-agent-teams-as-subagents.i18n.yaml | 4 +- .../2026-10-15-agent-teams-as-subagents.md | 2 +- .../2026-10-15-agent-teams-as-subagents.zh.md | 2 +- ...16-tree-wide-agent-communication.i18n.yaml | 4 +- ...026-10-16-tree-wide-agent-communication.md | 4 +- ...-10-16-tree-wide-agent-communication.zh.md | 4 +- ...ubagent-tool-owned-communication.i18n.yaml | 4 +- ...10-22-subagent-tool-owned-communication.md | 2 +- ...22-subagent-tool-owned-communication.zh.md | 2 +- ...026-11-02-subagent-catalog-names.i18n.yaml | 22 +++ .../2026-11-02-subagent-catalog-names.md | 45 ++++++ .../2026-11-02-subagent-catalog-names.zh.md | 45 ++++++ .../goal-round-driver/session.expected.jsonl | 2 +- .../goal-wrapup/session.expected.jsonl | 2 +- .../acp/tests/image-offload.expected.e2e.ts | 12 +- .../goal-tools/stream-json.expected.jsonl | 2 +- .../headless-profile/session.expected.jsonl | 2 +- .../stream-json.expected.jsonl | 2 +- .../stream-json.expected.jsonl | 2 +- .../provider-retry/stream-json.expected.jsonl | 2 +- .../session.expected.jsonl | 2 +- .../subagent-diagnostic/parent.expected.jsonl | 4 +- .../subagent-inheritance/child.expected.jsonl | 2 +- .../parent.expected.jsonl | 10 +- .../subagent-settlement/child.expected.jsonl | 2 +- .../stream-json.expected.jsonl | 10 +- .../offline-edit/session.expected.jsonl | 2 +- .../precedence-change/session.expected.jsonl | 2 +- .../headless/tests/fixtures/team-llm.mjs | 6 +- .../headless/tests/headless.expected.e2e.ts | 20 +-- .../tests/subagent-diagnostic.expected.e2e.ts | 4 +- apps/web/tests/agent-team-panel.e2e.ts | 4 +- .../navigation-only.expected.md | 4 +- .../subagent-result-notice/en.expected.md | 12 +- .../subagent-result-notice/zh.expected.md | 12 +- .../web/tests/official-subagent-bundle.e2e.ts | 2 +- apps/web/tests/shipped-composition.e2e.ts | 2 +- .../agent-team-panel/task.expected.md | 2 +- apps/web/tests/subagent-conversation.e2e.ts | 16 ++- apps/web/tests/subagent-result-notice.e2e.ts | 4 +- docs/config-catalog.md | 8 +- docs/config-catalog.zh.md | 8 +- docs/event-producer-consumer.md | 8 +- docs/event-producer-consumer.zh.md | 8 +- docs/persistence-catalog.i18n.yaml | 51 ++++--- docs/persistence-catalog.md | 58 ++++++-- docs/persistence-catalog.zh.md | 58 ++++++-- ...026-11-02-subagent-catalog-names.i18n.yaml | 28 ++++ .../2026-11-02-subagent-catalog-names.md | 48 +++++++ ...6-11-02-subagent-catalog-names.schema.json | 31 +++++ .../2026-11-02-subagent-catalog-names.zh.md | 48 +++++++ .../historical-formats/README.i18n.yaml | 4 +- .../historical-formats/README.md | 2 +- .../historical-formats/README.zh.md | 2 +- docs/persistence-schema.json | 16 ++- docs/subsystems/agent-team.i18n.yaml | 4 +- docs/subsystems/agent-team.md | 2 +- docs/subsystems/agent-team.zh.md | 2 +- docs/subsystems/subagent.i18n.yaml | 7 +- docs/subsystems/subagent.md | 52 ++++++- docs/subsystems/subagent.zh.md | 52 ++++++- docs/tool-catalog.i18n.yaml | 18 +-- docs/tool-catalog.md | 34 +++-- docs/tool-catalog.zh.md | 36 +++-- .../subagent-names/guide.i18n.yaml | 16 +++ .../v0.2.2-alpha.2/subagent-names/guide.md | 29 ++++ .../v0.2.2-alpha.2/subagent-names/guide.zh.md | 29 ++++ packages/bundle/base/README.i18n.yaml | 4 +- packages/bundle/base/README.md | 2 +- packages/bundle/base/README.zh.md | 2 +- .../client/ui-agent-team/README.i18n.yaml | 4 +- packages/client/ui-agent-team/README.md | 2 +- packages/client/ui-agent-team/README.zh.md | 2 +- .../ui-agent-team/src/client/TeamBoard.tsx | 9 +- .../ui-agent-team/src/client/owner-name.ts | 34 +++++ .../tests/owner-name.client.spec.ts | 61 ++++++++ .../tests/team-board.client.spec.tsx | 34 +++-- packages/client/ui-subagent/README.i18n.yaml | 8 +- packages/client/ui-subagent/README.md | 4 +- packages/client/ui-subagent/README.zh.md | 4 +- .../src/client/SubagentHeaderLineage.tsx | 12 +- .../tests/conversation-ui.client.spec.tsx | 36 +++++ packages/client/ui-tool/README.i18n.yaml | 8 +- packages/client/ui-tool/README.md | 6 +- packages/client/ui-tool/README.zh.md | 6 +- .../tool/models/control-details-model.ts | 13 +- .../client/tool/models/detail-model-shared.ts | 2 +- .../tests/details-models.client.spec.ts | 53 +++++-- .../ui-tool/tests/details-row.client.spec.tsx | 12 +- .../tests/loader-composition.spec.ts | 2 +- .../tool-subagent-oar/README.i18n.yaml | 20 +-- .../experimental/tool-subagent-oar/README.md | 10 +- .../tool-subagent-oar/README.zh.md | 10 +- .../tool-subagent-oar/src/index.ts | 14 +- .../tests/tool-subagent-oar.spec.ts | 16 ++- .../tests/vfs-example-fixture.ts | 4 +- .../extensions/tool-cordis/src/api-catalog.ts | 28 +++- .../session-format-v3-to-v4/README.i18n.yaml | 4 +- .../session/session-format-v3-to-v4/README.md | 2 +- .../session-format-v3-to-v4/README.zh.md | 2 +- .../session-format-v3-to-v4/src/facts.ts | 19 +++ .../session-format-v3-to-v4/src/validation.ts | 4 +- .../tests/codec.spec.ts | 21 +++ .../subagent/subagent-acp/README.i18n.yaml | 4 +- packages/subagent/subagent-acp/README.md | 2 +- packages/subagent/subagent-acp/README.zh.md | 2 +- .../tests/loader-composition.e2e.ts | 2 +- .../subagent-claude-code/README.i18n.yaml | 8 +- .../subagent/subagent-claude-code/README.md | 4 +- .../subagent-claude-code/README.zh.md | 4 +- .../subagent/subagent-codex/README.i18n.yaml | 8 +- packages/subagent/subagent-codex/README.md | 4 +- packages/subagent/subagent-codex/README.zh.md | 4 +- .../subagent-dsh-sdk/README.i18n.yaml | 4 +- packages/subagent/subagent-dsh-sdk/README.md | 2 +- .../subagent/subagent-dsh-sdk/README.zh.md | 2 +- .../tests/loader-composition.e2e.ts | 2 +- .../subagent-fork-in-process/README.i18n.yaml | 12 +- .../subagent-fork-in-process/README.md | 6 +- .../subagent-fork-in-process/README.zh.md | 6 +- .../README.i18n.yaml | 8 +- .../subagent-spawn-in-process/README.md | 4 +- .../subagent-spawn-in-process/README.zh.md | 4 +- packages/subagent/subagent/README.i18n.yaml | 27 ++-- packages/subagent/subagent/README.md | 20 ++- packages/subagent/subagent/README.zh.md | 20 ++- packages/subagent/subagent/src/activation.ts | 2 + packages/subagent/subagent/src/catalog.ts | 48 +++++-- .../subagent/src/continuation-messages.ts | 16 ++- packages/subagent/subagent/src/index.ts | 39 +++++- .../subagent/subagent/src/list-children.ts | 47 ++++++- packages/subagent/subagent/src/manager.ts | 118 ++++++++++++++-- packages/subagent/subagent/src/names.ts | 112 +++++++++++++++ .../subagent/subagent/src/projection-types.ts | 6 +- packages/subagent/subagent/src/types.ts | 19 +++ .../subagent/subagent/tests/catalog.spec.ts | 35 ++++- .../continuation-messages-adapter.spec.ts | 2 +- .../tests/continuation-messages.spec.ts | 15 +- .../subagent/tests/continuation.spec.ts | 36 ++--- .../tests/external-activation.spec.ts | 28 +++- .../subagent/tests/list-children.spec.ts | 4 +- .../subagent/tests/manager-internals.ts | 1 + .../subagent/tests/sibling-messages.spec.ts | 124 +++++++++++++++-- .../tool-subagent-control/README.i18n.yaml | 67 ++++----- .../subagent/tool-subagent-control/README.md | 43 +++--- .../tool-subagent-control/README.zh.md | 43 +++--- .../tool-subagent-control/package.json | 1 - .../tool-subagent-control/src/index.ts | 47 +++---- .../tool-subagent-control/src/list-agents.ts | 109 ++++----------- .../tests/list-agents.spec.ts | 58 ++++---- .../tests/tool-subagent-control.spec.ts | 79 +++++------ .../tests/wait-agent.spec.ts | 2 +- .../subagent/tool-subagent/README.i18n.yaml | 24 ++-- packages/subagent/tool-subagent/README.md | 12 +- packages/subagent/tool-subagent/README.zh.md | 12 +- packages/subagent/tool-subagent/src/index.ts | 15 +- .../tests/model-selection.spec.ts | 7 +- .../tool-subagent/tests/tool-subagent.spec.ts | 28 +++- packages/team/agent-team/README.i18n.yaml | 20 +-- packages/team/agent-team/README.md | 10 +- packages/team/agent-team/README.zh.md | 10 +- .../team/tool-agent-team/README.i18n.yaml | 16 +-- packages/team/tool-agent-team/README.md | 10 +- packages/team/tool-agent-team/README.zh.md | 10 +- packages/team/tool-agent-team/src/index.ts | 58 +++++--- .../tool-agent-team/tests/tool-team.spec.ts | 61 +++++--- packages/test-support/llm-replay/src/index.ts | 20 --- .../llm-replay/tests/llm-replay.spec.ts | 43 +----- .../workflow/workflow-ptc/tests/host.spec.ts | 6 +- .../workflow-ptc/tests/workflow-ptc.spec.ts | 2 +- pnpm-lock.yaml | 3 - scripts/gen-cordis-catalog.ts | 1 + scripts/gen-tool-catalog.ts | 2 +- .../advanced/result.json | 46 ++++--- .../advanced/session.v4.jsonl | 10 +- .../authoring/model-visible.json | 2 +- .../authoring/win-x64/model-visible.json | 2 +- scripts/type-equiv.manifest.json | 5 + .../acp/cancel-tool-calls/session.v4.jsonl | 2 +- snapshots/acp/cancel/session.v4.jsonl | 2 +- .../acp/escalation-approved/session.v4.jsonl | 2 +- .../acp/escalation-rejected/session.v4.jsonl | 2 +- .../fs-escalation-approved/session.v4.jsonl | 2 +- snapshots/acp/fs-same-mode/session.v4.jsonl | 2 +- .../acp/image-compaction/session.v4.jsonl | 2 +- .../notifications.expected.jsonl | 2 +- .../account-provider-signout/session.v4.jsonl | 2 +- snapshots/sdk/approval/session.v4.jsonl | 2 +- .../notifications.expected.jsonl | 2 +- .../background-wait-steer/session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 26 ++-- .../bash-tool/notifications.expected.jsonl | 2 +- snapshots/sdk/bash-tool/session.v4.jsonl | 2 +- .../sdk/bash-tool/tool-schemas.expected.json | 26 ++-- .../sdk/command-attachments/session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 26 ++-- .../sdk/command-identity/session.v4.jsonl | 4 +- .../sdk/desktop-context/session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 26 ++-- .../sdk/desktop-ptc-context/session.v4.jsonl | 2 +- .../system-prompt.expected.md | 39 +++--- .../notifications.expected.jsonl | 2 +- .../sdk/dynamic-tool-updates/session.v4.jsonl | 2 +- snapshots/sdk/goal-rounds/session.v4.jsonl | 2 +- .../sdk/inline-image-prompt/session.v4.jsonl | 2 +- snapshots/sdk/manual-compact/session.v4.jsonl | 8 +- .../sdk/max-tokens-continue/session.v4.jsonl | 2 +- .../sdk/multi-turn/writer.expected.jsonl | 6 +- .../sdk/permission-switch/session.v4.jsonl | 4 +- snapshots/sdk/plan-review/session.v4.jsonl | 2 +- .../plan-review/tool-schemas.expected.json | 130 ++++++++++++------ snapshots/sdk/ptc-cancel/session.v4.jsonl | 2 +- .../sdk/ptc-turn/notifications.expected.jsonl | 2 +- snapshots/sdk/ptc-turn/session.v4.jsonl | 2 +- snapshots/sdk/serial-created/session.v4.jsonl | 2 +- snapshots/sdk/session-resume/session.v4.jsonl | 2 +- .../session-resume/tool-schemas.expected.json | 52 ++++--- .../session-title-after-turn/session.v4.jsonl | 2 +- .../session-title-stability/session.v4.jsonl | 2 +- .../session.1.v4.jsonl | 2 +- .../session.v4.jsonl | 10 +- .../tool-schemas.1.expected.json | 26 ++-- .../session.1.v4.jsonl | 2 +- .../session.v4.jsonl | 10 +- .../tool-schemas.1.expected.json | 26 ++-- .../subagent-continuable/session.1.v4.jsonl | 10 +- .../sdk/subagent-continuable/session.v4.jsonl | 30 ++-- .../tool-schemas.1.expected.json | 26 ++-- .../notifications.expected.jsonl | 18 +-- .../session.v4.jsonl | 18 +-- .../tool-schemas.expected.json | 38 +++-- .../notifications.expected.jsonl | 10 +- .../session.1.v4.jsonl | 2 +- .../session.v4.jsonl | 10 +- .../tool-schemas.1.expected.json | 26 ++-- .../tool-schemas.expected.json | 26 ++-- .../session.1.v4.jsonl | 4 +- .../subagent-fork-in-process/session.v4.jsonl | 10 +- .../subagent-list-agents/session.1.v4.jsonl | 2 +- .../sdk/subagent-list-agents/session.v4.jsonl | 12 +- .../tool-schemas.1.expected.json | 26 ++-- .../sdk/subagent-mixed/session.1.v4.jsonl | 2 +- .../sdk/subagent-mixed/session.2.v4.jsonl | 4 +- snapshots/sdk/subagent-mixed/session.v4.jsonl | 18 +-- .../subagent-send-message/session.1.v4.jsonl | 8 +- .../subagent-send-message/session.v4.jsonl | 14 +- .../tool-schemas.1.expected.json | 26 ++-- .../notifications.expected.jsonl | 12 +- .../session.1.v4.jsonl | 2 +- .../session.v4.jsonl | 10 +- .../notifications.expected.jsonl | 2 +- .../system-prompt-in-history/session.v4.jsonl | 2 +- .../sdk/team-session-mode/session.v4.jsonl | 8 +- .../system-prompt.expected.md | 2 +- .../tool-schemas.expected.json | 28 ++-- .../text-turn/notifications.expected.jsonl | 2 +- snapshots/sdk/text-turn/session.v4.jsonl | 2 +- .../sdk/text-turn/tool-schemas.expected.json | 26 ++-- .../notifications.expected.jsonl | 2 +- .../sdk/tool-error-details/session.v4.jsonl | 2 +- .../system-prompt.expected.md | 39 +++--- .../tool-schemas.expected.json | 26 ++-- .../notifications.expected.jsonl | 2 +- .../tool-scheduler-recovery/session.v4.jsonl | 2 +- .../session.1.v4.jsonl | 2 +- .../session.2.v4.jsonl | 2 +- .../session.v4.jsonl | 12 +- .../advanced-toolchain/session.1.v4.jsonl | 2 +- .../advanced-toolchain/session.2.v4.jsonl | 2 +- .../advanced-toolchain/session.v4.jsonl | 12 +- .../system-prompt.expected.md | 39 +++--- .../tool-schemas.expected.json | 52 ++++--- .../session.v4.jsonl | 2 +- .../session.v4.jsonl | 2 +- .../session.v4.jsonl | 2 +- .../agent-instructions/session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 52 ++++--- .../apply-patch-partial/session.v4.jsonl | 2 +- .../apply-patch-rejected/session.v4.jsonl | 2 +- .../apply-patch-update/session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 26 ++-- .../session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 26 ++-- .../background-job-admission/session.v4.jsonl | 2 +- .../background-job-notice/session.v4.jsonl | 2 +- .../session.v4.jsonl | 2 +- snapshots/session/bash-spill/session.v4.jsonl | 2 +- .../bash-startup-timeout/session.v4.jsonl | 2 +- .../session/bash-tool-turn/session.v4.jsonl | 2 +- .../session/both-mode-turn/session.v4.jsonl | 2 +- .../both-mode-turn/system-prompt.expected.md | 39 +++--- .../session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 26 ++-- .../session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 26 ++-- .../session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 26 ++-- .../session/claude-code-mods/session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 26 ++-- .../cordis.snapshot.yml | 2 +- .../compaction-output-reserve/cordis.yml | 2 +- .../session.v4.jsonl | 8 +- .../compaction-recovery/session.v4.jsonl | 6 +- .../tool-schemas.expected.json | 52 ++++--- .../cordis.snapshot.yml | 2 +- .../compaction-summary-headroom/cordis.yml | 2 +- .../session.v4.jsonl | 8 +- .../session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 26 ++-- .../session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 26 ++-- .../cordis-inspect-jsdoc/session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 26 ++-- .../cordis-inspect-liveness/session.v4.jsonl | 2 +- .../cordis-inspect-timeout/session.v4.jsonl | 2 +- snapshots/session/cua-native/session.v4.jsonl | 2 +- .../cua-native/tool-schemas.expected.json | 26 ++-- snapshots/session/cua-ptc/session.v4.jsonl | 2 +- .../session/cua-ptc/system-prompt.expected.md | 39 +++--- .../session/deepseek-fetch/session.v4.jsonl | 2 +- .../session.v4.jsonl | 2 +- .../session.v4.jsonl | 2 +- .../session.v4.jsonl | 2 +- .../session.v4.jsonl | 2 +- .../deepseek-messages-replay/session.v4.jsonl | 2 +- .../session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 26 ++-- .../session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 26 ++-- .../session.v4.jsonl | 2 +- .../dynamic-tool-updates/session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 78 +++++++---- .../session.v4.jsonl | 2 +- .../writer.expected.jsonl | 6 +- .../error-finish/writer.expected.jsonl | 4 +- .../session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 26 ++-- .../fs-delete-recreate/session.v4.jsonl | 2 +- snapshots/session/fs-edit/session.v4.jsonl | 2 +- .../session/fs-glob-sampling/session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 26 ++-- .../session/fs-policy-reject/session.v4.jsonl | 2 +- .../session/fs-read-window/session.v4.jsonl | 2 +- snapshots/session/fs-read/session.v4.jsonl | 2 +- .../session.v4.jsonl | 2 +- .../session.v4.jsonl | 2 +- .../fs-write-overwrite/session.v4.jsonl | 2 +- snapshots/session/fs-write/session.v4.jsonl | 2 +- .../headless-auto-review/session.v4.jsonl | 2 +- .../headless-credentials/session.v4.jsonl | 2 +- snapshots/session/headless.snapshot.ts | 4 +- .../hook-cc-invalid-matcher/session.v4.jsonl | 2 +- .../hook-cc-posttool-block/session.v4.jsonl | 2 +- .../hook-cc-posttool-context/session.v4.jsonl | 2 +- .../hook-cc-pretool-ask/session.v4.jsonl | 2 +- .../hook-cc-pretool-deny/session.v4.jsonl | 2 +- .../session.v4.jsonl | 2 +- .../hook-cc-stop-continue/session.v4.jsonl | 2 +- .../session.v4.jsonl | 2 +- .../session.v4.jsonl | 2 +- .../session.v4.jsonl | 2 +- .../hook-codex-pretool-block/session.v4.jsonl | 2 +- .../session.v4.jsonl | 2 +- .../hook-codex-stop-continue/session.v4.jsonl | 2 +- .../in-history-series-start/session.v4.jsonl | 2 +- .../session/lsp-definition/session.v4.jsonl | 2 +- .../lsp-definition/tool-schemas.expected.json | 26 ++-- .../session/mcp-empty-ptc/session.v4.jsonl | 2 +- snapshots/session/mcp-empty/session.v4.jsonl | 2 +- .../mcp-resources-ptc/session.v4.jsonl | 2 +- .../system-prompt.expected.md | 39 +++--- .../session/mcp-resources/session.v4.jsonl | 2 +- .../mcp-resources/tool-schemas.expected.json | 26 ++-- .../missing-sandbox-runner/session.v4.jsonl | 2 +- .../cordis.snapshot.yml | 2 +- .../model-switch-compaction/cordis.yml | 2 +- .../model-switch-compaction/session.v4.jsonl | 8 +- .../model-switch-notice/session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 52 ++++--- .../multimodal-spill-ends/session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 26 ++-- .../multimodal-spill-middle/session.v4.jsonl | 2 +- .../office-authoring-checker/session.v4.jsonl | 2 +- .../session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 26 ++-- .../office-progressive-ptc/session.v4.jsonl | 2 +- .../system-prompt.expected.md | 39 +++--- .../writer.expected.jsonl | 12 +- .../office-render-tools/writer.expected.jsonl | 12 +- .../session.v4.jsonl | 2 +- .../session.v4.jsonl | 2 +- .../session/office-skills/session.v4.jsonl | 2 +- .../office-skills/tool-schemas.expected.json | 26 ++-- .../packed-chunks/writer.expected.jsonl | 8 +- .../parallel-tool-calls/session.v4.jsonl | 2 +- .../session.v4.jsonl | 2 +- .../session.v4.jsonl | 2 +- .../session.v4.jsonl | 2 +- .../session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 26 ++-- .../session.v4.jsonl | 2 +- .../session.v4.jsonl | 2 +- .../pi-ai-replay-metadata/session.v4.jsonl | 2 +- .../plugin-manager-mcp/session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 52 ++++--- .../session/plugin-manager/session.v4.jsonl | 2 +- .../plugin-manager/tool-schemas.expected.json | 26 ++-- .../product-subagent-both/session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 50 +++++-- .../product-subagent-codex/session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 38 +++-- .../product-subagent-oar/session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 32 +++-- .../session.v4.jsonl | 42 +++--- .../tool-schemas.expected.json | 32 +++-- .../session/provider-cwd/session.v4.jsonl | 2 +- .../provider-cwd/tool-schemas.expected.json | 26 ++-- .../ptc-node-read-only/session.v4.jsonl | 2 +- .../system-prompt.expected.md | 39 +++--- .../ptc-node-workspace/session.v4.jsonl | 2 +- .../system-prompt.expected.md | 39 +++--- .../session/ptc-persistent/session.v4.jsonl | 2 +- .../ptc-persistent/system-prompt.expected.md | 39 +++--- .../ptc-policy-change/session.v4.jsonl | 4 +- .../system-prompt.expected.md | 39 +++--- .../ptc-python-read-only/session.v4.jsonl | 2 +- .../session/ptc-python-turn/session.v4.jsonl | 2 +- .../ptc-python-turn/system-prompt.expected.md | 45 +++--- .../session.v4.jsonl | 2 +- .../session/ptc-read-image/session.v4.jsonl | 2 +- .../ptc-read-image/system-prompt.expected.md | 39 +++--- .../session/ptc-script-fresh/session.v4.jsonl | 2 +- .../system-prompt.expected.md | 39 +++--- .../ptc-script-retained/session.v4.jsonl | 2 +- .../system-prompt.expected.md | 39 +++--- snapshots/session/ptc-turn/session.v4.jsonl | 2 +- .../ptc-turn/system-prompt.expected.md | 39 +++--- .../ptc-workspace-context/session.v4.jsonl | 2 +- .../session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 26 ++-- .../session/pwsh-tool-turn/session.v4.jsonl | 2 +- .../pwsh-tool-turn/tool-schemas.expected.json | 26 ++-- .../session/ralph-loop/session.1.v4.jsonl | 2 +- .../session/ralph-loop/session.2.v4.jsonl | 2 +- snapshots/session/ralph-loop/session.v4.jsonl | 6 +- .../ralph-loop/tool-schemas.1.expected.json | 26 ++-- .../ralph-loop/tool-schemas.2.expected.json | 26 ++-- .../ralph-loop/tool-schemas.expected.json | 26 ++-- .../session.v4.jsonl | 2 +- .../read-image-dimension/session.v4.jsonl | 2 +- .../session/read-image-gif/session.v4.jsonl | 2 +- .../read-image-reencode/session.v4.jsonl | 2 +- .../read-image-text-route/session.v4.jsonl | 2 +- snapshots/session/read-image/session.v4.jsonl | 2 +- .../repeat-tool-reminder/session.v4.jsonl | 2 +- .../session-query-spill/session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 26 ++-- .../session-reference-spill/session.v4.jsonl | 2 +- .../session-sandbox-root/session.v4.jsonl | 2 +- snapshots/session/skill-load/session.v4.jsonl | 2 +- .../skill-root-isolation/session.v4.jsonl | 2 +- .../subagent-acp-diagnostic/session.v4.jsonl | 18 +-- .../tool-schemas.expected.json | 32 +++-- .../subagent-agent-preset/session.1.v4.jsonl | 2 +- .../subagent-agent-preset/session.v4.jsonl | 10 +- .../tool-schemas.1.expected.json | 32 +++-- .../tool-schemas.expected.json | 26 ++-- .../session.1.v4.jsonl | 4 +- .../session.2.v4.jsonl | 4 +- .../session.3.v4.jsonl | 2 +- .../subagent-archive-lineage/session.v4.jsonl | 4 +- .../session.1.v4.jsonl | 2 +- .../session.v4.jsonl | 10 +- .../tool-schemas.expected.json | 26 ++-- .../session.v4.jsonl | 2 +- .../session.1.v4.jsonl | 6 +- .../session.2.v4.jsonl | 2 +- .../session.v4.jsonl | 10 +- .../session.1.v4.jsonl | 2 +- .../session.v4.jsonl | 10 +- .../subagent-multi/writer.1.expected.jsonl | 4 +- .../subagent-multi/writer.2.expected.jsonl | 4 +- .../subagent-multi/writer.expected.jsonl | 24 ++-- .../subagent-no-message-tool/session.v4.jsonl | 10 +- .../tool-schemas.1.expected.json | 12 +- .../subagent-parallel/session.1.v4.jsonl | 2 +- .../subagent-parallel/session.2.v4.jsonl | 2 +- .../subagent-parallel/session.v4.jsonl | 18 +-- .../session.v4.jsonl | 2 +- .../session.1.v4.jsonl | 2 +- .../session.v4.jsonl | 10 +- .../session.1.v4.jsonl | 6 +- .../session.v4.jsonl | 8 +- .../tool-schemas.1.expected.json | 20 ++- .../tool-schemas.expected.json | 20 ++- .../subagent-tool-filter/session.1.v4.jsonl | 2 +- .../subagent-tool-filter/session.v4.jsonl | 10 +- .../tool-schemas.1.expected.json | 26 ++-- .../system-prompt-in-history/session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 26 ++-- .../team-direct-inbox/session.1.v4.jsonl | 4 +- .../team-direct-inbox/session.v4.jsonl | 18 +-- .../team-historical-records/session.v4.jsonl | 8 +- .../team-session-mode/session.v4.jsonl | 8 +- .../system-prompt.expected.md | 2 +- .../tool-schemas.expected.json | 28 ++-- .../session/team-targets/session.v4.jsonl | 8 +- .../team-targets/system-prompt.expected.md | 2 +- .../team-targets/tool-schemas.expected.json | 30 ++-- .../team-task-inbox/session.1.v4.jsonl | 6 +- .../session/team-task-inbox/session.v4.jsonl | 12 +- .../team-tree-messages/replay.override.json | 2 +- .../team-tree-messages/session.1.v4.jsonl | 4 +- .../team-tree-messages/session.2.v4.jsonl | 14 +- .../team-tree-messages/session.v4.jsonl | 16 +-- .../text-turn/tool-schemas.expected.json | 26 ++-- .../session/text-turn/writer.expected.jsonl | 4 +- snapshots/session/todo-write/session.v4.jsonl | 2 +- .../session/tool-call-turn/session.v4.jsonl | 2 +- .../toolcall-unused-args/session.v4.jsonl | 2 +- snapshots/session/web-fetch/session.v4.jsonl | 2 +- .../web-fetch/tool-schemas.expected.json | 26 ++-- .../session.v4.jsonl | 2 +- .../web-search-unconfigured/session.v4.jsonl | 2 +- .../windows-acl-skill/session.v4.jsonl | 2 +- .../workflow-confinement/session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 26 ++-- .../session/workflow-run/session.1.v4.jsonl | 2 +- .../session/workflow-run/session.v4.jsonl | 4 +- .../session.v4.jsonl | 2 +- .../session.v4.jsonl | 8 +- .../working-directory/session.v4.jsonl | 8 +- .../workspace-dependencies/session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 26 ++-- .../session/workspace-edit/session.v4.jsonl | 2 +- snapshots/session/worktree/session.v4.jsonl | 6 +- .../worktree/tool-schemas.expected.json | 26 ++-- .../web/approval-composer/session.v4.jsonl | 2 +- .../web/changed-files-turn/session.v4.jsonl | 2 +- snapshots/web/code-language/session.v4.jsonl | 2 +- .../creation-recommendations/session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 52 ++++--- .../deepseek-messages-chat/session.v4.jsonl | 2 +- .../web/feedback-command/session.v4.jsonl | 2 +- .../web/file-upload-round/session.v4.jsonl | 2 +- .../web/fresh-round-trip/session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 26 ++-- .../goal-multi-turn-actions/session.v4.jsonl | 2 +- .../web/lifecycle-chrome/session.v4.jsonl | 2 +- .../web/live-interactions/session.v4.jsonl | 2 +- .../official-subagent-bundle/session.v4.jsonl | 10 +- .../system-prompt.expected.md | 44 +++--- .../optional-string-editor/session.v4.jsonl | 2 +- .../system-prompt.expected.md | 39 +++--- .../session.v4.jsonl | 8 +- snapshots/web/plan-review/session.v4.jsonl | 2 +- .../plugin-recommendations/session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 52 ++++--- snapshots/web/present-svg/session.v4.jsonl | 2 +- snapshots/web/present/session.v4.jsonl | 2 +- .../ptc-escalation-approved/session.v4.jsonl | 2 +- snapshots/web/ptc-round/session.v4.jsonl | 2 +- .../web/ptc-round/system-prompt.expected.md | 39 +++--- .../session.v4.jsonl | 2 +- .../web/question-composer/session.v4.jsonl | 2 +- snapshots/web/steering/session.v4.jsonl | 2 +- .../subagent-conversation/tree.expected.md | 4 +- .../subagent-fork-schedule/session.1.v4.jsonl | 4 +- .../subagent-fork-schedule/session.v4.jsonl | 4 +- snapshots/web/tool-details/ui.expected.md | 4 +- .../web/turn-tail-actions/session.v4.jsonl | 2 +- .../web/web-search-round/session.v4.jsonl | 2 +- 581 files changed, 4677 insertions(+), 2745 deletions(-) create mode 100644 .agents/notes/implemented/architecture/2026-11-02-subagent-catalog-names.i18n.yaml create mode 100644 .agents/notes/implemented/architecture/2026-11-02-subagent-catalog-names.md create mode 100644 .agents/notes/implemented/architecture/2026-11-02-subagent-catalog-names.zh.md create mode 100644 docs/persistence-changes/2026-11-02-subagent-catalog-names.i18n.yaml create mode 100644 docs/persistence-changes/2026-11-02-subagent-catalog-names.md create mode 100644 docs/persistence-changes/2026-11-02-subagent-catalog-names.schema.json create mode 100644 docs/persistence-changes/2026-11-02-subagent-catalog-names.zh.md create mode 100644 docs/upgrade-guide/v0.2.2-alpha.2/subagent-names/guide.i18n.yaml create mode 100644 docs/upgrade-guide/v0.2.2-alpha.2/subagent-names/guide.md create mode 100644 docs/upgrade-guide/v0.2.2-alpha.2/subagent-names/guide.zh.md create mode 100644 packages/client/ui-agent-team/src/client/owner-name.ts create mode 100644 packages/client/ui-agent-team/tests/owner-name.client.spec.ts create mode 100644 packages/subagent/subagent/src/names.ts diff --git a/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.i18n.yaml index 97e7927b58..09305c1c93 100644 --- a/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.i18n.yaml @@ -15,11 +15,11 @@ en: 62284bd7dd804188 zh: b3a8dfebffc01bdb /agent-note-adjacent-agents-share-one-steer-send-message-operation/decision/complete-removal-and-reintroduction-condition: - en: 8c6d355d91ec83d2 - zh: fcc74d1e6ef477bf + en: 8cd78e21602f17f0 + zh: 3843fea07e9b93a3 /agent-note-adjacent-agents-share-one-steer-send-message-operation/alternatives-considered: en: 7386e88e7051dce7 zh: dcea5b9e30374be3 /agent-note-adjacent-agents-share-one-steer-send-message-operation/consequences: - en: 9c23dd6ace029ec3 - zh: f08ef3f142534843 + en: 22a3af4c51aee23e + zh: 9d59315313d0f761 diff --git a/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md b/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md index a4cfbc91cb..080f10f19a 100644 --- a/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md +++ b/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.md @@ -40,7 +40,7 @@ The globally registered model tool is direction-neutral and has one fixed schema ```ts interface SendMessageInput { - readonly agent_id: string + readonly agent: string readonly message: string } ``` @@ -53,7 +53,7 @@ Human browser prompts are not model-authored Agent messages. The remote prompt p ### Complete removal and reintroduction condition -The standalone `@deepseek-ai/dsh-tool-subagent-report` package, `report` schema, `tool:report` prompt section, `reportDelivery` configuration, report-specific message source, catalog entries, composition rows, and supported-behavior snapshots are absent. The unified tool gives up the recipient-free child shortcut and the old ability for a structural return tool to survive an explicit child allow-list. Those capabilities return only if a concrete use case requires semantics that a tree member’s `agent_id` and fixed Steer cannot express; reintroducing them requires a distinct model operation and prefix-cost evidence, not an alias over `sendMessage()`. +The standalone `@deepseek-ai/dsh-tool-subagent-report` package, `report` schema, `tool:report` prompt section, `reportDelivery` configuration, report-specific message source, catalog entries, composition rows, and supported-behavior snapshots are absent. The unified tool gives up the recipient-free child shortcut and the old ability for a structural return tool to survive an explicit child allow-list. Those capabilities return only if a concrete use case requires semantics that a tree member’s `agent` reference and fixed Steer cannot express; reintroducing them requires a distinct model operation and prefix-cost evidence, not an alias over `sendMessage()`. ## Alternatives considered @@ -71,7 +71,7 @@ The standalone `@deepseek-ai/dsh-tool-subagent-report` package, `report` schema, ## Consequences -- Model consumers expose one `send_message({ agent_id, message })` definition to parents and children, with no model-selected Queue versus Steer parameter. +- Model consumers expose one `send_message({ agent, message })` definition to parents and children, with no model-selected Queue versus Steer parameter. - The continuation manager remains the sole owner of tree authorization, residency, cold resume, waking admission, and teardown races. - Accepted messages may extend a running target's current turn; messages waiting together share next-step FIFO ordering. - Caller cancellation owns work only until inbox acceptance and does not retract an accepted message or dispose the target. diff --git a/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md b/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md index d907eaeac0..2c944b44ea 100644 --- a/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-27-adjacent-agent-steer-messaging.zh.md @@ -40,7 +40,7 @@ interface AgentMessageSource { ```ts interface SendMessageInput { - readonly agent_id: string + readonly agent: string readonly message: string } ``` @@ -53,7 +53,7 @@ parent 与 child 以相同注册表顺序继承相同定义。标准定义携带 ### 完整移除与重新引入条件 -独立的 `@deepseek-ai/dsh-tool-subagent-report` 包、`report` schema、`tool:report` 提示词 section、`reportDelivery` 配置、report 专属消息来源、目录项、组合行和受支持行为快照均已不存在。统一工具放弃了无需接收方的 child 快捷方式,也放弃了让结构性返回工具绕过显式 child allow-list 的旧能力。只有具体用例需要树成员的 `agent_id` 与固定 Steer 无法表达的语义时,这些能力才会重新出现;重新引入需要独立的模型操作与前缀成本证据,而非 `sendMessage()` 之上的别名。 +独立的 `@deepseek-ai/dsh-tool-subagent-report` 包、`report` schema、`tool:report` 提示词 section、`reportDelivery` 配置、report 专属消息来源、目录项、组合行和受支持行为快照均已不存在。统一工具放弃了无需接收方的 child 快捷方式,也放弃了让结构性返回工具绕过显式 child allow-list 的旧能力。只有具体用例需要树成员的 `agent` 引用与固定 Steer 无法表达的语义时,这些能力才会重新出现;重新引入需要独立的模型操作与前缀成本证据,而非 `sendMessage()` 之上的别名。 ## 考虑过的替代方案 @@ -71,7 +71,7 @@ parent 与 child 以相同注册表顺序继承相同定义。标准定义携带 ## 后果 -- 模型 Consumer 向 parent 与 child 公开一个 `send_message({ agent_id, message })` 定义,不提供模型选择的 Queue 与 Steer 参数。 +- 模型 Consumer 向 parent 与 child 公开一个 `send_message({ agent, message })` 定义,不提供模型选择的 Queue 与 Steer 参数。 - 继续执行管理器仍是整树授权、驻留、冷恢复、唤醒准入与拆卸竞态的唯一所有者。 - 被接受的消息可以延长运行中目标的当前轮次;一起等待的消息共享 next-step FIFO 顺序。 - 调用方取消只在 inbox 接受前掌管工作,不会撤回已接受消息或 dispose(资源释放)目标。 diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml index 33ebd75e3d..c922f333d8 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.i18n.yaml @@ -9,8 +9,8 @@ en: 5a991fec31578cea zh: 13b75aa36eb22907 /agent-note-parent-owned-subagent-catalog-events/decision: - en: 4319c502038ab34d - zh: b11825ff9d744822 + en: d2290d8e70d42054 + zh: 64a2d1d0915f6c3e /agent-note-parent-owned-subagent-catalog-events/alternatives-considered: en: 7a3d4b9f69c243d8 zh: 82b03d16ccc94dd2 diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md index 2fac858789..d5a03d7f9f 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md @@ -14,7 +14,7 @@ The child descriptor remains necessary for recovery and composition, but it cann The shared activation entry point, external execution ownership, and caller-versus-parent result delivery are governed by [Unified subagent activations](../simplification/2026-09-17-unified-subagent-activations.md). This record retains the independent rationale described below. -The parent Session's required `subagent/catalog` events are the persistent authority for direct-child discovery. Every local child enters the catalog independently of its result recipient; external children enter only with parent delivery. Workflow membership records describe execution progress and do not replace local Session discovery. Each event contains `childId`, `childCreatedAt`, mode, and the mode-discriminated label. Local children use `continuable`; external executions use `external` and cannot be opened as local Sessions. Historical `one-shot` and `unknown` entries remain readable. Invalid own facts, including unsupported payload versions, reject projection restoration because silently dropping a required fact would return an incomplete catalog. +The parent Session's required `subagent/catalog` events are the persistent authority for direct-child discovery. Every local child enters the catalog independently of its result recipient; external children enter only with parent delivery. Workflow membership records describe execution progress and do not replace local Session discovery. Each event contains `childId`, `childCreatedAt`, mode, the mode-discriminated label, and, from payload v3, the child's [name](2026-11-02-subagent-catalog-names.md). Local children use `continuable`; external executions use `external` and cannot be opened as local Sessions. Historical `one-shot` and `unknown` entries remain readable. Invalid own facts, including unsupported payload versions, reject projection restoration because silently dropping a required fact would return an incomplete catalog. Creation publishes only successful facts. A local activation admits the initial prompt before appending its catalog event. An external activation appends its event after provider startup and before returning the receipt. Failed admission or publication releases the unpublished execution; there is no compensating catalog event or rollback protocol. @@ -22,7 +22,7 @@ The child header and `subagent/descriptor` remain authoritative for recovery and The registered `subagentCatalog` projection materializes the parent facts. It delegates storage, append, iteration, and checkpoint validation to [`dsh-chunked-list`](../../../../packages/util/chunked-list/README.md), which stores facts in a persistent stack of 64-entry chunks, so an append copies at most the head chunk in bounded O(1) work. Materialization visits chunks from oldest to newest and preserves parent catalog event order in O(D) time for D facts. Concurrent creation is ordered by successful catalog append, independent of child timestamps and ids. A projection checkpoint clones the state once in O(D); projection-cache writes remain asynchronous and use the existing mandatory creation, turn-end, and disposal points. -The utility owns chunk layout and its shared capacity constant; the catalog owns event validation, fork filtering, and row conversion. Catalog projection state version 6 accepts external membership, so the projection registry rebuilds incompatible caches from Session events. Payload v0 and v1 remain unchanged; v2 records external executions. +The utility owns chunk layout and its shared capacity constant; the catalog owns event validation, fork filtering, and row conversion. Catalog projection state version 7 accepts external membership and child names, so the projection registry rebuilds incompatible caches from Session events. Payload v0 and v1 remain unchanged; v2 records external executions; v3 adds names to continuable and external entries. Fork isolation uses the exact `Session.inheritedEventCount` supplied to projection initialization. The fold ignores `subagent/catalog` events below that offset. The state stores the inherited offset but not each event seq because acceptance is decided during folding. diff --git a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md index 10088c52a7..7ebab0cef7 100644 --- a/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md @@ -14,7 +14,7 @@ child descriptor 对恢复与 composition 仍然必要,但它不能作为 disc 共享 activation 入口、外部执行所有权,以及调用方与父级之间的结果投递选择由[统一 subagent activation](../simplification/2026-09-17-unified-subagent-activations.zh.md) 决策拥有。本记录保留下述独立理由。 -父级 Session 的必读 `subagent/catalog` 事件是发现直接子级的持久化权威。每个本地子级都会进入目录,与结果接收方无关;外部子级仅在 parent 投递时进入目录。工作流成员记录描述执行进度,不替代本地 Session 发现。每个事件包含 `childId`、`childCreatedAt`、mode 与按 mode 区分的 label。本地子级使用 `continuable`;外部执行使用 `external`,不能作为本地 Session 打开。历史 `one-shot` 与 `unknown` 条目仍可读取。无效的自身事实(包括不支持的载荷版本)会使投影恢复失败,因为静默丢弃必读事实将返回不完整的 catalog。 +父级 Session 的必读 `subagent/catalog` 事件是发现直接子级的持久化权威。每个本地子级都会进入目录,与结果接收方无关;外部子级仅在 parent 投递时进入目录。工作流成员记录描述执行进度,不替代本地 Session 发现。每个事件包含 `childId`、`childCreatedAt`、mode、按 mode 区分的 label,以及从载荷 v3 起的子级[名称](2026-11-02-subagent-catalog-names.zh.md)。本地子级使用 `continuable`;外部执行使用 `external`,不能作为本地 Session 打开。历史 `one-shot` 与 `unknown` 条目仍可读取。无效的自身事实(包括不支持的载荷版本)会使投影恢复失败,因为静默丢弃必读事实将返回不完整的 catalog。 创建只发布成功事实。本地 activation 先接纳初始提示词,再追加 catalog 事件。外部 activation 在提供方启动后、返回回执前追加事件。准入或发布失败会释放未发布的执行;不存在补偿 catalog 事件或回滚协议。 @@ -22,7 +22,7 @@ child header 与 `subagent/descriptor` 继续拥有恢复与 composition 权威 注册的 `subagentCatalog` projection 物化 parent fact。它将存储、追加、迭代和检查点校验交给 [`dsh-chunked-list`](../../../../packages/util/chunked-list/README.zh.md),后者以每块 64 项的持久 stack 保存事实,因此 append 最多复制 head chunk,以有界 O(1) 工作完成。materialization 从旧到新访问 chunk,对 D 条事实以 O(D) 时间保留父目录事件顺序。并发创建按目录成功追加的顺序排列,与 child 时间戳和 id 无关。projection checkpoint 以 O(D) 克隆 state;projection-cache 继续异步写入,并使用既有创建、turn-end 与 disposal 强制点。 -工具库拥有 chunk 布局与共享容量常量;catalog 拥有事件校验、fork 过滤和行转换。catalog 投影状态版本 6 接受外部成员,因此投影注册表会从 Session 事件重建不兼容缓存。载荷 v0 与 v1 保持不变;v2 记录外部执行。 +工具库拥有 chunk 布局与共享容量常量;catalog 拥有事件校验、fork 过滤和行转换。catalog 投影状态版本 7 接受外部成员和子级名称,因此投影注册表会从 Session 事件重建不兼容缓存。载荷 v0 与 v1 保持不变;v2 记录外部执行;v3 为可继续和外部条目增加名称。 fork 隔离使用 projection 初始化时提供的精确 `Session.inheritedEventCount`。fold 忽略该 offset 之前的 `subagent/catalog` 事件。state 保存 inherited offset,但不保存每条 event seq,因为接受判定已在 fold 时完成。 diff --git a/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.i18n.yaml index f1c602359c..61d408cf40 100644 --- a/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.i18n.yaml @@ -3,8 +3,8 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.md /agent-note-subagent-identity-in-required-runtime-context: - en: ce8f378b6e7b57fa - zh: 26bd0415b4dba94a + en: 3da6e33c5dc2c917 + zh: 9c8d4542cda63fc4 /agent-note-subagent-identity-in-required-runtime-context/problem: en: f944254565541454 zh: 27fa9f67c671dec7 diff --git a/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.md b/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.md index fb8e92e811..bb2405a145 100644 --- a/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.md +++ b/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.md @@ -4,6 +4,8 @@ Status: implemented English | [中文](2026-10-13-team-identity-runtime-context.zh.md) +[Catalog names](2026-11-02-subagent-catalog-names.md) supersede the Session-id identity text below: agents see their own, direct parent, and root names. The runtime-context retention decision remains active. + ## Problem A teammate identity embedded in its initial task depends on ordinary history retention. Compaction can remove it, and an ordinary fork from that teammate can retain the old role even though the fork is an independent Lead. Identity must reflect the current subagent tree while keeping shared system instructions uniform. diff --git a/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.zh.md b/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.zh.md index f3ebb3e6b3..09adf8c130 100644 --- a/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.zh.md +++ b/.agents/notes/implemented/architecture/2026-10-13-team-identity-runtime-context.zh.md @@ -4,6 +4,8 @@ Status: implemented [English](2026-10-13-team-identity-runtime-context.md) | 中文 +[目录名称](2026-11-02-subagent-catalog-names.zh.md)取代下文以 Session id 表述身份的文本:agent 看到自身、直接父级和根的名称。runtime context 保留决策仍然有效。 + ## Problem 嵌入初始任务的 teammate 身份依赖普通历史保留。压缩可能移除它;从 teammate 创建的普通 fork 即使已成为独立 Lead,也可能保留旧角色。身份必须反映当前 subagent 树,同时保持共享系统指令一致。 diff --git a/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.i18n.yaml index 282acab92a..bdf1744de7 100644 --- a/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.i18n.yaml @@ -3,8 +3,8 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md /agent-note-agent-teams-teammates-are-subagents: - en: 51e2fc51a6e1622a - zh: 7e09ea6292e5921d + en: f49f9e7ef588e612 + zh: 7da804056221af51 /agent-note-agent-teams-teammates-are-subagents/problem: en: ca2848e3bbc3a009 zh: 0551e32ff6d187d0 diff --git a/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md b/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md index 8cab7023e0..2b11e98b0e 100644 --- a/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md +++ b/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.md @@ -4,7 +4,7 @@ Status: implemented English | [中文](2026-10-15-agent-teams-as-subagents.zh.md) -The adjacency and parent-lifetime restrictions are superseded by [tree-wide communication](2026-10-16-tree-wide-agent-communication.md). [Task-board inbox coordination](2026-10-19-team-task-board-inbox.md) supersedes the separate roster and Team-owned wait; the single subagent identity decision remains active. +The adjacency and parent-lifetime restrictions are superseded by [tree-wide communication](2026-10-16-tree-wide-agent-communication.md). [Task-board inbox coordination](2026-10-19-team-task-board-inbox.md) supersedes the separate roster and Team-owned wait; the single subagent identity decision remains active. [Catalog names](2026-11-02-subagent-catalog-names.md) supersede the statement that creation accepts no member name and the rejection of unique names below. ## Problem diff --git a/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.zh.md b/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.zh.md index 8c113d288a..a7467f1dbb 100644 --- a/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.zh.md +++ b/.agents/notes/implemented/architecture/2026-10-15-agent-teams-as-subagents.zh.md @@ -4,7 +4,7 @@ Status: implemented [English](2026-10-15-agent-teams-as-subagents.md) | 中文 -邻接和父级生命周期限制由[整树通信](2026-10-16-tree-wide-agent-communication.zh.md)取代。[任务板收件箱协调](2026-10-19-team-task-board-inbox.zh.md)取代独立名册和 Team 所有的等待;单一 subagent 身份决策仍有效。 +邻接和父级生命周期限制由[整树通信](2026-10-16-tree-wide-agent-communication.zh.md)取代。[任务板收件箱协调](2026-10-19-team-task-board-inbox.zh.md)取代独立名册和 Team 所有的等待;单一 subagent 身份决策仍有效。[目录名称](2026-11-02-subagent-catalog-names.zh.md)取代下文“创建不接收成员名称”的表述及对唯一名称的否决。 ## Problem diff --git a/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.i18n.yaml index ee52c30615..fd6879706d 100644 --- a/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.i18n.yaml @@ -9,8 +9,8 @@ en: c583c3cdb894e417 zh: 6edc0354b79f4f26 /agent-note-communication-follows-the-subagent-tree/decision: - en: da35a75e069fa1e4 - zh: 7edf62ad66ab04ec + en: 767b4b4dfd84f72e + zh: 36617ec0dab777ee /agent-note-communication-follows-the-subagent-tree/alternatives-considered: en: 470f9cf8c3e32af9 zh: 73bbe29767c5d02a diff --git a/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.md b/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.md index 30dee6781a..8b80b0b841 100644 --- a/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.md +++ b/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.md @@ -10,13 +10,13 @@ A Team shares one task board across the root's entire subagent tree. Restricting ## Decision -`list_agents` starts at the caller's live root and lists that root and all reachable continuable descendants, including the caller. Parent ids and depth remain relative to the root. The existing parent catalogs remain authoritative; no Team directory is stored separately. +`list_agents` starts at the caller's live root and lists that root and all reachable continuable descendants, including the caller. Full names remain relative to the root. The existing parent catalogs remain authoritative; no Team directory is stored separately. `send_message` accepts another root or continuable member of the same tree. The sender must be the exact live root or a resident continuable subagent. Cross-branch delivery verifies the target descriptor and creation catalogs, then restores only the target under the Team root. Final inbox admission rechecks the sender and root. Cancellation or rejection before acceptance releases the new target instance; accepted input follows normal Agent persistence. The existing Agent registry records runtime ownership by the Team root. Creation parent ids and catalogs preserve ancestry for discovery and interrupt authority. A member finishing or being released affects only its own instance; Team-root shutdown joins all startup and cleanup. This removes ancestor restoration, parent retention, and recursive disposal without adding a Team runtime registry. Saved target presets and permission events supply restoration configuration. -This supersedes the adjacency, discovery, and parent-lifetime restrictions in [adjacent messaging](2026-08-27-adjacent-agent-steer-messaging.md) and [subagent teammates](2026-10-15-agent-teams-as-subagents.md). Their shared inbox, scheduling, attribution, and Team reuse decisions remain active. Members use Session ids for addressing; ordinary Session forks lead independent Teams. +This supersedes the adjacency, discovery, and parent-lifetime restrictions in [adjacent messaging](2026-08-27-adjacent-agent-steer-messaging.md) and [subagent teammates](2026-10-15-agent-teams-as-subagents.md). Their shared inbox, scheduling, attribution, and Team reuse decisions remain active. Members address one another by the names that [catalog names](2026-11-02-subagent-catalog-names.md) define, or by Session id; ordinary Session forks lead independent Teams. ## Alternatives considered diff --git a/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.zh.md b/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.zh.md index 58d075e9a6..b026bda26c 100644 --- a/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.zh.md +++ b/.agents/notes/implemented/architecture/2026-10-16-tree-wide-agent-communication.zh.md @@ -10,13 +10,13 @@ Status: implemented ## Decision -`list_agents` 从调用方的在线根开始,列出该根与所有可达的可继续后代,包括调用方。父级 id 和深度始终相对根计算。现有父级目录仍是权威来源;不单独存储 Team 目录。 +`list_agents` 从调用方的在线根开始,列出该根与所有可达的可继续后代,包括调用方。完整名称始终相对根计算。现有父级目录仍是权威来源;不单独存储 Team 目录。 `send_message` 接受同一棵树中的其他根或可继续成员。发送者必须是确切在线根或驻留的可继续子代理。跨分支投递验证目标描述符和创建目录后,仅在 Team 根下恢复目标。最终收件箱准入重新检查发送者与根。接受前的取消或拒绝释放新建目标实例;已接受输入遵循普通 Agent 持久化。 现有 Agent 注册表记录 Team 根对运行时实例的所有权。创建父级 id 和目录保留用于发现及中断授权的祖先关系。成员完成或释放只影响自身实例;关闭 Team 根会等待全部启动和清理。这删除了祖先恢复、父级保留和递归释放,无需增加 Team 运行时注册表。目标保存的 preset 和权限事件提供恢复配置。 -本决策替代[相邻消息](2026-08-27-adjacent-agent-steer-messaging.zh.md)与[子代理队友](2026-10-15-agent-teams-as-subagents.zh.md)中的邻接、发现及父子生命周期限制。它们的共享收件箱、调度、发送者归属和 Team 复用决策仍有效。成员使用 Session id 寻址;普通 Session 分叉领导独立 Team。 +本决策替代[相邻消息](2026-08-27-adjacent-agent-steer-messaging.zh.md)与[子代理队友](2026-10-15-agent-teams-as-subagents.zh.md)中的邻接、发现及父子生命周期限制。它们的共享收件箱、调度、发送者归属和 Team 复用决策仍有效。成员使用[目录名称](2026-11-02-subagent-catalog-names.zh.md)定义的名称或 Session id 互相寻址;普通 Session 分叉领导独立 Team。 ## Alternatives considered diff --git a/.agents/notes/implemented/architecture/2026-10-22-subagent-tool-owned-communication.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-22-subagent-tool-owned-communication.i18n.yaml index 81682a6981..90ff666645 100644 --- a/.agents/notes/implemented/architecture/2026-10-22-subagent-tool-owned-communication.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-10-22-subagent-tool-owned-communication.i18n.yaml @@ -9,8 +9,8 @@ en: ac391b569d2bc7e9 zh: a95f613aa2b330b8 /agent-note-subagent-tools-own-communication-guidance/decision: - en: 52fb33963bedfe07 - zh: 75fe66d80f47323f + en: d83310e85ffcb3cf + zh: eb1f069948399d93 /agent-note-subagent-tools-own-communication-guidance/alternatives-considered: en: c55d44c6414e0c4c zh: 9fa2ab31843e259b diff --git a/.agents/notes/implemented/architecture/2026-10-22-subagent-tool-owned-communication.md b/.agents/notes/implemented/architecture/2026-10-22-subagent-tool-owned-communication.md index 2222cd5dd7..5aa5ddca3d 100644 --- a/.agents/notes/implemented/architecture/2026-10-22-subagent-tool-owned-communication.md +++ b/.agents/notes/implemented/architecture/2026-10-22-subagent-tool-owned-communication.md @@ -10,7 +10,7 @@ The continuation manager inspected a Symbol on the standard messaging tool befor ## Decision -`tool-subagent-control` owns messaging instructions and parent addressing. Its logged runtime context gives delegated children their direct creator’s id from the Session header, explains when to send findings, and states that sending does not finish a turn. The context is emitted only for delegated children when the plugin’s own tool definition is admitted in the current scope and step; roots and ordinary Session forks receive none. Communication guidance follows inherited history without changing the shared system prompt or tool schema. +`tool-subagent-control` owns messaging instructions and parent addressing. Its logged runtime context gives delegated children their direct creator’s name from `ctx.subagents.namesOf()`, explains when to send findings, and states that sending does not finish a turn. The context is emitted only for delegated children when the plugin’s own tool definition is admitted in the current scope and step; roots and ordinary Session forks receive none. Communication guidance follows inherited history without changing the shared system prompt or tool schema. The manager owns result delivery. Local activations with `delivery: 'parent'` receive automatic-result guidance even without messaging tools; caller-owned activations receive no parent-delivery promise. Settlement timing, recipients, output, and suppression of closing text after a final message to the parent remain unchanged. Tool visibility grants no messaging authority. diff --git a/.agents/notes/implemented/architecture/2026-10-22-subagent-tool-owned-communication.zh.md b/.agents/notes/implemented/architecture/2026-10-22-subagent-tool-owned-communication.zh.md index 21242507b7..67f1146819 100644 --- a/.agents/notes/implemented/architecture/2026-10-22-subagent-tool-owned-communication.zh.md +++ b/.agents/notes/implemented/architecture/2026-10-22-subagent-tool-owned-communication.zh.md @@ -10,7 +10,7 @@ Status: implemented ## 决策 -`tool-subagent-control` 拥有通信说明和父级寻址。持久记录的运行时上下文从 Session header 读取委派子级的直接创建者 id,说明何时发送发现,并指出发送不会结束轮次。仅当本插件的工具定义在当前作用域和 step 中可用时,才向委派子级提供上下文;根代理和普通 Session fork 不接收。通信指导位于继承历史之后,不改变共享系统提示词或工具 schema。 +`tool-subagent-control` 拥有通信说明和父级寻址。持久记录的运行时上下文通过 `ctx.subagents.namesOf()` 提供委派子级的直接创建者名称,说明何时发送发现,并指出发送不会结束轮次。仅当本插件的工具定义在当前作用域和 step 中可用时,才向委派子级提供上下文;根代理和普通 Session fork 不接收。通信指导位于继承历史之后,不改变共享系统提示词或工具 schema。 管理器拥有结果交付。采用 `delivery: 'parent'` 的本地 activation 即使没有通信工具,也会收到自动结果交付指导;调用方拥有的 activation 不会收到父级交付承诺。结算时序、接收方、输出,以及最后向父级发送消息后省略结束文本的规则保持不变。工具可见性不授予通信权限。 diff --git a/.agents/notes/implemented/architecture/2026-11-02-subagent-catalog-names.i18n.yaml b/.agents/notes/implemented/architecture/2026-11-02-subagent-catalog-names.i18n.yaml new file mode 100644 index 0000000000..efb1367ad4 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-11-02-subagent-catalog-names.i18n.yaml @@ -0,0 +1,22 @@ +# Bilingual-pair consistency record for 2026-11-02-subagent-catalog-names.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-11-02-subagent-catalog-names.md +/agent-note-subagent-names-live-in-the-creator-s-catalog: + en: 987d2272bb47fe75 + zh: 54e701ecbaa60457 +/agent-note-subagent-names-live-in-the-creator-s-catalog/problem: + en: d4798e7bbbd5d49d + zh: b388470cf1ff33ac +/agent-note-subagent-names-live-in-the-creator-s-catalog/decision: + en: ff9771851b64b1ca + zh: a032e7f14207d8bd +/agent-note-subagent-names-live-in-the-creator-s-catalog/alternatives-considered: + en: 14b564fd848248da + zh: 5f65c05bbe22d581 +/agent-note-subagent-names-live-in-the-creator-s-catalog/consequences: + en: adddb4d296db5c44 + zh: ed8c6559c53fc6f2 +/agent-note-subagent-names-live-in-the-creator-s-catalog/testing: + en: a7963eadbfe52c21 + zh: 10bfc9fb28f6ae73 diff --git a/.agents/notes/implemented/architecture/2026-11-02-subagent-catalog-names.md b/.agents/notes/implemented/architecture/2026-11-02-subagent-catalog-names.md new file mode 100644 index 0000000000..0d06e7133b --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-11-02-subagent-catalog-names.md @@ -0,0 +1,45 @@ +# Agent Note: Subagent names live in the creator's catalog + +Status: implemented + +English | [中文](2026-11-02-subagent-catalog-names.zh.md) + +## Problem + +Models coordinate subagents through `send_message`, `interrupt_agent`, `list_agents`, and Team task owners. Those tools addressed every member by a 36-character Session id. Models copy such ids poorly: they cannot recall which id belongs to which task, they mistype ids, and every listing, receipt, notice, and identity statement spends tokens on them. Descriptions explain responsibilities but repeat and cannot address a member. Models need short, readable references that survive restarts and resolve to exactly one member, without a second execution identity. + +## Decision + +The Session id remains the only execution identity for routing, authorization, persistence, task ownership, and navigation. A name is a model-facing reference to it. + +Each new child has a name that is unique among its creator's children. The name is a field of the `subagent/catalog` entry that the creator already appends when it creates the child, using catalog payload version 3. Local and parent-delivered external creation write it; caller-delivered external runs enter no catalog and take no name. A name is 1–32 lower-case letters, digits, or hyphens starting with a letter, and is never `lead`. A 36-character UUID therefore never parses as a name. Omission selects the first free `agent-N`. An explicit name already held by a sibling fails with `DUPLICATE_AGENT_NAME`, and an invalid name fails with `INVALID_AGENT_NAME`. + +The creator reserves the name synchronously at admission, before startup awaits anything. The reservation lives in process memory beside the creator's committed catalog names and is released when the catalog entry commits or startup fails. No other write records the name. + +The root is always `lead`. A descendant's full name is the slash-separated path of names from the root without a `lead/` prefix, such as `reviewer/checker`. Sibling-unique segments make full names unique in the tree. A child whose catalog entry predates names, and every descendant below it, keeps its Session id as its name. + +`ctx.subagents.resolveName()` returns the Session id for `lead` or a full name by reading only the catalogs along the path; any other text is returned unchanged as a Session id. Tools resolve before they call messaging, interruption, or Team assignment, and those operations authorize the resulting Session id. Resolution grants no authority. Runtime-authored text names agents by full name, while durable attribution such as `senderSessionId` and Team `ownerId` keeps Session ids. The [subagent runtime](../../../../packages/subagent/subagent/README.md#agent-names) owns the API contract and the [upgrade guide](../../../../docs/upgrade-guide/v0.2.2-alpha.2/subagent-names/guide.md) owns caller migration. + +This supersedes the rejection of "unique names as sibling-routing opt-in" and the statement that creation accepts no member name in [subagent teammates](2026-10-15-agent-teams-as-subagents.md), and the Session-id identity text in [required runtime-context identity](2026-10-13-team-identity-runtime-context.md). Their single-identity, Team, and runtime-context retention decisions remain active. + +## Alternatives considered + +**Optional naming plugin with a root-Session binding event.** A separate plugin would record a name-to-Session binding in the root Session after the child starts. That adds a second write that can fail independently after startup succeeds, so a started child can lack its binding, and binding after start can report errors for children that are already running. The presentation plugin would then own cancellation of a lifecycle it does not control, would rewrite runtime-authored messages that name agents, and would still change model-visible schemas when it is absent, because the core tools must accept and render either form. Recording the name in the creator's existing catalog fact needs no second write, no rollback protocol, and no optional service. + +**Flat names unique across the tree.** A tree-wide flat namespace needs one registry at the root that every creation must consult and update, including creations by descendants whose root may be cold. Sibling-unique segments joined into paths are unique without a root registry, and each creator checks only its own catalog. + +**Recyclable names.** Releasing a name when its child settles would let a later sibling take it. A model that still remembers the earlier child would then message or interrupt a different agent. A name stays taken for as long as the catalog entry exists. + +**Automatic suffixes on conflict.** Turning a duplicate `reviewer` into `reviewer-2` would let creation succeed, but the model would believe it holds a name that the runtime never assigned. A duplicate fails so that the model chooses a name it knows. + +## Consequences + +Catalog payload version 3 is a same-format addition: `SESSION_FORMAT_VERSION` is unchanged, current readers accept versions 0 through 3, and releases that predate names reject version 3 loudly. The `subagentCatalog` projection state version is 7, so caches rebuild from Session events. The [persistence acknowledgement](../../../../docs/persistence-changes/2026-11-02-subagent-catalog-names.md) records the type change. + +Tool schemas grow by one optional `name` parameter on each delegation tool, and `send_message` and `interrupt_agent` rename `agent_id` to `agent`. These schema changes invalidate cached request prefixes once after upgrading. Identity runtime context, receipts, settlement notices, agent messages, and listing rows become shorter for shallow trees, while full names lengthen with nesting depth. `list_agents` drops parent ids and depth because a full name encodes both. + +Names cannot be renamed or recycled. Historical children have no names, and no migration assigns them. Name reservations are process-local, as is subagent residency. Tools still accept Session ids, so recorded Session ids and members below unnamed creators remain addressable. + +## Testing + +Runtime tests cover requested and default names, duplicate and invalid names rejected before startup, reservation across concurrent startup and release after failed startup, nested paths and `namesOf`, resolution of names, unknown text, and Session ids without granting authority, cold restoration of a member's path from the catalogs along its creation chain, and Session-id addressing below an unnamed historical creator. Catalog tests restore version-3 names beside unnamed historical entries, and the V4 format reader admits native version-3 catalog facts. Control-tool and Team-tool tests cover name-addressed messaging, listing rows, owner names, and historical owners shown by Session id. Recorded sessions pin the tool schemas, identity runtime context, receipts, listing rows, and Team policy text. diff --git a/.agents/notes/implemented/architecture/2026-11-02-subagent-catalog-names.zh.md b/.agents/notes/implemented/architecture/2026-11-02-subagent-catalog-names.zh.md new file mode 100644 index 0000000000..79a2c435b4 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-11-02-subagent-catalog-names.zh.md @@ -0,0 +1,45 @@ +# Agent Note: Subagent 名称记录在创建者目录中 + +Status: implemented + +[English](2026-11-02-subagent-catalog-names.md) | 中文 + +## Problem + +模型通过 `send_message`、`interrupt_agent`、`list_agents` 和 Team 任务所有者协调 subagent(子智能体)。这些工具曾以 36 字符的 Session id 寻址每个成员。模型难以准确复制这类 id:记不清哪个 id 对应哪个任务,会输错 id,而且每次列表、回执、通知和身份声明都要为其消耗 token。描述能说明职责,但可以重复,无法寻址成员。模型需要简短、可读、跨重启有效且只解析到唯一成员的引用,同时不引入第二种执行身份。 + +## Decision + +Session id 仍是路由、授权、持久化、任务归属和导航使用的唯一执行身份。名称是指向它的面向模型的引用。 + +每个新子级都有一个名称,在其创建者的子级之间唯一。名称是创建者在创建子级时本就会追加的 `subagent/catalog` 条目中的一个字段,使用第 3 版目录载荷。本地创建和父级投递的外部创建写入名称;调用方投递的外部执行不进入目录,也没有名称。名称由 1–32 个小写字母、数字或连字符组成,以字母开头,且绝不能是 `lead`。因此 36 字符的 UUID 永远不会被解析为名称。省略时选择第一个空闲的 `agent-N`。显式请求同级已持有的名称会以 `DUPLICATE_AGENT_NAME` 失败,无效名称以 `INVALID_AGENT_NAME` 失败。 + +创建者在准入时、启动等待任何操作之前同步预留名称。预留保存在进程内存中,与创建者已提交的目录名称并列,并在目录条目提交或启动失败时释放。没有其他写入记录该名称。 + +根的名称始终为 `lead`。后代的完整名称是从根开始、以斜杠分隔的名称路径,不带 `lead/` 前缀,例如 `reviewer/checker`。同级唯一的各段使完整名称在整棵树中唯一。目录条目早于名称机制的子级及其下所有后代继续以 Session id 作为名称。 + +`ctx.subagents.resolveName()` 只读取路径上的目录,为 `lead` 或完整名称返回 Session id;其他文本原样作为 Session id 返回。工具先解析名称,再调用消息、中断或 Team 分配,由这些操作对解析出的 Session id 授权。解析本身不授予权限。运行时撰写的文本以完整名称指称 agent,而 `senderSessionId` 和 Team `ownerId` 等持久归属仍保留 Session id。[subagent 运行时](../../../../packages/subagent/subagent/README.zh.md#agent-names)定义 API 约定,[升级指南](../../../../docs/upgrade-guide/v0.2.2-alpha.2/subagent-names/guide.zh.md)负责调用方迁移。 + +本决策取代 [Agent Teams 队友是子代理](2026-10-15-agent-teams-as-subagents.zh.md)中对“以唯一名称选择性启用同级路由”的否决以及“创建不接收成员名称”的表述,并取代[必需运行时身份](2026-10-13-team-identity-runtime-context.zh.md)中以 Session id 表述身份的文本。它们的单一身份、Team 和运行时上下文保留决策仍然有效。 + +## Alternatives considered + +**带根 Session 绑定事件的可选命名插件。** 独立插件会在子级启动后,在根 Session 中记录名称到 Session 的绑定。这会增加第二次写入,且可能在启动成功后单独失败,导致已启动的子级缺少绑定;启动后再绑定还可能为已在运行的子级报告错误。展示用插件因此要负责取消一个不受它控制的生命周期,要改写指称 agent 的运行时消息;而且即使插件不存在,面向模型的 schema 仍会改变,因为核心工具必须接受并渲染两种形式。把名称记录在创建者已有的目录事实中,不需要第二次写入、回滚协议或可选服务。 + +**全树唯一的扁平名称。** 全树扁平命名空间需要根上的一个注册表,每次创建都要查询并更新它,包括根可能处于冷状态时由后代发起的创建。同级唯一的各段连接成路径后无需根注册表即可唯一,每个创建者只检查自己的目录。 + +**可回收名称。** 子级结算后释放名称会让之后的同级取得该名称。仍记得先前子级的模型会因此向另一个 agent 发消息或发出中断。只要目录条目存在,名称就保持占用。 + +**冲突时自动加后缀。** 把重复的 `reviewer` 改为 `reviewer-2` 能让创建成功,但模型会以为自己持有一个运行时从未分配的名称。重复名称直接失败,模型才会选择一个自己知道的名称。 + +## Consequences + +第 3 版目录载荷是同格式增量:`SESSION_FORMAT_VERSION` 不变,当前读取器接受第 0 至第 3 版,早于名称机制的发布会明确拒绝第 3 版。`subagentCatalog` 投影状态版本为 7,因此缓存会从 Session 事件重建。[持久化确认记录](../../../../docs/persistence-changes/2026-11-02-subagent-catalog-names.zh.md)记录了该类型变更。 + +每个委派工具的 schema 增加一个可选 `name` 参数,`send_message` 和 `interrupt_agent` 把 `agent_id` 改名为 `agent`。这些 schema 变更会在升级后使缓存的请求前缀失效一次。对浅层树而言,身份运行时上下文、回执、结算通知、agent 消息和列表行都变短,完整名称则随嵌套深度变长。`list_agents` 不再携带父级 id 和深度,因为完整名称已包含这两项信息。 + +名称不能修改或回收。历史子级没有名称,也没有迁移为其分配名称。名称预留与 subagent 驻留一样仅限进程内。工具仍接受 Session id,因此已记录的 Session id 和无名创建者下的成员仍可寻址。 + +## Testing + +运行时测试覆盖请求名称与默认名称、启动前拒绝重复和无效名称、并发启动期间的预留和启动失败后的释放、嵌套路径与 `namesOf`、在不授予权限的前提下解析名称、未知文本和 Session id、从创建链上的目录冷恢复成员路径,以及无名历史创建者之下以 Session id 寻址。目录测试在无名历史条目旁恢复第 3 版名称,V4 格式读取器接纳原生第 3 版目录事实。控制工具和 Team 工具测试覆盖按名称发消息、列表行、所有者名称,以及以 Session id 显示的历史所有者。录制会话固定工具 schema、身份运行时上下文、回执、列表行和 Team 策略文本。 diff --git a/apps/cli/tests/profiles/acp/tests/goal-expected/goal-round-driver/session.expected.jsonl b/apps/cli/tests/profiles/acp/tests/goal-expected/goal-round-driver/session.expected.jsonl index 8467970106..59a1c137aa 100644 --- a/apps/cli/tests/profiles/acp/tests/goal-expected/goal-round-driver/session.expected.jsonl +++ b/apps/cli/tests/profiles/acp/tests/goal-expected/goal-round-driver/session.expected.jsonl @@ -8,7 +8,7 @@ {"type":"step/start","data":{"turn":1,"step":1}} {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Create a durable two-round goal for the ACP snapshot, inspect it, then report readiness."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Create a durable two-round goal","messageSeqs":[8],"source":{"kind":"fallback"}}} diff --git a/apps/cli/tests/profiles/acp/tests/goal-expected/goal-wrapup/session.expected.jsonl b/apps/cli/tests/profiles/acp/tests/goal-expected/goal-wrapup/session.expected.jsonl index a4ac54b8c1..acac6db06d 100644 --- a/apps/cli/tests/profiles/acp/tests/goal-expected/goal-wrapup/session.expected.jsonl +++ b/apps/cli/tests/profiles/acp/tests/goal-expected/goal-wrapup/session.expected.jsonl @@ -8,7 +8,7 @@ {"type":"step/start","data":{"turn":1,"step":1}} {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Create a durable goal for the wrap-up snapshot, then report readiness."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Create a durable goal for","messageSeqs":[8],"source":{"kind":"fallback"}}} diff --git a/apps/cli/tests/profiles/acp/tests/image-offload.expected.e2e.ts b/apps/cli/tests/profiles/acp/tests/image-offload.expected.e2e.ts index 63715e4638..2d23d1bfa7 100644 --- a/apps/cli/tests/profiles/acp/tests/image-offload.expected.e2e.ts +++ b/apps/cli/tests/profiles/acp/tests/image-offload.expected.e2e.ts @@ -140,13 +140,13 @@ it('pins Messages Files offload and inline fallback in assembled requests', asyn const fileImage = { type: 'image', source: { type: 'file', file_id: 'file-api-snapshot-1' } } const offloadedImage = `[image omitted to fit request image limits; ${attachmentId}.${normalizedAccess}]` const imageHandle = `Image ${attachmentId}; request preview 1x1px.${normalizedAccess}` - const runtimeContext = (cwd: string, sessionId: string): string => 'Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\n' + const runtimeContext = (cwd: string): string => 'Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\n' + `Current working directory: ${JSON.stringify(cwd)}.\n\n` + 'Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\n' + 'Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\n' - + `You are the root agent. Your agent id is ${JSON.stringify(sessionId)}.\n\n` - + 'To message other agents, use send_message({ agent_id: "", message: "..." }).\n' - + 'Use list_agents({}) to find your collaborator agents and their ids.' + + 'You are the root agent. Your name is "lead".\n\n' + + 'To message other agents, use send_message({ agent: "", message: "..." }).\n' + + 'Use list_agents({}) to find your collaborator agents and their names.' const messages = requests[0]?.messages as { content?: unknown }[] | undefined const offloaded = messages?.find(message => JSON.stringify(message.content).includes('[image omitted')) expect(offloaded?.content).toEqual([ @@ -156,7 +156,7 @@ it('pins Messages Files offload and inline fallback in assembled requests', asyn { type: 'text', text: imageHandle }, fileImage, { type: 'text', text: ', then use read_image on red.png and reply with DONE.' }, - { type: 'text', text: runtimeContext(result.cwd, result.sessionId) }, + { type: 'text', text: runtimeContext(result.cwd) }, ]) const followup = requests[1]?.messages as Array<{ role: string; content: Array> }> @@ -208,7 +208,7 @@ it('pins Messages Files offload and inline fallback in assembled requests', asyn { type: 'text', text: `Image ${attachmentId}; request preview 1x1px.${fallbackAccess}` }, { type: 'image', source: { type: 'base64', media_type: 'image/png', data: image } }, { type: 'text', text: ', then use read_image on red.png and reply with DONE.' }, - { type: 'text', text: runtimeContext(fallback.cwd, fallback.sessionId) }, + { type: 'text', text: runtimeContext(fallback.cwd) }, ]) } finally { await new Promise(resolve => server.close(() => { resolve() })) diff --git a/apps/cli/tests/profiles/headless/tests/expected/goal-tools/stream-json.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/goal-tools/stream-json.expected.jsonl index 7779eb2181..7323185cf5 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/goal-tools/stream-json.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/goal-tools/stream-json.expected.jsonl @@ -4,7 +4,7 @@ {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Probe strict-schema fillers against missing-goal revision 1, then create a durable goal to finish the snapshot proof and inspect it."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"Probe strict-schema fillers against miss","messageSeqs":[8],"source":{"kind":"fallback"}}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/headless-profile/session.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/headless-profile/session.expected.jsonl index d4385b311d..233b696634 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/headless-profile/session.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/headless-profile/session.expected.jsonl @@ -8,7 +8,7 @@ {"type":"step/start","data":{"turn":1,"step":1}} {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Prove the product headless profile path with one real tool round trip."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"cli-mock","model":"cli-mock","reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"cli-mock","model":"cli-mock"}} {"type":"session/title","data":{"title":"Prove the product headless profile","messageSeqs":[8],"source":{"kind":"fallback"}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/invalid-credential/stream-json.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/invalid-credential/stream-json.expected.jsonl index 6cebc92a87..117f517cb8 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/invalid-credential/stream-json.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/invalid-credential/stream-json.expected.jsonl @@ -4,7 +4,7 @@ {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"say pong"}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":"{{tools}}"},"reason":"initial"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":1000000}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"say pong","messageSeqs":[8],"source":{"kind":"fallback"}}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/missing-credential/stream-json.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/missing-credential/stream-json.expected.jsonl index 8cc16706c1..39a2f16c0d 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/missing-credential/stream-json.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/missing-credential/stream-json.expected.jsonl @@ -4,7 +4,7 @@ {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"say pong"}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":"{{tools}}"},"reason":"initial"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":1000000}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"say pong","messageSeqs":[8],"source":{"kind":"fallback"}}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/provider-retry/stream-json.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/provider-retry/stream-json.expected.jsonl index 8488837003..e6861c5f68 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/provider-retry/stream-json.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/provider-retry/stream-json.expected.jsonl @@ -4,7 +4,7 @@ {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"retry the transient provider failure"}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"retry the transient provider failure","messageSeqs":[8],"source":{"kind":"fallback"}}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/semantic-checkpoint/session.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/semantic-checkpoint/session.expected.jsonl index c14e345b7f..b8c1abf034 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/semantic-checkpoint/session.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/semantic-checkpoint/session.expected.jsonl @@ -18,7 +18,7 @@ {"type":"step/start","data":{"turn":2,"step":1}} {"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}} {"type":"user/message","data":{"content":[{"type":"text","text":"Continue safely from the interrupted operation."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Perform one side-effecting remote mutati","messageSeqs":[3],"source":{"kind":"fallback"}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-diagnostic/parent.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/subagent-diagnostic/parent.expected.jsonl index 57f93111e7..db32c5f5bc 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-diagnostic/parent.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-diagnostic/parent.expected.jsonl @@ -16,13 +16,13 @@ {"type":"step/start","data":{"turn":2,"step":1}} {"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}} {"type":"user/message","data":{"content":[{"type":"text","text":"Call list_agents once and report what it shows."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Start a background job.","messageSeqs":[3],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"list-once","name":"list_agents","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"list-once","name":"list_agents","args":["{}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"list-once","name":"list_agents","arguments":"{}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":2,"step":1,"callId":"list-once","name":"list_agents","arguments":"{}"}} -{"type":"tool/result","data":{"turn":2,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"list-once"},"toolCallId":"list-once","content":[{"type":"text","text":"{{sessionId}} [running] — root agent\n{{sessionId}} [inactive] parent={{sessionId}} depth=1 — descriptorless child"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[22],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":2,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"list-once"},"toolCallId":"list-once","content":[{"type":"text","text":"lead [running] — root agent\n{{sessionId}} [inactive] — descriptorless child"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[22],"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"step/start","data":{"turn":2,"step":2}} {"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"The catalogued subagent is ready. PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["The catalogued subagent is ready. PARENT_DONE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"The catalogued subagent is ready. PARENT_DONE"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/child.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/child.expected.jsonl index 27712bcd00..d71a2334f8 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/child.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/child.expected.jsonl @@ -8,7 +8,7 @@ {"type":"step/start","data":{"turn":1,"step":1}} {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a subagent delegated by agent \"{{sessionId}}\".\nYour agent id is \"{{sessionId}}\".\nThe root agent of your collaboration tree is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids.\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:identity","text":"You are a subagent delegated by agent \"{{sessionId}}\".\nYour agent id is \"{{sessionId}}\".\nThe root agent of your collaboration tree is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a subagent delegated by agent \"lead\".\nYour name is \"agent-1\".\nThe root agent of your collaboration tree is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names.\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:identity","text":"You are a subagent delegated by agent \"lead\".\nYour name is \"agent-1\".\nThe root agent of your collaboration tree is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"low"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Use the write tool exactly","messageSeqs":[8],"source":{"kind":"fallback"}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/parent.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/parent.expected.jsonl index f8796478f5..f3402014fd 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/parent.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-inheritance/parent.expected.jsonl @@ -16,20 +16,20 @@ {"type":"step/start","data":{"turn":2,"step":1}} {"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}} {"type":"user/message","data":{"content":[{"type":"text","text":"Delegate the write probe to a subagent."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"low"},"tools":["apply_patch","bash","create_goal","edit","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","wait_agent","web_fetch","web_search","workflow","working_directory","write"]},"reason":"resume","startsSeries":true}} {"type":"developer/message","data":{"turn":2,"step":1,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"apply_patch"},{"type":"tool-addition","toolName":"bash"},{"type":"tool-addition","toolName":"create_goal"},{"type":"tool-addition","toolName":"edit"},{"type":"tool-addition","toolName":"get_goal"},{"type":"tool-addition","toolName":"glob"},{"type":"tool-addition","toolName":"grep"},{"type":"tool-addition","toolName":"interrupt_agent"},{"type":"tool-addition","toolName":"job_kill"},{"type":"tool-addition","toolName":"job_list"},{"type":"tool-addition","toolName":"job_output"},{"type":"tool-addition","toolName":"list_agents"},{"type":"tool-addition","toolName":"read"},{"type":"tool-addition","toolName":"read_image"},{"type":"tool-addition","toolName":"send_message"},{"type":"tool-addition","toolName":"skill"},{"type":"tool-addition","toolName":"subagent"},{"type":"tool-addition","toolName":"subagent_fork"},{"type":"tool-addition","toolName":"todo_write"},{"type":"tool-addition","toolName":"update_goal"},{"type":"tool-addition","toolName":"wait_agent"},{"type":"tool-addition","toolName":"web_fetch"},{"type":"tool-addition","toolName":"web_search"},{"type":"tool-addition","toolName":"workflow"},{"type":"tool-addition","toolName":"working_directory"},{"type":"tool-addition","toolName":"write"}],"role":"developer","id":"{{sessionId}}"},"headerSeq":18},"surfaceOp":"append"} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Tighten this session to read-only.","messageSeqs":[3],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"delegate-write","name":"subagent","arguments":"{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"delegate-write","name":"subagent","args":["{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"delegate-write","name":"subagent","arguments":"{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":2,"step":1,"callId":"delegate-write","name":"subagent","arguments":"{\"description\": \"Delegated write probe\", \"prompt\": \"Use the write tool exactly once with file_path set to exactly the relative path inherited.txt and content escaped. If the write is denied, reply with the single word CHILD_DENIED and the denial marker line; do not retry and do not request escalation. If it succeeds, reply CHILD_WROTE.\"}"}} -{"type":"subagent/catalog","data":{"version":0,"childId":"{{sessionId}}","childCreatedAt":0,"mode":"continuable","label":"Delegated write probe"}} -{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_DENIED [sandbox: file access denied under read-only mode]"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","stopReason":"completed","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"}]}} -{"type":"tool/result","data":{"turn":2,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"delegate-write"},"toolCallId":"delegate-write","content":[{"type":"text","text":"started subagent {{sessionId}}"}],"isError":false,"id":"{{sessionId}}"},"meta":{"kind":"activation","subagentId":"{{sessionId}}"}},"sourceEventSeqs":[23],"surfaceOp":"append"} +{"type":"subagent/catalog","data":{"version":3,"childId":"{{sessionId}}","childCreatedAt":0,"mode":"continuable","label":"Delegated write probe","name":"agent-1"}} +{"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent agent-1 finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_DENIED [sandbox: file access denied under read-only mode]"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent agent-1 finished and will do no further work unless you send it more.","stopReason":"completed","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"}]}} +{"type":"tool/result","data":{"turn":2,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"delegate-write"},"toolCallId":"delegate-write","content":[{"type":"text","text":"started subagent agent-1"}],"isError":false,"id":"{{sessionId}}"},"meta":{"kind":"activation","subagentId":"{{sessionId}}","name":"agent-1"}},"sourceEventSeqs":[23],"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":1}} {"type":"agent/inbox/spliced","data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}} {"type":"step/start","data":{"turn":2,"step":2}} -{"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_DENIED [sandbox: file access denied under read-only mode]"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","stopReason":"completed","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Background subagent agent-1 finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_DENIED [sandbox: file access denied under read-only mode]"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent agent-1 finished and will do no further work unless you send it more.","stopReason":"completed","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":2,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"The delegated child was denied by the sandbox. PARENT_DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["The delegated child was denied by the sandbox. PARENT_DONE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"The delegated child was denied by the sandbox. PARENT_DONE"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":2,"step":2}} {"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/child.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/child.expected.jsonl index 2305a8a7c2..b968ceddde 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/child.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/child.expected.jsonl @@ -8,7 +8,7 @@ {"type":"step/start","data":{"turn":1,"step":1}} {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message."},{"type":"text","text":"Your final answer is delivered automatically if your parent is still online. Include the result and relevant evidence in that answer; the parent does not receive your full transcript, tool output, or reasoning. If your last action is a message to your parent, the completion notice omits your final answer; include the result and relevant evidence in that message."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a subagent delegated by agent \"{{sessionId}}\".\nYour agent id is \"{{sessionId}}\".\nThe root agent of your collaboration tree is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids.\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:identity","text":"You are a subagent delegated by agent \"{{sessionId}}\".\nYour agent id is \"{{sessionId}}\".\nThe root agent of your collaboration tree is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).\n\nYou are a subagent delegated by agent \"lead\".\nYour name is \"agent-1\".\nThe root agent of your collaboration tree is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names.\n\nDelegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly.\n\nDelegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly.\n\nYou are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."},{"name":"subagent:identity","text":"You are a subagent delegated by agent \"lead\".\nYour name is \"agent-1\".\nThe root agent of your collaboration tree is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."},{"name":"tool:subagent:depth","text":"Delegation depth for `subagent`: current 1, maximum 1, remaining levels 0. Do not call `subagent` at this depth; carry out the work directly."},{"name":"tool:subagent_fork:depth","text":"Delegation depth for `subagent_fork`: current 1, maximum 1, remaining levels 0. Do not call `subagent_fork` at this depth; carry out the work directly."},{"name":"subagent:delegation","text":"You are a delegated subagent: your permission scope was fixed when you were started and cannot be widened from inside this session — operations that require approval are rejected automatically. When the task needs access beyond that scope, do not retry the denied operation; state the limitation in your reply so the delegating agent can handle it."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Reply with exactly CHILD_RESULT and","messageSeqs":[8],"source":{"kind":"fallback"}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl index f05ae06cef..c4262eed5a 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/subagent-settlement/stream-json.expected.jsonl @@ -4,7 +4,7 @@ {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":6,"time":0,"data":{"turn":1,"step":1}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"system/message","seq":7,"time":0,"data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Start one continuable subagent and acknowledge its completion notice. Do not call list_agents or send_message."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":9,"time":0,"data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"Start one continuable subagent and","messageSeqs":[8],"source":{"kind":"fallback"}}}} @@ -12,16 +12,16 @@ {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":14,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"start-child","name":"subagent","args":["{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":15,"time":0,"data":{"turn":1,"step":1,"callId":"start-child","name":"subagent","arguments":"{\"description\":\"Return child result\",\"prompt\":\"Reply with exactly CHILD_RESULT and nothing else. Do not call send_message.\"}"}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"session/title","seq":16,"time":0,"data":{"title":"Subagent settlement","messageSeqs":[8],"source":{"kind":"provider","provider":"session-title-first-prompt-llm","model":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"subagent/catalog","seq":17,"time":0,"data":{"version":0,"childId":"{{sessionId}}","childCreatedAt":0,"mode":"continuable","label":"Return child result"}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":18,"time":0,"data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"start-child"},"toolCallId":"start-child","content":[{"type":"text","text":"started subagent {{sessionId}}"}],"isError":false,"id":"{{sessionId}}"},"meta":{"kind":"activation","subagentId":"{{sessionId}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"subagent/catalog","seq":17,"time":0,"data":{"version":3,"childId":"{{sessionId}}","childCreatedAt":0,"mode":"continuable","label":"Return child result","name":"agent-1"}}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":18,"time":0,"data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"start-child"},"toolCallId":"start-child","content":[{"type":"text","text":"started subagent agent-1"}],"isError":false,"id":"{{sessionId}}"},"meta":{"kind":"activation","subagentId":"{{sessionId}}","name":"agent-1"}},"sourceEventSeqs":[15],"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":19,"time":0,"data":{"turn":1,"step":1}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":20,"time":0,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_RESULT"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","stopReason":"completed","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"}]}}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":20,"time":0,"data":{"target":"next-step","start":0,"inserted":[{"content":[{"type":"text","text":"Background subagent agent-1 finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_RESULT"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent agent-1 finished and will do no further work unless you send it more.","stopReason":"completed","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"}]}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":21,"time":0,"data":{"turn":1,"step":2}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":22,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"STARTED"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["STARTED"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"STARTED"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":23,"time":0,"data":{"turn":1,"step":2}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"agent/inbox/spliced","seq":24,"time":0,"data":{"target":"next-step","start":0,"removedCount":1,"inserted":[]}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/start","seq":25,"time":0,"data":{"turn":1,"step":3}}} -{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":26,"time":0,"data":{"content":[{"type":"text","text":"Background subagent {{sessionId}} finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_RESULT"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent {{sessionId}} finished and will do no further work unless you send it more.","stopReason":"completed","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} +{"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"user/message","seq":26,"time":0,"data":{"content":[{"type":"text","text":"Background subagent agent-1 finished and will do no further work unless you send it more."},{"type":"text","text":"Its closing message:"},{"type":"text","text":"CHILD_RESULT"}],"source":{"kind":"subagent-settled","form":"notice","summary":"Background subagent agent-1 finished and will do no further work unless you send it more.","stopReason":"completed","senderSessionId":"{{sessionId}}"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":27,"time":0,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"PARENT_RECEIVED_SETTLEMENT_NOTICE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["PARENT_RECEIVED_SETTLEMENT_NOTICE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"PARENT_RECEIVED_SETTLEMENT_NOTICE"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"step/end","seq":28,"time":0,"data":{"turn":1,"step":3}}} {"type":"session_event","sessionId":"{{sessionId}}","event":{"type":"turn/end","seq":29,"time":0,"data":{"turn":1,"reason":{"kind":"completed"}}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/offline-edit/session.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/offline-edit/session.expected.jsonl index 56b127865a..5a57e4bee6 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/offline-edit/session.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/offline-edit/session.expected.jsonl @@ -17,7 +17,7 @@ {"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}} {"type":"user/message","data":{"content":[{"type":"text","text":"Acknowledge the current workspace instruction."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"\nUpdated instructions from: AGENTS.md\n\nThis file changed after it was loaded. Use the following content instead of the previously loaded instructions from this file.\n\nNew workspace instruction after offline edit.\n\n"}],"source":{"kind":"agent-instructions","form":"instructions","changes":[{"action":"replace","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"d8375b516f158718bd3463bc8eb7ed42c011b29f"}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Remember the workspace instruction.","messageSeqs":[3],"source":{"kind":"fallback"}}} diff --git a/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/precedence-change/session.expected.jsonl b/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/precedence-change/session.expected.jsonl index dfe1d171a9..b70ee4a8df 100644 --- a/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/precedence-change/session.expected.jsonl +++ b/apps/cli/tests/profiles/headless/tests/expected/workspace-context-resume/precedence-change/session.expected.jsonl @@ -17,7 +17,7 @@ {"type":"system/message","data":{"turn":2,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[2],"surfaceOp":{"op":"replace","startSeq":2,"endSeq":2}} {"type":"user/message","data":{"content":[{"type":"text","text":"Acknowledge the current workspace instruction."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"\nThis complete workspace instruction baseline replaces all earlier workspace instruction baselines. The following workspace instructions may be relevant to your work. Use them as guidance when applicable. More specific instructions take precedence over broader ones. They do not override system, developer, or direct user instructions.\n\nInstructions from: AGENTS.md\n\nCurrent AGENTS rule.\n\n\nInstructions from: CLAUDE.md\n\nCurrent CLAUDE rule.\n\n"}],"source":{"kind":"agent-instructions","form":"instructions","baseline":true,"baselineIdentity":"{\"projectRoot\":\"\",\"projectRootMarkers\":[\".git\"],\"maxBytes\":65536,\"maxSourceBytes\":1048576,\"instructionFileCandidates\":[\"AGENTS.md\",\"CLAUDE.md\"],\"localInstructionFileCandidates\":[\"AGENTS.local.md\",\"CLAUDE.local.md\"]}","changes":[{"action":"set","scope":".\u0000AGENTS.md","path":"AGENTS.md","digest":"7f53d2327837129750aef117f9754a001c46cf68"},{"action":"set","scope":".\u0000CLAUDE.md","path":"CLAUDE.md","digest":"5b1e9e3fd759eee6b43ceff899e47fb10c64701a"}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your agent id is \"{{sessionId}}\".\n\nTo message other agents, use send_message({ agent_id: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their ids."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable.\n\nApproval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed.\n\nYou are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: workspace-write. Any available operation enforced by the DSH file sandbox may modify files under the session workspace: \"{{cwd}}\". Some platform temporary areas may also be writable."},{"name":"approval:policy","text":"Approval prompts are enabled in this session: operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."},{"name":"subagent:identity","text":"You are the root agent. Your name is \"lead\".\n\nTo message other agents, use send_message({ agent: \"\", message: \"...\" }).\nUse list_agents({}) to find your collaborator agents and their names."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial","startsSeries":true}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Remember the workspace instruction.","messageSeqs":[3],"source":{"kind":"fallback"}}} diff --git a/apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs b/apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs index 62751cd93f..a6b334193a 100644 --- a/apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs +++ b/apps/cli/tests/profiles/headless/tests/fixtures/team-llm.mjs @@ -87,7 +87,7 @@ function researcher(messages) { if (!names.includes('send_message')) { return toolChunks([ { name: 'team_task_update', args: { task_id: 'task-1', expected_revision: 2, action: 'complete' } }, - { name: 'send_message', args: { agent_id: sibling, message: 'Research complete: use the deterministic finding.' } }, + { name: 'send_message', args: { agent: sibling, message: 'Research complete: use the deterministic finding.' } }, ]) } return textChunks('Research teammate complete.') @@ -134,7 +134,7 @@ function implementer(messages, parentId) { if (!names.includes('send_message')) { return toolChunks([ { name: 'team_task_update', args: { task_id: 'task-2', expected_revision: 2, action: 'complete' } }, - { name: 'send_message', args: { agent_id: parentId, message: 'Implementation complete and verified.' } }, + { name: 'send_message', args: { agent: parentId, message: 'Implementation complete and verified.' } }, ]) } return textChunks('Implementation teammate complete.') @@ -203,7 +203,7 @@ class TeamFixtureAdapter extends LlmAdapter { if (workflowChild) chunks = textChunks('Fresh workflow child complete.') else if (task?.startsWith('RESEARCHER_MARK:')) chunks = researcher(options.messages) else if (task?.startsWith('IMPLEMENTER_MARK:')) chunks = implementer(options.messages, parentId) - else if (role?.startsWith('You are the root agent. Your agent id is')) chunks = lead(options.messages) + else if (role?.startsWith('You are the root agent. Your name is')) chunks = lead(options.messages) else throw new Error('Team runtime context is missing a recognized identity') for (const chunk of chunks) { options.signal?.throwIfAborted() diff --git a/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts b/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts index 9f53fb980a..69c95ef485 100644 --- a/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts +++ b/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts @@ -1254,18 +1254,18 @@ describe('headless stream-json snapshots', () => { "checkedAgents": true, "deliveredMessages": 0, "identities": [ - "You are a subagent delegated by agent "". - Your agent id is "". - The root agent of your collaboration tree is "". + "You are a subagent delegated by agent "lead". + Your name is "agent-2". + The root agent of your collaboration tree is "lead". - To message other agents, use send_message({ agent_id: "", message: "..." }). - Use list_agents({}) to find your collaborator agents and their ids.", - "You are a subagent delegated by agent "". - Your agent id is "". - The root agent of your collaboration tree is "". + To message other agents, use send_message({ agent: "", message: "..." }). + Use list_agents({}) to find your collaborator agents and their names.", + "You are a subagent delegated by agent "lead". + Your name is "agent-3". + The root agent of your collaboration tree is "lead". - To message other agents, use send_message({ agent_id: "", message: "..." }). - Use list_agents({}) to find your collaborator agents and their ids.", + To message other agents, use send_message({ agent: "", message: "..." }). + Use list_agents({}) to find your collaborator agents and their names.", ], "queuedMessages": 0, "sessions": 4, diff --git a/apps/cli/tests/profiles/headless/tests/subagent-diagnostic.expected.e2e.ts b/apps/cli/tests/profiles/headless/tests/subagent-diagnostic.expected.e2e.ts index 3aff474038..c89eb1703c 100644 --- a/apps/cli/tests/profiles/headless/tests/subagent-diagnostic.expected.e2e.ts +++ b/apps/cli/tests/profiles/headless/tests/subagent-diagnostic.expected.e2e.ts @@ -132,8 +132,8 @@ describe('parent-owned cold child catalog snapshot', () => { if (parent === undefined) throw new Error('missing persisted parent log') // Discovery reads child catalogs without requiring activation descriptors. - expect(parent).toContain(`${childId} [inactive] parent=${parentId} depth=1 — descriptorless child`) - expect(parent).not.toContain('[diagnostic: corrupt]') + expect(parent).toContain(`${childId} [inactive] — descriptorless child`) + expect(parent).not.toContain(`${childId} [corrupt]`) const context: NormalizeContext = { sessionIds: [parentId, childId], cwd } const normalizedParent = normalizeSessionSnapshot(parent, context) diff --git a/apps/web/tests/agent-team-panel.e2e.ts b/apps/web/tests/agent-team-panel.e2e.ts index 6da971cf03..a68c1908a7 100644 --- a/apps/web/tests/agent-team-panel.e2e.ts +++ b/apps/web/tests/agent-team-panel.e2e.ts @@ -420,7 +420,7 @@ describe('web e2e: agent teamwork panel', () => { taskId: task.id, expectedRevision: task.revision, action: 'claim', }) await row.getByRole('img', { name: 'In progress', exact: true }).waitFor() - await row.locator('[data-team-task-owner]').filter({ hasText: agent.id }).waitFor() + await row.locator('[data-team-task-owner]').filter({ hasText: 'Owner: lead' }).waitFor() await action.getByRole('heading', { name: 'Shared tasks 1 in progress', exact: true }).waitFor() expect(await action.getByRole('button', { name: /^(New task|Edit|Complete|Reopen|Delete)$/u }).count()).toBe(0) expect(await action.locator('input, select, textarea').count()).toBe(0) @@ -762,7 +762,7 @@ describe('web e2e: agent teamwork panel', () => { expect(await row.getAttribute('data-expanded')).toBe('false') expect(await row.locator('[data-team-task-details]').count()).toBe(0) expect(await board.locator('[data-team-task-details]').count()).toBe(1) - expect(await emptyRow.locator('[data-team-task-owner]').textContent()).toBe(`Owner: ${agent.id}`) + expect(await emptyRow.locator('[data-team-task-owner]').textContent()).toBe('Owner: lead') expect(await emptyRow.locator('[data-team-task-details] p:not([data-team-task-owner])').textContent()).toBe(completed.description) expect(await emptyRow.locator('dl, dt, dd').count()).toBe(0) expect(await emptyRow.locator('[data-team-task-details]').textContent()).not.toContain(completed.id) diff --git a/apps/web/tests/expected/subagent-conversation/navigation-only.expected.md b/apps/web/tests/expected/subagent-conversation/navigation-only.expected.md index 76df6f6c57..3474a2e8dd 100644 --- a/apps/web/tests/expected/subagent-conversation/navigation-only.expected.md +++ b/apps/web/tests/expected/subagent-conversation/navigation-only.expected.md @@ -1,8 +1,8 @@ - dialog "Subagent sessions": - tree "Subagent sessions": - - treeitem "event-sourcing researcher {{uuid}} · continuable · not running 15.7K tok · {{duration}}" [level=1]: + - treeitem "event-sourcing researcher agent-1 · continuable · not running 15.7K tok · {{duration}}" [level=1]: - button "Expand event-sourcing researcher descendants" - - text: event-sourcing researcher {{uuid}} 15.7K tok {{duration}} + - text: event-sourcing researcher agent-1 15.7K tok {{duration}} - treeitem "event-sourcing reviewer recorded-one-shot · one-shot · completed 0 tok · {{duration}}" [level=1]: - text: event-sourcing reviewer recorded-one-shot 0 tok ~6mo 12d - button "Open event-sourcing reviewer in sidebar" diff --git a/apps/web/tests/expected/subagent-result-notice/en.expected.md b/apps/web/tests/expected/subagent-result-notice/en.expected.md index a44d176b78..bd1394eded 100644 --- a/apps/web/tests/expected/subagent-result-notice/en.expected.md +++ b/apps/web/tests/expected/subagent-result-notice/en.expected.md @@ -2,37 +2,37 @@ - text: Subagent run completed - time: {{clock}} - paragraph: This notification triggered this response. -- text: Background subagent notice-result-child finished and will do no further work unless you send it more.Its closing message:## Research result The original child reply stays available. +- text: Background subagent agent-1 finished and will do no further work unless you send it more.Its closing message:## Research result The original child reply stays available. - button "Subagent run stopped {{clock}}" [expanded]: - text: Subagent run stopped - time: {{clock}} - paragraph: This notification triggered this response. -- text: Background subagent notice-result-child was stopped before it finished.Its closing message:## Research result The original child reply stays available. +- text: Background subagent agent-1 was stopped before it finished.Its closing message:## Research result The original child reply stays available. - button "Subagent run failed {{clock}}" [expanded]: - text: Subagent run failed - time: {{clock}} - paragraph: This notification triggered this response. -- text: Background subagent notice-result-child failed before it finished.Its closing message:## Research result The original child reply stays available. +- text: Background subagent agent-1 failed before it finished.Its closing message:## Research result The original child reply stays available. - button "Subagent reached the token limit {{clock}}" [expanded]: - text: Subagent reached the token limit - time: {{clock}} - paragraph: This notification triggered this response. -- text: Background subagent notice-result-child ran out of room before it finished.Its closing message:## Research result The original child reply stays available. +- text: Background subagent agent-1 ran out of room before it finished.Its closing message:## Research result The original child reply stays available. - button "Subagent declined the task {{clock}}" [expanded]: - text: Subagent declined the task - time: {{clock}} - paragraph: This notification triggered this response. -- text: Background subagent notice-result-child declined the task.Its closing message:## Research result The original child reply stays available. +- text: Background subagent agent-1 declined the task.Its closing message:## Research result The original child reply stays available. - button "Subagent run completed {{clock}}" [expanded]: - text: Subagent run completed - time: {{clock}} - paragraph: This notification triggered this response. -- text: Background subagent notice-result-child finished and will do no further work unless you send it more.It left no closing message. +- text: Background subagent agent-1 finished and will do no further work unless you send it more.It left no closing message. - button "Subtask status updated {{clock}}" [expanded]: - text: Subtask status updated diff --git a/apps/web/tests/expected/subagent-result-notice/zh.expected.md b/apps/web/tests/expected/subagent-result-notice/zh.expected.md index d26df1f590..a6e893576a 100644 --- a/apps/web/tests/expected/subagent-result-notice/zh.expected.md +++ b/apps/web/tests/expected/subagent-result-notice/zh.expected.md @@ -2,37 +2,37 @@ - text: 子智能体已完成任务 - time: {{clock}} - paragraph: 这条通知触发了本轮回复。 -- text: Background subagent notice-result-child finished and will do no further work unless you send it more.Its closing message:## Research result The original child reply stays available. +- text: Background subagent agent-1 finished and will do no further work unless you send it more.Its closing message:## Research result The original child reply stays available. - button "子智能体任务已停止 {{clock}}" [expanded]: - text: 子智能体任务已停止 - time: {{clock}} - paragraph: 这条通知触发了本轮回复。 -- text: Background subagent notice-result-child was stopped before it finished.Its closing message:## Research result The original child reply stays available. +- text: Background subagent agent-1 was stopped before it finished.Its closing message:## Research result The original child reply stays available. - button "子智能体执行失败 {{clock}}" [expanded]: - text: 子智能体执行失败 - time: {{clock}} - paragraph: 这条通知触发了本轮回复。 -- text: Background subagent notice-result-child failed before it finished.Its closing message:## Research result The original child reply stays available. +- text: Background subagent agent-1 failed before it finished.Its closing message:## Research result The original child reply stays available. - button "子智能体已达 token 上限 {{clock}}" [expanded]: - text: 子智能体已达 token 上限 - time: {{clock}} - paragraph: 这条通知触发了本轮回复。 -- text: Background subagent notice-result-child ran out of room before it finished.Its closing message:## Research result The original child reply stays available. +- text: Background subagent agent-1 ran out of room before it finished.Its closing message:## Research result The original child reply stays available. - button "子智能体拒绝执行 {{clock}}" [expanded]: - text: 子智能体拒绝执行 - time: {{clock}} - paragraph: 这条通知触发了本轮回复。 -- text: Background subagent notice-result-child declined the task.Its closing message:## Research result The original child reply stays available. +- text: Background subagent agent-1 declined the task.Its closing message:## Research result The original child reply stays available. - button "子智能体已完成任务 {{clock}}" [expanded]: - text: 子智能体已完成任务 - time: {{clock}} - paragraph: 这条通知触发了本轮回复。 -- text: Background subagent notice-result-child finished and will do no further work unless you send it more.It left no closing message. +- text: Background subagent agent-1 finished and will do no further work unless you send it more.It left no closing message. - button "子任务状态更新 {{clock}}" [expanded]: - text: 子任务状态更新 diff --git a/apps/web/tests/official-subagent-bundle.e2e.ts b/apps/web/tests/official-subagent-bundle.e2e.ts index ef6268cd42..4806441d3d 100644 --- a/apps/web/tests/official-subagent-bundle.e2e.ts +++ b/apps/web/tests/official-subagent-bundle.e2e.ts @@ -161,7 +161,7 @@ describe.skipIf(MODE === 'record')('web snapshot: Official native subagent bundl if (notice?.data.source.kind !== 'subagent-settled') throw new Error('Native completion was not delivered to the parent') expect(notice.data.source.senderSessionId).toBe(catalog[0]?.childId) expect(notice.data.content).toContainEqual({ type: 'text', text: RESPONSE }) - expect(dispatches[0]?.content).toEqual([{ type: 'text', text: `started subagent ${notice.data.source.senderSessionId}` }]) + expect(dispatches[0]?.content).toEqual([{ type: 'text', text: 'started subagent agent-1' }]) expect(events.filter(event => event.type === 'turn/end')).toHaveLength(2) expect(server.requests).toHaveLength(1) expect(server.requests[0]?.body).toMatchObject({ model: 'fixture-model' }) diff --git a/apps/web/tests/shipped-composition.e2e.ts b/apps/web/tests/shipped-composition.e2e.ts index 48ca2546c6..de9f756947 100644 --- a/apps/web/tests/shipped-composition.e2e.ts +++ b/apps/web/tests/shipped-composition.e2e.ts @@ -305,7 +305,7 @@ class ShippedChildAutoAdapter extends LlmAdapter { throw new Error('continuable child id was not observed before parent adjustment') } return toolChunks('parent-adjust', 'send_message', { - agent_id: this.continuableChildId, + agent: this.continuableChildId, message: [ AUTO_CHILD_ADJUSTED, `Replace the earlier no-deletion restriction: permanently delete exactly ${this.continuableDeletePath}.`, diff --git a/apps/web/tests/snapshots/agent-team-panel/task.expected.md b/apps/web/tests/snapshots/agent-team-panel/task.expected.md index 9032c3413b..091b670513 100644 --- a/apps/web/tests/snapshots/agent-team-panel/task.expected.md +++ b/apps/web/tests/snapshots/agent-team-panel/task.expected.md @@ -11,4 +11,4 @@ - img "In progress" - text: Agent task - paragraph: Created by the root agent - - paragraph: "Owner: session-{{uuid}}" + - paragraph: "Owner: lead" diff --git a/apps/web/tests/subagent-conversation.e2e.ts b/apps/web/tests/subagent-conversation.e2e.ts index 2b9cae7b84..9690154afe 100644 --- a/apps/web/tests/subagent-conversation.e2e.ts +++ b/apps/web/tests/subagent-conversation.e2e.ts @@ -39,6 +39,8 @@ const MIGRATING_CHILD_EXPECTED = fileURLToPath(new URL( )) const MODE = webSnapshotMode() const LABEL = 'event-sourcing researcher' +/** The live child's catalog name; it is the parent's first named child. */ +const CHILD_NAME = 'agent-1' const ONE_SHOT_LABEL = 'event-sourcing reviewer' const NESTED_LABEL = 'example editor' const CHILD_TITLE = 'Explain event sourcing in one' @@ -398,7 +400,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = } }) - it('shows complete descriptions and IDs without hover at desktop and touch widths', async () => { + it('shows complete descriptions and names without hover at desktop and touch widths', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-subagent-catalog-labels')) const originalViewport = page.viewportSize()! const originalUrl = page.url() @@ -410,7 +412,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = const panel = page.locator('[data-subagent-panel]') const row = panel.getByRole('treeitem', { name: new RegExp(LABEL) }) const primary = row.getByText(LABEL, { exact: true }) - const secondary = row.getByText(childId, { exact: true }) + const secondary = row.getByText(CHILD_NAME, { exact: true }) const primaryBounds = await primary.boundingBox() const secondaryBounds = await secondary.boundingBox() expect(primaryBounds).not.toBeNull() @@ -420,7 +422,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = expect(await label.evaluate(element => element.scrollWidth <= element.clientWidth && element.scrollHeight <= element.clientHeight)).toBe(true) } - expect(await row.getAttribute('aria-label')).toContain(`${LABEL} ${childId} `) + expect(await row.getAttribute('aria-label')).toContain(`${LABEL} ${CHILD_NAME} `) expect(await row.getByRole('button', { name: /description/u }).count()).toBe(0) expect(await row.getByRole('button', { name: `Expand ${LABEL} descendants`, exact: true }).count()).toBe(1) expect(await row.getByRole('button', { name: `Open ${LABEL} in sidebar`, exact: true }).count()).toBe(1) @@ -776,7 +778,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = .getByRole('treeitem', { name: /Ask a research subagent to/ }) .click() await page.getByRole('button', { name: '2 subagents' }).hover() - await page.getByRole('treeitem', { name: new RegExp(LABEL) }).getByText(childId, { exact: true }).click() + await page.getByRole('treeitem', { name: new RegExp(LABEL) }).getByText(CHILD_NAME, { exact: true }).click() await page.getByRole('textbox', { name: 'Message or run a task, / commands, @ files or sessions' }).waitFor() const forkResponse = page.waitForResponse(response => new URL(response.url()).pathname === '/api/session/fork') @@ -805,7 +807,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = const sessions = page.getByRole('tree', { name: 'Sessions' }) await sessions.getByRole('treeitem', { name: /Ask a research subagent to/ }).click() await page.getByRole('button', { name: '2 subagents' }).hover() - await page.getByRole('treeitem', { name: new RegExp(LABEL) }).getByText(childId, { exact: true }).click() + await page.getByRole('treeitem', { name: new RegExp(LABEL) }).getByText(CHILD_NAME, { exact: true }).click() await page.locator('[data-composer-input][contenteditable="true"]').first().waitFor() expect(scaffold.ctx.agents.get(childId)).toBeUndefined() @@ -822,7 +824,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = await sessions.getByRole('treeitem', { name: /Ask a research subagent to/ }).click() await page.getByRole('button', { name: '2 subagents' }).press('ArrowDown') - await page.getByRole('treeitem', { name: new RegExp(LABEL) }).getByText(childId, { exact: true }).click() + await page.getByRole('treeitem', { name: new RegExp(LABEL) }).getByText(CHILD_NAME, { exact: true }).click() const input = page.locator('[data-composer-input][contenteditable="true"]').first() await input.waitFor() const promptResponse = page.waitForResponse(response => @@ -868,7 +870,7 @@ describe('web e2e: persisted subagent conversation and human continuation', () = await page.getByRole('button', { name: `Expand ${LABEL} descendants`, exact: true }).click() await page.getByRole('treeitem', { name: new RegExp(NESTED_LABEL) }).waitFor() expect(await rootHeader.textContent()).toBe(progress) - await page.getByRole('treeitem', { name: new RegExp(LABEL) }).getByText(childId, { exact: true }).click() + await page.getByRole('treeitem', { name: new RegExp(LABEL) }).getByText(CHILD_NAME, { exact: true }).click() const switcher = page.getByRole('button', { name: `Switch subagent: ${LABEL}`, exact: true }) await switcher.click() const panel = page.locator('[data-subagent-panel]') diff --git a/apps/web/tests/subagent-result-notice.e2e.ts b/apps/web/tests/subagent-result-notice.e2e.ts index cff9482035..cb8525f617 100644 --- a/apps/web/tests/subagent-result-notice.e2e.ts +++ b/apps/web/tests/subagent-result-notice.e2e.ts @@ -63,13 +63,13 @@ describe.skipIf(MODE === 'record')('web e2e: subagent result notices', () => { const childId = SessionId('notice-result-child') const notices: { title: string; message: ReturnType }[] = OUTCOMES.map(outcome => ({ title: outcome[locale], - message: createSettlementMessage(childId, { + message: createSettlementMessage(childId, 'agent-1', { stopReason: outcome.reason, output: [{ type: 'text', text: OUTPUT }], }), })) notices.push({ title: OUTCOMES[0][locale], - message: createSettlementMessage(childId, { stopReason: 'completed', output: [] }), + message: createSettlementMessage(childId, 'agent-1', { stopReason: 'completed', output: [] }), }) notices.push({ title: locale === 'en' ? 'Subtask status updated' : '子任务状态更新', diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 07c2fbdf0d..48a15b9415 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -2223,7 +2223,7 @@ export type PiAiThinkingTokenBudgetField = NonNullable ## `@deepseek-ai/dsh-tool-agent-team` - `inject`: `agentTeams` · `subagents` · `tools` · `systemPrompt` -- `source`: [`packages/team/tool-agent-team/src/index.ts:18`](../packages/team/tool-agent-team/src/index.ts) +- `source`: [`packages/team/tool-agent-team/src/index.ts:19`](../packages/team/tool-agent-team/src/index.ts) ```ts config-catalog /** The Team tool plugin takes no configuration. */ @@ -4366,7 +4366,7 @@ export interface PresetSelectionConfig { ## `@deepseek-ai/dsh-tool-subagent-control` - `inject`: `tools` · `subagents` · `agents` · `systemPrompt` -- `source`: [`packages/subagent/tool-subagent-control/src/index.ts:26`](../packages/subagent/tool-subagent-control/src/index.ts) +- `source`: [`packages/subagent/tool-subagent-control/src/index.ts:24`](../packages/subagent/tool-subagent-control/src/index.ts) ```ts config-catalog /** Optional subagent control tools. */ diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 510316eb52..8a1a5be278 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -2225,7 +2225,7 @@ export type PiAiThinkingTokenBudgetField = NonNullable ## `@deepseek-ai/dsh-tool-agent-team` - `inject`: `agentTeams` · `subagents` · `tools` · `systemPrompt` -- `source`: [`packages/team/tool-agent-team/src/index.ts:18`](../packages/team/tool-agent-team/src/index.ts) +- `source`: [`packages/team/tool-agent-team/src/index.ts:19`](../packages/team/tool-agent-team/src/index.ts) ```ts config-catalog /** The Team tool plugin takes no configuration. */ @@ -4368,7 +4368,7 @@ export interface PresetSelectionConfig { ## `@deepseek-ai/dsh-tool-subagent-control` - `inject`: `tools` · `subagents` · `agents` · `systemPrompt` -- `source`: [`packages/subagent/tool-subagent-control/src/index.ts:26`](../packages/subagent/tool-subagent-control/src/index.ts) +- `source`: [`packages/subagent/tool-subagent-control/src/index.ts:24`](../packages/subagent/tool-subagent-control/src/index.ts) ```ts config-catalog /** Optional subagent control tools. */ diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index cc7a806a71..3b54192d01 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -76,10 +76,10 @@ Application listeners are outside this package matrix. Desktop Host subscribes t | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) | | `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:75`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`emit`) | `remotes` | | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:296`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - | -| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:161`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), `server` | -| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:137`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:143`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:153`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | +| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:166`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), `server` | +| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:142`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:148`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:158`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | | `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | `browser-use-runtime`, [`working-directory`](../packages/session/working-directory) | | `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - | | `tools/change` | `emit` | [`packages/core/tools/src/events.ts:56`](../packages/core/tools/src/events.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime` | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index 6c437ea8b6..871827d633 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -78,10 +78,10 @@ | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:84`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-telemetry`](../packages/session/session-telemetry) | | `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:75`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`emit`) | `remotes` | | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:296`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - | -| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:161`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), `server` | -| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:137`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:143`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:153`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | +| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:166`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code), `server` | +| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:142`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:148`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:158`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | | `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | `browser-use-runtime`, [`working-directory`](../packages/session/working-directory) | | `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - | | `tools/change` | `emit` | [`packages/core/tools/src/events.ts:56`](../packages/core/tools/src/events.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime` | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index 8f7366da1c..99cf1368ae 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -9,8 +9,8 @@ en: 1387c4505dde2faf zh: 48570fa1c13d6910 /session-persistence-event-catalog/persistence-type-fingerprints: - en: d334560286daab57 - zh: ddece2494dd2131c + en: 18fad247b1c849db + zh: 5cf48ae5db4615d7 /session-persistence-event-catalog/event-envelope: en: 56414ffd00cdc32a zh: 6dc11eb013e95e41 @@ -207,8 +207,8 @@ en: 9630752e97ddbfbc zh: 9630752e97ddbfbc /session-persistence-event-catalog/events/subagent/subagent-catalog-log-only: - en: e6ce4b5a98ff2f52 - zh: 9a8e112754679aef + en: cc33ef2805abbfdd + zh: d35696cb06672d6d /session-persistence-event-catalog/events/subagent/subagent-descriptor-log-only: en: f5d53639dc5afdfc zh: 8c553e7397d08832 @@ -963,8 +963,11 @@ en: 6884d3e0e7f31048 zh: 6884d3e0e7f31048 /session-persistence-event-catalog/resolved-persistence-types/2: - en: d462b9a5e0855776 - zh: d462b9a5e0855776 + en: ca872ec805526f3d + zh: ca872ec805526f3d +/session-persistence-event-catalog/resolved-persistence-types/3: + en: 27a98df839b87cc8 + zh: 27a98df839b87cc8 /session-persistence-event-catalog/resolved-persistence-types/4: en: 9ec6b8bd755f3e33 zh: 9ec6b8bd755f3e33 @@ -1239,11 +1242,11 @@ en: ecc15ebadb77afdf zh: 97ac27d6a4175f6f /session-persistence-event-catalog/resolved-persistence-types/streamchunk: - en: 76ef1d6de369c7b5 - zh: e033b6bbaa455e87 + en: 747e384b3176e0cb + zh: defbff408ea7c97c /session-persistence-event-catalog/resolved-persistence-types/subagentcatalogevent: - en: 725539dcd5c335ed - zh: 5ca08d3667f84ff9 + en: 8a56ae8e701509b3 + zh: caef4f0223f65470 /session-persistence-event-catalog/resolved-persistence-types/subagentdescriptordata: en: 2b9c68682eec6f78 zh: 8ef5e64326c1eae6 @@ -1446,21 +1449,27 @@ en: de1d59e9edd58a7d zh: 0e29b8e80db664dc /session-persistence-event-catalog/resolved-persistence-types/childcreatedat-childid-label-mode: - en: 2ef977cb4a83bb0a - zh: 893fded259888650 + en: c3690b3beea61c52 + zh: 7a788be5921e8d8f /session-persistence-event-catalog/resolved-persistence-types/childcreatedat-childid-label-mode~2: + en: 2af7543c81f83558 + zh: a96b05e9c12d0e7b +/session-persistence-event-catalog/resolved-persistence-types/childcreatedat-childid-label-mode~3: en: 669e284c656449c7 zh: e40ac8cb904bd983 -/session-persistence-event-catalog/resolved-persistence-types/childcreatedat-childid-label-mode~3: +/session-persistence-event-catalog/resolved-persistence-types/childcreatedat-childid-label-mode~4: en: 1dc667423caefb33 zh: bd38fa2473673d14 -/session-persistence-event-catalog/resolved-persistence-types/childcreatedat-childid-label-mode~4: - en: dc8738014c719f4e - zh: c773fd60c0090231 /session-persistence-event-catalog/resolved-persistence-types/childcreatedat-childid-label-mode~5: + en: 9599091956b57334 + zh: 54dbf4a35ec3ed35 +/session-persistence-event-catalog/resolved-persistence-types/childcreatedat-childid-label-mode~6: + en: b1cdfd349b457908 + zh: d5d75fb74a5cefc8 +/session-persistence-event-catalog/resolved-persistence-types/childcreatedat-childid-label-mode~7: en: 43cf5cd666b591a0 zh: 7c7a3011f8681f12 -/session-persistence-event-catalog/resolved-persistence-types/childcreatedat-childid-label-mode~6: +/session-persistence-event-catalog/resolved-persistence-types/childcreatedat-childid-label-mode~8: en: a1f188c9058cf621 zh: eb43c0f045fb9afb /session-persistence-event-catalog/resolved-persistence-types/code-name-reason: @@ -2106,11 +2115,11 @@ en: 7fc233c4a569dae2 zh: bc2b3fed4a397c00 /session-persistence-event-catalog/resolved-persistence-types/type-step-start: - en: bb27281e17bb5e4f - zh: b53e3fc8d916118b + en: 6ba6d118d44365e2 + zh: 846ea149908f0e04 /session-persistence-event-catalog/resolved-persistence-types/type-subagent-catalog: - en: 0f697ddcde1625dd - zh: 0dd87d2e108c5312 + en: b5dfa07e0b8d9f6a + zh: e93efb23da447ade /session-persistence-event-catalog/resolved-persistence-types/type-subagent-descriptor: en: 2adab4361a5b558d zh: 7f2a8da669f9e117 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 16c1cb90f8..d3175f1743 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -61,7 +61,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover | `event:session/title-llm-request` | event | `d95c624eddecdacf1ee74356196c09d2f3fcd625714be5e852a6b643fa12a7ca` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-d95c624eddecdacf1ee74356196c09d2f3fcd625714be5e852a6b643fa12a7ca) | | `event:step/end` | event | `e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05` | [`{ type: "step/end" }`](#persistence-type-sha256-e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05) | | `event:step/start` | event | `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | [`{ type: "step/start" }`](#persistence-type-sha256-4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986) | -| `event:subagent/catalog` | event | `53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac) | +| `event:subagent/catalog` | event | `d49164d37eab0f0f70fa787c65d887abbea869c010ccb103d717fab3519c3a41` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-d49164d37eab0f0f70fa787c65d887abbea869c010ccb103d717fab3519c3a41) | | `event:subagent/descriptor` | event | `b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5` | [`{ type: "subagent/descriptor" }`](#persistence-type-sha256-b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5) | | `event:subagent/model-selection-policy` | event | `a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc` | [`{ type: "subagent/model-selection-policy" }`](#persistence-type-sha256-a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc) | | `event:system/message` | event | `69081694be231d56fd9580ba14645fd5e35373202605d5c5c841a9435b5fa3b1` | [`{ type: "system/message" }`](#persistence-type-sha256-69081694be231d56fd9580ba14645fd5e35373202605d5c5c841a9435b5fa3b1) | @@ -938,7 +938,7 @@ Source: [`packages/core/session/src/types.ts:305`](../packages/core/session/src/ 'subagent/catalog': SubagentCatalogEvent ``` -Source: [`packages/subagent/subagent/src/catalog.ts:44`](../packages/subagent/subagent/src/catalog.ts) +Source: [`packages/subagent/subagent/src/catalog.ts:52`](../packages/subagent/subagent/src/catalog.ts) @@ -3102,6 +3102,14 @@ SHA-256: `a0f906856fc06f1c6f2083eb970e5a657a82b1a84c06c095445e5d6d161cda80` `2` + + +### `3` + +SHA-256: `137ac60e26bcb192210d9a32a704b73b0924ed34019323defa1b787be95bd45e` + +`3` + ### `4` @@ -4873,7 +4881,7 @@ One of: - [`{ type: "finish" }`](#persistence-type-sha256-a2b49331716ba0a7078b026d145306a70eae64b7313c3e6204bd931e75ec3bb1) - [`{ type: "usage" }`](#persistence-type-sha256-f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515) - + @@ -4881,18 +4889,20 @@ One of: ### `SubagentCatalogEvent` -SHA-256: `f731ffe80810b0289b3e694d8f40af6f01865c02a622ee457be90ca808b66e87` +SHA-256: `058fc9a6b4fbd5d43437c3a64170002070dfc98e18d87de294b564ae446ca8ae` -Sources: [`packages/subagent/subagent/src/catalog.ts:28`](../packages/subagent/subagent/src/catalog.ts) +Sources: [`packages/subagent/subagent/src/catalog.ts:32`](../packages/subagent/subagent/src/catalog.ts) One of: +- [`{ childCreatedAt, childId, label, mode, … }`](#persistence-type-sha256-8faed5b52ab2d2df40b94a926d0f0faa75280bddcc4b17b6bdd9c8bfed7c34b5) +- [`{ childCreatedAt, childId, label, mode, … }`](#persistence-type-sha256-0e10d7c096b47e61defe372a5fc8bd37d835199410eac44367f95c45082498bd) - [`{ childCreatedAt, childId, label, mode, … }`](#persistence-type-sha256-e2ad8f5bcd813f268ffef1b10a12d7ddd2b8ced8b9b6d0275b3f6b7ed094a187) +- [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-a43327a1739c3fa477f32981071fef7d8ba6ae5f32a84fd70f547b2eee59e93e) - [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-cbbe8377ee744964537dbafd83607219e58ba79a13f6db0d0645c7ad3d5e8a09) - [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-5d542ac7bdf13310c46e1a884dec9fb18ad8c6e153cff207e639cd7bf9d7e694) - [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-b6896938660a1dfe4ddd100fb3164d9f8fa57feeabfe37441a80106c9942697a) - [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-a0f25b5aeeebf2bf1542ce67726b095e3b139a39c71861d57e827f12dea1f499) -- [`{ childCreatedAt, childId, label, mode, … }`](#persistence-type-sha256-0e10d7c096b47e61defe372a5fc8bd37d835199410eac44367f95c45082498bd) @@ -5984,6 +5994,21 @@ SHA-256: `0e10d7c096b47e61defe372a5fc8bd37d835199410eac44367f95c45082498bd` | `mode` | required | `"continuable"` | | `version` | required | `0` | + + +### `{ childCreatedAt, childId, label, mode, … }` + +SHA-256: `8faed5b52ab2d2df40b94a926d0f0faa75280bddcc4b17b6bdd9c8bfed7c34b5` + +| Property | Presence | Type | +|---|---|---| +| `childCreatedAt` | required | `number` | +| `childId` | required | `string` | +| `label` | required | `string` | +| `mode` | required | `"continuable"` | +| `name` | required | `string` | +| `version` | required | `3` | + ### `{ childCreatedAt, childId, label, mode, … }` @@ -6026,6 +6051,21 @@ SHA-256: `a0f25b5aeeebf2bf1542ce67726b095e3b139a39c71861d57e827f12dea1f499` | `mode` | required | `"unknown"` | | `version` | required | `1` | + + +### `{ childCreatedAt, childId, label?, mode, … }` + +SHA-256: `a43327a1739c3fa477f32981071fef7d8ba6ae5f32a84fd70f547b2eee59e93e` + +| Property | Presence | Type | +|---|---|---| +| `childCreatedAt` | required | `number` | +| `childId` | required | `string` | +| `label` | optional | `string` | +| `mode` | required | `"external"` | +| `name` | required | `string` | +| `version` | required | `3` | + ### `{ childCreatedAt, childId, label?, mode, … }` @@ -8857,17 +8897,17 @@ SHA-256: `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | `time` | required | `number` | | `type` | required | `"step/start"` | - + ### `{ type: "subagent/catalog" }` -SHA-256: `53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac` +SHA-256: `d49164d37eab0f0f70fa787c65d887abbea869c010ccb103d717fab3519c3a41` | Property | Presence | Type | |---|---|---| -| `data` | required | [`SubagentCatalogEvent`](#persistence-type-sha256-f731ffe80810b0289b3e694d8f40af6f01865c02a622ee457be90ca808b66e87) | +| `data` | required | [`SubagentCatalogEvent`](#persistence-type-sha256-058fc9a6b4fbd5d43437c3a64170002070dfc98e18d87de294b564ae446ca8ae) | | `ignorable` | optional | `true` | | `seq` | required | `number` | | `time` | required | `number` | diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 5e045309fd..c292ae0314 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -63,7 +63,7 @@ | `event:session/title-llm-request` | event | `d95c624eddecdacf1ee74356196c09d2f3fcd625714be5e852a6b643fa12a7ca` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-d95c624eddecdacf1ee74356196c09d2f3fcd625714be5e852a6b643fa12a7ca) | | `event:step/end` | event | `e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05` | [`{ type: "step/end" }`](#persistence-type-sha256-e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05) | | `event:step/start` | event | `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | [`{ type: "step/start" }`](#persistence-type-sha256-4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986) | -| `event:subagent/catalog` | event | `53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac) | +| `event:subagent/catalog` | event | `d49164d37eab0f0f70fa787c65d887abbea869c010ccb103d717fab3519c3a41` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-d49164d37eab0f0f70fa787c65d887abbea869c010ccb103d717fab3519c3a41) | | `event:subagent/descriptor` | event | `b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5` | [`{ type: "subagent/descriptor" }`](#persistence-type-sha256-b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5) | | `event:subagent/model-selection-policy` | event | `a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc` | [`{ type: "subagent/model-selection-policy" }`](#persistence-type-sha256-a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc) | | `event:system/message` | event | `69081694be231d56fd9580ba14645fd5e35373202605d5c5c841a9435b5fa3b1` | [`{ type: "system/message" }`](#persistence-type-sha256-69081694be231d56fd9580ba14645fd5e35373202605d5c5c841a9435b5fa3b1) | @@ -940,7 +940,7 @@ export type SessionEvent = { 'subagent/catalog': SubagentCatalogEvent ``` -来源:[`packages/subagent/subagent/src/catalog.ts:44`](../packages/subagent/subagent/src/catalog.ts) +来源:[`packages/subagent/subagent/src/catalog.ts:52`](../packages/subagent/subagent/src/catalog.ts) @@ -3104,6 +3104,14 @@ SHA-256: `a0f906856fc06f1c6f2083eb970e5a657a82b1a84c06c095445e5d6d161cda80` `2` + + +### `3` + +SHA-256: `137ac60e26bcb192210d9a32a704b73b0924ed34019323defa1b787be95bd45e` + +`3` + ### `4` @@ -4875,7 +4883,7 @@ SHA-256: `47e9095591ae36e7c39ee62553097772f2bfa16304536a72dbbc3f691f605180` - [`{ type: "finish" }`](#persistence-type-sha256-a2b49331716ba0a7078b026d145306a70eae64b7313c3e6204bd931e75ec3bb1) - [`{ type: "usage" }`](#persistence-type-sha256-f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515) - + @@ -4883,18 +4891,20 @@ SHA-256: `47e9095591ae36e7c39ee62553097772f2bfa16304536a72dbbc3f691f605180` ### `SubagentCatalogEvent` -SHA-256: `f731ffe80810b0289b3e694d8f40af6f01865c02a622ee457be90ca808b66e87` +SHA-256: `058fc9a6b4fbd5d43437c3a64170002070dfc98e18d87de294b564ae446ca8ae` -来源:[`packages/subagent/subagent/src/catalog.ts:28`](../packages/subagent/subagent/src/catalog.ts) +来源:[`packages/subagent/subagent/src/catalog.ts:32`](../packages/subagent/subagent/src/catalog.ts) 以下类型之一: +- [`{ childCreatedAt, childId, label, mode, … }`](#persistence-type-sha256-8faed5b52ab2d2df40b94a926d0f0faa75280bddcc4b17b6bdd9c8bfed7c34b5) +- [`{ childCreatedAt, childId, label, mode, … }`](#persistence-type-sha256-0e10d7c096b47e61defe372a5fc8bd37d835199410eac44367f95c45082498bd) - [`{ childCreatedAt, childId, label, mode, … }`](#persistence-type-sha256-e2ad8f5bcd813f268ffef1b10a12d7ddd2b8ced8b9b6d0275b3f6b7ed094a187) +- [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-a43327a1739c3fa477f32981071fef7d8ba6ae5f32a84fd70f547b2eee59e93e) - [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-cbbe8377ee744964537dbafd83607219e58ba79a13f6db0d0645c7ad3d5e8a09) - [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-5d542ac7bdf13310c46e1a884dec9fb18ad8c6e153cff207e639cd7bf9d7e694) - [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-b6896938660a1dfe4ddd100fb3164d9f8fa57feeabfe37441a80106c9942697a) - [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-a0f25b5aeeebf2bf1542ce67726b095e3b139a39c71861d57e827f12dea1f499) -- [`{ childCreatedAt, childId, label, mode, … }`](#persistence-type-sha256-0e10d7c096b47e61defe372a5fc8bd37d835199410eac44367f95c45082498bd) @@ -5986,6 +5996,21 @@ SHA-256: `0e10d7c096b47e61defe372a5fc8bd37d835199410eac44367f95c45082498bd` | `mode` | 必需 | `"continuable"` | | `version` | 必需 | `0` | + + +### `{ childCreatedAt, childId, label, mode, … }` + +SHA-256: `8faed5b52ab2d2df40b94a926d0f0faa75280bddcc4b17b6bdd9c8bfed7c34b5` + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `childCreatedAt` | 必需 | `number` | +| `childId` | 必需 | `string` | +| `label` | 必需 | `string` | +| `mode` | 必需 | `"continuable"` | +| `name` | 必需 | `string` | +| `version` | 必需 | `3` | + ### `{ childCreatedAt, childId, label, mode, … }` @@ -6028,6 +6053,21 @@ SHA-256: `a0f25b5aeeebf2bf1542ce67726b095e3b139a39c71861d57e827f12dea1f499` | `mode` | 必需 | `"unknown"` | | `version` | 必需 | `1` | + + +### `{ childCreatedAt, childId, label?, mode, … }` + +SHA-256: `a43327a1739c3fa477f32981071fef7d8ba6ae5f32a84fd70f547b2eee59e93e` + +| 属性 | 存在性 | 类型 | +|---|---|---| +| `childCreatedAt` | 必需 | `number` | +| `childId` | 必需 | `string` | +| `label` | 可选 | `string` | +| `mode` | 必需 | `"external"` | +| `name` | 必需 | `string` | +| `version` | 必需 | `3` | + ### `{ childCreatedAt, childId, label?, mode, … }` @@ -8859,17 +8899,17 @@ SHA-256: `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | `time` | 必需 | `number` | | `type` | 必需 | `"step/start"` | - + ### `{ type: "subagent/catalog" }` -SHA-256: `53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac` +SHA-256: `d49164d37eab0f0f70fa787c65d887abbea869c010ccb103d717fab3519c3a41` | 属性 | 存在性 | 类型 | |---|---|---| -| `data` | 必需 | [`SubagentCatalogEvent`](#persistence-type-sha256-f731ffe80810b0289b3e694d8f40af6f01865c02a622ee457be90ca808b66e87) | +| `data` | 必需 | [`SubagentCatalogEvent`](#persistence-type-sha256-058fc9a6b4fbd5d43437c3a64170002070dfc98e18d87de294b564ae446ca8ae) | | `ignorable` | 可选 | `true` | | `seq` | 必需 | `number` | | `time` | 必需 | `number` | diff --git a/docs/persistence-changes/2026-11-02-subagent-catalog-names.i18n.yaml b/docs/persistence-changes/2026-11-02-subagent-catalog-names.i18n.yaml new file mode 100644 index 0000000000..9b9e015b7b --- /dev/null +++ b/docs/persistence-changes/2026-11-02-subagent-catalog-names.i18n.yaml @@ -0,0 +1,28 @@ +# Bilingual-pair consistency record for 2026-11-02-subagent-catalog-names.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/persistence-changes/2026-11-02-subagent-catalog-names.md +/: + en: 3d5a424ae7d7e46b + zh: c22a8e4a62d8cc1a +/2026-11-02-subagent-catalog-names: + en: d60db721936ec588 + zh: 9271248e40f283c5 +/2026-11-02-subagent-catalog-names/summary: + en: af5f42653bb5d143 + zh: 4441c72b0ab5d2b8 +/2026-11-02-subagent-catalog-names/table-of-contents: + en: 6cf314a069ceb451 + zh: 9adff678e9ecab3b +/2026-11-02-subagent-catalog-names/declaration: + en: 6907136f28e48c90 + zh: 02f8298485f3d8e7 +/2026-11-02-subagent-catalog-names/compatibility: + en: 201ca563853eb66b + zh: c7d7457a9a4a33b7 +/2026-11-02-subagent-catalog-names/verification: + en: cf8533363c2d36f7 + zh: 75de24564998ca27 +/2026-11-02-subagent-catalog-names/dev-note: + en: b75c8458928b72b9 + zh: 003ba4e17b4b5269 diff --git a/docs/persistence-changes/2026-11-02-subagent-catalog-names.md b/docs/persistence-changes/2026-11-02-subagent-catalog-names.md new file mode 100644 index 0000000000..3c0b93c2a4 --- /dev/null +++ b/docs/persistence-changes/2026-11-02-subagent-catalog-names.md @@ -0,0 +1,48 @@ +--- +description: "Records a persistence type transition and its compatibility acknowledgement." +kind: persistence-change +--- + +# 2026-11-02-subagent-catalog-names + +English | [中文](2026-11-02-subagent-catalog-names.zh.md) + +## Summary + +Adds version 3 of the parent-owned subagent/catalog payload, which records each continuable or parent-delivered external child's name, unique among its creator's children. + +## Table of Contents + +- [Declaration](#declaration) +- [Compatibility](#compatibility) +- [Verification](#verification) +- [Dev Note](#dev-note) + + +## Declaration + +```yaml persistence-change +schemaVersion: 1 +id: 2026-11-02-subagent-catalog-names +baseline: false +changes: + - root: "event:subagent/catalog" + previous: "2026-09-19-external-subagent-catalog" + after: "d49164d37eab0f0f70fa787c65d887abbea869c010ccb103d717fab3519c3a41" + decision: same-version +``` + + +## Compatibility + +Version 0, 1, and 2 payloads keep their schemas and meaning; current readers restore them as unnamed children addressed by Session id. New local and parent-delivered external creation writes version 3. Earlier releases reject the unknown payload version during native V4 admission and catalog projection restoration instead of misreading the entry, so a parent Session that recorded a named child fails loudly rather than losing a child. + + +## Verification + +pnpm exec vitest run packages/subagent/subagent/tests: 495 tests passed, including named catalog restoration, invalid version-3 facts, sibling-unique reservation, cold resume path recovery, and external parent-delivered names. pnpm exec vitest run packages/session/session-format-v3-to-v4/tests: 397 tests passed, including native version-3 admission and rejection. + + +## Dev Note + +None. diff --git a/docs/persistence-changes/2026-11-02-subagent-catalog-names.schema.json b/docs/persistence-changes/2026-11-02-subagent-catalog-names.schema.json new file mode 100644 index 0000000000..9a474fce46 --- /dev/null +++ b/docs/persistence-changes/2026-11-02-subagent-catalog-names.schema.json @@ -0,0 +1,31 @@ +{ + "formatVersion": 2, + "roots": [ + {"key":"event:subagent/catalog","kind":"event","event":"subagent/catalog","surface":false,"digest":"d49164d37eab0f0f70fa787c65d887abbea869c010ccb103d717fab3519c3a41"} + ], + "types": [], + "nodes": { + "058fc9a6b4fbd5d4": {"kind":"union","types":["0e10d7c096b47e61","5d542ac7bdf13310","8faed5b52ab2d2df","a0f25b5aeeebf2bf","a43327a1739c3fa4","b6896938660a1dfe","cbbe8377ee744964","e2ad8f5bcd813f26"]}, + "05a2796621b1ee6c": {"kind":"literal","value":"unknown"}, + "0657d3ca5ac4e7bf": {"kind":"primitive","type":"number"}, + "0e10d7c096b47e61": {"kind":"object","properties":[{"name":"childCreatedAt","type":"0657d3ca5ac4e7bf","optional":false},{"name":"childId","type":"4cd48f3e5108bd6e","optional":false},{"name":"label","type":"4cd48f3e5108bd6e","optional":false},{"name":"mode","type":"e5d3828df1ec3e2a","optional":false},{"name":"version","type":"93d38436347f67c9","optional":false}],"indices":[]}, + "137ac60e26bcb192": {"kind":"literal","value":3}, + "44c8c77aa9aa100b": {"kind":"literal","value":"subagent/catalog"}, + "4cd48f3e5108bd6e": {"kind":"primitive","type":"string"}, + "5d542ac7bdf13310": {"kind":"object","properties":[{"name":"childCreatedAt","type":"0657d3ca5ac4e7bf","optional":false},{"name":"childId","type":"4cd48f3e5108bd6e","optional":false},{"name":"label","type":"4cd48f3e5108bd6e","optional":true},{"name":"mode","type":"d28765f87d6b6ab4","optional":false},{"name":"version","type":"93d38436347f67c9","optional":false}],"indices":[]}, + "8faed5b52ab2d2df": {"kind":"object","properties":[{"name":"childCreatedAt","type":"0657d3ca5ac4e7bf","optional":false},{"name":"childId","type":"4cd48f3e5108bd6e","optional":false},{"name":"label","type":"4cd48f3e5108bd6e","optional":false},{"name":"mode","type":"e5d3828df1ec3e2a","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false},{"name":"version","type":"137ac60e26bcb192","optional":false}],"indices":[]}, + "93d38436347f67c9": {"kind":"literal","value":0}, + "a0f25b5aeeebf2bf": {"kind":"object","properties":[{"name":"childCreatedAt","type":"0657d3ca5ac4e7bf","optional":false},{"name":"childId","type":"4cd48f3e5108bd6e","optional":false},{"name":"label","type":"4cd48f3e5108bd6e","optional":true},{"name":"mode","type":"05a2796621b1ee6c","optional":false},{"name":"version","type":"f88f1b7b6bb733f6","optional":false}],"indices":[]}, + "a0f906856fc06f1c": {"kind":"literal","value":2}, + "a43327a1739c3fa4": {"kind":"object","properties":[{"name":"childCreatedAt","type":"0657d3ca5ac4e7bf","optional":false},{"name":"childId","type":"4cd48f3e5108bd6e","optional":false},{"name":"label","type":"4cd48f3e5108bd6e","optional":true},{"name":"mode","type":"fc351843b8ecbd05","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false},{"name":"version","type":"137ac60e26bcb192","optional":false}],"indices":[]}, + "b6896938660a1dfe": {"kind":"object","properties":[{"name":"childCreatedAt","type":"0657d3ca5ac4e7bf","optional":false},{"name":"childId","type":"4cd48f3e5108bd6e","optional":false},{"name":"label","type":"4cd48f3e5108bd6e","optional":true},{"name":"mode","type":"d28765f87d6b6ab4","optional":false},{"name":"version","type":"f88f1b7b6bb733f6","optional":false}],"indices":[]}, + "cbbe8377ee744964": {"kind":"object","properties":[{"name":"childCreatedAt","type":"0657d3ca5ac4e7bf","optional":false},{"name":"childId","type":"4cd48f3e5108bd6e","optional":false},{"name":"label","type":"4cd48f3e5108bd6e","optional":true},{"name":"mode","type":"fc351843b8ecbd05","optional":false},{"name":"version","type":"a0f906856fc06f1c","optional":false}],"indices":[]}, + "d28765f87d6b6ab4": {"kind":"literal","value":"one-shot"}, + "d49164d37eab0f0f": {"kind":"object","properties":[{"name":"data","type":"058fc9a6b4fbd5d4","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"44c8c77aa9aa100b","optional":false}],"indices":[]}, + "dacf3d5f3e4ff619": {"kind":"literal","value":true}, + "e2ad8f5bcd813f26": {"kind":"object","properties":[{"name":"childCreatedAt","type":"0657d3ca5ac4e7bf","optional":false},{"name":"childId","type":"4cd48f3e5108bd6e","optional":false},{"name":"label","type":"4cd48f3e5108bd6e","optional":false},{"name":"mode","type":"e5d3828df1ec3e2a","optional":false},{"name":"version","type":"f88f1b7b6bb733f6","optional":false}],"indices":[]}, + "e5d3828df1ec3e2a": {"kind":"literal","value":"continuable"}, + "f88f1b7b6bb733f6": {"kind":"literal","value":1}, + "fc351843b8ecbd05": {"kind":"literal","value":"external"} + } +} diff --git a/docs/persistence-changes/2026-11-02-subagent-catalog-names.zh.md b/docs/persistence-changes/2026-11-02-subagent-catalog-names.zh.md new file mode 100644 index 0000000000..ba61439415 --- /dev/null +++ b/docs/persistence-changes/2026-11-02-subagent-catalog-names.zh.md @@ -0,0 +1,48 @@ +--- +description: "记录持久化类型更改及其兼容性确认。" +kind: persistence-change +--- + +# 2026-11-02-subagent-catalog-names + +[English](2026-11-02-subagent-catalog-names.md) | 中文 + +## 概述 + +为父 Session 拥有的 subagent/catalog 载荷增加 version 3,记录每个 continuable 子代理或父级交付的 external 子代理在其创建者子代理中唯一的名称。 + +## 目录 + +- [声明](#declaration) +- [兼容性](#compatibility) +- [验证](#verification) +- [开发备注](#dev-note) + + +## 声明 + +```yaml persistence-change +schemaVersion: 1 +id: 2026-11-02-subagent-catalog-names +baseline: false +changes: + - root: "event:subagent/catalog" + previous: "2026-09-19-external-subagent-catalog" + after: "d49164d37eab0f0f70fa787c65d887abbea869c010ccb103d717fab3519c3a41" + decision: same-version +``` + + +## 兼容性 + +version 0、1、2 载荷的 schema 和含义保持不变;当前读取方将其恢复为按 Session id 寻址的无名子代理。新的本地创建和父级交付的 external 创建写入 version 3。较早版本在原生 V4 准入和目录投影恢复时拒绝未知载荷版本,而不会误读条目,因此记录了具名子代理的父 Session 会明确失败,而不是丢失子代理。 + + +## 验证 + +pnpm exec vitest run packages/subagent/subagent/tests:495 个测试通过,覆盖具名目录恢复、无效 version 3 事实、同级唯一预留、冷恢复路径还原以及父级交付 external 子代理的名称。pnpm exec vitest run packages/session/session-format-v3-to-v4/tests:397 个测试通过,覆盖原生 version 3 准入与拒绝。 + + +## 开发备注 + +无。 diff --git a/docs/persistence-changes/historical-formats/README.i18n.yaml b/docs/persistence-changes/historical-formats/README.i18n.yaml index fcd95ef300..827d4cc787 100644 --- a/docs/persistence-changes/historical-formats/README.i18n.yaml +++ b/docs/persistence-changes/historical-formats/README.i18n.yaml @@ -15,8 +15,8 @@ en: 14c57f30e676f6e3 zh: b1a0ccf26fab3727 /session-persistence-formats/format-references: - en: a660985fd2941e51 - zh: 0d4aed6be93be922 + en: 77ac7919d6cac2e4 + zh: 1ecb8a42e6d2a0fd /session-persistence-formats/scope-and-evidence: en: b220062139ddac1e zh: 7c4c0efe05dafa9c diff --git a/docs/persistence-changes/historical-formats/README.md b/docs/persistence-changes/historical-formats/README.md index 24b42751fa..b48a1e27ca 100644 --- a/docs/persistence-changes/historical-formats/README.md +++ b/docs/persistence-changes/historical-formats/README.md @@ -32,7 +32,7 @@ The index is generated from validated snapshots and the current writer constant. | 1 | PR #3349 | [V1](v1.md) | [JSON](v1.schema.json) | 54 / 415 | | 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.md) | [JSON](v2.schema.json) | 56 / 435 | | 3 | PR #4320 | [V3](v3.md) | [JSON](v3.schema.json) | 60 / 467 | -| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 66 / 631 | +| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 66 / 634 | diff --git a/docs/persistence-changes/historical-formats/README.zh.md b/docs/persistence-changes/historical-formats/README.zh.md index ef48b880a4..a686501c36 100644 --- a/docs/persistence-changes/historical-formats/README.zh.md +++ b/docs/persistence-changes/historical-formats/README.zh.md @@ -32,7 +32,7 @@ description: "查找从 V0 到工作区写入器版本的每个 Session 格式 | 1 | PR #3349 | [V1](v1.zh.md) | [JSON](v1.schema.json) | 54 / 415 | | 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.zh.md) | [JSON](v2.schema.json) | 56 / 435 | | 3 | PR #4320 | [V3](v3.zh.md) | [JSON](v3.schema.json) | 60 / 467 | -| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 66 / 631 | +| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 66 / 634 | diff --git a/docs/persistence-schema.json b/docs/persistence-schema.json index 3a30b6f025..80cae0df27 100644 --- a/docs/persistence-schema.json +++ b/docs/persistence-schema.json @@ -43,7 +43,7 @@ {"key":"event:session/title-llm-request","kind":"event","event":"session/title-llm-request","surface":false,"digest":"d95c624eddecdacf1ee74356196c09d2f3fcd625714be5e852a6b643fa12a7ca"}, {"key":"event:step/end","kind":"event","event":"step/end","surface":false,"digest":"e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05"}, {"key":"event:step/start","kind":"event","event":"step/start","surface":false,"digest":"4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986"}, - {"key":"event:subagent/catalog","kind":"event","event":"subagent/catalog","surface":false,"digest":"53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac"}, + {"key":"event:subagent/catalog","kind":"event","event":"subagent/catalog","surface":false,"digest":"d49164d37eab0f0f70fa787c65d887abbea869c010ccb103d717fab3519c3a41"}, {"key":"event:subagent/descriptor","kind":"event","event":"subagent/descriptor","surface":false,"digest":"b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5"}, {"key":"event:subagent/model-selection-policy","kind":"event","event":"subagent/model-selection-policy","surface":false,"digest":"a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc"}, {"key":"event:system/message","kind":"event","event":"system/message","surface":true,"digest":"69081694be231d56fd9580ba14645fd5e35373202605d5c5c841a9435b5fa3b1"}, @@ -77,6 +77,7 @@ {"digest":"02b2c23f2d94b028c9588a8faace3fce1217405db346da3389368b495457bf79","names":[],"sources":["packages/extensions/cordis-host-runner/src/index.ts:14"]}, {"digest":"0341d4c3dfcd53253cdc1e789d9130d3cd848b6f8b0706bf92f313a68b39ff23","names":[],"sources":[]}, {"digest":"05205074b24d8340cfd7995f6cb64779c7f1241490954700115601e264178877","names":[],"sources":[]}, + {"digest":"058fc9a6b4fbd5d43437c3a64170002070dfc98e18d87de294b564ae446ca8ae","names":["packages/subagent/subagent/src/catalog.ts#SubagentCatalogEvent"],"sources":["packages/subagent/subagent/src/catalog.ts:32"]}, {"digest":"05a2796621b1ee6c03ef7abe560b25116bb4da4d592312117242a78415a32b04","names":[],"sources":[]}, {"digest":"05d2ae1e0c7f8770f83f9a09047afb39a335d8ac986df3193659aa1e581472cf","names":["packages/interaction/user-approval/src/index.ts#ApprovalPolicy"],"sources":["packages/interaction/user-approval/src/index.ts:68"]}, {"digest":"0657d3ca5ac4e7bf79b00bcf860b1c7159d01a9da137497221ec2f1776e365f7","names":["packages/core/session/src/types.ts#SessionSeq"],"sources":["packages/core/session/src/types.ts:33"]}, @@ -113,6 +114,7 @@ {"digest":"12a0cfa619ac4a5fc6c3e8e359c8dae15769edc61497a575935c78d5842f8e42","names":[],"sources":[]}, {"digest":"12d5808fac1996b9b3c2afa806e03022252ba2d7b88f835862ae1896591f60c7","names":["packages/llm/llm/src/types.ts#ImageBlock"],"sources":["packages/llm/llm/src/types.ts:86"]}, {"digest":"1379f0b72275994d20f1ddca2d60a96ab16ec80397ca9f1af21bdc67ba5c269c","names":[],"sources":[]}, + {"digest":"137ac60e26bcb192210d9a32a704b73b0924ed34019323defa1b787be95bd45e","names":[],"sources":[]}, {"digest":"13d3d180f977bf78081d487ffa0ecb75857349bcab29a5a3fb48189fca2a6176","names":[],"sources":[]}, {"digest":"14230f9b8b5797bb7e8c621fa636531cfbde2ffba1ef5f1fd058216ff21952a0","names":[],"sources":[]}, {"digest":"142c37d302aa22a0a57915b5255939d3ca91c1811ca13fddcf84cf76ea00758a","names":[],"sources":[]}, @@ -271,7 +273,6 @@ {"digest":"52c6113cb0e53f345324bf84f01928bea5d5df57ee80bc0dd65407b8bc5a3734","names":[],"sources":[]}, {"digest":"5302e763151277614b97e77f00e3b326694c6c1247b916a20503c344dc970777","names":[],"sources":["packages/core/session/src/types.ts:383"]}, {"digest":"530da6c0267813bb4511af72883cc518834e624e3b3220a7062955fd57a788a6","names":["packages/core/session/src/types.ts#RequestHeaderReason"],"sources":["packages/core/session/src/types.ts:279"]}, - {"digest":"53750b9abf61efb7992b0aadc3e247ec927449c63266df28230fd1139f5590ac","names":[],"sources":[]}, {"digest":"53cf1704ae0709c7678bcf070c83c619fc48ca1b5c173b74cfa8702f22a8e492","names":[],"sources":[]}, {"digest":"54f9276a1617d7c12594722c89cfe864bcbe4517e9a6f9c98bab14f199a135e0","names":[],"sources":["packages/core/session/src/types.ts:363"]}, {"digest":"557e148d30b79ee48645850832b30c717d7c3b64641deb5d6133b8b4e91ee0b8","names":[],"sources":[]}, @@ -423,6 +424,7 @@ {"digest":"8e8e91646a5cae3fc78f7532a0013ff2088ae47077642c2edd507a508209b8af","names":["packages/schedule/schedule/src/types.ts#OneShotScheduleDispatchChange"],"sources":["packages/schedule/schedule/src/types.ts:213"]}, {"digest":"8f324cea3916c0517261e88167def8efb3a9994c1135e23d40ed9fd48f8c7bd4","names":[],"sources":[]}, {"digest":"8f6d609794bf5afc01d7ccf3e03811d32f4b8047f58a2c9a98db0154c2ad7cb9","names":[],"sources":[]}, + {"digest":"8faed5b52ab2d2df40b94a926d0f0faa75280bddcc4b17b6bdd9c8bfed7c34b5","names":[],"sources":[]}, {"digest":"8fcb0b44a575e998665c053643d1ab04c82f39253ebd190ac0c975b78e40b543","names":[],"sources":["packages/core/tools/src/index.ts:28"]}, {"digest":"904bf499d98be4a3a5f836fcac533b6c4899569b018071e6604e1d32657d00fd","names":[],"sources":[]}, {"digest":"920d87e22b7409bcc7224f8b1dc7f5101c33574c75627c65baab12b85cc96e75","names":[],"sources":[]}, @@ -474,6 +476,7 @@ {"digest":"a3c6f1ea38e9a61d7a9706057ba65aabe02dfe6d9dd90d4736aa62325d874531","names":[],"sources":[]}, {"digest":"a3edd1efd1fef21b72c55e213c8a8b24196b091abde2b89db52be0b2aaae3cfa","names":[],"sources":[]}, {"digest":"a40d12070f6f4a124f32fb3cc86e7857554702e32f0eda3080a25c4b9ac9b18c","names":[],"sources":["packages/team/agent-team/src/types.ts:175"]}, + {"digest":"a43327a1739c3fa477f32981071fef7d8ba6ae5f32a84fd70f547b2eee59e93e","names":[],"sources":[]}, {"digest":"a4afec59b75aad5d13ef82d6931f35cc2d98918503916cf069355b77c5e42302","names":[],"sources":["packages/api/session-controller/src/types.ts:397"]}, {"digest":"a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc","names":[],"sources":[]}, {"digest":"a6b41f5bede8094d7bf635be5d69b1bd7a25e006ca9cf9415bceb9322f1a7feb","names":["packages/experimental/hook-protocol/src/types.ts#HookDialect"],"sources":["packages/experimental/hook-protocol/src/types.ts:48"]}, @@ -586,6 +589,7 @@ {"digest":"cffcdc19881ab61ec0a9ce18743996f7ac44ac8889f34dfac36ae745574aa22f","names":["packages/goal/goal/src/domain.ts#GoalClearChangeMeta"],"sources":["packages/goal/goal/src/domain.ts:35"]}, {"digest":"d0fbb0ca1bbba059830826f33f801da1aced35a2504376d61e020bdfec8d50c0","names":[],"sources":[]}, {"digest":"d28765f87d6b6ab408e0b2f3e7e297791b004b806d8e302c49903ebae815feb2","names":[],"sources":[]}, + {"digest":"d49164d37eab0f0f70fa787c65d887abbea869c010ccb103d717fab3519c3a41","names":[],"sources":[]}, {"digest":"d4b739491689cc6bb13d07ffac1a32239875abda364c0d5edd8f74faf5e9c00d","names":[],"sources":["packages/llm/llm/src/types.ts:501"]}, {"digest":"d4c799e3d51cae1040362ba4968efc484739e14416c9c1df20c2f108b4ef2eb5","names":[],"sources":[]}, {"digest":"d4e4d575abeb4b72d616dc65c17ac0eaa943cc135609395044c38d5afcb10e31","names":[],"sources":[]}, @@ -680,7 +684,6 @@ {"digest":"f6afe41e3926f422fe849debe90819fddd739f0f59e20be9c71018b9554a7f09","names":[],"sources":[]}, {"digest":"f6cb462f808309e315fea634f1ed86268b8346bbcee6774d9097ca6eed7ac1f8","names":[],"sources":[]}, {"digest":"f7283223aacdc39b0adca728c8cbd4c090a20006a6fecec74421c30e943b489c","names":[],"sources":[]}, - {"digest":"f731ffe80810b0289b3e694d8f40af6f01865c02a622ee457be90ca808b66e87","names":["packages/subagent/subagent/src/catalog.ts#SubagentCatalogEvent"],"sources":["packages/subagent/subagent/src/catalog.ts:28"]}, {"digest":"f789af5ab844827eb5a4f357c0afae1eeed108c6c22d8ca1a56e586b349a7c85","names":[],"sources":[]}, {"digest":"f88f1b7b6bb733f685f5fb6a8744a5db9199fa53c90463ea1a33bad51153671d","names":[],"sources":[]}, {"digest":"f90eb4ab2d3897bc20c521a038e3119d581f791ec974b1a0004f63a8ab9e232d","names":[],"sources":["packages/core/session/src/types.ts:294","packages/deliverables/workspace-changes/src/types.ts:106"]}, @@ -710,6 +713,7 @@ "02b2c23f2d94b028": {"kind":"object","properties":[{"name":"kind","type":"c5687b1313206a6f","optional":false}],"indices":[]}, "0341d4c3dfcd5325": {"kind":"literal","value":"product-interaction"}, "05205074b24d8340": {"kind":"literal","value":"action-cards"}, + "058fc9a6b4fbd5d4": {"kind":"union","types":["0e10d7c096b47e61","5d542ac7bdf13310","8faed5b52ab2d2df","a0f25b5aeeebf2bf","a43327a1739c3fa4","b6896938660a1dfe","cbbe8377ee744964","e2ad8f5bcd813f26"]}, "05a2796621b1ee6c": {"kind":"literal","value":"unknown"}, "05d2ae1e0c7f8770": {"kind":"union","types":["1571525adc95a24e","9e07693fd7cd9bab"]}, "0657d3ca5ac4e7bf": {"kind":"primitive","type":"number"}, @@ -746,6 +750,7 @@ "12a0cfa619ac4a5f": {"kind":"literal","value":"read-only"}, "12d5808fac1996b9": {"kind":"object","properties":[{"name":"attachment","type":"b0b0eb1b16490f6b","optional":false},{"name":"offloaded","type":"dacf3d5f3e4ff619","optional":true},{"name":"type","type":"fb8b6a2e53cb9cf4","optional":false}],"indices":[]}, "1379f0b72275994d": {"kind":"literal","value":"developer/message"}, + "137ac60e26bcb192": {"kind":"literal","value":3}, "13d3d180f977bf78": {"kind":"object","properties":[{"name":"data","type":"1528539c63db8b23","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"1a2718b00937b236","optional":false}],"indices":[]}, "14230f9b8b5797bb": {"kind":"literal","value":"chunk"}, "142c37d302aa22a0": {"kind":"literal","value":"always"}, @@ -904,7 +909,6 @@ "52c6113cb0e53f34": {"kind":"object","properties":[{"name":"form","type":"6f9e00383836275e","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false},{"name":"sections","type":"c748fb42c80a1200","optional":false}],"indices":[]}, "5302e76315127761": {"kind":"object","properties":[{"name":"error","type":"a81059ca06999da1","optional":true},{"name":"message","type":"4b259f7f8f0a44ad","optional":false},{"name":"meta","type":"c592ce75aab73fca","optional":true},{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "530da6c0267813bb": {"kind":"union","types":["34d1ab5c5df37818","4f507598cf476214","e7ce49f12953583f","f789af5ab844827e"]}, - "53750b9abf61efb7": {"kind":"object","properties":[{"name":"data","type":"f731ffe80810b028","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"44c8c77aa9aa100b","optional":false}],"indices":[]}, "53cf1704ae0709c7": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"99e7e7d7fed55ae6","optional":false}],"indices":[]}, "54f9276a1617d7c1": {"kind":"object","properties":[{"name":"step","type":"0657d3ca5ac4e7bf","optional":false},{"name":"stream","type":"84bbfdce7d2eab5b","optional":false},{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, "557e148d30b79ee4": {"kind":"literal","value":"agent-preset/selected"}, @@ -1056,6 +1060,7 @@ "8e8e91646a5cae3f": {"kind":"object","properties":[{"name":"id","type":"4cd48f3e5108bd6e","optional":false},{"name":"operation","type":"26c879bdd130e8b8","optional":false},{"name":"version","type":"f88f1b7b6bb733f6","optional":false}],"indices":[]}, "8f324cea3916c051": {"kind":"object","properties":[{"name":"form","type":"16d0507cb94b4cfc","optional":false},{"name":"kind","type":"2f3f201c8846e19c","optional":false}],"indices":[]}, "8f6d609794bf5afc": {"kind":"tuple","elements":[{"type":"4e88776858652856","optional":false,"rest":false}]}, + "8faed5b52ab2d2df": {"kind":"object","properties":[{"name":"childCreatedAt","type":"0657d3ca5ac4e7bf","optional":false},{"name":"childId","type":"4cd48f3e5108bd6e","optional":false},{"name":"label","type":"4cd48f3e5108bd6e","optional":false},{"name":"mode","type":"e5d3828df1ec3e2a","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false},{"name":"version","type":"137ac60e26bcb192","optional":false}],"indices":[]}, "8fcb0b44a575e998": {"kind":"object","properties":[{"name":"kind","type":"a720757f704a2512","optional":false}],"indices":[]}, "904bf499d98be4a3": {"kind":"union","types":["7382c4fc95fe8c5d","b765ee31033cea46","f6cb462f808309e3"]}, "920d87e22b7409bc": {"kind":"object","properties":[{"name":"data","type":"c2a13242efc3bea4","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"d8a476d2c6fd30df","optional":false}],"indices":[]}, @@ -1107,6 +1112,7 @@ "a3c6f1ea38e9a61d": {"kind":"object","properties":[{"name":"form","type":"e8de0f457fed5b9f","optional":false},{"name":"kind","type":"9ac52f0f84fec2eb","optional":false},{"name":"summary","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "a3edd1efd1fef21b": {"kind":"literal","value":"workspace-write"}, "a40d12070f6f4a12": {"kind":"object","properties":[{"name":"task","type":"51edec100659da3a","optional":false},{"name":"teamId","type":"4cd48f3e5108bd6e","optional":false},{"name":"version","type":"a0f906856fc06f1c","optional":false}],"indices":[]}, + "a43327a1739c3fa4": {"kind":"object","properties":[{"name":"childCreatedAt","type":"0657d3ca5ac4e7bf","optional":false},{"name":"childId","type":"4cd48f3e5108bd6e","optional":false},{"name":"label","type":"4cd48f3e5108bd6e","optional":true},{"name":"mode","type":"fc351843b8ecbd05","optional":false},{"name":"name","type":"4cd48f3e5108bd6e","optional":false},{"name":"version","type":"137ac60e26bcb192","optional":false}],"indices":[]}, "a4afec59b75aad5d": {"kind":"object","properties":[{"name":"clientTimeZone","type":"4cd48f3e5108bd6e","optional":true},{"name":"kind","type":"68f44f59e91c4139","optional":false},{"name":"principal","type":"824a4aad44a54e24","optional":true},{"name":"rpcId","type":"4cd48f3e5108bd6e","optional":false}],"indices":[]}, "a6567ccb2e530606": {"kind":"object","properties":[{"name":"data","type":"0d47a8f3d847243c","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"0a8a062bc9c3c0d9","optional":false}],"indices":[]}, "a6b41f5bede8094d": {"kind":"union","types":["15495614ccfa9cbd","aff60ef947c18b01"]}, @@ -1219,6 +1225,7 @@ "cffcdc19881ab61e": {"kind":"object","properties":[{"name":"cleared","type":"0b6d718c693fdf7f","optional":false},{"name":"clearedAt","type":"0657d3ca5ac4e7bf","optional":false},{"name":"kind","type":"237c004c73b6b288","optional":false},{"name":"operation","type":"e7e154d47d5fbc53","optional":false},{"name":"version","type":"f88f1b7b6bb733f6","optional":false}],"indices":[]}, "d0fbb0ca1bbba059": {"kind":"literal","value":"delete"}, "d28765f87d6b6ab4": {"kind":"literal","value":"one-shot"}, + "d49164d37eab0f0f": {"kind":"object","properties":[{"name":"data","type":"058fc9a6b4fbd5d4","optional":false},{"name":"ignorable","type":"dacf3d5f3e4ff619","optional":true},{"name":"seq","type":"0657d3ca5ac4e7bf","optional":false},{"name":"time","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"44c8c77aa9aa100b","optional":false}],"indices":[]}, "d4b739491689cc6b": {"kind":"object","properties":[{"name":"block","type":"8064c03c87c357fd","optional":false},{"name":"index","type":"0657d3ca5ac4e7bf","optional":false},{"name":"type","type":"eefbea819caeeea9","optional":false}],"indices":[]}, "d4c799e3d51cae10": {"kind":"literal","value":"working-directory/change"}, "d4e4d575abeb4b72": {"kind":"literal","value":"canceled"}, @@ -1313,7 +1320,6 @@ "f6afe41e3926f422": {"kind":"object","properties":[{"name":"form","type":"fee1caf8ab6a65c1","optional":false},{"name":"kind","type":"8067b0d232c350f1","optional":false}],"indices":[]}, "f6cb462f808309e3": {"kind":"literal","value":"replace"}, "f7283223aacdc39b": {"kind":"object","properties":[{"name":"kind","type":"9ac52f0f84fec2eb","optional":false}],"indices":[]}, - "f731ffe80810b028": {"kind":"union","types":["0e10d7c096b47e61","5d542ac7bdf13310","a0f25b5aeeebf2bf","b6896938660a1dfe","cbbe8377ee744964","e2ad8f5bcd813f26"]}, "f789af5ab844827e": {"kind":"literal","value":"change"}, "f88f1b7b6bb733f6": {"kind":"literal","value":1}, "f90eb4ab2d3897bc": {"kind":"object","properties":[{"name":"turn","type":"0657d3ca5ac4e7bf","optional":false}],"indices":[]}, diff --git a/docs/subsystems/agent-team.i18n.yaml b/docs/subsystems/agent-team.i18n.yaml index 435e1b6bbb..0426fb7a10 100644 --- a/docs/subsystems/agent-team.i18n.yaml +++ b/docs/subsystems/agent-team.i18n.yaml @@ -15,8 +15,8 @@ en: c70f0fd199fc7db5 zh: c6b77cb5a5cfd1c2 /agent-teams/web-projection: - en: 059d4e5d6a9ddc71 - zh: cb3fd92096144e19 + en: af4ffca1efab0f56 + zh: 1f53c9c258cc5a75 /agent-teams/replay: en: 448d32856f87b067 zh: 7fafd96314c7e2c5 diff --git a/docs/subsystems/agent-team.md b/docs/subsystems/agent-team.md index 49b27ea176..c6f2d8db52 100644 --- a/docs/subsystems/agent-team.md +++ b/docs/subsystems/agent-team.md @@ -77,7 +77,7 @@ interface TeamTaskSnapshot { `agentTeam.enabled` exposes the root Session’s saved selection. New Sessions default to false; `team/mode` records explicit selections. Historical tasks imply enabled until an explicit selection. Only the root can call `setEnabled`; descendants share that mode. The Web menu calls the same method through `remote.agentTeams.setEnabled`, with the root Session id resolved to its live Agent by the Gateway. Mode writes take effect before awaiting persistence. Tools resolve availability at step preparation; prompt providers read the selection when evaluated. Task mutations check the current mode when their queued operation starts; admitted mutations may finish. Mode changes do not end inbox waits. A failed flush leaves the appended selection active; retrying the same selection flushes it without a duplicate event. -The root Session publishes non-deleted task views through `SessionProjectionMap.agentTeam`. `failure` reports a rejected persisted task or mode record beside the last valid state. Historical roster and mailbox records are not projected. The task panel identifies every owner by its Session id. +The root Session publishes non-deleted task views through `SessionProjectionMap.agentTeam`. `failure` reports a rejected persisted task or mode record beside the last valid state. Historical roster and mailbox records are not projected. Views identify every owner by its Session id; the task panel and task tools display the owner's agent name, or the Session id when the owner has no name. ```ts type-equiv /** Runtime-enriched task view returned to tools and hosts. */ diff --git a/docs/subsystems/agent-team.zh.md b/docs/subsystems/agent-team.zh.md index fb52d49be4..2043c70a0c 100644 --- a/docs/subsystems/agent-team.zh.md +++ b/docs/subsystems/agent-team.zh.md @@ -77,7 +77,7 @@ interface TeamTaskSnapshot { `agentTeam.enabled` 提供根 Session 保存的选择。新 Session 默认为 false;`team/mode` 记录显式选择。历史任务在显式选择前视为开启。仅根可调用 `setEnabled`;后代共享该模式。Web 菜单通过 `remote.agentTeams.setEnabled` 调用同一方法,Gateway 将根 Session id 解析为其存活 Agent。模式写入先立即生效,再等待刷盘。工具在步骤准备时确定可用性;提示词提供方在求值时读取选择。任务修改在排队操作开始时检查当前模式;已获准的修改可继续完成。模式变更不会结束收件箱等待。刷盘失败时已追加的选择仍然生效;重试相同选择会再次刷盘,不重复追加事件。 -根 Session 通过 `SessionProjectionMap.agentTeam` 发布未删除任务视图。`failure` 在最后有效状态旁报告被拒绝的持久化任务或模式记录。历史名册与邮箱记录不参与投影。任务面板以 Session id 标识每个负责人。 +根 Session 通过 `SessionProjectionMap.agentTeam` 发布未删除任务视图。`failure` 在最后有效状态旁报告被拒绝的持久化任务或模式记录。历史名册与邮箱记录不参与投影。视图以 Session id 标识每个负责人;任务面板和任务工具显示负责人的 agent 名称,负责人没有名称时显示其 Session id。 ```ts type-equiv /** Runtime-enriched task view returned to tools and hosts. */ diff --git a/docs/subsystems/subagent.i18n.yaml b/docs/subsystems/subagent.i18n.yaml index b2d7d88b37..a5dbf87b71 100644 --- a/docs/subsystems/subagent.i18n.yaml +++ b/docs/subsystems/subagent.i18n.yaml @@ -11,6 +11,9 @@ /subagents/local-children-and-activations: en: 6f87231d18d82916 zh: 9f064b23058f8766 +/subagents/agent-names: + en: 50a0b5505111af2c + zh: e4f4172ca212771d /subagents/messages-and-interruption: en: 9befacb931520842 zh: c214b4fffa779b06 @@ -24,5 +27,5 @@ en: 226ba8094a8cf96c zh: 719678662905d1eb /subagents/durable-enumeration-listdescendants-and-catalog-entries: - en: 8000138f84fd15fa - zh: 32fa91d3aebd3d01 + en: 992acbcc3ac77e30 + zh: 83b9dd42ade734ab diff --git a/docs/subsystems/subagent.md b/docs/subsystems/subagent.md index 0e3235cf91..2eacbc1514 100644 --- a/docs/subsystems/subagent.md +++ b/docs/subsystems/subagent.md @@ -33,6 +33,12 @@ interface SubagentActivationSpec extends Omit */ rootOf(caller: Agent): Agent +/** + * Report the model-facing names of a live tree member and its direct creator. + * The root is `lead`; a descendant is the slash-separated path of names from + * the root, or its Session id when it or a creator below the root has no name. + * @param agent - exact live root or resident tree member. + * @returns the member's name and, for a descendant, its creator's name. + * @throws when the caller or its runtime owner is unavailable. + */ +namesOf(agent: Agent): SubagentMemberNames + +/** + * Resolve a model-supplied agent reference within the caller's tree. `lead` + * selects the root; a slash-separated path is looked up through the catalogs + * along it; any other text is returned as a Session id. Resolution grants no + * authority: messaging, interruption, and assignment authorize the result. + * @param caller - exact live Agent whose tree root anchors the lookup. + * @param name - `lead`, a full name from the root, or a Session id. + * @param signal - cancellation forwarded to each catalog read. + * @returns the referenced Session id. + * @throws when the caller's root is unavailable or a catalog along the path cannot be read. + */ +async resolveName(caller: Agent, name: string, signal: AbortSignal): Promise + /** * Join progressing subagent tree work for its root. Idle members with parked input * stay resident without delaying host completion. Ordinary members return false. diff --git a/docs/subsystems/subagent.zh.md b/docs/subsystems/subagent.zh.md index 785644d1cf..5728567233 100644 --- a/docs/subsystems/subagent.zh.md +++ b/docs/subsystems/subagent.zh.md @@ -33,6 +33,12 @@ interface SubagentActivationSpec extends Omit */ rootOf(caller: Agent): Agent +/** + * Report the model-facing names of a live tree member and its direct creator. + * The root is `lead`; a descendant is the slash-separated path of names from + * the root, or its Session id when it or a creator below the root has no name. + * @param agent - exact live root or resident tree member. + * @returns the member's name and, for a descendant, its creator's name. + * @throws when the caller or its runtime owner is unavailable. + */ +namesOf(agent: Agent): SubagentMemberNames + +/** + * Resolve a model-supplied agent reference within the caller's tree. `lead` + * selects the root; a slash-separated path is looked up through the catalogs + * along it; any other text is returned as a Session id. Resolution grants no + * authority: messaging, interruption, and assignment authorize the result. + * @param caller - exact live Agent whose tree root anchors the lookup. + * @param name - `lead`, a full name from the root, or a Session id. + * @param signal - cancellation forwarded to each catalog read. + * @returns the referenced Session id. + * @throws when the caller's root is unavailable or a catalog along the path cannot be read. + */ +async resolveName(caller: Agent, name: string, signal: AbortSignal): Promise + /** * Join progressing subagent tree work for its root. Idle members with parked input * stay resident without delaying host completion. Ordinary members return false. diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml index 880989b55c..1395a8afc1 100644 --- a/docs/tool-catalog.i18n.yaml +++ b/docs/tool-catalog.i18n.yaml @@ -9,8 +9,8 @@ en: 6cc2cd9ced70bb09 zh: 1c0e6cb4ba2ed637 /tool-schema-catalog/tool-package-map: - en: 6e8419f195bc96a1 - zh: 564c4f04420d329a + en: b208722c649460ba + zh: 8f8224ad1158f34b /tool-schema-catalog/deepseek-ai-dsh-plugin-manager: en: e0143916bb634762 zh: e0143916bb634762 @@ -240,8 +240,8 @@ en: 8daafae6faf7898e zh: 8daafae6faf7898e /tool-schema-catalog/deepseek-ai-dsh-experimental-tool-subagent-oar/subagent-oar: - en: 6283c3023d6ea436 - zh: 9c018b8c12a3f1e7 + en: 4936c61a65f8cdb2 + zh: 5a1bfbb9738a62a9 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent: en: 841abe0dd5913ea7 zh: 841abe0dd5913ea7 @@ -249,17 +249,17 @@ en: 207228068bc61885 zh: 36e9344ee1af8869 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent/subagent: - en: 430672da3c1a6ca6 - zh: bd3a9ab4089f9dbb + en: 9912eeda5bc40b43 + zh: 0cb70f9a0d923bee /tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control: en: ab82e130c3413f04 zh: ab82e130c3413f04 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control/interrupt-agent: en: a622b15cc2598d51 - zh: 48e1b05956d63e26 + zh: ece4b2af6a11c49c /tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control/list-agents: - en: a56168133c21c6a0 - zh: f9df1c0fe49fa225 + en: fddccad0b31e7fbc + zh: fbaf63fa019c4df7 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control/send-message: en: b53326fedeab850d zh: ecb75b6ce31da009 diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index 1dea12b08f..b252c70bc6 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -40,7 +40,7 @@ This table connects model-visible tool names to the plugin package and service s | `@deepseek-ai/dsh-tool-skill` | `skill` | `ctx.workingDirectory`, `ctx.tools`, `ctx.agents`, `ctx.skills` | `tool/call`, `tool/result`, `user/message replacement catalogs via agent.inject()` | - | - | | `@deepseek-ai/dsh-tool-session-query` | `session_event_read`, `session_event_search`, `session_event_trace`, `session_search`, `session_trace` | `ctx.tools`, `ctx.systemPrompt`, `ctx.sessionQuery`, `ctx.agents`, `a calling Agent for workspace authority` | `tool/call`, `tool/result` | - | The five read-only tools hide provider cursors and authorize every result from the immutable calling agent session. The package is opt-in; compositions that need enforced deadlines or bounded inline output also mount the generic timeout or spill policies. | | `@deepseek-ai/dsh-experimental-tool-subagent-oar` | `subagent_oar` | `ctx.tools`, `ctx.subagents` | `tool/call`, `tool/result`, `parent subagent catalog and completion notices` | - | The subagent_oar tool selects among configured aliases for external providers. This catalog shows Codex and Claude Code aliases; each deployment chooses its providers. Native model and permission settings belong to the selected provider configuration. | -| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`, `subagent` | `ctx.tools`, `ctx.subagents`, `ctx.systemPrompt`, `ctx.sessionProjections`, `ctx.llm for model discovery and selected-route validation` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The load-time `toolName` selects the delegation name (default `subagent`); `modelSelection` adds optional route fields. `modelDiscovery` defaults off and registers shared discovery on one instance per scope. Web and Desktop presets enable spawn discovery and route fields and leave `subagent_fork` route fields disabled. The Host settings service records authorization for each new top-level Session; child Sessions inherit it. Without a recorded policy, discovery and explicit selection permit only the current parent route. Every delegation returns a child id promptly; local children accept follow-ups, while external backends report their single execution automatically. | +| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`, `subagent` | `ctx.tools`, `ctx.subagents`, `ctx.systemPrompt`, `ctx.sessionProjections`, `ctx.llm for model discovery and selected-route validation` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The load-time `toolName` selects the delegation name (default `subagent`); `modelSelection` adds optional route fields. `modelDiscovery` defaults off and registers shared discovery on one instance per scope. Web and Desktop presets enable spawn discovery and route fields and leave `subagent_fork` route fields disabled. The Host settings service records authorization for each new top-level Session; child Sessions inherit it. Without a recorded policy, discovery and explicit selection permit only the current parent route. Every delegation returns the child's name promptly; local children accept follow-ups, while external backends report their single execution automatically. | | `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`, `list_agents`, `send_message`, `wait_agent` | `ctx.tools`, `ctx.subagents`, `ctx.agents` | `tool/call`, `tool/result`, `child session events through ctx.subagents` | - | The shared control tools discover and contact agents in the subagent tree, interrupt owned descendants, and wait for input in the caller’s inbox. Provider-bound `tool-subagent` instances supply delegation tools. | | `@deepseek-ai/dsh-tool-jobs` | `job_kill`, `job_list`, `job_output` | `ctx.tools`, `ctx.jobs`, `ctx.systemPrompt` | `tool/call`, `tool/result`, `user/message via agent.inject() for background completion notices` | - | The kind-agnostic background-job controller: background bash commands and PTY sends are read, listed, and killed through the same three tools. Loading the plugin attaches the controller that arms producers' `ctx.jobs.start()`. | | `@deepseek-ai/dsh-tool-agent-team` | `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `ctx.subagents`, `an exact live Team member Agent` | `tool/call`, `team/task`, `tool/result` | - | Four task-board tools share the subagent tree’s root Session. Enabling Team mode on the root Session adds task-board guidance alongside standard subagent delegation and control tools. Standard subagent controls provide required identity independently of Team mode. | @@ -1994,7 +1994,7 @@ The five read-only tools hide provider cursors and authorize every result from t ### `subagent_oar` -Delegate a self-contained task to a configured external agent. Choose its runtime for each call. The child works in its own context and does not see this conversation. This tool returns a child id; a later completion notice includes its final answer. The child does not accept follow-up messages. Start independent tasks together and continue useful work while they run. When only waiting for their results remains, end your turn instead of polling. +Delegate a self-contained task to a configured external agent. Choose its runtime for each call. The child works in its own context and does not see this conversation. This tool returns the child's name; a later completion notice includes its final answer. The child does not accept follow-up messages. Start independent tasks together and continue useful work while they run. When only waiting for their results remains, end your turn instead of polling. ```json { @@ -2012,6 +2012,10 @@ Delegate a self-contained task to a configured external agent. Choose its runtim "type": "string", "description": "A short description of the delegated task, for display." }, + "name": { + "type": "string", + "description": "Optional short name for the subagent, unique among the subagents you create: lower-case letters, digits, and hyphens, starting with a letter, at most 32 characters. Omitted names default to agent-1, agent-2, and so on." + }, "prompt": { "type": "string", "description": "The complete, self-contained task. Include all context the external agent needs." @@ -2061,7 +2065,7 @@ Source: [`packages/subagent/tool-subagent/src/list-models.ts`](../packages/subag ### `subagent` -Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes. +Delegate a self-contained task to a subagent (a separate agent that works in its own context) to offload focused, independent work — research, a scoped implementation, an analysis — so it does not consume this conversation's context. The subagent returns its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its name. If you are still online when it finishes, the runtime automatically delivers its completion notice. The notice includes the final answer unless the child sent you a message just before finishing. Use `send_message` to steer it while running or continue its conversation after it finishes. ```json { @@ -2075,6 +2079,10 @@ Delegate a self-contained task to a subagent (a separate agent that works in its "type": "string", "description": "A short (3-5 word) description of the delegated task, for display. Use the language of the user's current request." }, + "name": { + "type": "string", + "description": "Optional short name for the subagent, unique among the subagents you create: lower-case letters, digits, and hyphens, starting with a letter, at most 32 characters. Omitted names default to agent-1, agent-2, and so on." + }, "prompt": { "type": "string", "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." @@ -2089,7 +2097,7 @@ Delegate a self-contained task to a subagent (a separate agent that works in its Source: [`packages/subagent/tool-subagent/src/index.ts`](../packages/subagent/tool-subagent/src/index.ts) -The load-time `toolName` selects the delegation name (default `subagent`); `modelSelection` adds optional route fields. `modelDiscovery` defaults off and registers shared discovery on one instance per scope. Web and Desktop presets enable spawn discovery and route fields and leave `subagent_fork` route fields disabled. The Host settings service records authorization for each new top-level Session; child Sessions inherit it. Without a recorded policy, discovery and explicit selection permit only the current parent route. Every delegation returns a child id promptly; local children accept follow-ups, while external backends report their single execution automatically. +The load-time `toolName` selects the delegation name (default `subagent`); `modelSelection` adds optional route fields. `modelDiscovery` defaults off and registers shared discovery on one instance per scope. Web and Desktop presets enable spawn discovery and route fields and leave `subagent_fork` route fields disabled. The Host settings service records authorization for each new top-level Session; child Sessions inherit it. Without a recorded policy, discovery and explicit selection permit only the current parent route. Every delegation returns the child's name promptly; local children accept follow-ups, while external backends report their single execution automatically. @@ -2103,13 +2111,13 @@ Ask a subagent to stop its current work. This call returns without waiting for i { "type": "object", "properties": { - "agent_id": { + "agent": { "type": "string", - "description": "The id of an agent created under you: your direct child or a deeper descendant." + "description": "The name of an agent created under you: your direct child or a deeper descendant." } }, "required": [ - "agent_id" + "agent" ] } ``` @@ -2118,7 +2126,7 @@ Source: [`packages/subagent/tool-subagent-control/src/index.ts`](../packages/sub ### `list_agents` -List your entire subagent tree: the root agent and all reachable continuable descendants, including yourself. Rows carry ids, descriptions, and status; descendant rows carry parent ids and depth from the root. You can message any other agent in the subagent tree listed as running or inactive. running means it is working; inactive means no turn is executing; it does not indicate task completion, success, or failure. While you remain active, you will be notified when a direct child finishes; there is no need to keep checking its status. Use send_message to continue the conversation. +List your entire subagent tree: the root agent and all reachable continuable descendants, including yourself. Rows carry names, descriptions, and status. A descendant's name is its path from the root, such as reviewer/checker. You can message any other agent in the subagent tree listed as running or inactive. running means it is working; inactive means no turn is executing; it does not indicate task completion, success, or failure. While you remain active, you will be notified when a direct child finishes; there is no need to keep checking its status. Use send_message to continue the conversation. ```json { @@ -2137,9 +2145,9 @@ Send a message to an agent. A working agent receives it at its next step; an idl { "type": "object", "properties": { - "agent_id": { + "agent": { "type": "string", - "description": "The agent id of another member of your subagent tree, as shown by list_agents." + "description": "The name of another agent in your subagent tree, as shown by list_agents." }, "message": { "type": "string", @@ -2147,7 +2155,7 @@ Send a message to an agent. A working agent receives it at its next step; an idl } }, "required": [ - "agent_id", + "agent", "message" ] } @@ -2332,7 +2340,7 @@ List shared tasks, including readiness, owner, revision, blockers, and write-sco }, "owner": { "type": "string", - "description": "Optional exact ownerId filter; use unowned for tasks without an owner." + "description": "Optional owner agent name filter; use unowned for tasks without an owner." }, "ready": { "type": "boolean", @@ -2406,7 +2414,7 @@ Compare-and-set a shared task action using the latest revision from team_task_ge }, "owner": { "type": "string", - "description": "Agent id of the root or any continuable descendant for root-only reassign; omit to unassign." + "description": "Agent name of the root or any continuable descendant for root-only reassign; omit to unassign." } }, "required": [ diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index b9c2e6d41e..7cb31cc9df 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -44,7 +44,7 @@ | `@deepseek-ai/dsh-tool-skill` | `skill` | `ctx.workingDirectory`、`ctx.tools`、`ctx.agents`、`ctx.skills` | `tool/call`、`tool/result`、`user/message replacement catalogs via agent.inject()` | - | - | | `@deepseek-ai/dsh-tool-session-query` | `session_event_read`、`session_event_search`、`session_event_trace`、`session_search`、`session_trace` | `ctx.tools`、`ctx.systemPrompt`、`ctx.sessionQuery`、`a calling Agent for workspace authority` | `tool/call`、`tool/result` | - | 这 5 个只读工具会隐藏提供方游标,并根据不可变的调用 agent 会话为每个结果授权。该包需要选择启用;需要强制截止时间或限制行内输出的组合还会挂载通用超时或 spill 策略。 | | `@deepseek-ai/dsh-experimental-tool-subagent-oar` | `subagent_oar` | `ctx.tools`, `ctx.subagents` | `tool/call`, `tool/result`, `parent subagent catalog and completion notices` | - | subagent_oar 工具在为外部提供方配置的别名之间选择。本目录展示 Codex 和 Claude Code 别名;每个部署自行选择提供方。原生模型和权限设置由所选提供方的配置决定。 | -| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`ctx.sessionProjections`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 加载时的 `toolName` 选择委派名称(默认为 `subagent`);`modelSelection` 增加可选路由字段。`modelDiscovery` 默认关闭,每个作用域由一个实例注册共享发现工具。Web 与 Desktop preset 启用 spawn 发现和路由字段,并为 `subagent_fork` 保持路由字段关闭。Host 设置服务为每个新顶层 Session 记录授权;子 Session 继承该授权。没有已记录策略时,发现和显式选择只允许当前父级路由。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 | +| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`ctx.sessionProjections`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 加载时的 `toolName` 选择委派名称(默认为 `subagent`);`modelSelection` 增加可选路由字段。`modelDiscovery` 默认关闭,每个作用域由一个实例注册共享发现工具。Web 与 Desktop preset 启用 spawn 发现和路由字段,并为 `subagent_fork` 保持路由字段关闭。Host 设置服务为每个新顶层 Session 记录授权;子 Session 继承该授权。没有已记录策略时,发现和显式选择只允许当前父级路由。每次委派都会立即返回子代理的名称;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 | | `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`、`list_agents`、`send_message`、`wait_agent` | `ctx.tools`、`ctx.subagents`、`ctx.agents` | `tool/call`、`tool/result`、`child session events through ctx.subagents` | - | 共享控制工具发现并联系 subagent 树中的 Agent,中断有权控制的后代,并等待调用者收件箱中的输入。绑定提供方的 `tool-subagent` 实例提供委派工具。 | | `@deepseek-ai/dsh-tool-jobs` | `job_kill`、`job_list`、`job_output` | `ctx.tools`、`ctx.jobs`、`ctx.systemPrompt` | `tool/call`、`tool/result`、`user/message via agent.inject() for background completion notices` | - | 与任务种类无关的后台任务控制器:后台 bash 命令与 PTY 发送 都通过相同的 3 个工具读取、列出和终止。加载该插件会挂接控制器,从而启用生产方的 `ctx.jobs.start()`。 | | `@deepseek-ai/dsh-tool-agent-team` | `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `ctx.subagents`, `an exact live Team member Agent` | `tool/call`, `team/task`, `tool/result` | - | 四个任务板工具共享 subagent 树的根 Session。在根 Session 开启 Team 模式,会在标准 subagent 委派和控制工具之外增加任务板指导。标准 subagent 控制工具提供必需身份,不受 Team 模式影响。 | @@ -2000,7 +2000,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, ### `subagent_oar` -将上下文完整的任务委派给已配置的外部 agent。每次调用选择一个运行时。subagent 在独立上下文中工作,无法看到当前对话。工具返回子级 id,后续完成通知包含其最终回答。subagent 不接受后续消息。并发启动相互独立的任务,并在其运行期间继续其他有用工作。只剩等待结果时结束当前轮次,不要轮询。 +将上下文完整的任务委派给已配置的外部 agent。每次调用选择一个运行时。subagent 在独立上下文中工作,无法看到当前对话。工具返回子级的名称,后续完成通知包含其最终回答。subagent 不接受后续消息。并发启动相互独立的任务,并在其运行期间继续其他有用工作。只剩等待结果时结束当前轮次,不要轮询。 ```json { @@ -2018,6 +2018,10 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, "type": "string", "description": "A short description of the delegated task, for display." }, + "name": { + "type": "string", + "description": "Optional short name for the subagent, unique among the subagents you create: lower-case letters, digits, and hyphens, starting with a letter, at most 32 characters. Omitted names default to agent-1, agent-2, and so on." + }, "prompt": { "type": "string", "description": "The complete, self-contained task. Include all context the external agent needs." @@ -2067,7 +2071,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 ### `subagent` -将一项自包含任务委派给 subagent(在自身上下文中工作的独立 agent),用它卸载聚焦且独立的工作,例如研究、限定范围的实现或分析,以免消耗当前对话的上下文。subagent 会返回结果,但不会返回中间步骤。此工具启动独立管理的 subagent 并立即返回其 id。如果它结束时你仍在线,运行时会自动递送其完成通知。通知包含最终回答,除非子任务在结束前刚给你发过消息。你可以使用 `send_message` 在运行时引导它,或在结束后继续对话。 +将一项自包含任务委派给 subagent(在自身上下文中工作的独立 agent),用它卸载聚焦且独立的工作,例如研究、限定范围的实现或分析,以免消耗当前对话的上下文。subagent 会返回结果,但不会返回中间步骤。此工具启动独立管理的 subagent 并立即返回其名称。如果它结束时你仍在线,运行时会自动递送其完成通知。通知包含最终回答,除非子任务在结束前刚给你发过消息。你可以使用 `send_message` 在运行时引导它,或在结束后继续对话。 ```json { @@ -2081,6 +2085,10 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 "type": "string", "description": "A short (3-5 word) description of the delegated task, for display. Use the language of the user's current request." }, + "name": { + "type": "string", + "description": "Optional short name for the subagent, unique among the subagents you create: lower-case letters, digits, and hyphens, starting with a letter, at most 32 characters. Omitted names default to agent-1, agent-2, and so on." + }, "prompt": { "type": "string", "description": "The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs." @@ -2095,7 +2103,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 来源:[`packages/subagent/tool-subagent/src/index.ts`](../packages/subagent/tool-subagent/src/index.ts) -加载时的 `toolName` 选择委派名称(默认为 `subagent`);`modelSelection` 增加可选路由字段。`modelDiscovery` 默认关闭,每个作用域由一个实例注册共享发现工具。Web 与 Desktop preset 启用 spawn 发现和路由字段,并为 `subagent_fork` 保持路由字段关闭。Host 设置服务为每个新顶层 Session 记录授权;子 Session 继承该授权。没有已记录策略时,发现和显式选择只允许当前父级路由。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 +加载时的 `toolName` 选择委派名称(默认为 `subagent`);`modelSelection` 增加可选路由字段。`modelDiscovery` 默认关闭,每个作用域由一个实例注册共享发现工具。Web 与 Desktop preset 启用 spawn 发现和路由字段,并为 `subagent_fork` 保持路由字段关闭。Host 设置服务为每个新顶层 Session 记录授权;子 Session 继承该授权。没有已记录策略时,发现和显式选择只允许当前父级路由。每次委派都会立即返回子代理的名称;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 @@ -2103,19 +2111,19 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 ### `interrupt_agent` -根据 agent id 请求取消后台 agent 的当前轮次。目标可以是你的直接子级,也可以是在你下方创建的更深层 agent。对于本地 agent,只有当前轮次会停止:已经排队发给该 agent 的消息会一直搁置到后续的 send_message;它启动的 agent 会继续运行;该 agent 本身仍可接受后续操作。外部执行会永久停止,无法接收后续消息。停止请求被接受后,此调用立即返回,因此目标可能还会短暂运行;中断一个已经完成的 agent 是可接受的空操作。 +请求 subagent 停止当前工作。此调用不等待其停止即返回。之后可以用 send_message 继续本地 subagent 的对话。外部执行会永久停止,无法接收后续消息。它启动的 subagent 会继续运行。 ```json { "type": "object", "properties": { - "agent_id": { + "agent": { "type": "string", - "description": "The id of an agent created under you: your direct child or a deeper descendant." + "description": "The name of an agent created under you: your direct child or a deeper descendant." } }, "required": [ - "agent_id" + "agent" ] } ``` @@ -2124,7 +2132,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 ### `list_agents` -列出整棵 subagent 树:根代理和所有可达的可继续后代,包括调用方自身。行包含 id、描述和状态;后代行包含父级 id 和相对根的深度。可以向树中列为 running 或 inactive 的任意其他代理发送消息。running 表示正在工作;inactive 表示当前没有轮次在执行,不表示任务完成、成功或失败。自身仍在线时,直属子代理结束会收到通知,无需反复查询其状态。使用 send_message 继续对话。 +列出整棵 subagent 树:根代理和所有可达的可继续后代,包括调用方自身。行包含名称、描述和状态。后代的名称是其相对根的路径,例如 reviewer/checker。可以向树中列为 running 或 inactive 的任意其他代理发送消息。running 表示正在工作;inactive 表示当前没有轮次在执行,不表示任务完成、成功或失败。自身仍在线时,直属子代理结束会收到通知,无需反复查询其状态。使用 send_message 继续对话。 ```json { @@ -2143,9 +2151,9 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 { "type": "object", "properties": { - "agent_id": { + "agent": { "type": "string", - "description": "The agent id of another member of your subagent tree, as shown by list_agents." + "description": "The name of another agent in your subagent tree, as shown by list_agents." }, "message": { "type": "string", @@ -2153,7 +2161,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 } }, "required": [ - "agent_id", + "agent", "message" ] } @@ -2338,7 +2346,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 }, "owner": { "type": "string", - "description": "Optional exact ownerId filter; use unowned for tasks without an owner." + "description": "Optional owner agent name filter; use unowned for tasks without an owner." }, "ready": { "type": "boolean", @@ -2412,7 +2420,7 @@ subagent_oar 工具在为外部提供方配置的别名之间选择。本目录 }, "owner": { "type": "string", - "description": "Agent id of the root or any continuable descendant for root-only reassign; omit to unassign." + "description": "Agent name of the root or any continuable descendant for root-only reassign; omit to unassign." } }, "required": [ diff --git a/docs/upgrade-guide/v0.2.2-alpha.2/subagent-names/guide.i18n.yaml b/docs/upgrade-guide/v0.2.2-alpha.2/subagent-names/guide.i18n.yaml new file mode 100644 index 0000000000..ee3b11b168 --- /dev/null +++ b/docs/upgrade-guide/v0.2.2-alpha.2/subagent-names/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.2-alpha.2/subagent-names/guide.md +/: + en: 9d10fcdf4af74152 + zh: 1e932a7fe192df48 +/subagent-tools-address-agents-by-name: + en: ae90ad0e4999f3e2 + zh: 312d85156ab6c77d +/subagent-tools-address-agents-by-name/change: + en: a08e4e84f7e3a99c + zh: 29f4b80788a9cbc5 +/subagent-tools-address-agents-by-name/migration: + en: 360eb6737d05896c + zh: 1689cd23cf2000bd diff --git a/docs/upgrade-guide/v0.2.2-alpha.2/subagent-names/guide.md b/docs/upgrade-guide/v0.2.2-alpha.2/subagent-names/guide.md new file mode 100644 index 0000000000..57b79fb887 --- /dev/null +++ b/docs/upgrade-guide/v0.2.2-alpha.2/subagent-names/guide.md @@ -0,0 +1,29 @@ +--- +kind: upgrade-guide +description: "Subagent control and Team task tools address agents by name, list_agents rows change, and subagent catalogs write payload version 3." +--- + +# Subagent tools address agents by name + +English | [中文](guide.zh.md) + +## Change + +Each new subagent has a name that is unique among its creator's children and recorded in the creator's `subagent/catalog` entry. The root is `lead`; a descendant's full name is its path from the root, such as `reviewer/checker`. The [subagent runtime](../../../../packages/subagent/subagent/README.md#agent-names) defines the name rule. Children created before upgrading keep their Session ids as names. These changes affect scripts, Code-mode (PTC) programs, SDK callers, and recorded transcripts that call or parse these tools, and code that reads catalogs or constructs activations: + +- `send_message({ agent_id, message })` becomes `send_message({ agent, message })`, and `interrupt_agent({ agent_id })` becomes `interrupt_agent({ agent })`. `agent` accepts a name or a Session id. A call that still passes `agent_id` fails argument validation. +- `subagent`, `subagent_fork`, and `subagent_oar` accept an optional `name`. They return `started subagent `, and their result is `{ kind: 'activation', subagentId, name }`. +- `list_agents` returns `{ name, status, description? }` rows instead of rows with `kind`, `id`, `label`, `parent`, `depth`, and `reason`. Unreadable branches report `corrupt`, `unsupported`, or `unavailable` as `status`. Text rows read ` [] — `. +- Team task results replace `ownerId` with `owner`, the owner's name. `team_task_list` and `team_task_update` accept a name as `owner`. +- Local and parent-delivered external creation write `subagent/catalog` payload version 3. Releases before this one reject catalogs that contain it, so they cannot open Sessions that create subagents after upgrading. See the [persistence acknowledgement](../../../persistence-changes/2026-11-02-subagent-catalog-names.md). +- `SubagentActivation` has a required `name` field, and `SubagentActivationSpec` accepts an optional `name`. + +## Migration + +1. In scripts, PTC programs, and SDK calls, rename the `agent_id` argument of `send_message` and `interrupt_agent` to `agent`. Pass the name that `list_agents` or the start receipt reports; a saved Session id still works. +2. Update parsers of `list_agents` output to read `name`, `status`, and `description`. Derive a member's creator from its name: the text before the last `/`, or `lead` for a name without `/`. Children created before upgrading appear under their Session ids. +3. Update Team task consumers to read `owner` instead of `ownerId`. Host code that calls `ctx.agentTeams` still reads `ownerId`. +4. Update recorded transcripts or fixtures that contain `agent_id` arguments, `started subagent ` receipts, identity runtime context text, or the earlier `list_agents` rows. +5. In code that constructs `SubagentActivation` values, such as test doubles, add `name`. Code that reads catalog rows can read the optional `name` field. +6. Upgrade every process that opens the same Session store before creating subagents. Older releases cannot open Sessions that contain catalog payload version 3. +7. To confirm the change, start a subagent with `name: "reviewer"` and check that the receipt reads `started subagent reviewer`, that `list_agents` lists `reviewer`, and that `send_message({ agent: "reviewer", message })` succeeds. diff --git a/docs/upgrade-guide/v0.2.2-alpha.2/subagent-names/guide.zh.md b/docs/upgrade-guide/v0.2.2-alpha.2/subagent-names/guide.zh.md new file mode 100644 index 0000000000..ea4ff7fbfe --- /dev/null +++ b/docs/upgrade-guide/v0.2.2-alpha.2/subagent-names/guide.zh.md @@ -0,0 +1,29 @@ +--- +kind: upgrade-guide +description: "Subagent 控制工具和 Team 任务工具按名称寻址 agent,list_agents 行格式改变,subagent 目录写入第 3 版载荷。" +--- + +# Subagent 工具按名称寻址 agent + +[English](guide.md) | 中文 + +## 变更 + +每个新 subagent(子智能体)都有一个名称,在其创建者的子级之间唯一,并记录在创建者的 `subagent/catalog` 条目中。根的名称为 `lead`;后代的完整名称是其相对根的路径,例如 `reviewer/checker`。[subagent 运行时](../../../../packages/subagent/subagent/README.zh.md#agent-names)定义名称规则。升级前创建的子级继续以 Session id 作为名称。以下变更影响调用或解析这些工具的脚本、Code 模式(PTC)程序、SDK 调用方和录制的 transcript(文本记录),以及读取目录或构造 activation 的代码: + +- `send_message({ agent_id, message })` 改为 `send_message({ agent, message })`,`interrupt_agent({ agent_id })` 改为 `interrupt_agent({ agent })`。`agent` 接受名称或 Session id。仍传入 `agent_id` 的调用无法通过参数校验。 +- `subagent`、`subagent_fork` 和 `subagent_oar` 接受可选的 `name`。它们返回 `started subagent `,结果为 `{ kind: 'activation', subagentId, name }`。 +- `list_agents` 返回 `{ name, status, description? }` 行,取代含 `kind`、`id`、`label`、`parent`、`depth` 和 `reason` 的行。不可读分支以 `status` 报告 `corrupt`、`unsupported` 或 `unavailable`。文本行格式为 ` [] — `。 +- Team 任务结果以 `owner`(所有者名称)取代 `ownerId`。`team_task_list` 和 `team_task_update` 的 `owner` 接受名称。 +- 本地创建和父级投递的外部创建写入第 3 版 `subagent/catalog` 载荷。本版本之前的发布会拒绝包含该载荷的目录,因此无法打开升级后创建过 subagent 的 Session。参见[持久化确认记录](../../../persistence-changes/2026-11-02-subagent-catalog-names.zh.md)。 +- `SubagentActivation` 新增必需字段 `name`,`SubagentActivationSpec` 接受可选的 `name`。 + +## 迁移 + +1. 在脚本、PTC 程序和 SDK 调用中,将 `send_message` 和 `interrupt_agent` 的 `agent_id` 参数改名为 `agent`。传入 `list_agents` 或启动回执报告的名称;已保存的 Session id 仍可使用。 +2. 更新 `list_agents` 输出的解析器,读取 `name`、`status` 和 `description`。从名称推导成员的创建者:最后一个 `/` 之前的文本;名称不含 `/` 时为 `lead`。升级前创建的子级以 Session id 列出。 +3. 更新 Team 任务消费方,读取 `owner` 而非 `ownerId`。调用 `ctx.agentTeams` 的 host 代码仍读取 `ownerId`。 +4. 更新包含 `agent_id` 参数、`started subagent ` 回执、身份运行时上下文文本或旧 `list_agents` 行的录制 transcript 或 fixture。 +5. 在构造 `SubagentActivation` 值的代码(例如测试替身)中添加 `name`。读取目录行的代码可读取可选的 `name` 字段。 +6. 在创建 subagent 之前,升级所有打开同一 Session 存储的进程。旧版本无法打开包含第 3 版目录载荷的 Session。 +7. 验证迁移:以 `name: "reviewer"` 启动一个 subagent,确认回执为 `started subagent reviewer`,`list_agents` 列出 `reviewer`,且 `send_message({ agent: "reviewer", message })` 成功。 diff --git a/packages/bundle/base/README.i18n.yaml b/packages/bundle/base/README.i18n.yaml index 8c6ebd500c..b475ce9b82 100644 --- a/packages/bundle/base/README.i18n.yaml +++ b/packages/bundle/base/README.i18n.yaml @@ -21,8 +21,8 @@ en: 0f971f7ac7c09139 zh: ceb866da681c709c /deepseek-ai-dsh-base/use-this-package/what-you-get: - en: 94430c891006526d - zh: 94ba771960ec6f71 + en: ab2fb279cdd726c5 + zh: ae84c61bddc00ddf /deepseek-ai-dsh-base/use-this-package/shell-tools-per-platform: en: 2dce12f82d6ee955 zh: d4599eb899a3cd88 diff --git a/packages/bundle/base/README.md b/packages/bundle/base/README.md index db77c345ec..5b5f163694 100644 --- a/packages/bundle/base/README.md +++ b/packages/bundle/base/README.md @@ -49,7 +49,7 @@ Run `dsh --profile my-profile "your task"` and you get a working agent with mode Out of the box, every profile built on this core provides: a DeepSeek model connection (the provider and model are configurable, and you can enable extra providers from your settings), the full tool set — file editing, shell commands, web search, HTTP(S) fetch, subagents, task and goal tracking — durable sessions that survive restarts, and the default permission policy that confines file writes to your workspace and asks before risky actions. Web fetch runs without per-call approval and can reach loopback and private-network hosts. Feedback stays in the Session log. [OTel session upload](../../session/session-telemetry-otel/README.md) defaults to `FEEDBACK_ONLY` for all users, including `deepseek-official`: new text feedback, message ratings, edits, and withdrawals release the complete canonical prefix through that event, including context. Later records wait for the next explicit feedback; sending an authorized batch needs no further interaction or model call. `DISABLED` prevents OTel capture. The default-on [DeepSeek session-log contributor](../../session/session-log-deepseek/README.md) remains a separate request path. -Both `subagent` and `subagent_fork` create managed children and return a child id immediately. Use `send_message` to steer active children or continue their conversations after completion. [Delegation-tool behavior](../../subagent/tool-subagent/README.md) describes result delivery and backend capabilities. +Both `subagent` and `subagent_fork` create managed children and return the child's name immediately. Use `send_message` to steer active children or continue their conversations after completion. [Delegation-tool behavior](../../subagent/tool-subagent/README.md) describes result delivery and backend capabilities. The bundle also mounts [Agent Teams](../../team/agent-team/README.md) and its task tools. Team mode defaults off per root Session; the root conversation’s **Use Agent Teams** menu switch enables collaboration for that tree without changing the profile. diff --git a/packages/bundle/base/README.zh.md b/packages/bundle/base/README.zh.md index 53c04f4b52..62d6ad03b9 100644 --- a/packages/bundle/base/README.zh.md +++ b/packages/bundle/base/README.zh.md @@ -49,7 +49,7 @@ kind: "package-bundle" 开箱即用,基于本核心构建的每个 profile 都提供:DeepSeek 模型连接(提供方与模型可配置,你还可以在设置中启用额外提供方)、完整工具集——文件编辑、shell 命令、web 搜索、HTTP(S) 抓取、subagent、任务与目标跟踪——可跨重启存活的持久会话,以及默认权限策略:把文件写入限制在工作区内,危险操作前征询许可。Web 抓取无需逐次审批,并且可以访问 loopback 与内网主机。反馈保存在会话日志中。[OTel 会话上传](../../session/session-telemetry-otel/README.zh.md)对所有用户默认使用 `FEEDBACK_ONLY`,包括 `deepseek-official`:新的文本反馈、消息评分、编辑与撤回会释放截至该事件的完整规范会话日志前缀,包含上下文。后续记录等待下一次显式反馈;发送已授权批次无需进一步交互或模型调用。`DISABLED` 阻止 OTel 捕获。默认开启的 [DeepSeek 会话日志贡献器](../../session/session-log-deepseek/README.zh.md)仍是独立的请求路径。 -`subagent` 与 `subagent_fork` 都创建受管理的子任务,并立即返回 child id。使用 `send_message` 可引导活跃子任务,或在完成后继续其对话。[委派工具行为](../../subagent/tool-subagent/README.zh.md)说明了结果交付和后端能力。 +`subagent` 与 `subagent_fork` 都创建受管理的子任务,并立即返回子任务名称。使用 `send_message` 可引导活跃子任务,或在完成后继续其对话。[委派工具行为](../../subagent/tool-subagent/README.zh.md)说明了结果交付和后端能力。 此 bundle 还挂载 [Agent Teams](../../team/agent-team/README.zh.md) 及其任务工具。每个根 Session 的 Team 模式默认关闭;根会话菜单中的**使用智能体团队**开关为当前树开启协作,无需修改 profile。 diff --git a/packages/client/ui-agent-team/README.i18n.yaml b/packages/client/ui-agent-team/README.i18n.yaml index dfab72bf26..511bc52f3d 100644 --- a/packages/client/ui-agent-team/README.i18n.yaml +++ b/packages/client/ui-agent-team/README.i18n.yaml @@ -18,8 +18,8 @@ en: d502ab38773eef34 zh: e1f9c4edbc74c38f /deepseek-ai-dsh-client-ui-agent-team/use-this-package/inspect-the-task-board: - en: 7e88fb00a3abd230 - zh: 08efcf80afecb0c0 + en: d3d67c28bd2379ff + zh: f0057ab13a554a25 /deepseek-ai-dsh-client-ui-agent-team/understand-the-implementation: en: 9b0b6a8505a9ebbf zh: beb5d89f80463b3e diff --git a/packages/client/ui-agent-team/README.md b/packages/client/ui-agent-team/README.md index d156e7b7cb..89d2e7f1b6 100644 --- a/packages/client/ui-agent-team/README.md +++ b/packages/client/ui-agent-team/README.md @@ -37,7 +37,7 @@ The [Web bundle](../../bundle/web-app/README.md) loads this package by default. Ready pending tasks use idle, blocked pending tasks use warning, in-progress tasks use ongoing, and completed tasks use done. -The noncollapsible Shared tasks header uses a Checklist icon and completed, in-progress, and pending counts, with zero counts omitted and wording and separators matching the input task list. Compact read-only rows show the subject, status, and a chevron, with 13px primary titles and hover geometry aligned to subagent rows; expanded details use 12px text. Task rows use 7px vertical padding, 6px horizontal padding, and 7px gaps to align their clickable content with subagent rows. All task details start closed; opening one closes the previous task. Details show only the full description when present, followed by a single-line owner label and Session id in the same color with a 4px gap. Long owner ids are ellipsized and reveal their full text in a tooltip only when clipped. Task-text and owner tooltips appear after 500ms of hover. Every owner is identified by its Session id. The shared menu is 360px wide with 6px outer padding, constrained to fit the viewport. Shared tasks have a 4px top margin rather than a divider. Both section headers keep a 7px icon/title gap and an additional 4px before their counts. Only the menu scrolls; task lists and details have no nested scrolling. Agents mutate tasks through tools; the panel provides no mutation controls. A projection failure appears separately above any retained tasks and remains visible when no tasks exist. +The noncollapsible Shared tasks header uses a Checklist icon and completed, in-progress, and pending counts, with zero counts omitted and wording and separators matching the input task list. Compact read-only rows show the subject, status, and a chevron, with 13px primary titles and hover geometry aligned to subagent rows; expanded details use 12px text. Task rows use 7px vertical padding, 6px horizontal padding, and 7px gaps to align their clickable content with subagent rows. All task details start closed; opening one closes the previous task. Details show only the full description when present, followed by a single-line owner label and owner name in the same color with a 4px gap. Long owner names are ellipsized and reveal their full text in a tooltip only when clipped. Task-text and owner tooltips appear after 500ms of hover. Owners use the names agent tools report: `lead` for the root conversation, and for a descendant the names from loaded subagent catalogs joined by `/` below the root, such as `reviewer/checker`. An owner keeps its Session id when its catalog path is not loaded or includes an entry recorded without a name. The shared menu is 360px wide with 6px outer padding, constrained to fit the viewport. Shared tasks have a 4px top margin rather than a divider. Both section headers keep a 7px icon/title gap and an additional 4px before their counts. Only the menu scrolls; task lists and details have no nested scrolling. Agents mutate tasks through tools; the panel provides no mutation controls. A projection failure appears separately above any retained tasks and remains visible when no tasks exist. ----- diff --git a/packages/client/ui-agent-team/README.zh.md b/packages/client/ui-agent-team/README.zh.md index 6a370c6eeb..1c416a7590 100644 --- a/packages/client/ui-agent-team/README.zh.md +++ b/packages/client/ui-agent-team/README.zh.md @@ -37,7 +37,7 @@ Chat 隐藏历史 `/team on` 和 `/team off` 记录中尚未完成或执行成 可开始的 pending 任务使用 idle 灰点,被依赖阻塞的 pending 任务使用 warning 橙点,in-progress 任务使用 ongoing loading,completed 任务使用 done 绿点。 -不可折叠的“共享任务”标题使用 Checklist 图标,并显示已完成、进行中和待处理数量;省略零值,文案和分隔符与输入区任务列表一致。紧凑只读行显示主题、状态和展开箭头,13px 主标题与悬停区域对齐子智能体行;展开详情使用 12px 文字。任务行使用 7px 垂直内边距、6px 水平内边距和 7px 间距,使可点击内容与 subagent 行对齐。所有任务详情初始关闭,打开一项会关闭上一项。详情仅显示完整描述(如有),下方以单行显示负责人标签和 Session id,两者使用相同颜色,间距为 4px。过长的负责人 id以省略号截断,仅在被截断时通过工具提示显示完整内容。任务文字和负责人的工具提示在悬停 500ms 后显示。每个负责人均通过其 Session id 标识。共享菜单宽度为 360px,外层内边距为 6px,并受视口可用宽度约束。共享任务上方保留 4px 外边距,不显示分隔线。两个区域标题均保留 7px 图标与文字间距,并在统计数量前额外留出 4px。仅菜单本身滚动,任务列表与详情不单独滚动。Agent 通过工具修改任务;面板不提供修改控件。投影失败以独立警告显示在保留的任务上方,即使没有任务也保持可见。 +不可折叠的“共享任务”标题使用 Checklist 图标,并显示已完成、进行中和待处理数量;省略零值,文案和分隔符与输入区任务列表一致。紧凑只读行显示主题、状态和展开箭头,13px 主标题与悬停区域对齐子智能体行;展开详情使用 12px 文字。任务行使用 7px 垂直内边距、6px 水平内边距和 7px 间距,使可点击内容与 subagent 行对齐。所有任务详情初始关闭,打开一项会关闭上一项。详情仅显示完整描述(如有),下方以单行显示负责人标签和负责人名称,两者使用相同颜色,间距为 4px。过长的负责人名称以省略号截断,仅在被截断时通过工具提示显示完整内容。任务文字和负责人的工具提示在悬停 500ms 后显示。负责人使用智能体工具报告的名称:根会话为 `lead`,后代为已加载 subagent 目录中根以下各级名称以 `/` 连接的路径,例如 `reviewer/checker`。目录路径未加载或包含没有名称的历史目录项时,负责人保留其 Session id。共享菜单宽度为 360px,外层内边距为 6px,并受视口可用宽度约束。共享任务上方保留 4px 外边距,不显示分隔线。两个区域标题均保留 7px 图标与文字间距,并在统计数量前额外留出 4px。仅菜单本身滚动,任务列表与详情不单独滚动。Agent 通过工具修改任务;面板不提供修改控件。投影失败以独立警告显示在保留的任务上方,即使没有任务也保持可见。 ----- diff --git a/packages/client/ui-agent-team/src/client/TeamBoard.tsx b/packages/client/ui-agent-team/src/client/TeamBoard.tsx index dbc554d33c..8136dbd211 100644 --- a/packages/client/ui-agent-team/src/client/TeamBoard.tsx +++ b/packages/client/ui-agent-team/src/client/TeamBoard.tsx @@ -10,6 +10,7 @@ import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots import type {} from '@deepseek-ai/dsh-client-ui-subagent/client' import type {} from '@deepseek-ai/dsh-client-ui-conversation/client' import { NS, type TeamKey } from './locales.ts' +import { ownerName } from './owner-name.ts' import css from './TeamBoard.module.css' /** Full props of the task board below the shared subagent catalog. */ @@ -37,13 +38,16 @@ function progressLabel(tasks: readonly VisibleTask[], t: TeamBoardProps['t']): s ].join('\u2002·\u2002') } -function TaskRow({ task, rootId, expanded, onToggle, t }: Pick & { +function TaskRow({ task, rootId, expanded, onToggle, useSessions, t }: Pick & { task: VisibleTask rootId: SessionId expanded: boolean onToggle: () => void }) { - const owner = task.ownerId ?? t('unowned') + const named = useSessions(state => !expanded || task.ownerId === undefined + ? undefined + : ownerName(state.projectionsBySession, rootId, task.ownerId)) + const owner = named ?? t('unowned') const blocked = task.status === 'pending' && !task.ready const detailsId = `team-task-${rootId}-${task.id}` return ( @@ -103,6 +107,7 @@ export function TeamBoard({ sessionId, useProjection, useSessions, t }: TeamBoar key={task.id} task={task} rootId={sessionId} expanded={expandedTaskId === task.id} onToggle={() => { setExpandedTaskId(current => current === task.id ? null : task.id) }} + useSessions={useSessions} t={t} /> ))} diff --git a/packages/client/ui-agent-team/src/client/owner-name.ts b/packages/client/ui-agent-team/src/client/owner-name.ts new file mode 100644 index 0000000000..57053c5214 --- /dev/null +++ b/packages/client/ui-agent-team/src/client/owner-name.ts @@ -0,0 +1,34 @@ +/** Model-facing agent name of a Team task owner, derived from loaded subagent catalogs. */ +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type { SessionListState } from '@deepseek-ai/dsh-api-session-controller/client' + +/** The root agent's fixed name, equal to `ROOT_AGENT_NAME` of the Host-only `@deepseek-ai/dsh-subagent` entry. */ +const ROOT_AGENT_NAME = 'lead' + +/** + * Name a task owner as agent tools name it. + * @param projections - Loaded projections by Session; each `subagentCatalog` lists that Session's direct children. + * @param rootId - Root Session of the subagent tree that owns the task board. + * @param ownerId - Session ID recorded as the task owner. + * @returns `lead` for the root; the slash-separated catalog names below the root, such as + * `reviewer/checker`, when every link to the owner is loaded and named; otherwise `ownerId`. + */ +export function ownerName( + projections: SessionListState['projectionsBySession'], rootId: SessionId, ownerId: SessionId, +): string { + if (ownerId === rootId) return ROOT_AGENT_NAME + // A Session listed by more than one loaded catalog is searched once, which also ends a cyclic listing. + const visited = new Set([rootId]) + const search = (parentId: SessionId): string | undefined => { + for (const entry of projections[parentId]?.values.subagentCatalog ?? []) { + const name = entry.mode === 'continuable' || entry.mode === 'external' ? entry.name : undefined + if (name === undefined || visited.has(entry.id)) continue + if (entry.id === ownerId) return name + visited.add(entry.id) + const below = search(entry.id) + if (below !== undefined) return `${name}/${below}` + } + return undefined + } + return search(rootId) ?? ownerId +} diff --git a/packages/client/ui-agent-team/tests/owner-name.client.spec.ts b/packages/client/ui-agent-team/tests/owner-name.client.spec.ts new file mode 100644 index 0000000000..efcc010300 --- /dev/null +++ b/packages/client/ui-agent-team/tests/owner-name.client.spec.ts @@ -0,0 +1,61 @@ +/** Team task owners use the agent names that agent tools report. */ +import { describe, expect, it } from 'vitest' +import type { SessionId } from '@deepseek-ai/dsh-session/types' +import type { SessionListState, SessionProjectionMap } from '@deepseek-ai/dsh-api-session-controller/client' +import { ownerName } from '../src/client/owner-name.ts' + +type Projections = SessionListState['projectionsBySession'] +type SubagentCatalogEntry = SessionProjectionMap['subagentCatalog'][number] + +const ROOT = 'root-id' as SessionId +const REVIEWER = 'reviewer-id' as SessionId +const CHECKER = 'checker-id' as SessionId +const HISTORICAL = 'historical-id' as SessionId + +function catalogs(entries: Readonly>): Projections { + return Object.fromEntries(Object.entries(entries).map(([id, subagentCatalog]) => [ + id, { state: 'ready', error: null, values: { subagentCatalog: [...subagentCatalog] } }, + ])) +} + +const tree = catalogs({ + [ROOT]: [ + { id: HISTORICAL, createdAt: 1, mode: 'continuable', label: 'Historical' }, + { id: 'one-shot-id' as SessionId, createdAt: 2, mode: 'one-shot' }, + { id: 'external-id' as SessionId, createdAt: 3, mode: 'external', name: 'outside' }, + { id: REVIEWER, createdAt: 4, mode: 'continuable', label: 'Review', name: 'reviewer' }, + ], + [REVIEWER]: [{ id: CHECKER, createdAt: 5, mode: 'continuable', label: 'Check', name: 'checker' }], + [HISTORICAL]: [{ id: 'below-historical-id' as SessionId, createdAt: 6, mode: 'continuable', label: 'Below', name: 'below' }], +}) + +describe('ownerName', () => { + it('names the root lead and descendants by their path below the root', () => { + expect(ownerName(tree, ROOT, ROOT)).toBe('lead') + expect(ownerName({}, ROOT, ROOT)).toBe('lead') + expect(ownerName(tree, ROOT, REVIEWER)).toBe('reviewer') + expect(ownerName(tree, ROOT, CHECKER)).toBe('reviewer/checker') + expect(ownerName(tree, ROOT, 'external-id' as SessionId)).toBe('outside') + }) + + it('keeps the Session ID when a link is unnamed, unloaded, or absent', () => { + expect(ownerName(tree, ROOT, HISTORICAL)).toBe(HISTORICAL) + expect(ownerName(tree, ROOT, 'below-historical-id' as SessionId)).toBe('below-historical-id') + expect(ownerName(tree, ROOT, 'one-shot-id' as SessionId)).toBe('one-shot-id') + expect(ownerName({ [ROOT]: tree[ROOT]! }, ROOT, CHECKER)).toBe(CHECKER) + expect(ownerName({ ...tree, [ROOT]: { state: 'loading', error: null, values: {} } }, ROOT, REVIEWER)).toBe(REVIEWER) + expect(ownerName(tree, ROOT, 'unknown-id' as SessionId)).toBe('unknown-id') + }) + + it('searches each Session once when catalogs list a member again', () => { + const repeated = catalogs({ + [ROOT]: [ + { id: REVIEWER, createdAt: 1, mode: 'continuable', label: 'Review', name: 'reviewer' }, + { id: REVIEWER, createdAt: 2, mode: 'continuable', label: 'Review', name: 'again' }, + ], + [REVIEWER]: [{ id: ROOT, createdAt: 3, mode: 'continuable', label: 'Root', name: 'cycle' }], + }) + expect(ownerName(repeated, ROOT, REVIEWER)).toBe('reviewer') + expect(ownerName(repeated, ROOT, CHECKER)).toBe(CHECKER) + }) +}) diff --git a/packages/client/ui-agent-team/tests/team-board.client.spec.tsx b/packages/client/ui-agent-team/tests/team-board.client.spec.tsx index b09aedc4e5..cc75efe991 100644 --- a/packages/client/ui-agent-team/tests/team-board.client.spec.tsx +++ b/packages/client/ui-agent-team/tests/team-board.client.spec.tsx @@ -141,8 +141,8 @@ describe('TeamBoard', () => { const details = document.getElementById(first.getAttribute('aria-controls')!)! const owner = details.querySelector('[data-team-task-owner]')! expect(owner.tagName).toBe('P') - expect(owner.textContent).toBe(`${zh.owner}: ${SESSION}`) - expect(ownerValue()).toBe(SESSION) + expect(owner.textContent).toBe(`${zh.owner}: lead`) + expect(ownerValue()).toBe('lead') const paragraphs = details.querySelectorAll('p') expect(paragraphs).toHaveLength(2) expect(paragraphs[0]?.textContent).toBe(description) @@ -179,7 +179,7 @@ describe('TeamBoard', () => { expect(row('Completed second').querySelector('[data-state="done"]')).toBeTruthy() fireEvent.click(row('Completed second')) expect(row('Completed second').getAttribute('aria-expanded')).toBe('true') - expect(ownerValue()).toBe(SESSION) + expect(ownerValue()).toBe('lead') expect(screen.getByText(task.description)).toBeTruthy() }) @@ -212,30 +212,44 @@ describe('TeamBoard', () => { fireEvent.click(row()) expect(within(row()).getByRole('img', { name: zh['status.pending'] })).toBeTruthy() expect(row().querySelector('[data-state="idle"]')).toBeTruthy() - expect(ownerValue()).toBe(SESSION) + expect(ownerValue()).toBe('lead') expect(document.querySelector('[data-team-task-details]')?.children).toHaveLength(1) expect(document.querySelector('[data-team-task-details] p:not([data-team-task-owner])')).toBeNull() }) - it('uses the owner id independently of catalog descriptions and Session titles', () => { + it('names the owner by its catalog path from the root, independently of descriptions and Session titles', () => { const b = bench({ byId: { [SESSION]: summary(SESSION), [NESTED]: { ...summary(NESTED, 'Session title'), parentId: WORKER } }, projections: { [SESSION]: { state: 'ready', error: null, values: { agentTeam: { ...team, tasks: [{ ...task, ownerId: NESTED }] }, + subagentCatalog: [{ id: WORKER, createdAt: 0, mode: 'continuable', label: 'Worker responsibility', name: 'worker' }], } }, [WORKER]: { state: 'ready', error: null, values: { - subagentCatalog: [{ id: NESTED, createdAt: 0, mode: 'continuable', label: 'Catalog responsibility' }], + subagentCatalog: [{ id: NESTED, createdAt: 0, mode: 'continuable', label: 'Catalog responsibility', name: 'checker' }], } }, }, }) render() fireEvent.click(row()) - expect(ownerValue()).toBe(NESTED) + expect(ownerValue()).toBe('worker/checker') setProjectionSnapshot(b.sessions, WORKER, { state: 'ready', error: null, values: {} }) act(() => { b.sessions.update((draft) => { draft.byId[NESTED] = { ...summary(NESTED), displayTitle: 'Display title' } }) }) expect(ownerValue()).toBe(NESTED) - setProjection(b.sessions, SESSION, { ...team, tasks: [{ ...task, ownerId: WORKER }] }) + setProjectionSnapshot(b.sessions, SESSION, { state: 'ready', error: null, values: { + agentTeam: { ...team, tasks: [{ ...task, ownerId: WORKER }] }, + subagentCatalog: [{ id: WORKER, createdAt: 0, mode: 'continuable', label: 'Worker responsibility', name: 'worker' }], + } }) + expect(ownerValue()).toBe('worker') + }) + + it('uses the owner Session ID when its catalog entry has no name', () => { + const b = bench({ projections: { [SESSION]: { state: 'ready', error: null, values: { + agentTeam: { ...team, tasks: [{ ...task, ownerId: WORKER }] }, + subagentCatalog: [{ id: WORKER, createdAt: 0, mode: 'continuable', label: 'Historical worker' }], + } } } }) + render() + fireEvent.click(row()) expect(ownerValue()).toBe(WORKER) }) @@ -344,8 +358,8 @@ describe('TeamBoard', () => { render() expect(screen.getByRole('heading').textContent).toBe(`${en.tasks}${makeTranslate(en)('progress.active', { count: 1 })}`) fireEvent.click(row()) - expect(ownerValue()).toBe(SESSION) - expect(document.querySelector('[data-team-task-owner]')?.textContent).toBe(`${en.owner}: ${SESSION}`) + expect(ownerValue()).toBe('lead') + expect(document.querySelector('[data-team-task-owner]')?.textContent).toBe(`${en.owner}: lead`) expect(screen.queryByText(TASK_1)).toBeNull() expect(screen.queryByText('Show more')).toBeNull() expect(screen.queryByText('Show less')).toBeNull() diff --git a/packages/client/ui-subagent/README.i18n.yaml b/packages/client/ui-subagent/README.i18n.yaml index 310cd7839b..1e7562c8a7 100644 --- a/packages/client/ui-subagent/README.i18n.yaml +++ b/packages/client/ui-subagent/README.i18n.yaml @@ -15,11 +15,11 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-client-ui-subagent/use-this-package: - en: d00a6c7ffced5dc7 - zh: 616220bbdfdc70a6 + en: cb12cbb59ca353f5 + zh: d5f370d69f431513 /deepseek-ai-dsh-client-ui-subagent/use-this-package/browsing-the-tree: - en: 4ecdca030e42109d - zh: 406b08590ba8df83 + en: 5ea28139189887e9 + zh: f5a51f041c39b0fa /deepseek-ai-dsh-client-ui-subagent/use-this-package/continuing-a-conversation: en: e4ec261d3b122eb5 zh: 320bb2efc93dccdc diff --git a/packages/client/ui-subagent/README.md b/packages/client/ui-subagent/README.md index 18bee2a024..d7e9c48d78 100644 --- a/packages/client/ui-subagent/README.md +++ b/packages/client/ui-subagent/README.md @@ -25,7 +25,7 @@ Browse local subagent conversations and external tasks from the parent header. L ## Use this package -The session header uses the session title for ordinary breadcrumbs. Subagent breadcrumbs prefer the original creation description (`entry.label`), then the Session id. Ordinary and subagent breadcrumb titles both use 14px text with a 20px line height. When the session's direct catalog has entries or a read has failed, the descendant-count trigger renders after the preset label in the header actions band, with no breadcrumb separator. An absent or empty catalog hides the count trigger unless the root Session has an enabled, available Team board with shared tasks or a task-record error. Enabling Team mode alone does not show an empty entry. The trigger opens that direct catalog, uses the total count as its accessible name and the running count as its accessible description, and loads nested catalogs only when their rows expand. Select a local child at any depth to open that child's conversation with its exact `{parentSessionId, childSessionId, mode}` address, or use a noncurrent local row's trailing arrow to open the same address in the right Sidebar, preferring a separate pane when room permits. +The session header uses the session title for ordinary breadcrumbs. Subagent breadcrumbs prefer the original creation description (`entry.label`), then the agent name (`entry.name`), then the Session id. Ordinary and subagent breadcrumb titles both use 14px text with a 20px line height. When the session's direct catalog has entries or a read has failed, the descendant-count trigger renders after the preset label in the header actions band, with no breadcrumb separator. An absent or empty catalog hides the count trigger unless the root Session has an enabled, available Team board with shared tasks or a task-record error. Enabling Team mode alone does not show an empty entry. The trigger opens that direct catalog, uses the total count as its accessible name and the running count as its accessible description, and loads nested catalogs only when their rows expand. Select a local child at any depth to open that child's conversation with its exact `{parentSessionId, childSessionId, mode}` address, or use a noncurrent local row's trailing arrow to open the same address in the right Sidebar, preferring a separate pane when room permits. This package registers the `dsh-resource://subagentchat/session/?parent=&mode=` resource and builtin Sidebar tab type. The resource retains the child `SessionReference` directly from its address without refreshing the parent catalog, and releases the reference when the tab record closes. The tab renders the shared `conversation.content` Factory through `sidebar.chat.conversation`, fixes the local View to Chat, and omits the main Conversation header and width controls. @@ -39,7 +39,7 @@ The wide root count trigger shows a 14px Users icon in its 14px slot when no chi Count, owner, and sidebar-action tooltips appear after 500ms of hover. Hovering a text-visible trigger opens its catalog after 150ms; leaving both trigger and catalog closes it after 120ms. Clicking the descendant-count trigger opens and pins its catalog, or closes an already pinned catalog. The icon-only count trigger does not open on hover, including when a pending hover crosses into the narrow layout. Clicking toggles its pinned menu; ArrowDown opens and pins the menu and focuses its first row. Outside click or Escape dismisses a pinned menu. Clicking moves focus into the panel, where ArrowDown/Home enters the first tree row and ArrowUp/End enters the last; Escape returns focus when it was inside. Breadcrumb-title clicks navigate to the corresponding conversation. -Rows show the creation description, or an available Session title, above the Session id. Without either description, the row shows only its Session id. Local rows use compact 4px vertical padding; their clickable content uses 6px horizontal padding and 7px gaps, aligned with shared-task rows while retaining the tree's left offset. A decorative trailing check marks the current conversation and is not an action; other local rows retain the sidebar arrow. Primary titles use 13px text and secondary ids use 12px. All rows wrap descriptions and ids in full, including on touch screens. Running uses the shared ongoing loader, an inactive child whose latest closed turn completed normally uses the shared success dot, and other inactive children use the shared idle dot. Every row reserves the same 14px status column, centering smaller dots so titles align with the loader state. The text-visible header trigger vertically centers its activity glyph and count with a 4px gap. The trailing column stacks total durable provider usage above active-turn duration. Keyboard navigation works with ArrowRight/ArrowLeft to expand and collapse branches and ArrowUp/ArrowDown, Home, End, and Escape to navigate or close the tree. An unlabeled one-shot row falls back to its session id. A row is a known leaf only after its own catalog loads empty. +Rows show the creation description, or an available Session title, above the agent name, which is unique among its siblings. Entries recorded without a name show the Session id in its place. Without either description, the row shows only the name or Session id. Navigation, row identity, and Sidebar addresses always use the Session id. Local rows use compact 4px vertical padding; their clickable content uses 6px horizontal padding and 7px gaps, aligned with shared-task rows while retaining the tree's left offset. A decorative trailing check marks the current conversation and is not an action; other local rows retain the sidebar arrow. Primary titles use 13px text and secondary names use 12px. All rows wrap descriptions and names in full, including on touch screens. Running uses the shared ongoing loader, an inactive child whose latest closed turn completed normally uses the shared success dot, and other inactive children use the shared idle dot. Every row reserves the same 14px status column, centering smaller dots so titles align with the loader state. The text-visible header trigger vertically centers its activity glyph and count with a 4px gap. The trailing column stacks total durable provider usage above active-turn duration. Keyboard navigation works with ArrowRight/ArrowLeft to expand and collapse branches and ArrowUp/ArrowDown, Home, End, and Escape to navigate or close the tree. An unlabeled one-shot row falls back to its session id. A row is a known leaf only after its own catalog loads empty. ### Continuing a conversation diff --git a/packages/client/ui-subagent/README.zh.md b/packages/client/ui-subagent/README.zh.md index 56a37645db..39be50a0de 100644 --- a/packages/client/ui-subagent/README.zh.md +++ b/packages/client/ui-subagent/README.zh.md @@ -25,7 +25,7 @@ kind: "package-reference" ## 使用本包 -会话页头的普通面包屑使用会话标题。subagent 面包屑优先使用创建时的原始描述(`entry.label`),缺少时回退到 Session id。普通与 subagent 面包屑标题均使用 14px 文字和 20px 行高。当会话的直接目录有子项或读取失败时,后代数量触发器渲染在页头操作区中预设标签之后,不带任何面包屑分隔符。目录缺席或为空时隐藏数量触发器,除非根 Session 已开启且可用的 Team 任务板有共享任务或任务记录错误。仅开启 Team 模式不会显示空入口。触发器打开该直接目录,以总数作为无障碍名称、运行数作为无障碍描述,并且只在行展开时加载嵌套目录。选择任意深度,即可用该子会话的确切 `{parentSessionId, childSessionId, mode}` 地址打开其对话;也可以使用非当前本地行的行尾箭头在右侧 Sidebar 打开同一地址,并在空间允许时优先使用独立分栏。 +会话页头的普通面包屑使用会话标题。subagent 面包屑优先使用创建时的原始描述(`entry.label`),其次使用智能体名称(`entry.name`),最后回退到 Session id。普通与 subagent 面包屑标题均使用 14px 文字和 20px 行高。当会话的直接目录有子项或读取失败时,后代数量触发器渲染在页头操作区中预设标签之后,不带任何面包屑分隔符。目录缺席或为空时隐藏数量触发器,除非根 Session 已开启且可用的 Team 任务板有共享任务或任务记录错误。仅开启 Team 模式不会显示空入口。触发器打开该直接目录,以总数作为无障碍名称、运行数作为无障碍描述,并且只在行展开时加载嵌套目录。选择任意深度,即可用该子会话的确切 `{parentSessionId, childSessionId, mode}` 地址打开其对话;也可以使用非当前本地行的行尾箭头在右侧 Sidebar 打开同一地址,并在空间允许时优先使用独立分栏。 本包注册 `dsh-resource://subagentchat/session/?parent=&mode=` 资源与 builtin Sidebar tab 类型。资源直接根据地址保留 child 的 `SessionReference`,不刷新 parent 目录,并在 tab 记录关闭时释放 reference。tab 通过 `sidebar.chat.conversation` 渲染共享 `conversation.content` Factory,把局部 View 固定为 Chat,并省略主 Conversation 的 Header 与宽度控制。 @@ -39,7 +39,7 @@ kind: "package-reference" 数量、负责人及侧边栏操作的工具提示在悬停 500ms 后显示。悬停文字可见的触发器 150ms 后打开目录;指针离开触发器和目录后,经过 120ms 关闭。点击后代数量触发器会打开并固定目录,或关闭已固定的目录。纯图标数量触发器不会因悬停而打开,包括等待悬停打开期间切换到窄布局的情况。点击可切换固定菜单的开关状态;ArrowDown 会打开并固定菜单,并将焦点移至首行。点击外部或按 Escape 可关闭已固定的菜单。点击会将焦点移入面板,此时 ArrowDown/Home 进入首个树行,ArrowUp/End 进入最后一个树行;Escape 在焦点原本位于面板内时将其送回。点击面包屑标题会导航至对应会话。 -目录行在 Session id 上方显示创建描述,缺少创建描述时使用可用的 Session title。两者均缺少时,该行仅显示 Session id。本地行使用紧凑的 4px 垂直内边距;可点击内容使用 6px 水平内边距和 7px 间距,与共享任务行对齐,同时保留树的左侧偏移。当前对话在行尾显示装饰性对勾,该对勾不是操作控件;其他本地行保留侧边栏箭头。主标题使用 13px 文字,次级 id 使用 12px。所有行均将描述与 id 换行完整展示,包括在触屏上。running 使用共享 ongoing loading,最近一个已结束轮次正常完成的 inactive child 使用共享 success 绿点,其他 inactive child 使用共享 idle 灰点。每行都为状态图标预留相同的 14px 列宽,并将较小的圆点居中,使 title 与 loading 状态对齐。文字可见的页头触发器会垂直居中活动图标与数量,并保留 4px 水平间距。尾随列在上行显示提供方的持久化 token 用量总计,在下行显示活跃轮次耗时。键盘导航:ArrowRight/ArrowLeft 展开和折叠分支;ArrowUp/ArrowDown、Home、End 与 Escape 用于导航或关闭树。没有 label 的 one-shot 行回退到其会话 id。只有一行自身的目录加载为空后,它才是已知叶子。 +目录行在智能体名称上方显示创建描述,缺少创建描述时使用可用的 Session title;名称在同级子智能体中唯一。没有名称的历史目录项在相同位置显示 Session id。两者均缺少时,该行仅显示名称或 Session id。导航、行身份与 Sidebar 地址始终使用 Session id。本地行使用紧凑的 4px 垂直内边距;可点击内容使用 6px 水平内边距和 7px 间距,与共享任务行对齐,同时保留树的左侧偏移。当前对话在行尾显示装饰性对勾,该对勾不是操作控件;其他本地行保留侧边栏箭头。主标题使用 13px 文字,次级名称使用 12px。所有行均将描述与名称换行完整展示,包括在触屏上。running 使用共享 ongoing loading,最近一个已结束轮次正常完成的 inactive child 使用共享 success 绿点,其他 inactive child 使用共享 idle 灰点。每行都为状态图标预留相同的 14px 列宽,并将较小的圆点居中,使 title 与 loading 状态对齐。文字可见的页头触发器会垂直居中活动图标与数量,并保留 4px 水平间距。尾随列在上行显示提供方的持久化 token 用量总计,在下行显示活跃轮次耗时。键盘导航:ArrowRight/ArrowLeft 展开和折叠分支;ArrowUp/ArrowDown、Home、End 与 Escape 用于导航或关闭树。没有 label 的 one-shot 行回退到其会话 id。只有一行自身的目录加载为空后,它才是已知叶子。 ### 续接对话 diff --git a/packages/client/ui-subagent/src/client/SubagentHeaderLineage.tsx b/packages/client/ui-subagent/src/client/SubagentHeaderLineage.tsx index 183e2d66ab..5788cdf8e4 100644 --- a/packages/client/ui-subagent/src/client/SubagentHeaderLineage.tsx +++ b/packages/client/ui-subagent/src/client/SubagentHeaderLineage.tsx @@ -208,6 +208,11 @@ function isKnownLeaf(catalog: SubagentCatalogSnapshot | undefined): boolean { return catalog?.state === 'ready' && catalog.entries.length === 0 } +/** The agent name unique among the row's siblings, or the Session ID for entries recorded without one. */ +function entryName(entry: SubagentCatalogSnapshot['entries'][number]): string { + return entry.mode === 'continuable' || entry.mode === 'external' ? entry.name ?? entry.id : entry.id +} + /** Only a local child's last closed, normally completed turn receives the completed marker. */ function completedEntry(entry: SubagentCatalogSnapshot['entries'][number], summary: SessionSummary | undefined): boolean { return entry.mode !== 'external' && entry.activity === 'inactive' @@ -282,8 +287,9 @@ function CatalogRows({ || (childCatalog.state === 'loading' && childCatalog.entries.length === 0) const summary = summaries[entry.id] const description = entry.label || summary?.title - const primaryLabel = description || entry.id - const secondaryLabel = description ? entry.id : undefined + const name = entryName(entry) + const primaryLabel = description || name + const secondaryLabel = description ? name : undefined const mode = external ? t('mode.external') : entry.mode === 'unknown' ? t('mode.unknown') : entry.mode === 'one-shot' ? t('mode.oneShot') : t('mode.continuable') const completed = completedEntry(entry, summary) @@ -533,7 +539,7 @@ function CatalogDropdown(props: CatalogDropdownProps) { ? undefined : catalog?.entries.find(entry => entry.id === currentSessionId) const switcherDisplayTitle = currentEntry !== undefined - ? currentEntry.label || currentEntry.id + ? currentEntry.label || entryName(currentEntry) : displayTitle const directChildren = catalog?.entries ?? [] const directCount = directChildren.length diff --git a/packages/client/ui-subagent/tests/conversation-ui.client.spec.tsx b/packages/client/ui-subagent/tests/conversation-ui.client.spec.tsx index 030730f6e7..5bb1b3a6fa 100644 --- a/packages/client/ui-subagent/tests/conversation-ui.client.spec.tsx +++ b/packages/client/ui-subagent/tests/conversation-ui.client.spec.tsx @@ -316,6 +316,29 @@ describe('SubagentHeaderLineage', () => { expect(row.getAttribute('aria-label')).toMatch(new RegExp(`^${CHILD}`)) }) + it('shows agent names in place of Session ids while navigation keeps the Session id', () => { + const external = 'external-id' as SessionId + const input = props(catalog({ + entries: [ + { id: CHILD, mode: 'continuable', label: 'Review the diff', name: 'reviewer', activity: 'running' }, + { id: GRANDCHILD, mode: 'continuable', label: '', name: 'checker', activity: 'inactive' }, + { id: external, mode: 'external', name: 'outside', activity: 'inactive' }, + ], + }), {}, { [CHILD]: { ...summary(CHILD, 1), running: true } }) + render() + fireEvent.click(screen.getByRole('button', { name: /\d+ 个子智能体$/ })) + const named = screen.getByRole('treeitem', { name: /^Review the diff reviewer/ }) + expect(within(named).getByText('Review the diff').compareDocumentPosition(within(named).getByText('reviewer')) + & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() + expect(within(named).queryByText(CHILD)).toBeNull() + const undescribed = screen.getByRole('treeitem', { name: /^checker/ }) + expect(within(undescribed).getAllByText('checker')).toHaveLength(1) + expect(within(undescribed).queryByText(GRANDCHILD)).toBeNull() + expect(screen.getByRole('treeitem', { name: /^outside/ }).getAttribute('aria-disabled')).toBe('true') + fireEvent.click(named) + expect(input.openChild).toHaveBeenCalledExactlyOnceWith({ parentSessionId: PARENT, childSessionId: CHILD, mode: 'continuable' }) + }) + it('hides an empty Team entry but keeps shared tasks and errors reachable without children', () => { const input = props(catalog({ entries: [] })) const initial = input.useSessions(state => state) @@ -1606,6 +1629,19 @@ describe('SubagentHeaderLineage', () => { expect(screen.getByRole('button', { name: `切换子智能体:${CHILD}` })).toBeTruthy() }) + it('names the breadcrumb by the agent name when the selected row has an empty description', () => { + const input = { + ...props(catalog({ entries: [{ id: CHILD, mode: 'continuable', label: '', name: 'reviewer', activity: 'inactive' }] }), {}, { + [CHILD]: { ...summary(CHILD, 1), parentId: PARENT, origin: 'subagent' as const }, + }), + sessionId: CHILD, + lineageSessionId: CHILD, + displayTitle: 'summary title', + } + render() + expect(screen.getByRole('button', { name: '切换子智能体:reviewer' }).textContent).toBe('reviewer') + }) + it('keeps an ancestor switcher muted and omits its descendant count and section heading', () => { const input = { ...props(catalog({ team: sharedTeam }), { diff --git a/packages/client/ui-tool/README.i18n.yaml b/packages/client/ui-tool/README.i18n.yaml index b2d8ee1464..efd4ce1f85 100644 --- a/packages/client/ui-tool/README.i18n.yaml +++ b/packages/client/ui-tool/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-client-ui-tool/use-this-package: - en: 2419beff9bcd8139 - zh: 5aafe9b168e571a5 + en: 64c4c250bfe2898f + zh: fe99277a67dbd84a /deepseek-ai-dsh-client-ui-tool/use-this-package/registering-a-business-tool-view: en: e458aff61e634190 zh: 650e417f450b3edf @@ -30,8 +30,8 @@ en: 672e63b2f92e689b zh: ba950ecb480325a6 /deepseek-ai-dsh-client-ui-tool/understand-the-implementation/cards: - en: d2ac2f855da27aff - zh: 61d257d60971f259 + en: 614a73a6652d3f76 + zh: 0321bc0f72bdcc07 /deepseek-ai-dsh-client-ui-tool/understand-the-implementation/declared-optional-capabilities: en: c1926deb4e5f3734 zh: c5845f465b94a459 diff --git a/packages/client/ui-tool/README.md b/packages/client/ui-tool/README.md index a55e8ab7ed..15ea494c03 100644 --- a/packages/client/ui-tool/README.md +++ b/packages/client/ui-tool/README.md @@ -25,13 +25,13 @@ English | [中文](README.zh.md) ## Use this package -Tool calls appear in the conversation as cards: a root call tree with its nested subcalls, each atomic call rendered by its owning view. Every lifecycle state retains the tool's ordinary business glyph; failure and interruption remain explicit through the frozen call/result state, accessible status text, and failure summary. Users can open files or inspect calls through the Host callbacks. A collapsed `web_fetch` row links its http(s) URL, which opens in a new browser tab. Control receipts display the agent ID supplied by the recorded call. Text agent lists show the ID above a single-line description, with compact spacing and recorded status. Click the description to expand or collapse its full text; its accessible name follows the selected locale. +Tool calls appear in the conversation as cards: a root call tree with its nested subcalls, each atomic call rendered by its owning view. Every lifecycle state retains the tool's ordinary business glyph; failure and interruption remain explicit through the frozen call/result state, accessible status text, and failure summary. Users can open files or inspect calls through the Host callbacks. A collapsed `web_fetch` row links its http(s) URL, which opens in a new browser tab. Control receipts display the agent name supplied by the recorded call, or the agent ID in historical calls. Text agent lists show each agent name, or the historical ID, above a single-line description, with compact spacing and recorded status. Click the description to expand or collapse its full text; its accessible name follows the selected locale. Settled foreground commands with a nonzero exit code, a terminating signal, or an explicit missing exit code keep their description in the ordinary gray text and show a neutral capsule with a red dot immediately after it. The capsule displays only the recorded code, signal, or localized missing-code status and stays on the same line; the description truncates with an ellipsis to reserve its space. Running, successful, and background calls omit it. Generic execution errors and stopped summaries retain their error and warning colors, including on hover. Before dispatch, a named model call appears as one non-expandable row with its tool-owned icon and title. A generic row shows `Tool call · `. Read/write/edit show an openable path once `file_path` closes and decodes without errors; write/edit show `NKB` after that path once content arrives, retaining it during execution and after success, before any diff totals. N rounds the decoded input's UTF-16 length up in units of 1024, summing old and new text for edit; it is not file bytes. Error and stopped rows omit the size and diff totals. Command rows show the streamed description. `tool/call` enables the existing call presentation; completing an argument block alone does not start execution. -`subagent` and `subagent_fork` creation rows stream the first line of `description`, retaining it during execution, after creation, and when expanded. After preparation, calls without a description fall back to the first nonempty `prompt` line; missing arguments leave only the tool title. Both tools and `list_subagent_models` use the existing Users icon; `subagent_fork` uses the distinct title “Fork subagent”. Creation receipts show the started state, the full `description` as the agent task description, the recorded agent Session id, and complete task instructions. Current receipts read `kind: activation` and `subagentId` from result metadata; only historical results without metadata use text parsing. Malformed metadata keeps the generic result view. Synchronous agent responses remain in the result body; errors retain their failure summary. +`subagent` and `subagent_fork` creation rows stream the first line of `description`, retaining it during execution, after creation, and when expanded. After preparation, calls without a description fall back to the first nonempty `prompt` line; missing arguments leave only the tool title. Both tools and `list_subagent_models` use the existing Users icon; `subagent_fork` uses the distinct title “Fork subagent”. Creation receipts show the started state, the full `description` as the agent task description, the recorded agent name when present, the agent Session id, and complete task instructions. Current receipts read `kind: activation`, `subagentId`, and optional `name` from result metadata; only historical results without metadata use text parsing. Malformed metadata keeps the generic result view. Synchronous agent responses remain in the result body; errors retain their failure summary. File and URL toolview Slots obtain `useContextMenu` directly and supply bound `fileContextMenuProps` or `urlContextMenuProps` to `ToolRow`. File registrations inject a gesture-time path resolver for the originating Session’s working directory, falling back to its workspace root. `ToolCallTree` supplies an explicit file adapter to its plain `GenericToolCard` fallback, including nested calls. No Markdown provider is required. Mouse and keyboard gestures retain the captured path, line, URL, and Session without expanding the row or opening the file. @@ -93,7 +93,7 @@ Chat diff cards keep nine rows before folding, enough for a file header, one rem An Auto denial takes precedence over keyed specialized views. Its generic row preserves the call identity, omits raw arguments, and normalizes the stored reason only for display: trim surrounding whitespace and collapse line separators to spaces, with localized fallback for an empty result. Session and SDK error details keep the original reason. -Recorded tool details cover goal and schedule tools, Cordis inspection, workflow and Ralph reports, Session event/search/trace queries, agent and teammate controls, background jobs, persistent terminals, and LSP navigation. These expanded bodies read successful logged results, preserve generic input/output for failures or unsupported data, and keep Inspect available. Dates include the viewer's time zone, and statuses reflect the call result rather than current session state. Agent control receipts display the recorded target Session id. `wait_agent` shows a single waiting, ended, or timed-out header without a repeated summary; historical `noProgress` results keep their recorded explanation. Session traces preserve descendant indentation. LSP results open filesystem paths through the Host callback and display other URIs as text. The browser adapter consumes recorded producer text and JSON; Host service objects and presenter callbacks do not cross into the Client. +Recorded tool details cover goal and schedule tools, Cordis inspection, workflow and Ralph reports, Session event/search/trace queries, agent and teammate controls, background jobs, persistent terminals, and LSP navigation. These expanded bodies read successful logged results, preserve generic input/output for failures or unsupported data, and keep Inspect available. Dates include the viewer's time zone, and statuses reflect the call result rather than current session state. Agent control receipts display the recorded target agent name, or the Session id in historical calls. `wait_agent` shows a single waiting, ended, or timed-out header without a repeated summary; historical `noProgress` results keep their recorded explanation. Session traces preserve descendant indentation. LSP results open filesystem paths through the Host callback and display other URIs as text. The browser adapter consumes recorded producer text and JSON; Host service objects and presenter callbacks do not cross into the Client. Expanded status dots and labels use static semantic colors. Receipt and job-output headers keep neutral text and omit the status while expanded. An interruption receipt confirms only that interruption was requested. diff --git a/packages/client/ui-tool/README.zh.md b/packages/client/ui-tool/README.zh.md index 55e1fa9ad1..5f3ef74f78 100644 --- a/packages/client/ui-tool/README.zh.md +++ b/packages/client/ui-tool/README.zh.md @@ -25,13 +25,13 @@ kind: "package-reference" ## 使用本包 -工具调用在对话中显示为卡片:一个根调用树带其嵌套子调用,每个原子调用由所属视图渲染。所有生命周期状态都保留工具的普通业务图标;失败与中断仍通过冻结调用/结果状态、无障碍状态文本和失败摘要明确表达。用户可通过宿主回调打开文件或检查调用。折叠的 `web_fetch` 行把其 http(s) URL 显示为链接,在新浏览器标签页中打开。控制工具回执显示记录的调用所提供的智能体 ID。文本形式的智能体列表以紧凑间距展示 ID、下方的单行描述及记录中的状态。点击描述可展开或收起全文;其无障碍名称使用当前语言。 +工具调用在对话中显示为卡片:一个根调用树带其嵌套子调用,每个原子调用由所属视图渲染。所有生命周期状态都保留工具的普通业务图标;失败与中断仍通过冻结调用/结果状态、无障碍状态文本和失败摘要明确表达。用户可通过宿主回调打开文件或检查调用。折叠的 `web_fetch` 行把其 http(s) URL 显示为链接,在新浏览器标签页中打开。控制工具回执显示记录的调用所提供的智能体名称,历史调用则显示智能体 ID。文本形式的智能体列表以紧凑间距展示智能体名称(历史记录为 ID)、下方的单行描述及记录中的状态。点击描述可展开或收起全文;其无障碍名称使用当前语言。 已结束的前台命令若返回非零退出码、终止信号,或明确记录为无退出码,描述保持普通灰色,后面紧接带红点的中性胶囊。胶囊仅显示记录的退出码、信号或本地化的“未正常退出”状态,并保持在同一行;描述超出可用宽度时显示省略号,为胶囊预留空间。运行中、成功和后台调用不显示胶囊。通用执行错误与停止摘要仍保留错误色和警告色,悬停时也不改变。 派发前,模型已给出名称的调用显示为不可展开的一行,使用工具自己的图标与标题。通用行显示为 `工具调用 · <工具名>`。read/write/edit 在 `file_path` 闭合且解码无误后显示可打开的路径;write/edit 从内容到达起在该路径后显示 `NKB`,执行中和成功后仍保留,位于差异统计之前。N 按解码后输入的 UTF-16 长度除以 1024 向上取整,edit 合计新旧文本长度;它不是文件字节数。失败和停止的行省略大小与差异统计。命令行显示流入中的描述。`tool/call` 才启用既有调用展示;参数块结束本身不代表开始执行。 -`subagent` 与 `subagent_fork` 的创建行逐步显示 `description` 的首行,执行中、创建完成后和展开时均保留。准备阶段结束后,缺少描述的调用回退到 `prompt` 的首个非空行;参数不可用时仅显示工具标题。两个工具与 `list_subagent_models` 均使用现有的 Users 图标;`subagent_fork` 使用独立标题“派生子智能体”。创建回执显示已启动状态、作为智能体任务描述的完整 `description`、记录的智能体 Session id,以及完整任务指令。当前回执从结果 metadata 读取 `kind: activation` 与 `subagentId`;仅缺少 metadata 的历史结果使用文本解析。metadata 无效时保留通用结果视图。同步返回的 agent 回复仍放在结果正文中;失败时保留错误摘要。 +`subagent` 与 `subagent_fork` 的创建行逐步显示 `description` 的首行,执行中、创建完成后和展开时均保留。准备阶段结束后,缺少描述的调用回退到 `prompt` 的首个非空行;参数不可用时仅显示工具标题。两个工具与 `list_subagent_models` 均使用现有的 Users 图标;`subagent_fork` 使用独立标题“派生子智能体”。创建回执显示已启动状态、作为智能体任务描述的完整 `description`、记录的智能体名称(如有)、智能体 Session id,以及完整任务指令。当前回执从结果 metadata 读取 `kind: activation`、`subagentId` 与可选的 `name`;仅缺少 metadata 的历史结果使用文本解析。metadata 无效时保留通用结果视图。同步返回的 agent 回复仍放在结果正文中;失败时保留错误摘要。 文件和 URL 工具视图 Slot 直接获取 `useContextMenu`,向 `ToolRow` 提供已绑定的 `fileContextMenuProps` 或 `urlContextMenuProps`。文件注册注入在触发时读取原始 Session 工作目录的路径解析器,工作目录缺失时使用工作区根目录。`ToolCallTree` 向普通 `GenericToolCard` fallback 显式提供文件适配函数,包含嵌套调用。无需 Markdown Provider。鼠标和键盘手势保留捕获的路径、行号、URL 与 Session,不会展开工具行或打开文件。 @@ -93,7 +93,7 @@ Chat diff 卡片在折叠前保留九行,足以容纳文件标题、一对删 Auto 拒绝优先于按工具名选择的专门视图。其通用行保留调用身份、省略原始参数,并且只在显示时归一化存储的理由:去除首尾空白,把行分隔符折叠为空格,结果为空时使用本地化通用理由。Session 与 SDK 错误详情保留原始理由。 -记录结果的工具详情覆盖目标和定时任务工具、Cordis 检查、workflow 与 Ralph 报告、Session 事件/搜索/轨迹查询、Agent 与 teammate 控制、后台作业、持久终端以及 LSP 导航。展开内容读取成功的记录结果,为失败或不支持的数据保留通用输入/输出,并保留 Inspect。日期包含查看者的时区,状态反映调用结果而非当前会话状态。Agent 控制回执显示记录的目标 Session id。`wait_agent` 的行标题仅显示等待中、已结束或已超时,不重复追加摘要;历史 `noProgress` 结果保留记录中的说明。Session 轨迹保留后代的缩进。LSP 结果通过 Host 回调打开文件系统路径,其他 URI 则显示为文本。浏览器适配器消费已记录的 producer 文本与 JSON;Host service 对象和 presenter 回调不会进入 Client。 +记录结果的工具详情覆盖目标和定时任务工具、Cordis 检查、workflow 与 Ralph 报告、Session 事件/搜索/轨迹查询、Agent 与 teammate 控制、后台作业、持久终端以及 LSP 导航。展开内容读取成功的记录结果,为失败或不支持的数据保留通用输入/输出,并保留 Inspect。日期包含查看者的时区,状态反映调用结果而非当前会话状态。Agent 控制回执显示记录的目标智能体名称,历史调用则显示 Session id。`wait_agent` 的行标题仅显示等待中、已结束或已超时,不重复追加摘要;历史 `noProgress` 结果保留记录中的说明。Session 轨迹保留后代的缩进。LSP 结果通过 Host 回调打开文件系统路径,其他 URI 则显示为文本。浏览器适配器消费已记录的 producer 文本与 JSON;Host service 对象和 presenter 回调不会进入 Client。 展开后的状态圆点和文字使用静态语义色。操作回执和任务输出的标题保持中性色,展开时省略标题中的状态。中断回执仅确认已发出中断请求。 diff --git a/packages/client/ui-tool/src/client/tool/models/control-details-model.ts b/packages/client/ui-tool/src/client/tool/models/control-details-model.ts index 2de87b4318..bd40492a80 100644 --- a/packages/client/ui-tool/src/client/tool/models/control-details-model.ts +++ b/packages/client/ui-tool/src/client/tool/models/control-details-model.ts @@ -24,12 +24,13 @@ function agentList(text: string, json: unknown, t: DetailTranslate): ToolDetails if (text === '(no subagents)') return detailList([], t('detail.agents.count', { count: 0 }), t) const items: DetailItem[] = [] for (const line of text.split('\n')) { + // Historical rows name agents by Session ID and carry parent and depth. const match = /^(\S+) \[([^\]]+)\](?: parent=\S+ depth=\d+)?(?: — (.*))?$/u.exec(line) if (match === null) return null - const [, id, state, title] = match - if (id === undefined || state === undefined) return null + const [, agent, state, title] = match + if (agent === undefined || state === undefined) return null items.push({ - title: id, ...(title === undefined ? {} : { subtitle: title }), + title: agent, ...(title === undefined ? {} : { subtitle: title }), badge: detailBadge(state, t), fields: [], }) @@ -99,6 +100,7 @@ function lspDetails(args: Record, text: string, t: DetailTransl * @param args - Recorded creation arguments, including description and full prompt. * @param text - Single recorded text block; absent for other content layouts. * @param meta - Tool-owned result metadata; only absent metadata permits legacy text parsing. + * Its `name` is the child's model-facing agent name and is absent from receipts recorded before agents had names. * @param t - Conversation translator. * @returns Started task details, a historical synchronous reply, or null for unsupported data. */ @@ -110,6 +112,8 @@ export function subagentDetails( if (nonempty(description)) fields.push({ label: t('detail.field.agentDescription'), value: description }) if (meta !== undefined) { if (!detailRecord(meta) || meta.kind !== 'activation' || !nonempty(meta.subagentId)) return null + if (meta.name !== undefined && !nonempty(meta.name)) return null + if (meta.name !== undefined) fields.push({ label: t('detail.field.agentName'), value: meta.name }) fields.push({ label: t('detail.field.id'), value: meta.subagentId }) } else { if (text === undefined) return null @@ -139,7 +143,8 @@ export function subagentDetails( export function controlDetails( name: string, args: Record, text: string, json: unknown, t: DetailTranslate, ): ToolDetailsModel | null { - const target = arg(args, 'target') || arg(args, 'agent_id') || arg(args, 'sessionId') || arg(args, 'job_id') + // Historical `send_message` and `interrupt_agent` records name their target with `agent_id`. + const target = arg(args, 'target') || arg(args, 'agent') || arg(args, 'agent_id') || arg(args, 'sessionId') || arg(args, 'job_id') switch (name) { case 'list_agents': return agentList(text, json, t) case 'job_list': return jobList(text, t) diff --git a/packages/client/ui-tool/src/client/tool/models/detail-model-shared.ts b/packages/client/ui-tool/src/client/tool/models/detail-model-shared.ts index 7bba7fb2de..62d4f975c5 100644 --- a/packages/client/ui-tool/src/client/tool/models/detail-model-shared.ts +++ b/packages/client/ui-tool/src/client/tool/models/detail-model-shared.ts @@ -66,7 +66,7 @@ export function detailBadge(status: string, t: DetailTranslate): NonNullable { }) it('uses producer wording for agent, job, terminal, and LSP lists', () => { - const agents = details('list_agents', 'a1 [running] — Build\na2 [ready] parent=a1 depth=1 — Review', { scope: 'descendants' }) - expect(agents?.summary).toBe('2 agents') + const agents = details('list_agents', 'lead [running] — root agent\nreviewer/checker [inactive] — Check tests\nold-id [unavailable]') + expect(agents?.summary).toBe('3 agents') expect(agents?.items).toEqual([ + { title: 'lead', subtitle: 'root agent', badge: { label: 'Running', tone: 'info' }, fields: [] }, + { title: 'reviewer/checker', subtitle: 'Check tests', badge: { label: 'Inactive', tone: 'warning' }, fields: [] }, + { title: 'old-id', badge: { label: 'unavailable', tone: 'neutral' }, fields: [] }, + ]) + const historical = details('list_agents', 'a1 [running] — Build\na2 [ready] parent=a1 depth=1 — Review\na3 [diagnostic: corrupt] parent=a1 depth=1', { scope: 'descendants' }) + expect(historical?.summary).toBe('3 agents') + expect(historical?.items).toEqual([ { title: 'a1', subtitle: 'Build', badge: { label: 'Running', tone: 'info' }, fields: [] }, { title: 'a2', subtitle: 'Review', badge: { label: 'Ready', tone: 'neutral' }, fields: [] }, + { title: 'a3', badge: { label: 'diagnostic: corrupt', tone: 'neutral' }, fields: [] }, ]) + const structured = details('list_agents', JSON.stringify([{ name: 'lead', status: 'running', description: 'root agent' }, { name: 'reviewer', status: 'inactive' }])) + expect(structured?.items).toEqual([ + { title: 'lead', description: 'root agent', badge: { label: 'Running', tone: 'info' }, fields: [] }, + { title: 'reviewer', badge: { label: 'Inactive', tone: 'warning' }, fields: [] }, + ]) + expect(details('list_agents', 'reviewer running')).toBeNull() expect(details('list_agents', '(no subagents)')?.empty).toBe('No results') expect(details('job_list', 'j1 [bash] running — Build')?.items[0]?.fields).toContainEqual({ label: 'Type', value: 'bash' }) expect(details('job_list', '(no background jobs)')?.empty).toBe('No results') @@ -103,20 +117,39 @@ describe('control detail adapters', () => { it.each(['subagent', 'subagent_fork'])('%s renders persisted creation metadata independently of result wording', (name) => { const args = { description: 'Review the changes', prompt: 'Read the diff and report defects.' } - const block = { ...output(name, 'Delegation accepted.', args), meta: { kind: 'activation', subagentId: 'child-1' } } + const block = { ...output(name, 'Delegation accepted.', args), meta: { kind: 'activation', subagentId: 'child-1', name: 'reviewer' } } const model = detailsCardModel(block, t, 'en') expect(model?.items[0]).toEqual({ title: 'Started', - fields: [{ label: 'Agent task description', value: args.description }, { label: 'ID', value: 'child-1' }], + fields: [ + { label: 'Agent task description', value: args.description }, + { label: 'Agent name', value: 'reviewer' }, + { label: 'ID', value: 'child-1' }, + ], groups: [{ label: 'Task', items: [{ description: args.prompt, fields: [] }] }], }) expect(detailsCardModel({ ...block, content: [] }, t, 'en')).toEqual(model) expect(detailsCardModel({ ...block, isError: true }, t, 'en')).toBeNull() - expect(detailsCardModel(block, makeTranslate(zh, commonZh), 'zh-CN')?.items[0]?.fields) - .toEqual([{ label: '智能体任务描述', value: args.description }, { label: 'ID', value: 'child-1' }]) + expect(detailsCardModel(block, makeTranslate(zh, commonZh), 'zh-CN')?.items[0]?.fields).toEqual([ + { label: '智能体任务描述', value: args.description }, + { label: '智能体名称', value: 'reviewer' }, + { label: 'ID', value: 'child-1' }, + ]) }) - it.each([null, [], {}, { kind: 'other', subagentId: 'child-1' }, { kind: 'activation' }, { kind: 'activation', subagentId: 1 }, { kind: 'activation', subagentId: ' ' }])( + it('omits the name from metadata recorded before agents had names', () => { + const args = { description: 'Review the changes', prompt: 'Read the diff and report defects.' } + const block = { ...output('subagent', 'started subagent child-1', args), meta: { kind: 'activation', subagentId: 'child-1' } } + expect(detailsCardModel(block, t, 'en')?.items[0]?.fields).toEqual([ + { label: 'Agent task description', value: args.description }, { label: 'ID', value: 'child-1' }, + ]) + }) + + it.each([ + null, [], {}, { kind: 'other', subagentId: 'child-1' }, { kind: 'activation' }, { kind: 'activation', subagentId: 1 }, + { kind: 'activation', subagentId: ' ' }, { kind: 'activation', subagentId: 'child-1', name: 1 }, + { kind: 'activation', subagentId: 'child-1', name: ' ' }, + ])( 'keeps malformed metadata on the generic result path (%j)', (meta) => { const block = { ...output('subagent', 'started subagent child-1'), meta } expect(detailsCardModel(block, t, 'en')).toBeNull() @@ -127,7 +160,10 @@ describe('control detail adapters', () => { expect(details('spawn_teammate', '{"member":{"id":"a1","status":"running"}}', { name: 'A' })?.items[0]).toMatchObject({ title: 'a1', badge: { label: 'Running' } }) expect(details('team_task_create', '{"id":"task-1","subject":"Review","status":"pending"}')?.summary).toBe('Review') expect(details('team_task_list', '{"tasks":[{"id":"task-1","subject":"Review"}],"nextCursor":3}')?.caption).toBe('More tasks available; next cursor is 3') - expect(details('send_message', '{"status":"queued"}', { agent_id: 'a1', message: 'hello' })?.items[0]?.badge?.label).toBe('Queued') + expect(details('team_task_get', '{"id":"task-1","subject":"Review","owner":"reviewer/checker"}')?.items[0]?.fields).toContainEqual({ label: 'Owner', value: 'reviewer/checker' }) + expect(details('team_task_get', '{"id":"task-1","subject":"Review","ownerId":"child-1"}')?.items[0]?.fields).toContainEqual({ label: 'Owner', value: 'child-1' }) + expect(details('send_message', '{"status":"queued"}', { agent: 'reviewer', message: 'hello' })?.items[0]).toMatchObject({ title: 'reviewer', badge: { label: 'Queued' } }) + expect(details('send_message', 'message delivered to agent reviewer/checker', { agent: 'reviewer/checker', message: 'hello' })?.summary).toBe('reviewer/checker · Message delivered') expect(details('send_message', 'message delivered to agent reviewer', { agent_id: 'reviewer', message: 'hello' })?.summary).toBe('reviewer · Message delivered') expect(details('interrupt_agent', '{"previousStatus":"running"}', { agent_id: 'a1' })?.items[0]?.fields).toContainEqual({ label: 'Previous status', value: 'Running' }) expect(details('wait_agent', '{"timedOut":true}')?.summary).toBe('Wait timed out') @@ -149,6 +185,7 @@ describe('control detail adapters', () => { expect(details('unknown', 'x')).toBeNull() expect(details('team_task_list', '{"tasks":[],"nextCursor":"bad"}')).toBeNull() expect(details('terminal_read', 'bad', { sessionId: 'pty-1' })).toBeNull() + expect(details('interrupt_agent', 'interrupt requested for agent reviewer', { agent: 'reviewer' })?.summary).toBe('reviewer · Interrupt requested') expect(details('interrupt_agent', 'interrupt requested for agent reviewer', { agent_id: 'reviewer' })?.summary).toContain('Interrupt') expect(details('interrupt_agent', 'unexpected', { agent_id: 'a1' })).toBeNull() expect(details('job_kill', 'unexpected', { job_id: 'j1' })).toBeNull() diff --git a/packages/client/ui-tool/tests/details-row.client.spec.tsx b/packages/client/ui-tool/tests/details-row.client.spec.tsx index aa02de3477..b5dde4a667 100644 --- a/packages/client/ui-tool/tests/details-row.client.spec.tsx +++ b/packages/client/ui-tool/tests/details-row.client.spec.tsx @@ -153,17 +153,17 @@ describe('todosDetail', () => { }) describe('DetailsRow', () => { - it('puts agent IDs first and lets a compact description expand and collapse', () => { + it('puts agent names first and lets a compact description expand and collapse', () => { const description = 'Verify every construction bound. '.repeat(20) - const block = { ...result('list_agents', null), content: [{ type: 'text' as const, text: `tree-checker-id [running] parent=root-id depth=1 — ${description}` }] } + const block = { ...result('list_agents', null), content: [{ type: 'text' as const, text: `reviewer/checker [running] — ${description}` }] } const model = detailsCardModel(block, t, 'en')! render() - const id = screen.getByText('tree-checker-id') - const toggle = screen.getByRole('button', { name: 'Expand description for tree-checker-id' }) - expect(id.compareDocumentPosition(toggle) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() + const name = screen.getByText('reviewer/checker') + const toggle = screen.getByRole('button', { name: 'Expand description for reviewer/checker' }) + expect(name.compareDocumentPosition(toggle) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() expect(toggle.textContent).toBe(description) fireEvent.click(toggle) - expect(screen.getByRole('button', { name: 'Collapse description for tree-checker-id' }).getAttribute('aria-expanded')).toBe('true') + expect(screen.getByRole('button', { name: 'Collapse description for reviewer/checker' }).getAttribute('aria-expanded')).toBe('true') fireEvent.click(toggle) expect(toggle.getAttribute('aria-expanded')).toBe('false') }) diff --git a/packages/experimental/creation-recommendations/tests/loader-composition.spec.ts b/packages/experimental/creation-recommendations/tests/loader-composition.spec.ts index 4e341db868..501dcb4ca0 100644 --- a/packages/experimental/creation-recommendations/tests/loader-composition.spec.ts +++ b/packages/experimental/creation-recommendations/tests/loader-composition.spec.ts @@ -73,7 +73,7 @@ it.each(['portable-bundle', 'foreign-nested-row'] as const)('validates and insta await rename(join(spec.cwd!, 'index.js'), join(spec.cwd!, 'runtime.mjs')) await symlink('./runtime.mjs', join(spec.cwd!, 'index.js')) } - return { childId: spec.childId!, dispose: async () => {}, + return { childId: spec.childId!, name: 'agent-1', dispose: async () => {}, result: Promise.resolve({ output: [], stopReason: 'completed', structured: { ready: true } }) } } }) Object.assign(ctx.loader.builtins, { sessions: Sessions, projections: Projections, llm: Llm, prompt: SystemPrompt, tools: Tools, diff --git a/packages/experimental/tool-subagent-oar/README.i18n.yaml b/packages/experimental/tool-subagent-oar/README.i18n.yaml index a652d96c43..6815709eae 100644 --- a/packages/experimental/tool-subagent-oar/README.i18n.yaml +++ b/packages/experimental/tool-subagent-oar/README.i18n.yaml @@ -9,8 +9,8 @@ en: 520d9dc51e2b0142 zh: 8f332776243107a9 /deepseek-ai-dsh-experimental-tool-subagent-oar/summary: - en: 8adbaf2511d2c47f - zh: 1495f6bdb84a30ea + en: eeb535e580132e01 + zh: 918fc55ce2be9581 /deepseek-ai-dsh-experimental-tool-subagent-oar/table-of-contents: en: d152484eb41ac6b4 zh: 8d1e20b826450ffe @@ -21,11 +21,11 @@ en: 368805e5bcffeb0a zh: 9340ff5074ada9e1 /deepseek-ai-dsh-experimental-tool-subagent-oar/use-this-package/minimal-configuration: - en: 5b2b33c1d0fae20a - zh: 26bff1e013e2bfcd + en: 2f456750c533e966 + zh: 89ed99fcbb664475 /deepseek-ai-dsh-experimental-tool-subagent-oar/understand-the-implementation: - en: 3e8a9fffa9c5afd0 - zh: 932b6d0b682208ba + en: 87b1d584ff51cc25 + zh: bc3858dc2f3a8099 /deepseek-ai-dsh-experimental-tool-subagent-oar/further-exploration: en: a796236202b695f5 zh: 8e76361f31517714 @@ -36,11 +36,11 @@ en: 8febbb031aa0452d zh: da3f9011631652da /deepseek-ai-dsh-experimental-tool-subagent-oar/model-experience/external-delegation-tool/what-the-model-sees: - en: 2e0f5274092f794b - zh: 83a14e5f7018717f + en: 7ec989dffbc0f65e + zh: fc734ea0a8801bab /deepseek-ai-dsh-experimental-tool-subagent-oar/model-experience/external-delegation-tool/token-effect: - en: 60d7ffec7f4dc858 - zh: 09f286978df6b4ec + en: 33d073eb994d2969 + zh: 320f4a82aa665a46 /deepseek-ai-dsh-experimental-tool-subagent-oar/model-experience/external-delegation-tool/kv-cache-effect: en: 7c1ac0b007b2f0ce zh: 2332a07c6f8bcbdc diff --git a/packages/experimental/tool-subagent-oar/README.md b/packages/experimental/tool-subagent-oar/README.md index 991dfc0c3e..bda799237c 100644 --- a/packages/experimental/tool-subagent-oar/README.md +++ b/packages/experimental/tool-subagent-oar/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -Use one `subagent_oar` tool to choose a configured external agent for each standalone text task. Configuration fixes the available runtime aliases and their providers; the model selects an alias without changing native arguments or permissions. Each task uses independent context and returns a child id, with a later completion notice delivered to the parent. +Use one `subagent_oar` tool to choose a configured external agent for each standalone text task. Configuration fixes the available runtime aliases and their providers; the model selects an alias without changing native arguments or permissions. Each task uses independent context and returns the child's name, with a later completion notice delivered to the parent. ## Table of Contents @@ -48,7 +48,7 @@ With `oar-codex` and `oar-claude` already registered, mount this row in the comp | `providers` | required | Nonempty map from model-visible runtime aliases to registered provider names | | `toolName` | `subagent_oar` | Tool name; every mounted instance needs a distinct name | -The [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-experimental-tool-subagent-oar) owns accepted fields and validation. A call requires `runtime`, `description`, and `prompt`; optional `cwd` selects the child directory, with relative paths resolved from the parent's current directory. The prompt must contain the context the child needs. Native installation, authentication, model settings, and permissions belong to the selected provider. +The [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-experimental-tool-subagent-oar) owns accepted fields and validation. A call requires `runtime`, `description`, and `prompt`. Optional `name` names the child, unique among the caller's children; omission selects `agent-1`, `agent-2`, and so on, under the [subagent runtime's name rule](../../subagent/subagent/README.md#agent-names). Optional `cwd` selects the child directory, with relative paths resolved from the parent's current directory. The prompt must contain the context the child needs. Native installation, authentication, model settings, and permissions belong to the selected provider. ----- @@ -58,7 +58,7 @@ The [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-exper
Implementation internals — click to expand -[`src/index.ts`](src/index.ts) builds the runtime enum from the configured aliases and registers one tool. Each call resolves its provider and starts an activation through the shared subagent service, which owns capacity, parent lifetime, cancellation, terminal recording, and completion delivery. The tool's call card uses the task description and submitted arguments. Disposing the plugin removes its tool registration; the activation manager retains ownership of accepted children. +[`src/index.ts`](src/index.ts) builds the runtime enum from the configured aliases and registers one tool. Each call resolves its provider and starts an activation through the shared subagent service, which owns capacity, parent lifetime, cancellation, terminal recording, and completion delivery. The tool's call card uses the task description and submitted arguments. The result records `{ kind: 'activation', subagentId, name }` as presentation metadata. Disposing the plugin removes its tool registration; the activation manager retains ownership of accepted children.
@@ -80,11 +80,11 @@ The [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-exper #### What the model sees -The [tool catalog](../../../docs/tool-catalog.md#deepseek-aidsh-experimental-tool-subagent-oar) owns the description and argument schema. The `runtime` enum contains the configured aliases. After acceptance, the tool returns `started subagent `, where `` identifies the child. The shared subagent service later supplies the child's answer or failure notice. +The [tool catalog](../../../docs/tool-catalog.md#deepseek-aidsh-experimental-tool-subagent-oar) owns the description and argument schema. The `runtime` enum contains the configured aliases. After acceptance, the tool returns `started subagent `, where `` is the child's full name from the root, such as `reviewer`. The shared subagent service later supplies the child's answer or failure notice. #### Token effect -Each mounted instance adds one tool schema. The acknowledgement and completion notice append to the parent context; the provider owns the child's independent context and token use. +Each mounted instance adds one tool schema, including the optional `name` field. The acknowledgement and completion notice append to the parent context; the provider owns the child's independent context and token use. #### KV Cache effect diff --git a/packages/experimental/tool-subagent-oar/README.zh.md b/packages/experimental/tool-subagent-oar/README.zh.md index 3213e82786..60b5ef8760 100644 --- a/packages/experimental/tool-subagent-oar/README.zh.md +++ b/packages/experimental/tool-subagent-oar/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -使用一个 `subagent_oar` 工具,为每个独立文本任务选择已配置的外部智能体。配置固定可用的运行时别名及其提供方;模型选择别名,不能更改原生参数或权限。每个任务使用独立上下文并返回子任务 id,完成通知稍后送达父智能体。 +使用一个 `subagent_oar` 工具,为每个独立文本任务选择已配置的外部智能体。配置固定可用的运行时别名及其提供方;模型选择别名,不能更改原生参数或权限。每个任务使用独立上下文并返回子任务的名称,完成通知稍后送达父智能体。 ## 目录 @@ -48,7 +48,7 @@ kind: "package-reference" | `providers` | 必填 | 从模型可见运行时别名映射到已注册提供方名称的非空映射 | | `toolName` | `subagent_oar` | 工具名称;每个挂载实例需要不同名称 | -[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-experimental-tool-subagent-oar)定义接受的字段和校验规则。调用要求提供 `runtime`、`description` 和 `prompt`;可选的 `cwd` 选择子任务目录,相对路径从父智能体的当前目录解析。提示必须包含子任务需要的上下文。原生安装、认证、模型设置和权限由所选提供方负责。 +[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-experimental-tool-subagent-oar)定义接受的字段和校验规则。调用要求提供 `runtime`、`description` 和 `prompt`。可选的 `name` 为子任务命名,在调用方的子级之间唯一;省略时按[子智能体运行时的名称规则](../../subagent/subagent/README.zh.md#agent-names)依次选择 `agent-1`、`agent-2` 等。可选的 `cwd` 选择子任务目录,相对路径从父智能体的当前目录解析。提示必须包含子任务需要的上下文。原生安装、认证、模型设置和权限由所选提供方负责。 ----- @@ -58,7 +58,7 @@ kind: "package-reference"
实现内部细节 — 点击展开 -[`src/index.ts`](src/index.ts)根据配置的别名构建运行时枚举,并注册一个工具。每次调用解析其提供方,并通过共享子智能体服务启动激活;该服务负责容量、父智能体生命周期、取消、最终记录和完成通知交付。工具调用卡片使用任务描述和提交的参数。释放插件会移除其工具注册;激活管理器继续负责已接受的子任务。 +[`src/index.ts`](src/index.ts)根据配置的别名构建运行时枚举,并注册一个工具。每次调用解析其提供方,并通过共享子智能体服务启动激活;该服务负责容量、父智能体生命周期、取消、最终记录和完成通知交付。工具调用卡片使用任务描述和提交的参数。结果以展示元数据记录 `{ kind: 'activation', subagentId, name }`。释放插件会移除其工具注册;激活管理器继续负责已接受的子任务。
@@ -80,11 +80,11 @@ kind: "package-reference" #### 模型看到什么 -[工具目录](../../../docs/tool-catalog.zh.md#deepseek-aidsh-experimental-tool-subagent-oar)定义描述和参数 schema。`runtime` 枚举包含配置的别名。接受任务后,工具返回 `started subagent `,其中 `` 标识子任务。共享子智能体服务稍后提供子任务的答复或失败通知。 +[工具目录](../../../docs/tool-catalog.zh.md#deepseek-aidsh-experimental-tool-subagent-oar)定义描述和参数 schema。`runtime` 枚举包含配置的别名。接受任务后,工具返回 `started subagent `,其中 `` 是子任务从根开始的完整名称,例如 `reviewer`。共享子智能体服务稍后提供子任务的答复或失败通知。 #### Token 影响 -每个挂载实例添加一个工具 schema。确认和完成通知追加到父上下文;提供方负责子任务的独立上下文和 token 使用。 +每个挂载实例添加一个工具 schema,其中包括可选的 `name` 字段。确认和完成通知追加到父上下文;提供方负责子任务的独立上下文和 token 使用。 #### KV Cache 影响 diff --git a/packages/experimental/tool-subagent-oar/src/index.ts b/packages/experimental/tool-subagent-oar/src/index.ts index c73af97901..b7af788e98 100644 --- a/packages/experimental/tool-subagent-oar/src/index.ts +++ b/packages/experimental/tool-subagent-oar/src/index.ts @@ -36,7 +36,7 @@ export function apply(ctx: Context, config: Config): void { name: toolName, description: 'Delegate a self-contained task to a configured external agent. Choose its runtime for each call. ' + 'The child works in its own context and does not see this conversation. ' - + 'This tool returns a child id; a later completion notice includes its final answer. ' + + 'This tool returns the child\'s name; a later completion notice includes its final answer. ' + 'The child does not accept follow-up messages. Start independent tasks together and continue useful work while they run. ' + 'When only waiting for their results remains, end your turn instead of polling.', parameters: { @@ -51,6 +51,11 @@ export function apply(ctx: Context, config: Config): void { required: true, description: 'A short description of the delegated task, for display.', }, + name: { + type: 'string', + description: 'Optional short name for the subagent, unique among the subagents you create: lower-case letters, ' + + 'digits, and hyphens, starting with a letter, at most 32 characters. Omitted names default to agent-1, agent-2, and so on.', + }, prompt: { type: 'string', required: true, @@ -70,9 +75,11 @@ export function apply(ctx: Context, config: Config): void { properties: { kind: { type: 'string', required: true, const: 'activation' }, subagentId: { type: 'string', required: true }, + name: { type: 'string', required: true }, }, }, - render: (_args, value) => [{ type: 'text', text: `started subagent ${value.subagentId}` }], + render: (_args, value) => [{ type: 'text', text: `started subagent ${value.name}` }], + presentationMeta: (_args, value) => ({ kind: value.kind, subagentId: value.subagentId, name: value.name }), }, /* jscpd:ignore-end */ isConcurrencySafe: () => true, @@ -88,13 +95,14 @@ export function apply(ctx: Context, config: Config): void { const activation = await ctx.subagents.startActivation({ provider: providerName, label: args.description, + ...args.name === undefined ? {} : { name: args.name }, parent, prompt: [{ type: 'text', text: args.prompt }], ...args.cwd === undefined ? {} : { cwd: args.cwd }, signal: exec.signal, delivery: 'parent', }) - return { kind: 'activation' as const, subagentId: activation.childId } + return { kind: 'activation' as const, subagentId: activation.childId, name: activation.name } }, presentCall: args => ({ card: 'generic', title: args.description, rawInput: args }), })) diff --git a/packages/experimental/tool-subagent-oar/tests/tool-subagent-oar.spec.ts b/packages/experimental/tool-subagent-oar/tests/tool-subagent-oar.spec.ts index 62bc37dee4..99f748c5d4 100644 --- a/packages/experimental/tool-subagent-oar/tests/tool-subagent-oar.spec.ts +++ b/packages/experimental/tool-subagent-oar/tests/tool-subagent-oar.spec.ts @@ -44,19 +44,21 @@ it('selects one configured provider per call and exposes only task inputs', asyn }) const schema = ctx.tools.schemas().find(tool => tool.name === 'subagent_oar') expect(schema?.parameters.required).toEqual(['runtime', 'description', 'prompt']) - expect(Object.keys(schema?.parameters.properties as Record).sort()).toEqual(['cwd', 'description', 'prompt', 'runtime']) + expect(Object.keys(schema?.parameters.properties as Record).sort()).toEqual(['cwd', 'description', 'name', 'prompt', 'runtime']) expect(schema?.parameters.properties).toMatchObject({ runtime: { enum: ['codex', 'claude', 'future'] } }) - for (const runtime of ['codex', 'claude', 'future']) { + for (const [runtime, name, expected] of [['codex', undefined, 'agent-1'], ['claude', 'reviewer', 'reviewer'], ['future', undefined, 'agent-2']] as const) { const result = await executeToolFixture(ctx, { agent: parent, name: 'subagent_oar', signal: test.signal, callId: ToolCallId(randomUUID()), - arguments: { runtime, description: `Use ${runtime}`, prompt: 'independent task' }, + arguments: { runtime, description: `Use ${runtime}`, prompt: 'independent task', ...name === undefined ? {} : { name } }, }) expect(result.isError, JSON.stringify(result.content)).toBe(false) - expect(result.content).toHaveLength(1) - const content = result.content[0] - expect(content?.type).toBe('text') - if (content?.type === 'text') expect(content.text).toMatch(/^started subagent /u) + expect(result.content).toEqual([{ type: 'text', text: `started subagent ${expected}` }]) + if (!result.isError) { + const { subagentId } = result.value as { subagentId: string } + expect(ctx.tools.get('subagent_oar')?.output.presentationMeta?.({}, result.value)) + .toEqual({ kind: 'activation', subagentId, name: expected }) + } } expect(calls.map(call => call.provider)).toEqual(['codex-provider', 'claude-provider', 'future-provider']) expect(calls[0]?.request.prompt).toEqual([{ type: 'text', text: 'independent task' }]) diff --git a/packages/experimental/webworker-runtime/tests/vfs-example-fixture.ts b/packages/experimental/webworker-runtime/tests/vfs-example-fixture.ts index 598f6a3cb8..e742b02023 100644 --- a/packages/experimental/webworker-runtime/tests/vfs-example-fixture.ts +++ b/packages/experimental/webworker-runtime/tests/vfs-example-fixture.ts @@ -17,7 +17,6 @@ import { import { projectionCacheDomainSpec } from '@deepseek-ai/dsh-session-projection-cache' import { scheduleDomain } from '@deepseek-ai/dsh-schedule' import { snapshotSubagentDescriptor, SUBAGENT_DESCRIPTOR_VERSION } from '@deepseek-ai/dsh-subagent' -import { SUBAGENT_CATALOG_VERSION } from '@deepseek-ai/dsh-subagent/src/catalog.ts' /** Root copied by the preview image's repository adapter. */ export const VFS_EXAMPLE_ROOT = fileURLToPath(new URL('./fixtures/vfs-example', import.meta.url)) @@ -386,7 +385,8 @@ function mainLog(): { { childId: VFS_EXAMPLE_SESSION_IDS.oneShot, childCreatedAt: CREATED_AT + 100_000, mode: 'one-shot', label: 'Review preview architecture' }, { childId: VFS_EXAMPLE_SESSION_IDS.continuable, childCreatedAt: CREATED_AT + 200_000, mode: 'continuable', label: 'Continue preview verification' }, ] as const) { - log.add({ type: 'subagent/catalog', data: { version: SUBAGENT_CATALOG_VERSION, ...child } }) + // Version 0 keeps both preview children unnamed, as releases before agent names recorded them. + log.add({ type: 'subagent/catalog', data: { version: 0, ...child } }) } log.add({ type: 'step/end', data: { turn, step: 1 } }) log.add({ type: 'step/start', data: { turn, step: 2 } }) diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 8656c25689..8cff994d7e 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -3649,6 +3649,20 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ returns: 'the live root; ordinary Session forks lead independent trees.', throws: ['when the caller or its runtime owner is unavailable.'], }, + { + signature: 'namesOf(agent: Agent): SubagentMemberNames', + description: 'Report the model-facing names of a live tree member and its direct creator. The root is `lead`; a descendant is the slash-separated path of names from the root, or its Session id when it or a creator below the root has no name.', + parameters: [{ name: 'agent', description: 'exact live root or resident tree member.' }], + returns: 'the member\'s name and, for a descendant, its creator\'s name.', + throws: ['when the caller or its runtime owner is unavailable.'], + }, + { + signature: 'async resolveName(caller: Agent, name: string, signal: AbortSignal): Promise', + description: 'Resolve a model-supplied agent reference within the caller\'s tree. `lead` selects the root; a slash-separated path is looked up through the catalogs along it; any other text is returned as a Session id. Resolution grants no authority: messaging, interruption, and assignment authorize the result.', + parameters: [{ name: 'caller', description: 'exact live Agent whose tree root anchors the lookup.' }, { name: 'name', description: '`lead`, a full name from the root, or a Session id.' }, { name: 'signal', description: 'cancellation forwarded to each catalog read.' }], + returns: 'the referenced Session id.', + throws: ['when the caller\'s root is unavailable or a catalog along the path cannot be read.'], + }, { signature: 'async waitForTreeWork(root: Agent): Promise', description: 'Join progressing subagent tree work for its root. Idle members with parked input stay resident without delaying host completion. Ordinary members return false.', @@ -6827,7 +6841,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'KnownSubagentCatalogFields', - declaration: 'export type KnownSubagentCatalogFields = {\n readonly mode: \'one-shot\';\n readonly label?: string;\n} | {\n readonly mode: \'external\';\n readonly label?: string;\n} | {\n readonly mode: \'continuable\';\n readonly label: string;\n};', + declaration: 'export type KnownSubagentCatalogFields = {\n readonly mode: \'one-shot\';\n readonly label?: string;\n} | {\n readonly mode: \'external\';\n readonly label?: string;\n readonly name?: string;\n} | {\n readonly mode: \'continuable\';\n readonly label: string;\n readonly name?: string;\n};', }, { name: 'KvFacet', @@ -9087,11 +9101,11 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SubagentActivation', - declaration: 'export interface SubagentActivation {\n readonly childId: SessionId;\n readonly messageId?: MessageId;\n readonly result: Promise;\n dispose(): Promise;\n}', + declaration: 'export interface SubagentActivation {\n readonly childId: SessionId;\n readonly name: string;\n readonly messageId?: MessageId;\n readonly result: Promise;\n dispose(): Promise;\n}', }, { name: 'SubagentActivationSpec', - declaration: 'export interface SubagentActivationSpec extends Omit {\n readonly provider: string;\n readonly label: string;\n readonly childId?: SessionId;\n readonly signal: AbortSignal;\n readonly delivery: \'parent\' | \'caller\';\n}', + declaration: 'export interface SubagentActivationSpec extends Omit {\n readonly provider: string;\n readonly label: string;\n readonly name?: string;\n readonly childId?: SessionId;\n readonly signal: AbortSignal;\n readonly delivery: \'parent\' | \'caller\';\n}', }, { name: 'SubagentCapabilities', @@ -9107,7 +9121,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SubagentDescendantListEntry', - declaration: 'export type SubagentDescendantListEntry = SubagentListEntry & {\n readonly parentId: SessionId;\n readonly depth: number;\n};', + declaration: 'export type SubagentDescendantListEntry = SubagentListEntry & {\n readonly parentId: SessionId;\n readonly depth: number;\n readonly path?: string;\n};', }, { name: 'SubagentInterruptAuthority', @@ -9121,6 +9135,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'SubagentListEntry', declaration: 'export type SubagentListEntry = SubagentCatalogRow & {\n readonly kind: \'child\';\n readonly hasChildren: boolean;\n} | {\n readonly kind: \'diagnostic\';\n readonly id: SessionId;\n readonly reason: \'corrupt\' | \'unsupported\' | \'unavailable\';\n};', }, + { + name: 'SubagentMemberNames', + declaration: 'export interface SubagentMemberNames {\n readonly self: string;\n readonly parent?: string;\n}', + }, { name: 'SubagentPromptReceipt', declaration: 'export interface SubagentPromptReceipt {\n readonly messageId: MessageId;\n}', @@ -9159,7 +9177,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'SubagentRuntime', - declaration: 'export class SubagentRuntime extends TypertRemoteService {\n static Config;\n static inject;\n constructor(ctx: Context, private config: Config);\n resolveMaxDepth(configured?: number | \'provider-managed\'): number | undefined;\n startActivation(spec: SubagentActivationSpec & {\n readonly childId: SessionId;\n }): Promise;\n startActivation(spec: SubagentActivationSpec): Promise;\n async startActivation(spec: SubagentActivationSpec): Promise;\n rootOf(caller: Agent): Agent;\n async waitForTreeWork(root: Agent): Promise;\n async sendMessage(sender: Agent, targetId: SessionId, content: ContentBlock[], options: SubagentSendMessageOptions): Promise;\n interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void;\n async drainDescendants(parents: readonly Agent[]): Promise;\n listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise;\n @Remote({ name: \'prompt\', resources: { session: request => request.childSessionId } })\n async prompt(request: SubagentPromptRequest, signal: AbortSignal): Promise;\n @Remote({ name: \'interruptByUser\', resources: { session: childSessionId => childSessionId } })\n interruptByUser(childSessionId: SessionId): SubagentInterruptReceipt;\n registerProvider(provider: SubagentProvider): () => /* …truncated — full shape in source */', + declaration: 'export class SubagentRuntime extends TypertRemoteService {\n static Config;\n static inject;\n constructor(ctx: Context, private config: Config);\n resolveMaxDepth(configured?: number | \'provider-managed\'): number | undefined;\n startActivation(spec: SubagentActivationSpec & {\n readonly childId: SessionId;\n }): Promise;\n startActivation(spec: SubagentActivationSpec): Promise;\n async startActivation(spec: SubagentActivationSpec): Promise;\n rootOf(caller: Agent): Agent;\n namesOf(agent: Agent): SubagentMemberNames;\n async resolveName(caller: Agent, name: string, signal: AbortSignal): Promise;\n async waitForTreeWork(root: Agent): Promise;\n async sendMessage(sender: Agent, targetId: SessionId, content: ContentBlock[], options: SubagentSendMessageOptions): Promise;\n interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void;\n async drainDescendants(parents: readonly Agent[]): Promise;\n listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise;\n @Remote({ name: \'prompt\', resources: { session: request => request.childSessionId } })\n async prompt(request: SubagentPromptRequest, signal: AbortSignal): Promise;\n @Remote({ name: \'interruptByUser\', resources: { session: childSessionId => childSess /* …truncated — full shape in source */', }, { name: 'SubagentSendMessageOptions', diff --git a/packages/session/session-format-v3-to-v4/README.i18n.yaml b/packages/session/session-format-v3-to-v4/README.i18n.yaml index 098cbd47da..48b7630c92 100644 --- a/packages/session/session-format-v3-to-v4/README.i18n.yaml +++ b/packages/session/session-format-v3-to-v4/README.i18n.yaml @@ -51,8 +51,8 @@ en: 610f35b8c44538a2 zh: 17e5c07f09783106 /deepseek-ai-dsh-session-format-v3-to-v4/native-v4-admission/lifecycle-and-reference-relationships: - en: 7dd4a236075233c7 - zh: 988e67acdd32d2ba + en: c297e71fea2ebc5e + zh: c902b367725baf75 /deepseek-ai-dsh-session-format-v3-to-v4/native-v4-admission/developer-changes-and-deferred-schemas: en: 471f0e087806057a zh: 76b2ee1f58118d71 diff --git a/packages/session/session-format-v3-to-v4/README.md b/packages/session/session-format-v3-to-v4/README.md index d8133ac866..570689ceda 100644 --- a/packages/session/session-format-v3-to-v4/README.md +++ b/packages/session/session-format-v3-to-v4/README.md @@ -245,7 +245,7 @@ Current common admission does not validate each user/tool/developer content bloc | `compaction/start`, `compaction/summary`, `compaction/end` | Match compaction id, source command, and the active turn context. Summary spans name exact current-surface nodes and exclude the protected head; successful completion has one summary. Inherited unfinished compactions expire at the end-seed marker. | | `compaction/prune` | Its span names exact current-surface nodes and excludes the protected head; it does not require a compaction transaction or its owner fields. | | Compact checkpoint replacement | Its `compact-checkpoint` source identifies the active compaction. | -| Native `subagent/catalog` | Validates own version-0/version-1/version-2 payloads after the inherited cut. Each child id must be unique. Native reads neither collect child logs nor compare their physical facts; inherited entries do not establish own membership. | +| Native `subagent/catalog` | Validates own version-0 through version-3 payloads after the inherited cut; version 3 is native-only and adds the child's name to a continuable or external entry. Each child id must be unique. Native reads neither collect child logs nor compare their physical facts; inherited entries do not establish own membership. | | Inherited cut and delivery | Apply the native V4 marker, coordinate, and generation-ownership rules above. | These checks are generation-owned in [relationships.ts](src/relationships.ts). Full common message/envelope acceptance and plugin-owned message projections additionally use the installed Session; the exported V4 restorer alone is not a replacement for complete catalog restoration. diff --git a/packages/session/session-format-v3-to-v4/README.zh.md b/packages/session/session-format-v3-to-v4/README.zh.md index 209119b521..574bea108a 100644 --- a/packages/session/session-format-v3-to-v4/README.zh.md +++ b/packages/session/session-format-v3-to-v4/README.zh.md @@ -245,7 +245,7 @@ System image 接纳要求非空 attachment id、PNG/JPEG/WebP/GIF MIME 类 | `compaction/start`、`compaction/summary`、`compaction/end` | 匹配 compaction id、源 command 和活动 turn 上下文。Summary 区间引用精确的当前表面节点且排除 protected head;成功完成需要一个 summary。继承的未完成 compaction 在 end-seed marker 处过期。 | | `compaction/prune` | 其区间引用精确的当前表面节点且排除 protected head;它不要求存在 compaction 事务或其所有者字段。 | | Compact checkpoint 替换 | 其 `compact-checkpoint` 来源标识活动 compaction。 | -| 原生 `subagent/catalog` | 校验继承截点之后的自身 version-0/version-1/version-2 载荷。每个子 id 必须唯一。原生读取不收集子日志,也不比较其物理事实;继承条目不建立自身成员关系。 | +| 原生 `subagent/catalog` | 校验继承截点之后的自身 version-0 至 version-3 载荷;version 3 仅由原生写入,为 continuable 或 external 条目增加子代理名称。每个子 id 必须唯一。原生读取不收集子日志,也不比较其物理事实;继承条目不建立自身成员关系。 | | 继承截点与投递 | 应用上文的原生 V4 标记、坐标及代际归属规则。 | 这些检查由 [relationships.ts](src/relationships.ts) 按代际拥有。完整的通用消息/信封接纳与插件拥有的消息投影还使用已安装 Session;单独的导出 V4 恢复器不能替代完整 catalog 恢复。 diff --git a/packages/session/session-format-v3-to-v4/src/facts.ts b/packages/session/session-format-v3-to-v4/src/facts.ts index ec36e7cb2b..c8a4d99927 100644 --- a/packages/session/session-format-v3-to-v4/src/facts.ts +++ b/packages/session/session-format-v3-to-v4/src/facts.ts @@ -83,6 +83,25 @@ export function catalogFact(value: SessionFormatJsonValue, subject = 'subagent/c return value } +/** + * Validate an own native V4 catalog payload: a historical version or version 3, + * which adds the child's name and is written only by native V4 creation. + * @param value - native `subagent/catalog` payload. + * @returns validated catalog payload. + */ +export function nativeCatalogFact(value: SessionFormatJsonValue): SessionFormatJsonObject { + if (!isSessionFormatJsonObject(value) || value['version'] !== 3) return catalogFact(value) + if (typeof value['childId'] !== 'string' + || (value['mode'] !== 'continuable' && value['mode'] !== 'external') + || typeof value['name'] !== 'string' + || (value['mode'] === 'continuable' && typeof value['label'] !== 'string') + || (value['label'] !== undefined && typeof value['label'] !== 'string')) { + throw new SessionFormatError('subagent/catalog requires a supported versioned catalog fact') + } + sessionFormatCount(value['childCreatedAt'], 'catalog child creation time') + return value +} + /** * Name the child and its optional storage-owned location in a migration diagnostic. * @param source - validated supplemental child evidence. diff --git a/packages/session/session-format-v3-to-v4/src/validation.ts b/packages/session/session-format-v3-to-v4/src/validation.ts index e2ba07e2b3..e6108df4eb 100644 --- a/packages/session/session-format-v3-to-v4/src/validation.ts +++ b/packages/session/session-format-v3-to-v4/src/validation.ts @@ -6,7 +6,7 @@ import type { SessionFormatArtifact, SessionFormatEvent } from '@deepseek-ai/dsh import { assertV4DeveloperData } from './developer.ts' import { assertV4LifecycleRelationships } from './relationships.ts' import { assertV4MessageSources } from './message-sources.ts' -import { catalogFact } from './facts.ts' +import { nativeCatalogFact } from './facts.ts' import { assertV4RetiredSyntax } from './retired-syntax.ts' import { assertV4SystemMessageFields } from './system-message.ts' import { assertV4ForkResult } from './fork-result.ts' @@ -120,7 +120,7 @@ export function assertReleasedV4Relationships(artifact: SessionFormatArtifact, k throw new SessionFormatError('current-generation delivery marker names the wrong Session') } if (event.type === 'subagent/catalog' && event.seq >= artifact.inheritedEventCount) { - const fact = catalogFact(event.data) + const fact = nativeCatalogFact(event.data) const id = fact['childId'] as string if (ids.has(id)) throw new SessionFormatError(`duplicate catalog child ${id}`) ids.add(id) diff --git a/packages/session/session-format-v3-to-v4/tests/codec.spec.ts b/packages/session/session-format-v3-to-v4/tests/codec.spec.ts index 21a764a44b..ba8a2184b4 100644 --- a/packages/session/session-format-v3-to-v4/tests/codec.spec.ts +++ b/packages/session/session-format-v3-to-v4/tests/codec.spec.ts @@ -179,3 +179,24 @@ it.each([ const data = { ...(catalog.data as SessionFormatJsonObject), ...descriptor } expect(() => restore([{ ...catalog, data }])).toThrow('supported versioned catalog fact') }) + +it.each([ + { version: 3, mode: 'continuable', label: 'Named', name: 'reviewer' }, + { version: 3, mode: 'external', label: 'Named', name: 'auditor' }, + { version: 3, mode: 'external', name: 'auditor' }, +])('admits a native named catalog fact: %j', (descriptor) => { + const entry = { ...catalog, data: { ...(catalog.data as SessionFormatJsonObject), ...descriptor } } + expect(restore([entry]).events).toEqual([entry]) +}) + +it.each([ + { version: 3, mode: 'continuable', label: 'Named' }, + { version: 3, mode: 'continuable', name: 'reviewer' }, + { version: 3, mode: 'external', label: 1, name: 'auditor' }, + { version: 3, mode: 'one-shot', name: 'reviewer' }, + { version: 3, mode: 'unknown', name: 'reviewer' }, + { version: 3, mode: 'continuable', label: 'Named', name: 'reviewer', childId: 1 }, +])('rejects an invalid native named catalog fact: %j', (descriptor) => { + const data = { ...(catalog.data as SessionFormatJsonObject), ...descriptor } + expect(() => restore([{ ...catalog, data }])).toThrow('supported versioned catalog fact') +}) diff --git a/packages/subagent/subagent-acp/README.i18n.yaml b/packages/subagent/subagent-acp/README.i18n.yaml index 07c723beb8..0ca2b62add 100644 --- a/packages/subagent/subagent-acp/README.i18n.yaml +++ b/packages/subagent/subagent-acp/README.i18n.yaml @@ -69,8 +69,8 @@ en: 75d061c2151624a7 zh: 71e58e020b60ff90 /deepseek-ai-dsh-subagent-acp/model-experience/parent-tool-result-indirectly/what-the-model-sees: - en: 5e4e3efe7517f782 - zh: 0465e164583f6bc3 + en: bacf407400c5a362 + zh: 7c9e3daea29508e9 /deepseek-ai-dsh-subagent-acp/model-experience/parent-tool-result-indirectly/token-effect: en: 4fae54e2ff0c4874 zh: 1574bfe874f66a1f diff --git a/packages/subagent/subagent-acp/README.md b/packages/subagent/subagent-acp/README.md index 32f3e21df8..dcf644a3b4 100644 --- a/packages/subagent/subagent-acp/README.md +++ b/packages/subagent/subagent-acp/README.md @@ -138,7 +138,7 @@ Independent of the parent request cache. Each ACP child can reuse only prefixes #### What the model sees -Through `dsh-tool-subagent`, the parent model first receives a child id, then a completion notice containing final or partial assistant text, the stop reason, and safe diagnostic. The parent Session independently retains the external task identity and complete terminal result. Intermediate messages and tool traffic do not enter the parent Session. +Through `dsh-tool-subagent`, the parent model first receives the child's name, then a completion notice containing final or partial assistant text, the stop reason, and safe diagnostic. The parent Session independently retains the external task identity and complete terminal result. Intermediate messages and tool traffic do not enter the parent Session. #### Token effect diff --git a/packages/subagent/subagent-acp/README.zh.md b/packages/subagent/subagent-acp/README.zh.md index 288dee7afe..d7b9459809 100644 --- a/packages/subagent/subagent-acp/README.zh.md +++ b/packages/subagent/subagent-acp/README.zh.md @@ -138,7 +138,7 @@ spawn、初始化或新建会话失败会在发布前拒绝,通常先证明 ma #### 模型看到什么 -通过 `dsh-tool-subagent`,父模型先收到 child id,随后收到含最终或部分 assistant 文本、停止原因与安全诊断的完成通知。父会话独立保存外部任务身份和完整终态结果。中间消息与工具通信不进入父会话。 +通过 `dsh-tool-subagent`,父模型先收到子任务名称,随后收到含最终或部分 assistant 文本、停止原因与安全诊断的完成通知。父会话独立保存外部任务身份和完整终态结果。中间消息与工具通信不进入父会话。 #### Token 影响 diff --git a/packages/subagent/subagent-acp/tests/loader-composition.e2e.ts b/packages/subagent/subagent-acp/tests/loader-composition.e2e.ts index 7d32f04fee..79ca510bef 100644 --- a/packages/subagent/subagent-acp/tests/loader-composition.e2e.ts +++ b/packages/subagent/subagent-acp/tests/loader-composition.e2e.ts @@ -43,7 +43,7 @@ function settlementText(events: SessionEvent[]): string[] { const notice = notices[0]! if (notice.type !== 'user/message' || notice.data.source.kind !== 'subagent-settled') throw new Error('missing completion notice') expect(results[0]!.data.message.content).toEqual([ - { type: 'text', text: `started subagent ${notice.data.source.senderSessionId}` }, + { type: 'text', text: 'started subagent agent-1' }, ]) return notice.data.content.slice(1).filter(block => block.type === 'text').map(block => block.text) } diff --git a/packages/subagent/subagent-claude-code/README.i18n.yaml b/packages/subagent/subagent-claude-code/README.i18n.yaml index 8ca95c1a0f..9ae58516c8 100644 --- a/packages/subagent/subagent-claude-code/README.i18n.yaml +++ b/packages/subagent/subagent-claude-code/README.i18n.yaml @@ -24,8 +24,8 @@ en: fbee5e4ab396c184 zh: dcf94832918d5010 /deepseek-ai-dsh-subagent-claude-code/use-this-package/exposing-the-tool: - en: ebf6ba7fef393ebb - zh: fad30b829e2e4188 + en: e070599b4ff8dd79 + zh: 105e7a4dfee82efe /deepseek-ai-dsh-subagent-claude-code/use-this-package/what-you-get: en: a731eabea54893d5 zh: e370dba101b3a7e8 @@ -66,8 +66,8 @@ en: 4c391bb16e3cd380 zh: ab3847efe35fa6dc /deepseek-ai-dsh-subagent-claude-code/model-experience/parent-scheduling-and-results-indirectly/what-the-model-sees: - en: fd177d7a8aa80ff0 - zh: aa04468315ed2e0b + en: 7e75fac459085412 + zh: 4d8e41b0baada834 /deepseek-ai-dsh-subagent-claude-code/model-experience/parent-scheduling-and-results-indirectly/token-effect: en: 0c436fb0897a6eca zh: 7ec0a953d0232af2 diff --git a/packages/subagent/subagent-claude-code/README.md b/packages/subagent/subagent-claude-code/README.md index 72a1b2eb7b..43ec7ba399 100644 --- a/packages/subagent/subagent-claude-code/README.md +++ b/packages/subagent/subagent-claude-code/README.md @@ -72,7 +72,7 @@ The bundle inserts `tool-subagent-claude-code` as a Host row, so `subagent_claud maxDepth: provider-managed ``` -The tool returns a child id after accepting the task and sends its result to the parent Agent on completion. Each external activation executes once and accepts no follow-up input or resume. +The tool returns the child's name after accepting the task and sends its result to the parent Agent on completion. Each external activation executes once and accepts no follow-up input or resume. ### What you get @@ -149,7 +149,7 @@ Independent of the parent request cache. Reuse depends only on Claude Code's own #### What the model sees -Through `dsh-tool-subagent`, the parent model first receives a child id, then the final Claude Code answer or a failure notice with its stop reason and safe diagnostic. Diagnostics contain only fixed stage, category, and observed protocol or process facts. Product reasoning, intermediate messages, tool activity, stderr, usage, product identifiers, commands, paths, and raw protocol payloads are not copied into the parent Session. +Through `dsh-tool-subagent`, the parent model first receives the child's name, then the final Claude Code answer or a failure notice with its stop reason and safe diagnostic. Diagnostics contain only fixed stage, category, and observed protocol or process facts. Product reasoning, intermediate messages, tool activity, stderr, usage, product identifiers, commands, paths, and raw protocol payloads are not copied into the parent Session. #### Token effect diff --git a/packages/subagent/subagent-claude-code/README.zh.md b/packages/subagent/subagent-claude-code/README.zh.md index 1e1a99d29e..059d9fdf46 100644 --- a/packages/subagent/subagent-claude-code/README.zh.md +++ b/packages/subagent/subagent-claude-code/README.zh.md @@ -72,7 +72,7 @@ Bundle 将 `tool-subagent-claude-code` 作为 Host 条目插入,因此 `subage maxDepth: provider-managed ``` -工具接受任务后返回 child id;任务完成后向父 agent 发送结果通知。外部 activation 只执行一次,不支持追加输入或恢复对话。 +工具接受任务后返回子任务名称;任务完成后向父 agent 发送结果通知。外部 activation 只执行一次,不支持追加输入或恢复对话。 ### 你会得到什么 @@ -149,7 +149,7 @@ Claude Code 子级会在一个全新的 SDK query 中接收独立文本任务。 #### 模型看到什么 -通过 `dsh-tool-subagent`,父模型先收到 child id,随后收到 Claude Code 最终答案或带停止原因与安全诊断的失败通知。诊断只包含固定的阶段、类别和已观测的协议或进程事实。产品推理、中间消息、工具活动、stderr、用量、产品标识符、命令、路径与原始协议载荷不会复制到父 Session。 +通过 `dsh-tool-subagent`,父模型先收到子任务名称,随后收到 Claude Code 最终答案或带停止原因与安全诊断的失败通知。诊断只包含固定的阶段、类别和已观测的协议或进程事实。产品推理、中间消息、工具活动、stderr、用量、产品标识符、命令、路径与原始协议载荷不会复制到父 Session。 #### Token 影响 diff --git a/packages/subagent/subagent-codex/README.i18n.yaml b/packages/subagent/subagent-codex/README.i18n.yaml index 1b6d354c28..15609babf2 100644 --- a/packages/subagent/subagent-codex/README.i18n.yaml +++ b/packages/subagent/subagent-codex/README.i18n.yaml @@ -24,8 +24,8 @@ en: b8163849d388478e zh: cf74fcaeef62c7ae /deepseek-ai-dsh-subagent-codex/use-this-package/exposing-the-tool: - en: a36399c7ac98724d - zh: d43815e08689da6b + en: b82b4698150bb45e + zh: 40f0d852d317491e /deepseek-ai-dsh-subagent-codex/use-this-package/what-you-get: en: abd75016d4c7f61e zh: 904158c896bd21ce @@ -66,8 +66,8 @@ en: 4c391bb16e3cd380 zh: ab3847efe35fa6dc /deepseek-ai-dsh-subagent-codex/model-experience/parent-scheduling-and-results-indirectly/what-the-model-sees: - en: 9ca324fb0b034a6c - zh: 83ff78167bd31a14 + en: f481d9a59562c629 + zh: eddd8fd082a44807 /deepseek-ai-dsh-subagent-codex/model-experience/parent-scheduling-and-results-indirectly/token-effect: en: f618e05fde7c8b57 zh: 7306c4535b6fa4c0 diff --git a/packages/subagent/subagent-codex/README.md b/packages/subagent/subagent-codex/README.md index 376e6e9fe4..418bff9436 100644 --- a/packages/subagent/subagent-codex/README.md +++ b/packages/subagent/subagent-codex/README.md @@ -70,7 +70,7 @@ The bundle inserts `tool-subagent-codex` as a Host row, so `subagent_codex` is a maxDepth: provider-managed ``` -The tool returns a child id after accepting the task and sends its result to the parent Agent on completion. Each external activation executes once and accepts no follow-up input or resume. +The tool returns the child's name after accepting the task and sends its result to the parent Agent on completion. Each external activation executes once and accepts no follow-up input or resume. ### What you get @@ -147,7 +147,7 @@ Independent of the parent request cache. Reuse depends only on Codex's own provi #### What the model sees -Through `dsh-tool-subagent`, the parent model first receives a child id, then the selected Codex answer (final, partial, or unphased, in that order of precedence) or a failure notice with its stop reason and safe diagnostic. Diagnostics contain only fixed stage, category, and observed protocol or process facts. Product reasoning, intermediate messages, tool activity, stderr, usage, product identifiers, commands, paths, and raw protocol payloads are not copied into the parent Session. +Through `dsh-tool-subagent`, the parent model first receives the child's name, then the selected Codex answer (final, partial, or unphased, in that order of precedence) or a failure notice with its stop reason and safe diagnostic. Diagnostics contain only fixed stage, category, and observed protocol or process facts. Product reasoning, intermediate messages, tool activity, stderr, usage, product identifiers, commands, paths, and raw protocol payloads are not copied into the parent Session. #### Token effect diff --git a/packages/subagent/subagent-codex/README.zh.md b/packages/subagent/subagent-codex/README.zh.md index 5e488daaf8..b4419a7d0a 100644 --- a/packages/subagent/subagent-codex/README.zh.md +++ b/packages/subagent/subagent-codex/README.zh.md @@ -70,7 +70,7 @@ Bundle 将 `tool-subagent-codex` 作为 Host 条目插入,因此 `subagent_cod maxDepth: provider-managed ``` -工具接受任务后返回 child id;任务完成后向父 agent 发送结果通知。外部 activation 只执行一次,不支持追加输入或恢复对话。 +工具接受任务后返回子任务名称;任务完成后向父 agent 发送结果通知。外部 activation 只执行一次,不支持追加输入或恢复对话。 ### 你会得到什么 @@ -147,7 +147,7 @@ Codex 子级会在一个全新的临时线程中,以单个轮次接收这些 #### 模型看到什么 -通过 `dsh-tool-subagent`,父模型先收到 child id,随后收到所选的 Codex 答案(按 final、partial、未标阶段的优先次序)或带停止原因与安全诊断的失败通知。诊断只包含固定的阶段、类别和已观测的协议或进程事实。产品推理、中间消息、工具活动、stderr、用量、产品标识符、命令、路径与原始协议载荷不会复制到父 Session。 +通过 `dsh-tool-subagent`,父模型先收到子任务名称,随后收到所选的 Codex 答案(按 final、partial、未标阶段的优先次序)或带停止原因与安全诊断的失败通知。诊断只包含固定的阶段、类别和已观测的协议或进程事实。产品推理、中间消息、工具活动、stderr、用量、产品标识符、命令、路径与原始协议载荷不会复制到父 Session。 #### Token 影响 diff --git a/packages/subagent/subagent-dsh-sdk/README.i18n.yaml b/packages/subagent/subagent-dsh-sdk/README.i18n.yaml index b5e6b036c3..2157b5fe7d 100644 --- a/packages/subagent/subagent-dsh-sdk/README.i18n.yaml +++ b/packages/subagent/subagent-dsh-sdk/README.i18n.yaml @@ -69,8 +69,8 @@ en: 75d061c2151624a7 zh: 71e58e020b60ff90 /deepseek-ai-dsh-subagent-dsh-sdk/model-experience/parent-tool-result-indirectly/what-the-model-sees: - en: 5e4e3efe7517f782 - zh: 0465e164583f6bc3 + en: bacf407400c5a362 + zh: 7c9e3daea29508e9 /deepseek-ai-dsh-subagent-dsh-sdk/model-experience/parent-tool-result-indirectly/token-effect: en: 4fae54e2ff0c4874 zh: 1574bfe874f66a1f diff --git a/packages/subagent/subagent-dsh-sdk/README.md b/packages/subagent/subagent-dsh-sdk/README.md index 080b49be6f..a0ccfe8faf 100644 --- a/packages/subagent/subagent-dsh-sdk/README.md +++ b/packages/subagent/subagent-dsh-sdk/README.md @@ -151,7 +151,7 @@ Independent of the parent request cache. Each SDK child can reuse only prefixes #### What the model sees -Through `dsh-tool-subagent`, the parent model first receives a child id, then a completion notice containing final or partial assistant text, the stop reason, and safe diagnostic. The parent Session independently retains the external task identity and complete terminal result. Intermediate messages and tool traffic do not enter the parent Session. +Through `dsh-tool-subagent`, the parent model first receives the child's name, then a completion notice containing final or partial assistant text, the stop reason, and safe diagnostic. The parent Session independently retains the external task identity and complete terminal result. Intermediate messages and tool traffic do not enter the parent Session. #### Token effect diff --git a/packages/subagent/subagent-dsh-sdk/README.zh.md b/packages/subagent/subagent-dsh-sdk/README.zh.md index ebab95c57f..b719ba9a8e 100644 --- a/packages/subagent/subagent-dsh-sdk/README.zh.md +++ b/packages/subagent/subagent-dsh-sdk/README.zh.md @@ -151,7 +151,7 @@ kind: "package-reference" #### 模型看到什么 -通过 `dsh-tool-subagent`,父模型先收到 child id,随后收到含最终或部分 assistant 文本、停止原因与安全诊断的完成通知。父会话独立保存外部任务身份和完整终态结果。中间消息与工具通信不进入父会话。 +通过 `dsh-tool-subagent`,父模型先收到子任务名称,随后收到含最终或部分 assistant 文本、停止原因与安全诊断的完成通知。父会话独立保存外部任务身份和完整终态结果。中间消息与工具通信不进入父会话。 #### Token 影响 diff --git a/packages/subagent/subagent-dsh-sdk/tests/loader-composition.e2e.ts b/packages/subagent/subagent-dsh-sdk/tests/loader-composition.e2e.ts index 3e8b4c2b4d..1f6a10b535 100644 --- a/packages/subagent/subagent-dsh-sdk/tests/loader-composition.e2e.ts +++ b/packages/subagent/subagent-dsh-sdk/tests/loader-composition.e2e.ts @@ -47,7 +47,7 @@ function settlementText(events: SessionEvent[]): string[] { const notice = notices[0]! if (notice.type !== 'user/message' || notice.data.source.kind !== 'subagent-settled') throw new Error('missing completion notice') expect(results[0]!.data.message.content).toEqual([ - { type: 'text', text: `started subagent ${notice.data.source.senderSessionId}` }, + { type: 'text', text: 'started subagent agent-1' }, ]) return notice.data.content.slice(1).filter(block => block.type === 'text').map(block => block.text) } diff --git a/packages/subagent/subagent-fork-in-process/README.i18n.yaml b/packages/subagent/subagent-fork-in-process/README.i18n.yaml index a7a3c2e9d0..f33da46d9b 100644 --- a/packages/subagent/subagent-fork-in-process/README.i18n.yaml +++ b/packages/subagent/subagent-fork-in-process/README.i18n.yaml @@ -27,8 +27,8 @@ en: d6058c358890c64c zh: 93f8cc2b60975d42 /deepseek-ai-dsh-subagent-fork-in-process/use-this-package/what-a-fork-delegation-does: - en: 461063deda821a1b - zh: 18d2d41d64884f22 + en: 69aa80db5f3aff0c + zh: 9b3b77ea42a6f29c /deepseek-ai-dsh-subagent-fork-in-process/understand-the-implementation: en: 6a0fe4b9cc780aaa zh: 50cd69ceb8296827 @@ -42,8 +42,8 @@ en: cca92ec964b4314b zh: edd8ecd4b59dbe8f /deepseek-ai-dsh-subagent-fork-in-process/understand-the-implementation/lifecycle-binding: - en: 8dad0155ed1b32d2 - zh: f4d82d5ede03c4d7 + en: b4dee8f57a537f0a + zh: 0c759154a69de3ba /deepseek-ai-dsh-subagent-fork-in-process/further-exploration: en: 29ea6e0669b665a8 zh: 46d435e79d356898 @@ -66,8 +66,8 @@ en: 75d061c2151624a7 zh: 71e58e020b60ff90 /deepseek-ai-dsh-subagent-fork-in-process/model-experience/parent-tool-result-indirectly/what-the-model-sees: - en: 54d10ba4da6cdece - zh: bb9cb4d685098483 + en: 7452223ab8b13d47 + zh: f315ec4db035c652 /deepseek-ai-dsh-subagent-fork-in-process/model-experience/parent-tool-result-indirectly/token-effect: en: 8c81bab3c5d6a38a zh: 44c798432a4bfdfb diff --git a/packages/subagent/subagent-fork-in-process/README.md b/packages/subagent/subagent-fork-in-process/README.md index aad91df474..556653b007 100644 --- a/packages/subagent/subagent-fork-in-process/README.md +++ b/packages/subagent/subagent-fork-in-process/README.md @@ -55,7 +55,7 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a ### What a fork delegation does -One tool call creates a background child and returns its session id immediately. The child works in its own session; the parent receives its final answer in a completion notice. The child can also send messages through `send_message`. A rejected start leaves no published child. Programmatic callers such as workflows can await the activation result directly. +One tool call creates a background child and returns its name immediately. The child works in its own session; the parent receives its final answer in a completion notice. The child can also send messages through `send_message`. A rejected start leaves no published child. Programmatic callers such as workflows can await the activation result directly. ----- @@ -83,7 +83,7 @@ This backend supplies the balanced completed-turn prefix of the parent log as th ### Lifecycle binding -The delegation tool creates a background activation and returns a stable child session id. Parent and child share the same messaging tool definitions and ordering; the parent id and return guidance follow inherited history in the initial task. Workflows await results with caller delivery and send no completion notice to the parent. +The delegation tool creates a background activation and returns the child's name. Parent and child share the same messaging tool definitions and ordering; the parent's name in identity runtime context and the return guidance in the initial task both follow inherited history. Workflows await results with caller delivery and send no completion notice to the parent. @@ -123,7 +123,7 @@ The child may reuse the inherited byte-identical prefix under the same provider #### What the model sees -Through `dsh-tool-subagent`, the parent first receives the child session id, then a completion notice with its final answer. Child-authored messages arrive through `send_message`; the inherited prefix and internal work remain in the child session. +Through `dsh-tool-subagent`, the parent first receives the child's name, then a completion notice with its final answer. Child-authored messages arrive through `send_message`; the inherited prefix and internal work remain in the child session. #### Token effect diff --git a/packages/subagent/subagent-fork-in-process/README.zh.md b/packages/subagent/subagent-fork-in-process/README.zh.md index 6d8d7ac2c7..7d232a1efb 100644 --- a/packages/subagent/subagent-fork-in-process/README.zh.md +++ b/packages/subagent/subagent-fork-in-process/README.zh.md @@ -55,7 +55,7 @@ kind: "package-reference" ### 一次 fork 委派会做什么 -一次工具调用创建后台子 agent 并立即返回子会话 ID。子级在自己的会话中工作,父级通过完成通知收到其最终答案。子级也可以通过 `send_message` 发送消息。被拒绝的启动不会留下已发布的子 agent;工作流等程序调用方可以直接等待 activation 的结果。 +一次工具调用创建后台子 agent 并立即返回其名称。子级在自己的会话中工作,父级通过完成通知收到其最终答案。子级也可以通过 `send_message` 发送消息。被拒绝的启动不会留下已发布的子 agent;工作流等程序调用方可以直接等待 activation 的结果。 ----- @@ -83,7 +83,7 @@ kind: "package-reference" ### 生命周期绑定 -委派工具以后台 activation 创建子级,并返回稳定的子会话 ID。父级与子级共享相同的消息工具定义及顺序;父级 ID 与返回指导位于继承历史之后的初始任务中。工作流以调用方收集模式等待结果,不向父级发送完成通知。 +委派工具以后台 activation 创建子级,并返回子级名称。父级与子级共享相同的消息工具定义及顺序;身份运行时上下文中的父级名称和初始任务中的返回指导都位于继承历史之后。工作流以调用方收集模式等待结果,不向父级发送完成通知。 @@ -123,7 +123,7 @@ fork 会把保留的已完成历史复制到子 agent 的请求中,子 agent #### 模型看到什么 -父级通过 `dsh-tool-subagent` 先收到子会话 ID,随后收到包含最终答案的完成通知。子级自行编写的消息通过 `send_message` 到达;继承的前缀与内部工作保留在子会话中。 +父级通过 `dsh-tool-subagent` 先收到子级名称,随后收到包含最终答案的完成通知。子级自行编写的消息通过 `send_message` 到达;继承的前缀与内部工作保留在子会话中。 #### Token 影响 diff --git a/packages/subagent/subagent-spawn-in-process/README.i18n.yaml b/packages/subagent/subagent-spawn-in-process/README.i18n.yaml index 0ee0a2191a..cd2ac2ed30 100644 --- a/packages/subagent/subagent-spawn-in-process/README.i18n.yaml +++ b/packages/subagent/subagent-spawn-in-process/README.i18n.yaml @@ -24,8 +24,8 @@ en: 3f53b0d5f4037171 zh: e0f2ac931cfbef95 /deepseek-ai-dsh-subagent-spawn-in-process/use-this-package/what-a-delegation-does: - en: 0c90404ed53cf451 - zh: 161e38b7d7c10182 + en: 3990b19cb5c58c39 + zh: 3c974311a437a84f /deepseek-ai-dsh-subagent-spawn-in-process/understand-the-implementation: en: 7f34637751939818 zh: 2937553168e1aee5 @@ -63,8 +63,8 @@ en: 75d061c2151624a7 zh: 71e58e020b60ff90 /deepseek-ai-dsh-subagent-spawn-in-process/model-experience/parent-tool-result-indirectly/what-the-model-sees: - en: caf11edef2516b8c - zh: 25bcb16cceaad06d + en: b540f44830cec85f + zh: 7a02564ac5fbc9cf /deepseek-ai-dsh-subagent-spawn-in-process/model-experience/parent-tool-result-indirectly/token-effect: en: ff8045aa0f466c2a zh: e38fc5c6af2ce076 diff --git a/packages/subagent/subagent-spawn-in-process/README.md b/packages/subagent/subagent-spawn-in-process/README.md index bd5a08b848..c81fb63901 100644 --- a/packages/subagent/subagent-spawn-in-process/README.md +++ b/packages/subagent/subagent-spawn-in-process/README.md @@ -51,7 +51,7 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a ### What a delegation does -One tool call creates a background child and returns its session id immediately. The child works in its own session; the parent receives its final answer in a completion notice. The child can also send messages through `send_message`. A rejected start leaves no published child. Programmatic callers such as workflows can await the activation result directly. +One tool call creates a background child and returns its name immediately. The child works in its own session; the parent receives its final answer in a completion notice. The child can also send messages through `send_message`. A rejected start leaves no published child. Programmatic callers such as workflows can await the activation result directly. An explicit `agentPreset` selects the currently registered preset independently; omission joins the parent's active revision. Only spawn supports this choice. Persona and tools belong in the [preset definition](../../preset/agent-preset-registry/README.md). Cold resume follows the shared [child composition rules](../subagent/README.md#restoring-child-composition). @@ -120,7 +120,7 @@ The child's request cache is independent of the parent's. Child history grows ap #### What the model sees -Through `dsh-tool-subagent`, the parent first receives the child session id, then a completion notice with its final answer. Child-authored messages arrive through `send_message`. Internal child tool calls remain in the child session. +Through `dsh-tool-subagent`, the parent first receives the child's name, then a completion notice with its final answer. Child-authored messages arrive through `send_message`. Internal child tool calls remain in the child session. #### Token effect diff --git a/packages/subagent/subagent-spawn-in-process/README.zh.md b/packages/subagent/subagent-spawn-in-process/README.zh.md index 5133d18837..b60b16cab2 100644 --- a/packages/subagent/subagent-spawn-in-process/README.zh.md +++ b/packages/subagent/subagent-spawn-in-process/README.zh.md @@ -51,7 +51,7 @@ kind: "package-reference" ### 一次委派会做什么 -一次工具调用创建后台子 agent 并立即返回子会话 ID。子级在自己的会话中工作,父级通过完成通知收到其最终答案。子级也可以通过 `send_message` 发送消息。被拒绝的启动不会留下已发布的子 agent;工作流等程序调用方可以直接等待 activation 的结果。 +一次工具调用创建后台子 agent 并立即返回其名称。子级在自己的会话中工作,父级通过完成通知收到其最终答案。子级也可以通过 `send_message` 发送消息。被拒绝的启动不会留下已发布的子 agent;工作流等程序调用方可以直接等待 activation 的结果。 显式 `agentPreset` 独立选择当前注册的 preset;省略时加入父方的活跃代际。只有 spawn 支持此选择。persona 与工具放在 [preset 定义](../../preset/agent-preset-registry/README.zh.md)中。冷恢复遵守共享的[子级组装规则](../subagent/README.zh.md#restoring-child-composition)。 @@ -120,7 +120,7 @@ kind: "package-reference" #### 模型看到什么 -通过 `dsh-tool-subagent`,父级先收到子会话 ID,随后收到包含最终答案的完成通知。子级自行编写的消息通过 `send_message` 到达。子级内部工具调用保留在子会话中。 +通过 `dsh-tool-subagent`,父级先收到子级名称,随后收到包含最终答案的完成通知。子级自行编写的消息通过 `send_message` 到达。子级内部工具调用保留在子会话中。 #### Token 影响 diff --git a/packages/subagent/subagent/README.i18n.yaml b/packages/subagent/subagent/README.i18n.yaml index 106360686c..96d19d384f 100644 --- a/packages/subagent/subagent/README.i18n.yaml +++ b/packages/subagent/subagent/README.i18n.yaml @@ -18,8 +18,8 @@ en: 924b8c15f9529028 zh: 9fb0c399b5a79e3e /deepseek-ai-dsh-subagent/use-this-package/enabling-delegation: - en: 025605eddae02478 - zh: 5659cba4340735d8 + en: c0718955687ff245 + zh: eb2d3cdbe92e0fe2 /deepseek-ai-dsh-subagent/use-this-package/delegation-settings: en: 86c982223fab1a3a zh: 3d220d8ed84eab9a @@ -27,8 +27,11 @@ en: 4b40bd380bea81ce zh: 896a584190981168 /deepseek-ai-dsh-subagent/use-this-package/local-and-external-children: - en: 191ef1dabe2fce7c - zh: f799fdcbb5c5a171 + en: d6eef66bc1d293fe + zh: e4754d38212c0b2e +/deepseek-ai-dsh-subagent/use-this-package/agent-names: + en: 3b0d77c9080687ed + zh: 77284193cb776195 /deepseek-ai-dsh-subagent/use-this-package/child-presets: en: a740e5e9bdd661d9 zh: 712a5458fc8b3615 @@ -51,8 +54,8 @@ en: 61cddba23f4e7d1c zh: 052c8331a747403f /deepseek-ai-dsh-subagent/understand-the-implementation/activation-settlement: - en: fc8938de925b1a97 - zh: b50e1f948d60b93c + en: bd7d6f3a7b76161b + zh: c49705e83bab984e /deepseek-ai-dsh-subagent/understand-the-implementation/restoring-child-composition: en: 843f9f4319ca2ae3 zh: 218bf355816d4ff6 @@ -69,11 +72,11 @@ en: f7cec22ba9d3fb4f zh: 67b1d40f9b136f71 /deepseek-ai-dsh-subagent/model-experience/settlement-notice/what-the-model-sees: - en: d5e6377fabfee1ca - zh: f5c442401264a60d + en: 45d8a22bad10fba2 + zh: 0ebd7f4570150713 /deepseek-ai-dsh-subagent/model-experience/settlement-notice/token-effect: - en: f98ad834e8ce8950 - zh: cf509805dd934094 + en: 435e801179ee02c8 + zh: 72551f95c5ee7bca /deepseek-ai-dsh-subagent/model-experience/settlement-notice/kv-cache-effect: en: d6b1d618083da79b zh: 9a28942b7d53c382 @@ -108,8 +111,8 @@ en: 59e848fca82a46fb zh: fcd6a8a2bb58609f /deepseek-ai-dsh-subagent/known-limitations-and-deferred-work: - en: 746c75e25114ab8b - zh: 168d577f1fcc4281 + en: fb3ae9c914da17ff + zh: 18d2fe7caadc9c70 /deepseek-ai-dsh-subagent/known-limitations-and-deferred-work/dev-note: en: 25cc0909348666bb zh: 95517f12de100049 diff --git a/packages/subagent/subagent/README.md b/packages/subagent/subagent/README.md index 4803cff356..25f7942a48 100644 --- a/packages/subagent/subagent/README.md +++ b/packages/subagent/subagent/README.md @@ -42,7 +42,7 @@ Mount the service with `@deepseek-ai/dsh-session-projection`, a provider, and th toolName: subagent ``` -The tool returns a child session id immediately. The parent receives a completion notice when the background activation settles. Mounting the service alone enables no delegation. +The tool returns the child's name immediately. The parent receives a completion notice when the background activation settles. Mounting the service alone enables no delegation. ### Delegation settings @@ -58,10 +58,17 @@ At capacity, creation or cold resume rejects with `ACTIVATION_LIMIT_REACHED` (br ### Local and external children -`startActivation({ provider, label, prompt, parent, signal, delivery })` returns `{ childId, messageId?, result, dispose }`. Supplying `childId` selects a local backend and guarantees `messageId`. Local children accept later messages and can cold-resume; external children expose one execution and reject continuation. `agentOptions` overrides supported child model settings. `delivery: 'parent'` supplies the model-facing completion notice; `delivery: 'caller'` lets workflows await the result without a parent notice or initial return guidance. Local parent delivery requires persistence; caller delivery can use an ephemeral local session. Cold resume requires persistence and Session query. +`startActivation({ provider, label, name?, prompt, parent, signal, delivery })` returns `{ childId, name, messageId?, result, dispose }`. Supplying `childId` selects a local backend and guarantees `messageId`. Local children accept later messages and can cold-resume; external children expose one execution and reject continuation. `agentOptions` overrides supported child model settings. `delivery: 'parent'` supplies the model-facing completion notice; `delivery: 'caller'` lets workflows await the result without a parent notice or initial return guidance. Local parent delivery requires persistence; caller delivery can use an ephemeral local session. Cold resume requires persistence and Session query. `rootOf(agent)` resolves a managed local Agent through its existing Activation and checks the exact live child and root instances. Intermediate creators can leave without changing that root; same-id replacements cannot inherit membership. Ordinary live Agents resolve to themselves, while unmanaged delegated Agents are rejected. The query does not activate Sessions. + +### Agent names + +Every cataloged child has a name that is unique among its creator's children. Pass `name` to choose it: 1–32 lower-case letters, digits, or hyphens, starting with a letter, other than `lead`. Omission selects the first free `agent-N` (`agent-1`, `agent-2`, …) among the names in the creator's catalog and the names held by creations still starting. A name that a sibling already holds rejects with `DUPLICATE_AGENT_NAME`, and a name that breaks the rule rejects with `INVALID_AGENT_NAME`; both reject before startup, and the runtime never substitutes another name. The creator reserves the name synchronously at admission and releases the reservation when the catalog entry commits or startup fails. Caller-delivered external runs enter no catalog: they reject a requested name with `UNSUPPORTED_CAPABILITY`, and their activation name is their Session id. + +The root is always `lead`. A descendant's full name is the slash-separated path of names from the root without a `lead/` prefix, such as `reviewer` or `reviewer/checker`; sibling-unique segments make full names unique in the tree. `SubagentActivation.name` and the `path` field of `listDescendants()` rows carry the full name. A child whose catalog entry has no name, and every descendant below it, uses its Session id as its name. `namesOf(agent)` reports a live member's name and its creator's name. `resolveName(caller, name, signal)` returns the Session id for `lead` or a full name, reading only the catalogs along the path, and returns any other text unchanged as a Session id. Resolution grants no authority: messaging, interruption, and task assignment authorize the resolved Session id. + ### Child presets In-process spawn accepts an independent `agentPreset`; fork and external backends reject this option. Without a selection, a local child joins the parent's current preset revision. An explicit id mounts the currently available revision, even when that id matches the parent's. Configure model choices through [`dsh-tool-subagent`](../tool-subagent/README.md#selecting-a-child-preset) and persona and tools in the [preset composition](../../preset/agent-preset-registry/README.md). Cold resume preserves persona and tool restrictions recorded in historical child descriptors. @@ -123,7 +130,7 @@ Settlement attribution records the exact execution stop reason without altering A local Agent failure without a subsequently committed `turn/end` settles as `error` independently of its descendants. Unclaimed input cannot keep that failed activation resident. A later committed turn ending supersedes the live failure and supplies the recovered outcome. -Successful local creation appends a `subagent/catalog` fact to the parent Session regardless of result delivery. External creation appends a catalog fact only with `delivery: 'parent'`; caller-owned external executions retain their caller's membership records. External entries use `mode: 'external'` to identify children without a local Session. Catalog entries record membership once at creation and carry no execution status. Complete results go to the caller or the parent completion notice. Projections exclude inherited facts. Direct lists read one parent projection, so completed local workflow children remain discoverable and can be opened or continued. Catalog payload v0 records local modes; v1 also accepts unknown mode; v2 records external executions. Readers support all three versions. Historical migration appends a v1 `subagent/catalog` from a readable child header when its descriptor is unavailable; local creation retains v0. Its `mode: 'unknown'` projection keeps the child visible without claiming continuation support; an existing complete entry remains authoritative. +Successful local creation appends a `subagent/catalog` fact to the parent Session regardless of result delivery. External creation appends a catalog fact only with `delivery: 'parent'`; caller-owned external executions retain their caller's membership records. External entries use `mode: 'external'` to identify children without a local Session. Catalog entries record membership once at creation and carry no execution status. Complete results go to the caller or the parent completion notice. Projections exclude inherited facts. Direct lists read one parent projection, so completed local workflow children remain discoverable and can be opened or continued. Catalog payload v0 records local modes; v1 also accepts unknown mode; v2 records external executions; v3 adds the child's name to continuable and external entries. Local and parent-delivered external creation write v3, and readers support all four versions. The `subagentCatalog` projection state version is 7, so caches without names rebuild from Session events. Historical migration appends a v1 `subagent/catalog` from a readable child header when its descriptor is unavailable. Its `mode: 'unknown'` projection keeps the child visible without claiming continuation support; an existing complete entry remains authoritative. ### Restoring child composition @@ -167,11 +174,11 @@ Read these pages when the package-level contract is not enough. They move from t #### What the model sees -Parent delivery sends a user-role status notice to the resident direct parent in the same tree, including an idle parent. It does not restore an offline parent. The notice contains the child’s nonempty final text blocks. When the child's last action before that text was a message to its parent, with no input claimed and no tool call logged after it, the notice replaces them with `It sent you a message just before finishing, so its closing message is omitted.`; the parent recovers omitted closing text only by messaging the child again. External notices also state that further messages are unsupported. Reasoning and other nontext blocks are excluded. Every parent notice includes available structured output as `Structured result: ` and the provider’s safe diagnostic as a separate text block. Empty closing text is reported as `It left no closing message.` Caller delivery emits no notice; SDK lifecycle notifications retain complete child output when cleanup succeeds. +Parent delivery sends a user-role status notice to the resident direct parent in the same tree, including an idle parent. It does not restore an offline parent. Its opening line names the child by its full name, such as `Background subagent reviewer finished and will do no further work unless you send it more.` The notice contains the child’s nonempty final text blocks. When the child's last action before that text was a message to its parent, with no input claimed and no tool call logged after it, the notice replaces them with `It sent you a message just before finishing, so its closing message is omitted.`; the parent recovers omitted closing text only by messaging the child again. External notices also state that further messages are unsupported. Reasoning and other nontext blocks are excluded. Every parent notice includes available structured output as `Structured result: ` and the provider’s safe diagnostic as a separate text block. Empty closing text is reported as `It left no closing message.` Caller delivery emits no notice; SDK lifecycle notifications retain complete child output when cleanup succeeds. #### Token effect -One notice per settled activation using parent delivery. Caller delivery adds no completion message; its consumer owns result presentation. +One notice per settled activation using parent delivery; a default name such as `agent-1` is shorter than the 36-character Session id that names a child without a recorded name. Caller delivery adds no completion message; its consumer owns result presentation. #### KV Cache effect @@ -225,8 +232,9 @@ Prefix-stable within a child: the statement never changes during the child's lif These limits define when the seam is a poor fit or needs special operational care. They are current package constraints, not a general delegation comparison or a task backlog. - **Descendant reads are sequential** — each reachable catalog, including one-shot children, takes one observation. A cold Session without a valid prepared observation requires a full-log read; large cold trees can accumulate storage latency. +- **Names are permanent** — a name stays taken among its creator's children for as long as the catalog entry exists; names cannot be changed or reused, and no migration names children whose catalog entries predate names. - **External children execute once** — ACP, DSH SDK, Codex, and Claude Code have no local child Session and accept no follow-up input. Their parent-owned records remain discoverable after disposal. -- **Messaging stays within one tree** — targets use Session ids and must be the root or continuable members. Only the restored target consumes activation capacity. Browser prompts retain their separate human Queue-or-Steer control path. +- **Messaging stays within one tree** — targets must be the root or continuable members. Only the restored target consumes activation capacity. Browser prompts retain their separate human Queue-or-Steer control path. - **Wake gap during cancellation convergence** — a follow-up accepted after an interrupt signal but before the Agent becomes idle stays queued until another waking send. - **Pending injected context retains an Activation** — settlement conservatively treats every Inbox occurrence as unfinished. Context parked after the Agent becomes idle keeps the child and its live ancestors resident until a waking delivery claims it, a queue mutation removes it, or manager teardown discards it. Host completion joins active work, pending child creation, and resource cleanup, but does not wait for a subtree containing only idle parked inboxes; it neither completes those activation results nor discards their messages. - **Process-local residency** — the Activation inbox and ownership graph do not coordinate two harness processes; concurrent access to one persistence store needs a durable mailbox and cross-process lease protocol. diff --git a/packages/subagent/subagent/README.zh.md b/packages/subagent/subagent/README.zh.md index c73f19f5ba..660ea662a7 100644 --- a/packages/subagent/subagent/README.zh.md +++ b/packages/subagent/subagent/README.zh.md @@ -42,7 +42,7 @@ kind: "package-reference" toolName: subagent ``` -工具立即返回子会话 ID,后台 activation 结算后父级收到完成通知。仅挂载服务本身不会启用委派。 +工具立即返回子级的名称,后台 activation 结算后父级收到完成通知。仅挂载服务本身不会启用委派。 ### 委派设置 @@ -58,10 +58,17 @@ kind: "package-reference" ### 本地与外部子级 -`startActivation({ provider, label, prompt, parent, signal, delivery })` 返回 `{ childId, messageId?, result, dispose }`。提供 `childId` 会选择本地后端,并保证返回 `messageId`。本地子级接受后续消息并支持冷恢复;外部子级只暴露一次执行,拒绝继续输入。`agentOptions` 覆盖后端支持的子级模型设置。`delivery: 'parent'` 提供面向模型的完成通知;`delivery: 'caller'` 让工作流等待结果,不发送父级通知,也不附加初始返回指导。本地父级投递需要持久化,调用方投递可以使用临时本地会话;冷恢复需要持久化与 Session 查询服务。 +`startActivation({ provider, label, name?, prompt, parent, signal, delivery })` 返回 `{ childId, name, messageId?, result, dispose }`。提供 `childId` 会选择本地后端,并保证返回 `messageId`。本地子级接受后续消息并支持冷恢复;外部子级只暴露一次执行,拒绝继续输入。`agentOptions` 覆盖后端支持的子级模型设置。`delivery: 'parent'` 提供面向模型的完成通知;`delivery: 'caller'` 让工作流等待结果,不发送父级通知,也不附加初始返回指导。本地父级投递需要持久化,调用方投递可以使用临时本地会话;冷恢复需要持久化与 Session 查询服务。 `rootOf(agent)` 通过托管本地 Agent 已有的 Activation 解析树根,并校验子级和树根的确切存活实例。中间创建者退出不改变该树根;同 id 替代实例不能继承成员身份。普通存活 Agent 返回自身,非托管的委派 Agent 会被拒绝。查询不会激活 Session。 + +### Agent 名称 + +每个进入目录的子级都有一个名称,在其创建者的子级之间唯一。传入 `name` 可选择名称:1–32 个小写字母、数字或连字符,以字母开头,且不能是 `lead`。省略时,运行时在创建者目录中的名称以及仍在启动的创建所持有的名称之外,选择第一个空闲的 `agent-N`(`agent-1`、`agent-2`……)。请求同级已持有的名称会以 `DUPLICATE_AGENT_NAME` 拒绝,不符合规则的名称会以 `INVALID_AGENT_NAME` 拒绝;两者都在启动前拒绝,运行时绝不替换为其他名称。创建者在准入时同步预留名称,并在目录条目提交或启动失败时释放预留。调用方投递的外部执行不进入目录:请求名称时以 `UNSUPPORTED_CAPABILITY` 拒绝,其 activation 名称为其 Session id。 + +根的名称始终为 `lead`。后代的完整名称是从根开始、以斜杠分隔的名称路径,不带 `lead/` 前缀,例如 `reviewer` 或 `reviewer/checker`;同级唯一的各段使完整名称在整棵树中唯一。`SubagentActivation.name` 和 `listDescendants()` 行的 `path` 字段携带完整名称。目录条目没有名称的子级及其下所有后代使用 Session id 作为名称。`namesOf(agent)` 报告在线成员及其创建者的名称。`resolveName(caller, name, signal)` 为 `lead` 或完整名称返回 Session id,只读取路径上的目录;其他文本原样作为 Session id 返回。解析不授予权限:消息、中断和任务分配会对解析出的 Session id 进行授权。 + ### 子级 preset 进程内 spawn 接受独立的 `agentPreset`;fork 与外部后端拒绝此选项。省略选择时,本地子级加入父方当前的 preset 代际。显式 id 挂载当前可用代际,即使该 id 与父方相同。通过 [`dsh-tool-subagent`](../tool-subagent/README.zh.md#selecting-a-child-preset) 配置模型可选项,并在 [preset 组装](../../preset/agent-preset-registry/README.zh.md)中配置 persona 与工具。冷恢复保留历史子级 descriptor 记录的 persona 和工具限制。 @@ -123,7 +130,7 @@ kind: "package-reference" 本地 Agent 发生失败且后续没有成功提交 `turn/end` 时,会独立于后代以 `error` 结算。未认领的输入不会使该失败 activation 一直驻留。后续成功提交的轮次结束记录会取代实时失败,提供恢复后的结果。 -本地创建成功后,无论结果交付给谁,都会向父 Session 追加 `subagent/catalog` 事实。外部创建仅在使用 `delivery: 'parent'` 时追加目录事实;调用方拥有的外部执行保留调用方自己的成员记录。外部条目以 `mode: 'external'` 标记没有本地 Session 的子级。目录仅在创建时记录一次成员关系,不携带执行状态。完整结果交给调用方或父级完成通知。投影排除继承事实。直接列表读取一个父级投影,因此已完成的本地工作流子级仍可被发现、打开或继续。Catalog 载荷 v0 记录本地模式,v1 还接受未知模式,v2 记录外部执行,读取器支持三版。历史迁移在 descriptor 不可用时根据可读子 header 追加 v1 `subagent/catalog`;本地创建保留 v0。其 `mode: 'unknown'` 投影让子会话保持可见,但不表示支持继续执行;已有完整条目仍具有权威性。 +本地创建成功后,无论结果交付给谁,都会向父 Session 追加 `subagent/catalog` 事实。外部创建仅在使用 `delivery: 'parent'` 时追加目录事实;调用方拥有的外部执行保留调用方自己的成员记录。外部条目以 `mode: 'external'` 标记没有本地 Session 的子级。目录仅在创建时记录一次成员关系,不携带执行状态。完整结果交给调用方或父级完成通知。投影排除继承事实。直接列表读取一个父级投影,因此已完成的本地工作流子级仍可被发现、打开或继续。Catalog 载荷 v0 记录本地模式,v1 还接受未知模式,v2 记录外部执行,v3 为可继续和外部条目增加子级名称。本地创建和父级投递的外部创建写入 v3,读取器支持全部四个版本。`subagentCatalog` 投影状态版本为 7,因此不含名称的缓存会从 Session 事件重建。历史迁移在 descriptor 不可用时根据可读子 header 追加 v1 `subagent/catalog`。其 `mode: 'unknown'` 投影让子会话保持可见,但不表示支持继续执行;已有完整条目仍具有权威性。 ### 恢复子级组装 @@ -167,11 +174,11 @@ kind: "package-reference" #### 模型看到什么 -父级投递向同树中驻留的直接父级发送 user-role 状态通知,包括空闲父级。它不会恢复离线父级。通知包含子级最终输出中的非空文本块。若子级在该文本之前的最后一个动作是向父级发送消息,且此后既未领取输入也未记录任何工具调用,通知会以 `It sent you a message just before finishing, so its closing message is omitted.` 替代这些文本块;父级只有再次向子级发送消息才能取回被省略的收尾文本。外部通知还说明不支持后续消息。推理及其他非文本块被排除。每条父级通知都会以 `Structured result: ` 附带可用的结构化输出,并以独立文本块附带提供方安全诊断。收尾文本为空时,通知使用 `It left no closing message.`。调用方投递不发送通知;清理成功时 SDK 生命周期通知保留完整子级输出。 +父级投递向同树中驻留的直接父级发送 user-role 状态通知,包括空闲父级。它不会恢复离线父级。首行使用子级的完整名称,例如 `Background subagent reviewer finished and will do no further work unless you send it more.`。通知包含子级最终输出中的非空文本块。若子级在该文本之前的最后一个动作是向父级发送消息,且此后既未领取输入也未记录任何工具调用,通知会以 `It sent you a message just before finishing, so its closing message is omitted.` 替代这些文本块;父级只有再次向子级发送消息才能取回被省略的收尾文本。外部通知还说明不支持后续消息。推理及其他非文本块被排除。每条父级通知都会以 `Structured result: ` 附带可用的结构化输出,并以独立文本块附带提供方安全诊断。收尾文本为空时,通知使用 `It left no closing message.`。调用方投递不发送通知;清理成功时 SDK 生命周期通知保留完整子级输出。 #### Token 影响 -父级投递为每个结算的 activation 添加一条通知。调用方投递不添加完成消息,由其消费者负责展示结果。 +父级投递为每个结算的 activation 添加一条通知;`agent-1` 等默认名称比没有记录名称的子级所用的 36 字符 Session id 更短。调用方投递不添加完成消息,由其消费者负责展示结果。 #### KV Cache 影响 @@ -225,8 +232,9 @@ You are a delegated subagent: your permission scope was fixed when you were star 这些限制说明该 seam 何时不合适,或何时需要特别的运维注意。它们是当前包约束,不是通用委派对比或任务积压。 - **后代读取串行执行**——每个可达目录(包括一次性子级)都需要一次观察。冷 Session 缺少有效的 prepared 观察时需要读取完整日志;大型冷会话树可能累积存储延迟。 +- **名称不可变更**——只要目录条目存在,名称就在创建者的子级之间保持占用;名称不能修改或复用,也没有迁移为早于名称机制的目录条目补充名称。 - **外部子级执行一次**——ACP、DSH SDK、Codex 与 Claude Code 没有本地子 Session,也不接受后续输入。释放后仍可发现父级拥有的执行记录。 -- **消息限定在一棵树内**——目标使用 Session id,必须是根或可继续成员。仅恢复的目标占用激活容量。浏览器提示词保留独立的人类 Queue 或 Steer 控制路径。 +- **消息限定在一棵树内**——目标必须是根或可继续成员。仅恢复的目标占用激活容量。浏览器提示词保留独立的人类 Queue 或 Steer 控制路径。 - **取消收敛期间存在唤醒缺口**——中断信号发出后、Agent 进入 idle 前被接受的后续消息会保持排队,直到另一条唤醒发送到达。 - **待处理的注入上下文会保留 Activation**——settlement 会保守地把每个 Inbox occurrence 都视为未完成。Agent 进入 idle 后停放的上下文会让 child 及其在线祖先继续驻留,直到唤醒投递将其 claim、queue 变更将其移除,或 manager teardown 将其丢弃。宿主完成检查等待活动工作、待创建子级与资源清理,但不会等待仅包含空闲停放 Inbox 的子树;它既不结算这些 activation 的结果,也不丢弃其消息。 - **驻留仅限进程内**——Activation inbox 与所有权图不会在两个 harness 进程之间协调;对单个持久化存储的并发访问需要持久化邮箱与跨进程租约协议。 diff --git a/packages/subagent/subagent/src/activation.ts b/packages/subagent/subagent/src/activation.ts index fb9fd56b51..b6d7ce6aff 100644 --- a/packages/subagent/subagent/src/activation.ts +++ b/packages/subagent/subagent/src/activation.ts @@ -51,6 +51,8 @@ interface ActivationBase { readonly root: Agent /** Verified creation ancestors, from subagent tree root to direct creator. */ readonly ancestors: readonly SessionId[] + /** Slash-separated names from the root; undefined when this child or a creator below the root has no name. */ + readonly path: string | undefined readonly delivery: 'parent' | 'caller' readonly result: PromiseWithResolvers /** Published synchronously before teardown; subsequent callers share this transaction. */ diff --git a/packages/subagent/subagent/src/catalog.ts b/packages/subagent/subagent/src/catalog.ts index 416625c594..4c8c3b96c3 100644 --- a/packages/subagent/subagent/src/catalog.ts +++ b/packages/subagent/subagent/src/catalog.ts @@ -15,16 +15,20 @@ import type { SessionLogOffset, } from '@deepseek-ai/dsh-session' import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection' +import { isAgentName } from './names.ts' import type { SubagentCatalogEntry } from './projection-types.ts' -/** Catalog payload version emitted by local child creation. */ -export const SUBAGENT_CATALOG_VERSION = 0 +/** Catalog payload version emitted by local and external child creation. */ +export const SUBAGENT_CATALOG_VERSION = 3 type KnownCatalogMode = | { readonly mode: 'one-shot'; readonly label?: string } | { readonly mode: 'continuable'; readonly label: string } -/** Parent catalog v0 records local modes, v1 adds unknown children, and v2 records external executions. */ +/** + * Parent catalog v0 records local modes, v1 adds unknown children, v2 records + * external executions, and v3 adds each child's name, unique among its siblings. + */ export type SubagentCatalogEvent = & { readonly childId: SessionId @@ -33,6 +37,10 @@ export type SubagentCatalogEvent = | ({ readonly version: 0 } & KnownCatalogMode) | ({ readonly version: 1 } & (KnownCatalogMode | { readonly mode: 'unknown'; readonly label?: string })) | { readonly version: 2; readonly mode: 'external'; readonly label?: string } + | ({ readonly version: 3; readonly name: string } & ( + | { readonly mode: 'continuable'; readonly label: string } + | { readonly mode: 'external'; readonly label?: string } + )) ) declare module '@deepseek-ai/dsh-session/types' { @@ -68,11 +76,16 @@ const continuableCatalogSchema = z.object({ }).strict() const externalCatalogSchema = oneShotCatalogSchema.extend({ version: z.literal(2), mode: z.literal('external') }) const unknownCatalogSchema = oneShotCatalogSchema.extend({ version: z.literal(1), mode: z.literal('unknown') }) +const nameSchema = z.string().refine(isAgentName, 'invalid agent name') +const namedContinuableCatalogSchema = continuableCatalogSchema.extend({ version: z.literal(3), name: nameSchema }) +const namedExternalCatalogSchema = externalCatalogSchema.extend({ version: z.literal(3), name: nameSchema }) const eventDataSchema = z.union([ oneShotCatalogSchema, externalCatalogSchema, continuableCatalogSchema, unknownCatalogSchema, + namedContinuableCatalogSchema, + namedExternalCatalogSchema, ]) as z.ZodType const viewSchema = z.array(z.union([ oneShotCatalogSchema.omit({ version: true, childId: true, childCreatedAt: true }).extend({ @@ -82,10 +95,12 @@ const viewSchema = z.array(z.union([ externalCatalogSchema.omit({ version: true, childId: true, childCreatedAt: true }).extend({ id: sessionIdSchema, createdAt: externalCatalogSchema.shape.childCreatedAt, + name: nameSchema.optional(), }), continuableCatalogSchema.omit({ version: true, childId: true, childCreatedAt: true }).extend({ id: sessionIdSchema, createdAt: continuableCatalogSchema.shape.childCreatedAt, + name: nameSchema.optional(), }), unknownCatalogSchema.omit({ version: true, childId: true, childCreatedAt: true }).extend({ id: sessionIdSchema, @@ -120,7 +135,7 @@ export const subagentCatalogProjectionDefinition = { if (event.type !== 'subagent/catalog' || event.seq < state.inheritedEventCount) return state return { ...state, head: appendChunkedList(state.head, eventDataSchema.parse(event.data)) } }, - stateVersion: 6, + stateVersion: 7, wire: { viewSchema, view: subagentCatalogEntries }, } satisfies ProjectionDefinition<'subagentCatalog', SubagentCatalogState> @@ -129,11 +144,13 @@ export const subagentCatalogProjectionDefinition = { * @param parent - durable direct parent receiving the discovery fact. * @param child - established child's immutable Session metadata. * @param descriptor - mode-discriminated creation label frozen with the child. + * @param name - the child's name, reserved unique among the parent's children. */ export function establishCatalogChild( parent: Session, child: SessionHeader, descriptor: { readonly mode: 'continuable'; readonly label: string }, + name: string, ): void { parent.append('subagent/catalog', { version: SUBAGENT_CATALOG_VERSION, @@ -141,24 +158,37 @@ export function establishCatalogChild( childCreatedAt: child.createdAt, mode: descriptor.mode, label: descriptor.label, + name, }) } -/** Catalog payload version emitted by external execution creation. */ -const EXTERNAL_SUBAGENT_CATALOG_VERSION = 2 - /** * Record an external execution without a local child Session. * @param parent - durable direct parent receiving the discovery fact. * @param childId - identity returned by the external provider. * @param label - display label assigned by the caller. + * @param name - the child's name, reserved unique among the parent's children. */ -export function establishExternalCatalogChild(parent: Session, childId: SessionId, label: string): void { +export function establishExternalCatalogChild(parent: Session, childId: SessionId, label: string, name: string): void { parent.append('subagent/catalog', { - version: EXTERNAL_SUBAGENT_CATALOG_VERSION, + version: SUBAGENT_CATALOG_VERSION, childId, childCreatedAt: Date.now(), mode: 'external', label, + name, }) } + +/** + * Collect the names recorded in one parent's catalog. + * @param entries - the parent's current catalog rows. + * @returns names of every named child; historical entries contribute none. + */ +export function catalogNames(entries: readonly SubagentCatalogEntry[]): Set { + const names = new Set() + for (const entry of entries) { + if ('name' in entry) names.add(entry.name) + } + return names +} diff --git a/packages/subagent/subagent/src/continuation-messages.ts b/packages/subagent/subagent/src/continuation-messages.ts index 8b97212a59..00d507c854 100644 --- a/packages/subagent/subagent/src/continuation-messages.ts +++ b/packages/subagent/subagent/src/continuation-messages.ts @@ -57,16 +57,18 @@ function agentMessageSource(sender: Agent): AgentMessageSource { /** * Build the model-visible and durable representation of one Agent message. * @param sender - exact live Agent that authored the message. + * @param senderName - the sender's model-facing name in its subagent tree. * @param content - model-visible message blocks supplied by the sender. * @returns the durable user-message representation delivered to the recipient. */ export function createAgentMessage( sender: Agent, + senderName: string, content: ContentBlock[], ): ReturnType { return createUserMessage({ content: [ - { type: 'text' as const, text: `Agent ${sender.id} sent a message: ` }, + { type: 'text' as const, text: `Agent ${senderName} sent a message: ` }, ...content, ], source: agentMessageSource(sender), @@ -96,13 +98,13 @@ export function withResultDeliveryGuidance( /** * One line telling a parent that a background child is finished and why, in * the parent's own task vocabulary. - * @param childId - the durable child the parent knows by id. + * @param childName - the model-facing name the parent knows the child by. * @param stopReason - how the child's last ordinary turn ended. * @param continuable - whether the child accepts a later message. * @returns the model-facing opening line of the settlement notice. */ -function settlementSummary(childId: SessionId, stopReason: SubagentResult['stopReason'], continuable: boolean): string { - const subject = `Background subagent ${childId}` +function settlementSummary(childName: string, stopReason: SubagentResult['stopReason'], continuable: boolean): string { + const subject = `Background subagent ${childName}` switch (stopReason) { case 'completed': return continuable @@ -128,7 +130,8 @@ function settlementSummary(childId: SessionId, stopReason: SubagentResult['stopR /** * Build the runtime-owned settlement notice from the child's nonempty closing text. - * @param childId - durable child session id named in the notice. + * @param childId - durable child session id recorded as the sender. + * @param childName - model-facing child name used in the notice text. * @param terminal - recorded terminal state for the settled Activation. * @param continuable - whether the child can accept another task. * @param omitClosingText - replace the closing text when the child's last action @@ -137,11 +140,12 @@ function settlementSummary(childId: SessionId, stopReason: SubagentResult['stopR */ export function createSettlementMessage( childId: SessionId, + childName: string, terminal: SubagentResult, continuable = true, omitClosingText = false, ): ReturnType { - const summary = settlementSummary(childId, terminal.stopReason, continuable) + const summary = settlementSummary(childName, terminal.stopReason, continuable) // Parent providers receive this notice as a user message and may reject // nontext assistant blocks. Keep this conversion local so SDK/UI consumers // retain the complete child output. diff --git a/packages/subagent/subagent/src/index.ts b/packages/subagent/subagent/src/index.ts index d79e12bb1d..6329fd5d05 100644 --- a/packages/subagent/subagent/src/index.ts +++ b/packages/subagent/subagent/src/index.ts @@ -33,6 +33,7 @@ import { Context, Service } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import type {} from '@deepseek-ai/dsh-working-directory' import type {} from '@deepseek-ai/dsh-attachment' +import { brandString } from '@deepseek-ai/dsh-brand' import { scopeTarget } from '@deepseek-ai/dsh-scope' import type { Scoped } from '@deepseek-ai/dsh-scope' import { assertObjectJsonSchema } from '@deepseek-ai/dsh-tools' @@ -55,6 +56,7 @@ import type { SubagentActivation, SubagentActivationSpec, SubagentInterruptAuthority, + SubagentMemberNames, SubagentProvider, SubagentRunEndInfo, SubagentRunInfo, @@ -66,16 +68,18 @@ import { assertSubagentMaxDepth } from './depth.ts' import { createLifecycleEmitter } from './lifecycle.ts' import type { LifecycleEmitter } from './lifecycle.ts' import SubagentManager from './manager.ts' -import { listDescendants as listSubagentDescendants } from './list-children.ts' +import { listDescendants as listSubagentDescendants, resolveAgentPath } from './list-children.ts' import type { SubagentDescendantListEntry } from './list-children.ts' import { subagentIdentityProjectionDefinition, subagentTimingProjectionDefinition } from './projection.ts' import { subagentCatalogProjectionDefinition } from './catalog.ts' +import { ROOT_AGENT_NAME } from './names.ts' export type {} from './catalog.ts' export type {} from './projection.ts' export * from './out-of-process.ts' export { AssistantOutputFold, finalAssistantOutput } from './assistant-output.ts' export { SubagentRunId } from './types.ts' +export { ROOT_AGENT_NAME, isAgentName } from './names.ts' export type { ContinuableCreateRequest, ContinuableCreateSpec, @@ -83,6 +87,7 @@ export type { SubagentActivation, SubagentActivationSpec, SubagentInterruptAuthority, + SubagentMemberNames, SubagentProvider, SubagentResult, SubagentRun, @@ -208,13 +213,14 @@ export class SubagentRuntime extends TypertRemoteService { protected async [Service.init](): Promise { const ctx = this.ctx - await ctx.sessionProjections.register(subagentCatalogProjectionDefinition) + const catalogProjection = await ctx.sessionProjections.register(subagentCatalogProjectionDefinition) await ctx.sessionProjections.register(subagentTimingProjectionDefinition) const identityProjection = await ctx.sessionProjections.register(subagentIdentityProjectionDefinition) if (ctx.fiber.uid === null) return ctx.inject(['agents'], (childCtx: Context) => { const manager = new SubagentManager(childCtx, { identityOf: session => identityProjection.stateOf(session).identity, + catalogOf: session => subagentCatalogProjectionDefinition.wire.view(catalogProjection.stateOf(session)), emitLifecycle: this.emitLifecycle, }, () => this.config.maxActiveSubagents.get()) this.manager = manager @@ -295,6 +301,35 @@ export class SubagentRuntime extends TypertRemoteService { return this.requireManager().rootOf(caller) } + /** + * Report the model-facing names of a live tree member and its direct creator. + * The root is `lead`; a descendant is the slash-separated path of names from + * the root, or its Session id when it or a creator below the root has no name. + * @param agent - exact live root or resident tree member. + * @returns the member's name and, for a descendant, its creator's name. + * @throws when the caller or its runtime owner is unavailable. + */ + namesOf(agent: Agent): SubagentMemberNames { + return this.requireManager().namesOf(agent) + } + + /** + * Resolve a model-supplied agent reference within the caller's tree. `lead` + * selects the root; a slash-separated path is looked up through the catalogs + * along it; any other text is returned as a Session id. Resolution grants no + * authority: messaging, interruption, and assignment authorize the result. + * @param caller - exact live Agent whose tree root anchors the lookup. + * @param name - `lead`, a full name from the root, or a Session id. + * @param signal - cancellation forwarded to each catalog read. + * @returns the referenced Session id. + * @throws when the caller's root is unavailable or a catalog along the path cannot be read. + */ + async resolveName(caller: Agent, name: string, signal: AbortSignal): Promise { + const root = this.rootOf(caller) + if (name === ROOT_AGENT_NAME) return root.id + return await resolveAgentPath(this.ctx, root.id, name, signal) ?? brandString(name) + } + /** * Join progressing subagent tree work for its root. Idle members with parked input * stay resident without delaying host completion. Ordinary members return false. diff --git a/packages/subagent/subagent/src/list-children.ts b/packages/subagent/subagent/src/list-children.ts index 0a737b98d7..ebad16f5e2 100644 --- a/packages/subagent/subagent/src/list-children.ts +++ b/packages/subagent/subagent/src/list-children.ts @@ -8,6 +8,7 @@ import type { Context } from '@deepseek-ai/cordis' import type { SessionId } from '@deepseek-ai/dsh-session' import type { SubagentListEntry } from './control-types.ts' import { SubagentError } from './error.ts' +import { isAgentName, joinAgentPath } from './names.ts' import type { SubagentCatalogEntry } from './projection-types.ts' export type { SubagentListEntry } from './control-types.ts' @@ -18,6 +19,8 @@ export type SubagentDescendantListEntry = SubagentListEntry & { readonly parentId: SessionId /** Edge distance from the requested root; direct children are `1`. */ readonly depth: number + /** Slash-separated names from the requested root; absent when this child or an ancestor below the root has no name. */ + readonly path?: string } /** @@ -85,15 +88,18 @@ export async function listDescendants( assertListingNotCancelled(signal) return children } - const stack = (await readChildren(rootSessionId)) - .map(entry => ({ entry, parentId: rootSessionId, depth: 1 })) + type Position = { entry: SubagentCatalogEntry; parentId: SessionId; depth: number; parentPath: string | undefined } + const stack: Position[] = (await readChildren(rootSessionId)) + .map(entry => ({ entry, parentId: rootSessionId, depth: 1, parentPath: '' })) .reverse() const visited = new Set([rootSessionId]) const result: SubagentDescendantListEntry[] = [] for (let position = stack.pop(); position !== undefined; position = stack.pop()) { - const { entry, parentId, depth } = position + const { entry, parentId, depth, parentPath } = position if (visited.has(entry.id)) continue visited.add(entry.id) + const path = joinAgentPath(parentPath, 'name' in entry ? entry.name : undefined) + const at = { parentId, depth, ...path === undefined ? {} : { path } } let children: SubagentCatalogEntry[] try { children = entry.mode === 'external' ? [] : await readChildren(entry.id) @@ -101,30 +107,57 @@ export async function listDescendants( if (error instanceof SubagentError) throw error const code = error instanceof Error && 'code' in error ? error.code : undefined result.push({ - kind: 'diagnostic', id: entry.id, parentId, depth, + kind: 'diagnostic', id: entry.id, ...at, reason: code === 'SESSION_QUERY_CORRUPT_SESSION' || code === 'SESSION_QUERY_SOURCE_CONFLICT' ? 'corrupt' : 'unavailable', }) continue } if (entry.mode === 'unknown') { - result.push({ kind: 'diagnostic', id: entry.id, parentId, depth, reason: 'unsupported' }) + result.push({ kind: 'diagnostic', id: entry.id, ...at, reason: 'unsupported' }) } else { const { createdAt: _createdAt, ...identity } = entry result.push({ - ...identity, kind: 'child', parentId, depth, + ...identity, kind: 'child', ...at, activity: sessions.get(entry.id) === undefined ? 'inactive' : 'running', hasChildren: children.length > 0, }) } // Catalog event order defines siblings; the stack visits the first one next. for (const child of [...children].reverse()) { - stack.push({ entry: child, parentId: entry.id, depth: depth + 1 }) + stack.push({ entry: child, parentId: entry.id, depth: depth + 1, parentPath: path }) } } return result } +/** + * Resolve a slash-separated name by reading only the catalogs along its path. + * @param ctx - context carrying the Session query service. + * @param rootSessionId - tree root whose catalog holds the first name. + * @param name - candidate full name relative to the root. + * @param signal - cancellation forwarded to each catalog read. + * @returns the named member's Session id, or undefined when the text is not a name in this tree. + * @throws {@link SubagentError} when a catalog dependency is unavailable; Session read failures propagate. + */ +export async function resolveAgentPath( + ctx: Context, + rootSessionId: SessionId, + name: string, + signal: AbortSignal, +): Promise { + const segments = name.split('/') + if (!segments.every(isAgentName)) return undefined + let current = rootSessionId + for (const segment of segments) { + const children = await listChildren(ctx, current, signal) + const child = children.find(entry => 'name' in entry && entry.name === segment) + if (child === undefined) return undefined + current = child.id + } + return current +} + /** Stop the complete traversal when its caller cancels. */ function assertListingNotCancelled(signal: AbortSignal | undefined): void { if (signal?.aborted) { diff --git a/packages/subagent/subagent/src/manager.ts b/packages/subagent/subagent/src/manager.ts index dbd9d3a382..db6f04b019 100644 --- a/packages/subagent/subagent/src/manager.ts +++ b/packages/subagent/subagent/src/manager.ts @@ -30,14 +30,16 @@ import { import { installSubagentArchiveAdmission } from './archive-admission.ts' import { foldSubagentDescriptor, snapshotSubagentDescriptor } from './descriptor.ts' import type { SubagentDescriptorData } from './descriptor.ts' -import { establishCatalogChild, establishExternalCatalogChild } from './catalog.ts' +import { catalogNames, establishCatalogChild, establishExternalCatalogChild } from './catalog.ts' import { SubagentError } from './error.ts' import type { LifecycleEmitter } from './lifecycle.ts' -import type { SubagentIdentityProjection } from './projection-types.ts' +import { ROOT_AGENT_NAME, SiblingNameReservations, agentAddress, joinAgentPath, parentAgentPath } from './names.ts' +import type { SiblingNameReservation } from './names.ts' +import type { SubagentCatalogEntry, SubagentIdentityProjection } from './projection-types.ts' import { SubagentRunId } from './types.ts' import type { ContinuableCreateRequest, ContinuableCreateSpec, SubagentInterruptAuthority, - SubagentActivationSpec, SubagentActivation, SubagentStartRequest, SubagentRun, + SubagentActivationSpec, SubagentActivation, SubagentMemberNames, SubagentStartRequest, SubagentRun, SubagentSendMessageOptions, SubagentResult, SubagentRunInfo, ResolvedSubagentStartRequest, } from './types.ts' import { attachStructuredRuntime } from './structured.ts' @@ -61,6 +63,8 @@ type ChildDeliveryOptions = { interface SubagentHost { /** Read the current descriptor identity through the service-owned projection. */ identityOf(session: Session): SubagentIdentityProjection | undefined + /** Read a live Session's direct-child catalog through the service-owned projection. */ + catalogOf(session: Session): readonly SubagentCatalogEntry[] /** Publish lifecycle events through the owning service's scoped dispatcher. */ readonly emitLifecycle: LifecycleEmitter } @@ -70,6 +74,8 @@ interface MaterializeBase { provider: string root: Agent ancestors: readonly SessionId[] + /** Slash-separated names from the root, undefined when any link is unnamed. */ + path: string | undefined signal: AbortSignal delivery?: 'parent' | 'caller' | undefined } @@ -143,6 +149,8 @@ export class SubagentManager { private readonly ownerCtx: Context /** Exact subagent tree roots whose host teardown has closed admission. */ private readonly closingRoots = new Set() + /** Sibling names held by creations that have not committed their catalog entry. */ + private readonly names = new SiblingNameReservations() private draining = false /** @@ -298,6 +306,24 @@ export class SubagentManager { const parent = spec.parent this.assertAdmitting(parent) const ancestry = this.ancestryForChild(parent) + const reservation = this.reserveName(parent, spec.name) + const path = joinAgentPath(this.pathOf(parent, ancestry.root), reservation.name) + try { + return await this.startLocalNamed(spec, prepare, ancestry, reservation, path) + } finally { + reservation.release() + } + } + + /** Create one local child under a name reserved before any await. */ + private startLocalNamed( + spec: SubagentActivationSpec, + prepare: (request: ContinuableCreateRequest) => Promise, + ancestry: Pick, + reservation: SiblingNameReservation, + path: string | undefined, + ): Promise { + const parent = spec.parent return this.trackStartup(ancestry.root, spec.signal, async (signal) => { const persistence = spec.delivery === 'caller' ? this.ctx.get('sessionPersistence') : this.requirePersistence() const childId = spec.childId ?? brandString(randomUUID()) @@ -352,6 +378,7 @@ export class SubagentManager { childId, provider: spec.provider, ...ancestry, + path, create: { parent, cwd, @@ -379,7 +406,7 @@ export class SubagentManager { ancestry.root, () => { this.assertAdmitting(parent) - establishCatalogChild(parent.session, childHeader, descriptor) + establishCatalogChild(parent.session, childHeader, descriptor, reservation.name) }, ) }) @@ -397,10 +424,32 @@ export class SubagentManager { spec: SubagentActivationSpec, start: (request: ResolvedSubagentStartRequest) => Promise, ): Promise { - const { provider, delivery, childId: _childId, ...request } = spec - const parent = request.parent + const { delivery, name: requestedName, parent } = spec this.assertAdmitting(parent) const ancestry = this.ancestryForChild(parent) + if (delivery === 'caller' && requestedName !== undefined) { + throw new SubagentError('agent names require a cataloged child; caller-delivered external runs are not cataloged', 'UNSUPPORTED_CAPABILITY') + } + // Only parent-delivered external runs enter the catalog, so only they hold a name. + const reservation = delivery === 'parent' ? this.reserveName(parent, requestedName) : undefined + const path = joinAgentPath(this.pathOf(parent, ancestry.root), reservation?.name) + try { + return await this.startExternalNamed(spec, start, ancestry, reservation, path) + } finally { + reservation?.release() + } + } + + /** Start one external execution, cataloging parent-delivered runs under their reserved name. */ + private startExternalNamed( + spec: SubagentActivationSpec, + start: (request: ResolvedSubagentStartRequest) => Promise, + ancestry: Pick, + reservation: SiblingNameReservation | undefined, + path: string | undefined, + ): Promise { + const { provider, delivery, childId: _childId, name: _name, ...request } = spec + const parent = request.parent return this.trackStartup(ancestry.root, spec.signal, async (signal) => { let activation: Activation | undefined try { @@ -409,6 +458,7 @@ export class SubagentManager { kind: 'external', provider, ...ancestry, + path, signal, delivery, start: signal => start({ @@ -417,8 +467,8 @@ export class SubagentManager { }) this.assertAdmitting(parent) signal.throwIfAborted() - if (delivery === 'parent') { - establishExternalCatalogChild(parent.session, activation.childId, spec.label) + if (reservation !== undefined) { + establishExternalCatalogChild(parent.session, activation.childId, spec.label, reservation.name) } this.accept(activation) return this.receipt(activation) @@ -472,6 +522,34 @@ export class SubagentManager { return { root, ancestors: [...ancestors, creator.id] } } + /** Claim a sibling name against the creator's committed catalog and in-flight siblings. */ + private reserveName(parent: Agent, requested: string | undefined): SiblingNameReservation { + return this.names.reserve(parent, catalogNames(this.host.catalogOf(parent.session)), requested) + } + + /** Path of an exact live tree member: `''` for the root, undefined for unnamed history. */ + private pathOf(member: Agent, root: Agent): string | undefined { + return member === root ? '' : this.resident.get(member.id)?.path + } + + /** + * Report the model-facing names of a live member and its direct creator. + * @param agent - exact live root or resident tree member. + * @returns `lead` for the root, otherwise the member's and creator's names or Session ids. + * @throws when the caller has no active runtime owner. + */ + namesOf(agent: Agent): SubagentMemberNames { + const root = this.rootOf(agent) + if (root === agent) return { self: ROOT_AGENT_NAME } + // rootOf returns another root only for the caller's own resident Activation. + const activation = this.resident.get(agent.id) as Activation + const parentId = activation.ancestors.at(-1) as SessionId + const parentPath = activation.path === undefined + ? parentId === root.id ? '' : this.resident.get(parentId)?.path + : parentAgentPath(activation.path) + return { self: agentAddress(activation.path, agent.id), parent: agentAddress(parentPath, parentId) } + } + /** * Deliver to another agent in the subagent tree, restoring only the target when absent. * @param sender - exact live root or resident continuable Agent. @@ -485,7 +563,7 @@ export class SubagentManager { const root = this.rootOf(sender) if (sender.id === targetId) throw new SubagentError('send_message requires another agent in the subagent tree', 'UNAUTHORIZED') this.assertAdmitting(sender) - const message = createAgentMessage(sender, content) + const message = createAgentMessage(sender, agentAddress(this.pathOf(sender, root), sender.id), content) const activation = this.resident.get(sender.id) return this.deliverToMember(root, targetId, [...message.content], { signal: options.signal, delivery: 'steer', source: message.source, @@ -499,10 +577,14 @@ export class SubagentManager { } /** Resolve the live owner through verified creation edges without activating ancestors. */ - private async resolveCreationAncestry(source: SessionObservation, signal: AbortSignal, root?: Agent): Promise> { + private async resolveCreationAncestry( + source: SessionObservation, signal: AbortSignal, root?: Agent, + ): Promise> { const ancestors: SessionId[] = [] + const names: (string | undefined)[] = [] const seen = new Set([source.header.id]) let { header, projections, inheritedEventCount } = source + const pathFromRoot = (): string | undefined => names.reduce(joinAgentPath, '') while (true) { signal.throwIfAborted() const parentId = header.parentSession @@ -511,11 +593,13 @@ export class SubagentManager { if (seen.has(parentId)) throw new SubagentError('cyclic subagent creation ancestry', 'UNAUTHORIZED') seen.add(parentId) using parent = await this.observeContinuation(parentId, signal) - if (!parent.projections?.values.subagentCatalog?.some(entry => entry.id === header.id)) { + const entry = parent.projections?.values.subagentCatalog?.find(entry => entry.id === header.id) + if (entry === undefined) { throw new SubagentError('subagent is absent from its creator catalog', 'UNAUTHORIZED') } ancestors.unshift(parentId) - if (parentId === root?.id) return { root, ancestors } + names.unshift('name' in entry ? entry.name : undefined) + if (parentId === root?.id) return { root, ancestors, path: pathFromRoot() } ;({ header, projections, inheritedEventCount } = parent) } if (root === undefined && ancestors.length > 0 && header.origin !== 'subagent') { @@ -523,7 +607,7 @@ export class SubagentManager { if (owner === undefined || this.rootOf(owner) !== owner) { throw new SubagentError('subagent tree root is not live', 'CONTINUATION_UNAVAILABLE') } - return { root: owner, ancestors } + return { root: owner, ancestors, path: pathFromRoot() } } throw new SubagentError(`agent "${source.header.id}" is not a member of this subagent tree`, 'UNAUTHORIZED') } @@ -793,6 +877,7 @@ export class SubagentManager { ...resource, root, ancestors: inputs.ancestors, + path: inputs.path, delivery: inputs.delivery ?? 'parent', result: Promise.withResolvers(), closing: undefined, @@ -1028,7 +1113,8 @@ export class SubagentManager { const parent = this.ctx.agents.get(parentId) if (parent === undefined || this.rootOf(parent) !== activation.root) return const message = createSettlementMessage( - activation.childId, terminal, activation.kind === 'local', activation.kind === 'local' && activation.omitClosingText, + activation.childId, agentAddress(activation.path, activation.childId), terminal, + activation.kind === 'local', activation.kind === 'local' && activation.omitClosingText, ) if (this.closingTeardownFor(parent) !== undefined) { parent.inject(message) @@ -1059,6 +1145,7 @@ export class SubagentManager { private receipt(activation: Activation): SubagentActivation { return { childId: activation.childId, + name: agentAddress(activation.path, activation.childId), result: activation.result.promise, dispose: () => this.dispose(activation), } @@ -1125,7 +1212,7 @@ export class SubagentManager { 'NOT_RESUMABLE', ) } - const { ancestors } = await this.resolveCreationAncestry(source, signal, root) + const { ancestors, path } = await this.resolveCreationAncestry(source, signal, root) let activation: Activation try { const projectedPreset = source.projections?.values.agentPreset @@ -1139,6 +1226,7 @@ export class SubagentManager { provider: descriptor.provider, root, ancestors, + path, agentOptions: { ...descriptor.agentProvider !== undefined ? { provider: descriptor.agentProvider } : {}, ...descriptor.agentModel !== undefined ? { model: descriptor.agentModel } : {}, diff --git a/packages/subagent/subagent/src/names.ts b/packages/subagent/subagent/src/names.ts new file mode 100644 index 0000000000..e5fd8ee8c7 --- /dev/null +++ b/packages/subagent/subagent/src/names.ts @@ -0,0 +1,112 @@ +/** + * Model-facing agent names. Each child's name is unique among its creator's + * children and is recorded in the creator's catalog; a member's full name is + * the slash-separated path of names from the root, whose own name is `lead`. + * + * @module @deepseek-ai/dsh-subagent/names + */ + +import type { Agent } from '@deepseek-ai/dsh-agent' +import { SubagentError } from './error.ts' + +/** The root agent's fixed name; no child may take it. */ +export const ROOT_AGENT_NAME = 'lead' + +/** One lower-case segment of at most 32 characters, so a 36-character UUID never matches. */ +const NAME_PATTERN = /^[a-z][a-z0-9-]{0,31}$/u + +/** + * Whether a string is a valid child name. + * @param value - candidate name segment. + * @returns true for 1–32 lower-case letters, digits, or hyphens starting with a letter, other than `lead`. + */ +export function isAgentName(value: string): boolean { + return NAME_PATTERN.test(value) && value !== ROOT_AGENT_NAME +} + +/** + * Append a child's name to its creator's path. The root's path is the empty string. + * @param parentPath - the creator's path, or undefined when an ancestor below the root has no name. + * @param name - the child's catalog name, absent on historical entries. + * @returns the child's path, or undefined when any link is unnamed. + */ +export function joinAgentPath(parentPath: string | undefined, name: string | undefined): string | undefined { + if (parentPath === undefined || name === undefined) return undefined + return parentPath === '' ? name : `${parentPath}/${name}` +} + +/** + * Select the model-facing name of one tree member. + * @param path - the member's path; `''` for the root and undefined for unnamed history. + * @param id - the member's Session id, used when no complete path exists. + * @returns `lead`, the full path, or the Session id. + */ +export function agentAddress(path: string | undefined, id: string): string { + if (path === '') return ROOT_AGENT_NAME + return path ?? id +} + +/** + * Remove the last name from a descendant path. + * @param path - a non-empty descendant path. + * @returns the creator's path; `''` when the creator is the root. + */ +export function parentAgentPath(path: string): string { + const separator = path.lastIndexOf('/') + return separator < 0 ? '' : path.slice(0, separator) +} + +/** A sibling name held from admission until the creator's catalog commits or startup fails. */ +export interface SiblingNameReservation { + readonly name: string + /** Return the name to the pool; the creating operation calls this exactly once. */ + release(): void +} + +/** Names claimed by in-flight creations, keyed by the exact live creator. */ +export class SiblingNameReservations { + private readonly pending = new Map>() + + /** + * Claim a name synchronously before startup awaits, so concurrent siblings cannot share it. + * @param parent - exact live creator whose catalog receives the name. + * @param committed - names already recorded in the creator's catalog. + * @param requested - requested name; omission selects the first free `agent-N`. + * @returns the claimed name and its release. + * @throws {@link SubagentError} `INVALID_AGENT_NAME` or `DUPLICATE_AGENT_NAME`. + */ + reserve(parent: Agent, committed: ReadonlySet, requested?: string): SiblingNameReservation { + const pending = this.pending.get(parent) ?? new Set() + const taken = (name: string): boolean => committed.has(name) || pending.has(name) + let name: string + if (requested === undefined) { + let ordinal = 1 + while (taken(`agent-${ordinal}`)) ordinal++ + name = `agent-${ordinal}` + } else { + if (!isAgentName(requested)) { + throw new SubagentError( + `agent name ${JSON.stringify(requested)} is invalid; use 1-32 lower-case letters, digits, or hyphens ` + + `starting with a letter, other than "${ROOT_AGENT_NAME}"`, + 'INVALID_AGENT_NAME', + ) + } + if (taken(requested)) { + throw new SubagentError( + `agent name ${JSON.stringify(requested)} is already used by another subagent you created; choose another name`, + 'DUPLICATE_AGENT_NAME', + ) + } + name = requested + } + pending.add(name) + this.pending.set(parent, pending) + return { + name, + release: () => { + pending.delete(name) + if (pending.size === 0) this.pending.delete(parent) + }, + } + } +} diff --git a/packages/subagent/subagent/src/projection-types.ts b/packages/subagent/subagent/src/projection-types.ts index c30f739475..36ed39b8e1 100644 --- a/packages/subagent/subagent/src/projection-types.ts +++ b/packages/subagent/subagent/src/projection-types.ts @@ -6,7 +6,7 @@ import type { SessionId, SessionSeq } from '@deepseek-ai/dsh-session/types' -/** Known child modes and their creation labels, shared by catalog views. */ +/** Known child modes with their creation labels and names, shared by catalog views. */ export type KnownSubagentCatalogFields = | { /** A terminal one-shot child. */ @@ -19,12 +19,16 @@ export type KnownSubagentCatalogFields = readonly mode: 'external' /** Optional durable creation label from the parent catalog. */ readonly label?: string + /** Name unique among the parent's children; absent on historical entries. */ + readonly name?: string } | { /** A resumable conversation. */ readonly mode: 'continuable' /** Durable creation label from the parent catalog. */ readonly label: string + /** Name unique among the parent's children; absent on historical entries. */ + readonly name?: string } /** One current direct-child discovery row materialized from parent facts. */ diff --git a/packages/subagent/subagent/src/types.ts b/packages/subagent/subagent/src/types.ts index 5c4ab63e7b..1786ed8e2e 100644 --- a/packages/subagent/subagent/src/types.ts +++ b/packages/subagent/subagent/src/types.ts @@ -31,6 +31,12 @@ export interface SubagentActivationSpec extends Omit } +/** Model-facing names of one live tree member and its direct creator. */ +export interface SubagentMemberNames { + /** `lead` for the root, the slash-separated path for a named descendant, otherwise the Session id. */ + readonly self: string + /** The direct creator's name by the same rule; absent for the root. */ + readonly parent?: string +} + /** * Authority under which one interrupt request is admitted. `user` is a trusted * host operation; Remote callers need permission on the target Session before diff --git a/packages/subagent/subagent/tests/catalog.spec.ts b/packages/subagent/subagent/tests/catalog.spec.ts index ac576639fa..ce801744dc 100644 --- a/packages/subagent/subagent/tests/catalog.spec.ts +++ b/packages/subagent/subagent/tests/catalog.spec.ts @@ -11,6 +11,7 @@ import { } from '@deepseek-ai/dsh-session' import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session' import { + catalogNames, establishExternalCatalogChild, subagentCatalogProjectionDefinition, } from '../src/catalog.ts' @@ -58,12 +59,12 @@ describe('subagent catalog projection', () => { await ctx.plugin(SessionStore) const parent = ctx.sessions.create(header.id) const before = Date.now() - establishExternalCatalogChild(parent, SessionId('external'), 'External work') + establishExternalCatalogChild(parent, SessionId('external'), 'External work', 'agent-1') const events = parent.snapshotEvents() expect(events).toHaveLength(1) expect(events[0]).toMatchObject({ type: 'subagent/catalog', - data: { version: 2, childId: 'external', mode: 'external', label: 'External work' }, + data: { version: 3, childId: 'external', mode: 'external', label: 'External work', name: 'agent-1' }, }) const entries = subagentCatalogProjectionDefinition.wire.view(fold(events)) expect(entries).toHaveLength(1) @@ -191,6 +192,13 @@ describe('subagent catalog projection', () => { { version: 9, childId: 'child', childCreatedAt: 0, mode: 'one-shot' }, { version: 0, childId: 'child', childCreatedAt: 0, mode: 'continuable' }, { version: 0, childId: 'child', childCreatedAt: -1, mode: 'one-shot' }, + { version: 3, childId: 'child', childCreatedAt: 0, mode: 'continuable', label: 'child' }, + { version: 3, childId: 'child', childCreatedAt: 0, mode: 'continuable', label: 'child', name: 'lead' }, + { version: 3, childId: 'child', childCreatedAt: 0, mode: 'continuable', label: 'child', name: 'Reviewer' }, + { version: 3, childId: 'child', childCreatedAt: 0, mode: 'one-shot', name: 'reviewer' }, + { version: 3, childId: 'child', childCreatedAt: 0, mode: 'unknown', name: 'reviewer' }, + { version: 2, childId: 'child', childCreatedAt: 0, mode: 'external', name: 'reviewer' }, + { version: 0, childId: 'child', childCreatedAt: 0, mode: 'continuable', label: 'child', name: 'reviewer' }, ])('refuses to restore a catalog containing an invalid fact: %j', async (data) => { const ctx = new Context() try { @@ -232,3 +240,26 @@ describe('external catalog membership', () => { expect(subagentCatalogProjectionDefinition.wire.viewSchema.safeParse([{ id: 'external', createdAt: 1, mode: 'external', external: true }]).success).toBe(false) }) }) + +describe('named catalog membership', () => { + it('restores version-3 names beside unnamed historical children', () => { + const named: SessionEvent<'subagent/catalog'>[] = [ + { type: 'subagent/catalog', seq: SessionSeq(1), time: 0, + data: { version: 3, childId: SessionId('reviewer'), childCreatedAt: 2, mode: 'continuable', label: 'Review', name: 'reviewer' } }, + { type: 'subagent/catalog', seq: SessionSeq(2), time: 0, + data: { version: 3, childId: SessionId('auditor'), childCreatedAt: 3, mode: 'external', label: 'Audit', name: 'auditor' } }, + ] + const state = fold([fact(0, 'historical', 1, { mode: 'continuable', label: 'Old work' }), ...named]) + const view = subagentCatalogProjectionDefinition.wire.view( + subagentCatalogProjectionDefinition.stateSchema.parse(JSON.parse(JSON.stringify(state))), + ) + expect(view).toEqual([ + { id: SessionId('historical'), createdAt: 1, mode: 'continuable', label: 'Old work' }, + { id: SessionId('reviewer'), createdAt: 2, mode: 'continuable', label: 'Review', name: 'reviewer' }, + { id: SessionId('auditor'), createdAt: 3, mode: 'external', label: 'Audit', name: 'auditor' }, + ]) + expect(subagentCatalogProjectionDefinition.wire.viewSchema.parse(view)).toEqual(view) + expect(subagentCatalogProjectionDefinition.wire.viewSchema.safeParse([{ ...view[1], name: 'Bad name' }]).success).toBe(false) + expect(catalogNames(view)).toEqual(new Set(['reviewer', 'auditor'])) + }) +}) diff --git a/packages/subagent/subagent/tests/continuation-messages-adapter.spec.ts b/packages/subagent/subagent/tests/continuation-messages-adapter.spec.ts index a4d42df4fc..1b1f5a2f3a 100644 --- a/packages/subagent/subagent/tests/continuation-messages-adapter.spec.ts +++ b/packages/subagent/subagent/tests/continuation-messages-adapter.spec.ts @@ -79,7 +79,7 @@ it('continues the parent through default Messages after a reasoning-bearing cont expect(requests.map(request => request.path)).toEqual(['/anthropic/v1/messages', '/anthropic/v1/messages']) const notice = parent.session.deriveMessages().find(message => message.source.kind === 'subagent-settled') const closingContent = [ - { type: 'text', text: `Background subagent ${started.childId} finished and will do no further work unless you send it more.` }, + { type: 'text', text: 'Background subagent agent-1 finished and will do no further work unless you send it more.' }, { type: 'text', text: 'Its closing message:' }, { type: 'text', text: 'child answer' }, ] diff --git a/packages/subagent/subagent/tests/continuation-messages.spec.ts b/packages/subagent/subagent/tests/continuation-messages.spec.ts index 925a251051..8e45bffcff 100644 --- a/packages/subagent/subagent/tests/continuation-messages.spec.ts +++ b/packages/subagent/subagent/tests/continuation-messages.spec.ts @@ -4,7 +4,8 @@ import { SessionId } from '@deepseek-ai/dsh-session' import { createSettlementMessage } from '../src/continuation-messages.ts' const childId = SessionId('settled-child') -const summary = { type: 'text', text: `Background subagent ${childId} finished and will do no further work unless you send it more.` } +const childName = 'reviewer/checker' +const summary = { type: 'text', text: `Background subagent ${childName} finished and will do no further work unless you send it more.` } const reasoning: ContentBlock = { type: 'reasoning', text: 'private child reasoning' } const toolCall: ContentBlock = { type: 'tool-call', id: ToolCallId('child-call'), name: 'read', arguments: '{}' } @@ -12,7 +13,7 @@ describe('settlement attribution', () => { it.each(['completed', 'aborted', 'max-tokens', 'refusal', 'error'] as const)( 'records the exact %s stop reason without changing model content', (stopReason) => { const terminal = { stopReason, output: [{ type: 'text' as const, text: ' answer\n' }] } - const message = createSettlementMessage(childId, terminal) + const message = createSettlementMessage(childId, childName, terminal) expect(message.source).toMatchObject({ kind: 'subagent-settled', form: 'notice', senderSessionId: childId, stopReason, @@ -41,7 +42,7 @@ describe('continuable settlement content', () => { ['a tool call after the answer', [{ type: 'text', text: 'answer' }, toolCall]], ] satisfies [string, ContentBlock[]][])('reports only the closing text with %s', (_label, output) => { const original = structuredClone(output) - const message = createSettlementMessage(childId, { stopReason: 'completed', output }) + const message = createSettlementMessage(childId, childName, { stopReason: 'completed', output }) expect(message.role).toBe('user') expect(message.content).toEqual([ @@ -57,7 +58,7 @@ describe('continuable settlement content', () => { ['reasoning-only output', [reasoning]], ['empty text', [{ type: 'text', text: '' }]], ] satisfies [string, ContentBlock[]][])('reports no closing message for %s', (_label, output) => { - const message = createSettlementMessage(childId, { stopReason: 'completed', output }) + const message = createSettlementMessage(childId, childName, { stopReason: 'completed', output }) expect(message.content).toEqual([ summary, @@ -68,7 +69,7 @@ describe('continuable settlement content', () => { it('preserves text block order and bytes around omitted reasoning and tool calls', () => { const first: ContentBlock = { type: 'text', text: ' first\n' } const second: ContentBlock = { type: 'text', text: '\n第二段 ' } - const message = createSettlementMessage(childId, { + const message = createSettlementMessage(childId, childName, { stopReason: 'completed', output: [reasoning, first, toolCall, second], }) @@ -83,7 +84,7 @@ describe('continuable settlement content', () => { }) it('includes closing text alongside structured results and diagnostics', () => { - expect(createSettlementMessage(childId, { + expect(createSettlementMessage(childId, childName, { stopReason: 'completed', output: [{ type: 'text', text: 'final answer' }], structured: { answer: 42 }, diagnostic: 'safe diagnostic', }).content).toEqual([ @@ -96,7 +97,7 @@ it('includes closing text alongside structured results and diagnostics', () => { }) it('omits the closing text after the child messaged its parent, keeping structured results and diagnostics', () => { - expect(createSettlementMessage(childId, { + expect(createSettlementMessage(childId, childName, { stopReason: 'completed', output: [{ type: 'text', text: 'final answer, restated' }], structured: { answer: 42 }, diagnostic: 'safe diagnostic', }, true, true).content).toEqual([ diff --git a/packages/subagent/subagent/tests/continuation.spec.ts b/packages/subagent/subagent/tests/continuation.spec.ts index e53dd28875..a3cb8d3b93 100644 --- a/packages/subagent/subagent/tests/continuation.spec.ts +++ b/packages/subagent/subagent/tests/continuation.spec.ts @@ -2864,7 +2864,7 @@ describe('continuable adjacent-Agent delivery', () => { senderSessionId: started.childId, }) expect(delivered?.content).toEqual([ - { type: 'text', text: `Agent ${started.childId} sent a message: ` }, + { type: 'text', text: `Agent ${started.name} sent a message: ` }, { type: 'text', text: 'an explicit message' }, ]) @@ -2950,7 +2950,7 @@ describe('continuable settlement delivery', () => { await leaf.result const notice = settlementNotices(creatorAgent).find(item => item.sender === leaf.childId)! expect(notice.text).toBe( - `Background subagent ${leaf.childId} finished and will do no further work unless you send it more.\n` + `Background subagent ${leaf.name} finished and will do no further work unless you send it more.\n` + (target === 'parent' ? 'It sent you a message just before finishing, so its closing message is omitted.' : 'Its closing message:\nleaf answer'), @@ -2972,11 +2972,11 @@ describe('continuable settlement delivery', () => { const notice = settlementNotices(parent)[0]! expect(notice.sender).toBe(started.childId) expect(notice.text).toBe( - `Background subagent ${started.childId} finished and will do no further work unless you send it more.\nIts closing message:\nthe answer`, + `Background subagent ${started.name} finished and will do no further work unless you send it more.\nIts closing message:\nthe answer`, ) // The collapsed row states the outcome without the child's content. expect(notice.summary).toBe( - `Background subagent ${started.childId} finished and will do no further work unless you send it more.`, + `Background subagent ${started.name} finished and will do no further work unless you send it more.`, ) }) @@ -2998,7 +2998,7 @@ describe('continuable settlement delivery', () => { // message, is left out. await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) }) expect(settlementNotices(parent)[0]!.text).toBe( - `Background subagent ${started.childId} finished and will do no further work unless you send it more.\n` + `Background subagent ${started.name} finished and will do no further work unless you send it more.\n` + 'It sent you a message just before finishing, so its closing message is omitted.', ) }) @@ -3028,7 +3028,7 @@ describe('continuable settlement delivery', () => { await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) }) expect(settlementNotices(parent)[0]!.text).toBe( - `Background subagent ${started.childId} finished and will do no further work unless you send it more.\nIts closing message:\nthe answer`, + `Background subagent ${started.name} finished and will do no further work unless you send it more.\nIts closing message:\nthe answer`, ) }) @@ -3067,7 +3067,7 @@ describe('continuable settlement delivery', () => { await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) }) expect(settlementNotices(parent)[0]!.text).toBe( - `Background subagent ${started.childId} finished and will do no further work unless you send it more.\nIts closing message:\nthe answer`, + `Background subagent ${started.name} finished and will do no further work unless you send it more.\nIts closing message:\nthe answer`, ) }) @@ -3084,7 +3084,7 @@ describe('continuable settlement delivery', () => { const parentId = /You are a subagent delegated by agent \\"([^\\]+)\\"/.exec(history)![1]! const calls = order.map(name => name === 'noop' ? { id: ToolCallId('n1'), name, arguments: '{}' } - : { id: ToolCallId('s1'), name, arguments: JSON.stringify({ agent_id: parentId, message: 'interim' }) }) + : { id: ToolCallId('s1'), name, arguments: JSON.stringify({ agent: parentId, message: 'interim' }) }) return [ ...calls.flatMap((call, index): StreamChunk[] => [ { type: 'block-start', index, blockType: 'tool-call' }, @@ -3112,7 +3112,7 @@ describe('continuable settlement delivery', () => { await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) }) expect(settlementNotices(parent)[0]!.text).toBe( - `Background subagent ${started.childId} finished and will do no further work unless you send it more.\n${ending}`, + `Background subagent ${started.name} finished and will do no further work unless you send it more.\n${ending}`, ) }) @@ -3123,7 +3123,7 @@ describe('continuable settlement delivery', () => { await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) }) expect(settlementNotices(parent)[0]!.text).toBe( - `Background subagent ${started.childId} ran out of room before it finished.\nIts closing message:\nhalf an ans`, + `Background subagent ${started.name} ran out of room before it finished.\nIts closing message:\nhalf an ans`, ) }) @@ -3141,7 +3141,7 @@ describe('continuable settlement delivery', () => { await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) }) expect(settlementNotices(parent)[0]!.text).toBe( - `Background subagent ${started.childId} declined the task.\nIt left no closing message.`, + `Background subagent ${started.name} declined the task.\nIt left no closing message.`, ) }) @@ -3171,7 +3171,7 @@ describe('continuable settlement delivery', () => { const child = await loadStoredSession(ctx.sessionPersistence, started.childId) expect(hasUserText(child.events, 'second task')).toBe(false) expect(settlementNotices(parent)[0]!.text).toBe( - `Background subagent ${started.childId} failed before it finished.\nIts closing message:\nthe answer`, + `Background subagent ${started.name} failed before it finished.\nIts closing message:\nthe answer`, ) }) @@ -3206,7 +3206,7 @@ describe('continuable settlement delivery', () => { // task had finished. await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) }) expect(settlementNotices(parent)[0]!.text).toBe( - `Background subagent ${started.childId} was stopped before it finished.\nIts closing message:\nthe answer`, + `Background subagent ${started.name} was stopped before it finished.\nIts closing message:\nthe answer`, ) }) @@ -3232,7 +3232,7 @@ describe('continuable settlement delivery', () => { // that genuinely finished with no output. await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) }) expect(settlementNotices(parent)[0]!.text).toBe( - `Background subagent ${started.childId} was stopped before it finished.\nIt left no closing message.`, + `Background subagent ${started.name} was stopped before it finished.\nIt left no closing message.`, ) }) @@ -3258,7 +3258,7 @@ describe('continuable settlement delivery', () => { await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) }) expect(settlementNotices(parent)[0]!.text).toBe( - `Background subagent ${started.childId} was stopped before it finished.\nIt left no closing message.`, + `Background subagent ${started.name} was stopped before it finished.\nIt left no closing message.`, ) }) @@ -3295,7 +3295,7 @@ describe('continuable settlement delivery', () => { expect(hasUserText(child.session.snapshotEvents(), 'never runs')).toBe(false) await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) }) expect(settlementNotices(parent)[0]!.text).toBe( - `Background subagent ${started.childId} was stopped before it finished.\nIts closing message:\nthe answer`, + `Background subagent ${started.name} was stopped before it finished.\nIts closing message:\nthe answer`, ) }) @@ -3317,7 +3317,7 @@ describe('continuable settlement delivery', () => { await waitNoActivation(ctx, started.childId) await vi.waitFor(() => { expect(settlementNotices(parent)).toHaveLength(1) }) expect(settlementNotices(parent)[0]!.text).toBe( - `Background subagent ${started.childId} finished and will do no further work unless you send it more.\nIts closing message:\nthe answer`, + `Background subagent ${started.name} finished and will do no further work unless you send it more.\nIts closing message:\nthe answer`, ) await expect(started.result).resolves.toMatchObject({ stopReason: 'completed' }) await expect(started.dispose()).rejects.toThrow('scope unwind failed') @@ -3393,7 +3393,7 @@ describe('continuable settlement delivery', () => { // message when that parent is disposed next is pinned by the test below. expect(settlementNotices(parent)).toHaveLength(1) expect(settlementNotices(parent)[0]!.text).toBe( - `Background subagent ${started.childId} was stopped before it finished.\nIt left no closing message.`, + `Background subagent ${started.name} was stopped before it finished.\nIt left no closing message.`, ) expect(parent.session.snapshotEvents().some(event => event.type === 'agent/inbox/spliced')).toBe(true) expect(parent.session.snapshotEvents().some(event => event.type === 'turn/start')).toBe(false) diff --git a/packages/subagent/subagent/tests/external-activation.spec.ts b/packages/subagent/subagent/tests/external-activation.spec.ts index 9f15aaed61..9398ee061f 100644 --- a/packages/subagent/subagent/tests/external-activation.spec.ts +++ b/packages/subagent/subagent/tests/external-activation.spec.ts @@ -27,8 +27,9 @@ async function setup(start: (request: SubagentStartRequest) => Promise ctx.subagents.startActivation({ + start: (delivery: 'parent' | 'caller' = 'caller', name?: string) => ctx.subagents.startActivation({ provider: 'external', label: 'External work', signal: controller.signal, delivery, + ...name === undefined ? {} : { name }, parent, prompt: [{ type: 'text', text: 'Work independently' }], }), records: () => ctx.sessionProjections.snapshot(parent.session, ['subagentCatalog']).values.subagentCatalog, @@ -389,3 +390,28 @@ it.each(['completed', 'aborted'] as const)('reports external %s without offering expect(JSON.stringify(fixture.parent.inbox.nextTurn)).toContain(stopReason === 'completed' ? 'cannot receive follow-up messages' : 'was stopped before it finished') expect(JSON.stringify(fixture.parent.inbox.nextTurn)).not.toContain('send it more') }) + +it('records a parent-delivered external run under its requested name', async () => { + const backend = execution() + const fixture = await setup(async () => backend.run) + const activation = await fixture.start('parent', 'auditor') + expect(activation.name).toBe('auditor') + expect(fixture.records()).toMatchObject([{ id: activation.childId, mode: 'external', name: 'auditor' }]) + backend.result.resolve(completed) + backend.cleanup.resolve(undefined) + await activation.dispose() +}) + +it('rejects a name for a caller-delivered external run that never enters the catalog', async () => { + const start = vi.fn<(request: SubagentStartRequest) => Promise>() + const fixture = await setup(start) + await expect(fixture.start('caller', 'auditor')).rejects.toMatchObject({ code: 'UNSUPPORTED_CAPABILITY' }) + expect(start).not.toHaveBeenCalled() + const backend = execution() + start.mockResolvedValue(backend.run) + const unnamed = await fixture.start('caller') + expect(unnamed.name).toBe(unnamed.childId) + backend.result.resolve(completed) + backend.cleanup.resolve(undefined) + await unnamed.dispose() +}) diff --git a/packages/subagent/subagent/tests/list-children.spec.ts b/packages/subagent/subagent/tests/list-children.spec.ts index 7fd31fe301..be656f9f80 100644 --- a/packages/subagent/subagent/tests/list-children.spec.ts +++ b/packages/subagent/subagent/tests/list-children.spec.ts @@ -135,8 +135,8 @@ describe('direct-child catalog reader', () => { const children = await listChildren(ctx, parent.id) expect(children.map(({ createdAt: _createdAt, ...child }) => child)).toEqual([ - { id: callerOwned.childId, label: 'spawn', mode: 'continuable' }, - { id: continuableId, label: 'continuable child', mode: 'continuable' }, + { id: callerOwned.childId, label: 'spawn', mode: 'continuable', name: 'agent-1' }, + { id: continuableId, label: 'continuable child', mode: 'continuable', name: 'agent-2' }, ]) expect(children.every(child => Number.isFinite(child.createdAt))).toBe(true) expect(listSessions).not.toHaveBeenCalled() diff --git a/packages/subagent/subagent/tests/manager-internals.ts b/packages/subagent/subagent/tests/manager-internals.ts index 5b2e65ca56..3cb20995eb 100644 --- a/packages/subagent/subagent/tests/manager-internals.ts +++ b/packages/subagent/subagent/tests/manager-internals.ts @@ -43,6 +43,7 @@ export function bindSubagentActivation( provider: 'fixture', root, ancestors, + path: undefined, releaseSlot() {}, handle: { agent: child, async dispose() {} }, outputStart: child.session.seq, diff --git a/packages/subagent/subagent/tests/sibling-messages.spec.ts b/packages/subagent/subagent/tests/sibling-messages.spec.ts index c82b6514ff..32559af637 100644 --- a/packages/subagent/subagent/tests/sibling-messages.spec.ts +++ b/packages/subagent/subagent/tests/sibling-messages.spec.ts @@ -17,7 +17,7 @@ import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-a import SubagentRuntime from '../src/index.ts' import { loadStoredSession } from './persistence-helpers.ts' import { TestSessionQuery } from './test-session-query.ts' -import { managerState, subagentManager } from './manager-internals.ts' +import { bindSubagentActivation, managerState, subagentManager } from './manager-internals.ts' import { requireLocalActivation } from '../src/activation.ts' /** A model reply, or a turn that stays open until its Agent is cancelled. */ @@ -92,6 +92,18 @@ async function start(ctx: Context, parent: Agent, marker: string, description = return started.childId } +/** Start one caller-delivery child under a requested or default name and return its receipt. */ +function startNamed(ctx: Context, parent: Agent, marker: string, name?: string, startSignal = signal) { + return ctx.subagents.startActivation({ + provider: 'spawn', + label: `task ${marker}`, + ...name === undefined ? {} : { name }, + prompt: [{ type: 'text', text: `TASK:${marker}` }], parent, + signal: startSignal, + delivery: 'caller', + }) +} + /** Wait until a child's first model request is open. */ async function running(ctx: Context, id: SessionId): Promise { await vi.waitFor(() => { expect(ctx.agents.get(id)?.status).toBe('running') }) @@ -116,7 +128,7 @@ function textOf(event: SessionEvent<'user/message'>): string { } describe('subagent sibling messages', () => { - it('keeps duplicate descriptions as display text and addresses siblings by Session id', async () => { + it('keeps duplicate descriptions as display text and names siblings uniquely', async () => { const { ctx, parent } = await setup({ a: ['hang'], b: [textResponse('done'), textResponse('resumed')] }) const a = await start(ctx, parent, 'a') const sender = await running(ctx, a) @@ -124,15 +136,15 @@ describe('subagent sibling messages', () => { await settled(ctx, b) expect(a).not.toBe(b) expect(await listChildren(ctx, parent.id)).toEqual([ - expect.objectContaining({ id: a, mode: 'continuable', label: 'shared responsibility' }), - expect.objectContaining({ id: b, mode: 'continuable', label: 'shared responsibility' }), + expect.objectContaining({ id: a, mode: 'continuable', label: 'shared responsibility', name: 'agent-1' }), + expect.objectContaining({ id: b, mode: 'continuable', label: 'shared responsibility', name: 'agent-2' }), ]) expect(parent.session.snapshotEvents().filter(event => event.type === 'subagent/catalog').map(event => event.data.version)) - .toEqual([0, 0]) + .toEqual([3, 3]) await ctx.subagents.sendMessage(sender, b, [{ type: 'text', text: 'hello' }], { signal }) const delivered = (await storedUserMessages(ctx, b)).find(event => event.data.source.kind === 'agent-message')! - expect(textOf(delivered)).toBe(`Agent ${a} sent a message: hello`) + expect(textOf(delivered)).toBe('Agent agent-1 sent a message: hello') expect(delivered.data.source).toEqual({ kind: 'agent-message', form: 'relay', senderSessionId: a }) ctx.subagents.interrupt(a, { kind: 'ancestor', agent: parent }) await settled(ctx, a) @@ -198,9 +210,9 @@ describe('tree-wide messages', () => { await ctx.subagents.sendMessage(parent, leafId, [{ type: 'text', text: 'grandchild' }], { signal }) await ctx.subagents.sendMessage(leaf, parent.id, [{ type: 'text', text: 'grandparent' }], { signal }) expect(received).toEqual([ - { target: leafId, sender: a, text: `Agent ${a} sent a message: cousin` }, - { target: leafId, sender: parent.id, text: `Agent ${parent.id} sent a message: grandchild` }, - { target: parent.id, sender: leafId, text: `Agent ${leafId} sent a message: grandparent` }, + { target: leafId, sender: a, text: 'Agent agent-1 sent a message: cousin' }, + { target: leafId, sender: parent.id, text: 'Agent lead sent a message: grandchild' }, + { target: parent.id, sender: leafId, text: 'Agent agent-2/agent-1 sent a message: grandparent' }, ]) expect(() => { ctx.subagents.interrupt(leafId, { kind: 'ancestor', agent: sender }) }) .toThrow(expect.objectContaining({ code: 'UNAUTHORIZED' })) @@ -226,7 +238,7 @@ describe('tree-wide messages', () => { const messages = await storedUserMessages(ctx, leafId) const delivered = messages.find(event => event.data.source.kind === 'agent-message')! - expect(textOf(delivered)).toBe(`Agent ${a} sent a message: cold cousin`) + expect(textOf(delivered)).toBe('Agent agent-2 sent a message: cold cousin') expect(delivered.data.source).toEqual({ kind: 'agent-message', form: 'relay', senderSessionId: a }) await settled(ctx, branchId) expect(resumed).toEqual([{ id: leafId, parent: branchId }]) @@ -355,7 +367,7 @@ describe('tree-wide messages', () => { expect(ctx.agents.get(branchId)).toBe(branch) release.resolve(undefined) const messages = await storedUserMessages(ctx, leafId) - expect(messages.some(message => textOf(message) === `Agent ${parent.id} sent a message: after close`)).toBe(true) + expect(messages.some(message => textOf(message) === 'Agent lead sent a message: after close')).toBe(true) await settled(ctx, branchId) } finally { release.resolve(undefined) @@ -364,3 +376,93 @@ describe('tree-wide messages', () => { } }) }) + +describe('agent names', () => { + it('records requested and default names and rejects duplicates and invalid names before startup', async () => { + const { ctx, parent, adapter } = await setup({ a: ['hang'], b: ['hang'] }) + const reviewer = await startNamed(ctx, parent, 'a', 'reviewer') + const unnamed = await startNamed(ctx, parent, 'b') + expect([reviewer.name, unnamed.name]).toEqual(['reviewer', 'agent-1']) + for (const name of ['reviewer', 'agent-1']) { + await expect(startNamed(ctx, parent, 'c', name)).rejects.toMatchObject({ code: 'DUPLICATE_AGENT_NAME' }) + } + for (const name of ['lead', 'Reviewer', '1st', 'a'.repeat(33), 'review/er', '']) { + await expect(startNamed(ctx, parent, 'c', name)).rejects.toMatchObject({ code: 'INVALID_AGENT_NAME' }) + } + expect(adapter.routed.some(request => JSON.stringify(request.messages).includes('TASK:c'))).toBe(false) + expect((await listChildren(ctx, parent.id)).map(entry => 'name' in entry ? entry.name : undefined)) + .toEqual(['reviewer', 'agent-1']) + for (const started of [reviewer, unnamed]) { + ctx.subagents.interrupt(started.childId, { kind: 'ancestor', agent: parent }) + await settled(ctx, started.childId) + } + }) + + it('holds a requested name across concurrent startup and releases it when startup fails', async () => { + const { ctx, parent } = await setup({ a: ['hang'], b: ['hang'], c: ['hang'] }) + const first = startNamed(ctx, parent, 'a', 'worker') + const concurrent = startNamed(ctx, parent, 'c') + await expect(startNamed(ctx, parent, 'b', 'worker')).rejects.toMatchObject({ code: 'DUPLICATE_AGENT_NAME' }) + const started = await first + const aborted = new AbortController() + aborted.abort(new Error('cancelled')) + await expect(startNamed(ctx, parent, 'b', 'helper', aborted.signal)).rejects.toThrow('cancelled') + const retried = await startNamed(ctx, parent, 'b', 'helper') + const sibling = await concurrent + expect([started.name, sibling.name, retried.name]).toEqual(['worker', 'agent-1', 'helper']) + for (const child of [started, retried, sibling]) { + ctx.subagents.interrupt(child.childId, { kind: 'ancestor', agent: parent }) + await settled(ctx, child.childId) + } + }) + + it('names nested members by path and resolves names without granting authority', async () => { + const { ctx, parent } = await setup({ branch: ['hang'], leaf: ['hang'] }) + const branch = await startNamed(ctx, parent, 'branch', 'reviewer') + const branchAgent = await running(ctx, branch.childId) + const leaf = await startNamed(ctx, branchAgent, 'leaf', 'checker') + const leafAgent = await running(ctx, leaf.childId) + expect(leaf.name).toBe('reviewer/checker') + expect(ctx.subagents.namesOf(parent)).toEqual({ self: 'lead' }) + expect(ctx.subagents.namesOf(branchAgent)).toEqual({ self: 'reviewer', parent: 'lead' }) + expect(ctx.subagents.namesOf(leafAgent)).toEqual({ self: 'reviewer/checker', parent: 'reviewer' }) + const resolved = await Promise.all(['lead', 'reviewer', 'reviewer/checker', 'reviewer/missing', 'missing', 'Not a name', leaf.childId] + .map(name => ctx.subagents.resolveName(leafAgent, name, signal))) + expect(resolved).toEqual([parent.id, branch.childId, leaf.childId, 'reviewer/missing', 'missing', 'Not a name', leaf.childId]) + expect((await ctx.subagents.listDescendants(parent.id, signal)).map(entry => entry.path)) + .toEqual(['reviewer', 'reviewer/checker']) + ctx.subagents.interrupt(branch.childId, { kind: 'ancestor', agent: parent }) + ctx.subagents.interrupt(leaf.childId, { kind: 'ancestor', agent: parent }) + await settled(ctx, branch.childId) + await settled(ctx, leaf.childId) + }) + + it('restores a cold member\'s path from the catalogs along its creation chain', async () => { + const { ctx, parent } = await setup({ branch: ['hang'], leaf: [textResponse('first'), 'hang'] }) + const branch = await startNamed(ctx, parent, 'branch', 'reviewer') + const branchAgent = await running(ctx, branch.childId) + const leaf = await startNamed(ctx, branchAgent, 'leaf', 'checker') + await settled(ctx, leaf.childId) + ctx.subagents.interrupt(branch.childId, { kind: 'ancestor', agent: parent }) + await settled(ctx, branch.childId) + + await ctx.subagents.sendMessage(parent, leaf.childId, [{ type: 'text', text: 'resume' }], { signal }) + const resumed = await running(ctx, leaf.childId) + expect(ctx.subagents.namesOf(resumed)).toEqual({ self: 'reviewer/checker', parent: 'reviewer' }) + ctx.subagents.interrupt(leaf.childId, { kind: 'ancestor', agent: parent }) + await settled(ctx, leaf.childId) + }) + + it('addresses members below an unnamed historical creator by Session id', async () => { + const { ctx, parent } = await setup({ legacy: ['hang'], leaf: ['hang'] }) + const legacy = await ctx.agentLoop.create(SessionId('legacy-child'), { provider: 'mock', model: 'mock' }) + bindSubagentActivation(ctx, legacy, parent) + expect(ctx.subagents.namesOf(legacy)).toEqual({ self: 'legacy-child', parent: 'lead' }) + const leaf = await startNamed(ctx, legacy, 'leaf', 'checker') + const leafAgent = await running(ctx, leaf.childId) + expect(leaf.name).toBe(leaf.childId) + expect(ctx.subagents.namesOf(leafAgent)).toEqual({ self: leaf.childId, parent: 'legacy-child' }) + ctx.subagents.interrupt(leaf.childId, { kind: 'ancestor', agent: parent }) + await settled(ctx, leaf.childId) + }) +}) diff --git a/packages/subagent/tool-subagent-control/README.i18n.yaml b/packages/subagent/tool-subagent-control/README.i18n.yaml index e920a94726..2487961dba 100644 --- a/packages/subagent/tool-subagent-control/README.i18n.yaml +++ b/packages/subagent/tool-subagent-control/README.i18n.yaml @@ -9,8 +9,8 @@ en: 465d36925c792167 zh: 269cac314f638827 /deepseek-ai-dsh-tool-subagent-control/summary: - en: 7e68f9a2d58e1ab6 - zh: 24edd923e898cc3b + en: cba97055d8a7fec5 + zh: 11f22dcf2670b878 /deepseek-ai-dsh-tool-subagent-control/table-of-contents: en: d152484eb41ac6b4 zh: 09388d293f9be9cb @@ -21,29 +21,29 @@ en: d224de77b41ed4e4 zh: f1e94f9cc9e04384 /deepseek-ai-dsh-tool-subagent-control/use-this-package/send-message: - en: 849b9b3e576aed01 - zh: d52239ca40d3a168 + en: fa4b7d45c45e172f + zh: d74e402d32b48b1c /deepseek-ai-dsh-tool-subagent-control/use-this-package/interrupt-agent: - en: de45d5a4b98d3c91 - zh: 666aa187170b9c0d + en: ac4cfa54d61e96a7 + zh: 367e4109549eac31 /deepseek-ai-dsh-tool-subagent-control/use-this-package/wait-agent: en: 29a3466bcc8e8e9d zh: ea38770df450a43e /deepseek-ai-dsh-tool-subagent-control/use-this-package/list-agents: - en: 912206fb3da120b5 - zh: 3b247046a52d5f2a + en: c56c64f22aa86c72 + zh: 8d6e3ecd67e9b3a1 /deepseek-ai-dsh-tool-subagent-control/understand-the-implementation: en: c5cdf01a78c990f2 zh: 53c180d726ce4502 /deepseek-ai-dsh-tool-subagent-control/understand-the-implementation/design-concept: - en: 3b6f1956d232964d - zh: 9949b906020c9a79 + en: 165ba4fcc8a90fa7 + zh: d07b700657601726 /deepseek-ai-dsh-tool-subagent-control/understand-the-implementation/delivery-and-signal-ownership: - en: 65137f5fe14ebb7f - zh: ce00e4f32709246a + en: c4dffa4789053044 + zh: 5c890ec2393f3261 /deepseek-ai-dsh-tool-subagent-control/understand-the-implementation/listing-projection: - en: ee3136b09f0c9ef2 - zh: 5cbe189bb857fdc7 + en: 34b0b770c7ae6726 + zh: d8c589b6bd62be0b /deepseek-ai-dsh-tool-subagent-control/understand-the-implementation/source-map: en: 988d8adc49bfc0c2 zh: b3091ceb8d6dcad3 @@ -57,8 +57,8 @@ en: e4c7376d4f29dbd5 zh: 6b7a9eaf69c9ea53 /deepseek-ai-dsh-tool-subagent-control/model-experience/tool-schema/what-the-model-sees: - en: db2e45684a4d0117 - zh: 5cd3b646c010b530 + en: 102ef57f634f4d01 + zh: 571304c1e6e5dd1a /deepseek-ai-dsh-tool-subagent-control/model-experience/tool-schema/token-effect: en: 5eb1d46a94b7ec1b zh: 5670760ca695d4ad @@ -69,20 +69,23 @@ en: fc88c8ffb8ebc083 zh: 0baf2e5df37d813e /deepseek-ai-dsh-tool-subagent-control/model-experience/identity-and-communication-context/what-the-model-sees: - en: f15da8ae032d1133 - zh: d778adb601756c27 + en: 3f59216388405b66 + zh: 70672463706bc5c5 +/deepseek-ai-dsh-tool-subagent-control/model-experience/identity-and-communication-context/what-the-model-sees/subagent-identity: + en: fc2b3383d52ded16 + zh: d5074e59b79c7afd /deepseek-ai-dsh-tool-subagent-control/model-experience/identity-and-communication-context/token-effect: - en: e2c1a9e7f3a0bbea - zh: d13f6963c8b2a8c5 + en: 37a7eed766856ea0 + zh: 4a564f74e08a3252 /deepseek-ai-dsh-tool-subagent-control/model-experience/identity-and-communication-context/kv-cache-effect: - en: feed847f7a028d67 - zh: 623a3438346d2bf8 + en: 49dc6a07ae9640e3 + zh: 465fcd7cfe452ad2 /deepseek-ai-dsh-tool-subagent-control/model-experience/interrupt-result: en: d64e936e83e89a2b zh: b9905c33af6cca45 /deepseek-ai-dsh-tool-subagent-control/model-experience/interrupt-result/what-the-model-sees: - en: 89305619897680ba - zh: e610a844fc09c53f + en: 917ab5f74a23687d + zh: 97dacd5811ef7d66 /deepseek-ai-dsh-tool-subagent-control/model-experience/interrupt-result/token-effect: en: 5d1bcef28b5f18da zh: 027d67d93d660e76 @@ -93,11 +96,11 @@ en: ab964e307bd37dca zh: 03f975252f838a42 /deepseek-ai-dsh-tool-subagent-control/model-experience/delivery-result/what-the-model-sees: - en: 8746cb9fa6557bfe - zh: 49828e113e901160 + en: 0b55f549e3a6fd6c + zh: 837e8b38ac42548a /deepseek-ai-dsh-tool-subagent-control/model-experience/delivery-result/token-effect: - en: f7e47f835a1e0628 - zh: d2818b98c773ad35 + en: 9454662d08749312 + zh: 7c3cf7788f461a1a /deepseek-ai-dsh-tool-subagent-control/model-experience/delivery-result/kv-cache-effect: en: f5d5f97a153f7c70 zh: 3d6bc05b9eadfbc2 @@ -105,11 +108,11 @@ en: 094e55d258f2ec2e zh: f364bc9046530bff /deepseek-ai-dsh-tool-subagent-control/model-experience/listing-result/what-the-model-sees: - en: 08c89e5dbbd691a8 - zh: 5d74e546d024735b + en: 3b88f602282087f5 + zh: d597e4681915d25a /deepseek-ai-dsh-tool-subagent-control/model-experience/listing-result/token-effect: - en: 027aaeb808238388 - zh: 5b974abc429d9e23 + en: 8b5491a6a5459bb3 + zh: 87338c341aa9aa56 /deepseek-ai-dsh-tool-subagent-control/model-experience/listing-result/kv-cache-effect: en: 4a629edb36a17e32 zh: 5a7539cfbf404c54 diff --git a/packages/subagent/tool-subagent-control/README.md b/packages/subagent/tool-subagent-control/README.md index 605b6c5122..f246a660a3 100644 --- a/packages/subagent/tool-subagent-control/README.md +++ b/packages/subagent/tool-subagent-control/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -`dsh-tool-subagent-control` lets agents discover and message their subagent tree by Session id, interrupt descendants, and wait for their own next-step inbox. By default, one plugin instance registers all four tools; members inherit the same definitions and ordering. +`dsh-tool-subagent-control` lets agents discover and message their subagent tree by name, interrupt descendants, and wait for their own next-step inbox. By default, one plugin instance registers all four tools; members inherit the same definitions and ordering. ## Table of Contents @@ -44,11 +44,11 @@ Set `config.interrupt: false` to omit `interrupt_agent`. The default is `true`. ### send_message -Sends a message to another agent in the same subagent tree by `agent_id`. The caller must be the live root or a resident continuable subagent. A working target receives Steer input at its next step; an inactive target starts or resumes a turn. Only the target resumes; its creation ancestors stay offline. Success returns inbox acceptance (`messageId`), never a reply. Self, unrelated trees, unsupported targets, unknown ids, unavailable history, and rejected admission return errors. +Sends a message to another agent in the same subagent tree. `agent` takes a name from `list_agents`: `lead` for the root or a full name such as `reviewer/checker`. Text that is not a name in the tree is treated as a Session id, so Session ids also work, including for members listed by Session id. The caller must be the live root or a resident continuable subagent. A working target receives Steer input at its next step; an inactive target starts or resumes a turn. Only the target resumes; its creation ancestors stay offline. Success returns inbox acceptance (`messageId`), never a reply. Self, unrelated trees, unsupported targets, unknown names or ids, unavailable history, and rejected admission return errors. ### interrupt_agent -`agent_id` identifies a descendant by Session id. External executions stop permanently and cannot receive follow-ups. Local targets stop only their current turn: queued messages stay parked until a later `send_message`, descendants keep running, and the child stays available for follow-ups. The call returns when the stop request is accepted, not when the target is quiet; interrupting an already-finished agent is an accepted no-op, and self, sibling, stale, and non-ancestor callers get errored results. +`agent` names a descendant the same way `send_message` names its target. External executions stop permanently and cannot receive follow-ups. Local targets stop only their current turn: queued messages stay parked until a later `send_message`, descendants keep running, and the child stays available for follow-ups. The call returns when the stop request is accepted, not when the target is quiet; interrupting an already-finished agent is an accepted no-op, and self, sibling, stale, and non-ancestor callers get errored results. ### wait_agent @@ -57,7 +57,7 @@ Waits for the exact caller’s next-step inbox. Pending input returns immediatel ### list_agents -Lists the root and all reachable continuable descendants, including the caller, in stable pre-order. Descendant rows carry their direct parent id and depth from the root. Live status is `running` or `inactive`. External entries are omitted without reading child Sessions; readable one-shot children remain traversal nodes. Unknown modes and unreadable catalogs produce diagnostics. Ordinary Session forks lead separate subagent trees. +Lists the root and all reachable continuable descendants, including the caller, in stable pre-order. Each row carries a `name`, a `status`, and, for the root and continuable descendants, a `description`. The root is `lead` with the description `root agent`; a descendant's name is its full path from the root and its description is its creation label. A child whose catalog entry has no name, and every descendant below it, is listed by Session id. Live status is `running` or `inactive`. External entries are omitted without reading child Sessions; readable one-shot children remain traversal nodes. Unknown modes and unreadable catalogs produce rows whose status is `unsupported`, `corrupt`, or `unavailable`. Ordinary Session forks lead separate subagent trees. ----- @@ -71,15 +71,15 @@ This section explains what the tools delegate to the subagent service; the obser ### Design concept -Messaging, interruption, and discovery delegate to `ctx.subagents`; residency, cold resume, and authorization belong to that service. The wait tool subscribes to the exact caller’s inbox events before starting the timer and checking pending next-step input, and removes its listeners and timer on every settlement. +Messaging, interruption, and discovery delegate to `ctx.subagents`; residency, cold resume, and authorization belong to that service. Messaging and interruption first map `agent` to a Session id through `ctx.subagents.resolveName()`, which grants no authority; the service then authorizes that Session id. The wait tool subscribes to the exact caller’s inbox events before starting the timer and checking pending next-step input, and removes its listeners and timer on every settlement. ### Delivery and signal ownership -The execution signal owns work only until inbox acceptance. Accepted messages cannot be cancelled through this tool. Every message is framed as `Agent sent a message:` and recorded with `{ kind: 'agent-message', form: 'relay', senderSessionId: sender.id }`. Tree membership authorizes communication; the subagent tree root owns runtime instances and capacity. +The execution signal owns work only until inbox acceptance. Accepted messages cannot be cancelled through this tool. Every message is framed as `Agent sent a message:` and recorded with `{ kind: 'agent-message', form: 'relay', senderSessionId: sender.id }`. Tree membership authorizes communication; the subagent tree root owns runtime instances and capacity. ### Listing projection -`list_agents` resolves the caller’s live root and traverses its catalogs without a cursor, refining activity through the live Agent registry. Unknown modes remain traversable with diagnostics; unreadable catalogs stop that branch. Sessions absent from reachable catalogs cannot be discovered. +`list_agents` resolves the caller’s live root and traverses its catalogs without a cursor, refining activity through the live Agent registry. Names come from the `path` of each `listDescendants()` row. Unknown modes remain traversable with diagnostics; unreadable catalogs stop that branch. Sessions absent from reachable catalogs cannot be discovered. ### Source map @@ -111,7 +111,7 @@ Read these pages when the package-level contract is not enough; they move from t #### What the model sees -The generated [schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent-control) define id-addressed messaging and interruption, whole-tree discovery, and inbox waiting with `timeout_ms`. `wait_agent` returns `{ "timedOut": false }` for available input, and `{ "timedOut": true }` on timeout. It returns no message body or peer-activity diagnosis. +The generated [schemas](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent-control) define name-addressed messaging and interruption, whole-tree discovery, and inbox waiting with `timeout_ms`. `wait_agent` returns `{ "timedOut": false }` for available input, and `{ "timedOut": true }` on timeout. It returns no message body or peer-activity diagnosis. #### Token effect @@ -125,21 +125,32 @@ Prefix-stable; the schemas do not change at runtime, and a parent and its contin #### What the model sees -Required `subagent:identity` runtime context identifies each agent by its Session id. Delegated children are identified as subagents and receive their direct parent and root ids. It is independent of Team mode and tool filtering; a missing live root is reported without preventing independent work. When the live root is available, identity context includes messaging and discovery instructions independently for each admitted standard tool. Automatic result delivery remains owned by the subagent service. +Required `subagent:identity` runtime context identifies each agent by name. It is independent of Team mode and tool filtering. When the live root is available, identity context includes messaging and discovery instructions independently for each admitted standard tool. The root sees `You are the root agent. Your name is "lead".` followed by the same guidance. A missing live root is reported with the agent's Session id, without preventing independent work. Automatic result delivery remains owned by the subagent service. A child of the root named `reviewer`, with both tools admitted, sees: + +##### Subagent identity + +```markdown +You are a subagent delegated by agent "lead". +Your name is "reviewer". +The root agent of your collaboration tree is "lead". + +To message other agents, use send_message({ agent: "", message: "..." }). +Use list_agents({}) to find your collaborator agents and their names. +``` #### Token effect -One short context entry per agent, independent of tree size. +One short context entry per agent, independent of tree size; full names lengthen with nesting depth. #### KV Cache effect -Identity and communication guidance follow inherited history as logged runtime context, leaving the shared system prompt and tool definitions unchanged. This required context remains available with `includeRuntimeContext: false` and is restored after compaction. Team mode does not change it. +Identity and communication guidance follow inherited history as logged runtime context, leaving the shared system prompt and tool definitions unchanged. When a retained snapshot's identity text differs from the current text, one current runtime-context snapshot is appended. This required context remains available with `includeRuntimeContext: false` and is restored after compaction. Team mode does not change it. ### Interrupt result #### What the model sees -`interrupt requested for agent ` on acceptance. An unauthorized caller — self, sibling, stale, or non-ancestor — is an errored result naming the rejection; an absent or settled target still renders the acceptance line. +`interrupt requested for agent ` on acceptance, echoing the `agent` argument. An unauthorized caller — self, sibling, stale, or non-ancestor — is an errored result naming the rejection; an absent or settled target still renders the acceptance line. #### Token effect @@ -153,11 +164,11 @@ Append-only; each result follows the reusable request prefix. #### What the model sees -`message delivered to agent ` on acceptance; canonical output carries the accepted `messageId`. A target outside the caller’s subagent tree or rejected admission produces an errored result. +`message delivered to agent ` on acceptance, echoing the `agent` argument; canonical output carries the accepted `messageId`. A target outside the caller’s subagent tree or rejected admission produces an errored result. #### Token effect -One short acknowledgement per call; replies arrive as separately addressed messages. Use ids from `list_agents` for follow-ups across the subagent tree. +One short acknowledgement per call; replies arrive as separately addressed messages. Use names from `list_agents` for follow-ups across the subagent tree. #### KV Cache effect @@ -167,11 +178,11 @@ Append-only; newly visible content follows the reusable request prefix and does #### What the model sees -The first row is ` [] — root agent`. Each continuable descendant follows as ` [] parent= depth= —
- {exitLabel === null && openFile !== undefined ? ( + {exitLabel === null && fileTargets !== undefined && openTarget !== undefined ? ( + + {fileTargets.map((target, index) => ( + + {index > 0 && ', '} + {target.from !== undefined && `${target.from} → `} + {target.filePath === undefined ? target.label : ( + + )} + + ))} + + ) : exitLabel === null && openFile !== undefined ? (