From c96d48d49161c53b9b2a84942b7e7f1cdc3bf900 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Thu, 24 Sep 2026 12:55:36 +0800 Subject: [PATCH 001/369] refactor(schedule): extract time vocabulary into dsh-util-time Move Schedule's strict RFC 3339 instant, local-calendar, and IANA zone parsing into @deepseek-ai/dsh-util-time, and have Schedule consume it. The shared entries answer discriminated results and own no failure vocabulary; Schedule keeps its error codes, diagnostics, durable record shapes, and all recurrence arithmetic. --- ...026-09-24-shared-time-vocabulary.i18n.yaml | 19 ++ .../2026-09-24-shared-time-vocabulary.md | 39 +++ .../2026-09-24-shared-time-vocabulary.zh.md | 39 +++ packages/schedule/schedule/package.json | 2 + packages/schedule/schedule/src/domain.ts | 238 +++++---------- packages/schedule/schedule/tsconfig.json | 3 + packages/util/README.i18n.yaml | 12 +- packages/util/README.md | 6 +- packages/util/README.zh.md | 6 +- packages/util/time/README.i18n.yaml | 43 +-- packages/util/time/README.md | 72 ++++- packages/util/time/README.zh.md | 72 ++++- packages/util/time/package.json | 5 +- packages/util/time/src/index.ts | 29 +- packages/util/time/src/instant.ts | 279 ++++++++++++++++++ packages/util/time/tests/instant.spec.ts | 239 +++++++++++++++ pnpm-lock.yaml | 7 + 17 files changed, 874 insertions(+), 236 deletions(-) create mode 100644 .agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml create mode 100644 .agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md create mode 100644 .agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md create mode 100644 packages/util/time/src/instant.ts create mode 100644 packages/util/time/tests/instant.spec.ts diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml new file mode 100644 index 0000000000..1048d91116 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for 2026-09-24-shared-time-vocabulary.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-09-24-shared-time-vocabulary.md +/agent-note-shared-time-vocabulary-for-wall-clock-selectors: + en: ae04747237709d0c + zh: a3731d98d5e940f9 +/agent-note-shared-time-vocabulary-for-wall-clock-selectors/problem: + en: 6adc7f8750843df4 + zh: 55a600663ece4b2a +/agent-note-shared-time-vocabulary-for-wall-clock-selectors/decision: + en: 290a11695db08ffa + zh: a19b859e39e8cf8a +/agent-note-shared-time-vocabulary-for-wall-clock-selectors/alternatives-considered: + en: db5c53007cf0ae82 + zh: c654d66934c55d42 +/agent-note-shared-time-vocabulary-for-wall-clock-selectors/consequences: + en: a0a11d4bd4fa11e3 + zh: 3536397be8dd0add diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md new file mode 100644 index 0000000000..c44b802411 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md @@ -0,0 +1,39 @@ +# Agent Note: Shared time vocabulary for wall-clock selectors + +Status: implemented + +English | [中文](2026-09-24-shared-time-vocabulary.zh.md) + +## Problem + +`dsh-util-time` existed for one job and one caller shape: `canonicalClientTimeZone` validated a caller-reported zone at a wire boundary and answered `undefined`, which `dsh-api-session-controller` and `dsh-subagent` mapped onto their own domain errors. Schedule had grown the other half of the same vocabulary privately inside `packages/schedule/schedule/src/domain.ts`: a strict RFC 3339 instant parser, a local-calendar parser, a four-digit-year bound and canonical formatter, a local wall-clock resolver, and a second IANA zone canonicalizer that threw `ScheduleInputError` instead of answering a result. Two consequences followed. The same three regular expressions and the same `Intl.DateTimeFormat` canonicalization existed twice, so a change to either profile had to be made twice and verified twice. And the next consumer of a wall-clock selector had no shared entry point to reach for, which is why the profile would have been copied a third time rather than imported. + +## Decision + +`dsh-util-time` owns the time vocabulary that boundaries accept. `packages/util/time/src/instant.ts` holds it, `src/index.ts` re-exports it and keeps `canonicalClientTimeZone` as the `string | undefined` wrapper the existing wire boundaries consume. + +The library answers discriminated results and carries no failure vocabulary. `parseOffsetInstant`, `fourDigitYearEpoch`, `parseLocalCalendar`, and `canonicalizeTimeZone` answer an `accepted` arm or a `rejected` arm naming a stable reason (`not_rfc3339`, `not_a_calendar_date`, `not_a_wall_clock_time`, `invalid_numeric_offset`, `malformed`, `out_of_range`, `blank`, `padded`, `not_area_location`, `unsupported`, `not_canonical_shape`). Each caller maps a reason onto its own code and diagnostic, so the library never writes model-facing text. `formatFourDigitYearInstant` returns the canonical instant text or `undefined` outside the bound, and `resolveLocalInstant` answers epoch milliseconds or `undefined` for a wall-clock value inside a forward transition's gap. + +Schedule consumes it and keeps every domain decision. `canonicalizeTimeZone`, `decodeInstant`, `parseAtInput`, and `futureInstant` remain Schedule entry points with their existing codes and diagnostics; they now call the shared parser and map its reason. Schedule still owns the durable record shapes, the fold and transition authority, the injection-resistant framing, and all recurrence arithmetic: the cron engine, the daily and weekly date walks, the latest-due search, and the schedule-specific accepted-decision bound. + +The maintained `@js-temporal/polyfill` 0.5.1 becomes a runtime dependency of `dsh-util-time` as well as of `dsh-schedule`. Local resolution continues to use it, so the Host's IANA data still decides gaps and overlaps and no caller owns offset sampling. `util/time`'s README and description lose the `zero-dependency` claim the added dependency contradicts. + +Observable behavior does not change. Every accepted input resolves to the same epoch, every refused input raises the same `ScheduleInputError` code and message, and the stored `scheduledAt` profile is unchanged, so no durable record and no recorded snapshot is re-recorded. `packages/util/time/tests/instant.spec.ts` covers the new entries directly, per-file 100% included. + +## Alternatives considered + +**Keep the shared module private to Schedule.** A `packages/schedule/schedule/src/time.ts` extraction would shrink `domain.ts` without touching a published package, its lockfile entry, its README, or its dependency set. It was rejected because the duplication this change exists to remove is the duplicate *profile*, not the duplicate line count: the zone regular expression, the canonical `Intl` step, and the instant profile would still exist twice, and the next boundary that accepts a wall-clock selector would copy a third. + +**Give the library its own error type.** A `TimeInputError` carrying the reason would let callers rethrow instead of mapping, shortening each call site. It was rejected because `util/time`'s existing contract is that each boundary owns its refusal vocabulary, and Schedule's diagnostics are pinned verbatim by `tools.spec.ts`, snapshot expectations, and the model-visible tool result; a shared error class invites one caller's wording into another caller's surface. + +**Have the library return `undefined` instead of reasons.** That is the shape `canonicalClientTimeZone` already has, and it was the smaller change. It was rejected because Schedule must distinguish a malformed instant from a non-existent calendar date from an out-of-range clock field to keep its existing diagnostics, and `canonicalizeTimeZone` must distinguish a bad name from a locally unresolvable one; collapsing those to one falsy value would have forced Schedule to re-derive the distinction with its own regular expressions, restoring the duplication. + +**Add formatting and DST arithmetic for other consumers too.** No current consumer needs model-facing formatting or offset arithmetic from this library: `dsh-time-context` owns its own formatter and zone policy text. Adding them now would give the library a public operation set with no consumer evidence, against the package rule that ties each abstraction to a current owner and need. + +## Consequences + +The time profile now has one home. A change to the accepted instant profile or the zone profile is made once and covered once, and a new boundary imports it instead of copying it. Schedule's `domain.ts` loses the three regular expressions, the `Intl` canonicalization, the hand-rolled calendar conversion, and the four-digit bound arithmetic. + +The cost is a wider published surface and a heavier dependency graph: `dsh-util-time` gains a runtime dependency on `@js-temporal/polyfill`, so a consumer that imports only `canonicalClientTimeZone` now loads that module too. The library is also no longer a pure "validation and canonicalization only" package, which its README and package description had to state and which its Known Limitations section now records. Schedule keeps a thin mapping layer at each entry, so a reason added to the library's union fails the build at each caller through the exhaustive switch rather than silently taking a default. + +`dsh-time-context` keeps its own IANA regular expression and canonicalization, and stays out of this change. It rejects a non-canonical alias at the request boundary where Schedule canonicalizes one, so unifying it means deciding which of the two behaviors the browser-time-zone instruction wants; that decision belongs to the consumer, not to this extraction. diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md new file mode 100644 index 0000000000..4c1d054d35 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md @@ -0,0 +1,39 @@ +# Agent Note: 面向墙钟选择器的共享时间词汇 + +Status: implemented + +[English](2026-09-24-shared-time-vocabulary.md) | 中文 + +## Problem + +`dsh-util-time` 此前只承担一项职责、面向一种调用方:`canonicalClientTimeZone` 在协议边界校验调用方所报时区并回答 `undefined`,由 `dsh-api-session-controller` 与 `dsh-subagent` 映射成各自的域错误。同一套词汇的另一半则在 `packages/schedule/schedule/src/domain.ts` 内部私下生长:严格的 RFC 3339 时刻解析器、本地日历解析器、四位年份边界与规范格式化、本地墙钟解析,以及第二个 IANA 时区规范化实现——它抛 `ScheduleInputError` 而不是回答结果。由此产生两个后果。同样的三段正则与同样的 `Intl.DateTimeFormat` 规范化存在两份,任何一侧剖面变更都要改两遍、验两遍。而下一个需要墙钟选择器的消费方没有共享入口可用,这正是该剖面会被第三次复制而不是被导入的原因。 + +## Decision + +`dsh-util-time` 拥有各边界所接收的时间词汇。它位于 `packages/util/time/src/instant.ts`,`src/index.ts` 转出它,并保留 `canonicalClientTimeZone` 作为既有协议边界消费的 `string | undefined` 包装。 + +本库回答带判别值的结果,不持有失败词汇。`parseOffsetInstant`、`fourDigitYearEpoch`、`parseLocalCalendar` 与 `canonicalizeTimeZone` 回答 `accepted` 分支,或回答携带稳定原因的 `rejected` 分支(`not_rfc3339`、`not_a_calendar_date`、`not_a_wall_clock_time`、`invalid_numeric_offset`、`malformed`、`out_of_range`、`blank`、`padded`、`not_area_location`、`unsupported`、`not_canonical_shape`)。每个调用方把原因映射成自己的错误码与诊断文案,因此本库从不撰写面向模型的文本。`formatFourDigitYearInstant` 返回规范时刻文本,越界时返回 `undefined`;`resolveLocalInstant` 回答纪元毫秒,对落在前向跳变缺口内的墙钟值回答 `undefined`。 + +Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`decodeInstant`、`parseAtInput` 与 `futureInstant` 仍是 Schedule 的入口,保留既有错误码与诊断文案;它们现在调用共享解析器并映射其原因。Schedule 仍拥有持久记录结构、折叠与状态转移权威、抗注入的框架文本,以及全部重复规则运算:cron 引擎、每日与每周日期遍历、最近到期搜索,以及 Schedule 特有的决策时刻边界。 + +受维护的 `@js-temporal/polyfill` 0.5.1 在成为 `dsh-schedule` 运行时依赖之外,也成为 `dsh-util-time` 的运行时依赖。本地解析继续使用它,因此缺口与重叠仍由宿主 IANA 数据决定,也没有调用方需要自己实现偏移采样。`util/time` 的 README 与包描述删去了新增依赖所否定的 `zero-dependency` 表述。 + +可观察行为不变。每个被接受的输入解析出相同的纪元值,每个被拒绝的输入抛出相同的 `ScheduleInputError` 错误码与消息,存储的 `scheduledAt` 剖面不变,因此没有持久记录或录制快照需要重录。`packages/util/time/tests/instant.spec.ts` 直接覆盖新增入口,包含逐文件 100% 覆盖。 + +## Alternatives considered + +**把共享模块保持为 Schedule 私有。** 抽成 `packages/schedule/schedule/src/time.ts` 可以缩小 `domain.ts`,而不触碰已发布包、其 lockfile 条目、README 或依赖集合。之所以否决,是因为本次要消除的重复是重复的**剖面**,不是重复的行数:时区正则、`Intl` 规范化步骤与时刻剖面仍会存在两份,下一个接收墙钟选择器的边界仍会复制出第三份。 + +**让本库自带错误类型。** 一个携带原因的 `TimeInputError` 可以让调用方直接重抛而不做映射,缩短每个调用点。之所以否决,是因为 `util/time` 既有契约规定每个边界拥有自己的拒绝词汇,而 Schedule 的诊断文案被 `tools.spec.ts`、快照期望与模型可见工具结果逐字固定;共享错误类会把一个调用方的措辞带进另一个调用方的界面。 + +**让本库回答 `undefined` 而不是原因。** 这正是 `canonicalClientTimeZone` 已有的形态,也是改动更小的做法。之所以否决,是因为 Schedule 必须区分格式错误的时刻、日历上不存在的日期与越界时钟字段,才能保留既有诊断;`canonicalizeTimeZone` 也必须区分名称本身不合法与本地无法解析。把二者压成一个假值,会迫使 Schedule 用自己的正则重新推导这层区分,从而恢复重复。 + +**同时为其他消费方加入格式化与 DST 运算。** 目前没有消费方需要本库提供面向模型的格式化或偏移运算:`dsh-time-context` 自己拥有格式化器与时区策略文本。现在加入只会给本库一个没有消费方证据的公开操作集合,违背"每项抽象都绑定当前所有者与需求"的包规则。 + +## Consequences + +时间剖面现在只有一个归宿。时刻剖面或时区剖面的变更只做一次、只覆盖一次,新边界导入它而不是复制它。Schedule 的 `domain.ts` 去掉了三段正则、`Intl` 规范化、手写日历换算与四位年份边界运算。 + +代价是更宽的已发布接口与更重的依赖图:`dsh-util-time` 增加了对 `@js-temporal/polyfill` 的运行时依赖,因此只导入 `canonicalClientTimeZone` 的消费方也会加载该模块。本库也不再是纯"只做校验与规范化"的包——其 README 与包描述必须说明这一点,其"已知限制"一节现在也记录了它。Schedule 在每个入口保留一层薄映射,因此本库联合类型新增原因时,每个调用方都会因穷尽 switch 而构建失败,而不是静默走默认分支。 + +`dsh-time-context` 保留自己的 IANA 正则与规范化,不参与本次改动。它在请求边界拒绝非规范别名,而 Schedule 会把它规范化,因此统一二者意味着决定浏览器时区指令需要两种行为中的哪一种;该决定属于消费方,而不属于这次抽取。 diff --git a/packages/schedule/schedule/package.json b/packages/schedule/schedule/package.json index 24fb47514a..5f3a142054 100644 --- a/packages/schedule/schedule/package.json +++ b/packages/schedule/schedule/package.json @@ -60,6 +60,7 @@ "@deepseek-ai/dsh-storage-domain": "workspace:*", "@deepseek-ai/dsh-tools": "workspace:*", "@deepseek-ai/dsh-typert-protocol": "workspace:*", + "@deepseek-ai/dsh-util-time": "workspace:*", "@deepseek-ai/dsh-workspace": "workspace:*", "@deepseek-ai/cordis": "workspace:~" }, @@ -87,6 +88,7 @@ "@deepseek-ai/dsh-system-prompt": "workspace:*", "@deepseek-ai/dsh-tools": "workspace:*", "@deepseek-ai/dsh-typert-protocol": "workspace:*", + "@deepseek-ai/dsh-util-time": "workspace:*", "@deepseek-ai/dsh-workspace": "workspace:*", "@deepseek-ai/cordis": "workspace:~" } diff --git a/packages/schedule/schedule/src/domain.ts b/packages/schedule/schedule/src/domain.ts index 4f9b868478..46bdce8207 100644 --- a/packages/schedule/schedule/src/domain.ts +++ b/packages/schedule/schedule/src/domain.ts @@ -4,6 +4,17 @@ */ import { Temporal } from '@js-temporal/polyfill' +import { + MAX_FOUR_DIGIT_YEAR_MS, + MIN_FOUR_DIGIT_YEAR_MS, + canonicalizeTimeZone as canonicalizeSharedTimeZone, + formatFourDigitYearInstant, + fourDigitYearEpoch, + parseLocalCalendar, + parseOffsetInstant, + resolveLocalInstant, +} from '@deepseek-ai/dsh-util-time' +import type { LocalCalendarTime, TimeZoneRejection } from '@deepseek-ai/dsh-util-time' import { SessionLogOffset } from '@deepseek-ai/dsh-session' import type { SessionEvent, SessionLogOffset as SessionLogOffsetType } from '@deepseek-ai/dsh-session' import type { @@ -50,19 +61,7 @@ export const MAX_TITLE_LENGTH = 120 */ export const CRON_SEARCH_HORIZON_YEARS = 400 -const MIN_FOUR_DIGIT_YEAR_MS = Date.parse('0001-01-01T00:00:00.000Z') -const MAX_FOUR_DIGIT_YEAR_MS = Date.parse('9999-12-31T23:59:59.999Z') -const UTC_INSTANT = /^(?!0000)\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d\.\d{3}Z$/ -const OFFSET_INSTANT = new RegExp( - String.raw`^(?\d{4})-(?\d{2})-(?\d{2})` - + String.raw`T(?\d{2}):(?\d{2}):(?\d{2})` - + String.raw`(?:\.(?\d{1,3}))?(?Z|(?[+-])` - + String.raw`(?\d{2}):(?\d{2}))$`, -) -const LOCAL_DATE = /^(?\d{4})-(?\d{2})-(?\d{2})$/ -const LOCAL_TIME = /^(?\d{2}):(?\d{2}):(?\d{2})(?:\.(?\d{1,3}))?$/ const LOCAL_CLOCK_TIME = /^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d{1,3})?$/ -const IANA_ZONE = /^[A-Za-z][A-Za-z0-9_+.-]*(?:\/[A-Za-z0-9_+.-]+)+$/ /** Error from malformed or transition-invalid durable Schedule data. */ export class ScheduleLogError extends Error { @@ -252,114 +251,26 @@ function decodeId(value: unknown): ScheduleIdType { /** Validate one canonical four-digit-year UTC instant. */ function decodeInstant(value: unknown): string { - if (typeof value !== 'string' || !UTC_INSTANT.test(value)) { - throw new ScheduleLogError('scheduledAt must be a canonical four-digit-year RFC 3339 UTC instant') - } - const epoch = Date.parse(value) - if (!Number.isFinite(epoch) || new Date(epoch).toISOString() !== value) { - throw new ScheduleLogError('scheduledAt is not a real UTC calendar instant') - } - return value -} - -interface CalendarParts { - readonly year: number - readonly month: number - readonly day: number - readonly hour: number - readonly minute: number - readonly second: number - readonly millisecond: number -} - -/** Read one required named regular-expression group as a number. */ -function groupNumber(groups: Record, name: string): number { - const value = groups[name] - /* v8 ignore next -- successful fixed regexes always provide every requested group. */ - if (value === undefined) throw new ScheduleInputError('invalid_rule', 'The at value has an invalid shape.') - return Number(value) -} - -/** Convert exact calendar fields to a UTC-shaped epoch while rejecting normalization. */ -function calendarEpoch(parts: CalendarParts): number { - const value = new Date(0) - value.setUTCHours(0, 0, 0, 0) - value.setUTCFullYear(parts.year, parts.month - 1, parts.day) - value.setUTCHours(parts.hour, parts.minute, parts.second, parts.millisecond) - const epoch = value.getTime() - if (!Number.isFinite(epoch) - || value.getUTCFullYear() !== parts.year - || value.getUTCMonth() + 1 !== parts.month - || value.getUTCDate() !== parts.day - || value.getUTCHours() !== parts.hour - || value.getUTCMinutes() !== parts.minute - || value.getUTCSeconds() !== parts.second - || value.getUTCMilliseconds() !== parts.millisecond) { - throw new ScheduleInputError('invalid_rule', 'The at value must be a real ISO calendar date and time.') - } - return epoch -} - -/** Normalize an optional one-to-three digit fractional second to milliseconds. */ -function milliseconds(value: string | undefined): number { - return value === undefined ? 0 : Number(value.padEnd(3, '0')) -} - -/** Require a safe, representable, strictly future UTC target. */ -function futureInstant(epoch: number, now: number): string { - if (!Number.isSafeInteger(now) || !Number.isSafeInteger(epoch) - || epoch < MIN_FOUR_DIGIT_YEAR_MS || epoch > MAX_FOUR_DIGIT_YEAR_MS) { - throw new ScheduleInputError( - 'time_out_of_range', - 'The scheduled time must be representable as a four-digit-year RFC 3339 UTC instant.', + const parsed = typeof value === 'string' ? fourDigitYearEpoch(value) : undefined + if (parsed === undefined || parsed.kind === 'rejected') { + throw new ScheduleLogError( + parsed?.reason === 'not_a_calendar_date' + ? 'scheduledAt is not a real UTC calendar instant' + : 'scheduledAt must be a canonical four-digit-year RFC 3339 UTC instant', ) } - if (epoch <= now) { - throw new ScheduleInputError('not_future', 'The scheduled time must be strictly in the future.') - } - const instant = new Date(epoch).toISOString() - /* v8 ignore next -- an in-range integral Date always formats as the canonical UTC profile. */ - if (!UTC_INSTANT.test(instant)) { - throw new ScheduleInputError( - 'time_out_of_range', - 'The scheduled time must be representable as a four-digit-year RFC 3339 UTC instant.', - ) - } - return instant + return value as string } -/** Parse a strict RFC 3339 instant whose numeric offset is part of the input. */ -function parseOffsetInstant(value: string): number { - const match = OFFSET_INSTANT.exec(value) - const groups = match?.groups - if (groups === undefined) { - throw new ScheduleInputError( - 'invalid_rule', - 'at must use YYYY-MM-DDTHH:mm:ss with optional 1-3 digit fractional seconds and an explicit Z or numeric offset.', - ) - } - const parts: CalendarParts = { - year: groupNumber(groups, 'year'), - month: groupNumber(groups, 'month'), - day: groupNumber(groups, 'day'), - hour: groupNumber(groups, 'hour'), - minute: groupNumber(groups, 'minute'), - second: groupNumber(groups, 'second'), - millisecond: milliseconds(groups['fraction']), - } - if (parts.year === 0 || parts.hour > 23 || parts.minute > 59 || parts.second > 59) { - throw new ScheduleInputError('invalid_rule', 'The at value must be a real ISO calendar date and time.') - } - const localEpoch = calendarEpoch(parts) - if (groups['zone'] === 'Z') return localEpoch - const offsetHour = groupNumber(groups, 'offsetHour') - const offsetMinute = groupNumber(groups, 'offsetMinute') - if (offsetHour > 23 || offsetMinute > 59 - || (groups['sign'] === '-' && offsetHour === 0 && offsetMinute === 0)) { - throw new ScheduleInputError('invalid_rule', 'The at numeric offset is invalid.') - } - const direction = groups['sign'] === '+' ? 1 : -1 - return localEpoch - direction * (offsetHour * 60 + offsetMinute) * 60_000 +/** + * Map one shared zone rejection onto the stable Schedule input code. + * @param reason - Reason the shared vocabulary refused the name. + * @returns The public Schedule diagnostic for that refusal. + */ +function timeZoneError(reason: TimeZoneRejection): ScheduleInputError { + return reason === 'not_canonical_shape' + ? new ScheduleInputError('invalid_time_zone', 'time_zone must resolve to UTC or an IANA Area/Location name.') + : new ScheduleInputError('invalid_time_zone', 'time_zone must be UTC or a valid IANA Area/Location name.') } /** @@ -368,52 +279,23 @@ function parseOffsetInstant(value: string): number { * @returns The runtime's canonical IANA name. */ export function canonicalizeTimeZone(value: string): string { - if (value.length === 0 || value.trim() !== value || (value !== 'UTC' && !IANA_ZONE.test(value))) { - throw new ScheduleInputError('invalid_time_zone', 'time_zone must be UTC or a valid IANA Area/Location name.') - } - let canonical: string - try { - canonical = new Intl.DateTimeFormat('en-US', { timeZone: value }).resolvedOptions().timeZone - } catch (error: unknown) { - throw new ScheduleInputError( - 'invalid_time_zone', - 'time_zone must be UTC or a valid IANA Area/Location name.', - { cause: error }, - ) - } - /* v8 ignore next -- Intl returns the requested canonical zone or an IANA canonical alias. */ - if (canonical !== 'UTC' && !IANA_ZONE.test(canonical)) { - throw new ScheduleInputError('invalid_time_zone', 'time_zone must resolve to UTC or an IANA Area/Location name.') - } - return canonical + const resolved = canonicalizeSharedTimeZone(value) + if (resolved.kind === 'rejected') throw timeZoneError(resolved.reason) + return resolved.timeZone } /** Parse strict local calendar fields without consulting a process time zone. */ -function parseLocalAt(value: LocalAtInput): CalendarParts { - const dateMatch = LOCAL_DATE.exec(value.date) - const timeMatch = LOCAL_TIME.exec(value.time) - const date = dateMatch?.groups - const time = timeMatch?.groups - if (date === undefined || time === undefined) { +function parseLocalAt(value: LocalAtInput): LocalCalendarTime { + const parsed = parseLocalCalendar(value.date, value.time) + if (parsed.kind === 'rejected') { throw new ScheduleInputError( 'invalid_rule', - 'Local at requires date YYYY-MM-DD and time HH:mm:ss with optional one-to-three digit milliseconds.', + parsed.reason === 'malformed' + ? 'Local at requires date YYYY-MM-DD and time HH:mm:ss with optional one-to-three digit milliseconds.' + : 'The local at value must be a real ISO calendar date and time.', ) } - const parts: CalendarParts = { - year: groupNumber(date, 'year'), - month: groupNumber(date, 'month'), - day: groupNumber(date, 'day'), - hour: groupNumber(time, 'hour'), - minute: groupNumber(time, 'minute'), - second: groupNumber(time, 'second'), - millisecond: milliseconds(time['fraction']), - } - if (parts.year === 0 || parts.hour > 23 || parts.minute > 59 || parts.second > 59) { - throw new ScheduleInputError('invalid_rule', 'The local at value must be a real ISO calendar date and time.') - } - calendarEpoch(parts) - return parts + return parsed.time } /** Resolve only the earlier overlap instant; a gap fails the local-field round trip. */ @@ -423,14 +305,52 @@ function localInstant(local: Temporal.PlainDateTime, timeZone: string): number | } /** Resolve a local one-shot, rejecting nonexistent wall-clock times. */ -function resolveLocalInstant(parts: CalendarParts, timeZone: string): number { - const target = localInstant(Temporal.PlainDateTime.from(parts, { overflow: 'reject' }), timeZone) +function resolveLocalAt(parts: LocalCalendarTime, timeZone: string): number { + const target = resolveLocalInstant(parts, timeZone) if (target === undefined) { throw new ScheduleInputError('invalid_rule', 'The local at time does not exist in the selected time zone.') } return target } +/** Require a safe, representable, strictly future UTC target. */ +function futureInstant(epoch: number, now: number): string { + const instant = formatFourDigitYearInstant(epoch) + if (!Number.isSafeInteger(now) || instant === undefined) { + throw new ScheduleInputError( + 'time_out_of_range', + 'The scheduled time must be representable as a four-digit-year RFC 3339 UTC instant.', + ) + } + if (epoch <= now) { + throw new ScheduleInputError('not_future', 'The scheduled time must be strictly in the future.') + } + return instant +} + +/** Parse a strict RFC 3339 instant whose numeric offset is part of the input. */ +function parseAtOffsetInstant(value: string): number { + const parsed = parseOffsetInstant(value) + if (parsed.kind === 'accepted') return parsed.epoch + switch (parsed.reason) { + case 'not_rfc3339': + throw new ScheduleInputError( + 'invalid_rule', + 'at must use YYYY-MM-DDTHH:mm:ss with optional 1-3 digit fractional seconds and an explicit Z or numeric offset.', + ) + case 'invalid_numeric_offset': + throw new ScheduleInputError('invalid_rule', 'The at numeric offset is invalid.') + case 'not_a_calendar_date': + case 'not_a_wall_clock_time': + throw new ScheduleInputError('invalid_rule', 'The at value must be a real ISO calendar date and time.') + /* v8 ignore next 3 -- the shared parser returns a closed rejection union. */ + default: { + const unreachable: never = parsed.reason + throw new ScheduleLogError(`unknown instant rejection ${String(unreachable)}`) + } + } +} + /** Decode the exact v1 after record shape. */ function decodeAfterRecord(value: unknown): LegacyAfterScheduleRecord { /* v8 ignore next -- decodeLegacyScheduleRecord rejects a non-object before it dispatches here. */ @@ -1318,7 +1238,7 @@ export function createAtScheduleRecord( export function parseAtInput(at: AtInput): number { let target: number if (typeof at === 'string') { - target = parseOffsetInstant(at) + target = parseAtOffsetInstant(at) } else if (isRecord(at)) { if (!hasExactKeys(at, ['date', 'time', 'time_zone'])) { throw new ScheduleInputError('invalid_rule', 'Local at must contain exactly date, time, and time_zone.') @@ -1335,7 +1255,7 @@ export function parseAtInput(at: AtInput): number { time: at['time'], time_zone: rawTimeZone, } - target = resolveLocalInstant(parseLocalAt(local), canonicalizeTimeZone(rawTimeZone)) + target = resolveLocalAt(parseLocalAt(local), canonicalizeTimeZone(rawTimeZone)) } else { throw new ScheduleInputError('invalid_rule', 'at must be an explicit-offset string or local calendar object.') } diff --git a/packages/schedule/schedule/tsconfig.json b/packages/schedule/schedule/tsconfig.json index 6c0589042e..e3a3e1cd6c 100644 --- a/packages/schedule/schedule/tsconfig.json +++ b/packages/schedule/schedule/tsconfig.json @@ -47,6 +47,9 @@ { "path": "../../typert/protocol" }, + { + "path": "../../util/time" + }, { "path": "../../workspace/workspace" }, diff --git a/packages/util/README.i18n.yaml b/packages/util/README.i18n.yaml index 70665431a3..70526d2d7e 100644 --- a/packages/util/README.i18n.yaml +++ b/packages/util/README.i18n.yaml @@ -3,20 +3,20 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/util/README.md /: - en: 036b48d7ea9bdc0e - zh: e8e35eb01d8eb2d4 + en: 225f3432e1eb335a + zh: 5d5acd66295641da /util-shared-utilities: en: 7283f2afb343d150 zh: 32c56bd67c0314c6 /util-shared-utilities/summary: - en: 63e37eb06d07deb5 - zh: 26d94a8a8f9079ce + en: 2424d44f0d79be74 + zh: 5bc05166e8d83c1e /util-shared-utilities/table-of-contents: en: 1eb35b015a6c423f zh: a8a3dceee2c9e943 /util-shared-utilities/packages: - en: 7952d6444283d100 - zh: 091165eb224731f9 + en: 662b2baeac105692 + zh: f076c6d39c2985b1 /util-shared-utilities/related-documentation: en: 432ae81110914571 zh: c5bf4efc007311f3 diff --git a/packages/util/README.md b/packages/util/README.md index b5fca2657a..7184f8f595 100644 --- a/packages/util/README.md +++ b/packages/util/README.md @@ -1,5 +1,5 @@ --- -description: "Package map for shared utilities: atomic file writes, branded ids, deques, JSON values, harness home paths, launch environment, native commands, output retention, time zones, and timeouts." +description: "Package map for shared utilities: atomic file writes, branded ids, deques, JSON values, harness home paths, launch environment, native commands, output retention, time and zone vocabulary, and timeouts." kind: "package-group" --- @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -The `util/` group gives capability packages shared mechanical primitives instead of duplicate implementations. It covers atomic writes, branded ids, deques, lossless JSON values, UUIDs, Harness-home paths, launch environments, outbound proxy policy, native commands, output retention, time-zone canonicalization, and timeout handling. Every root entry here is a library: it registers no product service or event, and the consuming capability retains the business semantics. +The `util/` group gives capability packages shared mechanical primitives instead of duplicate implementations. It covers atomic writes, branded ids, deques, lossless JSON values, UUIDs, Harness-home paths, launch environments, outbound proxy policy, native commands, output retention, time-zone and instant vocabulary, and timeout handling. Every root entry here is a library: it registers no product service or event, and the consuming capability retains the business semantics. ## Table of Contents @@ -41,7 +41,7 @@ Each package provides one primitive; open a package page for how to use it. | [`workspace-path/`](workspace-path/README.md) | Provides browser-safe Workspace path and display helpers | | [`code-language/`](code-language/README.md) | Maps a filename suffix to the syntax-highlighting language that the Code preview, diff review, and the read card render | | [`output-retention/`](output-retention/README.md) | Bounds model-facing output and reports exact omission metadata | -| [`time/`](time/README.md) | Validates and canonicalizes a caller-reported IANA time zone | +| [`time/`](time/README.md) | Parses strict instants, local calendars, and caller-reported IANA zones | | [`timeout/`](timeout/README.md) | Deadline arithmetic, signal fusion, and timeout-versus-cancel classification | ----- diff --git a/packages/util/README.zh.md b/packages/util/README.zh.md index dfe7d06219..33253cc139 100644 --- a/packages/util/README.zh.md +++ b/packages/util/README.zh.md @@ -1,5 +1,5 @@ --- -description: "共享工具家族的包映射:原子文件写入、品牌化 id、双端队列、JSON 值、harness 主目录路径、启动环境、原生命令、输出保留、时区与超时。" +description: "共享工具家族的包映射:原子文件写入、品牌化 id、双端队列、JSON 值、harness 主目录路径、启动环境、原生命令、输出保留、时间与时区词汇、超时。" kind: "package-group" --- @@ -9,7 +9,7 @@ kind: "package-group" ## 概述 -`util/` 组为能力包提供共享的机制原语,避免重复实现。它涵盖原子写入、品牌化 id、双端队列、无损 JSON 值、UUID、Harness home 路径、启动环境、出站代理策略、原生命令、输出保留、时区规范化和超时处理。这里的每个根入口都是库:它不注册产品服务或事件,业务语义仍由消费它的能力负责。 +`util/` 组为能力包提供共享的机制原语,避免重复实现。它涵盖原子写入、品牌化 id、双端队列、无损 JSON 值、UUID、Harness home 路径、启动环境、出站代理策略、原生命令、输出保留、时间与时区词汇,以及超时处理。这里的每个根入口都是库:它不注册产品服务或事件,业务语义仍由消费它的能力负责。 ## 目录 @@ -41,7 +41,7 @@ kind: "package-group" | [`workspace-path/`](workspace-path/README.zh.md) | 提供浏览器安全的 Workspace 路径与显示辅助函数 | | [`code-language/`](code-language/README.zh.md) | 把文件名后缀映射为 Code 预览、diff 审阅与 read 卡片所渲染的语法高亮语言 | | [`output-retention/`](output-retention/README.zh.md) | 限制面向模型的输出并报告精确的省略元数据 | -| [`time/`](time/README.zh.md) | 校验并规范化调用方所报的 IANA 时区 | +| [`time/`](time/README.zh.md) | 解析严格的时刻、本地日历与调用方所报的 IANA 时区 | | [`timeout/`](timeout/README.zh.md) | 截止时间运算、信号融合与超时和取消分类 | ----- diff --git a/packages/util/time/README.i18n.yaml b/packages/util/time/README.i18n.yaml index 6e0e48d71a..3d25e2ee8d 100644 --- a/packages/util/time/README.i18n.yaml +++ b/packages/util/time/README.i18n.yaml @@ -3,32 +3,41 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/util/time/README.md /: - en: 1816778111548561 - zh: 460c67cc44e59372 + en: 78b8a5dd36b637f5 + zh: 451e95d5bc7d5a66 /dsh-util-time: en: bc299a9b8fe7717f zh: 896661ee0c4edf25 /dsh-util-time/summary: - en: f37c9eb6571622ac - zh: d782978ff11bd40f + en: 6d4d39093c2a2b5a + zh: 1dc4af01352581f2 /dsh-util-time/table-of-contents: - en: cd2a60a905c978d6 - zh: cc71d387d0cbd442 + en: d152484eb41ac6b4 + zh: f14f34fce35a8d90 /dsh-util-time/use-this-package: - en: 1472cf19b0bd7447 - zh: 73bf36759bc2d492 -/dsh-util-time/api: - en: cbb4b726e939fc03 - zh: e9df4a7487433146 + en: bbff250eba0f1b89 + zh: 0a618f05cc88ee2b +/dsh-util-time/use-this-package/when-to-use-it: + en: 1c5733835410b678 + zh: ddf336123fed82db +/dsh-util-time/use-this-package/entry-point: + en: 657c118ff4d9b99b + zh: 2b11d97a40a2044f +/dsh-util-time/understand-the-implementation: + en: 4daf141b0fbee0c4 + zh: 3dda79572f6c224b +/dsh-util-time/further-exploration: + en: 7730949c9d91249b + zh: 006dc7155d6b5afc /dsh-util-time/model-experience: - en: 02d997b5c1318f27 - zh: 5c55d060adea756d + en: 401c6ad02e1cdd75 + zh: fd72235cb50cb3fa /dsh-util-time/model-experience/kv-cache-effect: - en: f63edbd9621eee70 - zh: 5e3982508b32b039 + en: daf6e45a18c087b0 + zh: e6714f1e65ed55ce /dsh-util-time/known-limitations-and-deferred-work: - en: 130db809b9a6db90 - zh: 5b0b72ada5b131dd + en: 8b29ec4add8f93e6 + zh: ee9bfd6224a4204c /dsh-util-time/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: e88aeb617ffb74ea diff --git a/packages/util/time/README.md b/packages/util/time/README.md index 788d7310f5..9ba371598d 100644 --- a/packages/util/time/README.md +++ b/packages/util/time/README.md @@ -1,5 +1,5 @@ --- -description: "IANA time-zone validation and canonicalization for maintainers accepting a caller-reported zone at a wire boundary." +description: "Strict RFC 3339 instant, local-calendar, and IANA zone parsing for maintainers accepting a caller-reported zone or a wall-clock selector at a boundary." kind: "package-library" --- @@ -9,12 +9,13 @@ English | [中文](README.zh.md) ## Summary -Zero-dependency zone vocabulary for the wire boundaries that accept a caller's time zone. `canonicalClientTimeZone` admits `UTC` or an IANA `Area/Location` name and answers the platform-canonical spelling of it, so an alias never reaches a durable record: a zone identity is stored on messages and re-derived later by another process, where an alias would not compare equal. The library validates and canonicalizes only — it formats no time and owns no failure vocabulary, because each boundary throws its own domain code. +Shared time vocabulary for the boundaries that accept a caller's zone or a wall-clock selector. `canonicalClientTimeZone` admits `UTC` or an IANA `Area/Location` name and answers the platform-canonical spelling, so an alias never reaches a durable record. A wall-clock consumer instead parses strict RFC 3339 instants, local calendar fields, and zone names through discriminated results, then resolves a local value with maintained `Temporal` DST and overlap behavior. The library owns no failure vocabulary or model-facing text: each caller maps a reason onto its own domain code. ## Table of Contents - [Use this package](#use-this-package) -- [API](#api) +- [Understand the implementation](#understand-the-implementation) +- [Further Exploration](#further-exploration) - [Model Experience](#model-experience) - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work) - [Dev Note](#dev-note) @@ -24,40 +25,81 @@ Zero-dependency zone vocabulary for the wire boundaries that accept a caller's t ## Use this package +### When to use it + It is a **library, not a service or plugin**: no `ctx`, registers nothing, holds no state. -No runtime invariant companion is published because this pure utility owns no event stream or mutable runtime data; unit tests verify zone canonicalization. +Call it at the boundary that receives the zone or the wall-clock selector, before the value reaches anything durable. `dsh-api-session-controller` and `dsh-subagent` admit a caller-reported zone through `canonicalClientTimeZone`; `dsh-schedule` reads every absolute and wall-clock selector through the strict parsers and resolves its local targets through `resolveLocalInstant`. A consumer that owns calendar or interval arithmetic of its own keeps that arithmetic and calls these entries only for the boundary value. -Call it at the boundary that receives the zone, before the value reaches anything durable. An unusable name answers `undefined`, and the caller raises its own refusal — `session/invalid-time-zone` for the Session prompt, `subagent/invalid-time-zone` for a subagent continuation. +No runtime invariant companion is published because this pure utility owns no event stream or mutable runtime data; unit tests verify every entry. + +### Entry point + +```ts +import { canonicalClientTimeZone, fourDigitYearEpoch, parseLocalCalendar, resolveLocalInstant } from '@deepseek-ai/dsh-util-time' + +const zone = canonicalClientTimeZone('US/Eastern') // 'America/New_York' or undefined +const stored = fourDigitYearEpoch('2026-03-08T02:30:00.000Z') // { kind: 'accepted', epoch } or a rejection reason +const local = parseLocalCalendar('2026-03-08', '02:30:00') // { kind: 'accepted', time } or a rejection reason + +if (local.kind === 'accepted') { + resolveLocalInstant(local.time, zone ?? 'UTC') // epoch milliseconds, or undefined inside a DST gap +} +``` + +`canonicalClientTimeZone` answers `undefined` for a name it refuses, and the caller raises its own refusal — `session/invalid-time-zone` for the Session prompt, `subagent/invalid-time-zone` for a subagent continuation. Every other entry answers a discriminated result whose `rejected` arm carries a stable reason, so each caller again chooses its own code and diagnostic. ----- - -## API + +## Understand the implementation -```ts -import { canonicalClientTimeZone } from '@deepseek-ai/dsh-util-time' -``` +
+Implementation internals — click to expand -| Export | Role | +`src/instant.ts` holds the whole implementation; `src/index.ts` re-exports it and reduces `canonicalizeTimeZone` to the `string | undefined` form the wire boundaries already consume. The module JSDoc there states each entry's accepted profile and every reason. + +The strict parsers match a fixed regular expression before constructing any date, so a malformed spelling, an out-of-range clock field, and a date the calendar does not have answer distinct reasons. Local resolution defers to `@js-temporal/polyfill`: `disambiguation: 'earlier'` picks the first instant of an overlap, and the local-field round trip answers `undefined` for a value inside a forward transition's gap. + +| Entry | Accepted input | |---|---| -| `canonicalClientTimeZone(value)` | Canonical `UTC` or IANA `Area/Location` name for an accepted zone, `undefined` for a blank, padded, abbreviated, single-segment, or platform-unsupported one. | +| `canonicalClientTimeZone` | `UTC` or an IANA `Area/Location` name; answers `undefined` when refused. | +| `canonicalizeTimeZone` | The same names; answers the canonical spelling or a refusal reason. | +| `parseLocalCalendar` | `YYYY-MM-DD` and `HH:mm:ss`, with optional one-to-three fractional digits. | +| `parseOffsetInstant` | RFC 3339 with an explicit `Z` or numeric offset; the offset is part of the input. | +| `fourDigitYearEpoch` | The canonical stored profile: exactly three fractional digits and a `Z`. | +| `formatFourDigitYearInstant` | An epoch inside the four-digit-year range; answers `undefined` outside it. | +| `resolveLocalInstant` | Exact calendar fields and an already-canonical zone. | + +
+ +----- + + +## Further Exploration + +- [Schedule Host storage](../../schedule/schedule/README.md) — the consumer that stores committed UTC targets. +- [Session controller](../../api/session-controller/README.md) — the request boundary that canonicalizes a browser zone. +- [util/ package map](../README.md) — where this library sits among the shared utilities. + +----- ## Model Experience -Indirectly, through the consumer that records a canonical zone on a durable message, from which `dsh-time-context` renders the turn's model-visible zone instruction and timestamp. +Indirectly, through the consumers that store a canonical zone or a committed UTC instant on durable records and render model-visible scheduler text from them. #### KV Cache effect -None of its own. The consumer that injects a zone-derived line into a request owns that request's cache behavior. +None of its own. The consumer that injects a zone- or schedule-derived line into a request owns that request's cache behavior. ## Known Limitations and Deferred Work - **Alias resolution follows the runtime's ICU data** — which name an alias group canonicalizes to is the platform's answer, so two processes on different Node builds can disagree about it. -- **Validation only** — no formatting, offset arithmetic, DST reasoning, or instant conversion; consumers needing those use `Intl` directly. +- **No recurrence arithmetic** — instant validity, local-calendar parsing, zone resolution, and canonical formatting only; a caller that walks candidate dates or intervals owns that search. +- **The `Temporal` polyfill is a runtime dependency** — a consumer that imports only `canonicalClientTimeZone` still resolves it at module load, because both entries share one module. ### Dev Note diff --git a/packages/util/time/README.zh.md b/packages/util/time/README.zh.md index 48f9d60919..c850608eb3 100644 --- a/packages/util/time/README.zh.md +++ b/packages/util/time/README.zh.md @@ -1,5 +1,5 @@ --- -description: "面向在协议边界接收调用方所报时区的维护者,说明 IANA 时区校验与规范化。" +description: "面向在协议边界接收调用方所报时区或墙钟选择器的维护者,说明严格的 RFC 3339 时刻、本地日历与时区解析。" kind: "package-library" --- @@ -9,12 +9,13 @@ kind: "package-library" ## 概述 -零依赖的时区词汇,供接收调用方时区的协议边界使用。`canonicalClientTimeZone` 只接受 `UTC` 或 IANA `Area/Location` 名称,并回答该名称在当前平台上的规范拼写,因此别名不会进入持久记录:时区标识会存储在消息中,并由另一个进程稍后重新推导;别名在该进程中无法与规范名称比较为相等。本库只做校验与规范化——不格式化任何时间,也不持有失败词汇,因为每个边界抛自己的域码。 +共享时间词汇,供接收调用方时区或墙钟选择器的协议边界使用。`canonicalClientTimeZone` 只接受 `UTC` 或 IANA `Area/Location` 名称,并回答当前平台上的规范拼写,因此别名不会进入持久记录。需要墙钟语义的消费方则通过带判别值的结果解析严格的 RFC 3339 时刻、本地日历字段与时区名称,再借助受维护的 `Temporal` 解析本地值,获得 DST 与重叠时段的既定行为。本库不持有失败词汇,也不生成面向模型的文本:每个调用方把拒绝原因映射成自己的域错误码。 ## 目录 - [使用本包](#use-this-package) -- [API](#api) +- [理解实现](#understand-the-implementation) +- [延伸阅读](#further-exploration) - [模型体验](#model-experience) - [已知限制与延期工作](#known-limitations-and-deferred-work) - [开发备注](#dev-note) @@ -24,40 +25,81 @@ kind: "package-library" ## 使用本包 +### 何时使用 + 它是**库,不是服务也不是插件**:无 `ctx`、不注册任何东西、不持有状态。 -不发布运行时不变量伴生入口;这个纯工具不拥有事件流或可变运行时数据,时区规范化由单元测试验证。 +在接收时区或墙钟选择器的那个边界上调用它,让值在进入任何持久物之前先过一遍。`dsh-api-session-controller` 与 `dsh-subagent` 通过 `canonicalClientTimeZone` 接收调用方所报时区;`dsh-schedule` 通过严格解析器读取每一个绝对选择器与墙钟选择器,并通过 `resolveLocalInstant` 解析其本地目标。自行拥有日历或间隔运算的消费方保留那部分运算,只在边界值上调用这些入口。 -在接收时区的那个边界上调用它,让值在进入任何持久物之前先过一遍。不可用的名称回答 `undefined`,由调用方抛出自己的拒绝——Session 提示词用 `session/invalid-time-zone`,subagent 续话用 `subagent/invalid-time-zone`。 +不发布运行时不变量伴生入口;这个纯工具不拥有事件流或可变运行时数据,每个入口都由单元测试验证。 + +### 入口 + +```ts +import { canonicalClientTimeZone, fourDigitYearEpoch, parseLocalCalendar, resolveLocalInstant } from '@deepseek-ai/dsh-util-time' + +const zone = canonicalClientTimeZone('US/Eastern') // 'America/New_York' or undefined +const stored = fourDigitYearEpoch('2026-03-08T02:30:00.000Z') // { kind: 'accepted', epoch } or a rejection reason +const local = parseLocalCalendar('2026-03-08', '02:30:00') // { kind: 'accepted', time } or a rejection reason + +if (local.kind === 'accepted') { + resolveLocalInstant(local.time, zone ?? 'UTC') // epoch milliseconds, or undefined inside a DST gap +} +``` + +不可用的时区名称由 `canonicalClientTimeZone` 回答 `undefined`,由调用方抛出自己的拒绝——Session 提示词用 `session/invalid-time-zone`,subagent 续话用 `subagent/invalid-time-zone`。其余每个入口都回答带判别值的结果,其 `rejected` 分支携带稳定的拒绝原因,同样由调用方选择自己的错误码与诊断文案。 ----- - -## API + +## 理解实现 -```ts -import { canonicalClientTimeZone } from '@deepseek-ai/dsh-util-time' -``` +
+实现内部细节 —— 点击展开 -| 导出 | 职责 | +`src/instant.ts` 承载全部实现;`src/index.ts` 转出它,并把 `canonicalizeTimeZone` 归约为协议边界已经在消费的 `string | undefined` 形式。该模块的 JSDoc 说明每个入口接受的输入剖面与全部拒绝原因。 + +严格解析器先匹配固定正则,再构造任何日期,因此拼写错误、越界时钟字段与日历上不存在的日期分别回答不同的原因。本地解析交给 `@js-temporal/polyfill`:`disambiguation: 'earlier'` 取重叠时段中的第一个时刻,而本地字段往返比对对前向跳变缺口内的值回答 `undefined`。 + +| 入口 | 接受的输入 | |---|---| -| `canonicalClientTimeZone(value)` | 对接受的时区回答规范的 `UTC` 或 IANA `Area/Location` 名称;空串、带空白、缩写、单段或平台不支持的名称回答 `undefined`。 | +| `canonicalClientTimeZone` | `UTC` 或 IANA `Area/Location` 名称;被拒绝时回答 `undefined`。 | +| `canonicalizeTimeZone` | 同样的名称;回答规范拼写或拒绝原因。 | +| `parseLocalCalendar` | `YYYY-MM-DD` 与 `HH:mm:ss`,可带 1 到 3 位小数秒。 | +| `parseOffsetInstant` | 带显式 `Z` 或数字偏移的 RFC 3339;偏移量属于输入的一部分。 | +| `fourDigitYearEpoch` | 规范存储剖面:恰好 3 位小数秒与 `Z`。 | +| `formatFourDigitYearInstant` | 四位年份范围内的纪元毫秒;超出范围回答 `undefined`。 | +| `resolveLocalInstant` | 精确日历字段与已规范化的时区。 | + +
+ +----- + + +## 延伸阅读 + +- [Schedule 宿主存储](../../schedule/schedule/README.zh.md) —— 保存已提交 UTC 时点的消费方。 +- [Session 控制器](../../api/session-controller/README.zh.md) —— 规范化浏览器时区的请求边界。 +- [util/ 包地图](../README.zh.md) —— 本库在共享工具中的位置。 + +----- ## 模型体验 -间接影响,取决于把规范时区记到持久消息上的那个消费方——`dsh-time-context` 据此渲染该轮模型可见的时区指令与时间戳。 +间接影响,取决于把规范时区或已提交 UTC 时刻记到持久记录上、并据此渲染模型可见调度文本的那些消费方。 #### KV Cache 影响 -自身没有。把时区派生文本注入请求的那个消费方,对该请求的缓存行为负责。 +自身没有。把时区或调度的派生文本注入请求的那个消费方,对该请求的缓存行为负责。 ## 已知限制与延期工作 - **别名解析取决于运行时的 ICU 数据**——一个别名组规范化成哪个名称由平台回答,因此两个跑在不同 Node 构建上的进程可能给出不同答案。 -- **只做校验**——不格式化、不做偏移运算、不推导 DST、不做时刻换算;需要这些的消费方直接用 `Intl`。 +- **不含重复规则运算**——只做时刻有效性、本地日历解析、时区解析与规范格式化;遍历候选日期或间隔的搜索由调用方自己拥有。 +- **`Temporal` polyfill 是运行时依赖**——只导入 `canonicalClientTimeZone` 的消费方在模块加载时同样会解析它,因为两个入口共用一个模块。 ### 开发备注 diff --git a/packages/util/time/package.json b/packages/util/time/package.json index 8aea082411..ac2ebb4033 100644 --- a/packages/util/time/package.json +++ b/packages/util/time/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-util-time", - "description": "Zero-dependency time vocabulary shared by wire boundaries: canonicalClientTimeZone (IANA zone validation and canonicalization only, no formatting)", + "description": "Time vocabulary shared by wire boundaries: canonicalClientTimeZone (IANA zone validation and canonicalization), and strict RFC 3339 instant, local-calendar, and zone resolution for wall-clock selectors", "version": "0.1.7-rc.1", "publishConfig": { "access": "public" @@ -26,6 +26,9 @@ "lib/types/**/*.d.ts" ], "license": "MIT", + "dependencies": { + "@js-temporal/polyfill": "0.5.1" + }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~" }, diff --git a/packages/util/time/src/index.ts b/packages/util/time/src/index.ts index 1867965cf2..57c7b5d330 100644 --- a/packages/util/time/src/index.ts +++ b/packages/util/time/src/index.ts @@ -1,12 +1,17 @@ /** - * Time vocabulary shared by the wire boundaries that accept a caller's zone. - * Validation and canonicalization only: this library formats nothing and owns - * no failure vocabulary — each boundary declares and throws its own refusal. + * Time vocabulary shared by the boundaries that accept a caller's zone or a + * wall-clock selector. + * + * Validation, canonicalization, and strict parsing only: this library formats + * no model-facing text and owns no failure vocabulary — every entry answers a + * discriminated result, and each boundary maps a reason onto its own domain + * code and diagnostic. * @module @deepseek-ai/dsh-util-time */ -/** Strict browser-zone profile: UTC or an IANA Area/Location-style identifier. */ -const IANA_TIME_ZONE = /^[A-Za-z][A-Za-z0-9_+.-]*(?:\/[A-Za-z0-9_+.-]+)+$/ +import { canonicalizeTimeZone } from './instant.ts' + +export * from './instant.ts' /** * Validate and canonicalize one caller-supplied IANA zone at a wire boundary. @@ -18,16 +23,6 @@ const IANA_TIME_ZONE = /^[A-Za-z][A-Za-z0-9_+.-]*(?:\/[A-Za-z0-9_+.-]+)+$/ * @returns the canonical zone, or `undefined` when the name is unusable. */ export function canonicalClientTimeZone(value: string): string | undefined { - if (value.length === 0 || value.trim() !== value - || (value !== 'UTC' && !IANA_TIME_ZONE.test(value))) return undefined - try { - const canonical = new Intl.DateTimeFormat('en-US', { timeZone: value }) - .resolvedOptions().timeZone - /* v8 ignore next -- Intl returns UTC or a canonical IANA Area/Location for accepted input. */ - if (canonical !== 'UTC' && !IANA_TIME_ZONE.test(canonical)) return undefined - return canonical - } catch { - // Intl rejects unsupported zone names; the caller maps that parser rejection. - return undefined - } + const resolved = canonicalizeTimeZone(value) + return resolved.kind === 'accepted' ? resolved.timeZone : undefined } diff --git a/packages/util/time/src/instant.ts b/packages/util/time/src/instant.ts new file mode 100644 index 0000000000..8a52c4a6dc --- /dev/null +++ b/packages/util/time/src/instant.ts @@ -0,0 +1,279 @@ +/** + * Strict instant and local-calendar vocabulary for the boundaries that accept a + * wall-clock selector. + * + * Every entry answers a discriminated result instead of throwing: the library + * carries no failure vocabulary, and each caller maps a reason onto its own + * domain code and diagnostic. Local wall-clock resolution uses the maintained + * `Temporal` implementation, so no caller owns offset sampling or DST rules. + * @module @deepseek-ai/dsh-util-time + */ + +import { Temporal } from '@js-temporal/polyfill' + +/** Inclusive epoch-millisecond bounds of a four-digit-year RFC 3339 UTC instant. */ +export const MIN_FOUR_DIGIT_YEAR_MS = Date.parse('0001-01-01T00:00:00.000Z') + +/** Inclusive epoch-millisecond bounds of a four-digit-year RFC 3339 UTC instant. */ +export const MAX_FOUR_DIGIT_YEAR_MS = Date.parse('9999-12-31T23:59:59.999Z') + +/** Canonical four-digit-year RFC 3339 UTC instant with mandatory milliseconds. */ +const UTC_INSTANT = + /^(?!0000)\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d\.\d{3}Z$/ + +/** RFC 3339 instant with mandatory date, time, and explicit zone; fraction is optional. */ +const OFFSET_INSTANT = new RegExp( + String.raw`^(?\d{4})-(?\d{2})-(?\d{2})` + + String.raw`T(?\d{2}):(?\d{2}):(?\d{2})` + + String.raw`(?:\.(?\d{1,3}))?(?Z|(?[+-])` + + String.raw`(?\d{2}):(?\d{2}))$`, +) + +/** Four-digit ISO calendar date. */ +const LOCAL_DATE = /^(?\d{4})-(?\d{2})-(?\d{2})$/ + +/** Local wall-clock time with optional one-to-three digit fractional seconds. */ +const LOCAL_TIME = /^(?\d{2}):(?\d{2}):(?\d{2})(?:\.(?\d{1,3}))?$/ + +/** Strict browser-zone profile: UTC or an IANA Area/Location-style identifier. */ +const IANA_TIME_ZONE = /^[A-Za-z][A-Za-z0-9_+.-]*(?:\/[A-Za-z0-9_+.-]+)+$/ + +/** Exact calendar fields of one local wall-clock value. */ +export interface LocalCalendarTime { + /** Four-digit year; year 0 is rejected. */ + readonly year: number + /** Month from 1 through 12. */ + readonly month: number + /** Day of month from 1 through the month's length. */ + readonly day: number + /** Hour from 0 through 23. */ + readonly hour: number + /** Minute from 0 through 59. */ + readonly minute: number + /** Second from 0 through 59; leap seconds are rejected. */ + readonly second: number + /** Millisecond from 0 through 999. */ + readonly millisecond: number +} + +/** Why an RFC 3339 instant string is unusable. */ +export type InstantRejection = + | 'not_rfc3339' + | 'not_a_calendar_date' + | 'not_a_wall_clock_time' + | 'invalid_numeric_offset' + +/** Why a local calendar selector is unusable. */ +export type LocalCalendarRejection = 'malformed' | 'out_of_range' | 'not_a_calendar_date' + +/** Why one IANA zone selector is unusable. */ +export type TimeZoneRejection = + | 'blank' + | 'padded' + | 'not_area_location' + | 'unsupported' + | 'not_canonical_shape' + +/** Discriminated result of parsing one RFC 3339 instant string. */ +export type InstantResult = + | { readonly kind: 'accepted'; readonly epoch: number } + | { readonly kind: 'rejected'; readonly reason: InstantRejection } + +/** Discriminated result of parsing one local calendar selector. */ +export type LocalCalendarResult = + | { readonly kind: 'accepted'; readonly time: LocalCalendarTime } + | { readonly kind: 'rejected'; readonly reason: LocalCalendarRejection } + +/** Discriminated result of resolving one raw IANA zone selector. */ +export type TimeZoneResult = + | { readonly kind: 'accepted'; readonly timeZone: string } + | { readonly kind: 'rejected'; readonly reason: TimeZoneRejection } + +/** Reject with one stable reason. */ +function rejected(reason: Reason): { kind: 'rejected'; reason: Reason } { + return Object.freeze({ kind: 'rejected', reason } as const) +} + +/** Read one required named regular-expression group as a number. */ +function groupNumber(groups: Record, name: string): number { + const value = groups[name] + /* v8 ignore next -- a successful fixed regex always provides every requested group. */ + if (value === undefined) throw new TypeError(`missing required regex group ${name}`) + return Number(value) +} + +/** Normalize an optional one-to-three digit fractional second to milliseconds. */ +function fractionalMilliseconds(value: string | undefined): number { + return value === undefined ? 0 : Number(value.padEnd(3, '0')) +} + +/** Whether an integral epoch is inside the four-digit-year range. */ +function inFourDigitYearRange(epoch: number): boolean { + return Number.isSafeInteger(epoch) && epoch >= MIN_FOUR_DIGIT_YEAR_MS && epoch <= MAX_FOUR_DIGIT_YEAR_MS +} + +/** + * Build one plain date-time while refusing a date the calendar does not have. + * + * Every field is already range-checked by the caller, so the only refusal left + * is a non-existent calendar date such as February 30. + * @param time - Calendar fields with every numeric field in range. + * @returns The plain date-time, or `undefined` when the date does not exist. + */ +function plainDateTime(time: LocalCalendarTime): Temporal.PlainDateTime | undefined { + try { + return Temporal.PlainDateTime.from(time, { overflow: 'reject' }) + } catch (error: unknown) { + /* v8 ignore next -- every numeric field is pre-checked, so only a non-existent date reaches here. */ + if (error instanceof RangeError) return undefined + /* v8 ignore next -- Temporal throws only RangeError for rejected overflow. */ + throw error + } +} + +/** + * Read one local calendar selector strictly, without consulting a process zone. + * + * The date and time texts are matched separately, so a malformed spelling, a + * non-existent calendar date such as February 30, and an out-of-range clock + * field answer distinct reasons. + * @param date - Candidate `YYYY-MM-DD` date text. + * @param time - Candidate `HH:mm:ss` time text with optional 1-3 fractional digits. + * @returns The parsed calendar fields, or the reason they are unusable. + */ +export function parseLocalCalendar(date: string, time: string): LocalCalendarResult { + const dateMatch = LOCAL_DATE.exec(date) + const timeMatch = LOCAL_TIME.exec(time) + const dateGroups = dateMatch?.groups + const timeGroups = timeMatch?.groups + if (dateGroups === undefined || timeGroups === undefined) return rejected('malformed') + const parts: LocalCalendarTime = { + year: groupNumber(dateGroups, 'year'), + month: groupNumber(dateGroups, 'month'), + day: groupNumber(dateGroups, 'day'), + hour: groupNumber(timeGroups, 'hour'), + minute: groupNumber(timeGroups, 'minute'), + second: groupNumber(timeGroups, 'second'), + millisecond: fractionalMilliseconds(timeGroups['fraction']), + } + if (parts.year === 0 || parts.hour > 23 || parts.minute > 59 || parts.second > 59) { + return rejected('out_of_range') + } + if (plainDateTime(parts) === undefined) return rejected('not_a_calendar_date') + return Object.freeze({ kind: 'accepted', time: parts }) +} + +/** + * Parse one RFC 3339 instant whose numeric offset is part of the input. + * + * A bare `Z` and an explicit numeric offset are both accepted, and the offset is + * subtracted from the local wall-clock value. A negative zero offset, an offset + * whose hour or minute is out of range, and a non-existent calendar date are refused. + * @param value - Candidate instant text. + * @returns The resolved epoch milliseconds, or the reason the text is unusable. + */ +export function parseOffsetInstant(value: string): InstantResult { + const groups = OFFSET_INSTANT.exec(value)?.groups + if (groups === undefined) return rejected('not_rfc3339') + const parts: LocalCalendarTime = { + year: groupNumber(groups, 'year'), + month: groupNumber(groups, 'month'), + day: groupNumber(groups, 'day'), + hour: groupNumber(groups, 'hour'), + minute: groupNumber(groups, 'minute'), + second: groupNumber(groups, 'second'), + millisecond: fractionalMilliseconds(groups['fraction']), + } + if (parts.year === 0 || parts.hour > 23 || parts.minute > 59 || parts.second > 59) { + return rejected('not_a_wall_clock_time') + } + const local = plainDateTime(parts) + if (local === undefined) return rejected('not_a_calendar_date') + const localEpoch = local.toZonedDateTime('UTC').epochMilliseconds + if (groups['zone'] === 'Z') return Object.freeze({ kind: 'accepted', epoch: localEpoch }) + const offsetHour = groupNumber(groups, 'offsetHour') + const offsetMinute = groupNumber(groups, 'offsetMinute') + if (offsetHour > 23 || offsetMinute > 59 + || (groups['sign'] === '-' && offsetHour === 0 && offsetMinute === 0)) { + return rejected('invalid_numeric_offset') + } + const direction = groups['sign'] === '+' ? 1 : -1 + return Object.freeze({ + kind: 'accepted', + epoch: localEpoch - direction * (offsetHour * 60 + offsetMinute) * 60_000, + }) +} + +/** + * Validate one canonical four-digit-year RFC 3339 UTC instant. + * + * The accepted profile carries exactly three fractional digits and a `Z`, and + * every year from 0001 through 9999 whose calendar date exists is representable. + * @param value - Untrusted candidate instant. + * @returns The epoch milliseconds, or the reason the text is not the canonical profile. + */ +export function fourDigitYearEpoch(value: string): InstantResult { + if (!UTC_INSTANT.test(value)) return rejected('not_rfc3339') + const epoch = Date.parse(value) + if (!Number.isFinite(epoch) || !inFourDigitYearRange(epoch) || new Date(epoch).toISOString() !== value) { + return rejected('not_a_calendar_date') + } + return Object.freeze({ kind: 'accepted', epoch }) +} + +/** + * Format one epoch as the canonical four-digit-year RFC 3339 UTC instant. + * + * The bound check runs first, so a caller that also compares the result with a + * decision time never formats an unrepresentable instant. + * @param epoch - Candidate epoch milliseconds. + * @returns The canonical instant text, or `undefined` when the epoch is unrepresentable. + */ +export function formatFourDigitYearInstant(epoch: number): string | undefined { + if (!inFourDigitYearRange(epoch)) return undefined + const instant = new Date(epoch).toISOString() + /* v8 ignore next -- an in-range integral Date always formats as the canonical UTC profile. */ + if (!UTC_INSTANT.test(instant)) return undefined + return instant +} + +/** + * Validate and canonicalize one raw IANA zone selector. + * + * A zone identity is stored on durable records and resolved again by another + * process, so an alias is answered by its platform-canonical name. Abbreviations, + * numeric offsets, single-segment names, and unknown names are refused. + * @param value - Candidate `UTC` or IANA Area/Location name. + * @returns The canonical zone, or the reason the name is unusable. + */ +export function canonicalizeTimeZone(value: string): TimeZoneResult { + if (value.length === 0) return rejected('blank') + if (value.trim() !== value) return rejected('padded') + if (value !== 'UTC' && !IANA_TIME_ZONE.test(value)) return rejected('not_area_location') + let canonical: string + try { + canonical = new Intl.DateTimeFormat('en-US', { timeZone: value }).resolvedOptions().timeZone + } catch { + // Intl rejects a well-formed name the platform's IANA data does not know. + return rejected('unsupported') + } + /* v8 ignore next -- Intl answers UTC or a canonical IANA Area/Location for every accepted name. */ + if (canonical !== 'UTC' && !IANA_TIME_ZONE.test(canonical)) return rejected('not_canonical_shape') + return Object.freeze({ kind: 'accepted', timeZone: canonical }) +} + +/** + * Resolve one local wall-clock value in an explicit zone. + * + * A value inside a forward transition's gap does not exist, so it answers + * `undefined`; a value inside an overlap resolves to the earlier instant, which + * is the same choice the maintained `Temporal` implementation makes. + * @param time - Exact local calendar fields. + * @param timeZone - Already-canonical `UTC` or IANA zone. + * @returns The resolved epoch milliseconds, or `undefined` for a nonexistent wall-clock time. + */ +export function resolveLocalInstant(time: LocalCalendarTime, timeZone: string): number | undefined { + const local = Temporal.PlainDateTime.from(time, { overflow: 'reject' }) + const zoned = local.toZonedDateTime(timeZone, { disambiguation: 'earlier' }) + return zoned.toPlainDateTime().equals(local) ? zoned.epochMilliseconds : undefined +} diff --git a/packages/util/time/tests/instant.spec.ts b/packages/util/time/tests/instant.spec.ts new file mode 100644 index 0000000000..0920c37351 --- /dev/null +++ b/packages/util/time/tests/instant.spec.ts @@ -0,0 +1,239 @@ +import { describe, expect, it } from 'vitest' +import { + MAX_FOUR_DIGIT_YEAR_MS, + MIN_FOUR_DIGIT_YEAR_MS, + canonicalizeTimeZone, + formatFourDigitYearInstant, + fourDigitYearEpoch, + parseLocalCalendar, + parseOffsetInstant, + resolveLocalInstant, +} from '@deepseek-ai/dsh-util-time' + +describe('parseLocalCalendar', () => { + it('reads exact calendar fields and normalizes an absent fraction to zero', () => { + expect(parseLocalCalendar('2026-03-08', '02:30:00')).toEqual({ + kind: 'accepted', + time: { year: 2026, month: 3, day: 8, hour: 2, minute: 30, second: 0, millisecond: 0 }, + }) + }) + + it('pads a one- or two-digit fraction to milliseconds', () => { + expect(parseLocalCalendar('2026-03-08', '02:30:00.5')).toEqual({ + kind: 'accepted', + time: { year: 2026, month: 3, day: 8, hour: 2, minute: 30, second: 0, millisecond: 500 }, + }) + expect(parseLocalCalendar('2026-03-08', '02:30:00.25')).toEqual({ + kind: 'accepted', + time: { year: 2026, month: 3, day: 8, hour: 2, minute: 30, second: 0, millisecond: 250 }, + }) + }) + + it('distinguishes malformed text from an out-of-range field', () => { + expect(parseLocalCalendar('2026-3-08', '02:30:00').kind).toBe('rejected') + expect(parseLocalCalendar('2026-3-08', '02:30:00')).toEqual({ kind: 'rejected', reason: 'malformed' }) + expect(parseLocalCalendar('2026-03-08', '2:30:00')).toEqual({ kind: 'rejected', reason: 'malformed' }) + expect(parseLocalCalendar('2026-03-08', '02:30:00.1234')).toEqual({ kind: 'rejected', reason: 'malformed' }) + expect(parseLocalCalendar('0000-03-08', '02:30:00')).toEqual({ kind: 'rejected', reason: 'out_of_range' }) + expect(parseLocalCalendar('2026-03-08', '24:00:00')).toEqual({ kind: 'rejected', reason: 'out_of_range' }) + expect(parseLocalCalendar('2026-03-08', '02:60:00')).toEqual({ kind: 'rejected', reason: 'out_of_range' }) + expect(parseLocalCalendar('2026-03-08', '02:30:60')).toEqual({ kind: 'rejected', reason: 'out_of_range' }) + }) + + it('rejects a date the calendar does not have', () => { + expect(parseLocalCalendar('2026-02-30', '02:30:00')).toEqual({ + kind: 'rejected', + reason: 'not_a_calendar_date', + }) + expect(parseLocalCalendar('2025-02-29', '02:30:00')).toEqual({ + kind: 'rejected', + reason: 'not_a_calendar_date', + }) + expect(parseLocalCalendar('2024-02-29', '02:30:00').kind).toBe('accepted') + }) +}) + +describe('parseOffsetInstant', () => { + it('reads a Z instant as its UTC epoch', () => { + expect(parseOffsetInstant('2026-03-08T02:30:00Z')).toEqual({ + kind: 'accepted', + epoch: Date.parse('2026-03-08T02:30:00.000Z'), + }) + }) + + it('subtracts a numeric offset from the written wall-clock value', () => { + const wallClock = Date.parse('2026-03-08T02:30:00.000Z') + expect(parseOffsetInstant('2026-03-08T02:30:00+08:00')).toEqual({ + kind: 'accepted', + epoch: wallClock - 8 * 3_600_000, + }) + expect(parseOffsetInstant('2026-03-08T02:30:00-05:30')).toEqual({ + kind: 'accepted', + epoch: wallClock + 5.5 * 3_600_000, + }) + }) + + it('normalizes an optional one-to-three digit fraction to milliseconds', () => { + expect(parseOffsetInstant('2026-03-08T02:30:00.5Z')).toEqual({ + kind: 'accepted', + epoch: Date.parse('2026-03-08T02:30:00.500Z'), + }) + expect(parseOffsetInstant('2026-03-08T02:30:00.25Z')).toEqual({ + kind: 'accepted', + epoch: Date.parse('2026-03-08T02:30:00.250Z'), + }) + }) + + it('rejects text that is not the accepted RFC 3339 profile', () => { + for (const value of [ + '2026-03-08T02:30:00', + '2026-03-08 02:30:00Z', + '2026-03-08T02:30Z', + '2026-03-08T02:30:00.1234Z', + '2026-3-08T02:30:00Z', + ]) { + expect(parseOffsetInstant(value)).toEqual({ kind: 'rejected', reason: 'not_rfc3339' }) + } + }) + + it('rejects a clock field, a zero year, and a non-existent calendar date', () => { + expect(parseOffsetInstant('0000-03-08T02:30:00Z')).toEqual({ + kind: 'rejected', + reason: 'not_a_wall_clock_time', + }) + expect(parseOffsetInstant('2026-03-08T24:30:00Z')).toEqual({ + kind: 'rejected', + reason: 'not_a_wall_clock_time', + }) + expect(parseOffsetInstant('2026-03-08T02:60:00Z')).toEqual({ + kind: 'rejected', + reason: 'not_a_wall_clock_time', + }) + expect(parseOffsetInstant('2026-03-08T02:30:60Z')).toEqual({ + kind: 'rejected', + reason: 'not_a_wall_clock_time', + }) + expect(parseOffsetInstant('2026-02-30T02:30:00Z')).toEqual({ + kind: 'rejected', + reason: 'not_a_calendar_date', + }) + }) + + it('rejects an unusable numeric offset', () => { + expect(parseOffsetInstant('2026-03-08T02:30:00+24:00')).toEqual({ + kind: 'rejected', + reason: 'invalid_numeric_offset', + }) + expect(parseOffsetInstant('2026-03-08T02:30:00+08:60')).toEqual({ + kind: 'rejected', + reason: 'invalid_numeric_offset', + }) + expect(parseOffsetInstant('2026-03-08T02:30:00-00:00')).toEqual({ + kind: 'rejected', + reason: 'invalid_numeric_offset', + }) + expect(parseOffsetInstant('2026-03-08T02:30:00+00:00')).toEqual({ + kind: 'accepted', + epoch: Date.parse('2026-03-08T02:30:00.000Z'), + }) + }) +}) + +describe('fourDigitYearEpoch', () => { + it('accepts the canonical profile and answers its epoch', () => { + expect(fourDigitYearEpoch('2026-03-08T02:30:00.000Z')).toEqual({ + kind: 'accepted', + epoch: Date.parse('2026-03-08T02:30:00.000Z'), + }) + expect(fourDigitYearEpoch('0001-01-01T00:00:00.000Z')).toEqual({ + kind: 'accepted', + epoch: MIN_FOUR_DIGIT_YEAR_MS, + }) + expect(fourDigitYearEpoch('9999-12-31T23:59:59.999Z')).toEqual({ + kind: 'accepted', + epoch: MAX_FOUR_DIGIT_YEAR_MS, + }) + }) + + it('rejects everything outside the canonical profile', () => { + for (const value of [ + '2026-03-08T02:30:00Z', + '2026-03-08T02:30:00.000+00:00', + '2026-03-08T02:30:00.00Z', + '0000-01-01T00:00:00.000Z', + '2026-3-08T02:30:00.000Z', + ]) { + expect(fourDigitYearEpoch(value)).toEqual({ kind: 'rejected', reason: 'not_rfc3339' }) + } + }) + + it('rejects a profile-shaped text whose date does not exist', () => { + expect(fourDigitYearEpoch('2026-02-30T02:30:00.000Z')).toEqual({ + kind: 'rejected', + reason: 'not_a_calendar_date', + }) + }) +}) + +describe('formatFourDigitYearInstant', () => { + it('formats both inclusive bounds in the canonical profile', () => { + expect(formatFourDigitYearInstant(MIN_FOUR_DIGIT_YEAR_MS)).toBe('0001-01-01T00:00:00.000Z') + expect(formatFourDigitYearInstant(MAX_FOUR_DIGIT_YEAR_MS)).toBe('9999-12-31T23:59:59.999Z') + }) + + it('refuses a non-integral or out-of-range epoch', () => { + for (const epoch of [MIN_FOUR_DIGIT_YEAR_MS - 1, MAX_FOUR_DIGIT_YEAR_MS + 1, 0.5, Number.NaN]) { + expect(formatFourDigitYearInstant(epoch)).toBeUndefined() + } + }) +}) + +describe('canonicalizeTimeZone', () => { + it('answers the runtime-canonical name', () => { + expect(canonicalizeTimeZone('UTC')).toEqual({ kind: 'accepted', timeZone: 'UTC' }) + expect(canonicalizeTimeZone('US/Eastern')).toEqual({ kind: 'accepted', timeZone: 'America/New_York' }) + expect(canonicalizeTimeZone('America/New_York')).toEqual({ + kind: 'accepted', + timeZone: 'America/New_York', + }) + }) + + it('reports why a name is refused', () => { + expect(canonicalizeTimeZone('')).toEqual({ kind: 'rejected', reason: 'blank' }) + expect(canonicalizeTimeZone(' UTC')).toEqual({ kind: 'rejected', reason: 'padded' }) + expect(canonicalizeTimeZone('UTC ')).toEqual({ kind: 'rejected', reason: 'padded' }) + expect(canonicalizeTimeZone('Asia')).toEqual({ kind: 'rejected', reason: 'not_area_location' }) + expect(canonicalizeTimeZone('GMT+8')).toEqual({ kind: 'rejected', reason: 'not_area_location' }) + expect(canonicalizeTimeZone('utc')).toEqual({ kind: 'rejected', reason: 'not_area_location' }) + expect(canonicalizeTimeZone('Not/A_Real_Zone')).toEqual({ kind: 'rejected', reason: 'unsupported' }) + }) +}) + +describe('resolveLocalInstant', () => { + const local = (hour: number, minute: number) => ({ + year: 2026, month: 3, day: 8, hour, minute, second: 0, millisecond: 0, + }) + + it('resolves a wall-clock value that exists exactly once', () => { + expect(resolveLocalInstant(local(12, 0), 'America/New_York')).toBe( + Date.parse('2026-03-08T16:00:00.000Z'), + ) + }) + + it('answers undefined for a forward-transition gap', () => { + // 2026-03-08T02:30 local does not exist in America/New_York. + expect(resolveLocalInstant(local(2, 30), 'America/New_York')).toBeUndefined() + }) + + it('selects the earlier instant of a backward-transition overlap', () => { + // 2026-11-01T01:30 local occurs twice in America/New_York. + const overlap = { ...local(1, 30), month: 11, day: 1 } + expect(resolveLocalInstant(overlap, 'America/New_York')).toBe( + Date.parse('2026-11-01T05:30:00.000Z'), + ) + }) + + it('resolves UTC without a transition', () => { + expect(resolveLocalInstant(local(2, 30), 'UTC')).toBe(Date.parse('2026-03-08T02:30:00.000Z')) + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4bcc5c5e0a..3b5b05c5b6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10251,6 +10251,9 @@ importers: '@deepseek-ai/dsh-typert-protocol': specifier: workspace:* version: link:../../typert/protocol + '@deepseek-ai/dsh-util-time': + specifier: workspace:* + version: link:../../util/time '@deepseek-ai/dsh-workspace': specifier: workspace:* version: link:../../workspace/workspace @@ -13286,6 +13289,10 @@ importers: version: link:../../../vendor/cordis packages/util/time: + dependencies: + '@js-temporal/polyfill': + specifier: 0.5.1 + version: 0.5.1 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ From 7ee7dfde55b2ef0b27825a87ee14551b0224dc97 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Thu, 24 Sep 2026 13:00:40 +0800 Subject: [PATCH 002/369] fix(schedule): preserve both local-calendar diagnostics A calendar date the month does not have keeps the at diagnostic; only an out-of-range clock field keeps the local at one. The extraction had collapsed both onto the local at wording. --- packages/schedule/schedule/src/domain.ts | 26 ++++++++++++++----- .../schedule/schedule/tests/domain.spec.ts | 19 ++++++++++++++ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/packages/schedule/schedule/src/domain.ts b/packages/schedule/schedule/src/domain.ts index 46bdce8207..ed970e1800 100644 --- a/packages/schedule/schedule/src/domain.ts +++ b/packages/schedule/schedule/src/domain.ts @@ -14,7 +14,7 @@ import { parseOffsetInstant, resolveLocalInstant, } from '@deepseek-ai/dsh-util-time' -import type { LocalCalendarTime, TimeZoneRejection } from '@deepseek-ai/dsh-util-time' +import type { LocalCalendarRejection, LocalCalendarTime, TimeZoneRejection } from '@deepseek-ai/dsh-util-time' import { SessionLogOffset } from '@deepseek-ai/dsh-session' import type { SessionEvent, SessionLogOffset as SessionLogOffsetType } from '@deepseek-ai/dsh-session' import type { @@ -288,16 +288,28 @@ export function canonicalizeTimeZone(value: string): string { function parseLocalAt(value: LocalAtInput): LocalCalendarTime { const parsed = parseLocalCalendar(value.date, value.time) if (parsed.kind === 'rejected') { - throw new ScheduleInputError( - 'invalid_rule', - parsed.reason === 'malformed' - ? 'Local at requires date YYYY-MM-DD and time HH:mm:ss with optional one-to-three digit milliseconds.' - : 'The local at value must be a real ISO calendar date and time.', - ) + throw new ScheduleInputError('invalid_rule', localCalendarDiagnostic(parsed.reason)) } return parsed.time } +/** Map one shared local-calendar rejection onto the stable Schedule diagnostic. */ +function localCalendarDiagnostic(reason: LocalCalendarRejection): string { + switch (reason) { + case 'malformed': + return 'Local at requires date YYYY-MM-DD and time HH:mm:ss with optional one-to-three digit milliseconds.' + case 'not_a_calendar_date': + return 'The at value must be a real ISO calendar date and time.' + case 'out_of_range': + return 'The local at value must be a real ISO calendar date and time.' + /* v8 ignore next 3 -- the shared parser returns a closed rejection union. */ + default: { + const unreachable: never = reason + throw new ScheduleLogError(`unknown local calendar rejection ${String(unreachable)}`) + } + } +} + /** Resolve only the earlier overlap instant; a gap fails the local-field round trip. */ function localInstant(local: Temporal.PlainDateTime, timeZone: string): number | undefined { const zoned = local.toZonedDateTime(timeZone, { disambiguation: 'earlier' }) diff --git a/packages/schedule/schedule/tests/domain.spec.ts b/packages/schedule/schedule/tests/domain.spec.ts index 41b1cd4913..0c2ed94c70 100644 --- a/packages/schedule/schedule/tests/domain.spec.ts +++ b/packages/schedule/schedule/tests/domain.spec.ts @@ -22,6 +22,7 @@ import { resolveEveryOccurrence, scheduleView, } from '../src/domain.ts' +import type { LocalAtInput } from '../src/types.ts' function scheduleEvent(data: unknown, seq = 0): SessionEvent { return { type: 'schedule/change', seq, time: 1, data } as SessionEvent @@ -557,6 +558,24 @@ describe('absolute record and time-zone resolution', () => { )).toThrow(ScheduleInputError) }) + it('keeps the three local-calendar diagnostics distinct', () => { + const diagnostic = (at: LocalAtInput): string => { + try { + createAtScheduleRecord(ScheduleId('schedule-at'), 'x', at, now, 'x') + throw new Error('expected local selector failure') + } catch (error: unknown) { + expect(error).toBeInstanceOf(ScheduleInputError) + return (error as ScheduleInputError).message + } + } + expect(diagnostic({ date: '2026-02-30', time: '09:00:00', time_zone: 'UTC' })) + .toBe('The at value must be a real ISO calendar date and time.') + expect(diagnostic({ date: '2026-08-06', time: '24:00:00', time_zone: 'UTC' })) + .toBe('The local at value must be a real ISO calendar date and time.') + expect(diagnostic({ date: '2026/08/06', time: '09:00:00', time_zone: 'UTC' })) + .toBe('Local at requires date YYYY-MM-DD and time HH:mm:ss with optional one-to-three digit milliseconds.') + }) + it('rejects empty prompts and local instants outside the four-digit range', () => { expect(() => createAtScheduleRecord( ScheduleId('schedule-at'), ' ', '2026-08-06T01:00:00Z', now, 'Title', From 882a18c57b35a3f59db9a9b925e31dc400bc0010 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Thu, 24 Sep 2026 13:02:11 +0800 Subject: [PATCH 003/369] docs: drop a nonexistent path reference from the time-vocabulary note --- .../architecture/2026-09-24-shared-time-vocabulary.i18n.yaml | 4 ++-- .../architecture/2026-09-24-shared-time-vocabulary.md | 2 +- .../architecture/2026-09-24-shared-time-vocabulary.zh.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml index 1048d91116..2ef600c588 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml @@ -12,8 +12,8 @@ en: 290a11695db08ffa zh: a19b859e39e8cf8a /agent-note-shared-time-vocabulary-for-wall-clock-selectors/alternatives-considered: - en: db5c53007cf0ae82 - zh: c654d66934c55d42 + en: bc00edc00a7f81f0 + zh: 958bc76bb209604b /agent-note-shared-time-vocabulary-for-wall-clock-selectors/consequences: en: a0a11d4bd4fa11e3 zh: 3536397be8dd0add diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md index c44b802411..86824d3b85 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md @@ -22,7 +22,7 @@ Observable behavior does not change. Every accepted input resolves to the same e ## Alternatives considered -**Keep the shared module private to Schedule.** A `packages/schedule/schedule/src/time.ts` extraction would shrink `domain.ts` without touching a published package, its lockfile entry, its README, or its dependency set. It was rejected because the duplication this change exists to remove is the duplicate *profile*, not the duplicate line count: the zone regular expression, the canonical `Intl` step, and the instant profile would still exist twice, and the next boundary that accepts a wall-clock selector would copy a third. +**Keep the shared module private to Schedule.** A Schedule-local `time` module beside `domain.ts` would shrink that file without touching a published package, its lockfile entry, its README, or its dependency set. It was rejected because the duplication this change exists to remove is the duplicate *profile*, not the duplicate line count: the zone regular expression, the canonical `Intl` step, and the instant profile would still exist twice, and the next boundary that accepts a wall-clock selector would copy a third. **Give the library its own error type.** A `TimeInputError` carrying the reason would let callers rethrow instead of mapping, shortening each call site. It was rejected because `util/time`'s existing contract is that each boundary owns its refusal vocabulary, and Schedule's diagnostics are pinned verbatim by `tools.spec.ts`, snapshot expectations, and the model-visible tool result; a shared error class invites one caller's wording into another caller's surface. diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md index 4c1d054d35..128dc1d9e2 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md @@ -22,7 +22,7 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec ## Alternatives considered -**把共享模块保持为 Schedule 私有。** 抽成 `packages/schedule/schedule/src/time.ts` 可以缩小 `domain.ts`,而不触碰已发布包、其 lockfile 条目、README 或依赖集合。之所以否决,是因为本次要消除的重复是重复的**剖面**,不是重复的行数:时区正则、`Intl` 规范化步骤与时刻剖面仍会存在两份,下一个接收墙钟选择器的边界仍会复制出第三份。 +**把共享模块保持为 Schedule 私有。** 在 `domain.ts` 旁抽出一个 Schedule 私有的 `time` 模块可以缩小它,而不触碰已发布包、其 lockfile 条目、README 或依赖集合。之所以否决,是因为本次要消除的重复是重复的**剖面**,不是重复的行数:时区正则、`Intl` 规范化步骤与时刻剖面仍会存在两份,下一个接收墙钟选择器的边界仍会复制出第三份。 **让本库自带错误类型。** 一个携带原因的 `TimeInputError` 可以让调用方直接重抛而不做映射,缩短每个调用点。之所以否决,是因为 `util/time` 既有契约规定每个边界拥有自己的拒绝词汇,而 Schedule 的诊断文案被 `tools.spec.ts`、快照期望与模型可见工具结果逐字固定;共享错误类会把一个调用方的措辞带进另一个调用方的界面。 From 6c9c7cb69e0160df9325348788f1fed53afd4a97 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Thu, 24 Sep 2026 13:08:06 +0800 Subject: [PATCH 004/369] docs(time): state the throwing preconditions and exact contracts - Instant entries that take already-validated values throw on violated preconditions; the module header said every entry answers a result. - formatFourDigitYearInstant also refuses a non-integral epoch. - Schedule's reason mappers switch over the closed unions with a never default, so a new library reason fails the build instead of falling through to a catch-all. - The Agent Note claimed the schedule suites pin these diagnostics; they assert codes only, and the note now records the added message assertion. --- ...026-09-24-shared-time-vocabulary.i18n.yaml | 12 ++--- .../2026-09-24-shared-time-vocabulary.md | 6 +-- .../2026-09-24-shared-time-vocabulary.zh.md | 6 +-- packages/schedule/schedule/src/domain.ts | 49 +++++++++++++++---- packages/util/time/README.i18n.yaml | 4 +- packages/util/time/README.md | 2 +- packages/util/time/README.zh.md | 2 +- packages/util/time/src/instant.ts | 17 ++++--- 8 files changed, 67 insertions(+), 31 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml index 2ef600c588..be43d6e233 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml @@ -9,11 +9,11 @@ en: 6adc7f8750843df4 zh: 55a600663ece4b2a /agent-note-shared-time-vocabulary-for-wall-clock-selectors/decision: - en: 290a11695db08ffa - zh: a19b859e39e8cf8a + en: 30ff0138ed91c0be + zh: 4df42e6b5759d465 /agent-note-shared-time-vocabulary-for-wall-clock-selectors/alternatives-considered: - en: bc00edc00a7f81f0 - zh: 958bc76bb209604b + en: 5bf3cc0d112531e7 + zh: 99ed3e958d9d8ccb /agent-note-shared-time-vocabulary-for-wall-clock-selectors/consequences: - en: a0a11d4bd4fa11e3 - zh: 3536397be8dd0add + en: 88e6959ae2fbeb84 + zh: 951922682320235d diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md index 86824d3b85..0a86af09d5 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md @@ -18,13 +18,13 @@ Schedule consumes it and keeps every domain decision. `canonicalizeTimeZone`, `d The maintained `@js-temporal/polyfill` 0.5.1 becomes a runtime dependency of `dsh-util-time` as well as of `dsh-schedule`. Local resolution continues to use it, so the Host's IANA data still decides gaps and overlaps and no caller owns offset sampling. `util/time`'s README and description lose the `zero-dependency` claim the added dependency contradicts. -Observable behavior does not change. Every accepted input resolves to the same epoch, every refused input raises the same `ScheduleInputError` code and message, and the stored `scheduledAt` profile is unchanged, so no durable record and no recorded snapshot is re-recorded. `packages/util/time/tests/instant.spec.ts` covers the new entries directly, per-file 100% included. +Observable behavior does not change. Every accepted input resolves to the same epoch, every refused input raises the same `ScheduleInputError` or `ScheduleLogError` code and the same message, and the stored `scheduledAt` profile is unchanged, so no durable record and no recorded snapshot is re-recorded. The schedule suites assert error codes and never these message strings, so `packages/schedule/schedule/tests/domain.spec.ts` gains an assertion on the three local-calendar diagnostics; without it a wording regression in the extraction passed the whole suite. `packages/util/time/tests/instant.spec.ts` covers the new entries directly, per-file 100% included. ## Alternatives considered **Keep the shared module private to Schedule.** A Schedule-local `time` module beside `domain.ts` would shrink that file without touching a published package, its lockfile entry, its README, or its dependency set. It was rejected because the duplication this change exists to remove is the duplicate *profile*, not the duplicate line count: the zone regular expression, the canonical `Intl` step, and the instant profile would still exist twice, and the next boundary that accepts a wall-clock selector would copy a third. -**Give the library its own error type.** A `TimeInputError` carrying the reason would let callers rethrow instead of mapping, shortening each call site. It was rejected because `util/time`'s existing contract is that each boundary owns its refusal vocabulary, and Schedule's diagnostics are pinned verbatim by `tools.spec.ts`, snapshot expectations, and the model-visible tool result; a shared error class invites one caller's wording into another caller's surface. +**Give the library its own error type.** A `TimeInputError` carrying the reason would let callers rethrow instead of mapping, shortening each call site. It was rejected because `util/time`'s existing contract is that each boundary owns its refusal vocabulary, and Schedule's messages reach the model through the tool result `{ code, message }` and the client's message card; a shared error class would put one caller's wording into another caller's surface. **Have the library return `undefined` instead of reasons.** That is the shape `canonicalClientTimeZone` already has, and it was the smaller change. It was rejected because Schedule must distinguish a malformed instant from a non-existent calendar date from an out-of-range clock field to keep its existing diagnostics, and `canonicalizeTimeZone` must distinguish a bad name from a locally unresolvable one; collapsing those to one falsy value would have forced Schedule to re-derive the distinction with its own regular expressions, restoring the duplication. @@ -34,6 +34,6 @@ Observable behavior does not change. Every accepted input resolves to the same e The time profile now has one home. A change to the accepted instant profile or the zone profile is made once and covered once, and a new boundary imports it instead of copying it. Schedule's `domain.ts` loses the three regular expressions, the `Intl` canonicalization, the hand-rolled calendar conversion, and the four-digit bound arithmetic. -The cost is a wider published surface and a heavier dependency graph: `dsh-util-time` gains a runtime dependency on `@js-temporal/polyfill`, so a consumer that imports only `canonicalClientTimeZone` now loads that module too. The library is also no longer a pure "validation and canonicalization only" package, which its README and package description had to state and which its Known Limitations section now records. Schedule keeps a thin mapping layer at each entry, so a reason added to the library's union fails the build at each caller through the exhaustive switch rather than silently taking a default. +The cost is a wider published surface and a heavier dependency graph: `dsh-util-time` gains a runtime dependency on `@js-temporal/polyfill`, so a consumer that imports only `canonicalClientTimeZone` now loads that module too. The library is also no longer a pure "validation and canonicalization only" package, which its README and package description had to state and which its Known Limitations section now records. Schedule keeps a thin mapping layer at each entry, and each mapper switches exhaustively over the library's closed reason unions with a `never` default, so a newly added reason fails the build at every affected caller instead of silently taking a catch-all. `dsh-time-context` keeps its own IANA regular expression and canonicalization, and stays out of this change. It rejects a non-canonical alias at the request boundary where Schedule canonicalizes one, so unifying it means deciding which of the two behaviors the browser-time-zone instruction wants; that decision belongs to the consumer, not to this extraction. diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md index 128dc1d9e2..ea9b437af9 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md @@ -18,13 +18,13 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec 受维护的 `@js-temporal/polyfill` 0.5.1 在成为 `dsh-schedule` 运行时依赖之外,也成为 `dsh-util-time` 的运行时依赖。本地解析继续使用它,因此缺口与重叠仍由宿主 IANA 数据决定,也没有调用方需要自己实现偏移采样。`util/time` 的 README 与包描述删去了新增依赖所否定的 `zero-dependency` 表述。 -可观察行为不变。每个被接受的输入解析出相同的纪元值,每个被拒绝的输入抛出相同的 `ScheduleInputError` 错误码与消息,存储的 `scheduledAt` 剖面不变,因此没有持久记录或录制快照需要重录。`packages/util/time/tests/instant.spec.ts` 直接覆盖新增入口,包含逐文件 100% 覆盖。 +可观察行为不变。每个被接受的输入解析出相同的纪元值,每个被拒绝的输入抛出相同的 `ScheduleInputError` 或 `ScheduleLogError` 错误码与相同的消息,存储的 `scheduledAt` 剖面不变,因此没有持久记录或录制快照需要重录。Schedule 的测试只断言错误码、从不断言这些消息字符串,因此 `packages/schedule/schedule/tests/domain.spec.ts` 新增了对三种本地日历诊断文案的断言;没有它,本次抽取中的一处措辞回退能通过整套测试。`packages/util/time/tests/instant.spec.ts` 直接覆盖新增入口,包含逐文件 100% 覆盖。 ## Alternatives considered **把共享模块保持为 Schedule 私有。** 在 `domain.ts` 旁抽出一个 Schedule 私有的 `time` 模块可以缩小它,而不触碰已发布包、其 lockfile 条目、README 或依赖集合。之所以否决,是因为本次要消除的重复是重复的**剖面**,不是重复的行数:时区正则、`Intl` 规范化步骤与时刻剖面仍会存在两份,下一个接收墙钟选择器的边界仍会复制出第三份。 -**让本库自带错误类型。** 一个携带原因的 `TimeInputError` 可以让调用方直接重抛而不做映射,缩短每个调用点。之所以否决,是因为 `util/time` 既有契约规定每个边界拥有自己的拒绝词汇,而 Schedule 的诊断文案被 `tools.spec.ts`、快照期望与模型可见工具结果逐字固定;共享错误类会把一个调用方的措辞带进另一个调用方的界面。 +**让本库自带错误类型。** 一个携带原因的 `TimeInputError` 可以让调用方直接重抛而不做映射,缩短每个调用点。之所以否决,是因为 `util/time` 既有契约规定每个边界拥有自己的拒绝词汇,而 Schedule 的消息会通过工具结果的 `{ code, message }` 与客户端的消息卡片到达模型;共享错误类会把一个调用方的措辞带进另一个调用方的界面。 **让本库回答 `undefined` 而不是原因。** 这正是 `canonicalClientTimeZone` 已有的形态,也是改动更小的做法。之所以否决,是因为 Schedule 必须区分格式错误的时刻、日历上不存在的日期与越界时钟字段,才能保留既有诊断;`canonicalizeTimeZone` 也必须区分名称本身不合法与本地无法解析。把二者压成一个假值,会迫使 Schedule 用自己的正则重新推导这层区分,从而恢复重复。 @@ -34,6 +34,6 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec 时间剖面现在只有一个归宿。时刻剖面或时区剖面的变更只做一次、只覆盖一次,新边界导入它而不是复制它。Schedule 的 `domain.ts` 去掉了三段正则、`Intl` 规范化、手写日历换算与四位年份边界运算。 -代价是更宽的已发布接口与更重的依赖图:`dsh-util-time` 增加了对 `@js-temporal/polyfill` 的运行时依赖,因此只导入 `canonicalClientTimeZone` 的消费方也会加载该模块。本库也不再是纯"只做校验与规范化"的包——其 README 与包描述必须说明这一点,其"已知限制"一节现在也记录了它。Schedule 在每个入口保留一层薄映射,因此本库联合类型新增原因时,每个调用方都会因穷尽 switch 而构建失败,而不是静默走默认分支。 +代价是更宽的已发布接口与更重的依赖图:`dsh-util-time` 增加了对 `@js-temporal/polyfill` 的运行时依赖,因此只导入 `canonicalClientTimeZone` 的消费方也会加载该模块。本库也不再是纯"只做校验与规范化"的包——其 README 与包描述必须说明这一点,其"已知限制"一节现在也记录了它。Schedule 在每个入口保留一层薄映射,每个映射函数都以 `never` 默认分支穷尽 switch 本库的封闭原因联合,因此新增原因会让每个受影响的调用方构建失败,而不是静默走兜底分支。 `dsh-time-context` 保留自己的 IANA 正则与规范化,不参与本次改动。它在请求边界拒绝非规范别名,而 Schedule 会把它规范化,因此统一二者意味着决定浏览器时区指令需要两种行为中的哪一种;该决定属于消费方,而不属于这次抽取。 diff --git a/packages/schedule/schedule/src/domain.ts b/packages/schedule/schedule/src/domain.ts index ed970e1800..d459e9d21f 100644 --- a/packages/schedule/schedule/src/domain.ts +++ b/packages/schedule/schedule/src/domain.ts @@ -14,7 +14,9 @@ import { parseOffsetInstant, resolveLocalInstant, } from '@deepseek-ai/dsh-util-time' -import type { LocalCalendarRejection, LocalCalendarTime, TimeZoneRejection } from '@deepseek-ai/dsh-util-time' +import type { + InstantRejection, LocalCalendarRejection, LocalCalendarTime, TimeZoneRejection, +} from '@deepseek-ai/dsh-util-time' import { SessionLogOffset } from '@deepseek-ai/dsh-session' import type { SessionEvent, SessionLogOffset as SessionLogOffsetType } from '@deepseek-ai/dsh-session' import type { @@ -253,24 +255,53 @@ function decodeId(value: unknown): ScheduleIdType { function decodeInstant(value: unknown): string { const parsed = typeof value === 'string' ? fourDigitYearEpoch(value) : undefined if (parsed === undefined || parsed.kind === 'rejected') { - throw new ScheduleLogError( - parsed?.reason === 'not_a_calendar_date' - ? 'scheduledAt is not a real UTC calendar instant' - : 'scheduledAt must be a canonical four-digit-year RFC 3339 UTC instant', - ) + throw new ScheduleLogError(instantDiagnostic(parsed?.reason)) } return value as string } +/** + * Map one shared instant rejection onto the stable stored-data diagnostic. + * @param reason - Reason the shared vocabulary refused the instant, or undefined for a non-string field. + * @returns The Schedule log diagnostic for that refusal. + */ +function instantDiagnostic(reason: InstantRejection | undefined): string { + switch (reason) { + case 'not_a_calendar_date': + return 'scheduledAt is not a real UTC calendar instant' + case 'not_rfc3339': + case 'not_a_wall_clock_time': + case 'invalid_numeric_offset': + case undefined: + return 'scheduledAt must be a canonical four-digit-year RFC 3339 UTC instant' + /* v8 ignore next 3 -- the shared parser returns a closed rejection union. */ + default: { + const unreachable: never = reason + throw new ScheduleLogError(`unknown instant rejection ${String(unreachable)}`) + } + } +} + /** * Map one shared zone rejection onto the stable Schedule input code. * @param reason - Reason the shared vocabulary refused the name. * @returns The public Schedule diagnostic for that refusal. */ function timeZoneError(reason: TimeZoneRejection): ScheduleInputError { - return reason === 'not_canonical_shape' - ? new ScheduleInputError('invalid_time_zone', 'time_zone must resolve to UTC or an IANA Area/Location name.') - : new ScheduleInputError('invalid_time_zone', 'time_zone must be UTC or a valid IANA Area/Location name.') + switch (reason) { + case 'not_canonical_shape': + return new ScheduleInputError('invalid_time_zone', 'time_zone must resolve to UTC or an IANA Area/Location name.') + case 'blank': + case 'padded': + case 'not_area_location': + case 'unsupported': + return new ScheduleInputError('invalid_time_zone', 'time_zone must be UTC or a valid IANA Area/Location name.') + /* v8 ignore next 3 -- the shared parser returns a closed rejection union. */ + default: { + const unreachable: never = reason + throw new ScheduleLogError(`unknown time zone rejection ${String(unreachable)}`) + } + } } /** diff --git a/packages/util/time/README.i18n.yaml b/packages/util/time/README.i18n.yaml index 3d25e2ee8d..750a0db194 100644 --- a/packages/util/time/README.i18n.yaml +++ b/packages/util/time/README.i18n.yaml @@ -24,8 +24,8 @@ en: 657c118ff4d9b99b zh: 2b11d97a40a2044f /dsh-util-time/understand-the-implementation: - en: 4daf141b0fbee0c4 - zh: 3dda79572f6c224b + en: 479489d729810a9f + zh: a238654f67e33b62 /dsh-util-time/further-exploration: en: 7730949c9d91249b zh: 006dc7155d6b5afc diff --git a/packages/util/time/README.md b/packages/util/time/README.md index 9ba371598d..32b61b83de 100644 --- a/packages/util/time/README.md +++ b/packages/util/time/README.md @@ -68,7 +68,7 @@ The strict parsers match a fixed regular expression before constructing any date | `parseLocalCalendar` | `YYYY-MM-DD` and `HH:mm:ss`, with optional one-to-three fractional digits. | | `parseOffsetInstant` | RFC 3339 with an explicit `Z` or numeric offset; the offset is part of the input. | | `fourDigitYearEpoch` | The canonical stored profile: exactly three fractional digits and a `Z`. | -| `formatFourDigitYearInstant` | An epoch inside the four-digit-year range; answers `undefined` outside it. | +| `formatFourDigitYearInstant` | An integral epoch inside the four-digit-year range; answers `undefined` outside it or when non-integral. | | `resolveLocalInstant` | Exact calendar fields and an already-canonical zone. | diff --git a/packages/util/time/README.zh.md b/packages/util/time/README.zh.md index c850608eb3..9fbc058d1a 100644 --- a/packages/util/time/README.zh.md +++ b/packages/util/time/README.zh.md @@ -68,7 +68,7 @@ if (local.kind === 'accepted') { | `parseLocalCalendar` | `YYYY-MM-DD` 与 `HH:mm:ss`,可带 1 到 3 位小数秒。 | | `parseOffsetInstant` | 带显式 `Z` 或数字偏移的 RFC 3339;偏移量属于输入的一部分。 | | `fourDigitYearEpoch` | 规范存储剖面:恰好 3 位小数秒与 `Z`。 | -| `formatFourDigitYearInstant` | 四位年份范围内的纪元毫秒;超出范围回答 `undefined`。 | +| `formatFourDigitYearInstant` | 四位年份范围内的整数纪元毫秒;超出范围或非整数时回答 `undefined`。 | | `resolveLocalInstant` | 精确日历字段与已规范化的时区。 | diff --git a/packages/util/time/src/instant.ts b/packages/util/time/src/instant.ts index 8a52c4a6dc..0bec170408 100644 --- a/packages/util/time/src/instant.ts +++ b/packages/util/time/src/instant.ts @@ -2,10 +2,14 @@ * Strict instant and local-calendar vocabulary for the boundaries that accept a * wall-clock selector. * - * Every entry answers a discriminated result instead of throwing: the library - * carries no failure vocabulary, and each caller maps a reason onto its own - * domain code and diagnostic. Local wall-clock resolution uses the maintained - * `Temporal` implementation, so no caller owns offset sampling or DST rules. + * Every entry that reads untrusted text answers a discriminated result instead + * of throwing: the library carries no failure vocabulary, and each caller maps + * a reason onto its own domain code and diagnostic. The two entries that take + * already-validated values throw on violated preconditions instead — + * `resolveLocalInstant` raises a `RangeError` for an unknown zone or an + * impossible calendar field, and `formatFourDigitYearInstant` cannot throw. + * Local wall-clock resolution uses the maintained `Temporal` implementation, so + * no caller owns offset sampling or DST rules. * @module @deepseek-ai/dsh-util-time */ @@ -226,7 +230,7 @@ export function fourDigitYearEpoch(value: string): InstantResult { * * The bound check runs first, so a caller that also compares the result with a * decision time never formats an unrepresentable instant. - * @param epoch - Candidate epoch milliseconds. + * @param epoch - Candidate epoch milliseconds; a non-integral value is unrepresentable. * @returns The canonical instant text, or `undefined` when the epoch is unrepresentable. */ export function formatFourDigitYearInstant(epoch: number): string | undefined { @@ -268,9 +272,10 @@ export function canonicalizeTimeZone(value: string): TimeZoneResult { * A value inside a forward transition's gap does not exist, so it answers * `undefined`; a value inside an overlap resolves to the earlier instant, which * is the same choice the maintained `Temporal` implementation makes. - * @param time - Exact local calendar fields. + * @param time - Exact calendar fields of a real date whose fields are already in range. * @param timeZone - Already-canonical `UTC` or IANA zone. * @returns The resolved epoch milliseconds, or `undefined` for a nonexistent wall-clock time. + * @throws RangeError when `timeZone` is not a known zone or `time` names an impossible calendar value. */ export function resolveLocalInstant(time: LocalCalendarTime, timeZone: string): number | undefined { const local = Temporal.PlainDateTime.from(time, { overflow: 'reject' }) From 0a89067fdeb3325d9f3bfbebbcf3589f46aea49a Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Thu, 24 Sep 2026 13:15:06 +0800 Subject: [PATCH 005/369] docs: regenerate the module graph for the schedule to util-time edge --- docs/module-graph.md | 3 ++- docs/module-graph.zh.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/module-graph.md b/docs/module-graph.md index ef7567f3dd..c040e60589 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -1321,6 +1321,7 @@ flowchart TD pkg_schedule --> pkg_storage_domain pkg_schedule --> pkg_tools pkg_schedule --> pkg_typert_protocol + pkg_schedule --> pkg_util_time pkg_schedule --> pkg_workspace pkg_sdk_client --> pkg_llm pkg_sdk_client --> pkg_sdk_protocol @@ -1670,7 +1671,7 @@ flowchart TD | [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session) | | [`experimental-client-ui-voice-input`](../packages/experimental/client-ui-voice-input) | `experimental` | [`api-gateway`](../packages/api/gateway), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-store`](../packages/client/store), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-plugin-manager`](../packages/client/ui-plugin-manager), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text), [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) | +| [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`util-time`](../packages/util/time), [`workspace`](../packages/workspace/workspace) | | [`sdk-client`](../packages/sdk/client) | `sdk` | [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) | | [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | | [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-hmr`](../packages/client/hmr), [`client-modules`](../packages/client/modules), [`client-store`](../packages/client/store), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web`](../packages/client/web), [`remote-mock`](../packages/test-support/remote-mock), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index ac98ecddd9..18f9c9ed80 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -1323,6 +1323,7 @@ flowchart TD pkg_schedule --> pkg_storage_domain pkg_schedule --> pkg_tools pkg_schedule --> pkg_typert_protocol + pkg_schedule --> pkg_util_time pkg_schedule --> pkg_workspace pkg_sdk_client --> pkg_llm pkg_sdk_client --> pkg_sdk_protocol @@ -1672,7 +1673,7 @@ flowchart TD | [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session) | | [`experimental-client-ui-voice-input`](../packages/experimental/client-ui-voice-input) | `experimental` | [`api-gateway`](../packages/api/gateway), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-store`](../packages/client/store), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-plugin-manager`](../packages/client/ui-plugin-manager), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text), [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) | +| [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`util-time`](../packages/util/time), [`workspace`](../packages/workspace/workspace) | | [`sdk-client`](../packages/sdk/client) | `sdk` | [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) | | [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`llm-deepseek-api-key`](../packages/llm/llm-deepseek-api-key), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | | [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`api-gateway`](../packages/api/gateway), [`api-session-controller`](../packages/api/session-controller), [`api-workspace-controller`](../packages/api/workspace-controller), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`client-hmr`](../packages/client/hmr), [`client-modules`](../packages/client/modules), [`client-store`](../packages/client/store), [`client-ui-chat`](../packages/client/ui-chat), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web`](../packages/client/web), [`remote-mock`](../packages/test-support/remote-mock), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`typert-protocol`](../packages/typert/protocol) | From b42cae0c8b2e20679e343416f0609253d78131d7 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Thu, 24 Sep 2026 13:25:05 +0800 Subject: [PATCH 006/369] fix(schedule): mark the unreachable zone-rejection branch The shared parser never answers not_canonical_shape, so a switch over the union left that case uncovered and failed the per-file coverage gate. --- ...026-09-24-shared-time-vocabulary.i18n.yaml | 4 ++-- .../2026-09-24-shared-time-vocabulary.md | 2 +- .../2026-09-24-shared-time-vocabulary.zh.md | 2 +- packages/schedule/schedule/src/domain.ts | 21 +++++++------------ 4 files changed, 11 insertions(+), 18 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml index be43d6e233..fe15d1a558 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml @@ -15,5 +15,5 @@ en: 5bf3cc0d112531e7 zh: 99ed3e958d9d8ccb /agent-note-shared-time-vocabulary-for-wall-clock-selectors/consequences: - en: 88e6959ae2fbeb84 - zh: 951922682320235d + en: 02aab911948d62d4 + zh: 78f9d683c0600727 diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md index 0a86af09d5..c38468502e 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md @@ -34,6 +34,6 @@ Observable behavior does not change. Every accepted input resolves to the same e The time profile now has one home. A change to the accepted instant profile or the zone profile is made once and covered once, and a new boundary imports it instead of copying it. Schedule's `domain.ts` loses the three regular expressions, the `Intl` canonicalization, the hand-rolled calendar conversion, and the four-digit bound arithmetic. -The cost is a wider published surface and a heavier dependency graph: `dsh-util-time` gains a runtime dependency on `@js-temporal/polyfill`, so a consumer that imports only `canonicalClientTimeZone` now loads that module too. The library is also no longer a pure "validation and canonicalization only" package, which its README and package description had to state and which its Known Limitations section now records. Schedule keeps a thin mapping layer at each entry, and each mapper switches exhaustively over the library's closed reason unions with a `never` default, so a newly added reason fails the build at every affected caller instead of silently taking a catch-all. +The cost is a wider published surface and a heavier dependency graph: `dsh-util-time` gains a runtime dependency on `@js-temporal/polyfill`, so a consumer that imports only `canonicalClientTimeZone` now loads that module too. The library is also no longer a pure "validation and canonicalization only" package, which its README and package description had to state and which its Known Limitations section now records. Schedule keeps a thin mapping layer at each entry. The local-calendar and instant mappers switch over the library's closed reason unions with a `never` default, so a newly added reason fails the build there instead of silently taking a catch-all. The zone mapper cannot: the library's `not_canonical_shape` reason is unreachable, so a switch would leave a branch the schedule suites can never execute. `dsh-time-context` keeps its own IANA regular expression and canonicalization, and stays out of this change. It rejects a non-canonical alias at the request boundary where Schedule canonicalizes one, so unifying it means deciding which of the two behaviors the browser-time-zone instruction wants; that decision belongs to the consumer, not to this extraction. diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md index ea9b437af9..3acbb58bd9 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md @@ -34,6 +34,6 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec 时间剖面现在只有一个归宿。时刻剖面或时区剖面的变更只做一次、只覆盖一次,新边界导入它而不是复制它。Schedule 的 `domain.ts` 去掉了三段正则、`Intl` 规范化、手写日历换算与四位年份边界运算。 -代价是更宽的已发布接口与更重的依赖图:`dsh-util-time` 增加了对 `@js-temporal/polyfill` 的运行时依赖,因此只导入 `canonicalClientTimeZone` 的消费方也会加载该模块。本库也不再是纯"只做校验与规范化"的包——其 README 与包描述必须说明这一点,其"已知限制"一节现在也记录了它。Schedule 在每个入口保留一层薄映射,每个映射函数都以 `never` 默认分支穷尽 switch 本库的封闭原因联合,因此新增原因会让每个受影响的调用方构建失败,而不是静默走兜底分支。 +代价是更宽的已发布接口与更重的依赖图:`dsh-util-time` 增加了对 `@js-temporal/polyfill` 的运行时依赖,因此只导入 `canonicalClientTimeZone` 的消费方也会加载该模块。本库也不再是纯"只做校验与规范化"的包——其 README 与包描述必须说明这一点,其"已知限制"一节现在也记录了它。Schedule 在每个入口保留一层薄映射。本地日历与时刻映射函数以 `never` 默认分支穷尽 switch 本库的封闭原因联合,因此新增原因会让这两个调用点构建失败,而不是静默走兜底分支。时区映射函数做不到这一点:本库的 `not_canonical_shape` 原因不可达,用 switch 会留下一个 Schedule 测试永远无法执行的分支。 `dsh-time-context` 保留自己的 IANA 正则与规范化,不参与本次改动。它在请求边界拒绝非规范别名,而 Schedule 会把它规范化,因此统一二者意味着决定浏览器时区指令需要两种行为中的哪一种;该决定属于消费方,而不属于这次抽取。 diff --git a/packages/schedule/schedule/src/domain.ts b/packages/schedule/schedule/src/domain.ts index d459e9d21f..ebb549a6c2 100644 --- a/packages/schedule/schedule/src/domain.ts +++ b/packages/schedule/schedule/src/domain.ts @@ -288,20 +288,13 @@ function instantDiagnostic(reason: InstantRejection | undefined): string { * @returns The public Schedule diagnostic for that refusal. */ function timeZoneError(reason: TimeZoneRejection): ScheduleInputError { - switch (reason) { - case 'not_canonical_shape': - return new ScheduleInputError('invalid_time_zone', 'time_zone must resolve to UTC or an IANA Area/Location name.') - case 'blank': - case 'padded': - case 'not_area_location': - case 'unsupported': - return new ScheduleInputError('invalid_time_zone', 'time_zone must be UTC or a valid IANA Area/Location name.') - /* v8 ignore next 3 -- the shared parser returns a closed rejection union. */ - default: { - const unreachable: never = reason - throw new ScheduleLogError(`unknown time zone rejection ${String(unreachable)}`) - } - } + // `not_canonical_shape` names a platform-canonical answer outside the Area/Location + // profile, which the shared parser's `Intl` step does not produce; this branch keeps + // the diagnostic this entry has always returned for that reason. + /* v8 ignore next 2 -- the shared parser never answers not_canonical_shape for a canonical zone. */ + return reason === 'not_canonical_shape' + ? new ScheduleInputError('invalid_time_zone', 'time_zone must resolve to UTC or an IANA Area/Location name.') + : new ScheduleInputError('invalid_time_zone', 'time_zone must be UTC or a valid IANA Area/Location name.') } /** From 22f8be30a1719f462732812a1d5de00128b71298 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Thu, 24 Sep 2026 21:54:34 +0800 Subject: [PATCH 007/369] refactor(time-context): consume the shared zone vocabulary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit time-context kept a third copy of the IANA profile: its own regular expression and its own Intl canonicalization in request-zone.ts. It now reads a request's reported zone through canonicalizeTimeZone and keeps its stricter policy — an alias whose canonical answer differs from the reported value is still refused, with the same three diagnostics. The shared rejection now carries the platform error it caught, so the unsupported refusal keeps its cause in both time-context and Schedule. --- ...026-09-24-shared-time-vocabulary.i18n.yaml | 12 +++--- .../2026-09-24-shared-time-vocabulary.md | 6 ++- .../2026-09-24-shared-time-vocabulary.zh.md | 6 ++- docs/module-graph.md | 3 +- docs/module-graph.zh.md | 3 +- packages/context/time-context/package.json | 6 ++- .../context/time-context/src/request-zone.ts | 38 +++++++++++-------- .../time-context/tests/request-zone.spec.ts | 21 ++++++++++ packages/context/time-context/tsconfig.json | 3 ++ packages/schedule/schedule/src/domain.ts | 18 +++++++-- packages/util/time/README.i18n.yaml | 12 +++--- packages/util/time/README.md | 5 ++- packages/util/time/README.zh.md | 5 ++- packages/util/time/src/instant.ts | 14 +++++-- packages/util/time/tests/instant.spec.ts | 8 +++- pnpm-lock.yaml | 3 ++ 16 files changed, 114 insertions(+), 49 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml index fe15d1a558..cb843b513d 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml @@ -6,14 +6,14 @@ en: ae04747237709d0c zh: a3731d98d5e940f9 /agent-note-shared-time-vocabulary-for-wall-clock-selectors/problem: - en: 6adc7f8750843df4 - zh: 55a600663ece4b2a + en: ad997234edc363a7 + zh: 25968ebb6fd9de71 /agent-note-shared-time-vocabulary-for-wall-clock-selectors/decision: - en: 30ff0138ed91c0be - zh: 4df42e6b5759d465 + en: 01f55d3c05812e4f + zh: 1764a14bcffeecd7 /agent-note-shared-time-vocabulary-for-wall-clock-selectors/alternatives-considered: en: 5bf3cc0d112531e7 zh: 99ed3e958d9d8ccb /agent-note-shared-time-vocabulary-for-wall-clock-selectors/consequences: - en: 02aab911948d62d4 - zh: 78f9d683c0600727 + en: 3e10b12bf9e85faf + zh: 1362af0ff00c4683 diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md index c38468502e..d0daddb3c9 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md @@ -6,7 +6,7 @@ English | [中文](2026-09-24-shared-time-vocabulary.zh.md) ## Problem -`dsh-util-time` existed for one job and one caller shape: `canonicalClientTimeZone` validated a caller-reported zone at a wire boundary and answered `undefined`, which `dsh-api-session-controller` and `dsh-subagent` mapped onto their own domain errors. Schedule had grown the other half of the same vocabulary privately inside `packages/schedule/schedule/src/domain.ts`: a strict RFC 3339 instant parser, a local-calendar parser, a four-digit-year bound and canonical formatter, a local wall-clock resolver, and a second IANA zone canonicalizer that threw `ScheduleInputError` instead of answering a result. Two consequences followed. The same three regular expressions and the same `Intl.DateTimeFormat` canonicalization existed twice, so a change to either profile had to be made twice and verified twice. And the next consumer of a wall-clock selector had no shared entry point to reach for, which is why the profile would have been copied a third time rather than imported. +`dsh-util-time` existed for one job and one caller shape: `canonicalClientTimeZone` validated a caller-reported zone at a wire boundary and answered `undefined`, which `dsh-api-session-controller` and `dsh-subagent` mapped onto their own domain errors. Three consumers had grown parts of the same vocabulary privately. Schedule held a strict RFC 3339 instant parser, a local-calendar parser, a four-digit-year bound and canonical formatter, a local wall-clock resolver, and its own IANA zone canonicalizer in `packages/schedule/schedule/src/domain.ts`, throwing `ScheduleInputError` instead of answering a result. `dsh-time-context` held a third copy of the IANA zone profile in `packages/context/time-context/src/request-zone.ts`, with its own regular expression and its own `Intl.DateTimeFormat` canonicalization, to enforce the stricter policy that a request may only name a zone another process resolves back to the same identity. The IANA profile and the `Intl` canonicalization step therefore existed three times, and the instant profile twice, so a change to either had to be made and verified once per copy, and the next consumer of a wall-clock selector had no shared entry point to reach for. ## Decision @@ -16,6 +16,8 @@ The library answers discriminated results and carries no failure vocabulary. `pa Schedule consumes it and keeps every domain decision. `canonicalizeTimeZone`, `decodeInstant`, `parseAtInput`, and `futureInstant` remain Schedule entry points with their existing codes and diagnostics; they now call the shared parser and map its reason. Schedule still owns the durable record shapes, the fold and transition authority, the injection-resistant framing, and all recurrence arithmetic: the cron engine, the daily and weekly date walks, the latest-due search, and the schedule-specific accepted-decision bound. +`dsh-time-context` consumes it too and keeps its stricter policy. `deriveBrowserTimeZoneContext` reads each user-rpc zone through `canonicalizeTimeZone` and refuses an alias whose canonical answer differs from the reported value, so the request boundary still requires an identity another process resolves back to the same name. Time-context owns its three refusals and their messages; the shared entry only answers the canonical name or a reason, and a rejection carries the platform error the library caught so the `unsupported` refusal keeps its `cause`. + The maintained `@js-temporal/polyfill` 0.5.1 becomes a runtime dependency of `dsh-util-time` as well as of `dsh-schedule`. Local resolution continues to use it, so the Host's IANA data still decides gaps and overlaps and no caller owns offset sampling. `util/time`'s README and description lose the `zero-dependency` claim the added dependency contradicts. Observable behavior does not change. Every accepted input resolves to the same epoch, every refused input raises the same `ScheduleInputError` or `ScheduleLogError` code and the same message, and the stored `scheduledAt` profile is unchanged, so no durable record and no recorded snapshot is re-recorded. The schedule suites assert error codes and never these message strings, so `packages/schedule/schedule/tests/domain.spec.ts` gains an assertion on the three local-calendar diagnostics; without it a wording regression in the extraction passed the whole suite. `packages/util/time/tests/instant.spec.ts` covers the new entries directly, per-file 100% included. @@ -36,4 +38,4 @@ The time profile now has one home. A change to the accepted instant profile or t The cost is a wider published surface and a heavier dependency graph: `dsh-util-time` gains a runtime dependency on `@js-temporal/polyfill`, so a consumer that imports only `canonicalClientTimeZone` now loads that module too. The library is also no longer a pure "validation and canonicalization only" package, which its README and package description had to state and which its Known Limitations section now records. Schedule keeps a thin mapping layer at each entry. The local-calendar and instant mappers switch over the library's closed reason unions with a `never` default, so a newly added reason fails the build there instead of silently taking a catch-all. The zone mapper cannot: the library's `not_canonical_shape` reason is unreachable, so a switch would leave a branch the schedule suites can never execute. -`dsh-time-context` keeps its own IANA regular expression and canonicalization, and stays out of this change. It rejects a non-canonical alias at the request boundary where Schedule canonicalizes one, so unifying it means deciding which of the two behaviors the browser-time-zone instruction wants; that decision belongs to the consumer, not to this extraction. +Time-context keeps the two policies this change did not touch: its ISO-shaped timestamp rendering in `packages/context/time-context/src/timestamp.ts`, which is that package's durable reading format, and the elapsed-duration wording in its own plugin. The browser client keeps its own localized date rendering and its own UTC-offset reading, which serve a different purpose (a reader's locale) from a boundary value; unifying those means deciding whether the shared library should also own display formatting, and which package then owns the durable reading format its invariant validates. diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md index 3acbb58bd9..0d886b3cac 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md @@ -6,7 +6,7 @@ Status: implemented ## Problem -`dsh-util-time` 此前只承担一项职责、面向一种调用方:`canonicalClientTimeZone` 在协议边界校验调用方所报时区并回答 `undefined`,由 `dsh-api-session-controller` 与 `dsh-subagent` 映射成各自的域错误。同一套词汇的另一半则在 `packages/schedule/schedule/src/domain.ts` 内部私下生长:严格的 RFC 3339 时刻解析器、本地日历解析器、四位年份边界与规范格式化、本地墙钟解析,以及第二个 IANA 时区规范化实现——它抛 `ScheduleInputError` 而不是回答结果。由此产生两个后果。同样的三段正则与同样的 `Intl.DateTimeFormat` 规范化存在两份,任何一侧剖面变更都要改两遍、验两遍。而下一个需要墙钟选择器的消费方没有共享入口可用,这正是该剖面会被第三次复制而不是被导入的原因。 +`dsh-util-time` 此前只承担一项职责、面向一种调用方:`canonicalClientTimeZone` 在协议边界校验调用方所报时区并回答 `undefined`,由 `dsh-api-session-controller` 与 `dsh-subagent` 映射成各自的域错误。同一套词汇的其余部分被三个消费方各自私下养大。Schedule 在 `packages/schedule/schedule/src/domain.ts` 里持有严格的 RFC 3339 时刻解析器、本地日历解析器、四位年份边界与规范格式化、本地墙钟解析,以及自己的 IANA 时区规范化实现——它抛 `ScheduleInputError` 而不是回答结果。`dsh-time-context` 在 `packages/context/time-context/src/request-zone.ts` 里持有第三份 IANA 时区剖面,配有自己的正则与自己的 `Intl.DateTimeFormat` 规范化,用来执行更严格的策略:一轮请求只能报出另一个进程会解析回同一标识的时区。因此 IANA 剖面与那一步 `Intl` 规范化存在三份、时刻剖面存在两份,任何一侧变更都要逐份改、逐份验;而下一个需要墙钟选择器的消费方没有共享入口可用。 ## Decision @@ -14,6 +14,8 @@ Status: implemented 本库回答带判别值的结果,不持有失败词汇。`parseOffsetInstant`、`fourDigitYearEpoch`、`parseLocalCalendar` 与 `canonicalizeTimeZone` 回答 `accepted` 分支,或回答携带稳定原因的 `rejected` 分支(`not_rfc3339`、`not_a_calendar_date`、`not_a_wall_clock_time`、`invalid_numeric_offset`、`malformed`、`out_of_range`、`blank`、`padded`、`not_area_location`、`unsupported`、`not_canonical_shape`)。每个调用方把原因映射成自己的错误码与诊断文案,因此本库从不撰写面向模型的文本。`formatFourDigitYearInstant` 返回规范时刻文本,越界时返回 `undefined`;`resolveLocalInstant` 回答纪元毫秒,对落在前向跳变缺口内的墙钟值回答 `undefined`。 +`dsh-time-context` 同样消费它,并保留自己更严格的策略。`deriveBrowserTimeZoneContext` 通过 `canonicalizeTimeZone` 读取每条 user-rpc 时区,并拒绝规范答案与所报值不同的别名,因此请求边界仍然要求另一个进程能解析回同一名称。三条拒绝与它们的文案归 time-context 所有;共享入口只回答规范名称或一个原因,而被拒绝的结果会携带本库捕获到的平台错误,使 `unsupported` 拒绝保留其 `cause`。 + Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`decodeInstant`、`parseAtInput` 与 `futureInstant` 仍是 Schedule 的入口,保留既有错误码与诊断文案;它们现在调用共享解析器并映射其原因。Schedule 仍拥有持久记录结构、折叠与状态转移权威、抗注入的框架文本,以及全部重复规则运算:cron 引擎、每日与每周日期遍历、最近到期搜索,以及 Schedule 特有的决策时刻边界。 受维护的 `@js-temporal/polyfill` 0.5.1 在成为 `dsh-schedule` 运行时依赖之外,也成为 `dsh-util-time` 的运行时依赖。本地解析继续使用它,因此缺口与重叠仍由宿主 IANA 数据决定,也没有调用方需要自己实现偏移采样。`util/time` 的 README 与包描述删去了新增依赖所否定的 `zero-dependency` 表述。 @@ -36,4 +38,4 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec 代价是更宽的已发布接口与更重的依赖图:`dsh-util-time` 增加了对 `@js-temporal/polyfill` 的运行时依赖,因此只导入 `canonicalClientTimeZone` 的消费方也会加载该模块。本库也不再是纯"只做校验与规范化"的包——其 README 与包描述必须说明这一点,其"已知限制"一节现在也记录了它。Schedule 在每个入口保留一层薄映射。本地日历与时刻映射函数以 `never` 默认分支穷尽 switch 本库的封闭原因联合,因此新增原因会让这两个调用点构建失败,而不是静默走兜底分支。时区映射函数做不到这一点:本库的 `not_canonical_shape` 原因不可达,用 switch 会留下一个 Schedule 测试永远无法执行的分支。 -`dsh-time-context` 保留自己的 IANA 正则与规范化,不参与本次改动。它在请求边界拒绝非规范别名,而 Schedule 会把它规范化,因此统一二者意味着决定浏览器时区指令需要两种行为中的哪一种;该决定属于消费方,而不属于这次抽取。 +本次未触及、仍归 time-context 的两项:`packages/context/time-context/src/timestamp.ts` 中的 ISO 形状时间戳渲染(那是该包的持久读数字格式),以及其插件内的耗时措辞。浏览器客户端也保留自己的本地化日期渲染与自己的 UTC 偏移读取——它们服务的目的(读者的区域设置)与边界值不同;统一它们意味着先决定共享库是否也要拥有展示格式化,以及届时由谁拥有其不变量所校验的持久读数字格式。 diff --git a/docs/module-graph.md b/docs/module-graph.md index c040e60589..13299ccc70 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -637,6 +637,7 @@ flowchart TD pkg_time_context --> pkg_llm pkg_time_context --> pkg_session pkg_time_context --> pkg_session_projection + pkg_time_context --> pkg_util_time pkg_tmux_context --> pkg_agent pkg_tmux_context --> pkg_session pkg_tmux_context --> pkg_session_projection @@ -1554,7 +1555,7 @@ flowchart TD | [`goal`](../packages/goal/goal) | `goal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) | | [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`web`](../packages/web/web) | | [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent) | -| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) | +| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`util-time`](../packages/util/time) | | [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell) | | [`deepseek-account`](../packages/credentials/deepseek-account) | `credentials` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) | | [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index 18f9c9ed80..adcf2c8e4a 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -639,6 +639,7 @@ flowchart TD pkg_time_context --> pkg_llm pkg_time_context --> pkg_session pkg_time_context --> pkg_session_projection + pkg_time_context --> pkg_util_time pkg_tmux_context --> pkg_agent pkg_tmux_context --> pkg_session pkg_tmux_context --> pkg_session_projection @@ -1556,7 +1557,7 @@ flowchart TD | [`goal`](../packages/goal/goal) | `goal` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`typert-protocol`](../packages/typert/protocol) | | [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`agent`](../packages/core/agent), [`credentials`](../packages/credentials/credentials), [`launch-environment`](../packages/util/launch-environment), [`session`](../packages/core/session), [`web`](../packages/web/web) | | [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent) | -| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) | +| [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`util-time`](../packages/util/time) | | [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell) | | [`deepseek-account`](../packages/credentials/deepseek-account) | `credentials` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) | | [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) | diff --git a/packages/context/time-context/package.json b/packages/context/time-context/package.json index 3ff8a1ed5c..998145cb93 100644 --- a/packages/context/time-context/package.json +++ b/packages/context/time-context/package.json @@ -42,7 +42,8 @@ "@deepseek-ai/dsh-invariants": "workspace:*", "@deepseek-ai/dsh-llm": "workspace:*", "@deepseek-ai/dsh-session": "workspace:*", - "@deepseek-ai/dsh-session-projection": "workspace:*" + "@deepseek-ai/dsh-session-projection": "workspace:*", + "@deepseek-ai/dsh-util-time": "workspace:*" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", @@ -60,6 +61,7 @@ "@deepseek-ai/dsh-session-projection": "workspace:*", "@deepseek-ai/dsh-subprocess-local": "workspace:*", "@deepseek-ai/dsh-system-prompt": "workspace:*", - "@deepseek-ai/dsh-tools": "workspace:*" + "@deepseek-ai/dsh-tools": "workspace:*", + "@deepseek-ai/dsh-util-time": "workspace:*" } } diff --git a/packages/context/time-context/src/request-zone.ts b/packages/context/time-context/src/request-zone.ts index 41dad757af..517a31e07d 100644 --- a/packages/context/time-context/src/request-zone.ts +++ b/packages/context/time-context/src/request-zone.ts @@ -1,17 +1,22 @@ /** Browser-zone derivation and model-facing policy text for one open request turn. */ +import { canonicalizeTimeZone } from '@deepseek-ai/dsh-util-time' import type { UserMessage } from '@deepseek-ai/dsh-llm' import { assertNever } from '@deepseek-ai/dsh-util-values' -const IANA_TIME_ZONE = /^[A-Za-z][A-Za-z0-9_+.-]*(?:\/[A-Za-z0-9_+.-]+)+$/ - /** Browser-zone facts derived from user-rpc messages in one open turn. */ export type BrowserTimeZoneContext = | { readonly kind: 'resolved'; readonly timeZone: string } | { readonly kind: 'mixed'; readonly timeZones: readonly string[] } | { readonly kind: 'missing' } -/** Read and validate a Host-canonicalized browser zone from one ordinary user-rpc message. */ +/** + * Read and validate a Host-canonicalized browser zone from one ordinary user-rpc message. + * + * A request may only name a zone another process resolves back to the same + * identity, so the shared vocabulary's canonical answer must equal the reported + * value; an alias is refused here rather than silently rewritten. + */ function browserTimeZone(message: UserMessage): string | undefined { const source = message.source const value = source.kind === 'user' @@ -22,21 +27,22 @@ function browserTimeZone(message: UserMessage): string | undefined { ? source.clientTimeZone : undefined if (value === undefined) return undefined - if (value !== 'UTC' && !IANA_TIME_ZONE.test(value)) { - throw new TypeError( - `browser time zone must be canonical UTC or IANA Area/Location: ${JSON.stringify(value)}`, - ) + const resolved = canonicalizeTimeZone(value) + if (resolved.kind === 'accepted') { + if (resolved.timeZone !== value) { + throw new TypeError(`browser time zone must be canonical: ${JSON.stringify(value)}`) + } + return value } - let canonical: string - try { - canonical = new Intl.DateTimeFormat('en-US', { timeZone: value }).resolvedOptions().timeZone - } catch (error: unknown) { - throw new TypeError(`browser time zone is unsupported: ${JSON.stringify(value)}`, { cause: error }) + if (resolved.reason === 'unsupported') { + throw new TypeError(`browser time zone is unsupported: ${JSON.stringify(value)}`, { cause: resolved.cause }) } - if (canonical !== value) { - throw new TypeError(`browser time zone must be canonical: ${JSON.stringify(value)}`) - } - return value + // `not_canonical_shape` names a platform-canonical answer outside the Area/Location + // profile, which the shared vocabulary does not produce for a name it accepts. + /* v8 ignore next 2 -- Intl answers a canonical Area/Location for every name it accepts. */ + throw resolved.reason === 'not_canonical_shape' + ? new TypeError(`browser time zone must be canonical: ${JSON.stringify(value)}`) + : new TypeError(`browser time zone must be canonical UTC or IANA Area/Location: ${JSON.stringify(value)}`) } /** diff --git a/packages/context/time-context/tests/request-zone.spec.ts b/packages/context/time-context/tests/request-zone.spec.ts index a4752eea73..8dbd648572 100644 --- a/packages/context/time-context/tests/request-zone.spec.ts +++ b/packages/context/time-context/tests/request-zone.spec.ts @@ -53,6 +53,27 @@ describe('browser request-zone context', () => { ])).toThrow(/browser time zone must be canonical/) }) + it('keeps the exact refusal and the platform cause for every unusable zone', () => { + const refusal = (timeZone: string): Error => { + try { + deriveBrowserTimeZoneContext([browserMessage(timeZone)]) + throw new Error('expected a browser-zone refusal') + } catch (error: unknown) { + expect(error).toBeInstanceOf(TypeError) + return error as Error + } + } + expect(refusal('+08:00').message) + .toBe('browser time zone must be canonical UTC or IANA Area/Location: "+08:00"') + expect(refusal(' UTC').message) + .toBe('browser time zone must be canonical UTC or IANA Area/Location: " UTC"') + expect(refusal('Etc/UTC').message).toBe('browser time zone must be canonical: "Etc/UTC"') + const unsupported = refusal('Not/A_Real_Zone') + expect(unsupported.message).toBe('browser time zone is unsupported: "Not/A_Real_Zone"') + // The platform refusal stays the operator's only evidence of why a well-formed name failed. + expect((unsupported as Error & { cause?: unknown }).cause).toBeInstanceOf(RangeError) + }) + it('renders one explicit model policy for every context', () => { expect(renderBrowserTimeZoneContext({ kind: 'resolved', timeZone: 'Asia/Shanghai' })) .toContain('Interpret otherwise-unqualified dates and times in this zone.') diff --git a/packages/context/time-context/tsconfig.json b/packages/context/time-context/tsconfig.json index bb6a7d6927..e97b4b03d7 100644 --- a/packages/context/time-context/tsconfig.json +++ b/packages/context/time-context/tsconfig.json @@ -40,6 +40,9 @@ }, { "path": "../../session/session-projection" + }, + { + "path": "../../util/time" } ] } diff --git a/packages/schedule/schedule/src/domain.ts b/packages/schedule/schedule/src/domain.ts index ebb549a6c2..bd041e287b 100644 --- a/packages/schedule/schedule/src/domain.ts +++ b/packages/schedule/schedule/src/domain.ts @@ -285,16 +285,26 @@ function instantDiagnostic(reason: InstantRejection | undefined): string { /** * Map one shared zone rejection onto the stable Schedule input code. * @param reason - Reason the shared vocabulary refused the name. + * @param cause - Platform refusal the shared vocabulary caught, when it carried one. * @returns The public Schedule diagnostic for that refusal. */ -function timeZoneError(reason: TimeZoneRejection): ScheduleInputError { +function timeZoneError(reason: TimeZoneRejection, cause?: unknown): ScheduleInputError { + const options = cause === undefined ? undefined : { cause } // `not_canonical_shape` names a platform-canonical answer outside the Area/Location // profile, which the shared parser's `Intl` step does not produce; this branch keeps // the diagnostic this entry has always returned for that reason. /* v8 ignore next 2 -- the shared parser never answers not_canonical_shape for a canonical zone. */ return reason === 'not_canonical_shape' - ? new ScheduleInputError('invalid_time_zone', 'time_zone must resolve to UTC or an IANA Area/Location name.') - : new ScheduleInputError('invalid_time_zone', 'time_zone must be UTC or a valid IANA Area/Location name.') + ? new ScheduleInputError( + 'invalid_time_zone', + 'time_zone must resolve to UTC or an IANA Area/Location name.', + options, + ) + : new ScheduleInputError( + 'invalid_time_zone', + 'time_zone must be UTC or a valid IANA Area/Location name.', + options, + ) } /** @@ -304,7 +314,7 @@ function timeZoneError(reason: TimeZoneRejection): ScheduleInputError { */ export function canonicalizeTimeZone(value: string): string { const resolved = canonicalizeSharedTimeZone(value) - if (resolved.kind === 'rejected') throw timeZoneError(resolved.reason) + if (resolved.kind === 'rejected') throw timeZoneError(resolved.reason, resolved.cause) return resolved.timeZone } diff --git a/packages/util/time/README.i18n.yaml b/packages/util/time/README.i18n.yaml index 750a0db194..3e2ceb2cb7 100644 --- a/packages/util/time/README.i18n.yaml +++ b/packages/util/time/README.i18n.yaml @@ -18,17 +18,17 @@ en: bbff250eba0f1b89 zh: 0a618f05cc88ee2b /dsh-util-time/use-this-package/when-to-use-it: - en: 1c5733835410b678 - zh: ddf336123fed82db + en: f97e654746aa3712 + zh: 52ba385e9f3b96a9 /dsh-util-time/use-this-package/entry-point: en: 657c118ff4d9b99b zh: 2b11d97a40a2044f /dsh-util-time/understand-the-implementation: - en: 479489d729810a9f - zh: a238654f67e33b62 + en: a144232203c0c487 + zh: 5821b2adb564152c /dsh-util-time/further-exploration: - en: 7730949c9d91249b - zh: 006dc7155d6b5afc + en: 8aa95ebdfd1f323e + zh: c6879eac9fa6e087 /dsh-util-time/model-experience: en: 401c6ad02e1cdd75 zh: fd72235cb50cb3fa diff --git a/packages/util/time/README.md b/packages/util/time/README.md index 32b61b83de..422e896d8c 100644 --- a/packages/util/time/README.md +++ b/packages/util/time/README.md @@ -29,7 +29,7 @@ Shared time vocabulary for the boundaries that accept a caller's zone or a wall- It is a **library, not a service or plugin**: no `ctx`, registers nothing, holds no state. -Call it at the boundary that receives the zone or the wall-clock selector, before the value reaches anything durable. `dsh-api-session-controller` and `dsh-subagent` admit a caller-reported zone through `canonicalClientTimeZone`; `dsh-schedule` reads every absolute and wall-clock selector through the strict parsers and resolves its local targets through `resolveLocalInstant`. A consumer that owns calendar or interval arithmetic of its own keeps that arithmetic and calls these entries only for the boundary value. +Call it at the boundary that receives the zone or the wall-clock selector, before the value reaches anything durable. `dsh-api-session-controller` and `dsh-subagent` admit a caller-reported zone through `canonicalClientTimeZone`; `dsh-time-context` admits the zone a request turn reports through `canonicalizeTimeZone` and refuses an alias rather than rewriting it; `dsh-schedule` reads every absolute and wall-clock selector through the strict parsers and resolves its local targets through `resolveLocalInstant`. A consumer that owns calendar or interval arithmetic of its own keeps that arithmetic and calls these entries only for the boundary value. No runtime invariant companion is published because this pure utility owns no event stream or mutable runtime data; unit tests verify every entry. @@ -64,7 +64,7 @@ The strict parsers match a fixed regular expression before constructing any date | Entry | Accepted input | |---|---| | `canonicalClientTimeZone` | `UTC` or an IANA `Area/Location` name; answers `undefined` when refused. | -| `canonicalizeTimeZone` | The same names; answers the canonical spelling or a refusal reason. | +| `canonicalizeTimeZone` | The same names; answers the canonical spelling or a refusal reason, and carries the platform error when the platform refused a well-formed name. | | `parseLocalCalendar` | `YYYY-MM-DD` and `HH:mm:ss`, with optional one-to-three fractional digits. | | `parseOffsetInstant` | RFC 3339 with an explicit `Z` or numeric offset; the offset is part of the input. | | `fourDigitYearEpoch` | The canonical stored profile: exactly three fractional digits and a `Z`. | @@ -80,6 +80,7 @@ The strict parsers match a fixed regular expression before constructing any date - [Schedule Host storage](../../schedule/schedule/README.md) — the consumer that stores committed UTC targets. - [Session controller](../../api/session-controller/README.md) — the request boundary that canonicalizes a browser zone. +- [Time context](../../context/time-context/README.md) — the request consumer that refuses a non-canonical browser zone. - [util/ package map](../README.md) — where this library sits among the shared utilities. ----- diff --git a/packages/util/time/README.zh.md b/packages/util/time/README.zh.md index 9fbc058d1a..d11c62a4e1 100644 --- a/packages/util/time/README.zh.md +++ b/packages/util/time/README.zh.md @@ -29,7 +29,7 @@ kind: "package-library" 它是**库,不是服务也不是插件**:无 `ctx`、不注册任何东西、不持有状态。 -在接收时区或墙钟选择器的那个边界上调用它,让值在进入任何持久物之前先过一遍。`dsh-api-session-controller` 与 `dsh-subagent` 通过 `canonicalClientTimeZone` 接收调用方所报时区;`dsh-schedule` 通过严格解析器读取每一个绝对选择器与墙钟选择器,并通过 `resolveLocalInstant` 解析其本地目标。自行拥有日历或间隔运算的消费方保留那部分运算,只在边界值上调用这些入口。 +在接收时区或墙钟选择器的那个边界上调用它,让值在进入任何持久物之前先过一遍。`dsh-api-session-controller` 与 `dsh-subagent` 通过 `canonicalClientTimeZone` 接收调用方所报时区;`dsh-time-context` 通过 `canonicalizeTimeZone` 接收某一轮请求所报时区,并拒绝别名而不是把它改写;`dsh-schedule` 通过严格解析器读取每一个绝对选择器与墙钟选择器,并通过 `resolveLocalInstant` 解析其本地目标。自行拥有日历或间隔运算的消费方保留那部分运算,只在边界值上调用这些入口。 不发布运行时不变量伴生入口;这个纯工具不拥有事件流或可变运行时数据,每个入口都由单元测试验证。 @@ -64,7 +64,7 @@ if (local.kind === 'accepted') { | 入口 | 接受的输入 | |---|---| | `canonicalClientTimeZone` | `UTC` 或 IANA `Area/Location` 名称;被拒绝时回答 `undefined`。 | -| `canonicalizeTimeZone` | 同样的名称;回答规范拼写或拒绝原因。 | +| `canonicalizeTimeZone` | 同样的名称;回答规范拼写或拒绝原因;平台拒绝了一个格式合法的名称时,还会携带该平台错误。 | | `parseLocalCalendar` | `YYYY-MM-DD` 与 `HH:mm:ss`,可带 1 到 3 位小数秒。 | | `parseOffsetInstant` | 带显式 `Z` 或数字偏移的 RFC 3339;偏移量属于输入的一部分。 | | `fourDigitYearEpoch` | 规范存储剖面:恰好 3 位小数秒与 `Z`。 | @@ -80,6 +80,7 @@ if (local.kind === 'accepted') { - [Schedule 宿主存储](../../schedule/schedule/README.zh.md) —— 保存已提交 UTC 时点的消费方。 - [Session 控制器](../../api/session-controller/README.zh.md) —— 规范化浏览器时区的请求边界。 +- [时间上下文](../../context/time-context/README.zh.md) —— 拒绝非规范浏览器时区的请求消费方。 - [util/ 包地图](../README.zh.md) —— 本库在共享工具中的位置。 ----- diff --git a/packages/util/time/src/instant.ts b/packages/util/time/src/instant.ts index 0bec170408..ff80c31ad4 100644 --- a/packages/util/time/src/instant.ts +++ b/packages/util/time/src/instant.ts @@ -91,7 +91,12 @@ export type LocalCalendarResult = /** Discriminated result of resolving one raw IANA zone selector. */ export type TimeZoneResult = | { readonly kind: 'accepted'; readonly timeZone: string } - | { readonly kind: 'rejected'; readonly reason: TimeZoneRejection } + | { + readonly kind: 'rejected' + readonly reason: TimeZoneRejection + /** Platform error caught while resolving the name; present when the platform refused a well-formed one. */ + readonly cause?: unknown + } /** Reject with one stable reason. */ function rejected(reason: Reason): { kind: 'rejected'; reason: Reason } { @@ -257,9 +262,10 @@ export function canonicalizeTimeZone(value: string): TimeZoneResult { let canonical: string try { canonical = new Intl.DateTimeFormat('en-US', { timeZone: value }).resolvedOptions().timeZone - } catch { - // Intl rejects a well-formed name the platform's IANA data does not know. - return rejected('unsupported') + } catch (error: unknown) { + // Intl rejects a well-formed name the platform's IANA data does not know; + // the caught error is the caller's only evidence for its own diagnostic. + return Object.freeze({ kind: 'rejected', reason: 'unsupported', cause: error } as const) } /* v8 ignore next -- Intl answers UTC or a canonical IANA Area/Location for every accepted name. */ if (canonical !== 'UTC' && !IANA_TIME_ZONE.test(canonical)) return rejected('not_canonical_shape') diff --git a/packages/util/time/tests/instant.spec.ts b/packages/util/time/tests/instant.spec.ts index 0920c37351..c0196fe974 100644 --- a/packages/util/time/tests/instant.spec.ts +++ b/packages/util/time/tests/instant.spec.ts @@ -205,7 +205,13 @@ describe('canonicalizeTimeZone', () => { expect(canonicalizeTimeZone('Asia')).toEqual({ kind: 'rejected', reason: 'not_area_location' }) expect(canonicalizeTimeZone('GMT+8')).toEqual({ kind: 'rejected', reason: 'not_area_location' }) expect(canonicalizeTimeZone('utc')).toEqual({ kind: 'rejected', reason: 'not_area_location' }) - expect(canonicalizeTimeZone('Not/A_Real_Zone')).toEqual({ kind: 'rejected', reason: 'unsupported' }) + const unsupported = canonicalizeTimeZone('Not/A_Real_Zone') + if (unsupported.kind !== 'rejected') throw new Error('expected a rejection') + expect(unsupported.reason).toBe('unsupported') + // The platform refusal is the caller's only evidence of why a well-formed name failed, + // so the rejection carries it instead of hiding it. + expect(unsupported.cause).toBeInstanceOf(RangeError) + expect(canonicalizeTimeZone('Asia')).not.toHaveProperty('cause') }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3b5b05c5b6..c2a648408e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6558,6 +6558,9 @@ importers: '@deepseek-ai/dsh-tools': specifier: workspace:* version: link:../../core/tools + '@deepseek-ai/dsh-util-time': + specifier: workspace:* + version: link:../../util/time packages/context/tmux-context: dependencies: From 2d28a1dee7d6cbadd458998a76db7a0eab677123 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Thu, 24 Sep 2026 22:19:01 +0800 Subject: [PATCH 008/369] docs: narrow the zone ignore and correct the time-vocabulary note - the guarded ignore covers only the uncovered branch point - the note says two consumers copied the profile, counts copies rather than consumers, and scopes the platform cause to the unsupported rejection - the note names the remaining instant-profile copy in session-format --- .../2026-09-24-shared-time-vocabulary.i18n.yaml | 12 ++++++------ .../2026-09-24-shared-time-vocabulary.md | 6 +++--- .../2026-09-24-shared-time-vocabulary.zh.md | 6 +++--- packages/context/time-context/src/request-zone.ts | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml index cb843b513d..8154996dd4 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml @@ -6,14 +6,14 @@ en: ae04747237709d0c zh: a3731d98d5e940f9 /agent-note-shared-time-vocabulary-for-wall-clock-selectors/problem: - en: ad997234edc363a7 - zh: 25968ebb6fd9de71 + en: abd0263d14861926 + zh: e1736cd08e87f795 /agent-note-shared-time-vocabulary-for-wall-clock-selectors/decision: - en: 01f55d3c05812e4f - zh: 1764a14bcffeecd7 + en: 1a837ec23f5d3fd5 + zh: d7985a6bfa119e18 /agent-note-shared-time-vocabulary-for-wall-clock-selectors/alternatives-considered: en: 5bf3cc0d112531e7 zh: 99ed3e958d9d8ccb /agent-note-shared-time-vocabulary-for-wall-clock-selectors/consequences: - en: 3e10b12bf9e85faf - zh: 1362af0ff00c4683 + en: 3b73743147ddab96 + zh: 245bd1bf275f5953 diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md index d0daddb3c9..d2d6c01032 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md @@ -6,7 +6,7 @@ English | [中文](2026-09-24-shared-time-vocabulary.zh.md) ## Problem -`dsh-util-time` existed for one job and one caller shape: `canonicalClientTimeZone` validated a caller-reported zone at a wire boundary and answered `undefined`, which `dsh-api-session-controller` and `dsh-subagent` mapped onto their own domain errors. Three consumers had grown parts of the same vocabulary privately. Schedule held a strict RFC 3339 instant parser, a local-calendar parser, a four-digit-year bound and canonical formatter, a local wall-clock resolver, and its own IANA zone canonicalizer in `packages/schedule/schedule/src/domain.ts`, throwing `ScheduleInputError` instead of answering a result. `dsh-time-context` held a third copy of the IANA zone profile in `packages/context/time-context/src/request-zone.ts`, with its own regular expression and its own `Intl.DateTimeFormat` canonicalization, to enforce the stricter policy that a request may only name a zone another process resolves back to the same identity. The IANA profile and the `Intl` canonicalization step therefore existed three times, and the instant profile twice, so a change to either had to be made and verified once per copy, and the next consumer of a wall-clock selector had no shared entry point to reach for. +`dsh-util-time` existed for one job and one caller shape: `canonicalClientTimeZone` validated a caller-reported zone at a wire boundary and answered `undefined`, which `dsh-api-session-controller` and `dsh-subagent` mapped onto their own domain errors. Two other consumers had grown parts of the same vocabulary privately. Schedule held a strict RFC 3339 instant parser, a local-calendar parser, a four-digit-year bound and canonical formatter, a local wall-clock resolver, and its own IANA zone canonicalizer in `packages/schedule/schedule/src/domain.ts`, throwing `ScheduleInputError` instead of answering a result. `dsh-time-context` held a second copy of the IANA zone profile in `packages/context/time-context/src/request-zone.ts`, with its own regular expression and its own `Intl.DateTimeFormat` canonicalization, to enforce the stricter policy that a request may only name a zone another process resolves back to the same identity. The IANA profile and the `Intl` canonicalization step therefore existed three times in all, so a change to that profile had to be made and verified once per copy, and the next consumer of a wall-clock selector had no shared entry point to reach for. ## Decision @@ -16,7 +16,7 @@ The library answers discriminated results and carries no failure vocabulary. `pa Schedule consumes it and keeps every domain decision. `canonicalizeTimeZone`, `decodeInstant`, `parseAtInput`, and `futureInstant` remain Schedule entry points with their existing codes and diagnostics; they now call the shared parser and map its reason. Schedule still owns the durable record shapes, the fold and transition authority, the injection-resistant framing, and all recurrence arithmetic: the cron engine, the daily and weekly date walks, the latest-due search, and the schedule-specific accepted-decision bound. -`dsh-time-context` consumes it too and keeps its stricter policy. `deriveBrowserTimeZoneContext` reads each user-rpc zone through `canonicalizeTimeZone` and refuses an alias whose canonical answer differs from the reported value, so the request boundary still requires an identity another process resolves back to the same name. Time-context owns its three refusals and their messages; the shared entry only answers the canonical name or a reason, and a rejection carries the platform error the library caught so the `unsupported` refusal keeps its `cause`. +`dsh-time-context` consumes it too and keeps its stricter policy. `deriveBrowserTimeZoneContext` reads each user-rpc zone through `canonicalizeTimeZone` and refuses an alias whose canonical answer differs from the reported value, so the request boundary still requires an identity another process resolves back to the same name. Time-context owns its three refusals and their messages; the shared entry only answers the canonical name or a reason, and its `unsupported` rejection carries the platform error the library caught, so that refusal keeps its `cause` in both consumers. The maintained `@js-temporal/polyfill` 0.5.1 becomes a runtime dependency of `dsh-util-time` as well as of `dsh-schedule`. Local resolution continues to use it, so the Host's IANA data still decides gaps and overlaps and no caller owns offset sampling. `util/time`'s README and description lose the `zero-dependency` claim the added dependency contradicts. @@ -38,4 +38,4 @@ The time profile now has one home. A change to the accepted instant profile or t The cost is a wider published surface and a heavier dependency graph: `dsh-util-time` gains a runtime dependency on `@js-temporal/polyfill`, so a consumer that imports only `canonicalClientTimeZone` now loads that module too. The library is also no longer a pure "validation and canonicalization only" package, which its README and package description had to state and which its Known Limitations section now records. Schedule keeps a thin mapping layer at each entry. The local-calendar and instant mappers switch over the library's closed reason unions with a `never` default, so a newly added reason fails the build there instead of silently taking a catch-all. The zone mapper cannot: the library's `not_canonical_shape` reason is unreachable, so a switch would leave a branch the schedule suites can never execute. -Time-context keeps the two policies this change did not touch: its ISO-shaped timestamp rendering in `packages/context/time-context/src/timestamp.ts`, which is that package's durable reading format, and the elapsed-duration wording in its own plugin. The browser client keeps its own localized date rendering and its own UTC-offset reading, which serve a different purpose (a reader's locale) from a boundary value; unifying those means deciding whether the shared library should also own display formatting, and which package then owns the durable reading format its invariant validates. +Time-context keeps the two policies this change did not touch: its ISO-shaped timestamp rendering in `packages/context/time-context/src/timestamp.ts`, which is that package's durable reading format, and the elapsed-duration wording in its own plugin. The browser client keeps its own localized date rendering and its own UTC-offset reading, which serve a different purpose (a reader's locale) from a boundary value; unifying those means deciding whether the shared library should also own display formatting, and which package then owns the durable reading format its invariant validates. `packages/session/session-format-v0-to-v1/src/payload-validation.ts` also validates the canonical four-digit-year UTC instant with its own regular expression and round trip, which is a remaining copy of the instant profile this change did not touch. diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md index 0d886b3cac..27709825de 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md @@ -6,7 +6,7 @@ Status: implemented ## Problem -`dsh-util-time` 此前只承担一项职责、面向一种调用方:`canonicalClientTimeZone` 在协议边界校验调用方所报时区并回答 `undefined`,由 `dsh-api-session-controller` 与 `dsh-subagent` 映射成各自的域错误。同一套词汇的其余部分被三个消费方各自私下养大。Schedule 在 `packages/schedule/schedule/src/domain.ts` 里持有严格的 RFC 3339 时刻解析器、本地日历解析器、四位年份边界与规范格式化、本地墙钟解析,以及自己的 IANA 时区规范化实现——它抛 `ScheduleInputError` 而不是回答结果。`dsh-time-context` 在 `packages/context/time-context/src/request-zone.ts` 里持有第三份 IANA 时区剖面,配有自己的正则与自己的 `Intl.DateTimeFormat` 规范化,用来执行更严格的策略:一轮请求只能报出另一个进程会解析回同一标识的时区。因此 IANA 剖面与那一步 `Intl` 规范化存在三份、时刻剖面存在两份,任何一侧变更都要逐份改、逐份验;而下一个需要墙钟选择器的消费方没有共享入口可用。 +`dsh-util-time` 此前只承担一项职责、面向一种调用方:`canonicalClientTimeZone` 在协议边界校验调用方所报时区并回答 `undefined`,由 `dsh-api-session-controller` 与 `dsh-subagent` 映射成各自的域错误。同一套词汇的其余部分被另外两个消费方各自私下养大。Schedule 在 `packages/schedule/schedule/src/domain.ts` 里持有严格的 RFC 3339 时刻解析器、本地日历解析器、四位年份边界与规范格式化、本地墙钟解析,以及自己的 IANA 时区规范化实现——它抛 `ScheduleInputError` 而不是回答结果。`dsh-time-context` 在 `packages/context/time-context/src/request-zone.ts` 里持有第二份 IANA 时区剖面,配有自己的正则与自己的 `Intl.DateTimeFormat` 规范化,用来执行更严格的策略:一轮请求只能报出另一个进程会解析回同一标识的时区。因此 IANA 剖面与那一步 `Intl` 规范化合计存在三份,该剖面的任何变更都要逐份改、逐份验;而下一个需要墙钟选择器的消费方没有共享入口可用。 ## Decision @@ -14,7 +14,7 @@ Status: implemented 本库回答带判别值的结果,不持有失败词汇。`parseOffsetInstant`、`fourDigitYearEpoch`、`parseLocalCalendar` 与 `canonicalizeTimeZone` 回答 `accepted` 分支,或回答携带稳定原因的 `rejected` 分支(`not_rfc3339`、`not_a_calendar_date`、`not_a_wall_clock_time`、`invalid_numeric_offset`、`malformed`、`out_of_range`、`blank`、`padded`、`not_area_location`、`unsupported`、`not_canonical_shape`)。每个调用方把原因映射成自己的错误码与诊断文案,因此本库从不撰写面向模型的文本。`formatFourDigitYearInstant` 返回规范时刻文本,越界时返回 `undefined`;`resolveLocalInstant` 回答纪元毫秒,对落在前向跳变缺口内的墙钟值回答 `undefined`。 -`dsh-time-context` 同样消费它,并保留自己更严格的策略。`deriveBrowserTimeZoneContext` 通过 `canonicalizeTimeZone` 读取每条 user-rpc 时区,并拒绝规范答案与所报值不同的别名,因此请求边界仍然要求另一个进程能解析回同一名称。三条拒绝与它们的文案归 time-context 所有;共享入口只回答规范名称或一个原因,而被拒绝的结果会携带本库捕获到的平台错误,使 `unsupported` 拒绝保留其 `cause`。 +`dsh-time-context` 同样消费它,并保留自己更严格的策略。`deriveBrowserTimeZoneContext` 通过 `canonicalizeTimeZone` 读取每条 user-rpc 时区,并拒绝规范答案与所报值不同的别名,因此请求边界仍然要求另一个进程能解析回同一名称。三条拒绝与它们的文案归 time-context 所有;共享入口只回答规范名称或一个原因;被拒绝的结果中只有 `unsupported` 会携带本库捕获到的平台错误,使该拒绝在两个消费方都保留其 `cause`。 Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`decodeInstant`、`parseAtInput` 与 `futureInstant` 仍是 Schedule 的入口,保留既有错误码与诊断文案;它们现在调用共享解析器并映射其原因。Schedule 仍拥有持久记录结构、折叠与状态转移权威、抗注入的框架文本,以及全部重复规则运算:cron 引擎、每日与每周日期遍历、最近到期搜索,以及 Schedule 特有的决策时刻边界。 @@ -38,4 +38,4 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec 代价是更宽的已发布接口与更重的依赖图:`dsh-util-time` 增加了对 `@js-temporal/polyfill` 的运行时依赖,因此只导入 `canonicalClientTimeZone` 的消费方也会加载该模块。本库也不再是纯"只做校验与规范化"的包——其 README 与包描述必须说明这一点,其"已知限制"一节现在也记录了它。Schedule 在每个入口保留一层薄映射。本地日历与时刻映射函数以 `never` 默认分支穷尽 switch 本库的封闭原因联合,因此新增原因会让这两个调用点构建失败,而不是静默走兜底分支。时区映射函数做不到这一点:本库的 `not_canonical_shape` 原因不可达,用 switch 会留下一个 Schedule 测试永远无法执行的分支。 -本次未触及、仍归 time-context 的两项:`packages/context/time-context/src/timestamp.ts` 中的 ISO 形状时间戳渲染(那是该包的持久读数字格式),以及其插件内的耗时措辞。浏览器客户端也保留自己的本地化日期渲染与自己的 UTC 偏移读取——它们服务的目的(读者的区域设置)与边界值不同;统一它们意味着先决定共享库是否也要拥有展示格式化,以及届时由谁拥有其不变量所校验的持久读数字格式。 +本次未触及、仍归 time-context 的两项:`packages/context/time-context/src/timestamp.ts` 中的 ISO 形状时间戳渲染(那是该包的持久读数字格式),以及其插件内的耗时措辞。浏览器客户端也保留自己的本地化日期渲染与自己的 UTC 偏移读取——它们服务的目的(读者的区域设置)与边界值不同;统一它们意味着先决定共享库是否也要拥有展示格式化,以及届时由谁拥有其不变量所校验的持久读数字格式。`packages/session/session-format-v0-to-v1/src/payload-validation.ts` 也用自己的一段正则与往返比对校验规范的四位年份 UTC 时刻,这是本次未触及、仍然存在的时刻剖面副本。 diff --git a/packages/context/time-context/src/request-zone.ts b/packages/context/time-context/src/request-zone.ts index 517a31e07d..02b957b726 100644 --- a/packages/context/time-context/src/request-zone.ts +++ b/packages/context/time-context/src/request-zone.ts @@ -39,7 +39,7 @@ function browserTimeZone(message: UserMessage): string | undefined { } // `not_canonical_shape` names a platform-canonical answer outside the Area/Location // profile, which the shared vocabulary does not produce for a name it accepts. - /* v8 ignore next 2 -- Intl answers a canonical Area/Location for every name it accepts. */ + /* v8 ignore next -- Intl answers a canonical Area/Location for every name it accepts. */ throw resolved.reason === 'not_canonical_shape' ? new TypeError(`browser time zone must be canonical: ${JSON.stringify(value)}`) : new TypeError(`browser time zone must be canonical UTC or IANA Area/Location: ${JSON.stringify(value)}`) From 51a757285278e32b2554f4db300476d86e84d397 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Fri, 25 Sep 2026 00:31:56 +0800 Subject: [PATCH 009/369] refactor(time): answer the shared resolver from one place - resolveLocalInstant accepts an already-built plain date-time, so the recurrence walkers drop their private copy of the disambiguation and round-trip decision - decodeInstant narrows its field explicitly and drops the assertion - the model-input exhaustiveness fallbacks throw a plain Error, matching the timing-change fallback, while durable decoding keeps ScheduleLogError - the library module and README state which entries answer a discriminated result --- packages/schedule/schedule/src/domain.ts | 34 ++++++++++-------------- packages/util/time/README.i18n.yaml | 8 +++--- packages/util/time/README.md | 4 +-- packages/util/time/README.zh.md | 4 +-- packages/util/time/src/index.ts | 8 +++--- packages/util/time/src/instant.ts | 13 +++++---- packages/util/time/tests/instant.spec.ts | 8 ++++++ 7 files changed, 43 insertions(+), 36 deletions(-) diff --git a/packages/schedule/schedule/src/domain.ts b/packages/schedule/schedule/src/domain.ts index 84083f7cf2..97046c6ad7 100644 --- a/packages/schedule/schedule/src/domain.ts +++ b/packages/schedule/schedule/src/domain.ts @@ -253,26 +253,26 @@ function decodeId(value: unknown): ScheduleIdType { /** Validate one canonical four-digit-year UTC instant. */ function decodeInstant(value: unknown): string { - const parsed = typeof value === 'string' ? fourDigitYearEpoch(value) : undefined - if (parsed === undefined || parsed.kind === 'rejected') { - throw new ScheduleLogError(instantDiagnostic(parsed?.reason)) + if (typeof value !== 'string') { + throw new ScheduleLogError('scheduledAt must be a canonical four-digit-year RFC 3339 UTC instant') } - return value as string + const parsed = fourDigitYearEpoch(value) + if (parsed.kind === 'rejected') throw new ScheduleLogError(instantDiagnostic(parsed.reason)) + return value } /** * Map one shared instant rejection onto the stable stored-data diagnostic. - * @param reason - Reason the shared vocabulary refused the instant, or undefined for a non-string field. + * @param reason - Reason the shared vocabulary refused the instant. * @returns The Schedule log diagnostic for that refusal. */ -function instantDiagnostic(reason: InstantRejection | undefined): string { +function instantDiagnostic(reason: InstantRejection): string { switch (reason) { case 'not_a_calendar_date': return 'scheduledAt is not a real UTC calendar instant' case 'not_rfc3339': case 'not_a_wall_clock_time': case 'invalid_numeric_offset': - case undefined: return 'scheduledAt must be a canonical four-digit-year RFC 3339 UTC instant' /* v8 ignore next 3 -- the shared parser returns a closed rejection union. */ default: { @@ -339,17 +339,11 @@ function localCalendarDiagnostic(reason: LocalCalendarRejection): string { /* v8 ignore next 3 -- the shared parser returns a closed rejection union. */ default: { const unreachable: never = reason - throw new ScheduleLogError(`unknown local calendar rejection ${String(unreachable)}`) + throw new Error(`unknown local calendar rejection ${String(unreachable)}`) } } } -/** Resolve only the earlier overlap instant; a gap fails the local-field round trip. */ -function localInstant(local: Temporal.PlainDateTime, timeZone: string): number | undefined { - const zoned = local.toZonedDateTime(timeZone, { disambiguation: 'earlier' }) - return zoned.toPlainDateTime().equals(local) ? zoned.epochMilliseconds : undefined -} - /** Resolve a local one-shot, rejecting nonexistent wall-clock times. */ function resolveLocalAt(parts: LocalCalendarTime, timeZone: string): number { const target = resolveLocalInstant(parts, timeZone) @@ -392,7 +386,7 @@ function parseAtOffsetInstant(value: string): number { /* v8 ignore next 3 -- the shared parser returns a closed rejection union. */ default: { const unreachable: never = parsed.reason - throw new ScheduleLogError(`unknown instant rejection ${String(unreachable)}`) + throw new Error(`unknown instant rejection ${String(unreachable)}`) } } } @@ -818,7 +812,7 @@ function nextMatchingTarget( } for (; Temporal.PlainDate.compare(date, lastDate) <= 0; date = date.add({ days: 1 })) { if (!matches(date)) continue - const target = localInstant(date.toPlainDateTime(time), timeZone) + const target = resolveLocalInstant(date.toPlainDateTime(time), timeZone) if (target === undefined) continue if (target > MAX_FOUR_DIGIT_YEAR_MS) return undefined if (target >= MIN_FOUR_DIGIT_YEAR_MS && target > now) return new Date(target).toISOString() @@ -838,7 +832,7 @@ function latestDueOccurrence( let date = localDate(acceptedAt, 'UTC').add({ days: 1 }) for (;; date = date.subtract({ days: 1 })) { if (!matches(date)) continue - const candidate = localInstant(date.toPlainDateTime(time), timeZone) + const candidate = resolveLocalInstant(date.toPlainDateTime(time), timeZone) if (candidate === undefined || candidate > acceptedAt) continue // A committed target remains due even if current tzdata places the rule before it. return Math.max(candidate, savedTarget) @@ -986,7 +980,7 @@ function nextCronTarget(parsed: ParsedCronExpression, timeZone: string, floor: n if (matches(date)) { for (const time of times) { if (date.equals(firstDate) && Temporal.PlainTime.compare(time, floorTime) < 0) continue - const target = localInstant(date.toPlainDateTime(time), timeZone) + const target = resolveLocalInstant(date.toPlainDateTime(time), timeZone) if (target === undefined) continue if (target > MAX_FOUR_DIGIT_YEAR_MS) return undefined if (target >= MIN_FOUR_DIGIT_YEAR_MS && target > floor) return new Date(target).toISOString() @@ -1040,7 +1034,7 @@ function latestCronOccurrence( // An earliest time that a DST gap removes leaves the bound to the first time // that does exist, and a date with no existing time keeps the full walk. const earliest = times.reduce( - (found, time) => found ?? localInstant(date.toPlainDateTime(time), timeZone), + (found, time) => found ?? resolveLocalInstant(date.toPlainDateTime(time), timeZone), undefined, ) if (earliest !== undefined && earliest > decision) { @@ -1048,7 +1042,7 @@ function latestCronOccurrence( continue } for (const time of [...times].reverse()) { - const candidate = localInstant(date.toPlainDateTime(time), timeZone) + const candidate = resolveLocalInstant(date.toPlainDateTime(time), timeZone) if (candidate === undefined || candidate > decision || candidate < MIN_FOUR_DIGIT_YEAR_MS) continue // A committed target remains due even if current tzdata places the rule before it. return Math.max(candidate, savedTarget) diff --git a/packages/util/time/README.i18n.yaml b/packages/util/time/README.i18n.yaml index 3e2ceb2cb7..d9e8ac85ed 100644 --- a/packages/util/time/README.i18n.yaml +++ b/packages/util/time/README.i18n.yaml @@ -21,11 +21,11 @@ en: f97e654746aa3712 zh: 52ba385e9f3b96a9 /dsh-util-time/use-this-package/entry-point: - en: 657c118ff4d9b99b - zh: 2b11d97a40a2044f + en: 01ed3bdce21f0b71 + zh: 67e9fa556c3d25fd /dsh-util-time/understand-the-implementation: - en: a144232203c0c487 - zh: 5821b2adb564152c + en: a118f02fc58a14e5 + zh: ee5e0c0703f7eea2 /dsh-util-time/further-exploration: en: 8aa95ebdfd1f323e zh: c6879eac9fa6e087 diff --git a/packages/util/time/README.md b/packages/util/time/README.md index 422e896d8c..c491092c8c 100644 --- a/packages/util/time/README.md +++ b/packages/util/time/README.md @@ -47,7 +47,7 @@ if (local.kind === 'accepted') { } ``` -`canonicalClientTimeZone` answers `undefined` for a name it refuses, and the caller raises its own refusal — `session/invalid-time-zone` for the Session prompt, `subagent/invalid-time-zone` for a subagent continuation. Every other entry answers a discriminated result whose `rejected` arm carries a stable reason, so each caller again chooses its own code and diagnostic. +`canonicalClientTimeZone` answers `undefined` for a name it refuses, and the caller raises its own refusal — `session/invalid-time-zone` for the Session prompt, `subagent/invalid-time-zone` for a subagent continuation. Every entry that reads untrusted text answers a discriminated result whose `rejected` arm carries a stable reason, so each caller again chooses its own code and diagnostic. `formatFourDigitYearInstant` and `resolveLocalInstant` instead take already-validated values and answer a string or `undefined`. ----- @@ -57,7 +57,7 @@ if (local.kind === 'accepted') {
Implementation internals — click to expand -`src/instant.ts` holds the whole implementation; `src/index.ts` re-exports it and reduces `canonicalizeTimeZone` to the `string | undefined` form the wire boundaries already consume. The module JSDoc there states each entry's accepted profile and every reason. +`src/instant.ts` holds the whole implementation; `src/index.ts` re-exports it and adds `canonicalClientTimeZone`, the `string | undefined` form the wire boundaries already consume. The module JSDoc there states each entry's accepted profile and every reason. The strict parsers match a fixed regular expression before constructing any date, so a malformed spelling, an out-of-range clock field, and a date the calendar does not have answer distinct reasons. Local resolution defers to `@js-temporal/polyfill`: `disambiguation: 'earlier'` picks the first instant of an overlap, and the local-field round trip answers `undefined` for a value inside a forward transition's gap. diff --git a/packages/util/time/README.zh.md b/packages/util/time/README.zh.md index d11c62a4e1..57b83e9f59 100644 --- a/packages/util/time/README.zh.md +++ b/packages/util/time/README.zh.md @@ -47,7 +47,7 @@ if (local.kind === 'accepted') { } ``` -不可用的时区名称由 `canonicalClientTimeZone` 回答 `undefined`,由调用方抛出自己的拒绝——Session 提示词用 `session/invalid-time-zone`,subagent 续话用 `subagent/invalid-time-zone`。其余每个入口都回答带判别值的结果,其 `rejected` 分支携带稳定的拒绝原因,同样由调用方选择自己的错误码与诊断文案。 +不可用的时区名称由 `canonicalClientTimeZone` 回答 `undefined`,由调用方抛出自己的拒绝——Session 提示词用 `session/invalid-time-zone`,subagent 续话用 `subagent/invalid-time-zone`。读取不可信文本的每个入口都回答带判别值的结果,其 `rejected` 分支携带稳定的拒绝原因,同样由调用方选择自己的错误码与诊断文案。`formatFourDigitYearInstant` 与 `resolveLocalInstant` 则接收已校验的值,直接回答字符串或 `undefined`。 ----- @@ -57,7 +57,7 @@ if (local.kind === 'accepted') {
实现内部细节 —— 点击展开 -`src/instant.ts` 承载全部实现;`src/index.ts` 转出它,并把 `canonicalizeTimeZone` 归约为协议边界已经在消费的 `string | undefined` 形式。该模块的 JSDoc 说明每个入口接受的输入剖面与全部拒绝原因。 +`src/instant.ts` 承载全部实现;`src/index.ts` 转出它,并新增 `canonicalClientTimeZone`——协议边界已经在消费的 `string | undefined` 形式。该模块的 JSDoc 说明每个入口接受的输入剖面与全部拒绝原因。 严格解析器先匹配固定正则,再构造任何日期,因此拼写错误、越界时钟字段与日历上不存在的日期分别回答不同的原因。本地解析交给 `@js-temporal/polyfill`:`disambiguation: 'earlier'` 取重叠时段中的第一个时刻,而本地字段往返比对对前向跳变缺口内的值回答 `undefined`。 diff --git a/packages/util/time/src/index.ts b/packages/util/time/src/index.ts index 57c7b5d330..e81d831482 100644 --- a/packages/util/time/src/index.ts +++ b/packages/util/time/src/index.ts @@ -3,9 +3,11 @@ * wall-clock selector. * * Validation, canonicalization, and strict parsing only: this library formats - * no model-facing text and owns no failure vocabulary — every entry answers a - * discriminated result, and each boundary maps a reason onto its own domain - * code and diagnostic. + * no model-facing text and owns no failure vocabulary. An entry that reads + * untrusted text answers a discriminated result, and each caller maps a reason + * onto its own domain code and diagnostic; `formatFourDigitYearInstant` and + * `resolveLocalInstant` take already-validated values and answer their result + * directly. * @module @deepseek-ai/dsh-util-time */ diff --git a/packages/util/time/src/instant.ts b/packages/util/time/src/instant.ts index ff80c31ad4..a2e4de2d05 100644 --- a/packages/util/time/src/instant.ts +++ b/packages/util/time/src/instant.ts @@ -133,9 +133,9 @@ function plainDateTime(time: LocalCalendarTime): Temporal.PlainDateTime | undefi try { return Temporal.PlainDateTime.from(time, { overflow: 'reject' }) } catch (error: unknown) { - /* v8 ignore next -- every numeric field is pre-checked, so only a non-existent date reaches here. */ + /* v8 ignore next -- a rejected overflow answers a RangeError, so the false path is unreachable. */ if (error instanceof RangeError) return undefined - /* v8 ignore next -- Temporal throws only RangeError for rejected overflow. */ + /* v8 ignore next -- reached only when Temporal throws something other than a RangeError. */ throw error } } @@ -278,12 +278,15 @@ export function canonicalizeTimeZone(value: string): TimeZoneResult { * A value inside a forward transition's gap does not exist, so it answers * `undefined`; a value inside an overlap resolves to the earlier instant, which * is the same choice the maintained `Temporal` implementation makes. - * @param time - Exact calendar fields of a real date whose fields are already in range. + * @param time - Exact calendar fields of a real date, or an already-built plain date-time. * @param timeZone - Already-canonical `UTC` or IANA zone. * @returns The resolved epoch milliseconds, or `undefined` for a nonexistent wall-clock time. - * @throws RangeError when `timeZone` is not a known zone or `time` names an impossible calendar value. + * @throws RangeError when `timeZone` is not a known zone or the calendar value does not exist. */ -export function resolveLocalInstant(time: LocalCalendarTime, timeZone: string): number | undefined { +export function resolveLocalInstant( + time: LocalCalendarTime | Temporal.PlainDateTime, + timeZone: string, +): number | undefined { const local = Temporal.PlainDateTime.from(time, { overflow: 'reject' }) const zoned = local.toZonedDateTime(timeZone, { disambiguation: 'earlier' }) return zoned.toPlainDateTime().equals(local) ? zoned.epochMilliseconds : undefined diff --git a/packages/util/time/tests/instant.spec.ts b/packages/util/time/tests/instant.spec.ts index c0196fe974..9403c9d5f8 100644 --- a/packages/util/time/tests/instant.spec.ts +++ b/packages/util/time/tests/instant.spec.ts @@ -1,3 +1,4 @@ +import { Temporal } from '@js-temporal/polyfill' import { describe, expect, it } from 'vitest' import { MAX_FOUR_DIGIT_YEAR_MS, @@ -242,4 +243,11 @@ describe('resolveLocalInstant', () => { it('resolves UTC without a transition', () => { expect(resolveLocalInstant(local(2, 30), 'UTC')).toBe(Date.parse('2026-03-08T02:30:00.000Z')) }) + + it('accepts an already-built plain date-time, so a caller that holds one needs no copy', () => { + expect(resolveLocalInstant(Temporal.PlainDateTime.from('2026-03-08T12:00:00'), 'America/New_York')) + .toBe(Date.parse('2026-03-08T16:00:00.000Z')) + expect(resolveLocalInstant(Temporal.PlainDateTime.from('2026-03-08T02:30:00'), 'America/New_York')) + .toBeUndefined() + }) }) From 92ee4e98f099674b91860f3758b1b88b21b1b2e1 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Fri, 25 Sep 2026 00:41:31 +0800 Subject: [PATCH 010/369] docs(time): state each entry's return precisely - the README no longer claims resolveLocalInstant answers a string - the accepted-input table records the widened plain date-time input - canonicalClientTimeZone is named as the wire-boundary form rather than left outside a blanket claim about discriminated results - the non-canonical scheduledAt diagnostic has one source --- packages/schedule/schedule/src/domain.ts | 7 +++++-- packages/util/time/README.i18n.yaml | 8 ++++---- packages/util/time/README.md | 4 ++-- packages/util/time/README.zh.md | 4 ++-- packages/util/time/src/index.ts | 11 ++++++----- 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/schedule/schedule/src/domain.ts b/packages/schedule/schedule/src/domain.ts index 97046c6ad7..59e32e819c 100644 --- a/packages/schedule/schedule/src/domain.ts +++ b/packages/schedule/schedule/src/domain.ts @@ -251,10 +251,13 @@ function decodeId(value: unknown): ScheduleIdType { return ScheduleId(value) } +/** Stable Schedule diagnostic for a `scheduledAt` outside the canonical profile. */ +const NON_CANONICAL_INSTANT_MESSAGE = 'scheduledAt must be a canonical four-digit-year RFC 3339 UTC instant' + /** Validate one canonical four-digit-year UTC instant. */ function decodeInstant(value: unknown): string { if (typeof value !== 'string') { - throw new ScheduleLogError('scheduledAt must be a canonical four-digit-year RFC 3339 UTC instant') + throw new ScheduleLogError(NON_CANONICAL_INSTANT_MESSAGE) } const parsed = fourDigitYearEpoch(value) if (parsed.kind === 'rejected') throw new ScheduleLogError(instantDiagnostic(parsed.reason)) @@ -273,7 +276,7 @@ function instantDiagnostic(reason: InstantRejection): string { case 'not_rfc3339': case 'not_a_wall_clock_time': case 'invalid_numeric_offset': - return 'scheduledAt must be a canonical four-digit-year RFC 3339 UTC instant' + return NON_CANONICAL_INSTANT_MESSAGE /* v8 ignore next 3 -- the shared parser returns a closed rejection union. */ default: { const unreachable: never = reason diff --git a/packages/util/time/README.i18n.yaml b/packages/util/time/README.i18n.yaml index d9e8ac85ed..cd17b47167 100644 --- a/packages/util/time/README.i18n.yaml +++ b/packages/util/time/README.i18n.yaml @@ -21,11 +21,11 @@ en: f97e654746aa3712 zh: 52ba385e9f3b96a9 /dsh-util-time/use-this-package/entry-point: - en: 01ed3bdce21f0b71 - zh: 67e9fa556c3d25fd + en: e228864c11fc4cc7 + zh: 4da8068d3ec8e9b3 /dsh-util-time/understand-the-implementation: - en: a118f02fc58a14e5 - zh: ee5e0c0703f7eea2 + en: b4ff4a3e9574c97f + zh: 6ac32e07baec78e4 /dsh-util-time/further-exploration: en: 8aa95ebdfd1f323e zh: c6879eac9fa6e087 diff --git a/packages/util/time/README.md b/packages/util/time/README.md index c491092c8c..ba2983ee0b 100644 --- a/packages/util/time/README.md +++ b/packages/util/time/README.md @@ -47,7 +47,7 @@ if (local.kind === 'accepted') { } ``` -`canonicalClientTimeZone` answers `undefined` for a name it refuses, and the caller raises its own refusal — `session/invalid-time-zone` for the Session prompt, `subagent/invalid-time-zone` for a subagent continuation. Every entry that reads untrusted text answers a discriminated result whose `rejected` arm carries a stable reason, so each caller again chooses its own code and diagnostic. `formatFourDigitYearInstant` and `resolveLocalInstant` instead take already-validated values and answer a string or `undefined`. +`canonicalClientTimeZone` answers `undefined` for a name it refuses, and the caller raises its own refusal — `session/invalid-time-zone` for the Session prompt, `subagent/invalid-time-zone` for a subagent continuation. `canonicalizeTimeZone` reports why it refused, as a discriminated result whose `rejected` arm carries a stable reason, so each caller chooses its own code and diagnostic; the strict parsers do the same. `canonicalClientTimeZone` is the wire-boundary form of the zone entry and answers the canonical name or `undefined`. `formatFourDigitYearInstant` and `resolveLocalInstant` take already-validated values and answer a string, or epoch milliseconds, with `undefined` for an unrepresentable epoch or a wall-clock value inside a DST gap. ----- @@ -69,7 +69,7 @@ The strict parsers match a fixed regular expression before constructing any date | `parseOffsetInstant` | RFC 3339 with an explicit `Z` or numeric offset; the offset is part of the input. | | `fourDigitYearEpoch` | The canonical stored profile: exactly three fractional digits and a `Z`. | | `formatFourDigitYearInstant` | An integral epoch inside the four-digit-year range; answers `undefined` outside it or when non-integral. | -| `resolveLocalInstant` | Exact calendar fields and an already-canonical zone. | +| `resolveLocalInstant` | Exact calendar fields of a real date, or an already-built `Temporal.PlainDateTime`, plus an already-canonical zone. |
diff --git a/packages/util/time/README.zh.md b/packages/util/time/README.zh.md index 57b83e9f59..c28c8ae629 100644 --- a/packages/util/time/README.zh.md +++ b/packages/util/time/README.zh.md @@ -47,7 +47,7 @@ if (local.kind === 'accepted') { } ``` -不可用的时区名称由 `canonicalClientTimeZone` 回答 `undefined`,由调用方抛出自己的拒绝——Session 提示词用 `session/invalid-time-zone`,subagent 续话用 `subagent/invalid-time-zone`。读取不可信文本的每个入口都回答带判别值的结果,其 `rejected` 分支携带稳定的拒绝原因,同样由调用方选择自己的错误码与诊断文案。`formatFourDigitYearInstant` 与 `resolveLocalInstant` 则接收已校验的值,直接回答字符串或 `undefined`。 +不可用的时区名称由 `canonicalClientTimeZone` 回答 `undefined`,由调用方抛出自己的拒绝——Session 提示词用 `session/invalid-time-zone`,subagent 续话用 `subagent/invalid-time-zone`。`canonicalizeTimeZone` 会说明拒绝原因,以带判别值的结果回答,其 `rejected` 分支携带稳定的拒绝原因,由各调用方选择自己的错误码与诊断文案;严格解析器同样如此。`canonicalClientTimeZone` 是时区入口在协议边界上的形式,回答规范名称或 `undefined`。`formatFourDigitYearInstant` 与 `resolveLocalInstant` 接收已校验的值,分别回答字符串与纪元毫秒,无法表示的纪元或落在 DST 缺口内的墙钟值回答 `undefined`。 ----- @@ -69,7 +69,7 @@ if (local.kind === 'accepted') { | `parseOffsetInstant` | 带显式 `Z` 或数字偏移的 RFC 3339;偏移量属于输入的一部分。 | | `fourDigitYearEpoch` | 规范存储剖面:恰好 3 位小数秒与 `Z`。 | | `formatFourDigitYearInstant` | 四位年份范围内的整数纪元毫秒;超出范围或非整数时回答 `undefined`。 | -| `resolveLocalInstant` | 精确日历字段与已规范化的时区。 | +| `resolveLocalInstant` | 真实日期的精确日历字段,或已构造好的 `Temporal.PlainDateTime`,外加已规范化的时区。 |
diff --git a/packages/util/time/src/index.ts b/packages/util/time/src/index.ts index e81d831482..712ccc81bc 100644 --- a/packages/util/time/src/index.ts +++ b/packages/util/time/src/index.ts @@ -3,11 +3,12 @@ * wall-clock selector. * * Validation, canonicalization, and strict parsing only: this library formats - * no model-facing text and owns no failure vocabulary. An entry that reads - * untrusted text answers a discriminated result, and each caller maps a reason - * onto its own domain code and diagnostic; `formatFourDigitYearInstant` and - * `resolveLocalInstant` take already-validated values and answer their result - * directly. + * no model-facing text and owns no failure vocabulary. A refusal that carries a + * reason is a discriminated result, and each caller maps that reason onto its + * own domain code and diagnostic. `canonicalClientTimeZone` is the wire-boundary + * form and answers a name or `undefined`; `formatFourDigitYearInstant` and + * `resolveLocalInstant` take already-validated values and answer a string or + * epoch milliseconds directly. * @module @deepseek-ai/dsh-util-time */ From 7edd3efe85a4bca56d3e638c21721d81f5718ab6 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Fri, 25 Sep 2026 00:53:09 +0800 Subject: [PATCH 011/369] refactor(time): give the four-digit bound one owner - isFourDigitYearInstant is the shared predicate; Schedule's five representability guards delegate to it instead of re-deriving the range - the reason for a field outside its calendar range is out_of_range in both strict parsers, so year 0 no longer reads as a wall-clock problem - LocalCalendarTime.year states which entries reject year 0 and which accept it at the UTC edges - the note counts the five removed expressions and records that the zone mapper is a silent catch-all; the Chinese side mirrors the English paragraph order --- ...026-09-24-shared-time-vocabulary.i18n.yaml | 8 ++--- .../2026-09-24-shared-time-vocabulary.md | 6 ++-- .../2026-09-24-shared-time-vocabulary.zh.md | 10 +++--- packages/schedule/schedule/src/domain.ts | 20 +++++------ packages/util/time/README.i18n.yaml | 4 +-- packages/util/time/README.md | 1 + packages/util/time/README.zh.md | 1 + packages/util/time/src/instant.ts | 35 ++++++++++++++----- packages/util/time/tests/instant.spec.ts | 20 ++++++++--- 9 files changed, 67 insertions(+), 38 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml index 8154996dd4..1aeb7a8c7a 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml @@ -9,11 +9,11 @@ en: abd0263d14861926 zh: e1736cd08e87f795 /agent-note-shared-time-vocabulary-for-wall-clock-selectors/decision: - en: 1a837ec23f5d3fd5 - zh: d7985a6bfa119e18 + en: c210eaa5a6e384b0 + zh: 216fcad4fdb7c403 /agent-note-shared-time-vocabulary-for-wall-clock-selectors/alternatives-considered: en: 5bf3cc0d112531e7 zh: 99ed3e958d9d8ccb /agent-note-shared-time-vocabulary-for-wall-clock-selectors/consequences: - en: 3b73743147ddab96 - zh: 245bd1bf275f5953 + en: 8e01e84e42601cd9 + zh: 96d102ce5b05445a diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md index d2d6c01032..ffeb8f2405 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md @@ -12,7 +12,7 @@ English | [中文](2026-09-24-shared-time-vocabulary.zh.md) `dsh-util-time` owns the time vocabulary that boundaries accept. `packages/util/time/src/instant.ts` holds it, `src/index.ts` re-exports it and keeps `canonicalClientTimeZone` as the `string | undefined` wrapper the existing wire boundaries consume. -The library answers discriminated results and carries no failure vocabulary. `parseOffsetInstant`, `fourDigitYearEpoch`, `parseLocalCalendar`, and `canonicalizeTimeZone` answer an `accepted` arm or a `rejected` arm naming a stable reason (`not_rfc3339`, `not_a_calendar_date`, `not_a_wall_clock_time`, `invalid_numeric_offset`, `malformed`, `out_of_range`, `blank`, `padded`, `not_area_location`, `unsupported`, `not_canonical_shape`). Each caller maps a reason onto its own code and diagnostic, so the library never writes model-facing text. `formatFourDigitYearInstant` returns the canonical instant text or `undefined` outside the bound, and `resolveLocalInstant` answers epoch milliseconds or `undefined` for a wall-clock value inside a forward transition's gap. +The library answers discriminated results and carries no failure vocabulary. `parseOffsetInstant`, `fourDigitYearEpoch`, `parseLocalCalendar`, and `canonicalizeTimeZone` answer an `accepted` arm or a `rejected` arm naming a stable reason (`not_rfc3339`, `out_of_range`, `not_a_calendar_date`, `invalid_numeric_offset`, `malformed`, `blank`, `padded`, `not_area_location`, `unsupported`, `not_canonical_shape`). Each caller maps a reason onto its own code and diagnostic, so the library never writes model-facing text. `formatFourDigitYearInstant` returns the canonical instant text or `undefined` outside the bound, `isFourDigitYearInstant` answers whether an epoch is representable at all, and `resolveLocalInstant` answers epoch milliseconds or `undefined` for a wall-clock value inside a forward transition's gap. The reason vocabulary is parallel between the two strict parsers: the same condition is `out_of_range` in both, and `LocalCalendarTime.year` states which of them rejects year 0 (`parseLocalCalendar`) and which accepts it (`resolveLocalInstant`, whose callers derive local dates at the UTC edges). Schedule consumes it and keeps every domain decision. `canonicalizeTimeZone`, `decodeInstant`, `parseAtInput`, and `futureInstant` remain Schedule entry points with their existing codes and diagnostics; they now call the shared parser and map its reason. Schedule still owns the durable record shapes, the fold and transition authority, the injection-resistant framing, and all recurrence arithmetic: the cron engine, the daily and weekly date walks, the latest-due search, and the schedule-specific accepted-decision bound. @@ -34,8 +34,8 @@ Observable behavior does not change. Every accepted input resolves to the same e ## Consequences -The time profile now has one home. A change to the accepted instant profile or the zone profile is made once and covered once, and a new boundary imports it instead of copying it. Schedule's `domain.ts` loses the three regular expressions, the `Intl` canonicalization, the hand-rolled calendar conversion, and the four-digit bound arithmetic. +The time profile now has one home. A change to the accepted instant profile or the zone profile is made once and covered once, and a new boundary imports it instead of copying it. Schedule's `domain.ts` loses the five regular expressions that spelled the instant, calendar-date, local-time, and zone profiles, the `Intl` canonicalization, and the hand-rolled calendar conversion. It keeps the two bound constants for the date arithmetic at the UTC edges and delegates every representability guard to `isFourDigitYearInstant`, so the range itself has one owner. -The cost is a wider published surface and a heavier dependency graph: `dsh-util-time` gains a runtime dependency on `@js-temporal/polyfill`, so a consumer that imports only `canonicalClientTimeZone` now loads that module too. The library is also no longer a pure "validation and canonicalization only" package, which its README and package description had to state and which its Known Limitations section now records. Schedule keeps a thin mapping layer at each entry. The local-calendar and instant mappers switch over the library's closed reason unions with a `never` default, so a newly added reason fails the build there instead of silently taking a catch-all. The zone mapper cannot: the library's `not_canonical_shape` reason is unreachable, so a switch would leave a branch the schedule suites can never execute. +The cost is a wider published surface and a heavier dependency graph: `dsh-util-time` gains a runtime dependency on `@js-temporal/polyfill`, so a consumer that imports only `canonicalClientTimeZone` now loads that module too. The library is also no longer a pure "validation and canonicalization only" package, which its README and package description had to state and which its Known Limitations section now records. Schedule keeps a thin mapping layer at each entry. The local-calendar and instant mappers switch over the library's closed reason unions with a `never` default, so a newly added reason fails the build there instead of silently taking a catch-all. The zone mapper cannot: the library's `not_canonical_shape` reason is unreachable on the current IANA data, so a switch would leave a branch the schedule suites can never execute. That mapper therefore folds `blank`, `padded`, `not_area_location`, and `unsupported` into one diagnostic, and a reason added to `TimeZoneRejection` later would compile there and silently take that generic message. Time-context keeps the two policies this change did not touch: its ISO-shaped timestamp rendering in `packages/context/time-context/src/timestamp.ts`, which is that package's durable reading format, and the elapsed-duration wording in its own plugin. The browser client keeps its own localized date rendering and its own UTC-offset reading, which serve a different purpose (a reader's locale) from a boundary value; unifying those means deciding whether the shared library should also own display formatting, and which package then owns the durable reading format its invariant validates. `packages/session/session-format-v0-to-v1/src/payload-validation.ts` also validates the canonical four-digit-year UTC instant with its own regular expression and round trip, which is a remaining copy of the instant profile this change did not touch. diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md index 27709825de..7fde2e5452 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md @@ -12,12 +12,12 @@ Status: implemented `dsh-util-time` 拥有各边界所接收的时间词汇。它位于 `packages/util/time/src/instant.ts`,`src/index.ts` 转出它,并保留 `canonicalClientTimeZone` 作为既有协议边界消费的 `string | undefined` 包装。 -本库回答带判别值的结果,不持有失败词汇。`parseOffsetInstant`、`fourDigitYearEpoch`、`parseLocalCalendar` 与 `canonicalizeTimeZone` 回答 `accepted` 分支,或回答携带稳定原因的 `rejected` 分支(`not_rfc3339`、`not_a_calendar_date`、`not_a_wall_clock_time`、`invalid_numeric_offset`、`malformed`、`out_of_range`、`blank`、`padded`、`not_area_location`、`unsupported`、`not_canonical_shape`)。每个调用方把原因映射成自己的错误码与诊断文案,因此本库从不撰写面向模型的文本。`formatFourDigitYearInstant` 返回规范时刻文本,越界时返回 `undefined`;`resolveLocalInstant` 回答纪元毫秒,对落在前向跳变缺口内的墙钟值回答 `undefined`。 - -`dsh-time-context` 同样消费它,并保留自己更严格的策略。`deriveBrowserTimeZoneContext` 通过 `canonicalizeTimeZone` 读取每条 user-rpc 时区,并拒绝规范答案与所报值不同的别名,因此请求边界仍然要求另一个进程能解析回同一名称。三条拒绝与它们的文案归 time-context 所有;共享入口只回答规范名称或一个原因;被拒绝的结果中只有 `unsupported` 会携带本库捕获到的平台错误,使该拒绝在两个消费方都保留其 `cause`。 +本库回答带判别值的结果,不持有失败词汇。`parseOffsetInstant`、`fourDigitYearEpoch`、`parseLocalCalendar` 与 `canonicalizeTimeZone` 回答 `accepted` 分支,或回答携带稳定原因的 `rejected` 分支(`not_rfc3339`、`out_of_range`、`not_a_calendar_date`、`invalid_numeric_offset`、`malformed`、`blank`、`padded`、`not_area_location`、`unsupported`、`not_canonical_shape`)。每个调用方把原因映射成自己的错误码与诊断文案,因此本库从不撰写面向模型的文本。`formatFourDigitYearInstant` 返回规范时刻文本,越界时返回 `undefined`;`isFourDigitYearInstant` 回答某个纪元是否可表示;`resolveLocalInstant` 回答纪元毫秒,对落在前向跳变缺口内的墙钟值回答 `undefined`。两个严格解析器的原因词汇彼此对称:同一条件在两者中都叫 `out_of_range`,而 `LocalCalendarTime.year` 说明了二者中谁拒绝年份 0(`parseLocalCalendar`)、谁接受它(`resolveLocalInstant`,其调用方需要在 UTC 边界推导本地日期)。 Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`decodeInstant`、`parseAtInput` 与 `futureInstant` 仍是 Schedule 的入口,保留既有错误码与诊断文案;它们现在调用共享解析器并映射其原因。Schedule 仍拥有持久记录结构、折叠与状态转移权威、抗注入的框架文本,以及全部重复规则运算:cron 引擎、每日与每周日期遍历、最近到期搜索,以及 Schedule 特有的决策时刻边界。 +`dsh-time-context` 同样消费它,并保留自己更严格的策略。`deriveBrowserTimeZoneContext` 通过 `canonicalizeTimeZone` 读取每条 user-rpc 时区,并拒绝规范答案与所报值不同的别名,因此请求边界仍然要求另一个进程能解析回同一名称。三条拒绝与它们的文案归 time-context 所有;共享入口只回答规范名称或一个原因;被拒绝的结果中只有 `unsupported` 会携带本库捕获到的平台错误,使该拒绝在两个消费方都保留其 `cause`。 + 受维护的 `@js-temporal/polyfill` 0.5.1 在成为 `dsh-schedule` 运行时依赖之外,也成为 `dsh-util-time` 的运行时依赖。本地解析继续使用它,因此缺口与重叠仍由宿主 IANA 数据决定,也没有调用方需要自己实现偏移采样。`util/time` 的 README 与包描述删去了新增依赖所否定的 `zero-dependency` 表述。 可观察行为不变。每个被接受的输入解析出相同的纪元值,每个被拒绝的输入抛出相同的 `ScheduleInputError` 或 `ScheduleLogError` 错误码与相同的消息,存储的 `scheduledAt` 剖面不变,因此没有持久记录或录制快照需要重录。Schedule 的测试只断言错误码、从不断言这些消息字符串,因此 `packages/schedule/schedule/tests/domain.spec.ts` 新增了对三种本地日历诊断文案的断言;没有它,本次抽取中的一处措辞回退能通过整套测试。`packages/util/time/tests/instant.spec.ts` 直接覆盖新增入口,包含逐文件 100% 覆盖。 @@ -34,8 +34,8 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec ## Consequences -时间剖面现在只有一个归宿。时刻剖面或时区剖面的变更只做一次、只覆盖一次,新边界导入它而不是复制它。Schedule 的 `domain.ts` 去掉了三段正则、`Intl` 规范化、手写日历换算与四位年份边界运算。 +时间剖面现在只有一个归宿。时刻剖面或时区剖面的变更只做一次、只覆盖一次,新边界导入它而不是复制它。Schedule 的 `domain.ts` 去掉了为时刻、日历日期、本地时间与时区剖面命名的五段正则、`Intl` 规范化与手写日历换算。它保留了用于 UTC 边界日期运算的两个边界常量,并把每处可表示性守卫委托给 `isFourDigitYearInstant`,因此范围本身只有一个所有者。 -代价是更宽的已发布接口与更重的依赖图:`dsh-util-time` 增加了对 `@js-temporal/polyfill` 的运行时依赖,因此只导入 `canonicalClientTimeZone` 的消费方也会加载该模块。本库也不再是纯"只做校验与规范化"的包——其 README 与包描述必须说明这一点,其"已知限制"一节现在也记录了它。Schedule 在每个入口保留一层薄映射。本地日历与时刻映射函数以 `never` 默认分支穷尽 switch 本库的封闭原因联合,因此新增原因会让这两个调用点构建失败,而不是静默走兜底分支。时区映射函数做不到这一点:本库的 `not_canonical_shape` 原因不可达,用 switch 会留下一个 Schedule 测试永远无法执行的分支。 +代价是更宽的已发布接口与更重的依赖图:`dsh-util-time` 增加了对 `@js-temporal/polyfill` 的运行时依赖,因此只导入 `canonicalClientTimeZone` 的消费方也会加载该模块。本库也不再是纯"只做校验与规范化"的包——其 README 与包描述必须说明这一点,其"已知限制"一节现在也记录了它。Schedule 在每个入口保留一层薄映射。本地日历与时刻映射函数以 `never` 默认分支穷尽 switch 本库的封闭原因联合,因此新增原因会让这两个调用点构建失败,而不是静默走兜底分支。时区映射函数做不到这一点:在当前 IANA 数据下本库的 `not_canonical_shape` 原因不可达,用 switch 会留下一个 Schedule 测试永远无法执行的分支。因此该映射把 `blank`、`padded`、`not_area_location` 与 `unsupported` 合并成一条诊断,日后给 `TimeZoneRejection` 新增原因时会通过编译并静默套用那条通用文案。 本次未触及、仍归 time-context 的两项:`packages/context/time-context/src/timestamp.ts` 中的 ISO 形状时间戳渲染(那是该包的持久读数字格式),以及其插件内的耗时措辞。浏览器客户端也保留自己的本地化日期渲染与自己的 UTC 偏移读取——它们服务的目的(读者的区域设置)与边界值不同;统一它们意味着先决定共享库是否也要拥有展示格式化,以及届时由谁拥有其不变量所校验的持久读数字格式。`packages/session/session-format-v0-to-v1/src/payload-validation.ts` 也用自己的一段正则与往返比对校验规范的四位年份 UTC 时刻,这是本次未触及、仍然存在的时刻剖面副本。 diff --git a/packages/schedule/schedule/src/domain.ts b/packages/schedule/schedule/src/domain.ts index 59e32e819c..9d48935dd6 100644 --- a/packages/schedule/schedule/src/domain.ts +++ b/packages/schedule/schedule/src/domain.ts @@ -10,6 +10,7 @@ import { canonicalizeTimeZone as canonicalizeSharedTimeZone, formatFourDigitYearInstant, fourDigitYearEpoch, + isFourDigitYearInstant, parseLocalCalendar, parseOffsetInstant, resolveLocalInstant, @@ -274,7 +275,7 @@ function instantDiagnostic(reason: InstantRejection): string { case 'not_a_calendar_date': return 'scheduledAt is not a real UTC calendar instant' case 'not_rfc3339': - case 'not_a_wall_clock_time': + case 'out_of_range': case 'invalid_numeric_offset': return NON_CANONICAL_INSTANT_MESSAGE /* v8 ignore next 3 -- the shared parser returns a closed rejection union. */ @@ -384,7 +385,7 @@ function parseAtOffsetInstant(value: string): number { case 'invalid_numeric_offset': throw new ScheduleInputError('invalid_rule', 'The at numeric offset is invalid.') case 'not_a_calendar_date': - case 'not_a_wall_clock_time': + case 'out_of_range': throw new ScheduleInputError('invalid_rule', 'The at value must be a real ISO calendar date and time.') /* v8 ignore next 3 -- the shared parser returns a closed rejection union. */ default: { @@ -755,9 +756,7 @@ export function resolveEveryOccurrence( ): RecurringOccurrence { const target = Date.parse(record.scheduledAt) const interval = record.everySeconds * 1_000 - if (!Number.isSafeInteger(acceptedAt) - || acceptedAt < MIN_FOUR_DIGIT_YEAR_MS - || acceptedAt > MAX_FOUR_DIGIT_YEAR_MS) { + if (!isFourDigitYearInstant(acceptedAt)) { throw new ScheduleLogError('every acceptedAt must be a representable four-digit-year instant') } if (!Number.isSafeInteger(interval) || interval <= 0) { @@ -774,7 +773,7 @@ export function resolveEveryOccurrence( } const occurrenceAt = new Date(occurrence).toISOString() const next = occurrence + interval - if (!Number.isSafeInteger(next) || next > MAX_FOUR_DIGIT_YEAR_MS) { + if (!isFourDigitYearInstant(next)) { return Object.freeze({ occurrenceAt }) } return Object.freeze({ @@ -844,8 +843,7 @@ function latestDueOccurrence( /** Resolve a wall-clock rule only when its decision is at a representable four-digit-year instant. */ function acceptedDecision(selector: string, acceptedAt: number): number { - if (!Number.isSafeInteger(acceptedAt) - || acceptedAt < MIN_FOUR_DIGIT_YEAR_MS || acceptedAt > MAX_FOUR_DIGIT_YEAR_MS) { + if (!isFourDigitYearInstant(acceptedAt)) { throw new ScheduleLogError(`${selector} acceptedAt must be a representable four-digit-year instant`) } return acceptedAt @@ -1368,7 +1366,7 @@ export function createDailyScheduleRecord( throw new ScheduleInputError('invalid_prompt', 'prompt must be non-empty after trimming.') } const { time, timeZone } = parseDailyInput(daily) - if (!Number.isSafeInteger(now) || now < MIN_FOUR_DIGIT_YEAR_MS || now > MAX_FOUR_DIGIT_YEAR_MS) { + if (!isFourDigitYearInstant(now)) { throw new ScheduleInputError('time_out_of_range', 'Daily creation time must be a representable four-digit-year UTC instant.') } const scheduledAt = nextDailyTarget(dailyTime(time), timeZone, now) @@ -1421,7 +1419,7 @@ export function createWeeklyScheduleRecord( throw new ScheduleInputError('invalid_prompt', 'prompt must be non-empty after trimming.') } const { time, timeZone, weekdays } = parseWeeklyInput(weekly) - if (!Number.isSafeInteger(now) || now < MIN_FOUR_DIGIT_YEAR_MS || now > MAX_FOUR_DIGIT_YEAR_MS) { + if (!isFourDigitYearInstant(now)) { throw new ScheduleInputError('time_out_of_range', 'Weekly creation time must be a representable four-digit-year UTC instant.') } const scheduledAt = nextWeeklyTarget(weeklyTime(time), timeZone, weekdays, now) @@ -1455,7 +1453,7 @@ export function createCronScheduleRecord( throw new ScheduleInputError('invalid_prompt', 'prompt must be non-empty after trimming.') } const { expression, timeZone } = parseCronInput(cron) - if (!Number.isSafeInteger(now) || now < MIN_FOUR_DIGIT_YEAR_MS || now > MAX_FOUR_DIGIT_YEAR_MS) { + if (!isFourDigitYearInstant(now)) { throw new ScheduleInputError('time_out_of_range', 'Cron creation time must be a representable four-digit-year UTC instant.') } const scheduledAt = nextCronTarget(parseCronExpression(expression), timeZone, now) diff --git a/packages/util/time/README.i18n.yaml b/packages/util/time/README.i18n.yaml index cd17b47167..783391fab4 100644 --- a/packages/util/time/README.i18n.yaml +++ b/packages/util/time/README.i18n.yaml @@ -24,8 +24,8 @@ en: e228864c11fc4cc7 zh: 4da8068d3ec8e9b3 /dsh-util-time/understand-the-implementation: - en: b4ff4a3e9574c97f - zh: 6ac32e07baec78e4 + en: c814c83b60f43e0c + zh: 89021dc77356392b /dsh-util-time/further-exploration: en: 8aa95ebdfd1f323e zh: c6879eac9fa6e087 diff --git a/packages/util/time/README.md b/packages/util/time/README.md index ba2983ee0b..a9d1354bd3 100644 --- a/packages/util/time/README.md +++ b/packages/util/time/README.md @@ -69,6 +69,7 @@ The strict parsers match a fixed regular expression before constructing any date | `parseOffsetInstant` | RFC 3339 with an explicit `Z` or numeric offset; the offset is part of the input. | | `fourDigitYearEpoch` | The canonical stored profile: exactly three fractional digits and a `Z`. | | `formatFourDigitYearInstant` | An integral epoch inside the four-digit-year range; answers `undefined` outside it or when non-integral. | +| `isFourDigitYearInstant` | The same range, as a predicate, so a caller that only needs the bound does not re-derive it. | | `resolveLocalInstant` | Exact calendar fields of a real date, or an already-built `Temporal.PlainDateTime`, plus an already-canonical zone. | diff --git a/packages/util/time/README.zh.md b/packages/util/time/README.zh.md index c28c8ae629..81feca9d13 100644 --- a/packages/util/time/README.zh.md +++ b/packages/util/time/README.zh.md @@ -69,6 +69,7 @@ if (local.kind === 'accepted') { | `parseOffsetInstant` | 带显式 `Z` 或数字偏移的 RFC 3339;偏移量属于输入的一部分。 | | `fourDigitYearEpoch` | 规范存储剖面:恰好 3 位小数秒与 `Z`。 | | `formatFourDigitYearInstant` | 四位年份范围内的整数纪元毫秒;超出范围或非整数时回答 `undefined`。 | +| `isFourDigitYearInstant` | 同一范围的谓词形式,只判断范围边界的调用方无需自行推导。 | | `resolveLocalInstant` | 真实日期的精确日历字段,或已构造好的 `Temporal.PlainDateTime`,外加已规范化的时区。 | diff --git a/packages/util/time/src/instant.ts b/packages/util/time/src/instant.ts index a2e4de2d05..4abc541f6d 100644 --- a/packages/util/time/src/instant.ts +++ b/packages/util/time/src/instant.ts @@ -4,12 +4,12 @@ * * Every entry that reads untrusted text answers a discriminated result instead * of throwing: the library carries no failure vocabulary, and each caller maps - * a reason onto its own domain code and diagnostic. The two entries that take - * already-validated values throw on violated preconditions instead — - * `resolveLocalInstant` raises a `RangeError` for an unknown zone or an - * impossible calendar field, and `formatFourDigitYearInstant` cannot throw. - * Local wall-clock resolution uses the maintained `Temporal` implementation, so - * no caller owns offset sampling or DST rules. + * a reason onto its own domain code and diagnostic. `formatFourDigitYearInstant` + * and `isFourDigitYearInstant` never throw, while `resolveLocalInstant` raises a + * `RangeError` when a caller violates its precondition — an unknown zone, or + * calendar fields naming a value the calendar does not have. Local wall-clock + * resolution uses the maintained `Temporal` implementation, so no caller owns + * offset sampling or DST rules. * @module @deepseek-ai/dsh-util-time */ @@ -44,7 +44,11 @@ const IANA_TIME_ZONE = /^[A-Za-z][A-Za-z0-9_+.-]*(?:\/[A-Za-z0-9_+.-]+)+$/ /** Exact calendar fields of one local wall-clock value. */ export interface LocalCalendarTime { - /** Four-digit year; year 0 is rejected. */ + /** + * Year of the calendar date. `parseLocalCalendar` rejects year 0 in untrusted + * text; `resolveLocalInstant` accepts any year a real `Temporal` date carries, + * so a caller that needs the four-digit bound filters the answered epoch. + */ readonly year: number /** Month from 1 through 12. */ readonly month: number @@ -63,8 +67,9 @@ export interface LocalCalendarTime { /** Why an RFC 3339 instant string is unusable. */ export type InstantRejection = | 'not_rfc3339' + /** A field outside its calendar range: year 0, or an hour, minute, or second above its maximum. */ + | 'out_of_range' | 'not_a_calendar_date' - | 'not_a_wall_clock_time' | 'invalid_numeric_offset' /** Why a local calendar selector is unusable. */ @@ -194,7 +199,7 @@ export function parseOffsetInstant(value: string): InstantResult { millisecond: fractionalMilliseconds(groups['fraction']), } if (parts.year === 0 || parts.hour > 23 || parts.minute > 59 || parts.second > 59) { - return rejected('not_a_wall_clock_time') + return rejected('out_of_range') } const local = plainDateTime(parts) if (local === undefined) return rejected('not_a_calendar_date') @@ -230,6 +235,18 @@ export function fourDigitYearEpoch(value: string): InstantResult { return Object.freeze({ kind: 'accepted', epoch }) } +/** + * Whether an epoch is representable as a canonical four-digit-year RFC 3339 UTC instant. + * + * Callers that only need the bound use this instead of re-deriving it from the + * two constants, so the range has one owner. + * @param epoch - Candidate epoch milliseconds. + * @returns Whether the epoch is a safe integer inside the four-digit-year range. + */ +export function isFourDigitYearInstant(epoch: number): boolean { + return inFourDigitYearRange(epoch) +} + /** * Format one epoch as the canonical four-digit-year RFC 3339 UTC instant. * diff --git a/packages/util/time/tests/instant.spec.ts b/packages/util/time/tests/instant.spec.ts index 9403c9d5f8..1be860b3fc 100644 --- a/packages/util/time/tests/instant.spec.ts +++ b/packages/util/time/tests/instant.spec.ts @@ -6,6 +6,7 @@ import { canonicalizeTimeZone, formatFourDigitYearInstant, fourDigitYearEpoch, + isFourDigitYearInstant, parseLocalCalendar, parseOffsetInstant, resolveLocalInstant, @@ -100,19 +101,19 @@ describe('parseOffsetInstant', () => { it('rejects a clock field, a zero year, and a non-existent calendar date', () => { expect(parseOffsetInstant('0000-03-08T02:30:00Z')).toEqual({ kind: 'rejected', - reason: 'not_a_wall_clock_time', + reason: 'out_of_range', }) expect(parseOffsetInstant('2026-03-08T24:30:00Z')).toEqual({ kind: 'rejected', - reason: 'not_a_wall_clock_time', + reason: 'out_of_range', }) expect(parseOffsetInstant('2026-03-08T02:60:00Z')).toEqual({ kind: 'rejected', - reason: 'not_a_wall_clock_time', + reason: 'out_of_range', }) expect(parseOffsetInstant('2026-03-08T02:30:60Z')).toEqual({ kind: 'rejected', - reason: 'not_a_wall_clock_time', + reason: 'out_of_range', }) expect(parseOffsetInstant('2026-02-30T02:30:00Z')).toEqual({ kind: 'rejected', @@ -176,6 +177,17 @@ describe('fourDigitYearEpoch', () => { }) }) +describe('isFourDigitYearInstant', () => { + it('answers the range including both bounds and excludes everything else', () => { + expect(isFourDigitYearInstant(MIN_FOUR_DIGIT_YEAR_MS)).toBe(true) + expect(isFourDigitYearInstant(MAX_FOUR_DIGIT_YEAR_MS)).toBe(true) + expect(isFourDigitYearInstant(Date.parse('2026-03-08T02:30:00.000Z'))).toBe(true) + for (const epoch of [MIN_FOUR_DIGIT_YEAR_MS - 1, MAX_FOUR_DIGIT_YEAR_MS + 1, 0.5, Number.NaN]) { + expect(isFourDigitYearInstant(epoch)).toBe(false) + } + }) +}) + describe('formatFourDigitYearInstant', () => { it('formats both inclusive bounds in the canonical profile', () => { expect(formatFourDigitYearInstant(MIN_FOUR_DIGIT_YEAR_MS)).toBe('0001-01-01T00:00:00.000Z') From bbdac8febeabbbe0641351b017138e9d707d463a Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Fri, 25 Sep 2026 01:01:26 +0800 Subject: [PATCH 012/369] docs(time): state the module contract and the zone mapper as they are - the constants name one bound each - resolveLocalInstant reuses an instance a caller already holds instead of copying it per candidate date - the README entry-point paragraph drops its duplicate sentence - the note replaces an invalid reason for the zone mapper and a review-vantage sentence with the facts --- .../2026-09-24-shared-time-vocabulary.i18n.yaml | 8 ++++---- .../architecture/2026-09-24-shared-time-vocabulary.md | 4 ++-- .../architecture/2026-09-24-shared-time-vocabulary.zh.md | 4 ++-- packages/util/time/README.i18n.yaml | 4 ++-- packages/util/time/README.md | 2 +- packages/util/time/README.zh.md | 2 +- packages/util/time/src/instant.ts | 9 ++++++--- 7 files changed, 18 insertions(+), 15 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml index 1aeb7a8c7a..43250c42a4 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml @@ -9,11 +9,11 @@ en: abd0263d14861926 zh: e1736cd08e87f795 /agent-note-shared-time-vocabulary-for-wall-clock-selectors/decision: - en: c210eaa5a6e384b0 - zh: 216fcad4fdb7c403 + en: d97690dac210f970 + zh: b78439b9f402b278 /agent-note-shared-time-vocabulary-for-wall-clock-selectors/alternatives-considered: en: 5bf3cc0d112531e7 zh: 99ed3e958d9d8ccb /agent-note-shared-time-vocabulary-for-wall-clock-selectors/consequences: - en: 8e01e84e42601cd9 - zh: 96d102ce5b05445a + en: 22e0e257ba361a33 + zh: 599ad36dccf8bd64 diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md index ffeb8f2405..3654d58a15 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md @@ -20,7 +20,7 @@ Schedule consumes it and keeps every domain decision. `canonicalizeTimeZone`, `d The maintained `@js-temporal/polyfill` 0.5.1 becomes a runtime dependency of `dsh-util-time` as well as of `dsh-schedule`. Local resolution continues to use it, so the Host's IANA data still decides gaps and overlaps and no caller owns offset sampling. `util/time`'s README and description lose the `zero-dependency` claim the added dependency contradicts. -Observable behavior does not change. Every accepted input resolves to the same epoch, every refused input raises the same `ScheduleInputError` or `ScheduleLogError` code and the same message, and the stored `scheduledAt` profile is unchanged, so no durable record and no recorded snapshot is re-recorded. The schedule suites assert error codes and never these message strings, so `packages/schedule/schedule/tests/domain.spec.ts` gains an assertion on the three local-calendar diagnostics; without it a wording regression in the extraction passed the whole suite. `packages/util/time/tests/instant.spec.ts` covers the new entries directly, per-file 100% included. +Observable behavior does not change. Every accepted input resolves to the same epoch, every refused input raises the same `ScheduleInputError` or `ScheduleLogError` code and the same message, and the stored `scheduledAt` profile is unchanged, so no durable record and no recorded snapshot is re-recorded. Schedule's other suites assert error codes only, so the assertion this change adds to `packages/schedule/schedule/tests/domain.spec.ts` is the only place that pins the three local-calendar message strings. `packages/util/time/tests/instant.spec.ts` covers the new entries directly, per-file 100% included. ## Alternatives considered @@ -36,6 +36,6 @@ Observable behavior does not change. Every accepted input resolves to the same e The time profile now has one home. A change to the accepted instant profile or the zone profile is made once and covered once, and a new boundary imports it instead of copying it. Schedule's `domain.ts` loses the five regular expressions that spelled the instant, calendar-date, local-time, and zone profiles, the `Intl` canonicalization, and the hand-rolled calendar conversion. It keeps the two bound constants for the date arithmetic at the UTC edges and delegates every representability guard to `isFourDigitYearInstant`, so the range itself has one owner. -The cost is a wider published surface and a heavier dependency graph: `dsh-util-time` gains a runtime dependency on `@js-temporal/polyfill`, so a consumer that imports only `canonicalClientTimeZone` now loads that module too. The library is also no longer a pure "validation and canonicalization only" package, which its README and package description had to state and which its Known Limitations section now records. Schedule keeps a thin mapping layer at each entry. The local-calendar and instant mappers switch over the library's closed reason unions with a `never` default, so a newly added reason fails the build there instead of silently taking a catch-all. The zone mapper cannot: the library's `not_canonical_shape` reason is unreachable on the current IANA data, so a switch would leave a branch the schedule suites can never execute. That mapper therefore folds `blank`, `padded`, `not_area_location`, and `unsupported` into one diagnostic, and a reason added to `TimeZoneRejection` later would compile there and silently take that generic message. +The cost is a wider published surface and a heavier dependency graph: `dsh-util-time` gains a runtime dependency on `@js-temporal/polyfill`, so a consumer that imports only `canonicalClientTimeZone` now loads that module too. The library is also no longer a pure "validation and canonicalization only" package, which its README and package description had to state and which its Known Limitations section now records. Schedule keeps a thin mapping layer at each entry. The local-calendar and instant mappers switch over the library's closed reason unions with a `never` default, so a newly added reason fails the build there instead of silently taking a catch-all. The zone mapper instead branches only on `not_canonical_shape` — a reason the current IANA data never produces, so it is guarded by a `v8 ignore` rather than a `never` default — and folds `blank`, `padded`, `not_area_location`, and `unsupported` into one diagnostic. A reason added to `TimeZoneRejection` later would therefore compile there and silently take that generic message. Time-context keeps the two policies this change did not touch: its ISO-shaped timestamp rendering in `packages/context/time-context/src/timestamp.ts`, which is that package's durable reading format, and the elapsed-duration wording in its own plugin. The browser client keeps its own localized date rendering and its own UTC-offset reading, which serve a different purpose (a reader's locale) from a boundary value; unifying those means deciding whether the shared library should also own display formatting, and which package then owns the durable reading format its invariant validates. `packages/session/session-format-v0-to-v1/src/payload-validation.ts` also validates the canonical four-digit-year UTC instant with its own regular expression and round trip, which is a remaining copy of the instant profile this change did not touch. diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md index 7fde2e5452..5e9f324332 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md @@ -20,7 +20,7 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec 受维护的 `@js-temporal/polyfill` 0.5.1 在成为 `dsh-schedule` 运行时依赖之外,也成为 `dsh-util-time` 的运行时依赖。本地解析继续使用它,因此缺口与重叠仍由宿主 IANA 数据决定,也没有调用方需要自己实现偏移采样。`util/time` 的 README 与包描述删去了新增依赖所否定的 `zero-dependency` 表述。 -可观察行为不变。每个被接受的输入解析出相同的纪元值,每个被拒绝的输入抛出相同的 `ScheduleInputError` 或 `ScheduleLogError` 错误码与相同的消息,存储的 `scheduledAt` 剖面不变,因此没有持久记录或录制快照需要重录。Schedule 的测试只断言错误码、从不断言这些消息字符串,因此 `packages/schedule/schedule/tests/domain.spec.ts` 新增了对三种本地日历诊断文案的断言;没有它,本次抽取中的一处措辞回退能通过整套测试。`packages/util/time/tests/instant.spec.ts` 直接覆盖新增入口,包含逐文件 100% 覆盖。 +可观察行为不变。每个被接受的输入解析出相同的纪元值,每个被拒绝的输入抛出相同的 `ScheduleInputError` 或 `ScheduleLogError` 错误码与相同的消息,存储的 `scheduledAt` 剖面不变,因此没有持久记录或录制快照需要重录。Schedule 其余测试只断言错误码,因此本次在 `packages/schedule/schedule/tests/domain.spec.ts` 新增的断言是唯一固定这三条本地日历消息的地方。`packages/util/time/tests/instant.spec.ts` 直接覆盖新增入口,包含逐文件 100% 覆盖。 ## Alternatives considered @@ -36,6 +36,6 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec 时间剖面现在只有一个归宿。时刻剖面或时区剖面的变更只做一次、只覆盖一次,新边界导入它而不是复制它。Schedule 的 `domain.ts` 去掉了为时刻、日历日期、本地时间与时区剖面命名的五段正则、`Intl` 规范化与手写日历换算。它保留了用于 UTC 边界日期运算的两个边界常量,并把每处可表示性守卫委托给 `isFourDigitYearInstant`,因此范围本身只有一个所有者。 -代价是更宽的已发布接口与更重的依赖图:`dsh-util-time` 增加了对 `@js-temporal/polyfill` 的运行时依赖,因此只导入 `canonicalClientTimeZone` 的消费方也会加载该模块。本库也不再是纯"只做校验与规范化"的包——其 README 与包描述必须说明这一点,其"已知限制"一节现在也记录了它。Schedule 在每个入口保留一层薄映射。本地日历与时刻映射函数以 `never` 默认分支穷尽 switch 本库的封闭原因联合,因此新增原因会让这两个调用点构建失败,而不是静默走兜底分支。时区映射函数做不到这一点:在当前 IANA 数据下本库的 `not_canonical_shape` 原因不可达,用 switch 会留下一个 Schedule 测试永远无法执行的分支。因此该映射把 `blank`、`padded`、`not_area_location` 与 `unsupported` 合并成一条诊断,日后给 `TimeZoneRejection` 新增原因时会通过编译并静默套用那条通用文案。 +代价是更宽的已发布接口与更重的依赖图:`dsh-util-time` 增加了对 `@js-temporal/polyfill` 的运行时依赖,因此只导入 `canonicalClientTimeZone` 的消费方也会加载该模块。本库也不再是纯"只做校验与规范化"的包——其 README 与包描述必须说明这一点,其"已知限制"一节现在也记录了它。Schedule 在每个入口保留一层薄映射。本地日历与时刻映射函数以 `never` 默认分支穷尽 switch 本库的封闭原因联合,因此新增原因会让这两个调用点构建失败,而不是静默走兜底分支。时区映射函数则只对 `not_canonical_shape` 分支——当前 IANA 数据不会产生该原因,因此它由 `v8 ignore` 守护而不是 `never` 默认分支——并把 `blank`、`padded`、`not_area_location` 与 `unsupported` 合并成一条诊断。因此日后给 `TimeZoneRejection` 新增原因时会通过编译并静默套用那条通用文案。 本次未触及、仍归 time-context 的两项:`packages/context/time-context/src/timestamp.ts` 中的 ISO 形状时间戳渲染(那是该包的持久读数字格式),以及其插件内的耗时措辞。浏览器客户端也保留自己的本地化日期渲染与自己的 UTC 偏移读取——它们服务的目的(读者的区域设置)与边界值不同;统一它们意味着先决定共享库是否也要拥有展示格式化,以及届时由谁拥有其不变量所校验的持久读数字格式。`packages/session/session-format-v0-to-v1/src/payload-validation.ts` 也用自己的一段正则与往返比对校验规范的四位年份 UTC 时刻,这是本次未触及、仍然存在的时刻剖面副本。 diff --git a/packages/util/time/README.i18n.yaml b/packages/util/time/README.i18n.yaml index 783391fab4..5bbae49730 100644 --- a/packages/util/time/README.i18n.yaml +++ b/packages/util/time/README.i18n.yaml @@ -21,8 +21,8 @@ en: f97e654746aa3712 zh: 52ba385e9f3b96a9 /dsh-util-time/use-this-package/entry-point: - en: e228864c11fc4cc7 - zh: 4da8068d3ec8e9b3 + en: b5b79b2f1617b788 + zh: 72606179d6a7c33c /dsh-util-time/understand-the-implementation: en: c814c83b60f43e0c zh: 89021dc77356392b diff --git a/packages/util/time/README.md b/packages/util/time/README.md index a9d1354bd3..cad20e46c2 100644 --- a/packages/util/time/README.md +++ b/packages/util/time/README.md @@ -47,7 +47,7 @@ if (local.kind === 'accepted') { } ``` -`canonicalClientTimeZone` answers `undefined` for a name it refuses, and the caller raises its own refusal — `session/invalid-time-zone` for the Session prompt, `subagent/invalid-time-zone` for a subagent continuation. `canonicalizeTimeZone` reports why it refused, as a discriminated result whose `rejected` arm carries a stable reason, so each caller chooses its own code and diagnostic; the strict parsers do the same. `canonicalClientTimeZone` is the wire-boundary form of the zone entry and answers the canonical name or `undefined`. `formatFourDigitYearInstant` and `resolveLocalInstant` take already-validated values and answer a string, or epoch milliseconds, with `undefined` for an unrepresentable epoch or a wall-clock value inside a DST gap. +`canonicalClientTimeZone` answers `undefined` for a name it refuses, and the caller raises its own refusal — `session/invalid-time-zone` for the Session prompt, `subagent/invalid-time-zone` for a subagent continuation. `canonicalizeTimeZone`, the strict parsers, and `fourDigitYearEpoch` instead report why they refused, as a discriminated result whose `rejected` arm carries a stable reason, so each caller chooses its own code and diagnostic. `formatFourDigitYearInstant` and `resolveLocalInstant` take already-validated values and answer a string, or epoch milliseconds, with `undefined` for an unrepresentable epoch or a wall-clock value inside a DST gap. ----- diff --git a/packages/util/time/README.zh.md b/packages/util/time/README.zh.md index 81feca9d13..c1537dcbab 100644 --- a/packages/util/time/README.zh.md +++ b/packages/util/time/README.zh.md @@ -47,7 +47,7 @@ if (local.kind === 'accepted') { } ``` -不可用的时区名称由 `canonicalClientTimeZone` 回答 `undefined`,由调用方抛出自己的拒绝——Session 提示词用 `session/invalid-time-zone`,subagent 续话用 `subagent/invalid-time-zone`。`canonicalizeTimeZone` 会说明拒绝原因,以带判别值的结果回答,其 `rejected` 分支携带稳定的拒绝原因,由各调用方选择自己的错误码与诊断文案;严格解析器同样如此。`canonicalClientTimeZone` 是时区入口在协议边界上的形式,回答规范名称或 `undefined`。`formatFourDigitYearInstant` 与 `resolveLocalInstant` 接收已校验的值,分别回答字符串与纪元毫秒,无法表示的纪元或落在 DST 缺口内的墙钟值回答 `undefined`。 +不可用的时区名称由 `canonicalClientTimeZone` 回答 `undefined`,由调用方抛出自己的拒绝——Session 提示词用 `session/invalid-time-zone`,subagent 续话用 `subagent/invalid-time-zone`。`canonicalizeTimeZone`、各严格解析器与 `fourDigitYearEpoch` 则会说明拒绝原因,以带判别值的结果回答,其 `rejected` 分支携带稳定的拒绝原因,由各调用方选择自己的错误码与诊断文案。`formatFourDigitYearInstant` 与 `resolveLocalInstant` 接收已校验的值,分别回答字符串与纪元毫秒,无法表示的纪元或落在 DST 缺口内的墙钟值回答 `undefined`。 ----- diff --git a/packages/util/time/src/instant.ts b/packages/util/time/src/instant.ts index 4abc541f6d..60aac90ffb 100644 --- a/packages/util/time/src/instant.ts +++ b/packages/util/time/src/instant.ts @@ -15,10 +15,10 @@ import { Temporal } from '@js-temporal/polyfill' -/** Inclusive epoch-millisecond bounds of a four-digit-year RFC 3339 UTC instant. */ +/** Inclusive lower epoch-millisecond bound of a four-digit-year RFC 3339 UTC instant. */ export const MIN_FOUR_DIGIT_YEAR_MS = Date.parse('0001-01-01T00:00:00.000Z') -/** Inclusive epoch-millisecond bounds of a four-digit-year RFC 3339 UTC instant. */ +/** Inclusive upper epoch-millisecond bound of a four-digit-year RFC 3339 UTC instant. */ export const MAX_FOUR_DIGIT_YEAR_MS = Date.parse('9999-12-31T23:59:59.999Z') /** Canonical four-digit-year RFC 3339 UTC instant with mandatory milliseconds. */ @@ -304,7 +304,10 @@ export function resolveLocalInstant( time: LocalCalendarTime | Temporal.PlainDateTime, timeZone: string, ): number | undefined { - const local = Temporal.PlainDateTime.from(time, { overflow: 'reject' }) + // A caller walking candidate dates already holds an instance, and `from` copies it. + const local = time instanceof Temporal.PlainDateTime + ? time + : Temporal.PlainDateTime.from(time, { overflow: 'reject' }) const zoned = local.toZonedDateTime(timeZone, { disambiguation: 'earlier' }) return zoned.toPlainDateTime().equals(local) ? zoned.epochMilliseconds : undefined } From 0939392e71ab7901edaaf5fc81ee7a1d665d77e5 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Fri, 25 Sep 2026 14:48:01 +0800 Subject: [PATCH 013/369] feat(util-time): add a millisecond Duration span Duration holds one finite millisecond count behind a frozen class: a class rather than a branded number because TypeScript erases a brand through arithmetic, so only an instance can stop seconds and milliseconds from being added. It adds, subtracts, orders, converts to larger units fractionally, and splits a non-negative span into whole days, hours, minutes, and seconds. There is no valueOf and no toString, so a reader always states its unit. dsh-time-context reads its elapsed line through it and keeps the wording: a backward clock reading still clamps to zero there. The rendered text is byte-identical for every finite sample, verified by a differential over more than eight thousand inputs. A non-finite sample now throws from the span constructor instead of rendering 'NaNs', which the package's own reading invariant already refused. --- ...026-09-24-shared-time-vocabulary.i18n.yaml | 12 +- .../2026-09-24-shared-time-vocabulary.md | 8 + .../2026-09-24-shared-time-vocabulary.zh.md | 8 + packages/context/time-context/src/index.ts | 19 +- packages/util/README.i18n.yaml | 6 +- packages/util/README.md | 4 +- packages/util/README.zh.md | 2 +- packages/util/time/README.i18n.yaml | 24 +-- packages/util/time/README.md | 18 +- packages/util/time/README.zh.md | 18 +- packages/util/time/package.json | 2 +- packages/util/time/src/duration.ts | 184 ++++++++++++++++++ packages/util/time/src/index.ts | 6 +- packages/util/time/tests/duration.spec.ts | 102 ++++++++++ 14 files changed, 366 insertions(+), 47 deletions(-) create mode 100644 packages/util/time/src/duration.ts create mode 100644 packages/util/time/tests/duration.spec.ts diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml index 43250c42a4..31c914f4ff 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml @@ -9,11 +9,11 @@ en: abd0263d14861926 zh: e1736cd08e87f795 /agent-note-shared-time-vocabulary-for-wall-clock-selectors/decision: - en: d97690dac210f970 - zh: b78439b9f402b278 + en: 34feee585cb33001 + zh: d882bbba56a679bd /agent-note-shared-time-vocabulary-for-wall-clock-selectors/alternatives-considered: - en: 5bf3cc0d112531e7 - zh: 99ed3e958d9d8ccb + en: 1737484c099fab52 + zh: 303b144c52d167bd /agent-note-shared-time-vocabulary-for-wall-clock-selectors/consequences: - en: 22e0e257ba361a33 - zh: 599ad36dccf8bd64 + en: 7e3004318640cca7 + zh: 007d7fe761718d37 diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md index 3654d58a15..46dfbe6504 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md @@ -18,6 +18,8 @@ Schedule consumes it and keeps every domain decision. `canonicalizeTimeZone`, `d `dsh-time-context` consumes it too and keeps its stricter policy. `deriveBrowserTimeZoneContext` reads each user-rpc zone through `canonicalizeTimeZone` and refuses an alias whose canonical answer differs from the reported value, so the request boundary still requires an identity another process resolves back to the same name. Time-context owns its three refusals and their messages; the shared entry only answers the canonical name or a reason, and its `unsupported` rejection carries the platform error the library caught, so that refusal keeps its `cause` in both consumers. +`Duration` holds one span as a single millisecond count, and it is a class rather than a branded number because TypeScript's arithmetic erases a brand: adding two differently branded numbers answers an unbranded `number`, so a brand cannot stop seconds and milliseconds from being mixed, while an instance can. The unit is milliseconds because every producer and consumer in this repository already uses it (`Date.now()`, `SessionEvent.time`, the session log, the client), and because a double holds every millisecond of the whole `Date` range exactly, so no precision is spent on the unit. Instances are frozen; the count is readable only as `milliseconds` or through a fractional `toSeconds`/`toMinutes`/`toHours`/`toDays` conversion, and there is deliberately no `valueOf` and no `toString`, so no reader ever receives a bare count whose unit it has to remember. Addition, subtraction, ordering, and a whole-part `decompose()` are the whole operation set. + The maintained `@js-temporal/polyfill` 0.5.1 becomes a runtime dependency of `dsh-util-time` as well as of `dsh-schedule`. Local resolution continues to use it, so the Host's IANA data still decides gaps and overlaps and no caller owns offset sampling. `util/time`'s README and description lose the `zero-dependency` claim the added dependency contradicts. Observable behavior does not change. Every accepted input resolves to the same epoch, every refused input raises the same `ScheduleInputError` or `ScheduleLogError` code and the same message, and the stored `scheduledAt` profile is unchanged, so no durable record and no recorded snapshot is re-recorded. Schedule's other suites assert error codes only, so the assertion this change adds to `packages/schedule/schedule/tests/domain.spec.ts` is the only place that pins the three local-calendar message strings. `packages/util/time/tests/instant.spec.ts` covers the new entries directly, per-file 100% included. @@ -30,6 +32,10 @@ Observable behavior does not change. Every accepted input resolves to the same e **Have the library return `undefined` instead of reasons.** That is the shape `canonicalClientTimeZone` already has, and it was the smaller change. It was rejected because Schedule must distinguish a malformed instant from a non-existent calendar date from an out-of-range clock field to keep its existing diagnostics, and `canonicalizeTimeZone` must distinguish a bad name from a locally unresolvable one; collapsing those to one falsy value would have forced Schedule to re-derive the distinction with its own regular expressions, restoring the duplication. +**Brand the millisecond count instead of building a class.** `Branded` already exists in this repository for opaque identities, and a branded count is smaller than a class. It was rejected because the brand does not survive the operation that matters: TypeScript accepts `brandedA + brandedB` and answers an unbranded `number`, so a caller who adds a span to an epoch silently loses the unit, which is the exact mistake the type exists to prevent. A class makes that addition a type error, and it also gives the whole-part split one home. + +**Hold the span in nanoseconds.** `Temporal` carries nanoseconds internally as a `bigint`, so a nanosecond span is representable. It was rejected for the same reason the boundaries stay in milliseconds: the count would have to be a `bigint`, which no `number` arithmetic, no `Date`, and no `JSON.stringify` accepts, so every consumer and every durable record would need a string or binary hop to move it. A nanosecond span as a `number` would instead be quantized to roughly 256 ns at the current epoch, which is a different value wearing the same name. + **Add formatting and DST arithmetic for other consumers too.** No current consumer needs model-facing formatting or offset arithmetic from this library: `dsh-time-context` owns its own formatter and zone policy text. Adding them now would give the library a public operation set with no consumer evidence, against the package rule that ties each abstraction to a current owner and need. ## Consequences @@ -38,4 +44,6 @@ The time profile now has one home. A change to the accepted instant profile or t The cost is a wider published surface and a heavier dependency graph: `dsh-util-time` gains a runtime dependency on `@js-temporal/polyfill`, so a consumer that imports only `canonicalClientTimeZone` now loads that module too. The library is also no longer a pure "validation and canonicalization only" package, which its README and package description had to state and which its Known Limitations section now records. Schedule keeps a thin mapping layer at each entry. The local-calendar and instant mappers switch over the library's closed reason unions with a `never` default, so a newly added reason fails the build there instead of silently taking a catch-all. The zone mapper instead branches only on `not_canonical_shape` — a reason the current IANA data never produces, so it is guarded by a `v8 ignore` rather than a `never` default — and folds `blank`, `padded`, `not_area_location`, and `unsupported` into one diagnostic. A reason added to `TimeZoneRejection` later would therefore compile there and silently take that generic message. +The elapsed reading keeps its split in the library and its wording in the consumer: `dsh-time-context` still decides that a backward clock reading clamps to zero and how the compact line reads, while `Duration.decompose()` answers the whole parts. That swap is byte-identical for every finite sample, verified by a differential over more than eight thousand inputs including negatives, fractions, and values past a year. One input does change: a non-finite elapsed sample now throws from the span constructor instead of rendering `NaNs`, which the package's own reading invariant already refused, so the failure moves to the sample that produced it. + Time-context keeps the two policies this change did not touch: its ISO-shaped timestamp rendering in `packages/context/time-context/src/timestamp.ts`, which is that package's durable reading format, and the elapsed-duration wording in its own plugin. The browser client keeps its own localized date rendering and its own UTC-offset reading, which serve a different purpose (a reader's locale) from a boundary value; unifying those means deciding whether the shared library should also own display formatting, and which package then owns the durable reading format its invariant validates. `packages/session/session-format-v0-to-v1/src/payload-validation.ts` also validates the canonical four-digit-year UTC instant with its own regular expression and round trip, which is a remaining copy of the instant profile this change did not touch. diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md index 5e9f324332..8ff750c478 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md @@ -18,6 +18,8 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec `dsh-time-context` 同样消费它,并保留自己更严格的策略。`deriveBrowserTimeZoneContext` 通过 `canonicalizeTimeZone` 读取每条 user-rpc 时区,并拒绝规范答案与所报值不同的别名,因此请求边界仍然要求另一个进程能解析回同一名称。三条拒绝与它们的文案归 time-context 所有;共享入口只回答规范名称或一个原因;被拒绝的结果中只有 `unsupported` 会携带本库捕获到的平台错误,使该拒绝在两个消费方都保留其 `cause`。 +`Duration` 以单一毫秒计数保存一段时间;它做成类而不是 branded number,是因为 TypeScript 的算术会抹掉 brand:两个不同品牌的数字相加得到的是无品牌的 `number`,所以 brand 挡不住秒与毫秒的混用,而实例可以。单位选毫秒,是因为本仓库的每个生产者与消费者本来就用它(`Date.now()`、`SessionEvent.time`、Session 日志、客户端),也因为 double 能精确表示 `Date` 全范围内的每一个毫秒,这个单位不消耗任何精度。实例被冻结;计数只能通过 `milliseconds` 或按小数换算的 `toSeconds`/`toMinutes`/`toHours`/`toDays` 读取,且刻意不提供 `valueOf` 与 `toString`,因此读取方永远不会拿到一个需要自己记单位的裸数字。加、减、排序与整数量拆解 `decompose()` 就是全部操作集合。 + 受维护的 `@js-temporal/polyfill` 0.5.1 在成为 `dsh-schedule` 运行时依赖之外,也成为 `dsh-util-time` 的运行时依赖。本地解析继续使用它,因此缺口与重叠仍由宿主 IANA 数据决定,也没有调用方需要自己实现偏移采样。`util/time` 的 README 与包描述删去了新增依赖所否定的 `zero-dependency` 表述。 可观察行为不变。每个被接受的输入解析出相同的纪元值,每个被拒绝的输入抛出相同的 `ScheduleInputError` 或 `ScheduleLogError` 错误码与相同的消息,存储的 `scheduledAt` 剖面不变,因此没有持久记录或录制快照需要重录。Schedule 其余测试只断言错误码,因此本次在 `packages/schedule/schedule/tests/domain.spec.ts` 新增的断言是唯一固定这三条本地日历消息的地方。`packages/util/time/tests/instant.spec.ts` 直接覆盖新增入口,包含逐文件 100% 覆盖。 @@ -30,6 +32,10 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec **让本库回答 `undefined` 而不是原因。** 这正是 `canonicalClientTimeZone` 已有的形态,也是改动更小的做法。之所以否决,是因为 Schedule 必须区分格式错误的时刻、日历上不存在的日期与越界时钟字段,才能保留既有诊断;`canonicalizeTimeZone` 也必须区分名称本身不合法与本地无法解析。把二者压成一个假值,会迫使 Schedule 用自己的正则重新推导这层区分,从而恢复重复。 +**给毫秒计数加 brand,而不是造类。** 本仓库已有用于不透明标识的 `Branded`,branded 计数也比类更小。之所以否决,是因为 brand 撑不过真正要紧的那步运算:TypeScript 接受 `brandedA + brandedB` 并回答无品牌的 `number`,于是把时间段加到纪元上的调用方会静默丢掉单位,而这正是该类型要防的错误。类会把这种相加变成类型错误,也让整数量拆解只有一个归宿。 + +**用纳秒保存时间段。** `Temporal` 内部以 `bigint` 携带纳秒,所以纳秒时间段是可表示的。之所以否决,理由与边界保持毫秒相同:计数必须变成 `bigint`,而 `number` 算术、`Date` 与 `JSON.stringify` 都不接受它,于是每个消费方与每条持久记录都要多一次字符串或二进制转换。若改用 `number` 表示纳秒,在当前离线纪元量级上它会被量化到约 256 ns,那是同一个名字下的另一个值。 + **同时为其他消费方加入格式化与 DST 运算。** 目前没有消费方需要本库提供面向模型的格式化或偏移运算:`dsh-time-context` 自己拥有格式化器与时区策略文本。现在加入只会给本库一个没有消费方证据的公开操作集合,违背"每项抽象都绑定当前所有者与需求"的包规则。 ## Consequences @@ -38,4 +44,6 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec 代价是更宽的已发布接口与更重的依赖图:`dsh-util-time` 增加了对 `@js-temporal/polyfill` 的运行时依赖,因此只导入 `canonicalClientTimeZone` 的消费方也会加载该模块。本库也不再是纯"只做校验与规范化"的包——其 README 与包描述必须说明这一点,其"已知限制"一节现在也记录了它。Schedule 在每个入口保留一层薄映射。本地日历与时刻映射函数以 `never` 默认分支穷尽 switch 本库的封闭原因联合,因此新增原因会让这两个调用点构建失败,而不是静默走兜底分支。时区映射函数则只对 `not_canonical_shape` 分支——当前 IANA 数据不会产生该原因,因此它由 `v8 ignore` 守护而不是 `never` 默认分支——并把 `blank`、`padded`、`not_area_location` 与 `unsupported` 合并成一条诊断。因此日后给 `TimeZoneRejection` 新增原因时会通过编译并静默套用那条通用文案。 +耗时读数的拆分上收到库、措辞留在一侧:`dsh-time-context` 仍然决定回退的时钟读数钳到零、以及那行紧凑文本怎么写,而 `Duration.decompose()` 回答整数量。这次替换对每个有限样本都逐字不变,已用一份覆盖八千多个输入(含负数、小数与一年以上的值)的差分验证。有一个输入会变:非有限的耗时样本现在由时间段构造函数抛出,而不是渲染出 `NaNs`——而那本来就被该包自己的读数不变量拒绝,所以失败点前移到了产生它的那次采样。 + 本次未触及、仍归 time-context 的两项:`packages/context/time-context/src/timestamp.ts` 中的 ISO 形状时间戳渲染(那是该包的持久读数字格式),以及其插件内的耗时措辞。浏览器客户端也保留自己的本地化日期渲染与自己的 UTC 偏移读取——它们服务的目的(读者的区域设置)与边界值不同;统一它们意味着先决定共享库是否也要拥有展示格式化,以及届时由谁拥有其不变量所校验的持久读数字格式。`packages/session/session-format-v0-to-v1/src/payload-validation.ts` 也用自己的一段正则与往返比对校验规范的四位年份 UTC 时刻,这是本次未触及、仍然存在的时刻剖面副本。 diff --git a/packages/context/time-context/src/index.ts b/packages/context/time-context/src/index.ts index 493630a811..55eb5779ad 100644 --- a/packages/context/time-context/src/index.ts +++ b/packages/context/time-context/src/index.ts @@ -19,6 +19,7 @@ declare module '@deepseek-ai/dsh-llm' { import type { UserMessage } from '@deepseek-ai/dsh-llm' import { SessionSeq } from '@deepseek-ai/dsh-session' +import { Duration } from '@deepseek-ai/dsh-util-time' import type {} from '@deepseek-ai/dsh-session-projection' import { deriveBrowserTimeZoneContext, @@ -66,15 +67,17 @@ export const Config: z = z.object({ refreshIntervalMs: z.number(), }) -/** Format a non-negative elapsed millisecond count as compact whole-second units. */ +/** + * Format a non-negative elapsed millisecond count as compact whole-second units. + * + * A backward wall-clock reading is a property of the clock rather than of the + * span, so it clamps to zero here; the shared span type then supplies the whole + * parts, and this package keeps the wording. + */ function formatDuration(elapsedMs: number): string { - let seconds = Math.floor(Math.max(0, elapsedMs) / 1000) - const days = Math.floor(seconds / 86_400) - seconds %= 86_400 - const hours = Math.floor(seconds / 3600) - seconds %= 3600 - const minutes = Math.floor(seconds / 60) - seconds %= 60 + const { days, hours, minutes, seconds } = Duration + .fromMilliseconds(Math.max(0, elapsedMs)) + .decompose() const parts: string[] = [] if (days > 0) parts.push(`${days}d`) if (hours > 0) parts.push(`${hours}h`) diff --git a/packages/util/README.i18n.yaml b/packages/util/README.i18n.yaml index 70526d2d7e..c63d668ebb 100644 --- a/packages/util/README.i18n.yaml +++ b/packages/util/README.i18n.yaml @@ -9,14 +9,14 @@ en: 7283f2afb343d150 zh: 32c56bd67c0314c6 /util-shared-utilities/summary: - en: 2424d44f0d79be74 + en: f21c74532a9c929e zh: 5bc05166e8d83c1e /util-shared-utilities/table-of-contents: en: 1eb35b015a6c423f zh: a8a3dceee2c9e943 /util-shared-utilities/packages: - en: 662b2baeac105692 - zh: f076c6d39c2985b1 + en: 44eb08c6c7a5bbfb + zh: 9857e5374e5b14de /util-shared-utilities/related-documentation: en: 432ae81110914571 zh: c5bf4efc007311f3 diff --git a/packages/util/README.md b/packages/util/README.md index 7184f8f595..022eb85ec7 100644 --- a/packages/util/README.md +++ b/packages/util/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -The `util/` group gives capability packages shared mechanical primitives instead of duplicate implementations. It covers atomic writes, branded ids, deques, lossless JSON values, UUIDs, Harness-home paths, launch environments, outbound proxy policy, native commands, output retention, time-zone and instant vocabulary, and timeout handling. Every root entry here is a library: it registers no product service or event, and the consuming capability retains the business semantics. +The `util/` group gives capability packages shared mechanical primitives instead of duplicate implementations. It covers atomic writes, branded ids, deques, lossless JSON values, UUIDs, Harness-home paths, launch environments, outbound proxy policy, native commands, output retention, time-zone, instant, and span vocabulary, and timeout handling. Every root entry here is a library: it registers no product service or event, and the consuming capability retains the business semantics. ## Table of Contents @@ -41,7 +41,7 @@ Each package provides one primitive; open a package page for how to use it. | [`workspace-path/`](workspace-path/README.md) | Provides browser-safe Workspace path and display helpers | | [`code-language/`](code-language/README.md) | Maps a filename suffix to the syntax-highlighting language that the Code preview, diff review, and the read card render | | [`output-retention/`](output-retention/README.md) | Bounds model-facing output and reports exact omission metadata | -| [`time/`](time/README.md) | Parses strict instants, local calendars, and caller-reported IANA zones | +| [`time/`](time/README.md) | Parses strict instants, local calendars, and caller-reported IANA zones, and holds a millisecond span | | [`timeout/`](timeout/README.md) | Deadline arithmetic, signal fusion, and timeout-versus-cancel classification | ----- diff --git a/packages/util/README.zh.md b/packages/util/README.zh.md index 33253cc139..a31d113d30 100644 --- a/packages/util/README.zh.md +++ b/packages/util/README.zh.md @@ -41,7 +41,7 @@ kind: "package-group" | [`workspace-path/`](workspace-path/README.zh.md) | 提供浏览器安全的 Workspace 路径与显示辅助函数 | | [`code-language/`](code-language/README.zh.md) | 把文件名后缀映射为 Code 预览、diff 审阅与 read 卡片所渲染的语法高亮语言 | | [`output-retention/`](output-retention/README.zh.md) | 限制面向模型的输出并报告精确的省略元数据 | -| [`time/`](time/README.zh.md) | 解析严格的时刻、本地日历与调用方所报的 IANA 时区 | +| [`time/`](time/README.zh.md) | 解析严格的时刻、本地日历与调用方所报的 IANA 时区,并持有毫秒精度的时间段 | | [`timeout/`](timeout/README.zh.md) | 截止时间运算、信号融合与超时和取消分类 | ----- diff --git a/packages/util/time/README.i18n.yaml b/packages/util/time/README.i18n.yaml index 5bbae49730..c0d042b47a 100644 --- a/packages/util/time/README.i18n.yaml +++ b/packages/util/time/README.i18n.yaml @@ -3,14 +3,14 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/util/time/README.md /: - en: 78b8a5dd36b637f5 - zh: 451e95d5bc7d5a66 + en: 6daf8db648646a31 + zh: 156a7d47413a6a55 /dsh-util-time: en: bc299a9b8fe7717f zh: 896661ee0c4edf25 /dsh-util-time/summary: - en: 6d4d39093c2a2b5a - zh: 1dc4af01352581f2 + en: cdc849b4f44094b1 + zh: b03edaa6a020d39d /dsh-util-time/table-of-contents: en: d152484eb41ac6b4 zh: f14f34fce35a8d90 @@ -18,14 +18,14 @@ en: bbff250eba0f1b89 zh: 0a618f05cc88ee2b /dsh-util-time/use-this-package/when-to-use-it: - en: f97e654746aa3712 - zh: 52ba385e9f3b96a9 + en: 7015b3ed33de4c53 + zh: d4fc9aee6f6910ca /dsh-util-time/use-this-package/entry-point: - en: b5b79b2f1617b788 - zh: 72606179d6a7c33c + en: 306f0f5d65af8223 + zh: a79b60c39be361c8 /dsh-util-time/understand-the-implementation: - en: c814c83b60f43e0c - zh: 89021dc77356392b + en: f42cb0fc332d7740 + zh: f980bcae2f72fdd8 /dsh-util-time/further-exploration: en: 8aa95ebdfd1f323e zh: c6879eac9fa6e087 @@ -36,8 +36,8 @@ en: daf6e45a18c087b0 zh: e6714f1e65ed55ce /dsh-util-time/known-limitations-and-deferred-work: - en: 8b29ec4add8f93e6 - zh: ee9bfd6224a4204c + en: b4108682b9b18245 + zh: 7dfb014c2dc1775f /dsh-util-time/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: e88aeb617ffb74ea diff --git a/packages/util/time/README.md b/packages/util/time/README.md index cad20e46c2..7bb1e068ae 100644 --- a/packages/util/time/README.md +++ b/packages/util/time/README.md @@ -1,5 +1,5 @@ --- -description: "Strict RFC 3339 instant, local-calendar, and IANA zone parsing for maintainers accepting a caller-reported zone or a wall-clock selector at a boundary." +description: "Strict RFC 3339 instant, local-calendar, and IANA zone parsing, plus a millisecond span type, for maintainers accepting a caller-reported zone, a wall-clock selector, or an elapsed time at a boundary." kind: "package-library" --- @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -Shared time vocabulary for the boundaries that accept a caller's zone or a wall-clock selector. `canonicalClientTimeZone` admits `UTC` or an IANA `Area/Location` name and answers the platform-canonical spelling, so an alias never reaches a durable record. A wall-clock consumer instead parses strict RFC 3339 instants, local calendar fields, and zone names through discriminated results, then resolves a local value with maintained `Temporal` DST and overlap behavior. The library owns no failure vocabulary or model-facing text: each caller maps a reason onto its own domain code. +Shared time vocabulary for the boundaries that accept a caller's zone, a wall-clock selector, or an elapsed time. `canonicalClientTimeZone` answers the platform-canonical `UTC` or IANA `Area/Location` spelling, so an alias never reaches a durable record. The strict parsers read RFC 3339 instants, local calendar fields, and zone names as discriminated results, and `resolveLocalInstant` applies maintained `Temporal` DST and overlap behavior. `Duration` holds a span in milliseconds and splits it into whole parts. The library owns no failure vocabulary and no model-facing text: each caller maps a reason onto its own domain code. ## Table of Contents @@ -29,14 +29,14 @@ Shared time vocabulary for the boundaries that accept a caller's zone or a wall- It is a **library, not a service or plugin**: no `ctx`, registers nothing, holds no state. -Call it at the boundary that receives the zone or the wall-clock selector, before the value reaches anything durable. `dsh-api-session-controller` and `dsh-subagent` admit a caller-reported zone through `canonicalClientTimeZone`; `dsh-time-context` admits the zone a request turn reports through `canonicalizeTimeZone` and refuses an alias rather than rewriting it; `dsh-schedule` reads every absolute and wall-clock selector through the strict parsers and resolves its local targets through `resolveLocalInstant`. A consumer that owns calendar or interval arithmetic of its own keeps that arithmetic and calls these entries only for the boundary value. +Call it at the boundary that receives the zone or the wall-clock selector, before the value reaches anything durable. `dsh-api-session-controller` and `dsh-subagent` admit a caller-reported zone through `canonicalClientTimeZone`; `dsh-time-context` admits the zone a request turn reports through `canonicalizeTimeZone` and refuses an alias rather than rewriting it; `dsh-schedule` reads every absolute and wall-clock selector through the strict parsers and resolves its local targets through `resolveLocalInstant`. `dsh-time-context` reads its elapsed reading through `Duration` and keeps the wording of the line it injects. A consumer that owns calendar or interval arithmetic of its own keeps that arithmetic and calls these entries only for the boundary value. No runtime invariant companion is published because this pure utility owns no event stream or mutable runtime data; unit tests verify every entry. ### Entry point ```ts -import { canonicalClientTimeZone, fourDigitYearEpoch, parseLocalCalendar, resolveLocalInstant } from '@deepseek-ai/dsh-util-time' +import { Duration, canonicalClientTimeZone, fourDigitYearEpoch, parseLocalCalendar, resolveLocalInstant } from '@deepseek-ai/dsh-util-time' const zone = canonicalClientTimeZone('US/Eastern') // 'America/New_York' or undefined const stored = fourDigitYearEpoch('2026-03-08T02:30:00.000Z') // { kind: 'accepted', epoch } or a rejection reason @@ -45,9 +45,12 @@ const local = parseLocalCalendar('2026-03-08', '02:30:00') // { kind: 'accept if (local.kind === 'accepted') { resolveLocalInstant(local.time, zone ?? 'UTC') // epoch milliseconds, or undefined inside a DST gap } + +Duration.fromSeconds(90).decompose() // { days: 0, hours: 0, minutes: 1, seconds: 30, milliseconds: 0 } +Duration.fromSeconds(90).milliseconds // 90000 — the unit is in the name, and no coercion answers the count ``` -`canonicalClientTimeZone` answers `undefined` for a name it refuses, and the caller raises its own refusal — `session/invalid-time-zone` for the Session prompt, `subagent/invalid-time-zone` for a subagent continuation. `canonicalizeTimeZone`, the strict parsers, and `fourDigitYearEpoch` instead report why they refused, as a discriminated result whose `rejected` arm carries a stable reason, so each caller chooses its own code and diagnostic. `formatFourDigitYearInstant` and `resolveLocalInstant` take already-validated values and answer a string, or epoch milliseconds, with `undefined` for an unrepresentable epoch or a wall-clock value inside a DST gap. +`canonicalClientTimeZone` answers `undefined` for a name it refuses, and the caller raises its own refusal — `session/invalid-time-zone` for the Session prompt, `subagent/invalid-time-zone` for a subagent continuation. `canonicalizeTimeZone`, the strict parsers, and `fourDigitYearEpoch` instead report why they refused, as a discriminated result whose `rejected` arm carries a stable reason, so each caller chooses its own code and diagnostic. `formatFourDigitYearInstant` and `resolveLocalInstant` take already-validated values and answer a string, or epoch milliseconds, with `undefined` for an unrepresentable epoch or a wall-clock value inside a DST gap. `Duration` is a frozen value type over one millisecond count: it validates that the count is finite, adds and orders without any unit conversion, converts to seconds, minutes, hours, and days fractionally, and splits a non-negative span into whole parts. ----- @@ -71,6 +74,7 @@ The strict parsers match a fixed regular expression before constructing any date | `formatFourDigitYearInstant` | An integral epoch inside the four-digit-year range; answers `undefined` outside it or when non-integral. | | `isFourDigitYearInstant` | The same range, as a predicate, so a caller that only needs the bound does not re-derive it. | | `resolveLocalInstant` | Exact calendar fields of a real date, or an already-built `Temporal.PlainDateTime`, plus an already-canonical zone. | +| `Duration` | One finite millisecond count, built from milliseconds or seconds, and never a bare count on read. | @@ -100,7 +104,9 @@ None of its own. The consumer that injects a zone- or schedule-derived line into - **Alias resolution follows the runtime's ICU data** — which name an alias group canonicalizes to is the platform's answer, so two processes on different Node builds can disagree about it. -- **No recurrence arithmetic** — instant validity, local-calendar parsing, zone resolution, and canonical formatting only; a caller that walks candidate dates or intervals owns that search. +- **No recurrence arithmetic** — instant validity, local-calendar parsing, zone resolution, canonical formatting, and a millisecond span only; a caller that walks candidate dates or intervals owns that search. +- **`Duration` has no calendar units** — a day is exactly 24 hours and a month has no fixed length, so a caller that needs local calendar days derives them from the zone. +- **`Duration` does not convert units implicitly** — it has no `valueOf` and no `toString`, so a reader states its unit through `milliseconds` or a `to*` conversion. - **The `Temporal` polyfill is a runtime dependency** — a consumer that imports only `canonicalClientTimeZone` still resolves it at module load, because both entries share one module. diff --git a/packages/util/time/README.zh.md b/packages/util/time/README.zh.md index c1537dcbab..acf2074ffc 100644 --- a/packages/util/time/README.zh.md +++ b/packages/util/time/README.zh.md @@ -1,5 +1,5 @@ --- -description: "面向在协议边界接收调用方所报时区或墙钟选择器的维护者,说明严格的 RFC 3339 时刻、本地日历与时区解析。" +description: "面向在协议边界接收调用方所报时区、墙钟选择器或耗时的维护者,说明严格的 RFC 3339 时刻、本地日历与时区解析,以及毫秒精度的时间段类型。" kind: "package-library" --- @@ -9,7 +9,7 @@ kind: "package-library" ## 概述 -共享时间词汇,供接收调用方时区或墙钟选择器的协议边界使用。`canonicalClientTimeZone` 只接受 `UTC` 或 IANA `Area/Location` 名称,并回答当前平台上的规范拼写,因此别名不会进入持久记录。需要墙钟语义的消费方则通过带判别值的结果解析严格的 RFC 3339 时刻、本地日历字段与时区名称,再借助受维护的 `Temporal` 解析本地值,获得 DST 与重叠时段的既定行为。本库不持有失败词汇,也不生成面向模型的文本:每个调用方把拒绝原因映射成自己的域错误码。 +共享时间词汇,供接收调用方时区、墙钟选择器或耗时的边界使用。`canonicalClientTimeZone` 回答当前平台上的规范 `UTC` 或 IANA `Area/Location` 拼写,因此别名不会进入持久记录。严格解析器以带判别值的结果读取 RFC 3339 时刻、本地日历字段与时区名称,`resolveLocalInstant` 则套用受维护的 `Temporal` 的 DST 与重叠行为。`Duration` 以毫秒保存一段时间并拆成整数量。本库不持有失败词汇,也不生成面向模型的文本:每个调用方把拒绝原因映射成自己的域错误码。 ## 目录 @@ -29,14 +29,14 @@ kind: "package-library" 它是**库,不是服务也不是插件**:无 `ctx`、不注册任何东西、不持有状态。 -在接收时区或墙钟选择器的那个边界上调用它,让值在进入任何持久物之前先过一遍。`dsh-api-session-controller` 与 `dsh-subagent` 通过 `canonicalClientTimeZone` 接收调用方所报时区;`dsh-time-context` 通过 `canonicalizeTimeZone` 接收某一轮请求所报时区,并拒绝别名而不是把它改写;`dsh-schedule` 通过严格解析器读取每一个绝对选择器与墙钟选择器,并通过 `resolveLocalInstant` 解析其本地目标。自行拥有日历或间隔运算的消费方保留那部分运算,只在边界值上调用这些入口。 +在接收时区或墙钟选择器的那个边界上调用它,让值在进入任何持久物之前先过一遍。`dsh-api-session-controller` 与 `dsh-subagent` 通过 `canonicalClientTimeZone` 接收调用方所报时区;`dsh-time-context` 通过 `canonicalizeTimeZone` 接收某一轮请求所报时区,并拒绝别名而不是把它改写;`dsh-schedule` 通过严格解析器读取每一个绝对选择器与墙钟选择器,并通过 `resolveLocalInstant` 解析其本地目标。`dsh-time-context` 通过 `Duration` 读取其耗时读数,并保留它注入那一行的措辞。自行拥有日历或间隔运算的消费方保留那部分运算,只在边界值上调用这些入口。 不发布运行时不变量伴生入口;这个纯工具不拥有事件流或可变运行时数据,每个入口都由单元测试验证。 ### 入口 ```ts -import { canonicalClientTimeZone, fourDigitYearEpoch, parseLocalCalendar, resolveLocalInstant } from '@deepseek-ai/dsh-util-time' +import { Duration, canonicalClientTimeZone, fourDigitYearEpoch, parseLocalCalendar, resolveLocalInstant } from '@deepseek-ai/dsh-util-time' const zone = canonicalClientTimeZone('US/Eastern') // 'America/New_York' or undefined const stored = fourDigitYearEpoch('2026-03-08T02:30:00.000Z') // { kind: 'accepted', epoch } or a rejection reason @@ -45,9 +45,12 @@ const local = parseLocalCalendar('2026-03-08', '02:30:00') // { kind: 'accept if (local.kind === 'accepted') { resolveLocalInstant(local.time, zone ?? 'UTC') // epoch milliseconds, or undefined inside a DST gap } + +Duration.fromSeconds(90).decompose() // { days: 0, hours: 0, minutes: 1, seconds: 30, milliseconds: 0 } +Duration.fromSeconds(90).milliseconds // 90000 — the unit is in the name, and no coercion answers the count ``` -不可用的时区名称由 `canonicalClientTimeZone` 回答 `undefined`,由调用方抛出自己的拒绝——Session 提示词用 `session/invalid-time-zone`,subagent 续话用 `subagent/invalid-time-zone`。`canonicalizeTimeZone`、各严格解析器与 `fourDigitYearEpoch` 则会说明拒绝原因,以带判别值的结果回答,其 `rejected` 分支携带稳定的拒绝原因,由各调用方选择自己的错误码与诊断文案。`formatFourDigitYearInstant` 与 `resolveLocalInstant` 接收已校验的值,分别回答字符串与纪元毫秒,无法表示的纪元或落在 DST 缺口内的墙钟值回答 `undefined`。 +不可用的时区名称由 `canonicalClientTimeZone` 回答 `undefined`,由调用方抛出自己的拒绝——Session 提示词用 `session/invalid-time-zone`,subagent 续话用 `subagent/invalid-time-zone`。`canonicalizeTimeZone`、各严格解析器与 `fourDigitYearEpoch` 则会说明拒绝原因,以带判别值的结果回答,其 `rejected` 分支携带稳定的拒绝原因,由各调用方选择自己的错误码与诊断文案。`formatFourDigitYearInstant` 与 `resolveLocalInstant` 接收已校验的值,分别回答字符串与纪元毫秒,无法表示的纪元或落在 DST 缺口内的墙钟值回答 `undefined`。`Duration` 是一个基于单一毫秒计数的冻结值类型:它校验计数有限,加减与排序都不做单位换算,按小数换算到秒、分、小时与天,并把非负的时间段拆成整数量。 ----- @@ -71,6 +74,7 @@ if (local.kind === 'accepted') { | `formatFourDigitYearInstant` | 四位年份范围内的整数纪元毫秒;超出范围或非整数时回答 `undefined`。 | | `isFourDigitYearInstant` | 同一范围的谓词形式,只判断范围边界的调用方无需自行推导。 | | `resolveLocalInstant` | 真实日期的精确日历字段,或已构造好的 `Temporal.PlainDateTime`,外加已规范化的时区。 | +| `Duration` | 一个有限的毫秒计数,可由毫秒或秒构造,读取时从不交出裸计数。 | @@ -100,7 +104,9 @@ if (local.kind === 'accepted') { - **别名解析取决于运行时的 ICU 数据**——一个别名组规范化成哪个名称由平台回答,因此两个跑在不同 Node 构建上的进程可能给出不同答案。 -- **不含重复规则运算**——只做时刻有效性、本地日历解析、时区解析与规范格式化;遍历候选日期或间隔的搜索由调用方自己拥有。 +- **不含重复规则运算**——只做时刻有效性、本地日历解析、时区解析、规范格式化与毫秒精度的时间段;遍历候选日期或间隔的搜索由调用方自己拥有。 +- **`Duration` 没有日历单位**——一天恰好是 24 小时,月没有固定长度;需要本地日历天的调用方从时区推导。 +- **`Duration` 不做隐式单位换算**——它没有 `valueOf` 也没有 `toString`,读取方必须通过 `milliseconds` 或某个 `to*` 换算声明单位。 - **`Temporal` polyfill 是运行时依赖**——只导入 `canonicalClientTimeZone` 的消费方在模块加载时同样会解析它,因为两个入口共用一个模块。 diff --git a/packages/util/time/package.json b/packages/util/time/package.json index 70e234df7f..b18fc42f3e 100644 --- a/packages/util/time/package.json +++ b/packages/util/time/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-util-time", - "description": "Time vocabulary shared by wire boundaries: canonicalClientTimeZone (IANA zone validation and canonicalization), and strict RFC 3339 instant, local-calendar, and zone resolution for wall-clock selectors", + "description": "Time vocabulary shared by wire boundaries: canonicalClientTimeZone (IANA zone validation and canonicalization), strict RFC 3339 instant, local-calendar, and zone resolution for wall-clock selectors, and a millisecond Duration span", "version": "0.1.7-rc.2", "publishConfig": { "access": "public" diff --git a/packages/util/time/src/duration.ts b/packages/util/time/src/duration.ts new file mode 100644 index 0000000000..727d6743fd --- /dev/null +++ b/packages/util/time/src/duration.ts @@ -0,0 +1,184 @@ +/** + * One span of time, held as a count of milliseconds. + * + * A class rather than a branded number: arithmetic on two branded numbers + * answers an unbranded `number`, so a brand cannot stop seconds and + * milliseconds from being added, while an instance can. There is no `valueOf` + * and no `toString`, so no reader ever receives a bare count whose unit it has + * to remember; a caller states the unit by reading `milliseconds` or by + * converting through one of the `to*` entries. + * @module @deepseek-ai/dsh-util-time + */ + +const MS_PER_SECOND = 1_000 +const MS_PER_MINUTE = 60 * MS_PER_SECOND +const MS_PER_HOUR = 60 * MS_PER_MINUTE +const MS_PER_DAY = 24 * MS_PER_HOUR +const SECONDS_PER_DAY = 86_400 +const SECONDS_PER_HOUR = 3_600 +const SECONDS_PER_MINUTE = 60 + +/** Whole parts of one non-negative span, in the units a reading is written in. */ +export interface DurationParts { + /** Whole days. */ + readonly days: number + /** Whole hours left after the days, from 0 through 23. */ + readonly hours: number + /** Whole minutes left after the hours, from 0 through 59. */ + readonly minutes: number + /** Whole seconds left after the minutes, from 0 through 59. */ + readonly seconds: number + /** Fractional milliseconds left after the seconds, from 0 up to (but excluding) 1000. */ + readonly milliseconds: number +} + +/** + * One span of time in milliseconds. + * + * Instances are frozen, so a span cannot drift after it is built, and they carry + * no calendar meaning: a month is not a fixed count of milliseconds, so no entry + * here converts to or from calendar units. + */ +export class Duration { + /** The additive identity. */ + static readonly zero: Duration = new Duration(0) + + private readonly span: number + + private constructor(milliseconds: number) { + this.span = milliseconds + Object.freeze(this) + } + + /** + * Build one span from a millisecond count. + * @param milliseconds - Finite millisecond count; a fractional count is kept as given. + * @returns The span. + * @throws TypeError when the count is not a finite number. + */ + static fromMilliseconds(milliseconds: number): Duration { + if (!Number.isFinite(milliseconds)) { + throw new TypeError(`duration milliseconds must be finite, got ${String(milliseconds)}`) + } + return new Duration(milliseconds) + } + + /** + * Build one span from a second count, for a boundary whose unit is seconds. + * @param seconds - Finite second count; a fractional count is kept as given. + * @returns The span. + * @throws TypeError when the count is not finite, or its millisecond equivalent is not. + */ + static fromSeconds(seconds: number): Duration { + if (!Number.isFinite(seconds)) { + throw new TypeError(`duration seconds must be finite, got ${String(seconds)}`) + } + return Duration.fromMilliseconds(seconds * MS_PER_SECOND) + } + + /** The span as a millisecond count; the unit is part of the name. */ + get milliseconds(): number { + return this.span + } + + /** + * Add one span to this span. + * @param other - Span to add; both operands already share the millisecond unit. + * @returns The sum. + */ + plus(other: Duration): Duration { + return new Duration(this.span + other.span) + } + + /** + * Subtract one span from this span. + * @param other - Span to subtract. + * @returns The difference, which is negative when `other` is longer. + */ + minus(other: Duration): Duration { + return new Duration(this.span - other.span) + } + + /** + * Order two spans. + * @param other - Span to compare with. + * @returns `-1` when this span is shorter, `0` when equal, `1` when longer. + */ + compare(other: Duration): -1 | 0 | 1 { + if (this.span < other.span) return -1 + return this.span > other.span ? 1 : 0 + } + + /** + * Whether the span is a negative interval, which every sign-sensitive reader + * must decide how to handle. + * @returns Whether the millisecond count is below zero. + */ + isNegative(): boolean { + return this.span < 0 + } + + /** + * The span in seconds. + * @returns The span as a fractional second count, lossless in both directions. + */ + toSeconds(): number { + return this.span / MS_PER_SECOND + } + + /** + * The span in minutes. + * @returns The span as a fractional minute count, lossless in both directions. + */ + toMinutes(): number { + return this.span / MS_PER_MINUTE + } + + /** + * The span in hours. + * @returns The span as a fractional hour count, lossless in both directions. + */ + toHours(): number { + return this.span / MS_PER_HOUR + } + + /** + * The span in days. + * + * A day here is exactly 24 hours. A wall-clock reading that crosses a + * daylight-saving transition is not a whole number of days, so a caller that + * needs local calendar days derives them from the zone instead. + * @returns The span as a fractional day count, lossless in both directions. + */ + toDays(): number { + return this.span / MS_PER_DAY + } + + /** + * Split the span into whole days, hours, minutes, seconds, and the remaining + * fraction of a second. Each part is floored, so the parts never exceed the + * span. + * @returns The frozen whole parts. + * @throws RangeError when the span is negative, because a negative interval + * has no single whole-part decomposition: the caller owns that policy. + */ + decompose(): DurationParts { + if (this.span < 0) { + throw new RangeError('a negative duration has no whole-part decomposition') + } + let seconds = Math.floor(this.span / MS_PER_SECOND) + const days = Math.floor(seconds / SECONDS_PER_DAY) + seconds %= SECONDS_PER_DAY + const hours = Math.floor(seconds / SECONDS_PER_HOUR) + seconds %= SECONDS_PER_HOUR + const minutes = Math.floor(seconds / SECONDS_PER_MINUTE) + seconds %= SECONDS_PER_MINUTE + return Object.freeze({ + days, + hours, + minutes, + seconds, + milliseconds: this.span - Math.floor(this.span / MS_PER_SECOND) * MS_PER_SECOND, + }) + } +} diff --git a/packages/util/time/src/index.ts b/packages/util/time/src/index.ts index 712ccc81bc..bbbd316134 100644 --- a/packages/util/time/src/index.ts +++ b/packages/util/time/src/index.ts @@ -1,6 +1,6 @@ /** * Time vocabulary shared by the boundaries that accept a caller's zone or a - * wall-clock selector. + * wall-clock selector, plus one span type for elapsed and interval arithmetic. * * Validation, canonicalization, and strict parsing only: this library formats * no model-facing text and owns no failure vocabulary. A refusal that carries a @@ -8,12 +8,14 @@ * own domain code and diagnostic. `canonicalClientTimeZone` is the wire-boundary * form and answers a name or `undefined`; `formatFourDigitYearInstant` and * `resolveLocalInstant` take already-validated values and answer a string or - * epoch milliseconds directly. + * epoch milliseconds directly; `Duration` holds a span in milliseconds with no + * implicit numeric conversion. * @module @deepseek-ai/dsh-util-time */ import { canonicalizeTimeZone } from './instant.ts' +export * from './duration.ts' export * from './instant.ts' /** diff --git a/packages/util/time/tests/duration.spec.ts b/packages/util/time/tests/duration.spec.ts new file mode 100644 index 0000000000..866e1e1669 --- /dev/null +++ b/packages/util/time/tests/duration.spec.ts @@ -0,0 +1,102 @@ +import { describe, expect, it } from 'vitest' +import { Duration } from '@deepseek-ai/dsh-util-time' + +describe('Duration construction', () => { + it('holds the millisecond count it was built from, fraction included', () => { + expect(Duration.fromMilliseconds(1_500).milliseconds).toBe(1_500) + expect(Duration.fromMilliseconds(0.5).milliseconds).toBe(0.5) + expect(Duration.zero.milliseconds).toBe(0) + }) + + it('converts a second count at the boundary that carries seconds', () => { + expect(Duration.fromSeconds(300).milliseconds).toBe(300_000) + expect(Duration.fromSeconds(0.25).milliseconds).toBe(250) + expect(Duration.fromSeconds(-60).milliseconds).toBe(-60_000) + }) + + it('refuses a count that is not a finite number', () => { + for (const value of [Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]) { + expect(() => Duration.fromMilliseconds(value)).toThrow(TypeError) + expect(() => Duration.fromSeconds(value)).toThrow(TypeError) + } + }) + + it('refuses a finite second count whose millisecond equivalent leaves the finite range', () => { + expect(() => Duration.fromSeconds(1e306)).toThrow(TypeError) + }) + + it('is frozen, and carries no implicit numeric conversion', () => { + const span = Duration.fromMilliseconds(1) + expect(Object.isFrozen(span)).toBe(true) + expect(Reflect.set(span, 'span', 2)).toBe(false) + expect(span.milliseconds).toBe(1) + // Without a `valueOf` or `toString` override, a coercion never answers the count. + expect(Number(span)).toBe(Number.NaN) + expect(String(span)).toBe('[object Object]') + }) +}) + +describe('Duration arithmetic', () => { + it('adds and subtracts in the one shared unit', () => { + expect(Duration.fromSeconds(2).plus(Duration.fromMilliseconds(500)).milliseconds).toBe(2_500) + expect(Duration.fromSeconds(2).minus(Duration.fromMilliseconds(500)).milliseconds).toBe(1_500) + expect(Duration.fromMilliseconds(1).minus(Duration.fromSeconds(1)).isNegative()).toBe(true) + expect(Duration.zero.plus(Duration.zero).milliseconds).toBe(0) + }) + + it('orders spans without surrendering the unit', () => { + const minute = Duration.fromSeconds(60) + const hour = Duration.fromSeconds(3_600) + expect(minute.compare(hour)).toBe(-1) + expect(hour.compare(minute)).toBe(1) + expect(minute.compare(Duration.fromMilliseconds(60_000))).toBe(0) + expect(minute.isNegative()).toBe(false) + expect(Duration.fromMilliseconds(-1).isNegative()).toBe(true) + }) +}) + +describe('Duration conversion', () => { + it('converts to larger units fractionally, so nothing is lost either way', () => { + const span = Duration.fromMilliseconds(90_061_500) + expect(span.toSeconds()).toBe(90_061.5) + expect(span.toMinutes()).toBe(1_501.025) + expect(span.toHours()).toBe(25.017_083_333_333_332) + expect(span.toDays()).toBe(1.042_378_472_222_222_2) + expect(Duration.fromSeconds(span.toSeconds()).milliseconds).toBe(span.milliseconds) + }) + + it('covers a day as exactly twenty-four hours', () => { + expect(Duration.fromMilliseconds(86_400_000).toDays()).toBe(1) + expect(Duration.fromMilliseconds(-86_400_000).toDays()).toBe(-1) + }) +}) + +describe('Duration decompose', () => { + it('splits a span into floored whole parts and the fractional remainder', () => { + expect(Duration.fromMilliseconds(0).decompose()) + .toEqual({ days: 0, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }) + expect(Duration.fromMilliseconds(999).decompose()) + .toEqual({ days: 0, hours: 0, minutes: 0, seconds: 0, milliseconds: 999 }) + expect(Duration.fromMilliseconds(1_000).decompose()) + .toEqual({ days: 0, hours: 0, minutes: 0, seconds: 1, milliseconds: 0 }) + expect(Duration.fromMilliseconds(90_061_500).decompose()) + .toEqual({ days: 1, hours: 1, minutes: 1, seconds: 1, milliseconds: 500 }) + expect(Duration.fromMilliseconds(86_400_000).decompose()) + .toEqual({ days: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }) + expect(Duration.fromMilliseconds(90_061_500.75).decompose()) + .toEqual({ days: 1, hours: 1, minutes: 1, seconds: 1, milliseconds: 500.75 }) + }) + + it('answers frozen parts whose sum never exceeds the span', () => { + const parts = Duration.fromMilliseconds(3_661_999).decompose() + expect(Object.isFrozen(parts)).toBe(true) + const summed = parts.days * 86_400_000 + parts.hours * 3_600_000 + + parts.minutes * 60_000 + parts.seconds * 1_000 + parts.milliseconds + expect(summed).toBe(3_661_999) + expect(summed).toBeLessThanOrEqual(3_661_999) + }) + + it('refuses a negative span instead of inventing a sign policy', () => { + expect(() => Duration.fromMilliseconds(-1).decompose()).toThrow(RangeError) + }) +}) From 994b9e07bffa64425ae29109bd33a346000d754f Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Fri, 25 Sep 2026 14:55:58 +0800 Subject: [PATCH 014/369] fix: satisfy the lint and generated-catalog gates - the immutability assertion checked coercion overrides instead of stringifying the instance, which no-base-to-string rejects - the config catalog is regenerated for the package description --- docs/config-catalog.md | 2 +- docs/config-catalog.zh.md | 2 +- packages/util/time/tests/duration.spec.ts | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 7978a4afaf..da3bd03763 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -3380,7 +3380,7 @@ export type ShellDialect = 'bash' | 'pwsh' ## `@deepseek-ai/dsh-time-context` - `inject`: `agents` · `sessionProjections` -- `source`: [`packages/context/time-context/src/index.ts:56`](../packages/context/time-context/src/index.ts) +- `source`: [`packages/context/time-context/src/index.ts:57`](../packages/context/time-context/src/index.ts) ```ts config-catalog /** Request-preparation clock formatting and append scheduling. Invalid values fail plugin load. */ diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 12383e538d..ddf0cb6456 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -3382,7 +3382,7 @@ export type ShellDialect = 'bash' | 'pwsh' ## `@deepseek-ai/dsh-time-context` - `inject`: `agents` · `sessionProjections` -- `source`: [`packages/context/time-context/src/index.ts:56`](../packages/context/time-context/src/index.ts) +- `source`: [`packages/context/time-context/src/index.ts:57`](../packages/context/time-context/src/index.ts) ```ts config-catalog /** Request-preparation clock formatting and append scheduling. Invalid values fail plugin load. */ diff --git a/packages/util/time/tests/duration.spec.ts b/packages/util/time/tests/duration.spec.ts index 866e1e1669..1a90124337 100644 --- a/packages/util/time/tests/duration.spec.ts +++ b/packages/util/time/tests/duration.spec.ts @@ -25,14 +25,14 @@ describe('Duration construction', () => { expect(() => Duration.fromSeconds(1e306)).toThrow(TypeError) }) - it('is frozen, and carries no implicit numeric conversion', () => { + it('is frozen, and overrides no implicit conversion', () => { const span = Duration.fromMilliseconds(1) expect(Object.isFrozen(span)).toBe(true) expect(Reflect.set(span, 'span', 2)).toBe(false) expect(span.milliseconds).toBe(1) - // Without a `valueOf` or `toString` override, a coercion never answers the count. - expect(Number(span)).toBe(Number.NaN) - expect(String(span)).toBe('[object Object]') + // Neither coercion is overridden, so no caller obtains the count by coercion. + expect(Object.hasOwn(Duration.prototype, 'valueOf')).toBe(false) + expect(Object.hasOwn(Duration.prototype, 'toString')).toBe(false) }) }) From 0b5803cf0c03f6afac41a2d598a5f56f5a548af9 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Fri, 25 Sep 2026 15:14:50 +0800 Subject: [PATCH 015/369] fix(util-time): bound a span, and state the conversions as they are - Duration rejects a count outside the exactly representable millisecond range, which is what makes decompose's whole-part identity exact; the postcondition is asserted over a sweep and a random sample - the to* JSDoc no longer claims a round trip restores the count: each is one division, so it answers the nearest double - a test rules out a Symbol.toPrimitive coercion hook, the README names both implementation modules, and the note records that the value type ships its full operation set before its second consumer exists --- ...026-09-24-shared-time-vocabulary.i18n.yaml | 8 ++-- .../2026-09-24-shared-time-vocabulary.md | 8 ++-- .../2026-09-24-shared-time-vocabulary.zh.md | 8 ++-- packages/util/time/README.i18n.yaml | 12 +++--- packages/util/time/README.md | 7 ++-- packages/util/time/README.zh.md | 7 ++-- packages/util/time/src/duration.ts | 39 +++++++++++++------ packages/util/time/tests/duration.spec.ts | 37 ++++++++++++++++-- 8 files changed, 90 insertions(+), 36 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml index 31c914f4ff..dcfe0eeb4e 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml @@ -9,11 +9,11 @@ en: abd0263d14861926 zh: e1736cd08e87f795 /agent-note-shared-time-vocabulary-for-wall-clock-selectors/decision: - en: 34feee585cb33001 - zh: d882bbba56a679bd + en: 4130cbbd30e06857 + zh: 85f9153cf5ad44ce /agent-note-shared-time-vocabulary-for-wall-clock-selectors/alternatives-considered: en: 1737484c099fab52 zh: 303b144c52d167bd /agent-note-shared-time-vocabulary-for-wall-clock-selectors/consequences: - en: 7e3004318640cca7 - zh: 007d7fe761718d37 + en: f7181f87764118d2 + zh: 0af236ff4b6a935b diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md index 46dfbe6504..d9fc530938 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md @@ -10,7 +10,7 @@ English | [中文](2026-09-24-shared-time-vocabulary.zh.md) ## Decision -`dsh-util-time` owns the time vocabulary that boundaries accept. `packages/util/time/src/instant.ts` holds it, `src/index.ts` re-exports it and keeps `canonicalClientTimeZone` as the `string | undefined` wrapper the existing wire boundaries consume. +`dsh-util-time` owns the time vocabulary that boundaries accept. `packages/util/time/src/instant.ts` holds the parsers, `src/duration.ts` holds the span type, and `src/index.ts` re-exports both and keeps `canonicalClientTimeZone` as the `string | undefined` wrapper the existing wire boundaries consume. The library answers discriminated results and carries no failure vocabulary. `parseOffsetInstant`, `fourDigitYearEpoch`, `parseLocalCalendar`, and `canonicalizeTimeZone` answer an `accepted` arm or a `rejected` arm naming a stable reason (`not_rfc3339`, `out_of_range`, `not_a_calendar_date`, `invalid_numeric_offset`, `malformed`, `blank`, `padded`, `not_area_location`, `unsupported`, `not_canonical_shape`). Each caller maps a reason onto its own code and diagnostic, so the library never writes model-facing text. `formatFourDigitYearInstant` returns the canonical instant text or `undefined` outside the bound, `isFourDigitYearInstant` answers whether an epoch is representable at all, and `resolveLocalInstant` answers epoch milliseconds or `undefined` for a wall-clock value inside a forward transition's gap. The reason vocabulary is parallel between the two strict parsers: the same condition is `out_of_range` in both, and `LocalCalendarTime.year` states which of them rejects year 0 (`parseLocalCalendar`) and which accepts it (`resolveLocalInstant`, whose callers derive local dates at the UTC edges). @@ -18,7 +18,7 @@ Schedule consumes it and keeps every domain decision. `canonicalizeTimeZone`, `d `dsh-time-context` consumes it too and keeps its stricter policy. `deriveBrowserTimeZoneContext` reads each user-rpc zone through `canonicalizeTimeZone` and refuses an alias whose canonical answer differs from the reported value, so the request boundary still requires an identity another process resolves back to the same name. Time-context owns its three refusals and their messages; the shared entry only answers the canonical name or a reason, and its `unsupported` rejection carries the platform error the library caught, so that refusal keeps its `cause` in both consumers. -`Duration` holds one span as a single millisecond count, and it is a class rather than a branded number because TypeScript's arithmetic erases a brand: adding two differently branded numbers answers an unbranded `number`, so a brand cannot stop seconds and milliseconds from being mixed, while an instance can. The unit is milliseconds because every producer and consumer in this repository already uses it (`Date.now()`, `SessionEvent.time`, the session log, the client), and because a double holds every millisecond of the whole `Date` range exactly, so no precision is spent on the unit. Instances are frozen; the count is readable only as `milliseconds` or through a fractional `toSeconds`/`toMinutes`/`toHours`/`toDays` conversion, and there is deliberately no `valueOf` and no `toString`, so no reader ever receives a bare count whose unit it has to remember. Addition, subtraction, ordering, and a whole-part `decompose()` are the whole operation set. +`Duration` holds one span as a single millisecond count, and it is a class rather than a branded number because TypeScript's arithmetic erases a brand: adding two differently branded numbers answers an unbranded `number`, so a brand cannot stop seconds and milliseconds from being mixed, while an instance can. The unit is milliseconds because every producer and consumer in this repository already uses it (`Date.now()`, `SessionEvent.time`, the session log, the client), and because a double holds every millisecond of the whole `Date` range exactly, so no precision is spent on the unit. Instances are frozen; the count is readable only as `milliseconds` or through a fractional `toSeconds`/`toMinutes`/`toHours`/`toDays` conversion, and there is deliberately no `valueOf` and no `toString`, so no reader ever receives a bare count whose unit it has to remember. Addition, subtraction, ordering, a negative-span test, and a whole-part `decompose()` are the whole operation set, alongside a `zero` identity and a `fromSeconds` constructor for a boundary whose unit is seconds. The maintained `@js-temporal/polyfill` 0.5.1 becomes a runtime dependency of `dsh-util-time` as well as of `dsh-schedule`. Local resolution continues to use it, so the Host's IANA data still decides gaps and overlaps and no caller owns offset sampling. `util/time`'s README and description lose the `zero-dependency` claim the added dependency contradicts. @@ -44,6 +44,8 @@ The time profile now has one home. A change to the accepted instant profile or t The cost is a wider published surface and a heavier dependency graph: `dsh-util-time` gains a runtime dependency on `@js-temporal/polyfill`, so a consumer that imports only `canonicalClientTimeZone` now loads that module too. The library is also no longer a pure "validation and canonicalization only" package, which its README and package description had to state and which its Known Limitations section now records. Schedule keeps a thin mapping layer at each entry. The local-calendar and instant mappers switch over the library's closed reason unions with a `never` default, so a newly added reason fails the build there instead of silently taking a catch-all. The zone mapper instead branches only on `not_canonical_shape` — a reason the current IANA data never produces, so it is guarded by a `v8 ignore` rather than a `never` default — and folds `blank`, `padded`, `not_area_location`, and `unsupported` into one diagnostic. A reason added to `TimeZoneRejection` later would therefore compile there and silently take that generic message. -The elapsed reading keeps its split in the library and its wording in the consumer: `dsh-time-context` still decides that a backward clock reading clamps to zero and how the compact line reads, while `Duration.decompose()` answers the whole parts. That swap is byte-identical for every finite sample, verified by a differential over more than eight thousand inputs including negatives, fractions, and values past a year. One input does change: a non-finite elapsed sample now throws from the span constructor instead of rendering `NaNs`, which the package's own reading invariant already refused, so the failure moves to the sample that produced it. +The value type ships its whole operation set before its second consumer exists. Only `fromMilliseconds` and `decompose()` are called in production today, by time-context; `fromSeconds`, `milliseconds`, `plus`, `minus`, `compare`, `isNegative`, `zero`, and the four `to*` conversions are published without a caller. That is a deliberate choice rather than an oversight: the seconds-valued fields at `dsh-schedule` and the DeepSeek file store are the intended next consumers, and the alternative — a class that holds a span but can only split it — would have to grow the same members one call site at a time. The package README records the un-migrated boundaries as a known limitation. + +The elapsed reading keeps its split in the library and its wording in the consumer: `dsh-time-context` still decides that a backward clock reading clamps to zero and how the compact line reads, while `Duration.decompose()` answers the whole parts. That swap is byte-identical for every finite sample, measured with a differential over more than eight thousand inputs including negatives, fractions, and values past a year; that differential was a review probe, not a committed test, and the consumer's own suite pins the readings it renders (`1d 1h 1m 1s`, `1m 1s`, `4m 2s`, and the clamped zero). One input does change: a non-finite elapsed sample now throws from the span constructor instead of rendering `NaNs`, which the package's own reading invariant already refused, so the failure moves to the sample that produced it. Time-context keeps the two policies this change did not touch: its ISO-shaped timestamp rendering in `packages/context/time-context/src/timestamp.ts`, which is that package's durable reading format, and the elapsed-duration wording in its own plugin. The browser client keeps its own localized date rendering and its own UTC-offset reading, which serve a different purpose (a reader's locale) from a boundary value; unifying those means deciding whether the shared library should also own display formatting, and which package then owns the durable reading format its invariant validates. `packages/session/session-format-v0-to-v1/src/payload-validation.ts` also validates the canonical four-digit-year UTC instant with its own regular expression and round trip, which is a remaining copy of the instant profile this change did not touch. diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md index 8ff750c478..ff8368f558 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md @@ -10,7 +10,7 @@ Status: implemented ## Decision -`dsh-util-time` 拥有各边界所接收的时间词汇。它位于 `packages/util/time/src/instant.ts`,`src/index.ts` 转出它,并保留 `canonicalClientTimeZone` 作为既有协议边界消费的 `string | undefined` 包装。 +`dsh-util-time` 拥有各边界所接收的时间词汇。解析器位于 `packages/util/time/src/instant.ts`,时间段类型位于 `src/duration.ts`,`src/index.ts` 转出两者,并保留 `canonicalClientTimeZone` 作为既有协议边界消费的 `string | undefined` 包装。 本库回答带判别值的结果,不持有失败词汇。`parseOffsetInstant`、`fourDigitYearEpoch`、`parseLocalCalendar` 与 `canonicalizeTimeZone` 回答 `accepted` 分支,或回答携带稳定原因的 `rejected` 分支(`not_rfc3339`、`out_of_range`、`not_a_calendar_date`、`invalid_numeric_offset`、`malformed`、`blank`、`padded`、`not_area_location`、`unsupported`、`not_canonical_shape`)。每个调用方把原因映射成自己的错误码与诊断文案,因此本库从不撰写面向模型的文本。`formatFourDigitYearInstant` 返回规范时刻文本,越界时返回 `undefined`;`isFourDigitYearInstant` 回答某个纪元是否可表示;`resolveLocalInstant` 回答纪元毫秒,对落在前向跳变缺口内的墙钟值回答 `undefined`。两个严格解析器的原因词汇彼此对称:同一条件在两者中都叫 `out_of_range`,而 `LocalCalendarTime.year` 说明了二者中谁拒绝年份 0(`parseLocalCalendar`)、谁接受它(`resolveLocalInstant`,其调用方需要在 UTC 边界推导本地日期)。 @@ -18,7 +18,7 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec `dsh-time-context` 同样消费它,并保留自己更严格的策略。`deriveBrowserTimeZoneContext` 通过 `canonicalizeTimeZone` 读取每条 user-rpc 时区,并拒绝规范答案与所报值不同的别名,因此请求边界仍然要求另一个进程能解析回同一名称。三条拒绝与它们的文案归 time-context 所有;共享入口只回答规范名称或一个原因;被拒绝的结果中只有 `unsupported` 会携带本库捕获到的平台错误,使该拒绝在两个消费方都保留其 `cause`。 -`Duration` 以单一毫秒计数保存一段时间;它做成类而不是 branded number,是因为 TypeScript 的算术会抹掉 brand:两个不同品牌的数字相加得到的是无品牌的 `number`,所以 brand 挡不住秒与毫秒的混用,而实例可以。单位选毫秒,是因为本仓库的每个生产者与消费者本来就用它(`Date.now()`、`SessionEvent.time`、Session 日志、客户端),也因为 double 能精确表示 `Date` 全范围内的每一个毫秒,这个单位不消耗任何精度。实例被冻结;计数只能通过 `milliseconds` 或按小数换算的 `toSeconds`/`toMinutes`/`toHours`/`toDays` 读取,且刻意不提供 `valueOf` 与 `toString`,因此读取方永远不会拿到一个需要自己记单位的裸数字。加、减、排序与整数量拆解 `decompose()` 就是全部操作集合。 +`Duration` 以单一毫秒计数保存一段时间;它做成类而不是 branded number,是因为 TypeScript 的算术会抹掉 brand:两个不同品牌的数字相加得到的是无品牌的 `number`,所以 brand 挡不住秒与毫秒的混用,而实例可以。单位选毫秒,是因为本仓库的每个生产者与消费者本来就用它(`Date.now()`、`SessionEvent.time`、Session 日志、客户端),也因为 double 能精确表示 `Date` 全范围内的每一个毫秒,这个单位不消耗任何精度。实例被冻结;计数只能通过 `milliseconds` 或按小数换算的 `toSeconds`/`toMinutes`/`toHours`/`toDays` 读取,且刻意不提供 `valueOf` 与 `toString`,因此读取方永远不会拿到一个需要自己记单位的裸数字。加、减、排序、负值判断与整数量拆解 `decompose()` 就是全部操作集合,另有 `zero` 恒等元与面向以秒为单位边界的 `fromSeconds` 构造入口。 受维护的 `@js-temporal/polyfill` 0.5.1 在成为 `dsh-schedule` 运行时依赖之外,也成为 `dsh-util-time` 的运行时依赖。本地解析继续使用它,因此缺口与重叠仍由宿主 IANA 数据决定,也没有调用方需要自己实现偏移采样。`util/time` 的 README 与包描述删去了新增依赖所否定的 `zero-dependency` 表述。 @@ -44,6 +44,8 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec 代价是更宽的已发布接口与更重的依赖图:`dsh-util-time` 增加了对 `@js-temporal/polyfill` 的运行时依赖,因此只导入 `canonicalClientTimeZone` 的消费方也会加载该模块。本库也不再是纯"只做校验与规范化"的包——其 README 与包描述必须说明这一点,其"已知限制"一节现在也记录了它。Schedule 在每个入口保留一层薄映射。本地日历与时刻映射函数以 `never` 默认分支穷尽 switch 本库的封闭原因联合,因此新增原因会让这两个调用点构建失败,而不是静默走兜底分支。时区映射函数则只对 `not_canonical_shape` 分支——当前 IANA 数据不会产生该原因,因此它由 `v8 ignore` 守护而不是 `never` 默认分支——并把 `blank`、`padded`、`not_area_location` 与 `unsupported` 合并成一条诊断。因此日后给 `TimeZoneRejection` 新增原因时会通过编译并静默套用那条通用文案。 -耗时读数的拆分上收到库、措辞留在一侧:`dsh-time-context` 仍然决定回退的时钟读数钳到零、以及那行紧凑文本怎么写,而 `Duration.decompose()` 回答整数量。这次替换对每个有限样本都逐字不变,已用一份覆盖八千多个输入(含负数、小数与一年以上的值)的差分验证。有一个输入会变:非有限的耗时样本现在由时间段构造函数抛出,而不是渲染出 `NaNs`——而那本来就被该包自己的读数不变量拒绝,所以失败点前移到了产生它的那次采样。 +该值类型在第二个消费方出现之前就交付了完整操作集合。目前生产代码只调用 `fromMilliseconds` 与 `decompose()`(由 time-context 调用);`fromSeconds`、`milliseconds`、`plus`、`minus`、`compare`、`isNegative`、`zero` 与四个 `to*` 换算都没有调用方。这是有意的选择而不是疏漏:`dsh-schedule` 与 DeepSeek 文件存储里以秒为单位的字段是预期的下一批消费方,而另一种做法——一个能保存时间段却只能拆解它的类——会在每个调用点出现时再长出同样的成员。包 README 把这些尚未迁移的边界记为已知限制。 + +耗时读数的拆分上收到库、措辞留在一侧:`dsh-time-context` 仍然决定回退的时钟读数钳到零、以及那行紧凑文本怎么写,而 `Duration.decompose()` 回答整数量。这次替换对每个有限样本都逐字不变,用一份覆盖八千多个输入(含负数、小数与一年以上的值)的差分测得;该差分是评审用的探针而非提交进仓库的测试,消费方自己那套测试固定了它渲染出的读数(`1d 1h 1m 1s`、`1m 1s`、`4m 2s`,以及被钳到零的那次)。有一个输入会变:非有限的耗时样本现在由时间段构造函数抛出,而不是渲染出 `NaNs`——而那本来就被该包自己的读数不变量拒绝,所以失败点前移到了产生它的那次采样。 本次未触及、仍归 time-context 的两项:`packages/context/time-context/src/timestamp.ts` 中的 ISO 形状时间戳渲染(那是该包的持久读数字格式),以及其插件内的耗时措辞。浏览器客户端也保留自己的本地化日期渲染与自己的 UTC 偏移读取——它们服务的目的(读者的区域设置)与边界值不同;统一它们意味着先决定共享库是否也要拥有展示格式化,以及届时由谁拥有其不变量所校验的持久读数字格式。`packages/session/session-format-v0-to-v1/src/payload-validation.ts` 也用自己的一段正则与往返比对校验规范的四位年份 UTC 时刻,这是本次未触及、仍然存在的时刻剖面副本。 diff --git a/packages/util/time/README.i18n.yaml b/packages/util/time/README.i18n.yaml index c0d042b47a..8559bd7e18 100644 --- a/packages/util/time/README.i18n.yaml +++ b/packages/util/time/README.i18n.yaml @@ -21,11 +21,11 @@ en: 7015b3ed33de4c53 zh: d4fc9aee6f6910ca /dsh-util-time/use-this-package/entry-point: - en: 306f0f5d65af8223 - zh: a79b60c39be361c8 + en: 88668c3dd1bdb75b + zh: fb48d941ae433335 /dsh-util-time/understand-the-implementation: - en: f42cb0fc332d7740 - zh: f980bcae2f72fdd8 + en: ef1369f1e986acf5 + zh: b7a98e3d79048d50 /dsh-util-time/further-exploration: en: 8aa95ebdfd1f323e zh: c6879eac9fa6e087 @@ -36,8 +36,8 @@ en: daf6e45a18c087b0 zh: e6714f1e65ed55ce /dsh-util-time/known-limitations-and-deferred-work: - en: b4108682b9b18245 - zh: 7dfb014c2dc1775f + en: 20e184351878f41b + zh: 1c94d8a715df7c1b /dsh-util-time/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: e88aeb617ffb74ea diff --git a/packages/util/time/README.md b/packages/util/time/README.md index 7bb1e068ae..4f1d2ee9ea 100644 --- a/packages/util/time/README.md +++ b/packages/util/time/README.md @@ -50,7 +50,7 @@ Duration.fromSeconds(90).decompose() // { days: 0, hours: 0, minutes: 1, seconds Duration.fromSeconds(90).milliseconds // 90000 — the unit is in the name, and no coercion answers the count ``` -`canonicalClientTimeZone` answers `undefined` for a name it refuses, and the caller raises its own refusal — `session/invalid-time-zone` for the Session prompt, `subagent/invalid-time-zone` for a subagent continuation. `canonicalizeTimeZone`, the strict parsers, and `fourDigitYearEpoch` instead report why they refused, as a discriminated result whose `rejected` arm carries a stable reason, so each caller chooses its own code and diagnostic. `formatFourDigitYearInstant` and `resolveLocalInstant` take already-validated values and answer a string, or epoch milliseconds, with `undefined` for an unrepresentable epoch or a wall-clock value inside a DST gap. `Duration` is a frozen value type over one millisecond count: it validates that the count is finite, adds and orders without any unit conversion, converts to seconds, minutes, hours, and days fractionally, and splits a non-negative span into whole parts. +`canonicalClientTimeZone` answers `undefined` for a name it refuses, and the caller raises its own refusal — `session/invalid-time-zone` for the Session prompt, `subagent/invalid-time-zone` for a subagent continuation. `canonicalizeTimeZone`, the strict parsers, and `fourDigitYearEpoch` instead report why they refused, as a discriminated result whose `rejected` arm carries a stable reason, so each caller chooses its own code and diagnostic. `formatFourDigitYearInstant` and `resolveLocalInstant` take already-validated values and answer a string, or epoch milliseconds, with `undefined` for an unrepresentable epoch or a wall-clock value inside a DST gap. `Duration` is a frozen value type over one millisecond count: it validates that the count is finite and inside the exactly representable range, adds, subtracts, orders, and reports a negative span without any unit conversion, converts to seconds, minutes, hours, and days fractionally, and splits a non-negative span into whole parts. ----- @@ -60,7 +60,7 @@ Duration.fromSeconds(90).milliseconds // 90000 — the unit is in the name, and
Implementation internals — click to expand -`src/instant.ts` holds the whole implementation; `src/index.ts` re-exports it and adds `canonicalClientTimeZone`, the `string | undefined` form the wire boundaries already consume. The module JSDoc there states each entry's accepted profile and every reason. +`src/instant.ts` holds the parsers and `src/duration.ts` the span; `src/index.ts` re-exports both and adds `canonicalClientTimeZone`, the `string | undefined` form the wire boundaries already consume. The module JSDoc there states each entry's accepted profile and every reason. The strict parsers match a fixed regular expression before constructing any date, so a malformed spelling, an out-of-range clock field, and a date the calendar does not have answer distinct reasons. Local resolution defers to `@js-temporal/polyfill`: `disambiguation: 'earlier'` picks the first instant of an overlap, and the local-field round trip answers `undefined` for a value inside a forward transition's gap. @@ -106,7 +106,8 @@ None of its own. The consumer that injects a zone- or schedule-derived line into - **Alias resolution follows the runtime's ICU data** — which name an alias group canonicalizes to is the platform's answer, so two processes on different Node builds can disagree about it. - **No recurrence arithmetic** — instant validity, local-calendar parsing, zone resolution, canonical formatting, and a millisecond span only; a caller that walks candidate dates or intervals owns that search. - **`Duration` has no calendar units** — a day is exactly 24 hours and a month has no fixed length, so a caller that needs local calendar days derives them from the zone. -- **`Duration` does not convert units implicitly** — it has no `valueOf` and no `toString`, so a reader states its unit through `milliseconds` or a `to*` conversion. +- **`Duration` does not convert units implicitly** — it has no `valueOf`, no `toString`, and no `Symbol.toPrimitive`, so a reader states its unit through `milliseconds` or a `to*` conversion. +- **Seconds-unit boundaries still multiply** — `dsh-schedule` and the DeepSeek file store convert their own second-valued fields with `* 1_000`; `Duration.fromSeconds` is the intended next consumer for those spans, and none of them has moved yet. - **The `Temporal` polyfill is a runtime dependency** — a consumer that imports only `canonicalClientTimeZone` still resolves it at module load, because both entries share one module. diff --git a/packages/util/time/README.zh.md b/packages/util/time/README.zh.md index acf2074ffc..dcf29ed9ee 100644 --- a/packages/util/time/README.zh.md +++ b/packages/util/time/README.zh.md @@ -50,7 +50,7 @@ Duration.fromSeconds(90).decompose() // { days: 0, hours: 0, minutes: 1, seconds Duration.fromSeconds(90).milliseconds // 90000 — the unit is in the name, and no coercion answers the count ``` -不可用的时区名称由 `canonicalClientTimeZone` 回答 `undefined`,由调用方抛出自己的拒绝——Session 提示词用 `session/invalid-time-zone`,subagent 续话用 `subagent/invalid-time-zone`。`canonicalizeTimeZone`、各严格解析器与 `fourDigitYearEpoch` 则会说明拒绝原因,以带判别值的结果回答,其 `rejected` 分支携带稳定的拒绝原因,由各调用方选择自己的错误码与诊断文案。`formatFourDigitYearInstant` 与 `resolveLocalInstant` 接收已校验的值,分别回答字符串与纪元毫秒,无法表示的纪元或落在 DST 缺口内的墙钟值回答 `undefined`。`Duration` 是一个基于单一毫秒计数的冻结值类型:它校验计数有限,加减与排序都不做单位换算,按小数换算到秒、分、小时与天,并把非负的时间段拆成整数量。 +不可用的时区名称由 `canonicalClientTimeZone` 回答 `undefined`,由调用方抛出自己的拒绝——Session 提示词用 `session/invalid-time-zone`,subagent 续话用 `subagent/invalid-time-zone`。`canonicalizeTimeZone`、各严格解析器与 `fourDigitYearEpoch` 则会说明拒绝原因,以带判别值的结果回答,其 `rejected` 分支携带稳定的拒绝原因,由各调用方选择自己的错误码与诊断文案。`formatFourDigitYearInstant` 与 `resolveLocalInstant` 接收已校验的值,分别回答字符串与纪元毫秒,无法表示的纪元或落在 DST 缺口内的墙钟值回答 `undefined`。`Duration` 是一个基于单一毫秒计数的冻结值类型:它校验计数有限且落在可精确表示的范围内,加减、排序与负值判断都不做单位换算,按小数换算到秒、分、小时与天,并把非负的时间段拆成整数量。 ----- @@ -60,7 +60,7 @@ Duration.fromSeconds(90).milliseconds // 90000 — the unit is in the name, and
实现内部细节 —— 点击展开 -`src/instant.ts` 承载全部实现;`src/index.ts` 转出它,并新增 `canonicalClientTimeZone`——协议边界已经在消费的 `string | undefined` 形式。该模块的 JSDoc 说明每个入口接受的输入剖面与全部拒绝原因。 +`src/instant.ts` 承载各解析器,`src/duration.ts` 承载时间段;`src/index.ts` 转出两者,并新增 `canonicalClientTimeZone`——协议边界已经在消费的 `string | undefined` 形式。该模块的 JSDoc 说明每个入口接受的输入剖面与全部拒绝原因。 严格解析器先匹配固定正则,再构造任何日期,因此拼写错误、越界时钟字段与日历上不存在的日期分别回答不同的原因。本地解析交给 `@js-temporal/polyfill`:`disambiguation: 'earlier'` 取重叠时段中的第一个时刻,而本地字段往返比对对前向跳变缺口内的值回答 `undefined`。 @@ -106,7 +106,8 @@ Duration.fromSeconds(90).milliseconds // 90000 — the unit is in the name, and - **别名解析取决于运行时的 ICU 数据**——一个别名组规范化成哪个名称由平台回答,因此两个跑在不同 Node 构建上的进程可能给出不同答案。 - **不含重复规则运算**——只做时刻有效性、本地日历解析、时区解析、规范格式化与毫秒精度的时间段;遍历候选日期或间隔的搜索由调用方自己拥有。 - **`Duration` 没有日历单位**——一天恰好是 24 小时,月没有固定长度;需要本地日历天的调用方从时区推导。 -- **`Duration` 不做隐式单位换算**——它没有 `valueOf` 也没有 `toString`,读取方必须通过 `milliseconds` 或某个 `to*` 换算声明单位。 +- **`Duration` 不做隐式单位换算**——它没有 `valueOf`、`toString` 与 `Symbol.toPrimitive`,读取方必须通过 `milliseconds` 或某个 `to*` 换算声明单位。 +- **秒为单位的边界仍在自行相乘**——`dsh-schedule` 与 DeepSeek 的文件存储用 `* 1_000` 换算各自以秒为单位的字段;`Duration.fromSeconds` 是这些时间段的预期下一个消费方,目前都还没改。 - **`Temporal` polyfill 是运行时依赖**——只导入 `canonicalClientTimeZone` 的消费方在模块加载时同样会解析它,因为两个入口共用一个模块。 diff --git a/packages/util/time/src/duration.ts b/packages/util/time/src/duration.ts index 727d6743fd..172e9948fd 100644 --- a/packages/util/time/src/duration.ts +++ b/packages/util/time/src/duration.ts @@ -32,12 +32,23 @@ export interface DurationParts { readonly milliseconds: number } +/** + * Largest millisecond count a span holds. + * + * Every integer up to this bound is exactly representable, which is what makes + * the whole-part split exact; beyond it the parts a reader gets back would not + * add up to the span. + */ +const MAX_SPAN_MS = Number.MAX_SAFE_INTEGER + /** * One span of time in milliseconds. * - * Instances are frozen, so a span cannot drift after it is built, and they carry - * no calendar meaning: a month is not a fixed count of milliseconds, so no entry - * here converts to or from calendar units. + * Instances are frozen, so a span cannot drift after it is built. A span is + * bounded by the exactly representable millisecond range, which is wider than + * any interval this harness schedules, and it carries no calendar meaning: a + * month is not a fixed count of milliseconds, so no entry here converts to or + * from calendar units. */ export class Duration { /** The additive identity. */ @@ -52,14 +63,19 @@ export class Duration { /** * Build one span from a millisecond count. - * @param milliseconds - Finite millisecond count; a fractional count is kept as given. + * @param milliseconds - Count within the exactly representable range (about 285,000 years); fractions are kept. * @returns The span. - * @throws TypeError when the count is not a finite number. + * @throws TypeError when the count is not finite, or exceeds the exactly representable millisecond range. */ static fromMilliseconds(milliseconds: number): Duration { if (!Number.isFinite(milliseconds)) { throw new TypeError(`duration milliseconds must be finite, got ${String(milliseconds)}`) } + if (Math.abs(milliseconds) > MAX_SPAN_MS) { + throw new TypeError( + `duration milliseconds must not exceed ${MAX_SPAN_MS} so its parts stay exact, got ${String(milliseconds)}`, + ) + } return new Duration(milliseconds) } @@ -120,7 +136,7 @@ export class Duration { /** * The span in seconds. - * @returns The span as a fractional second count, lossless in both directions. + * @returns The span as a fractional seconds count: one division, so the answer is the double nearest the exact ratio. */ toSeconds(): number { return this.span / MS_PER_SECOND @@ -128,7 +144,7 @@ export class Duration { /** * The span in minutes. - * @returns The span as a fractional minute count, lossless in both directions. + * @returns The span as a fractional minutes count: one division, so the answer is the double nearest the exact ratio. */ toMinutes(): number { return this.span / MS_PER_MINUTE @@ -136,7 +152,7 @@ export class Duration { /** * The span in hours. - * @returns The span as a fractional hour count, lossless in both directions. + * @returns The span as a fractional hours count: one division, so the answer is the double nearest the exact ratio. */ toHours(): number { return this.span / MS_PER_HOUR @@ -148,7 +164,7 @@ export class Duration { * A day here is exactly 24 hours. A wall-clock reading that crosses a * daylight-saving transition is not a whole number of days, so a caller that * needs local calendar days derives them from the zone instead. - * @returns The span as a fractional day count, lossless in both directions. + * @returns The span as a fractional days count: one division, so the answer is the double nearest the exact ratio. */ toDays(): number { return this.span / MS_PER_DAY @@ -156,8 +172,9 @@ export class Duration { /** * Split the span into whole days, hours, minutes, seconds, and the remaining - * fraction of a second. Each part is floored, so the parts never exceed the - * span. + * fraction of a second. Each part is floored, and every span this class accepts + * is inside the exactly representable millisecond range, so the parts add back + * up to the span and the remainder stays below one second. * @returns The frozen whole parts. * @throws RangeError when the span is negative, because a negative interval * has no single whole-part decomposition: the caller owns that policy. diff --git a/packages/util/time/tests/duration.spec.ts b/packages/util/time/tests/duration.spec.ts index 1a90124337..f2b8de4a8b 100644 --- a/packages/util/time/tests/duration.spec.ts +++ b/packages/util/time/tests/duration.spec.ts @@ -25,14 +25,25 @@ describe('Duration construction', () => { expect(() => Duration.fromSeconds(1e306)).toThrow(TypeError) }) + it('refuses a count above the exactly representable millisecond range', () => { + expect(() => Duration.fromMilliseconds(Number.MAX_SAFE_INTEGER + 1)).toThrow(TypeError) + expect(Duration.fromMilliseconds(Number.MAX_SAFE_INTEGER).milliseconds).toBe(Number.MAX_SAFE_INTEGER) + // The bound is what keeps the whole-part identity exact, so it is asserted, not assumed. + const parts = Duration.fromMilliseconds(Number.MAX_SAFE_INTEGER).decompose() + const summed = parts.days * 86_400_000 + parts.hours * 3_600_000 + + parts.minutes * 60_000 + parts.seconds * 1_000 + parts.milliseconds + expect(summed).toBe(Number.MAX_SAFE_INTEGER) + }) + it('is frozen, and overrides no implicit conversion', () => { const span = Duration.fromMilliseconds(1) expect(Object.isFrozen(span)).toBe(true) expect(Reflect.set(span, 'span', 2)).toBe(false) expect(span.milliseconds).toBe(1) - // Neither coercion is overridden, so no caller obtains the count by coercion. + // No coercion hook is overridden, so no caller obtains the count by coercion. expect(Object.hasOwn(Duration.prototype, 'valueOf')).toBe(false) expect(Object.hasOwn(Duration.prototype, 'toString')).toBe(false) + expect(Object.hasOwn(Duration.prototype, Symbol.toPrimitive)).toBe(false) }) }) @@ -56,13 +67,18 @@ describe('Duration arithmetic', () => { }) describe('Duration conversion', () => { - it('converts to larger units fractionally, so nothing is lost either way', () => { + it('converts to larger units fractionally, by one division each', () => { const span = Duration.fromMilliseconds(90_061_500) expect(span.toSeconds()).toBe(90_061.5) expect(span.toMinutes()).toBe(1_501.025) expect(span.toHours()).toBe(25.017_083_333_333_332) expect(span.toDays()).toBe(1.042_378_472_222_222_2) expect(Duration.fromSeconds(span.toSeconds()).milliseconds).toBe(span.milliseconds) + // A conversion is a single division, so a round trip returns the nearest + // double rather than the original count: 1001 ms is 1.001 s, and 1.001 s is + // not exactly 1001 ms. + expect(Duration.fromMilliseconds(1_001).toSeconds()).toBe(1.001) + expect(Duration.fromSeconds(1.001).milliseconds).toBe(1_000.999_999_999_999_9) }) it('covers a day as exactly twenty-four hours', () => { @@ -93,7 +109,22 @@ describe('Duration decompose', () => { const summed = parts.days * 86_400_000 + parts.hours * 3_600_000 + parts.minutes * 60_000 + parts.seconds * 1_000 + parts.milliseconds expect(summed).toBe(3_661_999) - expect(summed).toBeLessThanOrEqual(3_661_999) + }) + + it('keeps the whole-part identity across the accepted range', () => { + const sum = (parts: { days: number; hours: number; minutes: number; seconds: number; milliseconds: number }): number => + parts.days * 86_400_000 + parts.hours * 3_600_000 + + parts.minutes * 60_000 + parts.seconds * 1_000 + parts.milliseconds + for (let span = 0; span <= 120_000; span += 1) { + expect(sum(Duration.fromMilliseconds(span).decompose())).toBe(span) + } + for (let i = 0; i < 2_000; i += 1) { + const span = Math.random() * Number.MAX_SAFE_INTEGER + const parts = Duration.fromMilliseconds(span).decompose() + expect(sum(parts)).toBe(span) + expect(parts.milliseconds).toBeGreaterThanOrEqual(0) + expect(parts.milliseconds).toBeLessThan(1_000) + } }) it('refuses a negative span instead of inventing a sign policy', () => { From 830f66006e54a7495dac6b731aff234097778422 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Fri, 25 Sep 2026 16:33:11 +0800 Subject: [PATCH 016/369] fix(util-time): enforce the span bound in arithmetic, and correct the record - plus and minus construct through fromMilliseconds, so the documented bound has one enforcement point; the negative control reddens the new assertion - the class and README say Duration overrides no coercion hook rather than having none, and decompose says which parts are floored - the note no longer calls the differential a review probe, records that Schedule imports the bound constants, that both strict parsers refuse year 0, that Schedule's suites do assert a few message substrings, and that the util charter bans harness dependencies rather than external packages --- ...026-09-24-shared-time-vocabulary.i18n.yaml | 8 +++---- .../2026-09-24-shared-time-vocabulary.md | 12 +++++----- .../2026-09-24-shared-time-vocabulary.zh.md | 12 +++++----- packages/util/README.i18n.yaml | 2 +- packages/util/README.zh.md | 2 +- packages/util/time/README.i18n.yaml | 4 ++-- packages/util/time/README.md | 2 +- packages/util/time/README.zh.md | 2 +- packages/util/time/package.json | 2 +- packages/util/time/src/duration.ts | 23 +++++++++++-------- packages/util/time/tests/duration.spec.ts | 8 +++++++ 11 files changed, 44 insertions(+), 33 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml index dcfe0eeb4e..0c6bd7165b 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml @@ -9,11 +9,11 @@ en: abd0263d14861926 zh: e1736cd08e87f795 /agent-note-shared-time-vocabulary-for-wall-clock-selectors/decision: - en: 4130cbbd30e06857 - zh: 85f9153cf5ad44ce + en: a573289069f0505c + zh: c4a125cbb0a7bcd4 /agent-note-shared-time-vocabulary-for-wall-clock-selectors/alternatives-considered: en: 1737484c099fab52 zh: 303b144c52d167bd /agent-note-shared-time-vocabulary-for-wall-clock-selectors/consequences: - en: f7181f87764118d2 - zh: 0af236ff4b6a935b + en: 99a03606d21acf8d + zh: f7b56ff5f7a61a76 diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md index d9fc530938..31d3f31927 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md @@ -12,7 +12,7 @@ English | [中文](2026-09-24-shared-time-vocabulary.zh.md) `dsh-util-time` owns the time vocabulary that boundaries accept. `packages/util/time/src/instant.ts` holds the parsers, `src/duration.ts` holds the span type, and `src/index.ts` re-exports both and keeps `canonicalClientTimeZone` as the `string | undefined` wrapper the existing wire boundaries consume. -The library answers discriminated results and carries no failure vocabulary. `parseOffsetInstant`, `fourDigitYearEpoch`, `parseLocalCalendar`, and `canonicalizeTimeZone` answer an `accepted` arm or a `rejected` arm naming a stable reason (`not_rfc3339`, `out_of_range`, `not_a_calendar_date`, `invalid_numeric_offset`, `malformed`, `blank`, `padded`, `not_area_location`, `unsupported`, `not_canonical_shape`). Each caller maps a reason onto its own code and diagnostic, so the library never writes model-facing text. `formatFourDigitYearInstant` returns the canonical instant text or `undefined` outside the bound, `isFourDigitYearInstant` answers whether an epoch is representable at all, and `resolveLocalInstant` answers epoch milliseconds or `undefined` for a wall-clock value inside a forward transition's gap. The reason vocabulary is parallel between the two strict parsers: the same condition is `out_of_range` in both, and `LocalCalendarTime.year` states which of them rejects year 0 (`parseLocalCalendar`) and which accepts it (`resolveLocalInstant`, whose callers derive local dates at the UTC edges). +The library answers discriminated results and carries no failure vocabulary. `parseOffsetInstant`, `fourDigitYearEpoch`, `parseLocalCalendar`, and `canonicalizeTimeZone` answer an `accepted` arm or a `rejected` arm naming a stable reason (`not_rfc3339`, `out_of_range`, `not_a_calendar_date`, `invalid_numeric_offset`, `malformed`, `blank`, `padded`, `not_area_location`, `unsupported`, `not_canonical_shape`). Each caller maps a reason onto its own code and diagnostic, so the library never writes model-facing text. `formatFourDigitYearInstant` returns the canonical instant text or `undefined` outside the bound, `isFourDigitYearInstant` answers whether an epoch is representable at all, and `resolveLocalInstant` answers epoch milliseconds or `undefined` for a wall-clock value inside a forward transition's gap. The reason vocabulary is parallel between the two strict parsers: the same condition is `out_of_range` in both, so year 0 is refused by `parseOffsetInstant` and `parseLocalCalendar` alike, while `resolveLocalInstant` accepts any year a real `Temporal` date carries because its callers derive local dates at the UTC edges. `LocalCalendarTime.year` records that split. Schedule consumes it and keeps every domain decision. `canonicalizeTimeZone`, `decodeInstant`, `parseAtInput`, and `futureInstant` remain Schedule entry points with their existing codes and diagnostics; they now call the shared parser and map its reason. Schedule still owns the durable record shapes, the fold and transition authority, the injection-resistant framing, and all recurrence arithmetic: the cron engine, the daily and weekly date walks, the latest-due search, and the schedule-specific accepted-decision bound. @@ -20,9 +20,9 @@ Schedule consumes it and keeps every domain decision. `canonicalizeTimeZone`, `d `Duration` holds one span as a single millisecond count, and it is a class rather than a branded number because TypeScript's arithmetic erases a brand: adding two differently branded numbers answers an unbranded `number`, so a brand cannot stop seconds and milliseconds from being mixed, while an instance can. The unit is milliseconds because every producer and consumer in this repository already uses it (`Date.now()`, `SessionEvent.time`, the session log, the client), and because a double holds every millisecond of the whole `Date` range exactly, so no precision is spent on the unit. Instances are frozen; the count is readable only as `milliseconds` or through a fractional `toSeconds`/`toMinutes`/`toHours`/`toDays` conversion, and there is deliberately no `valueOf` and no `toString`, so no reader ever receives a bare count whose unit it has to remember. Addition, subtraction, ordering, a negative-span test, and a whole-part `decompose()` are the whole operation set, alongside a `zero` identity and a `fromSeconds` constructor for a boundary whose unit is seconds. -The maintained `@js-temporal/polyfill` 0.5.1 becomes a runtime dependency of `dsh-util-time` as well as of `dsh-schedule`. Local resolution continues to use it, so the Host's IANA data still decides gaps and overlaps and no caller owns offset sampling. `util/time`'s README and description lose the `zero-dependency` claim the added dependency contradicts. +The maintained `@js-temporal/polyfill` 0.5.1 becomes a runtime dependency of `dsh-util-time` as well as of `dsh-schedule`. Local resolution continues to use it, so the Host's IANA data still decides gaps and overlaps and no caller owns offset sampling. `util/time`'s own description and README drop the `zero-dependency` phrase as ambiguous: the group's charter forbids harness dependencies so any group may depend on util, and does not ban external packages, which is why the group and root package maps keep the phrase while this package's own description names what it loads. -Observable behavior does not change. Every accepted input resolves to the same epoch, every refused input raises the same `ScheduleInputError` or `ScheduleLogError` code and the same message, and the stored `scheduledAt` profile is unchanged, so no durable record and no recorded snapshot is re-recorded. Schedule's other suites assert error codes only, so the assertion this change adds to `packages/schedule/schedule/tests/domain.spec.ts` is the only place that pins the three local-calendar message strings. `packages/util/time/tests/instant.spec.ts` covers the new entries directly, per-file 100% included. +Observable behavior does not change. Every accepted input resolves to the same epoch, every refused input raises the same `ScheduleInputError` or `ScheduleLogError` code and the same message, and the stored `scheduledAt` profile is unchanged, so no durable record and no recorded snapshot is re-recorded. Schedule's suites assert error codes and a few unrelated message substrings, so the assertion this change adds to `packages/schedule/schedule/tests/domain.spec.ts` is the only place that pins the three local-calendar message strings. `packages/util/time/tests/instant.spec.ts` covers the new entries directly, per-file 100% included. ## Alternatives considered @@ -40,12 +40,12 @@ Observable behavior does not change. Every accepted input resolves to the same e ## Consequences -The time profile now has one home. A change to the accepted instant profile or the zone profile is made once and covered once, and a new boundary imports it instead of copying it. Schedule's `domain.ts` loses the five regular expressions that spelled the instant, calendar-date, local-time, and zone profiles, the `Intl` canonicalization, and the hand-rolled calendar conversion. It keeps the two bound constants for the date arithmetic at the UTC edges and delegates every representability guard to `isFourDigitYearInstant`, so the range itself has one owner. +The time profile now has one home. A change to the accepted instant profile or the zone profile is made once and covered once, and a new boundary imports it instead of copying it. Schedule's `domain.ts` loses the five regular expressions that spelled the instant, calendar-date, local-time, and zone profiles, the `Intl` canonicalization, and the hand-rolled calendar conversion. It still imports the library's two bound constants for its date arithmetic at the UTC edges and delegates every representability guard to `isFourDigitYearInstant`, so the range itself has one owner. The cost is a wider published surface and a heavier dependency graph: `dsh-util-time` gains a runtime dependency on `@js-temporal/polyfill`, so a consumer that imports only `canonicalClientTimeZone` now loads that module too. The library is also no longer a pure "validation and canonicalization only" package, which its README and package description had to state and which its Known Limitations section now records. Schedule keeps a thin mapping layer at each entry. The local-calendar and instant mappers switch over the library's closed reason unions with a `never` default, so a newly added reason fails the build there instead of silently taking a catch-all. The zone mapper instead branches only on `not_canonical_shape` — a reason the current IANA data never produces, so it is guarded by a `v8 ignore` rather than a `never` default — and folds `blank`, `padded`, `not_area_location`, and `unsupported` into one diagnostic. A reason added to `TimeZoneRejection` later would therefore compile there and silently take that generic message. -The value type ships its whole operation set before its second consumer exists. Only `fromMilliseconds` and `decompose()` are called in production today, by time-context; `fromSeconds`, `milliseconds`, `plus`, `minus`, `compare`, `isNegative`, `zero`, and the four `to*` conversions are published without a caller. That is a deliberate choice rather than an oversight: the seconds-valued fields at `dsh-schedule` and the DeepSeek file store are the intended next consumers, and the alternative — a class that holds a span but can only split it — would have to grow the same members one call site at a time. The package README records the un-migrated boundaries as a known limitation. +The value type ships its whole operation set before its second consumer exists. Only `fromMilliseconds` and `decompose()` have a production caller, `dsh-time-context`; `fromSeconds`, `milliseconds`, `plus`, `minus`, `compare`, `isNegative`, `zero`, and the four `to*` conversions are published without a caller. That is a deliberate choice rather than an oversight: the seconds-valued fields at `dsh-schedule` and the DeepSeek file store are the intended next consumers, and the alternative — a class that holds a span but can only split it — would have to grow the same members one call site at a time. The package README records the un-migrated boundaries as a known limitation. -The elapsed reading keeps its split in the library and its wording in the consumer: `dsh-time-context` still decides that a backward clock reading clamps to zero and how the compact line reads, while `Duration.decompose()` answers the whole parts. That swap is byte-identical for every finite sample, measured with a differential over more than eight thousand inputs including negatives, fractions, and values past a year; that differential was a review probe, not a committed test, and the consumer's own suite pins the readings it renders (`1d 1h 1m 1s`, `1m 1s`, `4m 2s`, and the clamped zero). One input does change: a non-finite elapsed sample now throws from the span constructor instead of rendering `NaNs`, which the package's own reading invariant already refused, so the failure moves to the sample that produced it. +The elapsed reading keeps its split in the library and its wording in the consumer: `dsh-time-context` still decides that a backward clock reading clamps to zero and how the compact line reads, while `Duration.decompose()` answers the whole parts. That swap is byte-identical for every finite sample, measured over more than eight thousand inputs including negatives, fractions, and values past a year. That measurement is not a committed test: the consumer's own suite pins the readings it renders (`1d 1h 1m 1s`, `1m 1s`, `4m 2s`, and the clamped zero). One input does change: a non-finite elapsed sample now throws from the span constructor instead of rendering `NaNs`, which the package's own reading invariant already refused, so the failure moves to the sample that produced it. Time-context keeps the two policies this change did not touch: its ISO-shaped timestamp rendering in `packages/context/time-context/src/timestamp.ts`, which is that package's durable reading format, and the elapsed-duration wording in its own plugin. The browser client keeps its own localized date rendering and its own UTC-offset reading, which serve a different purpose (a reader's locale) from a boundary value; unifying those means deciding whether the shared library should also own display formatting, and which package then owns the durable reading format its invariant validates. `packages/session/session-format-v0-to-v1/src/payload-validation.ts` also validates the canonical four-digit-year UTC instant with its own regular expression and round trip, which is a remaining copy of the instant profile this change did not touch. diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md index ff8368f558..687cd858b6 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md @@ -12,7 +12,7 @@ Status: implemented `dsh-util-time` 拥有各边界所接收的时间词汇。解析器位于 `packages/util/time/src/instant.ts`,时间段类型位于 `src/duration.ts`,`src/index.ts` 转出两者,并保留 `canonicalClientTimeZone` 作为既有协议边界消费的 `string | undefined` 包装。 -本库回答带判别值的结果,不持有失败词汇。`parseOffsetInstant`、`fourDigitYearEpoch`、`parseLocalCalendar` 与 `canonicalizeTimeZone` 回答 `accepted` 分支,或回答携带稳定原因的 `rejected` 分支(`not_rfc3339`、`out_of_range`、`not_a_calendar_date`、`invalid_numeric_offset`、`malformed`、`blank`、`padded`、`not_area_location`、`unsupported`、`not_canonical_shape`)。每个调用方把原因映射成自己的错误码与诊断文案,因此本库从不撰写面向模型的文本。`formatFourDigitYearInstant` 返回规范时刻文本,越界时返回 `undefined`;`isFourDigitYearInstant` 回答某个纪元是否可表示;`resolveLocalInstant` 回答纪元毫秒,对落在前向跳变缺口内的墙钟值回答 `undefined`。两个严格解析器的原因词汇彼此对称:同一条件在两者中都叫 `out_of_range`,而 `LocalCalendarTime.year` 说明了二者中谁拒绝年份 0(`parseLocalCalendar`)、谁接受它(`resolveLocalInstant`,其调用方需要在 UTC 边界推导本地日期)。 +本库回答带判别值的结果,不持有失败词汇。`parseOffsetInstant`、`fourDigitYearEpoch`、`parseLocalCalendar` 与 `canonicalizeTimeZone` 回答 `accepted` 分支,或回答携带稳定原因的 `rejected` 分支(`not_rfc3339`、`out_of_range`、`not_a_calendar_date`、`invalid_numeric_offset`、`malformed`、`blank`、`padded`、`not_area_location`、`unsupported`、`not_canonical_shape`)。每个调用方把原因映射成自己的错误码与诊断文案,因此本库从不撰写面向模型的文本。`formatFourDigitYearInstant` 返回规范时刻文本,越界时返回 `undefined`;`isFourDigitYearInstant` 回答某个纪元是否可表示;`resolveLocalInstant` 回答纪元毫秒,对落在前向跳变缺口内的墙钟值回答 `undefined`。两个严格解析器的原因词汇彼此对称:同一条件在两者中都叫 `out_of_range`,所以年份 0 被 `parseOffsetInstant` 与 `parseLocalCalendar` 同样拒绝;而 `resolveLocalInstant` 接受真实 `Temporal` 日期携带的任何年份,因为它的调用方需要在 UTC 边界推导本地日期。`LocalCalendarTime.year` 记录了这一分工。 Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`decodeInstant`、`parseAtInput` 与 `futureInstant` 仍是 Schedule 的入口,保留既有错误码与诊断文案;它们现在调用共享解析器并映射其原因。Schedule 仍拥有持久记录结构、折叠与状态转移权威、抗注入的框架文本,以及全部重复规则运算:cron 引擎、每日与每周日期遍历、最近到期搜索,以及 Schedule 特有的决策时刻边界。 @@ -20,9 +20,9 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec `Duration` 以单一毫秒计数保存一段时间;它做成类而不是 branded number,是因为 TypeScript 的算术会抹掉 brand:两个不同品牌的数字相加得到的是无品牌的 `number`,所以 brand 挡不住秒与毫秒的混用,而实例可以。单位选毫秒,是因为本仓库的每个生产者与消费者本来就用它(`Date.now()`、`SessionEvent.time`、Session 日志、客户端),也因为 double 能精确表示 `Date` 全范围内的每一个毫秒,这个单位不消耗任何精度。实例被冻结;计数只能通过 `milliseconds` 或按小数换算的 `toSeconds`/`toMinutes`/`toHours`/`toDays` 读取,且刻意不提供 `valueOf` 与 `toString`,因此读取方永远不会拿到一个需要自己记单位的裸数字。加、减、排序、负值判断与整数量拆解 `decompose()` 就是全部操作集合,另有 `zero` 恒等元与面向以秒为单位边界的 `fromSeconds` 构造入口。 -受维护的 `@js-temporal/polyfill` 0.5.1 在成为 `dsh-schedule` 运行时依赖之外,也成为 `dsh-util-time` 的运行时依赖。本地解析继续使用它,因此缺口与重叠仍由宿主 IANA 数据决定,也没有调用方需要自己实现偏移采样。`util/time` 的 README 与包描述删去了新增依赖所否定的 `zero-dependency` 表述。 +受维护的 `@js-temporal/polyfill` 0.5.1 在成为 `dsh-schedule` 运行时依赖之外,也成为 `dsh-util-time` 的运行时依赖。本地解析继续使用它,因此缺口与重叠仍由宿主 IANA 数据决定,也没有调用方需要自己实现偏移采样。`util/time` 自身的描述与 README 去掉了 `zero-dependency` 这一说法,因为它容易被误读:该组的约定禁止的是 harness 依赖(以便任何组都能依赖 util),并不禁止外部包;因此组与根包映射保留该说法,而本包自己的描述写明它加载了什么。 -可观察行为不变。每个被接受的输入解析出相同的纪元值,每个被拒绝的输入抛出相同的 `ScheduleInputError` 或 `ScheduleLogError` 错误码与相同的消息,存储的 `scheduledAt` 剖面不变,因此没有持久记录或录制快照需要重录。Schedule 其余测试只断言错误码,因此本次在 `packages/schedule/schedule/tests/domain.spec.ts` 新增的断言是唯一固定这三条本地日历消息的地方。`packages/util/time/tests/instant.spec.ts` 直接覆盖新增入口,包含逐文件 100% 覆盖。 +可观察行为不变。每个被接受的输入解析出相同的纪元值,每个被拒绝的输入抛出相同的 `ScheduleInputError` 或 `ScheduleLogError` 错误码与相同的消息,存储的 `scheduledAt` 剖面不变,因此没有持久记录或录制快照需要重录。Schedule 的测试断言错误码以及少量无关的消息子串,因此本次在 `packages/schedule/schedule/tests/domain.spec.ts` 新增的断言是唯一固定这三条本地日历消息的地方。`packages/util/time/tests/instant.spec.ts` 直接覆盖新增入口,包含逐文件 100% 覆盖。 ## Alternatives considered @@ -40,12 +40,12 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec ## Consequences -时间剖面现在只有一个归宿。时刻剖面或时区剖面的变更只做一次、只覆盖一次,新边界导入它而不是复制它。Schedule 的 `domain.ts` 去掉了为时刻、日历日期、本地时间与时区剖面命名的五段正则、`Intl` 规范化与手写日历换算。它保留了用于 UTC 边界日期运算的两个边界常量,并把每处可表示性守卫委托给 `isFourDigitYearInstant`,因此范围本身只有一个所有者。 +时间剖面现在只有一个归宿。时刻剖面或时区剖面的变更只做一次、只覆盖一次,新边界导入它而不是复制它。Schedule 的 `domain.ts` 去掉了为时刻、日历日期、本地时间与时区剖面命名的五段正则、`Intl` 规范化与手写日历换算。它仍然导入本库的两个边界常量用于其 UTC 边界日期运算,并把每处可表示性守卫委托给 `isFourDigitYearInstant`,因此范围本身只有一个所有者。 代价是更宽的已发布接口与更重的依赖图:`dsh-util-time` 增加了对 `@js-temporal/polyfill` 的运行时依赖,因此只导入 `canonicalClientTimeZone` 的消费方也会加载该模块。本库也不再是纯"只做校验与规范化"的包——其 README 与包描述必须说明这一点,其"已知限制"一节现在也记录了它。Schedule 在每个入口保留一层薄映射。本地日历与时刻映射函数以 `never` 默认分支穷尽 switch 本库的封闭原因联合,因此新增原因会让这两个调用点构建失败,而不是静默走兜底分支。时区映射函数则只对 `not_canonical_shape` 分支——当前 IANA 数据不会产生该原因,因此它由 `v8 ignore` 守护而不是 `never` 默认分支——并把 `blank`、`padded`、`not_area_location` 与 `unsupported` 合并成一条诊断。因此日后给 `TimeZoneRejection` 新增原因时会通过编译并静默套用那条通用文案。 -该值类型在第二个消费方出现之前就交付了完整操作集合。目前生产代码只调用 `fromMilliseconds` 与 `decompose()`(由 time-context 调用);`fromSeconds`、`milliseconds`、`plus`、`minus`、`compare`、`isNegative`、`zero` 与四个 `to*` 换算都没有调用方。这是有意的选择而不是疏漏:`dsh-schedule` 与 DeepSeek 文件存储里以秒为单位的字段是预期的下一批消费方,而另一种做法——一个能保存时间段却只能拆解它的类——会在每个调用点出现时再长出同样的成员。包 README 把这些尚未迁移的边界记为已知限制。 +该值类型在第二个消费方出现之前就交付了完整操作集合。目前只有 `fromMilliseconds` 与 `decompose()` 有生产调用方,即 `dsh-time-context`;`fromSeconds`、`milliseconds`、`plus`、`minus`、`compare`、`isNegative`、`zero` 与四个 `to*` 换算都没有调用方。这是有意的选择而不是疏漏:`dsh-schedule` 与 DeepSeek 文件存储里以秒为单位的字段是预期的下一批消费方,而另一种做法——一个能保存时间段却只能拆解它的类——会在每个调用点出现时再长出同样的成员。包 README 把这些尚未迁移的边界记为已知限制。 -耗时读数的拆分上收到库、措辞留在一侧:`dsh-time-context` 仍然决定回退的时钟读数钳到零、以及那行紧凑文本怎么写,而 `Duration.decompose()` 回答整数量。这次替换对每个有限样本都逐字不变,用一份覆盖八千多个输入(含负数、小数与一年以上的值)的差分测得;该差分是评审用的探针而非提交进仓库的测试,消费方自己那套测试固定了它渲染出的读数(`1d 1h 1m 1s`、`1m 1s`、`4m 2s`,以及被钳到零的那次)。有一个输入会变:非有限的耗时样本现在由时间段构造函数抛出,而不是渲染出 `NaNs`——而那本来就被该包自己的读数不变量拒绝,所以失败点前移到了产生它的那次采样。 +耗时读数的拆分上收到库、措辞留在一侧:`dsh-time-context` 仍然决定回退的时钟读数钳到零、以及那行紧凑文本怎么写,而 `Duration.decompose()` 回答整数量。这次替换对每个有限样本都逐字不变,用一份覆盖八千多个输入(含负数、小数与一年以上的值)的差分测得。该测量不是提交进仓库的测试:消费方自己那套测试固定了它渲染出的读数(`1d 1h 1m 1s`、`1m 1s`、`4m 2s`,以及被钳到零的那次)。有一个输入会变:非有限的耗时样本现在由时间段构造函数抛出,而不是渲染出 `NaNs`——而那本来就被该包自己的读数不变量拒绝,所以失败点前移到了产生它的那次采样。 本次未触及、仍归 time-context 的两项:`packages/context/time-context/src/timestamp.ts` 中的 ISO 形状时间戳渲染(那是该包的持久读数字格式),以及其插件内的耗时措辞。浏览器客户端也保留自己的本地化日期渲染与自己的 UTC 偏移读取——它们服务的目的(读者的区域设置)与边界值不同;统一它们意味着先决定共享库是否也要拥有展示格式化,以及届时由谁拥有其不变量所校验的持久读数字格式。`packages/session/session-format-v0-to-v1/src/payload-validation.ts` 也用自己的一段正则与往返比对校验规范的四位年份 UTC 时刻,这是本次未触及、仍然存在的时刻剖面副本。 diff --git a/packages/util/README.i18n.yaml b/packages/util/README.i18n.yaml index c63d668ebb..38356f27bc 100644 --- a/packages/util/README.i18n.yaml +++ b/packages/util/README.i18n.yaml @@ -10,7 +10,7 @@ zh: 32c56bd67c0314c6 /util-shared-utilities/summary: en: f21c74532a9c929e - zh: 5bc05166e8d83c1e + zh: f34c908f47f4a58a /util-shared-utilities/table-of-contents: en: 1eb35b015a6c423f zh: a8a3dceee2c9e943 diff --git a/packages/util/README.zh.md b/packages/util/README.zh.md index a31d113d30..40b04df6d8 100644 --- a/packages/util/README.zh.md +++ b/packages/util/README.zh.md @@ -9,7 +9,7 @@ kind: "package-group" ## 概述 -`util/` 组为能力包提供共享的机制原语,避免重复实现。它涵盖原子写入、品牌化 id、双端队列、无损 JSON 值、UUID、Harness home 路径、启动环境、出站代理策略、原生命令、输出保留、时间与时区词汇,以及超时处理。这里的每个根入口都是库:它不注册产品服务或事件,业务语义仍由消费它的能力负责。 +`util/` 组为能力包提供共享的机制原语,避免重复实现。它涵盖原子写入、品牌化 id、双端队列、无损 JSON 值、UUID、Harness home 路径、启动环境、出站代理策略、原生命令、输出保留、时区、时刻与时间段词汇,以及超时处理。这里的每个根入口都是库:它不注册产品服务或事件,业务语义仍由消费它的能力负责。 ## 目录 diff --git a/packages/util/time/README.i18n.yaml b/packages/util/time/README.i18n.yaml index 8559bd7e18..c9519add50 100644 --- a/packages/util/time/README.i18n.yaml +++ b/packages/util/time/README.i18n.yaml @@ -36,8 +36,8 @@ en: daf6e45a18c087b0 zh: e6714f1e65ed55ce /dsh-util-time/known-limitations-and-deferred-work: - en: 20e184351878f41b - zh: 1c94d8a715df7c1b + en: c045ad46a696321a + zh: 648cd96b1bec5fff /dsh-util-time/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: e88aeb617ffb74ea diff --git a/packages/util/time/README.md b/packages/util/time/README.md index 4f1d2ee9ea..6fc568b106 100644 --- a/packages/util/time/README.md +++ b/packages/util/time/README.md @@ -106,7 +106,7 @@ None of its own. The consumer that injects a zone- or schedule-derived line into - **Alias resolution follows the runtime's ICU data** — which name an alias group canonicalizes to is the platform's answer, so two processes on different Node builds can disagree about it. - **No recurrence arithmetic** — instant validity, local-calendar parsing, zone resolution, canonical formatting, and a millisecond span only; a caller that walks candidate dates or intervals owns that search. - **`Duration` has no calendar units** — a day is exactly 24 hours and a month has no fixed length, so a caller that needs local calendar days derives them from the zone. -- **`Duration` does not convert units implicitly** — it has no `valueOf`, no `toString`, and no `Symbol.toPrimitive`, so a reader states its unit through `milliseconds` or a `to*` conversion. +- **`Duration` does not convert units implicitly** — it overrides no coercion hook (`valueOf`, `toString`, `Symbol.toPrimitive`), so a reader states its unit through `milliseconds` or a `to*` conversion. - **Seconds-unit boundaries still multiply** — `dsh-schedule` and the DeepSeek file store convert their own second-valued fields with `* 1_000`; `Duration.fromSeconds` is the intended next consumer for those spans, and none of them has moved yet. - **The `Temporal` polyfill is a runtime dependency** — a consumer that imports only `canonicalClientTimeZone` still resolves it at module load, because both entries share one module. diff --git a/packages/util/time/README.zh.md b/packages/util/time/README.zh.md index dcf29ed9ee..53c408b332 100644 --- a/packages/util/time/README.zh.md +++ b/packages/util/time/README.zh.md @@ -106,7 +106,7 @@ Duration.fromSeconds(90).milliseconds // 90000 — the unit is in the name, and - **别名解析取决于运行时的 ICU 数据**——一个别名组规范化成哪个名称由平台回答,因此两个跑在不同 Node 构建上的进程可能给出不同答案。 - **不含重复规则运算**——只做时刻有效性、本地日历解析、时区解析、规范格式化与毫秒精度的时间段;遍历候选日期或间隔的搜索由调用方自己拥有。 - **`Duration` 没有日历单位**——一天恰好是 24 小时,月没有固定长度;需要本地日历天的调用方从时区推导。 -- **`Duration` 不做隐式单位换算**——它没有 `valueOf`、`toString` 与 `Symbol.toPrimitive`,读取方必须通过 `milliseconds` 或某个 `to*` 换算声明单位。 +- **`Duration` 不做隐式单位换算**——它不覆盖任何强制转换钩子(`valueOf`、`toString`、`Symbol.toPrimitive`),读取方必须通过 `milliseconds` 或某个 `to*` 换算声明单位。 - **秒为单位的边界仍在自行相乘**——`dsh-schedule` 与 DeepSeek 的文件存储用 `* 1_000` 换算各自以秒为单位的字段;`Duration.fromSeconds` 是这些时间段的预期下一个消费方,目前都还没改。 - **`Temporal` polyfill 是运行时依赖**——只导入 `canonicalClientTimeZone` 的消费方在模块加载时同样会解析它,因为两个入口共用一个模块。 diff --git a/packages/util/time/package.json b/packages/util/time/package.json index b18fc42f3e..4bf0fdf4ae 100644 --- a/packages/util/time/package.json +++ b/packages/util/time/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-util-time", - "description": "Time vocabulary shared by wire boundaries: canonicalClientTimeZone (IANA zone validation and canonicalization), strict RFC 3339 instant, local-calendar, and zone resolution for wall-clock selectors, and a millisecond Duration span", + "description": "Time vocabulary shared by the boundaries that accept a caller's zone, a wall-clock selector, or an elapsed time: canonicalClientTimeZone (IANA zone validation and canonicalization), strict RFC 3339 instant, local-calendar, and zone resolution, and a millisecond Duration span", "version": "0.1.7-rc.2", "publishConfig": { "access": "public" diff --git a/packages/util/time/src/duration.ts b/packages/util/time/src/duration.ts index 172e9948fd..55a221e6b5 100644 --- a/packages/util/time/src/duration.ts +++ b/packages/util/time/src/duration.ts @@ -3,10 +3,10 @@ * * A class rather than a branded number: arithmetic on two branded numbers * answers an unbranded `number`, so a brand cannot stop seconds and - * milliseconds from being added, while an instance can. There is no `valueOf` - * and no `toString`, so no reader ever receives a bare count whose unit it has - * to remember; a caller states the unit by reading `milliseconds` or by - * converting through one of the `to*` entries. + * milliseconds from being added, while an instance can. No coercion hook is + * overridden, so no reader receives a bare count whose unit it has to remember; + * a caller states the unit by reading `milliseconds` or by converting through + * one of the `to*` entries. * @module @deepseek-ai/dsh-util-time */ @@ -83,7 +83,7 @@ export class Duration { * Build one span from a second count, for a boundary whose unit is seconds. * @param seconds - Finite second count; a fractional count is kept as given. * @returns The span. - * @throws TypeError when the count is not finite, or its millisecond equivalent is not. + * @throws TypeError when the count is not finite, or its millisecond equivalent is not finite or leaves the exactly representable range. */ static fromSeconds(seconds: number): Duration { if (!Number.isFinite(seconds)) { @@ -101,18 +101,20 @@ export class Duration { * Add one span to this span. * @param other - Span to add; both operands already share the millisecond unit. * @returns The sum. + * @throws TypeError when the sum leaves the exactly representable millisecond range. */ plus(other: Duration): Duration { - return new Duration(this.span + other.span) + return Duration.fromMilliseconds(this.span + other.span) } /** * Subtract one span from this span. * @param other - Span to subtract. * @returns The difference, which is negative when `other` is longer. + * @throws TypeError when the difference leaves the exactly representable millisecond range. */ minus(other: Duration): Duration { - return new Duration(this.span - other.span) + return Duration.fromMilliseconds(this.span - other.span) } /** @@ -172,9 +174,10 @@ export class Duration { /** * Split the span into whole days, hours, minutes, seconds, and the remaining - * fraction of a second. Each part is floored, and every span this class accepts - * is inside the exactly representable millisecond range, so the parts add back - * up to the span and the remainder stays below one second. + * fraction of a second. Days, hours, minutes, and seconds are floored; the + * millisecond part keeps the fraction below one second. Every span this class + * accepts is inside the exactly representable millisecond range, so the parts + * add back up to the span. * @returns The frozen whole parts. * @throws RangeError when the span is negative, because a negative interval * has no single whole-part decomposition: the caller owns that policy. diff --git a/packages/util/time/tests/duration.spec.ts b/packages/util/time/tests/duration.spec.ts index f2b8de4a8b..8edd4f1ead 100644 --- a/packages/util/time/tests/duration.spec.ts +++ b/packages/util/time/tests/duration.spec.ts @@ -55,6 +55,14 @@ describe('Duration arithmetic', () => { expect(Duration.zero.plus(Duration.zero).milliseconds).toBe(0) }) + it('refuses a sum or difference that leaves the exactly representable range', () => { + const max = Duration.fromMilliseconds(Number.MAX_SAFE_INTEGER) + expect(() => max.plus(Duration.fromMilliseconds(1))).toThrow(TypeError) + expect(() => Duration.zero.minus(max).minus(Duration.fromMilliseconds(1))).toThrow(TypeError) + // The bound is checked at every construction, not only at the factories. + expect(max.plus(Duration.fromMilliseconds(0)).milliseconds).toBe(Number.MAX_SAFE_INTEGER) + }) + it('orders spans without surrendering the unit', () => { const minute = Duration.fromSeconds(60) const hour = Duration.fromSeconds(3_600) From 9facb516149bb17ce937141fe956d0ad25bd11a3 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Sat, 26 Sep 2026 01:22:52 +0800 Subject: [PATCH 017/369] docs: state the coercion, changed samples, and charter files exactly - the note says Duration overrides no coercion hook, matching the README and the class doc rather than claiming it has no toString - the elapsed change names the two samples that move (NaN and +Infinity, not -Infinity, which the consumer still clamps to zero) - the charter sentence names the files that keep the phrase - the seconds-unit limitation separates a span from an epoch in seconds --- .../2026-09-24-shared-time-vocabulary.i18n.yaml | 8 ++++---- .../architecture/2026-09-24-shared-time-vocabulary.md | 6 +++--- .../architecture/2026-09-24-shared-time-vocabulary.zh.md | 6 +++--- packages/util/time/README.i18n.yaml | 4 ++-- packages/util/time/README.md | 2 +- packages/util/time/README.zh.md | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml index 0c6bd7165b..c22f9dd39e 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml @@ -9,11 +9,11 @@ en: abd0263d14861926 zh: e1736cd08e87f795 /agent-note-shared-time-vocabulary-for-wall-clock-selectors/decision: - en: a573289069f0505c - zh: c4a125cbb0a7bcd4 + en: db91e6074fd74bdb + zh: 1957b8b0e1243afc /agent-note-shared-time-vocabulary-for-wall-clock-selectors/alternatives-considered: en: 1737484c099fab52 zh: 303b144c52d167bd /agent-note-shared-time-vocabulary-for-wall-clock-selectors/consequences: - en: 99a03606d21acf8d - zh: f7b56ff5f7a61a76 + en: 78c1f9d2cbaee3e2 + zh: 8adbaa9f8d80b2eb diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md index 31d3f31927..70d8289089 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md @@ -18,9 +18,9 @@ Schedule consumes it and keeps every domain decision. `canonicalizeTimeZone`, `d `dsh-time-context` consumes it too and keeps its stricter policy. `deriveBrowserTimeZoneContext` reads each user-rpc zone through `canonicalizeTimeZone` and refuses an alias whose canonical answer differs from the reported value, so the request boundary still requires an identity another process resolves back to the same name. Time-context owns its three refusals and their messages; the shared entry only answers the canonical name or a reason, and its `unsupported` rejection carries the platform error the library caught, so that refusal keeps its `cause` in both consumers. -`Duration` holds one span as a single millisecond count, and it is a class rather than a branded number because TypeScript's arithmetic erases a brand: adding two differently branded numbers answers an unbranded `number`, so a brand cannot stop seconds and milliseconds from being mixed, while an instance can. The unit is milliseconds because every producer and consumer in this repository already uses it (`Date.now()`, `SessionEvent.time`, the session log, the client), and because a double holds every millisecond of the whole `Date` range exactly, so no precision is spent on the unit. Instances are frozen; the count is readable only as `milliseconds` or through a fractional `toSeconds`/`toMinutes`/`toHours`/`toDays` conversion, and there is deliberately no `valueOf` and no `toString`, so no reader ever receives a bare count whose unit it has to remember. Addition, subtraction, ordering, a negative-span test, and a whole-part `decompose()` are the whole operation set, alongside a `zero` identity and a `fromSeconds` constructor for a boundary whose unit is seconds. +`Duration` holds one span as a single millisecond count, and it is a class rather than a branded number because TypeScript's arithmetic erases a brand: adding two differently branded numbers answers an unbranded `number`, so a brand cannot stop seconds and milliseconds from being mixed, while an instance can. The unit is milliseconds because every producer and consumer in this repository already uses it (`Date.now()`, `SessionEvent.time`, the session log, the client), and because a double holds every millisecond of the whole `Date` range exactly, so no precision is spent on the unit. Instances are frozen; the count is readable only as `milliseconds` or through a fractional `toSeconds`/`toMinutes`/`toHours`/`toDays` conversion, and it deliberately overrides no coercion hook, so no reader receives a bare count whose unit it has to remember. Addition, subtraction, ordering, a negative-span test, and a whole-part `decompose()` are the whole operation set, alongside a `zero` identity and a `fromSeconds` constructor for a boundary whose unit is seconds. -The maintained `@js-temporal/polyfill` 0.5.1 becomes a runtime dependency of `dsh-util-time` as well as of `dsh-schedule`. Local resolution continues to use it, so the Host's IANA data still decides gaps and overlaps and no caller owns offset sampling. `util/time`'s own description and README drop the `zero-dependency` phrase as ambiguous: the group's charter forbids harness dependencies so any group may depend on util, and does not ban external packages, which is why the group and root package maps keep the phrase while this package's own description names what it loads. +The maintained `@js-temporal/polyfill` 0.5.1 becomes a runtime dependency of `dsh-util-time` as well as of `dsh-schedule`. Local resolution continues to use it, so the Host's IANA data still decides gaps and overlaps and no caller owns offset sampling. `util/time`'s own description and README drop the `zero-dependency` phrase as ambiguous: the group's charter forbids harness dependencies so any group may depend on util, and does not ban external packages, which is why `packages/README.md` and the `util/` line in `AGENTS.md` keep the phrase while `packages/util/README.md` and this package's own description name what it loads. Observable behavior does not change. Every accepted input resolves to the same epoch, every refused input raises the same `ScheduleInputError` or `ScheduleLogError` code and the same message, and the stored `scheduledAt` profile is unchanged, so no durable record and no recorded snapshot is re-recorded. Schedule's suites assert error codes and a few unrelated message substrings, so the assertion this change adds to `packages/schedule/schedule/tests/domain.spec.ts` is the only place that pins the three local-calendar message strings. `packages/util/time/tests/instant.spec.ts` covers the new entries directly, per-file 100% included. @@ -46,6 +46,6 @@ The cost is a wider published surface and a heavier dependency graph: `dsh-util- The value type ships its whole operation set before its second consumer exists. Only `fromMilliseconds` and `decompose()` have a production caller, `dsh-time-context`; `fromSeconds`, `milliseconds`, `plus`, `minus`, `compare`, `isNegative`, `zero`, and the four `to*` conversions are published without a caller. That is a deliberate choice rather than an oversight: the seconds-valued fields at `dsh-schedule` and the DeepSeek file store are the intended next consumers, and the alternative — a class that holds a span but can only split it — would have to grow the same members one call site at a time. The package README records the un-migrated boundaries as a known limitation. -The elapsed reading keeps its split in the library and its wording in the consumer: `dsh-time-context` still decides that a backward clock reading clamps to zero and how the compact line reads, while `Duration.decompose()` answers the whole parts. That swap is byte-identical for every finite sample, measured over more than eight thousand inputs including negatives, fractions, and values past a year. That measurement is not a committed test: the consumer's own suite pins the readings it renders (`1d 1h 1m 1s`, `1m 1s`, `4m 2s`, and the clamped zero). One input does change: a non-finite elapsed sample now throws from the span constructor instead of rendering `NaNs`, which the package's own reading invariant already refused, so the failure moves to the sample that produced it. +The elapsed reading keeps its split in the library and its wording in the consumer: `dsh-time-context` still decides that a backward clock reading clamps to zero and how the compact line reads, while `Duration.decompose()` answers the whole parts. That swap is byte-identical for every finite sample, measured over more than eight thousand inputs including negatives, fractions, and values past a year. That measurement is not a committed test: the consumer's own suite pins the readings it renders (`1d 1h 1m 1s`, `1m 1s`, `4m 2s`, and the clamped zero). Two samples do change: `NaN` rendered `NaNs` and `+Infinity` rendered `Infinityd NaNs`, and both now throw from the span constructor; the package's own reading invariant already refused those two renderings, so the failure moves to the sample that produced it. A `-Infinity` sample is unchanged, because the consumer clamps it to zero before the span is built. Time-context keeps the two policies this change did not touch: its ISO-shaped timestamp rendering in `packages/context/time-context/src/timestamp.ts`, which is that package's durable reading format, and the elapsed-duration wording in its own plugin. The browser client keeps its own localized date rendering and its own UTC-offset reading, which serve a different purpose (a reader's locale) from a boundary value; unifying those means deciding whether the shared library should also own display formatting, and which package then owns the durable reading format its invariant validates. `packages/session/session-format-v0-to-v1/src/payload-validation.ts` also validates the canonical four-digit-year UTC instant with its own regular expression and round trip, which is a remaining copy of the instant profile this change did not touch. diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md index 687cd858b6..44d867a15e 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md @@ -18,9 +18,9 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec `dsh-time-context` 同样消费它,并保留自己更严格的策略。`deriveBrowserTimeZoneContext` 通过 `canonicalizeTimeZone` 读取每条 user-rpc 时区,并拒绝规范答案与所报值不同的别名,因此请求边界仍然要求另一个进程能解析回同一名称。三条拒绝与它们的文案归 time-context 所有;共享入口只回答规范名称或一个原因;被拒绝的结果中只有 `unsupported` 会携带本库捕获到的平台错误,使该拒绝在两个消费方都保留其 `cause`。 -`Duration` 以单一毫秒计数保存一段时间;它做成类而不是 branded number,是因为 TypeScript 的算术会抹掉 brand:两个不同品牌的数字相加得到的是无品牌的 `number`,所以 brand 挡不住秒与毫秒的混用,而实例可以。单位选毫秒,是因为本仓库的每个生产者与消费者本来就用它(`Date.now()`、`SessionEvent.time`、Session 日志、客户端),也因为 double 能精确表示 `Date` 全范围内的每一个毫秒,这个单位不消耗任何精度。实例被冻结;计数只能通过 `milliseconds` 或按小数换算的 `toSeconds`/`toMinutes`/`toHours`/`toDays` 读取,且刻意不提供 `valueOf` 与 `toString`,因此读取方永远不会拿到一个需要自己记单位的裸数字。加、减、排序、负值判断与整数量拆解 `decompose()` 就是全部操作集合,另有 `zero` 恒等元与面向以秒为单位边界的 `fromSeconds` 构造入口。 +`Duration` 以单一毫秒计数保存一段时间;它做成类而不是 branded number,是因为 TypeScript 的算术会抹掉 brand:两个不同品牌的数字相加得到的是无品牌的 `number`,所以 brand 挡不住秒与毫秒的混用,而实例可以。单位选毫秒,是因为本仓库的每个生产者与消费者本来就用它(`Date.now()`、`SessionEvent.time`、Session 日志、客户端),也因为 double 能精确表示 `Date` 全范围内的每一个毫秒,这个单位不消耗任何精度。实例被冻结;计数只能通过 `milliseconds` 或按小数换算的 `toSeconds`/`toMinutes`/`toHours`/`toDays` 读取,且刻意不覆盖任何强制转换钩子,因此读取方永远不会拿到一个需要自己记单位的裸数字。加、减、排序、负值判断与整数量拆解 `decompose()` 就是全部操作集合,另有 `zero` 恒等元与面向以秒为单位边界的 `fromSeconds` 构造入口。 -受维护的 `@js-temporal/polyfill` 0.5.1 在成为 `dsh-schedule` 运行时依赖之外,也成为 `dsh-util-time` 的运行时依赖。本地解析继续使用它,因此缺口与重叠仍由宿主 IANA 数据决定,也没有调用方需要自己实现偏移采样。`util/time` 自身的描述与 README 去掉了 `zero-dependency` 这一说法,因为它容易被误读:该组的约定禁止的是 harness 依赖(以便任何组都能依赖 util),并不禁止外部包;因此组与根包映射保留该说法,而本包自己的描述写明它加载了什么。 +受维护的 `@js-temporal/polyfill` 0.5.1 在成为 `dsh-schedule` 运行时依赖之外,也成为 `dsh-util-time` 的运行时依赖。本地解析继续使用它,因此缺口与重叠仍由宿主 IANA 数据决定,也没有调用方需要自己实现偏移采样。`util/time` 自身的描述与 README 去掉了 `zero-dependency` 这一说法,因为它容易被误读:该组的约定禁止的是 harness 依赖(以便任何组都能依赖 util),并不禁止外部包;因此 `packages/README.md` 与 `AGENTS.md` 里的 `util/` 一行保留该说法,而 `packages/util/README.md` 与本包自己的描述写明它加载了什么。 可观察行为不变。每个被接受的输入解析出相同的纪元值,每个被拒绝的输入抛出相同的 `ScheduleInputError` 或 `ScheduleLogError` 错误码与相同的消息,存储的 `scheduledAt` 剖面不变,因此没有持久记录或录制快照需要重录。Schedule 的测试断言错误码以及少量无关的消息子串,因此本次在 `packages/schedule/schedule/tests/domain.spec.ts` 新增的断言是唯一固定这三条本地日历消息的地方。`packages/util/time/tests/instant.spec.ts` 直接覆盖新增入口,包含逐文件 100% 覆盖。 @@ -46,6 +46,6 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec 该值类型在第二个消费方出现之前就交付了完整操作集合。目前只有 `fromMilliseconds` 与 `decompose()` 有生产调用方,即 `dsh-time-context`;`fromSeconds`、`milliseconds`、`plus`、`minus`、`compare`、`isNegative`、`zero` 与四个 `to*` 换算都没有调用方。这是有意的选择而不是疏漏:`dsh-schedule` 与 DeepSeek 文件存储里以秒为单位的字段是预期的下一批消费方,而另一种做法——一个能保存时间段却只能拆解它的类——会在每个调用点出现时再长出同样的成员。包 README 把这些尚未迁移的边界记为已知限制。 -耗时读数的拆分上收到库、措辞留在一侧:`dsh-time-context` 仍然决定回退的时钟读数钳到零、以及那行紧凑文本怎么写,而 `Duration.decompose()` 回答整数量。这次替换对每个有限样本都逐字不变,用一份覆盖八千多个输入(含负数、小数与一年以上的值)的差分测得。该测量不是提交进仓库的测试:消费方自己那套测试固定了它渲染出的读数(`1d 1h 1m 1s`、`1m 1s`、`4m 2s`,以及被钳到零的那次)。有一个输入会变:非有限的耗时样本现在由时间段构造函数抛出,而不是渲染出 `NaNs`——而那本来就被该包自己的读数不变量拒绝,所以失败点前移到了产生它的那次采样。 +耗时读数的拆分上收到库、措辞留在一侧:`dsh-time-context` 仍然决定回退的时钟读数钳到零、以及那行紧凑文本怎么写,而 `Duration.decompose()` 回答整数量。这次替换对每个有限样本都逐字不变,用一份覆盖八千多个输入(含负数、小数与一年以上的值)的差分测得。该测量不是提交进仓库的测试:消费方自己那套测试固定了它渲染出的读数(`1d 1h 1m 1s`、`1m 1s`、`4m 2s`,以及被钳到零的那次)。有两个样本会变:`NaN` 原本渲染出 `NaNs`、`+Infinity` 原本渲染出 `Infinityd NaNs`,现在两者都由时间段构造函数抛出;这两种渲染本来就被该包自己的读数不变量拒绝,所以失败点前移到了产生它的那次采样。`-Infinity` 样本不变,因为消费方在构造时间段之前已把它钳到零。 本次未触及、仍归 time-context 的两项:`packages/context/time-context/src/timestamp.ts` 中的 ISO 形状时间戳渲染(那是该包的持久读数字格式),以及其插件内的耗时措辞。浏览器客户端也保留自己的本地化日期渲染与自己的 UTC 偏移读取——它们服务的目的(读者的区域设置)与边界值不同;统一它们意味着先决定共享库是否也要拥有展示格式化,以及届时由谁拥有其不变量所校验的持久读数字格式。`packages/session/session-format-v0-to-v1/src/payload-validation.ts` 也用自己的一段正则与往返比对校验规范的四位年份 UTC 时刻,这是本次未触及、仍然存在的时刻剖面副本。 diff --git a/packages/util/time/README.i18n.yaml b/packages/util/time/README.i18n.yaml index c9519add50..3e35ad4c58 100644 --- a/packages/util/time/README.i18n.yaml +++ b/packages/util/time/README.i18n.yaml @@ -36,8 +36,8 @@ en: daf6e45a18c087b0 zh: e6714f1e65ed55ce /dsh-util-time/known-limitations-and-deferred-work: - en: c045ad46a696321a - zh: 648cd96b1bec5fff + en: 06ea0ac6f5102d88 + zh: bdc0d5d2a1faba41 /dsh-util-time/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: e88aeb617ffb74ea diff --git a/packages/util/time/README.md b/packages/util/time/README.md index 6fc568b106..373f050a30 100644 --- a/packages/util/time/README.md +++ b/packages/util/time/README.md @@ -107,7 +107,7 @@ None of its own. The consumer that injects a zone- or schedule-derived line into - **No recurrence arithmetic** — instant validity, local-calendar parsing, zone resolution, canonical formatting, and a millisecond span only; a caller that walks candidate dates or intervals owns that search. - **`Duration` has no calendar units** — a day is exactly 24 hours and a month has no fixed length, so a caller that needs local calendar days derives them from the zone. - **`Duration` does not convert units implicitly** — it overrides no coercion hook (`valueOf`, `toString`, `Symbol.toPrimitive`), so a reader states its unit through `milliseconds` or a `to*` conversion. -- **Seconds-unit boundaries still multiply** — `dsh-schedule` and the DeepSeek file store convert their own second-valued fields with `* 1_000`; `Duration.fromSeconds` is the intended next consumer for those spans, and none of them has moved yet. +- **Seconds-unit spans still multiply** — `dsh-schedule`'s interval and delay fields and the DeepSeek file store's refresh margin are second-valued spans converted with `* 1_000`; `Duration.fromSeconds` is their intended next consumer, and none has moved yet. An epoch expressed in seconds is not a span and must not be built into one. - **The `Temporal` polyfill is a runtime dependency** — a consumer that imports only `canonicalClientTimeZone` still resolves it at module load, because both entries share one module. diff --git a/packages/util/time/README.zh.md b/packages/util/time/README.zh.md index 53c408b332..c61b4ef01c 100644 --- a/packages/util/time/README.zh.md +++ b/packages/util/time/README.zh.md @@ -107,7 +107,7 @@ Duration.fromSeconds(90).milliseconds // 90000 — the unit is in the name, and - **不含重复规则运算**——只做时刻有效性、本地日历解析、时区解析、规范格式化与毫秒精度的时间段;遍历候选日期或间隔的搜索由调用方自己拥有。 - **`Duration` 没有日历单位**——一天恰好是 24 小时,月没有固定长度;需要本地日历天的调用方从时区推导。 - **`Duration` 不做隐式单位换算**——它不覆盖任何强制转换钩子(`valueOf`、`toString`、`Symbol.toPrimitive`),读取方必须通过 `milliseconds` 或某个 `to*` 换算声明单位。 -- **秒为单位的边界仍在自行相乘**——`dsh-schedule` 与 DeepSeek 的文件存储用 `* 1_000` 换算各自以秒为单位的字段;`Duration.fromSeconds` 是这些时间段的预期下一个消费方,目前都还没改。 +- **以秒为单位的跨度仍在自行相乘**——`dsh-schedule` 的间隔与延迟字段、DeepSeek 文件存储的刷新余量都是以秒为单位的跨度,用 `* 1_000` 换算;`Duration.fromSeconds` 是它们预期的下一个消费方,目前都还没改。以秒表示的纪元不是跨度,不得构造成跨度。 - **`Temporal` polyfill 是运行时依赖**——只导入 `canonicalClientTimeZone` 的消费方在模块加载时同样会解析它,因为两个入口共用一个模块。 From 8987e6c45afb89ee8ac4ec8040d8b0d6c8b2dae0 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Thu, 8 Oct 2026 16:20:08 +0800 Subject: [PATCH 018/369] docs: drop references to the removed invariant mechanism origin/master removed the runtime invariant plugins and the package rule that asked a README to justify the absence, so the text that cited that mechanism no longer refers to anything: - the Agent Note states the elapsed-rendering change without the reading invariant it used to cite - the same note no longer says the durable reading format is one an invariant validates - the util/time README no longer explains why no invariant companion is published, and its module JSDoc no longer claims the library formats nothing while listing a canonical instant renderer --- .../architecture/2026-09-24-shared-time-vocabulary.i18n.yaml | 4 ++-- .../architecture/2026-09-24-shared-time-vocabulary.md | 4 ++-- .../architecture/2026-09-24-shared-time-vocabulary.zh.md | 4 ++-- packages/util/time/README.i18n.yaml | 4 ++-- packages/util/time/README.md | 1 - packages/util/time/README.zh.md | 1 - packages/util/time/src/index.ts | 4 ++-- 7 files changed, 10 insertions(+), 12 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml index c22f9dd39e..c7d4d181a9 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml @@ -15,5 +15,5 @@ en: 1737484c099fab52 zh: 303b144c52d167bd /agent-note-shared-time-vocabulary-for-wall-clock-selectors/consequences: - en: 78c1f9d2cbaee3e2 - zh: 8adbaa9f8d80b2eb + en: ef18e69924eb420b + zh: 27e42a923b94204a diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md index 70d8289089..5983ca85cf 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md @@ -46,6 +46,6 @@ The cost is a wider published surface and a heavier dependency graph: `dsh-util- The value type ships its whole operation set before its second consumer exists. Only `fromMilliseconds` and `decompose()` have a production caller, `dsh-time-context`; `fromSeconds`, `milliseconds`, `plus`, `minus`, `compare`, `isNegative`, `zero`, and the four `to*` conversions are published without a caller. That is a deliberate choice rather than an oversight: the seconds-valued fields at `dsh-schedule` and the DeepSeek file store are the intended next consumers, and the alternative — a class that holds a span but can only split it — would have to grow the same members one call site at a time. The package README records the un-migrated boundaries as a known limitation. -The elapsed reading keeps its split in the library and its wording in the consumer: `dsh-time-context` still decides that a backward clock reading clamps to zero and how the compact line reads, while `Duration.decompose()` answers the whole parts. That swap is byte-identical for every finite sample, measured over more than eight thousand inputs including negatives, fractions, and values past a year. That measurement is not a committed test: the consumer's own suite pins the readings it renders (`1d 1h 1m 1s`, `1m 1s`, `4m 2s`, and the clamped zero). Two samples do change: `NaN` rendered `NaNs` and `+Infinity` rendered `Infinityd NaNs`, and both now throw from the span constructor; the package's own reading invariant already refused those two renderings, so the failure moves to the sample that produced it. A `-Infinity` sample is unchanged, because the consumer clamps it to zero before the span is built. +The elapsed reading keeps its split in the library and its wording in the consumer: `dsh-time-context` still decides that a backward clock reading clamps to zero and how the compact line reads, while `Duration.decompose()` answers the whole parts. That swap is byte-identical for every finite sample, measured over more than eight thousand inputs including negatives, fractions, and values past a year. That measurement is not a committed test: the consumer's own suite pins the readings it renders (`1d 1h 1m 1s`, `1m 1s`, `4m 2s`, and the clamped zero). Two samples do change: `NaN` rendered `NaNs` and `+Infinity` rendered `Infinityd NaNs`, and both now throw from the span constructor, so a reading that is not a finite number fails where it is produced instead of rendering text. A `-Infinity` sample is unchanged, because the consumer clamps it to zero before the span is built. -Time-context keeps the two policies this change did not touch: its ISO-shaped timestamp rendering in `packages/context/time-context/src/timestamp.ts`, which is that package's durable reading format, and the elapsed-duration wording in its own plugin. The browser client keeps its own localized date rendering and its own UTC-offset reading, which serve a different purpose (a reader's locale) from a boundary value; unifying those means deciding whether the shared library should also own display formatting, and which package then owns the durable reading format its invariant validates. `packages/session/session-format-v0-to-v1/src/payload-validation.ts` also validates the canonical four-digit-year UTC instant with its own regular expression and round trip, which is a remaining copy of the instant profile this change did not touch. +Time-context keeps the two policies this change did not touch: its ISO-shaped timestamp rendering in `packages/context/time-context/src/timestamp.ts`, which is that package's durable reading format, and the elapsed-duration wording in its own plugin. The browser client keeps its own localized date rendering and its own UTC-offset reading, which serve a different purpose (a reader's locale) from a boundary value; unifying those means deciding whether the shared library should also own display formatting, and which package then owns the durable reading format. `packages/session/session-format-v0-to-v1/src/payload-validation.ts` also validates the canonical four-digit-year UTC instant with its own regular expression and round trip, which is a remaining copy of the instant profile this change did not touch. diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md index 44d867a15e..fab2092c09 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md @@ -46,6 +46,6 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec 该值类型在第二个消费方出现之前就交付了完整操作集合。目前只有 `fromMilliseconds` 与 `decompose()` 有生产调用方,即 `dsh-time-context`;`fromSeconds`、`milliseconds`、`plus`、`minus`、`compare`、`isNegative`、`zero` 与四个 `to*` 换算都没有调用方。这是有意的选择而不是疏漏:`dsh-schedule` 与 DeepSeek 文件存储里以秒为单位的字段是预期的下一批消费方,而另一种做法——一个能保存时间段却只能拆解它的类——会在每个调用点出现时再长出同样的成员。包 README 把这些尚未迁移的边界记为已知限制。 -耗时读数的拆分上收到库、措辞留在一侧:`dsh-time-context` 仍然决定回退的时钟读数钳到零、以及那行紧凑文本怎么写,而 `Duration.decompose()` 回答整数量。这次替换对每个有限样本都逐字不变,用一份覆盖八千多个输入(含负数、小数与一年以上的值)的差分测得。该测量不是提交进仓库的测试:消费方自己那套测试固定了它渲染出的读数(`1d 1h 1m 1s`、`1m 1s`、`4m 2s`,以及被钳到零的那次)。有两个样本会变:`NaN` 原本渲染出 `NaNs`、`+Infinity` 原本渲染出 `Infinityd NaNs`,现在两者都由时间段构造函数抛出;这两种渲染本来就被该包自己的读数不变量拒绝,所以失败点前移到了产生它的那次采样。`-Infinity` 样本不变,因为消费方在构造时间段之前已把它钳到零。 +耗时读数的拆分上收到库、措辞留在一侧:`dsh-time-context` 仍然决定回退的时钟读数钳到零、以及那行紧凑文本怎么写,而 `Duration.decompose()` 回答整数量。这次替换对每个有限样本都逐字不变,用一份覆盖八千多个输入(含负数、小数与一年以上的值)的差分测得。该测量不是提交进仓库的测试:消费方自己那套测试固定了它渲染出的读数(`1d 1h 1m 1s`、`1m 1s`、`4m 2s`,以及被钳到零的那次)。有两个样本会变:`NaN` 原本渲染出 `NaNs`、`+Infinity` 原本渲染出 `Infinityd NaNs`,现在两者都由时间段构造函数抛出,于是非有限的读数在产生它的地方失败,而不是渲染出一段文本。`-Infinity` 样本不变,因为消费方在构造时间段之前已把它钳到零。 -本次未触及、仍归 time-context 的两项:`packages/context/time-context/src/timestamp.ts` 中的 ISO 形状时间戳渲染(那是该包的持久读数字格式),以及其插件内的耗时措辞。浏览器客户端也保留自己的本地化日期渲染与自己的 UTC 偏移读取——它们服务的目的(读者的区域设置)与边界值不同;统一它们意味着先决定共享库是否也要拥有展示格式化,以及届时由谁拥有其不变量所校验的持久读数字格式。`packages/session/session-format-v0-to-v1/src/payload-validation.ts` 也用自己的一段正则与往返比对校验规范的四位年份 UTC 时刻,这是本次未触及、仍然存在的时刻剖面副本。 +本次未触及、仍归 time-context 的两项:`packages/context/time-context/src/timestamp.ts` 中的 ISO 形状时间戳渲染(那是该包的持久读数字格式),以及其插件内的耗时措辞。浏览器客户端也保留自己的本地化日期渲染与自己的 UTC 偏移读取——它们服务的目的(读者的区域设置)与边界值不同;统一它们意味着先决定共享库是否也要拥有展示格式化,以及届时由谁拥有持久读数字格式。`packages/session/session-format-v0-to-v1/src/payload-validation.ts` 也用自己的一段正则与往返比对校验规范的四位年份 UTC 时刻,这是本次未触及、仍然存在的时刻剖面副本。 diff --git a/packages/util/time/README.i18n.yaml b/packages/util/time/README.i18n.yaml index 66374badf0..efc4bd77f1 100644 --- a/packages/util/time/README.i18n.yaml +++ b/packages/util/time/README.i18n.yaml @@ -18,8 +18,8 @@ en: bbff250eba0f1b89 zh: 0a618f05cc88ee2b /dsh-util-time/use-this-package/when-to-use-it: - en: 7015b3ed33de4c53 - zh: d4fc9aee6f6910ca + en: 85291d5fb4c9000b + zh: d1717c3b01efb5d0 /dsh-util-time/use-this-package/entry-point: en: 88668c3dd1bdb75b zh: fb48d941ae433335 diff --git a/packages/util/time/README.md b/packages/util/time/README.md index 01813d8b4b..50976eb86f 100644 --- a/packages/util/time/README.md +++ b/packages/util/time/README.md @@ -31,7 +31,6 @@ It is a **library, not a service or plugin**: no `ctx`, registers nothing, holds Call it at the boundary that receives the zone or the wall-clock selector, before the value reaches anything durable. `dsh-api-session-controller` and `dsh-subagent` admit a caller-reported zone through `canonicalClientTimeZone`; `dsh-time-context` admits the zone a request turn reports through `canonicalizeTimeZone` and refuses an alias rather than rewriting it; `dsh-schedule` reads every absolute and wall-clock selector through the strict parsers and resolves its local targets through `resolveLocalInstant`. `dsh-time-context` reads its elapsed reading through `Duration` and keeps the wording of the line it injects. A consumer that owns calendar or interval arithmetic of its own keeps that arithmetic and calls these entries only for the boundary value. -No runtime invariant companion is published because this pure utility owns no event stream or mutable runtime data; unit tests verify every entry. ### Entry point diff --git a/packages/util/time/README.zh.md b/packages/util/time/README.zh.md index 3302a38ee8..642f921618 100644 --- a/packages/util/time/README.zh.md +++ b/packages/util/time/README.zh.md @@ -31,7 +31,6 @@ kind: "package-library" 在接收时区或墙钟选择器的那个边界上调用它,让值在进入任何持久物之前先过一遍。`dsh-api-session-controller` 与 `dsh-subagent` 通过 `canonicalClientTimeZone` 接收调用方所报时区;`dsh-time-context` 通过 `canonicalizeTimeZone` 接收某一轮请求所报时区,并拒绝别名而不是把它改写;`dsh-schedule` 通过严格解析器读取每一个绝对选择器与墙钟选择器,并通过 `resolveLocalInstant` 解析其本地目标。`dsh-time-context` 通过 `Duration` 读取其耗时读数,并保留它注入那一行的措辞。自行拥有日历或间隔运算的消费方保留那部分运算,只在边界值上调用这些入口。 -不发布运行时不变量伴生入口;这个纯工具不拥有事件流或可变运行时数据,每个入口都由单元测试验证。 ### 入口 diff --git a/packages/util/time/src/index.ts b/packages/util/time/src/index.ts index bbbd316134..e143295709 100644 --- a/packages/util/time/src/index.ts +++ b/packages/util/time/src/index.ts @@ -2,8 +2,8 @@ * Time vocabulary shared by the boundaries that accept a caller's zone or a * wall-clock selector, plus one span type for elapsed and interval arithmetic. * - * Validation, canonicalization, and strict parsing only: this library formats - * no model-facing text and owns no failure vocabulary. A refusal that carries a + * The library validates, canonicalizes, parses, and renders one canonical instant + * format; it produces no model-facing text and owns no failure vocabulary. A refusal that carries a * reason is a discriminated result, and each caller maps that reason onto its * own domain code and diagnostic. `canonicalClientTimeZone` is the wire-boundary * form and answers a name or `undefined`; `formatFourDigitYearInstant` and From 7d640365b63bbd7ee244d1490efa0f1a3f12d544 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Sun, 27 Sep 2026 19:57:04 +0800 Subject: [PATCH 019/369] feat(subagent): share fixed model-selection tools across sessions --- ...authorized-subagent-model-routes.i18n.yaml | 12 +- ...4-user-authorized-subagent-model-routes.md | 12 +- ...ser-authorized-subagent-model-routes.zh.md | 12 +- apps/cli/composition.md | 3 + .../tests/profiles/sdk/keyless-smoke.e2e.ts | 2 +- apps/cli/tests/web-agent-presets.e2e.ts | 12 +- docs/config-catalog.md | 2 +- docs/config-catalog.zh.md | 2 +- docs/event-producer-consumer.md | 2 +- docs/event-producer-consumer.zh.md | 2 +- docs/persistence-catalog.md | 6 +- docs/persistence-catalog.zh.md | 6 +- docs/tool-catalog.i18n.yaml | 12 +- docs/tool-catalog.md | 22 ++- docs/tool-catalog.zh.md | 22 ++- packages/bundle/base/cordis.patch.yml | 8 +- packages/bundle/web-app/cordis.patch.yml | 3 + .../bundle/web-app/presets/cordis.patch.yml | 2 + packages/bundle/web-app/presets/ptc.patch.yml | 2 + .../bundle/web-app/presets/standard.patch.yml | 2 + .../subagent/tool-subagent/README.i18n.yaml | 28 ++-- packages/subagent/tool-subagent/README.md | 17 +-- packages/subagent/tool-subagent/README.zh.md | 17 +-- packages/subagent/tool-subagent/package.json | 7 +- packages/subagent/tool-subagent/src/index.ts | 81 ++++------- .../subagent/tool-subagent/src/list-models.ts | 47 ++++-- .../src/model-selection-state.ts | 18 +-- .../tool-subagent/src/model-selection.ts | 11 +- .../subagent/tool-subagent/tests/harness.ts | 5 +- .../tool-subagent/tests/list-models.spec.ts | 52 +++++-- .../tests/model-selection-settings.spec.ts | 112 +++++++++------ .../tests/model-selection.spec.ts | 59 +++++++- .../tool-subagent/tests/tool-subagent.spec.ts | 9 +- .../subagent/tool-subagent/tsdown.config.ts | 1 + scripts/check-workspace-constraints.ts | 5 +- scripts/gen-tool-catalog.ts | 6 +- .../advanced/result.json | 6 + .../advanced/session.1.v4.jsonl | 2 +- .../advanced/session.2.v4.jsonl | 2 +- .../advanced/session.v4.jsonl | 4 +- .../scheduler-recovery/session.v4.jsonl | 2 +- .../sdk/bash-tool/tool-schemas.expected.json | 45 +++++- .../notifications.expected.jsonl | 6 +- .../sdk/dynamic-tool-updates/session.v4.jsonl | 6 +- snapshots/sdk/persistent-tools/cordis.yml | 4 + .../tool-schemas.1.expected.json | 45 +++++- .../tool-schemas.1.expected.json | 45 +++++- .../tool-schemas.1.expected.json | 45 +++++- .../tool-schemas.expected.json | 73 +++++++++- .../tool-schemas.1.expected.json | 45 +++++- .../tool-schemas.expected.json | 18 ++- .../tool-schemas.1.expected.json | 45 +++++- .../tool-schemas.1.expected.json | 45 +++++- .../sdk/text-turn/tool-schemas.expected.json | 45 +++++- .../notifications.expected.jsonl | 4 +- .../sdk/tool-error-details/session.v4.jsonl | 4 +- .../system-prompt.expected.md | 24 +++- .../tool-schemas.expected.json | 45 +++++- .../advanced-toolchain/session.v4.jsonl | 8 +- .../system-prompt.expected.md | 24 +++- .../tool-schemas.expected.json | 90 +++++++++++- .../tool-schemas.expected.json | 90 +++++++++++- .../tool-schemas.expected.json | 45 +++++- .../both-mode-turn/system-prompt.expected.md | 24 +++- .../tool-schemas.expected.json | 45 +++++- .../tool-schemas.expected.json | 45 +++++- .../tool-schemas.expected.json | 45 +++++- .../tool-schemas.expected.json | 45 +++++- .../cordis.snapshot.yml | 4 +- .../compaction-output-reserve/cordis.yml | 2 +- .../session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 90 +++++++++++- .../cordis.snapshot.yml | 4 +- .../compaction-summary-headroom/cordis.yml | 2 +- .../session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 45 +++++- .../tool-schemas.expected.json | 45 +++++- .../tool-schemas.expected.json | 45 +++++- .../tool-schemas.expected.json | 45 +++++- .../tool-schemas.expected.json | 45 +++++- .../session.v4.jsonl | 6 +- .../dynamic-tool-updates/session.v4.jsonl | 6 +- .../tool-schemas.expected.json | 135 +++++++++++++++++- .../tool-schemas.expected.json | 45 +++++- .../tool-schemas.expected.json | 45 +++++- .../lsp-definition/tool-schemas.expected.json | 45 +++++- .../system-prompt.expected.md | 24 +++- .../mcp-resources/tool-schemas.expected.json | 45 +++++- .../tool-schemas.expected.json | 45 +++++- .../office-skills/tool-schemas.expected.json | 45 +++++- .../plugin-manager-mcp/session.v4.jsonl | 4 +- .../tool-schemas.expected.json | 90 +++++++++++- .../plugin-manager/tool-schemas.expected.json | 45 +++++- .../tool-schemas.expected.json | 45 +++++- .../tool-schemas.expected.json | 45 +++++- .../tool-schemas.expected.json | 45 +++++- .../provider-cwd/tool-schemas.expected.json | 45 +++++- .../system-prompt.expected.md | 24 +++- .../system-prompt.expected.md | 24 +++- .../ptc-python-turn/system-prompt.expected.md | 25 +++- .../ptc-read-image/system-prompt.expected.md | 24 +++- .../ptc-turn/system-prompt.expected.md | 24 +++- .../tool-schemas.expected.json | 45 +++++- .../ralph-loop/tool-schemas.1.expected.json | 45 +++++- .../ralph-loop/tool-schemas.2.expected.json | 45 +++++- .../ralph-loop/tool-schemas.expected.json | 45 +++++- .../tool-schemas.expected.json | 45 +++++- .../tool-schemas.expected.json | 45 +++++- .../tool-schemas.expected.json | 45 +++++- .../tool-schemas.1.expected.json | 45 +++++- .../tool-schemas.1.expected.json | 45 +++++- .../tool-schemas.expected.json | 45 +++++- .../tool-schemas.1.expected.json | 45 +++++- .../tool-schemas.expected.json | 45 +++++- .../team-targets/tool-schemas.expected.json | 17 +++ .../text-turn/tool-schemas.expected.json | 45 +++++- .../web-fetch/tool-schemas.expected.json | 45 +++++- .../tool-schemas.expected.json | 45 +++++- .../tool-schemas.expected.json | 45 +++++- .../worktree/tool-schemas.expected.json | 45 +++++- tsconfig.base.json | 1 + 121 files changed, 3226 insertions(+), 411 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml index d4f615792e..c5c1287e45 100644 --- a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml @@ -9,14 +9,14 @@ en: 3238dfb7db7dfdd8 zh: ec3bdd9a9da0a321 /agent-note-user-authorized-subagent-model-routes/decision: - en: 0166e452a774ade5 - zh: 747061b9424b3610 + en: 499721e05ad15d9c + zh: 2bcf5ade8910da8e /agent-note-user-authorized-subagent-model-routes/alternatives-considered: en: e41c17226190ea8b zh: d3b1395fafe75f01 /agent-note-user-authorized-subagent-model-routes/consequences: - en: f1927d31ffc1dee9 - zh: b0fed04b80043c71 + en: 29ca812656549a01 + zh: aacd814d7d649dbf /agent-note-user-authorized-subagent-model-routes/related-decisions: - en: 595a9485d91592c6 - zh: 7b32611ea380498c + en: 17fdfabedb673935 + zh: 496f4f85622d7d15 diff --git a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md index e7f3b4fcae..3e2ab42e5a 100644 --- a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md +++ b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md @@ -12,11 +12,11 @@ Registering an LLM adapter makes its routes reachable, but does not authorize an The Host-owned `subagent-model-selection` settings section stores an explicit `enabled` switch and `allowedModels`, an array of exact `{ provider, model }` routes. Enabling requires at least one route; disabling may retain the selected routes for later reuse. The Plugins settings card reads the live adapter directory through `session/modelCatalog`, lets the user stage the switch and routes, and saves both fields in one revision-fenced settings mutation. It stores no adapter-owned display names, descriptions, or reasoning-effort metadata. A stored or staged route absent from the current directory remains visible as unavailable and removable; a provider-local catalog failure does not block other providers or erase saved authorization or an unsaved selection. A connection reset discards the draft because namespace revisions are comparable only within one Host process. -A newly composed top-level Session snapshots the route list in `subagent/model-selection-policy` when the setting is enabled, before its model-selectable definitions can reach a request. Event presence means selection was enabled; the event does not store the global switch. Child Sessions inherit that exact list from their live parent, and resumed Sessions use the recorded event instead of current settings. Settings changes therefore affect only subsequently composed top-level Sessions, while a legacy Session without the event remains disabled, including an explicitly empty restored Session. +A newly composed top-level Session snapshots the route list in `subagent/model-selection-policy` when the setting is enabled, before its first model request. Event presence means selection was enabled; the event does not store the global switch. Child Sessions inherit that exact list from their live parent, and resumed Sessions use the recorded event instead of current settings. Settings changes therefore affect only subsequently composed top-level Sessions. Without the event, including an explicitly empty restored Session, explicit selection remains restricted to the current parent route. -The fixed `list_subagent_models` schema does not enumerate the policy. At call time, provider and model listings are the intersection of the Session route list and the adapter's live advertised directory. An exact provider/model lookup first requires authorization, then resolves the adapter-owned model metadata and all advertised reasoning efforts. The delegation executor independently rejects any explicit provider, model, or effort selection whose effective provider/model route is outside the Session list before `resolveCallConfig()` validates adapter availability and effort support. A call that supplies no selection field retains configured or inherited routing because the model made no route choice. +The fixed `list_subagent_models` schema does not enumerate the policy. At call time, it lists authorized registered providers and resolves their allowed exact model ids, including ids absent from advisory catalogs. Exact lookup requires authorization before resolving model metadata and all supported reasoning efforts. Without a recorded policy, discovery uses the parent Agent’s current effective route. The delegation executor independently checks any explicit provider, model, or effort selection against the same authorization before adapter preflight. A call with no selection fields retains configured or inherited routing. -Model selection has no unrestricted static mode. The default-off Host setting is the only authority, and an enabled Session always carries an exact allowlist. The primary spawn tool reads that setting; the shipped fork tool still exposes no route selection so inherited conversation prefixes remain eligible for provider-side KV Cache reuse. +Model selection has no unrestricted static mode. The default-off Host setting authorizes alternative child routes; without it, the parent route and its supported efforts remain selectable. The primary spawn tool captures the setting once, while all capable delegation tools and shared discovery read the same Session policy. Fork defaults preserve the parent route, and its description warns about prefix recomputation when that route changes. ## Alternatives considered @@ -33,11 +33,11 @@ Model selection has no unrestricted static mode. The default-off Host setting is ## Consequences - New adapter registrations and newly advertised models do not expand user authorization. -- Adapter removals or catalog failures can reduce what discovery currently lists without deleting the saved route decision; an exact authorized route remains usable when its adapter accepts it even if the advisory catalog omits it. +- Adapter removal or exact-model resolution failure can make discovery unavailable without deleting the saved route decision; an allowed model remains usable when its adapter accepts it even if the advisory catalog omits it. - The allowlist itself consumes no parent-request tokens. Only a `list_subagent_models` result enters the transcript. - The policy event is log-only and is appended while an Agent is composed, before either SDK begins its run subscription. Shipped SDK profiles do not enable this Web-owned preference, so the event changes neither SDK's expected notifications or persisted-session output; package restore tests own its durable projection instead of fabricating an SDK composition solely to emit it. -- Unit coverage pins settings validation, malformed durable values, Session sampling and inheritance, discovery intersection, executor denial, live UI catalog invalidation, staged-route retention, connection-generation invalidation, staged whole-array writes, stale-revision rejection, and retry after scoped installation failure. The assembled Web scenario pins the real settings document and Plugins card flow. +- Unit coverage pins settings validation, malformed durable values, Session sampling and inheritance, shared definitions, parent-route defaults, discovery restrictions, executor denial, and the settings UI’s staged writes and revision checks. The assembled Web scenario pins the real settings document and Plugins card flow. ## Related decisions -The route arguments, adapter preflight, discovery tool, and fork cache restriction remain owned by [model-selected subagent routes](../../../../packages/subagent/tool-subagent/README.md). +The route arguments, adapter preflight, discovery tool, and fork cache costs remain owned by [model-selected subagent routes](../../../../packages/subagent/tool-subagent/README.md). diff --git a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md index f55f60de1b..d90bca9358 100644 --- a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md +++ b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md @@ -12,11 +12,11 @@ Status: implemented Host 自有的 `subagent-model-selection` 设置 section 保存显式 `enabled` 开关与 `allowedModels`,后者是由精确 `{ provider, model }` 路由组成的数组。启用时必须至少有一条路由;关闭时可以保留已选路由,供以后重新启用。Plugins 设置卡通过 `session/modelCatalog` 读取实时适配器目录,让用户暂存开关与路由,再在一次带 revision 限制的设置 mutation 中保存两个字段。它不保存适配器自有的显示名称、描述或推理强度元数据。当前目录中缺失的已存或暂存路由仍显示为不可用并允许移除;某个提供方的目录失败不会阻塞其他提供方,也不会清除已存授权或未保存选择。连接重置会丢弃草稿,因为 namespace revision 只能在同一个 Host 进程内比较。 -设置启用时,新组合的顶层 Session 会在模型可选定义进入请求之前,把路由列表快照记录为 `subagent/model-selection-policy`。事件存在就表示模型选择已启用;事件不保存全局开关。子 Session 从在线父级继承同一份精确列表,恢复的 Session 使用已记录事件而不是当前设置。因此,设置修改只影响之后组合的顶层 Session,而没有该事件的旧 Session 仍保持禁用,包括显式为空的恢复 Session。 +设置启用时,新组合的顶层 Session 在首个模型请求前将路由列表记录为 `subagent/model-selection-policy`。事件存在表示选择已启用;事件不保存全局开关。子 Session 从在线父级继承同一精确列表,恢复的 Session 使用已记录事件而非当前设置。因此设置变更只影响之后组合的顶层 Session。没有该事件时,包括显式为空的恢复 Session,显式选择仍仅限当前父级路由。 -固定的 `list_subagent_models` schema 不会枚举该策略。调用时,提供方和模型列表是 Session 路由列表与适配器实时公布目录的交集。精确 provider/model 查询先要求授权,再解析适配器自有的模型元数据和全部已公布推理强度。委派执行器还会独立拒绝任何生效 provider/model 路由不在 Session 列表内的显式提供方、模型或强度选择,然后才由 `resolveCallConfig()` 校验适配器可用性与强度支持。完全没有选择字段的调用保留配置或继承路由,因为模型没有作出路由选择。 +固定的 `list_subagent_models` schema 不枚举策略。调用时,它列出已授权且已注册的提供方,并解析允许的精确模型 ID,包括建议性目录未列出的 ID。精确查询先要求授权,再解析模型元数据及全部支持的推理强度。没有已记录策略时,发现使用父 Agent 当前生效的路由。委派执行器会在适配器预检前独立校验显式提供方、模型或强度选择是否符合相同授权。没有选择字段的调用保留配置或继承的路由。 -模型选择不再有无限制的静态模式。默认关闭的 Host 设置是唯一授权来源,启用的 Session 始终携带精确允许列表。主 spawn 工具读取该设置;随附 fork 工具仍不公开路由选择,使继承的对话前缀继续符合提供方侧 KV Cache 复用条件。 +模型选择没有无限制的静态模式。默认关闭的 Host 设置授权其他子级路由;没有该授权时,仍可选择父级路由及其支持的强度。主 spawn 工具一次性记录设置,所有支持选择的委派工具和共享发现均读取同一 Session 策略。fork 默认保留父级路由,其描述会提示改变路由时的前缀重算成本。 ## Alternatives considered @@ -33,11 +33,11 @@ Host 自有的 `subagent-model-selection` 设置 section 保存显式 `enabled` ## Consequences - 新适配器注册和新公布模型不会扩大用户授权。 -- 适配器移除或目录失败可以减少发现当前列出的内容,但不会删除已存路由决定;即使建议性目录省略某条精确已授权路由,只要适配器接受它,该路由仍然可用。 +- 适配器移除或精确模型解析失败可能使发现不可用,但不会删除已存路由决定;只要适配器接受某个获准模型,即使建议性目录省略它,该模型仍可使用。 - 允许列表本身不消耗父级请求 token。只有 `list_subagent_models` 结果进入 transcript。 - 策略事件仅存在于日志,并在 Agent 组合期间、两套 SDK 开始订阅运行前追加。随附 SDK profile 不启用这项 Web 自有偏好,因此该事件不会改变任一 SDK 的预期通知或持久 Session 输出;其持久投影由包级恢复测试负责,不会为了发出该事件而虚构 SDK 组合。 -- 单元覆盖固定设置校验、异常持久值、Session 取样与继承、发现交集、执行器拒绝、UI 实时目录失效、暂存路由保留、连接换代失效、暂存后的整数组写入、陈旧 revision 拒绝,以及作用域安装失败后的重试。组装 Web 场景固定真实设置文档与 Plugins 设置卡流程。 +- 单元覆盖固定设置校验、异常持久值、Session 取样与继承、共享定义、父级路由默认值、发现限制、执行器拒绝,以及设置 UI 的暂存写入与 revision 检查。组装 Web 场景固定真实设置文档与 Plugins 设置卡流程。 ## Related decisions -路由参数、适配器预检、发现工具与 fork 缓存限制仍由[模型选择的 subagent 路由](../../../../packages/subagent/tool-subagent/README.zh.md)负责。 +路由参数、适配器预检、发现工具与 fork 缓存成本仍由[模型选择的 subagent 路由](../../../../packages/subagent/tool-subagent/README.zh.md)负责。 diff --git a/apps/cli/composition.md b/apps/cli/composition.md index 9888602296..f2bae814f4 100644 --- a/apps/cli/composition.md +++ b/apps/cli/composition.md @@ -144,6 +144,8 @@ flowchart LR cfg --> plugin_dsh_base_subagent_fork_in_process plugin_dsh_base_tool_subagent_control["tool-subagent-control
@deepseek-ai/dsh-tool-subagent-control"] cfg --> plugin_dsh_base_tool_subagent_control + plugin_dsh_base_tool_subagent_list_models["tool-subagent-list-models
@deepseek-ai/dsh-tool-subagent/list-models"] + cfg --> plugin_dsh_base_tool_subagent_list_models plugin_dsh_base_tool_subagent_list_agents["tool-subagent-list-agents
@deepseek-ai/dsh-tool-subagent-control/list-agents"] cfg --> plugin_dsh_base_tool_subagent_list_agents plugin_dsh_base_tool_subagent["tool-subagent
@deepseek-ai/dsh-tool-subagent"] @@ -268,6 +270,7 @@ flowchart LR | `subagent-spawn-in-process` | `@deepseek-ai/dsh-subagent-spawn-in-process` | | `subagent-fork-in-process` | `@deepseek-ai/dsh-subagent-fork-in-process` | | `tool-subagent-control` | `@deepseek-ai/dsh-tool-subagent-control` | +| `tool-subagent-list-models` | `@deepseek-ai/dsh-tool-subagent/list-models` | | `tool-subagent-list-agents` | `@deepseek-ai/dsh-tool-subagent-control/list-agents` | | `tool-subagent` | `@deepseek-ai/dsh-tool-subagent` | | `tool-subagent-fork` | `@deepseek-ai/dsh-tool-subagent` | diff --git a/apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts b/apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts index 64c1c20ab3..f804cde4fa 100644 --- a/apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts +++ b/apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts @@ -178,7 +178,7 @@ describe('Python SDK dsh profile keyless smoke', () => { expect(modelRequests[0]?.max_tokens).toBe(1234) expect(toolNames).toEqual(expect.arrayContaining(['read', 'write', 'edit', 'web_fetch', 'web_search'])) expect(toolNames.includes('str_replace_editor')).toBe(editorEnabled) - expect(toolNames).not.toContain('list_subagent_models') + expect(toolNames).toContain('list_subagent_models') child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'shutdown' })}\n`) const shutdown = await waitForLine(lines, value => value.id === 3, () => stderr) diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts index db0d8e26e3..bb82f5349c 100644 --- a/apps/cli/tests/web-agent-presets.e2e.ts +++ b/apps/cli/tests/web-agent-presets.e2e.ts @@ -254,7 +254,7 @@ describe('the shipped Web composition', () => { // depend on ripgrep being present on the machine. expect(toolNames(ctx, handle.agent).filter(name => name !== 'glob' && name !== 'grep')).toEqual([ 'ask_user_question', 'bash', 'create_goal', 'edit', 'exit_plan_mode', - 'get_goal', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', 'list_agents', 'present', 'read', 'read_image', + 'get_goal', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', 'list_agents', 'list_subagent_models', 'present', 'read', 'read_image', 'schedule_create', 'schedule_delete', 'schedule_list', 'schedule_update', 'send_message', 'skill', 'subagent', 'subagent_fork', 'todo_write', 'update_goal', 'web_fetch', 'web_search', @@ -284,15 +284,19 @@ describe('the shipped Web composition', () => { setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'standard').then(() => undefined), }) try { - expect(toolNames(ctx, disabled.agent)).not.toContain('list_subagent_models') - expect(toolParameterNames(ctx, disabled.agent, 'subagent')).not.toEqual(expect.arrayContaining([ + expect(toolNames(ctx, disabled.agent)).toContain('list_subagent_models') + expect(toolParameterNames(ctx, disabled.agent, 'subagent')).toEqual(expect.arrayContaining([ 'model', 'provider', 'reasoning_effort', ])) + expect(ctx.sessionProjections.stateOf(disabled.agent.session, 'subagentModelSelectionPolicy')).toBeNull() + expect(ctx.sessionProjections.stateOf(enabled.agent.session, 'subagentModelSelectionPolicy')).toEqual([ + { provider: 'deepseek-official', model: 'deepseek-v4-flash' }, + ]) expect(toolNames(ctx, enabled.agent)).toContain('list_subagent_models') expect(toolParameterNames(ctx, enabled.agent, 'subagent')).toEqual(expect.arrayContaining([ 'model', 'provider', 'reasoning_effort', ])) - expect(toolNames(ctx, disabled.agent)).not.toContain('list_subagent_models') + expect(toolNames(ctx, disabled.agent)).toContain('list_subagent_models') } finally { await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, { enabled: false }) await enabled.dispose() diff --git a/docs/config-catalog.md b/docs/config-catalog.md index b091fa4da4..775ece7747 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -3916,7 +3916,7 @@ export interface Config { - `inject`: `tools` · `subagents` · `systemPrompt` · `sessionProjections` - `refs`: [`AgentOptions`](subsystems/core.md) -- `source`: [`packages/subagent/tool-subagent/src/index.ts:48`](../packages/subagent/tool-subagent/src/index.ts) +- `source`: [`packages/subagent/tool-subagent/src/index.ts:47`](../packages/subagent/tool-subagent/src/index.ts) ```ts config-catalog /** Config: which registered provider this tool delegates to, plus child defaults. */ diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 399101c45c..370b740e74 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -3918,7 +3918,7 @@ export interface Config { - `inject`: `tools` · `subagents` · `systemPrompt` · `sessionProjections` - `refs`: [`AgentOptions`](subsystems/core.zh.md) -- `source`: [`packages/subagent/tool-subagent/src/index.ts:48`](../packages/subagent/tool-subagent/src/index.ts) +- `source`: [`packages/subagent/tool-subagent/src/index.ts:47`](../packages/subagent/tool-subagent/src/index.ts) ```ts config-catalog /** Config: which registered provider this tool delegates to, plus child defaults. */ diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index d070d6fd23..c36cd9801d 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -12,7 +12,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:69`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` | | `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../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:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`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), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | -| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`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`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`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:393`](../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:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) | | `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index 4d62fcfea2..2cc429dc63 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -14,7 +14,7 @@ | `agent-preset/selected` | `emit` | [`packages/preset/agent-preset-registry/src/types.ts:69`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` | | `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../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:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`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), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | -| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`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`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:270`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `claude-code-mods`, [`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:393`](../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:299`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs), [`user-questions`](../packages/interaction/user-questions) | | `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:307`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`user-questions`](../packages/interaction/user-questions) | diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 7de9efa156..0d3b9ea113 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -928,9 +928,9 @@ Source: [`packages/subagent/subagent/src/descriptor.ts:38`](../packages/subagent ```ts persistence-catalog /** - * Records that this session's delegation tool exposes child provider, - * model, and reasoning-effort selection. Appended before the first model - * request; absence means the fixed-route definition. Log-only: it carries + * Records this Session’s authorized child provider/model routes. Appended + * before the first model request; absence restricts explicit selection + * to the current parent route. Log-only: it carries * no `surfaceOp` and never enters model history. */ 'subagent/model-selection-policy': { diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 3d75f6ccfb..f48ef72c71 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -930,9 +930,9 @@ export type SessionEvent = { ```ts persistence-catalog /** - * Records that this session's delegation tool exposes child provider, - * model, and reasoning-effort selection. Appended before the first model - * request; absence means the fixed-route definition. Log-only: it carries + * Records this Session’s authorized child provider/model routes. Appended + * before the first model request; absence restricts explicit selection + * to the current parent route. Log-only: it carries * no `surfaceOp` and never enters model history. */ 'subagent/model-selection-policy': { diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml index 2ab2f56624..c6d4e1c326 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: e18a3e3c916afb23 - zh: 4284b50a5e9bd12f + en: 6f5f5f987c218948 + zh: 018ff71df684009c /tool-schema-catalog/deepseek-ai-dsh-plugin-manager: en: e0143916bb634762 zh: e0143916bb634762 @@ -225,11 +225,11 @@ en: 841abe0dd5913ea7 zh: 841abe0dd5913ea7 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent/list-subagent-models: - en: c89084b31ffccbce - zh: d7b3b7000f2d2be9 + en: aba834ab4f70005a + zh: 142f819e8da5acc7 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent/subagent: - en: 9270966e03a2a62b - zh: 0544ac614aed3144 + en: c272328d36a5afaf + zh: fcf69fd0efd55ea9 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control: en: ab82e130c3413f04 zh: ab82e130c3413f04 diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index 8aa9fb0096..6b075043d7 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -37,7 +37,7 @@ This table connects model-visible tool names to the plugin package and service s | `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`, `ctx.workflowEngine`, `ctx.subagents`, `ctx.systemPrompt`, `a calling Agent (exec.agent parents every fresh round)` | `tool/call`, `tool/result`, `workflow and child session events during execution` | - | A fixed foreground workflow starts one fresh structured child per round; the model selects only the immutable objective and an optional round cap. | | `@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` | - | 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-tool-subagent` | `list_subagent_models`, `subagent` | `ctx.tools`, `ctx.subagents`, `ctx.systemPrompt`, `ctx.llm for model discovery and selected-route validation` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion available in an enabled Session. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` remains fixed-route. Each instance selects its backend through `provider` and reads model-selection settings through `modelSelectionSettings`. 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.llm for model discovery and selected-route validation` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered delegation name is the load-time `toolName` config (default `subagent`); backends supporting agentOptions always expose optional route fields, and the shared discovery plugin resolves the calling Session’s allowed routes. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` uses that same policy, and without an enabled policy explicit selection and discovery are restricted to the current parent route. Each instance selects its backend through `provider` and reads model-selection settings through `modelSelectionSettings`. 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-control` | `interrupt_agent`, `list_agents`, `send_message` | `ctx.tools`, `ctx.subagents`, `ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`, `tool/result`, `child session events through ctx.subagents` | - | The globally named discovery, interruption, and local-message tools over managed subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` and `interrupt_agent` once, plus `list_agents` from its separately loaded `/list-agents` plugin (whose catalog rows use the sessionProjections and live Agent registries). | | `@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-experimental-tool-agent-team` | `interrupt_agent`, `list_agents`, `send_message`, `spawn_teammate`, `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update`, `wait_agent` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `an exact live Team member Agent` | `tool/call`, `team/member`, `team/task`, `agent/inbox/spliced`, `user/message on inbox claim`, `tool/result` | - | All nine tools are scoped to implicit Team Leads and durable teammates. The shipped dsh-base bundle keeps the package disabled; the documented Agent Teams profile patch enables it while disabling the legacy continuable-child control names. | @@ -2017,7 +2017,7 @@ The five read-only tools hide provider cursors and authorize every result from t ### `list_subagent_models` -Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. +Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. ```json { @@ -2029,7 +2029,7 @@ Discover LLM routes for subagents without changing the current Agent. Call with }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's advertised models." + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." } } } @@ -2039,7 +2039,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. The runtime notifies you when it finishes. The child reports results with `send_message`; 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 id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. ```json { @@ -2056,6 +2056,18 @@ Delegate a self-contained task to a subagent (a separate agent that works in its "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -2067,7 +2079,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 registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion available in an enabled Session. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` remains fixed-route. Each instance selects its backend through `provider` and reads model-selection settings through `modelSelectionSettings`. Every delegation returns a child id promptly; local children accept follow-ups, while external backends report their single execution automatically. +The registered delegation name is the load-time `toolName` config (default `subagent`); backends supporting agentOptions always expose optional route fields, and the shared discovery plugin resolves the calling Session’s allowed routes. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` uses that same policy, and without an enabled policy explicit selection and discovery are restricted to the current parent route. Each instance selects its backend through `provider` and reads model-selection settings through `modelSelectionSettings`. Every delegation returns a child id promptly; local children accept follow-ups, while external backends report their single execution automatically. diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index b505568dde..c67ea4ecdf 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -41,7 +41,7 @@ | `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`、`ctx.workflowEngine`、`ctx.subagents`、`ctx.systemPrompt`、`a calling Agent (exec.agent parents every fresh round)` | `tool/call`、`tool/result`、`workflow and child session events during execution` | - | 固定的前台工作流会在每个 Round 启动一个全新的结构化子级;模型只能选择不可变目标和可选的 Round 上限。 | | `@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-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述默认 schema 关闭模型选择,而发现 schema 则展示为已启用 Session 中可用的固定配套工具。Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;`subagent_fork` 始终使用固定路由。每个实例通过 `provider` 选择后端,通过 `modelSelectionSettings` 读取模型选择设置。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 | +| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);支持 agentOptions 的后端始终公开可选路由字段,共享发现插件解析调用 Session 允许的路由。Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;`subagent_fork` 使用同一策略,没有启用策略时,显式选择和发现仅限当前父级路由。每个实例通过 `provider` 选择后端,通过 `modelSelectionSettings` 读取模型选择设置。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 | | `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`、`list_agents`、`send_message` | `ctx.tools`、`ctx.subagents`、`ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`、`tool/result`、`child session events through ctx.subagents` | - | 这些是用于发现、中断托管 subagent 和向本地子代理发送消息的全局命名工具:绑定提供方的 `tool-subagent` 实例注册不同的委派工具;本包注册一次 `send_message` 和 `interrupt_agent`,另由 `list_agents` 通过单独加载的 `/list-agents` 插件提供,其目录行使用 sessionProjections 和实时 Agent 注册表。 | | `@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-experimental-tool-agent-team` | `interrupt_agent`、`list_agents`、`send_message`、`spawn_teammate`、`team_task_create`、`team_task_get`、`team_task_list`、`team_task_update`、`wait_agent` | `ctx.tools`、`ctx.systemPrompt`、`ctx.agentTeams`、`an exact live Team member Agent` | `tool/call`、`team/member`、`team/task`、`agent/inbox/spliced`、`user/message on inbox claim`、`tool/result` | - | 这 9 个工具限定于隐式 Team Lead 与持久 teammate 作用域。随产品发布的 dsh-base bundle 默认禁用该包;文档中的 Agent Teams profile patch 会启用它,并禁用旧 continuable child 的同名控制工具。 | @@ -2023,7 +2023,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, ### `list_subagent_models` -发现 subagent 可用的 LLM 路由,不更改当前 Agent。无参数调用会列出已注册提供方;提供 `provider` 时会列出其公布的模型;同时提供 `provider` 和 `model` 时会检查该精确模型及其推理强度。目录条目只提供建议:adapter 可能接受未列出的模型 id。把返回的 id 用于委派工具的 `provider`、`model` 与 `reasoning_effort` 字段。 +发现 subagent 可用的 LLM 路由,不更改当前 Agent。无参数调用会列出已注册提供方;提供 `provider` 时会列出其允许的模型;同时提供 `provider` 和 `model` 时会检查该精确模型及其推理强度。目录条目只提供建议:adapter 可能接受未列出的模型 id。把返回的 id 用于委派工具的 `provider`、`model` 与 `reasoning_effort` 字段。 ```json { @@ -2035,7 +2035,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's advertised models." + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." } } } @@ -2045,7 +2045,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, ### `subagent` -将一项自包含任务委派给 subagent(在自身上下文中工作的独立 agent),用它卸载聚焦且独立的工作,例如研究、限定范围的实现或分析,以免消耗当前对话的上下文。subagent 会返回结果,但不会返回中间步骤。请提供完整、独立的提示词,因为它看不到当前对话。此工具启动独立管理的 subagent 并立即返回其 id。运行时会在任务结束时通知你。子代理通过 `send_message` 汇报结果;你可以使用 `send_message` 在运行时引导它,或在结束后继续对话。 +将一项自包含任务委派给 subagent(在自身上下文中工作的独立 agent),用它卸载聚焦且独立的工作,例如研究、限定范围的实现或分析,以免消耗当前对话的上下文。subagent 会返回结果,但不会返回中间步骤。此工具启动独立管理的 subagent 并立即返回其 id。运行时会在任务结束时通知你。子代理通过 `send_message` 汇报结果;你可以使用 `send_message` 在运行时引导它,或在结束后继续对话。子级 LLM 选择是可选的。省略 `provider`、`model` 和 `reasoning_effort` 会使用配置的子级默认值,并从父 Agent 继承缺失且兼容的值。先通过 `list_subagent_models` 查询路由和强度,再同时提供 `provider` 与 `model`。更改生效路由却不指定强度时,使用所选模型的默认强度。 ```json { @@ -2062,6 +2062,18 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -2073,7 +2085,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, 来源:[`packages/subagent/tool-subagent/src/index.ts`](../packages/subagent/tool-subagent/src/index.ts) -注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述默认 schema 关闭模型选择,而发现 schema 则展示为已启用 Session 中可用的固定配套工具。Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;`subagent_fork` 始终使用固定路由。每个实例通过 `provider` 选择后端,通过 `modelSelectionSettings` 读取模型选择设置。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 +注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);支持 agentOptions 的后端始终公开可选路由字段,共享发现插件解析调用 Session 允许的路由。Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;`subagent_fork` 使用同一策略,没有启用策略时,显式选择和发现仅限当前父级路由。每个实例通过 `provider` 选择后端,通过 `modelSelectionSettings` 读取模型选择设置。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 diff --git a/packages/bundle/base/cordis.patch.yml b/packages/bundle/base/cordis.patch.yml index 5219c2444f..fb913cd92f 100644 --- a/packages/bundle/base/cordis.patch.yml +++ b/packages/bundle/base/cordis.patch.yml @@ -369,6 +369,9 @@ - id: tool-subagent-control name: '@deepseek-ai/dsh-tool-subagent-control' + - id: tool-subagent-list-models + name: '@deepseek-ai/dsh-tool-subagent/list-models' + - id: tool-subagent-list-agents name: '@deepseek-ai/dsh-tool-subagent-control/list-agents' @@ -378,10 +381,9 @@ provider: spawn toolName: subagent - # Fork omits model selection so provider/model stay equal to the parent and - # the inherited history remains eligible for KV Cache reuse. Parent and child + # Fork inherits the parent route by default for KV Cache reuse. Parent and child # share the messaging tools; return guidance follows the inherited history. - # See .agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.md + # See packages/subagent/tool-subagent/README.md # and .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md. - id: tool-subagent-fork name: '@deepseek-ai/dsh-tool-subagent' diff --git a/packages/bundle/web-app/cordis.patch.yml b/packages/bundle/web-app/cordis.patch.yml index ee5201b9c7..ec3815178e 100644 --- a/packages/bundle/web-app/cordis.patch.yml +++ b/packages/bundle/web-app/cordis.patch.yml @@ -542,6 +542,9 @@ - id: tool-subagent-control disabled: true +- id: tool-subagent-list-models + disabled: true + - id: tool-subagent-list-agents disabled: true diff --git a/packages/bundle/web-app/presets/cordis.patch.yml b/packages/bundle/web-app/presets/cordis.patch.yml index 277176f1a0..25b7ddfa0f 100644 --- a/packages/bundle/web-app/presets/cordis.patch.yml +++ b/packages/bundle/web-app/presets/cordis.patch.yml @@ -87,6 +87,8 @@ config: - id: tool-subagent-control name: '@deepseek-ai/dsh-tool-subagent-control' + - id: tool-subagent-list-models + name: '@deepseek-ai/dsh-tool-subagent/list-models' - id: tool-subagent-list-agents name: '@deepseek-ai/dsh-tool-subagent-control/list-agents' - id: tool-subagent diff --git a/packages/bundle/web-app/presets/ptc.patch.yml b/packages/bundle/web-app/presets/ptc.patch.yml index 1aed4cb9e9..22e37cbbee 100644 --- a/packages/bundle/web-app/presets/ptc.patch.yml +++ b/packages/bundle/web-app/presets/ptc.patch.yml @@ -88,6 +88,8 @@ config: - id: tool-subagent-control name: '@deepseek-ai/dsh-tool-subagent-control' + - id: tool-subagent-list-models + name: '@deepseek-ai/dsh-tool-subagent/list-models' - id: tool-subagent-list-agents name: '@deepseek-ai/dsh-tool-subagent-control/list-agents' - id: tool-subagent diff --git a/packages/bundle/web-app/presets/standard.patch.yml b/packages/bundle/web-app/presets/standard.patch.yml index f2a9e080fe..daf8f63591 100644 --- a/packages/bundle/web-app/presets/standard.patch.yml +++ b/packages/bundle/web-app/presets/standard.patch.yml @@ -88,6 +88,8 @@ config: - id: tool-subagent-control name: '@deepseek-ai/dsh-tool-subagent-control' + - id: tool-subagent-list-models + name: '@deepseek-ai/dsh-tool-subagent/list-models' - id: tool-subagent-list-agents name: '@deepseek-ai/dsh-tool-subagent-control/list-agents' - id: tool-subagent diff --git a/packages/subagent/tool-subagent/README.i18n.yaml b/packages/subagent/tool-subagent/README.i18n.yaml index e390d7ddca..41e61ad7ac 100644 --- a/packages/subagent/tool-subagent/README.i18n.yaml +++ b/packages/subagent/tool-subagent/README.i18n.yaml @@ -24,14 +24,14 @@ en: 42673064ba88a211 zh: 8fa26285bd649dc9 /deepseek-ai-dsh-tool-subagent/use-this-package/selecting-a-child-llm: - en: 0d37e3b3957515e4 - zh: ea1a94b2f29e7d9a + en: b4ba9650c228b9b6 + zh: 027833db9aafd752 /deepseek-ai-dsh-tool-subagent/understand-the-implementation: en: 6ce5f749256a7e6c zh: a4f2adb04a70d9cf /deepseek-ai-dsh-tool-subagent/understand-the-implementation/design-concept: - en: 5ceacfed4ea0c648 - zh: 005284534923d854 + en: 4847e2bcdde8e282 + zh: cc833748d954a65e /deepseek-ai-dsh-tool-subagent/understand-the-implementation/activation-ownership: en: ef692c4d17290920 zh: b1d0d3dab011e2cd @@ -54,11 +54,11 @@ en: e4c7376d4f29dbd5 zh: 6b7a9eaf69c9ea53 /deepseek-ai-dsh-tool-subagent/model-experience/tool-schema/what-the-model-sees: - en: 12c872e4264bb6c0 - zh: 20f870a78ebf8d29 + en: c6da697a3965147c + zh: 4f4a48054bcbda88 /deepseek-ai-dsh-tool-subagent/model-experience/tool-schema/token-effect: - en: 02da2a5e0a75fd91 - zh: fd2d8b11386fc83d + en: 6a1a6351a3a88424 + zh: c574e9ee286518ed /deepseek-ai-dsh-tool-subagent/model-experience/tool-schema/kv-cache-effect: en: 46f736c6c389052a zh: a7765541030e9104 @@ -66,11 +66,11 @@ en: a4ca31b9cbacda6b zh: 91ac3e0784a14697 /deepseek-ai-dsh-tool-subagent/model-experience/model-selection-and-discovery/what-the-model-sees: - en: 1c9dd328eaf96683 - zh: 3be88d3b00f51cfa + en: 14c12f23474ee1bf + zh: bd803212b3b0ba82 /deepseek-ai-dsh-tool-subagent/model-experience/model-selection-and-discovery/token-effect: - en: 8137e72f630f0190 - zh: 73b1d9374b6cbccd + en: cfdfe3b1106c3b57 + zh: 3f421f2901d6f7ab /deepseek-ai-dsh-tool-subagent/model-experience/model-selection-and-discovery/kv-cache-effect: en: 7dc47a34d5574fbd zh: bd7bd2a4da2f8ae2 @@ -114,8 +114,8 @@ en: f5d5f97a153f7c70 zh: 3d6bc05b9eadfbc2 /deepseek-ai-dsh-tool-subagent/known-limitations-and-deferred-work: - en: 40e99fb684ccffc4 - zh: 08c6820c7a5343e8 + en: 93d06d9646fe848d + zh: bc86f8bd38b4b61a /deepseek-ai-dsh-tool-subagent/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/subagent/tool-subagent/README.md b/packages/subagent/tool-subagent/README.md index 09fc202ba6..e407e5f7ae 100644 --- a/packages/subagent/tool-subagent/README.md +++ b/packages/subagent/tool-subagent/README.md @@ -36,6 +36,7 @@ Load the subagent service, an in-process or remote backend, and this tool; then ```yaml - name: '@deepseek-ai/dsh-subagent' - name: '@deepseek-ai/dsh-subagent-spawn-in-process' +- name: '@deepseek-ai/dsh-tool-subagent/list-models' - name: '@deepseek-ai/dsh-tool-subagent' config: provider: spawn @@ -64,7 +65,7 @@ Every backend’s completion notice carries its final answer. Local Spawn and Fo ### Selecting a child LLM -Set `modelSelectionSettings: true` to sample the Host's `subagent-model-selection` preference when each fresh top-level Session is composed. A restored Session without a recorded policy remains disabled, including an explicitly empty restore. When enabled, the non-empty exact provider/model route list is recorded in the Session, inherited by child Sessions, and unchanged by later settings edits. The tool then exposes optional `provider`, `model`, and `reasoning_effort` fields and registers the shared `list_subagent_models` tool. This mode requires a backend that advertises `agentOptions`; both in-process backends and DSH SDK support it, while ACP, Codex, and Claude Code reject it rather than ignore it. +Backends that support `agentOptions` always expose optional `provider`, `model`, and `reasoning_effort` fields. Load `@deepseek-ai/dsh-tool-subagent/list-models` once alongside the delegation tools for shared discovery; shipped compositions include it. Without a recorded Session policy, discovery and explicit selection permit only the parent Agent’s current effective provider/model and its supported efforts. Set `modelSelectionSettings: true` to sample the Host’s `subagent-model-selection` preference for fresh top-level Sessions. When enabled, its exact route list is recorded in the Session, inherited by child Sessions, and preserved on restore; later settings edits affect only new Sessions. A restored Session without a policy keeps the parent-route restriction. ACP, Codex, and Claude Code do not support `agentOptions` and reject selection fields. A call supplies `provider` and `model` together, or supplies only an effort when configured, parent, or provider-owned defaults provide the route. Static `provider.agentRouteDefaults`, when present, form the provider/model baseline; tool configuration and model fields overlay it before route-aware effort merging and exact-route preflight. Providers without these defaults use compatible values from the parent's latest logged request, then the parent's creation options before its first request, while retaining the configured `maxTokens`. Changing the route without an explicit effort clears the inherited route-owned effort, so the selected model resolves its default. The live LLM adapter validates the effective route before child creation. Catalog membership remains advisory, so a model can use an unlisted id when its adapter accepts it. @@ -80,7 +81,7 @@ This section explains how the tool mirrors provider lifecycle and settles runs; ### Design concept -One instance is one provider plus one tool name. The plugin mirrors provider lifecycle: it registers the tool when the named provider appears and disposes it when the provider leaves, so sibling load order and HMR replacement cannot strand a dangling tool. Direct Agent setup passes its unpublished Session explicitly and awaits installation before publication. A settings-backed standing preset receives each matching Agent through `agent/created`, selects policy from its Session, and awaits installation through its Context; installation failure rejects creation. A numeric `maxDepth` or configured LLM selection the provider cannot enforce fails the mount instead of the first delegation. At most one instance in a tool scope may own model selection because `list_subagent_models` has a global name. +One instance is one provider plus one tool name. The plugin registers its definition in the owning composition while the provider is present and disposes it when the provider leaves. Agents inherit that shared definition, so their existing tool filters apply without per-Agent registration. Settings-backed instances capture Session authorization during Agent creation; calls read the durable policy independently of registration. Direct Agent setup supplies its unpublished Session explicitly. A numeric `maxDepth` or configured LLM selection unsupported by the provider fails the mount. Discovery has one separate composition entry shared by spawn and fork tools. No runtime invariant companion is published: policy comes directly from the Session projection and is checked at execution, with no separately retained per-Agent definition state to reconcile. ### Activation ownership @@ -128,11 +129,11 @@ Read these pages when the package-level contract is not enough; they move from t #### What the model sees -The generated default [`subagent` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent) appears under the configured tool name while its provider exists. An enabled Session policy adds `provider`, `model`, and `reasoning_effort` plus route guidance; the backend must support `agentOptions`. Context inheritance and continuation support determine the task and follow-up descriptions. A tool restriction removes both the schema and its `tool:` guidance section. +The generated default [`subagent` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent) appears under the configured tool name while its provider exists. Backends supporting `agentOptions` always expose `provider`, `model`, and `reasoning_effort`. Context inheritance and continuation support determine task and follow-up descriptions. A tool restriction removes both the schema and its `tool:` guidance section. #### Token effect -Fixed schema cost per parent request; model selection adds three parameters. Each tool instance adds one schema and one short system-prompt section. +Fixed schema cost per parent request, including three optional route fields on capable backends. Each tool instance adds one schema; visible delegation tools share one short system-prompt section. #### KV Cache effect @@ -142,11 +143,11 @@ Prefix-stable while provider instances and their configuration are unchanged. Ad #### What the model sees -A settings-controlled instance whose Session carries a policy exposes the child LLM selection fields and `list_subagent_models`. Calls reject while the optional `ctx.llm` service is unavailable. Discovery returns only registered providers and advertised models in the exact route policy; an unauthorized provider is rejected before its adapter catalog is called, and an exact lookup must be allowed before it resolves the model's reasoning efforts and default. Execution independently enforces the same policy. +The shared `list_subagent_models` definition reads the calling Session’s route policy, falling back to the current parent route when no policy is recorded. Calls reject while the optional `ctx.llm` service is unavailable. Discovery resolves only allowed routes, including exact model ids absent from advisory catalogs; unauthorized provider/model lookups fail before adapter access. Execution independently enforces the same policy. #### Token effect -One fixed discovery schema is present in enabled compositions. Directory contents enter the transcript only when the model calls the tool. +One fixed discovery schema is present in shipped compositions, regardless of the model-selection preference. Route metadata enters the transcript only when the model calls the tool. #### KV Cache effect @@ -208,8 +209,8 @@ Append-only; newly visible content follows the reusable request prefix and does These limits define what this tool does not return or enforce; they are current package constraints. - **This tool does not wait for results** — use the completion notice or child messages. Programmatic result waiting belongs to the subagent service. -- **Shipped fork tools cannot select a child LLM route** — they inherit the parent's provider and model to keep the copied conversation prefix eligible for KV Cache reuse. Re-enable selection only when route changes preserve reuse or expose a bounded recomputation cost. -- **Non-routing child policy is fixed per instance** — another persona, tool filter, or depth cap requires another distinctly named tool. LLM selection requires an enabled per-Session preference and a provider that advertises `agentOptions`; both in-process providers and DSH SDK advertise it, while ACP, Codex, and Claude Code reject it rather than ignore it. +- **Fork route changes can lose KV Cache reuse** — optional route fields follow the same Session policy as spawn; changing provider or model may require prefilling the inherited conversation again. +- **Non-routing child policy is fixed per instance** — another persona, tool filter, or depth cap requires another distinctly named tool. Explicit LLM selection is restricted to the Session’s authorized routes, or the current parent route without an enabled policy; unsupported backends reject selection fields. ### Dev Note diff --git a/packages/subagent/tool-subagent/README.zh.md b/packages/subagent/tool-subagent/README.zh.md index 4b695ccf37..ea0a06a3ba 100644 --- a/packages/subagent/tool-subagent/README.zh.md +++ b/packages/subagent/tool-subagent/README.zh.md @@ -36,6 +36,7 @@ kind: "package-reference" ```yaml - name: '@deepseek-ai/dsh-subagent' - name: '@deepseek-ai/dsh-subagent-spawn-in-process' +- name: '@deepseek-ai/dsh-tool-subagent/list-models' - name: '@deepseek-ai/dsh-tool-subagent' config: provider: spawn @@ -64,7 +65,7 @@ kind: "package-reference" ### 选择子级 LLM -设置 `modelSelectionSettings: true`,即可在组合每个全新顶层 Session 时读取宿主的 `subagent-model-selection` 偏好。没有已记录策略的恢复 Session 会保持禁用,包括显式为空的恢复。启用后,非空的精确 provider/model 路由列表会记录进 Session、由子 Session 继承,后续设置编辑不会改变它。工具随后公开可选的 `provider`、`model` 与 `reasoning_effort` 字段,并注册共享的 `list_subagent_models` 工具。此模式要求后端声明 `agentOptions`;两个进程内后端和 DSH SDK 支持该能力,而 ACP、Codex 与 Claude Code 会拒绝它,而不是忽略它。 +支持 `agentOptions` 的后端始终公开可选的 `provider`、`model` 和 `reasoning_effort` 字段。在委派工具旁加载一次 `@deepseek-ai/dsh-tool-subagent/list-models` 即可共享发现功能;随附组合已包含它。没有已记录 Session 策略时,发现和显式选择只允许父 Agent 当前生效的 provider/model 及其支持的强度。设置 `modelSelectionSettings: true` 后,新顶层 Session 会读取 Host 的 `subagent-model-selection` 偏好。启用时,精确路由列表记录在 Session 中,由子 Session 继承,并在恢复时保留;之后的设置编辑只影响新 Session。没有策略的恢复 Session 仍受父级路由限制。ACP、Codex 和 Claude Code 不支持 `agentOptions`,会拒绝选择字段。 一次调用需同时提供 `provider` 与 `model`;当配置值、父 agent 值或提供方持有的默认值能提供路由时,也可只提供推理等级。静态的 `provider.agentRouteDefaults` 在存在时构成提供方/模型基线;工具配置与模型字段会在路由相关强度合并和确切路由预检前覆盖它。没有这些默认值的提供方会使用父 agent 最新已记录请求中的兼容值,再使用父级首次请求前的创建选项,并保留配置的 `maxTokens`。更改路由但未显式提供推理等级时,会清除继承的路由自有等级,使所选模型解析自己的默认值。实时 LLM 适配器在创建子 agent 前校验有效路由。目录成员资格只提供建议,因此适配器接受时,模型可以使用未列出的 id。 @@ -80,7 +81,7 @@ kind: "package-reference" ### 设计理念 -一个实例就是一个提供方加一个工具名称。插件镜像提供方生命周期:具名提供方出现时注册工具,提供方离开时释放工具,因此同级加载顺序与 HMR 替换不会让工具悬空。直接 Agent setup 显式传入尚未发布的 Session,并在发布前等待安装完成。由设置控制的常驻 preset 通过 `agent/created` 接收每个匹配 Agent,从其 Session 选择策略,并等待通过其 Context 发起的安装;安装失败会拒绝创建。提供方无法执行的数值型 `maxDepth` 或已配置 LLM 选择会在挂载时失败,而不是在首次委派时失败。每个工具作用域内最多一个实例可以拥有模型选择,因为 `list_subagent_models` 使用全局名称。 +一个实例对应一个提供方和一个工具名。插件在提供方存在时向所属组合注册定义,并在提供方离开时释放它。Agent 继承这份共享定义,因此现有工具过滤器即可生效,无需逐 Agent 注册。读取设置的实例在 Agent 创建时记录 Session 授权;调用时独立于注册读取持久策略。直接配置 Agent 时显式传入尚未发布的 Session。提供方不支持的数字 `maxDepth` 或配置的 LLM 选择会使挂载失败。发现功能由 spawn 和 fork 工具共享的独立组合项提供。本包不导出运行时不变量:策略直接来自 Session 投影,并在执行时校验,没有另行保存的逐 Agent 定义状态需要协调。 ### Activation 所有权 @@ -128,11 +129,11 @@ kind: "package-reference" #### 模型看到什么 -提供方存在时,以配置的工具名称公开生成的默认 [`subagent` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-subagent)。启用的 Session 策略会添加 `provider`、`model`、`reasoning_effort` 和路由指引;后端必须支持 `agentOptions`。上下文继承和继续执行能力决定任务与后续消息的描述。工具限制会同时移除 schema 及其 `tool:` 指引 section。 +生成的默认 [`subagent` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-subagent) 在提供方存在时以配置的工具名公开。支持 `agentOptions` 的后端始终公开 `provider`、`model` 和 `reasoning_effort`。上下文继承和继续执行能力决定任务与后续工作描述。工具限制同时移除 schema 及其 `tool:` 指导段。 #### Token 影响 -每个父级请求支付固定的 schema 成本;模型选择增加三个参数。每个工具实例增加一个 schema 和一个简短的系统提示词 section。 +每次父级请求承担固定 schema 成本,其中支持选择的后端包含三个可选路由字段。每个工具实例增加一份 schema;可见委派工具共享一个简短系统提示词段。 #### KV Cache 影响 @@ -142,11 +143,11 @@ kind: "package-reference" #### 模型看到什么 -Session 携带策略的 settings 控制实例会公开子级 LLM 选择字段与 `list_subagent_models`。可选 `ctx.llm` 服务不可用时,调用会失败。发现只返回精确路由策略中的已注册提供方与已公布模型;未授权提供方会在调用其适配器目录前被拒绝,精确查询也必须先获准,才会解析模型的推理强度与默认值。执行阶段会独立强制同一策略。 +共享的 `list_subagent_models` 定义读取调用 Session 的路由策略,没有已记录策略时使用当前父级路由。可选的 `ctx.llm` 服务不可用时,调用会被拒绝。发现只解析允许的路由,包括建议性目录未列出的精确模型 ID;未授权的提供方/模型查询会在访问适配器前失败。执行过程独立强制同一策略。 #### Token 影响 -启用的组合中存在一个固定发现 schema。只有模型调用工具时,目录内容才进入 transcript。 +随附组合中始终存在一份固定发现 schema,不受模型选择偏好影响。只有模型调用工具时,路由元数据才进入 transcript。 #### KV Cache 影响 @@ -208,8 +209,8 @@ Start independent delegations with `subagent` or `subagent_fork` together in one 这些限制说明本工具不返回或不强制执行什么;它们是当前包约束。 - **本工具不等待结果**——使用完成通知或子任务消息。程序化结果等待由 subagent 服务提供。 -- **随附 fork 工具不能选择子级 LLM 路由**——它们继承父级提供方与模型,使复制的对话前缀仍有资格复用 KV Cache。仅当路由变更能保留复用或公开有界重算成本时,才重新启用选择。 -- **非路由子 agent 策略按实例固定**——另一个 persona、工具过滤器或深度上限需要另一个名称不同的工具。LLM 选择要求启用逐 Session 偏好,且提供方必须声明 `agentOptions`;两个进程内提供方和 DSH SDK 会声明该能力,而 ACP、Codex 与 Claude Code 会拒绝它,而不是忽略它。 +- **更改 fork 路由可能失去 KV Cache 复用** — 可选路由字段遵循与 spawn 相同的 Session 策略;更改提供方或模型可能需要重新预填充继承的对话。 +- **非路由子级策略按实例固定** — 更换角色、工具过滤器或深度上限,需要另一个不同名称的工具。显式 LLM 选择受 Session 授权路由限制,没有启用策略时只允许当前父级路由;不支持选择的后端会拒绝选择字段。 ### 开发备注 diff --git a/packages/subagent/tool-subagent/package.json b/packages/subagent/tool-subagent/package.json index f6c8100036..0117a36a9a 100644 --- a/packages/subagent/tool-subagent/package.json +++ b/packages/subagent/tool-subagent/package.json @@ -23,10 +23,15 @@ "default": "./lib/model-selection-settings.js" }, "./src/*": "./src/*", - "./package.json": "./package.json" + "./package.json": "./package.json", + "./list-models": { + "types": "./lib/types/list-models.d.ts", + "default": "./lib/list-models.js" + } }, "files": [ "lib/index.js", + "lib/list-models.js", "lib/model-selection-settings.js", "lib/types/**/*.d.ts" ], diff --git a/packages/subagent/tool-subagent/src/index.ts b/packages/subagent/tool-subagent/src/index.ts index 968b68ebf3..90f98d10aa 100644 --- a/packages/subagent/tool-subagent/src/index.ts +++ b/packages/subagent/tool-subagent/src/index.ts @@ -11,7 +11,7 @@ import z from '@deepseek-ai/schemastery' import { scopeChainOf, scopeOf } from '@deepseek-ai/dsh-scope' import { defineTool } from '@deepseek-ai/dsh-tools' import type { ToolDefinition } from '@deepseek-ai/dsh-tools' -import type { Agent, AgentOptions } from '@deepseek-ai/dsh-agent' +import type { AgentOptions } from '@deepseek-ai/dsh-agent' import { ReasoningEffortId } from '@deepseek-ai/dsh-llm' import type { ContentBlock } from '@deepseek-ai/dsh-llm' import { SessionSeq } from '@deepseek-ai/dsh-session' @@ -28,8 +28,7 @@ import { preflightChildLlmRoute, requestedAgentOptions, } from './model-selection.ts' -import type { DelegationModelRequest, ModelSelectionPolicy } from './model-selection.ts' -import { registerListSubagentModels } from './list-models.ts' +import type { DelegationModelRequest } from './model-selection.ts' import type {} from './model-selection-settings.ts' import { recordSubagentModelSelection, @@ -201,14 +200,13 @@ export function apply(ctx: Context, config: Config, session?: Session): void { const initialProvider = ctx.subagents.getProvider(config.provider) if (initialProvider !== undefined) assertSubagentProviderConfiguration(initialProvider) - const install = (runtimeCtx: Context, modelSelectionPolicy: ModelSelectionPolicy | undefined): void => { - const modelSelectionEnabled = modelSelectionPolicy !== undefined - if (modelSelectionPolicy !== undefined) registerListSubagentModels(runtimeCtx, modelSelectionPolicy) + const install = (runtimeCtx: Context): void => { // Load order and HMR replacement can change provider availability while // this fiber remains active. let mounted: { subagentProvider: SubagentProvider; disposeTool: () => void } | undefined const mount = (subagentProvider: SubagentProvider): void => { assertSubagentProviderConfiguration(subagentProvider) + const modelSelectionEnabled = subagentProvider.capabilities.agentOptions const wording = providerWording(subagentProvider.inheritsParentContext) const providerRouteDefaults = subagentProvider.agentRouteDefaults const selectionDescription = providerRouteDefaults !== undefined @@ -284,6 +282,8 @@ export function apply(ctx: Context, config: Config, session?: Session): void { throw new Error('subagent tool requires a calling agent (exec.agent was undefined)') } + const routes = subagentModelSelectionPolicy(runtimeCtx.sessionProjections, parent.session) + const modelSelectionPolicy = routes === undefined ? undefined : { routes } const modelRequest = args as DelegationModelRequest const parentOptions = parentAgentOptionsForDelegation(parent) const requiresRoutePreflight = hasDelegationModelRequest(modelRequest) @@ -386,7 +386,7 @@ export function apply(ctx: Context, config: Config, session?: Session): void { } if (config.modelSelectionSettings !== true) { - install(ctx, undefined) + install(ctx) return } @@ -397,7 +397,7 @@ export function apply(ctx: Context, config: Config, session?: Session): void { + '@deepseek-ai/dsh-tool-subagent/model-selection-settings in the Host scope', ) } - const selectForSession = (target: Session): ModelSelectionPolicy | undefined => { + const selectForSession = (target: Session): void => { const freshSession = target.firstLiveSeq === 0 // oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred. && target.eventAt(SessionSeq(0))?.type !== 'session/end-seed' @@ -423,11 +423,11 @@ export function apply(ctx: Context, config: Config, session?: Session): void { if (allowedModels !== undefined) { recordSubagentModelSelection(ctx.sessionProjections, target, allowedModels) } - return allowedModels === undefined ? undefined : { routes: allowedModels } } if (session !== undefined) { - install(ctx, selectForSession(session)) + selectForSession(session) + install(ctx) return } @@ -438,54 +438,21 @@ export function apply(ctx: Context, config: Config, session?: Session): void { const agents = ctx.get('agents') /* v8 ignore next -- shipped preset compositions always include the Agent registry. */ if (agents === undefined) throw new Error('tool-subagent: standing `modelSelectionSettings` requires the Agent registry') - const scopedInstalls = new WeakMap>() - const installing = new WeakSet() - const belongsToComposition = (candidate: Agent): boolean => - scopeChainOf(scopeOf(candidate.ctx)).includes(compositionScope) - const installScoped = (candidate: Agent): ReturnType | undefined => { - const existing = scopedInstalls.get(candidate) - if (existing !== undefined) return existing - if (installing.has(candidate)) return - // Reserve before the injected fiber runs: tool registration emits - // `tools/change` synchronously, which re-enters the reconciliation below. - installing.add(candidate) - let fiber: ReturnType - try { - const policy = selectForSession(candidate.session) - fiber = candidate.ctx.inject(['tools', 'subagents', 'systemPrompt'], (runtimeCtx) => { - install(runtimeCtx, policy) - }) - } finally { - installing.delete(candidate) - } - scopedInstalls.set(candidate, fiber) - return fiber + const captured = new WeakSet() + const capture = (target: Session): void => { + if (captured.has(target)) return + selectForSession(target) + captured.add(target) } - const removeScoped = (candidate: Agent): void => { - const fiber = scopedInstalls.get(candidate) - if (fiber === undefined) return - scopedInstalls.delete(candidate) - /* v8 ignore next 3 -- Cordis Fiber disposal contains registration cleanup failures; this is the final diagnostic sink. */ - void fiber.dispose().catch((error: unknown) => { - ctx.logger.warn(`tool-subagent: failed to remove recomposed Agent "${candidate.id}" definitions: ${String(error)}`) - }) - } - const reconcileComposedAgents = (): void => { - for (const candidate of agents.list()) { - if (belongsToComposition(candidate)) installScoped(candidate) - else removeScoped(candidate) + const captureComposedSessions = (): void => { + for (const agent of agents.list()) { + if (scopeChainOf(scopeOf(agent.ctx)).includes(compositionScope)) capture(agent.session) } } - // The preset-scoped listener admits descendant Agents and installs the - // sampled tool definition in each Agent's own scope, so a later settings - // change cannot mutate a live session. - ctx.on('agent/created', async ({ agent: created }) => { - await installScoped(created) - }) - ctx.on('agent/disposed', ({ agent: disposed }) => { removeScoped(disposed) }) - // Reparenting an Agent between standing presets changes its inherited tool - // set and emits `tools/change`; reconcile the Agent-owned override with the - // new ancestry. Other registry changes are idempotent no-ops here. - ctx.on('tools/change', reconcileComposedAgents) - reconcileComposedAgents() + ctx.on('agent/created', ({ agent }) => { capture(agent.session) }) + // Blank Sessions can join a preset after creation; absence also records the + // sampled off decision for this plugin lifetime. + ctx.on('tools/change', captureComposedSessions) + captureComposedSessions() + install(ctx) } diff --git a/packages/subagent/tool-subagent/src/list-models.ts b/packages/subagent/tool-subagent/src/list-models.ts index a1265934d3..c642c4a743 100644 --- a/packages/subagent/tool-subagent/src/list-models.ts +++ b/packages/subagent/tool-subagent/src/list-models.ts @@ -4,6 +4,9 @@ import type { Context } from '@deepseek-ai/cordis' import type LlmRuntime from '@deepseek-ai/dsh-llm' import type { LlmProviderInfo } from '@deepseek-ai/dsh-llm' import { defineTool } from '@deepseek-ai/dsh-tools' +import { parentAgentOptionsForDelegation } from '@deepseek-ai/dsh-subagent' +import type { ToolRunContext } from '@deepseek-ai/dsh-tools' +import { subagentModelSelectionPolicy, subagentModelSelectionProjectionDefinition } from './model-selection-state.ts' import type { ModelSelectionPolicy } from './model-selection.ts' interface ListSubagentModelsRequest { @@ -32,7 +35,7 @@ function modelLine(provider: string, model: { id: string; name: string; descript return `${provider}/${model.id} — ${model.name}${model.description === undefined ? '' : `: ${model.description}`}` } -/** Read the requested provider, advertised models, or exact-model efforts. */ +/** Read the requested provider, allowed models, or exact-model efforts. */ async function listSubagentModels( ctx: Context, policy: ModelSelectionPolicy, @@ -60,11 +63,8 @@ async function listSubagentModels( } const provider = registeredProvider(llm, policy, request.provider) if (request.model === undefined) { - const models = (await llm.listModels(provider.id)) - .filter(model => allowedRoutes.some(route => route.model === model.id)) - return models.length === 0 - ? `(no advertised models for ${provider.id})` - : models.map(model => modelLine(provider.id, model)).join('\n') + const models = await Promise.all(allowedRoutes.map(route => llm.resolveModelInfo(provider.id, route.model, signal))) + return models.map(model => modelLine(provider.id, model)).join('\n') } if (request.model.length === 0) throw new Error('`model` must be non-empty') if (!allowedRoutes.some(route => route.model === request.model)) { @@ -79,16 +79,16 @@ async function listSubagentModels( } /** - * Register `list_subagent_models` for one owning delegation-tool instance. + * Register `list_subagent_models` in the owning composition. * @param ctx - Context whose tool registry owns the fixed discovery definition. - * @param policy - Route policy captured for this Session. + * @param policyFor - Resolve the calling Session’s authorized routes. */ -export function registerListSubagentModels(ctx: Context, policy: ModelSelectionPolicy): void { +export function registerListSubagentModels(ctx: Context, policyFor: (exec: ToolRunContext) => ModelSelectionPolicy): void { ctx.tools.register(defineTool({ name: 'list_subagent_models', description: 'Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list ' - + 'registered providers, with `provider` to list its advertised models, or with `provider` and `model` ' + + 'registered providers, with `provider` to list its allowed models, or with `provider` and `model` ' + 'to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may ' + 'accept an unlisted model id. Use the returned ids with a delegation tool\'s `provider`, `model`, and ' + '`reasoning_effort` fields.', @@ -99,7 +99,7 @@ export function registerListSubagentModels(ctx: Context, policy: ModelSelectionP }, model: { type: 'string', - description: 'Exact model id to inspect. Requires provider; omit to list that provider\'s advertised models.', + description: 'Exact model id to inspect. Requires provider; omit to list that provider\'s allowed models.', }, }, output: { @@ -107,7 +107,30 @@ export function registerListSubagentModels(ctx: Context, policy: ModelSelectionP render: (_args, result) => [{ type: 'text', text: result }], }, execute(args, exec) { - return listSubagentModels(ctx, policy, args, exec.signal) + return listSubagentModels(ctx, policyFor(exec), args, exec.signal) }, })) } + +/** Shared discovery plugin for all delegation tools in one composition. */ +export const name = 'tool-subagent-list-models' +/** Services used to register discovery and read durable route policy. */ +export const inject = ['tools', 'sessionProjections'] + +/** + * Register one discovery tool whose results follow the calling Session’s policy. + * @param ctx - Owning composition context. + */ +export function apply(ctx: Context): void { + ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition) + registerListSubagentModels(ctx, (exec) => { + if (exec.agent === undefined) throw new Error('list_subagent_models requires a calling agent') + const routes = subagentModelSelectionPolicy(ctx.sessionProjections, exec.agent.session) + if (routes !== undefined) return { routes } + const { provider, model } = parentAgentOptionsForDelegation(exec.agent) + if (provider === undefined || model === undefined) { + throw new Error('cannot discover child LLM routes without a parent provider and model') + } + return { routes: [{ provider, model }] } + }) +} diff --git a/packages/subagent/tool-subagent/src/model-selection-state.ts b/packages/subagent/tool-subagent/src/model-selection-state.ts index d3b04b828b..0854425c83 100644 --- a/packages/subagent/tool-subagent/src/model-selection-state.ts +++ b/packages/subagent/tool-subagent/src/model-selection-state.ts @@ -9,9 +9,9 @@ import { assertAllowedModelRoutes, type AllowedModelRoute } from './model-select declare module '@deepseek-ai/dsh-session/types' { interface SessionEventMap { /** - * Records that this session's delegation tool exposes child provider, - * model, and reasoning-effort selection. Appended before the first model - * request; absence means the fixed-route definition. Log-only: it carries + * Records this Session’s authorized child provider/model routes. Appended + * before the first model request; absence restricts explicit selection + * to the current parent route. Log-only: it carries * no `surfaceOp` and never enters model history. */ 'subagent/model-selection-policy': { @@ -23,7 +23,7 @@ declare module '@deepseek-ai/dsh-session/types' { declare module '@deepseek-ai/dsh-session-projection/types' { interface SessionProjectionStateMap { - /** Exact routes authorized for child LLM selection, or null when disabled. */ + /** Exact routes authorized for child LLM selection, or null for the parent route. */ subagentModelSelectionPolicy: AllowedModelRoute[] | null } } @@ -51,10 +51,10 @@ export const subagentModelSelectionProjectionDefinition = { } satisfies ProjectionDefinition<'subagentModelSelectionPolicy', AllowedModelRoute[] | null> /** - * Read the exact route list captured for a model-selectable definition. + * Read the exact route list authorized for a Session. * @param projections - registry that owns the policy projection. * @param session - session whose durable decision is read. - * @returns a detached route list, or undefined for the fixed-route definition. + * @returns a detached route list, or undefined to restrict selection to the parent route. */ export function subagentModelSelectionPolicy( projections: Pick, @@ -64,10 +64,10 @@ export function subagentModelSelectionPolicy( } /** - * Append the route policy once, before its definition can reach a model request. + * Append the route policy once, before the Session’s first model request. * @param projections - registry that owns the policy projection. - * @param session - session receiving the model-selectable definition. - * @param allowedModels - exact routes the definition may select explicitly. + * @param session - session receiving route authorization. + * @param allowedModels - exact routes the Session may select explicitly. */ export function recordSubagentModelSelection( projections: Pick, diff --git a/packages/subagent/tool-subagent/src/model-selection.ts b/packages/subagent/tool-subagent/src/model-selection.ts index 5f30b71742..04f4d33b5f 100644 --- a/packages/subagent/tool-subagent/src/model-selection.ts +++ b/packages/subagent/tool-subagent/src/model-selection.ts @@ -19,7 +19,7 @@ export const AllowedModelRouteSchema: z = z.object({ model: z.string().min(1).required(), }) -/** Route-selection authority captured by one delegation definition. */ +/** Exact routes authorized for one Session. */ export interface ModelSelectionPolicy { /** Exact provider/model routes authorized for explicit selection. */ readonly routes: readonly AllowedModelRoute[] @@ -131,7 +131,7 @@ export function requestedAgentOptions( * Enforce a settings-owned route list at the operation that creates the child. * Pure inheritance remains outside this policy because no model-facing choice * occurred; any explicit route or effort field must resolve to an allowed route. - * @param policy - Selection authority captured for this Session. + * @param policy - Session allowlist; omission permits only the current parent route. * @param parentOptions - Current parent values that supply missing child values. * @param requested - Effective child options after request/config merging. * @param request - Model-facing selection fields from the tool call. @@ -142,13 +142,16 @@ export function assertAllowedModelSelection( requested: AgentOptions | undefined, request: DelegationModelRequest, ): void { - if (policy === undefined || !hasDelegationModelRequest(request)) return + if (!hasDelegationModelRequest(request)) return const provider = requested?.provider ?? parentOptions.provider const model = requested?.model ?? parentOptions.model if (provider === undefined || model === undefined) { throw new Error('cannot select child LLM values without an effective provider and model') } - if (policy.routes.some(route => route.provider === provider && route.model === model)) return + const allowed = policy === undefined + ? provider === parentOptions.provider && model === parentOptions.model + : policy.routes.some(route => route.provider === provider && route.model === model) + if (allowed) return throw new Error(`child LLM route "${provider}/${model}" is not allowed for this Session`) } diff --git a/packages/subagent/tool-subagent/tests/harness.ts b/packages/subagent/tool-subagent/tests/harness.ts index 422d65ba0b..061161673e 100644 --- a/packages/subagent/tool-subagent/tests/harness.ts +++ b/packages/subagent/tool-subagent/tests/harness.ts @@ -9,6 +9,7 @@ import SubagentRuntime from '@deepseek-ai/dsh-subagent' import { SessionId } from '@deepseek-ai/dsh-session' import * as mock from './scripted-provider.ts' import * as tool from '../src/index.ts' +import * as discovery from '../src/list-models.ts' import SubagentModelSelectionConfig from '../src/model-selection-settings.ts' const contexts = new Set() @@ -58,6 +59,7 @@ export async function setup(toolConfig: SetupConfig, mockConfig: Partial ({ routes })) return ctx } @@ -78,13 +78,13 @@ async function setupAllowedListTool() { await ctx.plugin(LlmRuntime) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) - registerListSubagentModels(ctx, { + registerListSubagentModels(ctx, () => ({ routes: [ { provider: 'alpha', model: 'fast' }, { provider: 'alpha', model: 'unlisted' }, { provider: 'missing', model: 'hidden' }, ], - }) + })) return ctx } @@ -100,7 +100,7 @@ function call(ctx: Context, args: unknown) { } describe('list_subagent_models', () => { - it('is omitted unless its delegation-tool instance owns discovery', async () => { + it('is loaded through its own composition entry', async () => { const ctx = new Context() await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(LlmRuntime) @@ -112,13 +112,43 @@ describe('list_subagent_models', () => { expect(ctx.tools.get('list_subagent_models')).toBeUndefined() }) + it('requires a calling Agent and an effective parent route', async () => { + const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRuntime) + await ctx.plugin({ apply, inject, name }) + expect(text(await call(ctx, {}))).toContain('requires a calling agent') + await ctx.fiber.dispose() + + const parentContext = await setup({ provider: 'mock' }) + const agent = modelSelectionSetupAgent(parentContext) + const result = await parentContext.tools.execute({ + signal: testToolSignal, callId: ToolCallId('missing-parent-route'), + name: 'list_subagent_models', arguments: {}, agent, + }) + expect(result.isError).toBe(true) + expect(text(result)).toContain('without a parent provider and model') + }) + + it('reads the recorded Session allowlist through the shared discovery definition', async () => { + const ctx = await setup({ provider: 'mock', withModelSelection: true }) + ctx.llm.registerAdapter(['alpha', 'secret'], new CatalogAdapter()) + const result = await ctx.tools.execute({ + signal: testToolSignal, callId: ToolCallId('recorded-routes'), + name: 'list_subagent_models', arguments: {}, agent: modelSelectionSetupAgent(ctx), + }) + expect(result.isError).toBe(false) + expect(text(result)).toBe('alpha — ALPHA API') + }) + it('stays registered without the optional LLM service and rejects discovery calls', async () => { const ctx = new Context() await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) await mountWorkingDirectoryFixture(ctx) await ctx.plugin(SubagentRuntime) - registerListSubagentModels(ctx, { routes: [{ provider: 'alpha', model: 'fast' }] }) + registerListSubagentModels(ctx, () => ({ routes: [{ provider: 'alpha', model: 'fast' }] })) const result = await call(ctx, {}) expect(result.isError).toBe(true) expect(text(result)).toContain('`llm` service is unavailable') @@ -127,7 +157,7 @@ describe('list_subagent_models', () => { it('rejects two discovery-owning instances in one tool scope', async () => { const ctx = await setupListTool() expect(() => { - registerListSubagentModels(ctx, { routes: [{ provider: 'alpha', model: 'fast' }] }) + registerListSubagentModels(ctx, () => ({ routes: [{ provider: 'alpha', model: 'fast' }] })) }).toThrow('tool "list_subagent_models" is already registered') }) @@ -164,7 +194,7 @@ describe('list_subagent_models', () => { ctx.llm.registerAdapter(['alpha', 'beta'], new CatalogAdapter()) expect(text(await call(ctx, {}))).toBe('alpha — ALPHA API') - expect(text(await call(ctx, { provider: 'alpha' }))).toBe('alpha/fast — Fast: Focused work.') + expect(text(await call(ctx, { provider: 'alpha' }))).toBe('alpha/fast — Fast: Focused work.\nalpha/unlisted — Fast: Focused work.') expect(text(await call(ctx, { provider: 'alpha', model: 'unlisted' }))) .toContain('alpha/unlisted — Fast') @@ -186,12 +216,12 @@ describe('list_subagent_models', () => { expect(listModels).not.toHaveBeenCalled() }) - it('renders an empty advertised model list', async () => { + it('resolves an allowed model even when the advertised catalog is empty', async () => { const ctx = await setupListTool([{ provider: 'alpha', model: 'fast' }]) ctx.llm.registerAdapter(['alpha'], new CatalogAdapter(true)) const result = await call(ctx, { provider: 'alpha' }) expect(result.isError).toBe(false) - expect(text(result)).toBe('(no advertised models for alpha)') + expect(text(result)).toBe('alpha/fast — Fast: Focused work.') }) it('inspects exact-model efforts, descriptions, and defaults', async () => { diff --git a/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts b/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts index 31ab63b973..a4dc33b0fa 100644 --- a/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts +++ b/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts @@ -14,6 +14,7 @@ import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import SubagentRuntime from '@deepseek-ai/dsh-subagent' import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process' import * as tool from '../src/index.ts' +import * as discovery from '../src/list-models.ts' import SubagentModelSelectionConfig from '../src/model-selection-settings.ts' import { subagentModelSelectionPolicy, @@ -48,8 +49,10 @@ async function boot(withPreset = true): Promise { await mountWorkingDirectoryFixture(ctx) subagentConfigs.set(ctx, await liveConfig(ctx, SubagentRuntime)) await ctx.plugin(SubagentSpawn, { providerName: 'spawn' }) + await ctx.plugin(discovery) if (withPreset) { const preset = createScope(ctx, { preset: 'model-selection-test' }) + await preset.ctx.plugin(discovery) await preset.ctx.plugin(tool, { provider: 'spawn', modelSelectionSettings: true, @@ -136,7 +139,7 @@ describe('SubagentModelSelectionConfig', () => { it('samples each new root Session without changing existing definitions', async () => { const ctx = await boot() const disabled = await createAgent(ctx, 'disabled') - expect(selectable(ctx, disabled)).toBe(false) + expect(selectable(ctx, disabled)).toBe(true) expect(subagentModelSelectionPolicy(ctx.sessionProjections, disabled.session)).toBeUndefined() await selectionConfigs.get(ctx)!.update({ @@ -146,11 +149,11 @@ describe('SubagentModelSelectionConfig', () => { const enabled = await createAgent(ctx, 'enabled') expect(subagentModelSelectionPolicy(ctx.sessionProjections, enabled.session)).toEqual(ALLOWED_MODELS) expect(selectable(ctx, enabled)).toBe(true) - expect(selectable(ctx, disabled)).toBe(false) + expect(selectable(ctx, disabled)).toBe(true) await selectionConfigs.get(ctx)!.update({ enabled: false }) const disabledAgain = await createAgent(ctx, 'disabled-again') - expect(selectable(ctx, disabledAgain)).toBe(false) + expect(selectable(ctx, disabledAgain)).toBe(true) expect(selectable(ctx, enabled)).toBe(true) await ctx.fiber.dispose() }) @@ -187,7 +190,35 @@ describe('SubagentModelSelectionConfig', () => { await ctx.fiber.dispose() }) - it('installs one tool when recording the Session policy triggers a registry refresh', async () => { + it('captures a blank Session joining the preset without resampling existing Sessions', async () => { + const ctx = await boot() + const disabled = await createAgent(ctx, 'already-disabled') + const joining = await ctx.agents.create({ sessionId: SessionId('joining-preset') }) + await selectionConfigs.get(ctx)!.update({ enabled: true, allowedModels: ALLOWED_MODELS }) + const preset = modelSelectionPresets.get(ctx)! + bindScopeParent(scopeOf(joining.agent.ctx)!, scopeOf(preset.ctx)!) + ctx.emit(scopeTarget({}, scopeOf(preset.ctx)), 'tools/change') + expect(subagentModelSelectionPolicy(ctx.sessionProjections, joining.agent.session)).toEqual(ALLOWED_MODELS) + expect(subagentModelSelectionPolicy(ctx.sessionProjections, disabled.session)).toBeUndefined() + await ctx.fiber.dispose() + }) + + it('captures policy for existing Agents only in the mounting preset', async () => { + const ctx = await boot(false) + const preset = createScope(ctx, { preset: 'late-tools' }) + const inside = await ctx.agents.create({ + sessionId: SessionId('existing-inside'), + setup: (agentCtx) => { bindScopeParent(scopeOf(agentCtx)!, scopeOf(preset.ctx)!) }, + }) + const outside = await ctx.agents.create({ sessionId: SessionId('existing-outside') }) + await selectionConfigs.get(ctx)!.update({ enabled: true, allowedModels: ALLOWED_MODELS }) + await preset.ctx.plugin(tool, { provider: 'spawn', modelSelectionSettings: true }) + expect(subagentModelSelectionPolicy(ctx.sessionProjections, inside.agent.session)).toEqual(ALLOWED_MODELS) + expect(subagentModelSelectionPolicy(ctx.sessionProjections, outside.agent.session)).toBeUndefined() + await ctx.fiber.dispose() + }) + + it('does not register tools when recording the Session policy triggers a registry refresh', async () => { const ctx = await boot() try { await selectionConfigs.get(ctx)!.update({ @@ -205,7 +236,7 @@ describe('SubagentModelSelectionConfig', () => { const agent = await createAgent(ctx, 'policy-refresh') expect(selectable(ctx, agent)).toBe(true) - expect(register.mock.calls.filter(([definition]) => definition.name === 'subagent')).toHaveLength(1) + expect(register).not.toHaveBeenCalled() } finally { await ctx.fiber.dispose() } @@ -237,10 +268,11 @@ describe('SubagentModelSelectionConfig', () => { await ctx.fiber.dispose() }) - it('installs per-Agent definitions for a shared preset scope', async () => { + it('shares definitions across Agents and lets scope ancestry own their visibility', async () => { const ctx = await boot(false) const preset = createScope(ctx, { preset: 'standard' }) const other = createScope(ctx, { preset: 'minimal' }) + await preset.ctx.plugin(discovery) await preset.ctx.plugin(tool, { provider: 'spawn', modelSelectionSettings: true, @@ -256,14 +288,16 @@ describe('SubagentModelSelectionConfig', () => { }) const disabled = await createComposed('preset-disabled') - expect(selectable(ctx, disabled.agent)).toBe(false) + expect(selectable(ctx, disabled.agent)).toBe(true) await selectionConfigs.get(ctx)!.update({ enabled: true, allowedModels: ALLOWED_MODELS, }) const enabled = await createComposed('preset-enabled') expect(selectable(ctx, enabled.agent)).toBe(true) - expect(selectable(ctx, disabled.agent)).toBe(false) + expect(selectable(ctx, disabled.agent)).toBe(true) + expect(ctx.tools.get('subagent', enabled.agent)).toBe(ctx.tools.get('subagent', disabled.agent)) + expect(ctx.tools.get('subagent', enabled.agent)).toBe(ctx.tools.get('subagent', scopeOf(preset.ctx))) enabledBinding!.rebind(scopeOf(other.ctx)!) ctx.emit(scopeTarget({}, scopeOf(preset.ctx)), 'tools/change') @@ -291,37 +325,31 @@ describe('SubagentModelSelectionConfig', () => { await ctx.fiber.dispose() }) - it('releases a shared-preset installation reservation after policy selection fails', async () => { - const ctx = await boot(false) - const preset = createScope(ctx, { preset: 'standard' }) - const other = createScope(ctx, { preset: 'minimal' }) - await preset.ctx.plugin(tool, { - provider: 'spawn', - modelSelectionSettings: true, - }) - let binding: ReturnType | undefined - const handle = await ctx.agents.create({ - sessionId: SessionId('preset-policy-retry'), - setup: (agentCtx) => { - binding = bindScopeParent(scopeOf(agentCtx)!, scopeOf(preset.ctx)!) - }, - }) - expect(selectable(ctx, handle.agent)).toBe(false) - - binding!.rebind(scopeOf(other.ctx)!) - ctx.emit(scopeTarget({}, scopeOf(preset.ctx)), 'tools/change') - binding!.rebind(scopeOf(preset.ctx)!) - vi.spyOn(ctx.subagentModelSelection, 'current') - .mockImplementationOnce(() => { throw new Error('transient settings read') }) - .mockReturnValue({ enabled: true, allowedModels: ALLOWED_MODELS }) - - expect(() => { ctx.emit(scopeTarget({}, scopeOf(preset.ctx)), 'tools/change') }) - .toThrow('transient settings read') - ctx.emit(scopeTarget({}, scopeOf(preset.ctx)), 'tools/change') - await vi.waitFor(() => { expect(selectable(ctx, handle.agent)).toBe(true) }) - - await handle.dispose() - await ctx.fiber.dispose() + it('keeps shared delegation and discovery hidden by the existing inherited-tool filter', async () => { + const ctx = await boot() + try { + const parent = await createAgent(ctx, 'filter-parent') + const preset = modelSelectionPresets.get(ctx)! + const child = (await ctx.agents.create({ + sessionId: SessionId('filter-child'), + meta: { parentSession: parent.id, origin: 'subagent' }, + setup: (childCtx) => { + bindScopeParent(scopeOf(childCtx)!, scopeOf(preset.ctx)!) + childCtx.tools.restrict({ deny: ['subagent', 'list_subagent_models'] }) + }, + })).agent + expect(ctx.tools.get('subagent', parent)).toBeDefined() + expect(ctx.tools.get('subagent', child)).toBeUndefined() + expect(ctx.tools.get('list_subagent_models', child)).toBeUndefined() + const result = await ctx.tools.execute({ + name: 'subagent', callId: ToolCallId('filtered-delegation'), arguments: {}, + agent: child, signal: new AbortController().signal, + }) + expect(result.isError).toBe(true) + expect(text(result)).toContain('unknown tool') + } finally { + await ctx.fiber.dispose() + } }) it('inherits the parent decision and preserves seeded decisions across composition', async () => { @@ -341,7 +369,7 @@ describe('SubagentModelSelectionConfig', () => { const orphan = await createAgent(ctx, 'orphan', { meta: { parentSession: SessionId('missing-parent'), origin: 'subagent' }, }) - expect(selectable(ctx, orphan)).toBe(false) + expect(selectable(ctx, orphan)).toBe(true) const enabledSeed = Session.create(SessionId('enabled-seed')) enabledSeed.append('subagent/model-selection-policy', { allowedModels: ALLOWED_MODELS }) @@ -354,11 +382,11 @@ describe('SubagentModelSelectionConfig', () => { allowedModels: ALLOWED_MODELS, }) const resumedEmpty = await createAgent(ctx, 'resumed-empty', { seed: [] }) - expect(selectable(ctx, resumedEmpty)).toBe(false) + expect(selectable(ctx, resumedEmpty)).toBe(true) expect(subagentModelSelectionPolicy(ctx.sessionProjections, resumedEmpty.session)).toBeUndefined() const resumedDisabled = await createAgent(ctx, 'resumed-disabled', { seed: oldSeed.snapshotEvents() }) - expect(selectable(ctx, resumedDisabled)).toBe(false) + expect(selectable(ctx, resumedDisabled)).toBe(true) expect(subagentModelSelectionPolicy(ctx.sessionProjections, resumedDisabled.session)).toBeUndefined() await ctx.fiber.dispose() }) diff --git a/packages/subagent/tool-subagent/tests/model-selection.spec.ts b/packages/subagent/tool-subagent/tests/model-selection.spec.ts index 5182ecfcc0..4af0606e66 100644 --- a/packages/subagent/tool-subagent/tests/model-selection.spec.ts +++ b/packages/subagent/tool-subagent/tests/model-selection.spec.ts @@ -1,7 +1,7 @@ import { mountWorkingDirectoryFixture } from '../../subagent/tests/working-directory-fixture.ts' import { describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { ReasoningEffortId } from '@deepseek-ai/dsh-llm' +import { ReasoningEffortId, ToolCallId } from '@deepseek-ai/dsh-llm' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime from '@deepseek-ai/dsh-tools' import type { Agent } from '@deepseek-ai/dsh-agent' @@ -132,13 +132,22 @@ describe('dsh-tool-subagent model selection', () => { expect(definition?.description).not.toContain('beta') }) - it('hides and rejects route fields when selection is disabled', async () => { - const ctx = await setup({ provider: 'mock' }) + it('rejects route arguments for a backend without Agent options support', async () => { + const ctx = await setup({ provider: 'mock' }, { capabilities: { agentOptions: false } }) + const result = await callSubagent(ctx, { + description: 'unsupported route', prompt: 'do it', provider: 'alpha', model: 'parent-model', + }) + expect(result.isError).toBe(true) + expect(text(result)).toContain('model selection is disabled for this tool instance') + }) + + it('keeps route fields visible but rejects other routes when selection is disabled', async () => { + const ctx = await setup({ provider: 'mock', parentAgentOptions: { provider: 'alpha', model: 'parent-model' } }) const schema = ctx.tools.schemas().find(entry => entry.name === 'subagent')! const props = (schema.parameters as { properties?: Record }).properties ?? {} - expect(Object.keys(props).sort()).toEqual(['cwd', 'description', 'prompt']) - expect(schema.description).not.toContain('list_subagent_models') - expect(ctx.tools.get('list_subagent_models')).toBeUndefined() + expect(Object.keys(props).sort()).toEqual(['cwd', 'description', 'model', 'prompt', 'provider', 'reasoning_effort']) + expect(schema.description).toContain('list_subagent_models') + expect(ctx.tools.get('list_subagent_models')).toBeDefined() const result = await callSubagent(ctx, { description: 'forced route', @@ -147,7 +156,43 @@ describe('dsh-tool-subagent model selection', () => { model: 'fast-model', }) expect(result.isError).toBe(true) - expect(text(result)).toContain('child model selection is disabled for this tool instance') + expect(text(result)).toContain('is not allowed for this Session') + }) + + it('discovers only the parent route by default and permits its explicit route and effort', async () => { + const requests: SubagentStartRequest[] = [] + const ctx = await setup({ + provider: 'mock', + parentAgentOptions: parentWithRoute().options, + }, { onStart: (request) => { requests.push(request) } }) + ctx.llm.registerAdapter(['alpha', 'other-provider'], new MockAdapter([], REASONING)) + const parent = modelSelectionSetupAgent(ctx) + const discover = (args: unknown) => ctx.tools.execute({ + callId: ToolCallId('default-model-discovery'), name: 'list_subagent_models', + arguments: args, agent: parent, signal: new AbortController().signal, + }) + expect(text(await discover({}))).toContain('alpha') + expect(text(await discover({}))).not.toContain('other-provider') + expect(text(await discover({ provider: 'alpha' }))).toContain('alpha/parent-model') + expect(text(await discover({ provider: 'alpha', model: 'parent-model' }))).toContain('low') + expect((await discover({ provider: 'alpha', model: 'other-model' })).isError).toBe(true) + + const omitted = await callSubagent(ctx, { description: 'inherit', prompt: 'work' }) + expect(omitted.isError, text(omitted)).toBe(false) + expect(requests[0]?.agentOptions).toBeUndefined() + const explicit = await callSubagent(ctx, { + description: 'same route', prompt: 'work', provider: 'alpha', model: 'parent-model', + }) + expect(explicit.isError, text(explicit)).toBe(false) + expect(requests[1]?.agentOptions).toEqual({ provider: 'alpha', model: 'parent-model' }) + const effort = await callSubagent(ctx, { description: 'effort', prompt: 'work', reasoning_effort: 'low' }) + expect(effort.isError, text(effort)).toBe(false) + expect(requests[2]?.agentOptions).toEqual({ reasoningEffort: 'low' }) + const rejected = await callSubagent(ctx, { + description: 'other route', prompt: 'work', provider: 'alpha', model: 'other-model', + }) + expect(rejected.isError).toBe(true) + expect(requests).toHaveLength(3) }) it('rejects enabled model selection when the provider cannot apply Agent options', async () => { diff --git a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts index ea090fa0a3..5993003d03 100644 --- a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts +++ b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts @@ -102,7 +102,10 @@ describe('dsh-tool-subagent', () => { expect(Object.keys(props).sort()).toEqual([ 'cwd', 'description', + 'model', 'prompt', + 'provider', + 'reasoning_effort', ]) expect(schema!.description).not.toContain('job_output') }) @@ -510,7 +513,7 @@ describe('dsh-tool-subagent', () => { const schema = ctx.tools.schemas().find(s => s.name === 'subagent')! expect(schema.description).toContain('inherits this conversation') expect(schema.description).not.toContain('does not see this conversation') - expect(schema.description).not.toContain('can prevent provider-side reuse of the inherited conversation prefix') + expect(schema.description).toContain('can prevent provider-side reuse of the inherited conversation prefix') const props = (schema.parameters as { properties: Record }).properties expect(props['prompt']!.description).toContain('completed turns') }) @@ -760,7 +763,7 @@ describe('dsh-tool-subagent local activation', () => { { description: 'continuable work', prompt: 'dig in' }, { agent: parent }, ) - expect(started.isError).toBe(false) + expect(started.isError, text(started)).toBe(false) const match = /^started subagent (\S+)$/.exec(text(started)) expect(match).not.toBeNull() const [, childId] = match! @@ -828,7 +831,7 @@ describe('dsh-tool-subagent local activation', () => { const [failed, succeeded] = await Promise.all([cancelledResult, survivingResult]) expect(preparationCount).toBe(2) expect(failed.isError).toBe(true) - expect(succeeded.isError).toBe(false) + expect(succeeded.isError, text(succeeded)).toBe(false) expect(cancelledChildId).toBeDefined() expect(survivingChildId).toBeDefined() expect(ctx.agents.get(cancelledChildId!)).toBeUndefined() diff --git a/packages/subagent/tool-subagent/tsdown.config.ts b/packages/subagent/tool-subagent/tsdown.config.ts index 4a467f2c5e..8cdb8ec132 100644 --- a/packages/subagent/tool-subagent/tsdown.config.ts +++ b/packages/subagent/tool-subagent/tsdown.config.ts @@ -15,4 +15,5 @@ const entry = (path: string) => ({ export default defineConfig([ entry('lib/types/index.js'), entry('lib/types/model-selection-settings.js'), + entry('lib/types/list-models.js'), ]) diff --git a/scripts/check-workspace-constraints.ts b/scripts/check-workspace-constraints.ts index c670b46f2a..a15d3c071d 100644 --- a/scripts/check-workspace-constraints.ts +++ b/scripts/check-workspace-constraints.ts @@ -195,9 +195,8 @@ const packageFileExtras: Readonly> = { '@deepseek-ai/dsh-experimental-inspector': ['lib/client.*.js', 'lib/worker.js', 'lib/devtools/**'], // Creator's composition guidance travels with the declaration package. '@deepseek-ai/dsh-agent-preset': ['skills'], - // The Web Host mounts the default-off settings owner independently of each - // Agent-scoped delegation-tool instance. - '@deepseek-ai/dsh-tool-subagent': ['lib/model-selection-settings.js'], + // Settings and shared discovery have independent composition entries. + '@deepseek-ai/dsh-tool-subagent': ['lib/list-models.js', 'lib/model-selection-settings.js'], // The JSONL backend resolves its private verification Worker relative to // import.meta.url; it is shipped without a public package subpath. '@deepseek-ai/dsh-session-persistence-jsonl': ['lib/worker.cjs'], diff --git a/scripts/gen-tool-catalog.ts b/scripts/gen-tool-catalog.ts index 14d4630c98..fa03619cba 100644 --- a/scripts/gen-tool-catalog.ts +++ b/scripts/gen-tool-catalog.ts @@ -73,7 +73,7 @@ import * as PluginManagerTools from '@deepseek-ai/dsh-plugin-manager/tools' import SandboxPolicy from '@deepseek-ai/dsh-sandbox-policy' import McpResources from '@deepseek-ai/dsh-mcp-resources' import * as ToolSubagent from '@deepseek-ai/dsh-tool-subagent' -import { registerListSubagentModels } from '../packages/subagent/tool-subagent/src/list-models.ts' +import * as SubagentModelDiscovery from '../packages/subagent/tool-subagent/src/list-models.ts' import * as ToolWeb from '@deepseek-ai/dsh-tool-web' import WorkflowEngine from '@deepseek-ai/dsh-workflow' import type { WorkflowRun, WorkflowStartRequest } from '@deepseek-ai/dsh-workflow' @@ -519,10 +519,10 @@ const TOOL_PACKAGES: ToolPackage[] = [ await ctx.plugin(LlmRuntime) registerCatalogSubagentProvider(ctx, 'mock') await ctx.plugin(ToolSubagent, { provider: 'mock' }) - registerListSubagentModels(ctx, { routes: [{ provider: 'mock', model: 'mock' }] }) + await ctx.plugin(SubagentModelDiscovery) }, note: - 'The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion available in an enabled Session. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` remains fixed-route. Each instance selects its backend through `provider` and reads model-selection settings through `modelSelectionSettings`. Every delegation returns a child id promptly; local children accept follow-ups, while external backends report their single execution automatically.', + 'The registered delegation name is the load-time `toolName` config (default `subagent`); backends supporting agentOptions always expose optional route fields, and the shared discovery plugin resolves the calling Session’s allowed routes. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` uses that same policy, and without an enabled policy explicit selection and discovery are restricted to the current parent route. Each instance selects its backend through `provider` and reads model-selection settings through `modelSelectionSettings`. Every delegation returns a child id promptly; local children accept follow-ups, while external backends report their single execution automatically.', }, { pkg: '@deepseek-ai/dsh-tool-subagent-control', diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/result.json b/scripts/snapshots/python-sdk-single-exe/advanced/result.json index 8c9eb58af3..bc0bfd16a4 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/result.json +++ b/scripts/snapshots/python-sdk-single-exe/advanced/result.json @@ -207,6 +207,7 @@ "job_kill", "job_list", "job_output", + "list_subagent_models", "snapshot_double", "subagent", "workflow", @@ -303,6 +304,7 @@ "job_kill", "job_list", "job_output", + "list_subagent_models", "snapshot_double", "subagent", "workflow", @@ -1725,6 +1727,7 @@ "job_kill", "job_list", "job_output", + "list_subagent_models", "snapshot_double", "subagent", "workflow", @@ -1851,6 +1854,7 @@ "job_kill", "job_list", "job_output", + "list_subagent_models", "snapshot_double", "subagent", "workflow", @@ -2873,6 +2877,7 @@ "job_kill", "job_list", "job_output", + "list_subagent_models", "snapshot_double", "subagent", "workflow", @@ -3631,6 +3636,7 @@ "job_kill", "job_list", "job_output", + "list_subagent_models", "snapshot_double", "subagent", "workflow", diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.1.v4.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.1.v4.jsonl index a169f4f81a..2c5e9c5f05 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.1.v4.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.1.v4.jsonl @@ -13,7 +13,7 @@ {"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin:serial-created-fixture"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"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 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: 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: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":"{{messageId}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","reasoningEffort":"high","maxTokens":256000},"adapterDefaults":{"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","snapshot_double","subagent","workflow","working_directory"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","reasoningEffort":"high","maxTokens":256000},"adapterDefaults":{"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","list_subagent_models","snapshot_double","subagent","workflow","working_directory"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} {"type":"session/title","data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[12],"source":{"kind":"fallback"}}} {"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{child-1}}","sessionFormatVersion":4,"throughSeq":16}} diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.2.v4.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.2.v4.jsonl index 88aaf9d5e2..5ec93b7783 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.2.v4.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.2.v4.jsonl @@ -13,7 +13,7 @@ {"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin:serial-created-fixture"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"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 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: 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: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":"{{messageId}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","reasoningEffort":"high","maxTokens":256000},"adapterDefaults":{"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","snapshot_double","subagent","workflow","working_directory"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","reasoningEffort":"high","maxTokens":256000},"adapterDefaults":{"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","list_subagent_models","snapshot_double","subagent","workflow","working_directory"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} {"type":"session/title","data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[12],"source":{"kind":"fallback"}}} {"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{child-2}}","sessionFormatVersion":4,"throughSeq":16}} diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.v4.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.v4.jsonl index 3a941d0115..b6dd8f6a4a 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.v4.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.v4.jsonl @@ -13,12 +13,12 @@ {"type":"user/message","data":{"content":[{"type":"text","text":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} {"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","width":1,"height":1,"bytes":69}}],"source":{"kind":"python-snapshot-image-offload"}},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","snapshot_double","subagent","workflow","working_directory"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","list_subagent_models","snapshot_double","subagent","workflow","working_directory"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} {"type":"session/title","data":{"title":"Run the advanced packaged-runtime snapsh","messageSeqs":[11],"source":{"kind":"fallback"}}} {"type":"assistant/attempt","data":{"turn":1,"step":1,"stream":[{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"code":"IMAGE_OFFLOAD_REQUIRED","message":"authored image budget failure","offloadImages":1}}}}]}} {"type":"image/offload","data":{"targets":[{"seq":13,"imageIndexes":[0]}]}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","snapshot_double","subagent","workflow","working_directory"]},"reason":"series"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","list_subagent_models","snapshot_double","subagent","workflow","working_directory"]},"reason":"series"}} {"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":4,"throughSeq":19}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-native","name":"snapshot_double","arguments":"{\"value\": 21}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-native","name":"snapshot_double","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-native","args":["{\"value\": 21}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-native","name":"snapshot_double","arguments":"{\"value\": 21}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"advanced-native","name":"snapshot_double","arguments":"{\"value\": 21}"}} 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..fc8d44ed52 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 @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{messageId}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Run three todo updates; the scheduler will reject the second call."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","subagent","todo_write","workflow","working_directory"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","list_subagent_models","subagent","todo_write","workflow","working_directory"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} {"type":"session/title","data":{"title":"Run three todo updates; the","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"scheduler-complete","name":"todo_write","arguments":"{\"todos\": []}"},{"type":"tool-call","id":"scheduler-fail","name":"todo_write","arguments":"{\"todos\": []}"},{"type":"tool-call","id":"scheduler-unstarted","name":"todo_write","arguments":"{\"todos\": []}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"},{"type":"tool-call"},{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"scheduler-complete","name":"todo_write","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"scheduler-complete","args":["{\"todos\": []}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"scheduler-complete","name":"todo_write","arguments":"{\"todos\": []}"}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":1,"dt":[],"id":"scheduler-fail","name":"todo_write","args":[""]},{"type":"tool-call-chunks","time0":0,"index":1,"dt":[],"id":"scheduler-fail","args":["{\"todos\": []}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"scheduler-fail","name":"todo_write","arguments":"{\"todos\": []}"}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":2,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":2,"dt":[],"id":"scheduler-unstarted","name":"todo_write","args":[""]},{"type":"tool-call-chunks","time0":0,"index":2,"dt":[],"id":"scheduler-unstarted","args":["{\"todos\": []}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":2,"block":{"type":"tool-call","id":"scheduler-unstarted","name":"todo_write","arguments":"{\"todos\": []}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"},{"type":"tool-call"},{"type":"tool-call"}]}}}]},"surfaceOp":"append"} diff --git a/snapshots/sdk/bash-tool/tool-schemas.expected.json b/snapshots/sdk/bash-tool/tool-schemas.expected.json index 2b4b1f069f..2dd1f341df 100644 --- a/snapshots/sdk/bash-tool/tool-schemas.expected.json +++ b/snapshots/sdk/bash-tool/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -364,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -379,6 +408,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/sdk/dynamic-tool-updates/notifications.expected.jsonl b/snapshots/sdk/dynamic-tool-updates/notifications.expected.jsonl index bfec6f1e36..b497f77dbd 100644 --- a/snapshots/sdk/dynamic-tool-updates/notifications.expected.jsonl +++ b/snapshots/sdk/dynamic-tool-updates/notifications.expected.jsonl @@ -6,7 +6,7 @@ {"method":"session.event","params":{"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"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Read task.txt, call the newly available snapshot_ping tool once, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} {"method":"session.event","params":{"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: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} -{"method":"session.event","params":{"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":["bash","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}}}} +{"method":"session.event","params":{"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":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"Read task.txt, call the newly","messageSeqs":[8],"source":{"kind":"fallback"}}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":13,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}],"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":"call_task_read","name":"read","args":["{\"file_path\":\"task.txt\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}}},{"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"}}} @@ -14,14 +14,14 @@ {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":15,"time":0,"data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_task_read"},"toolCallId":"call_task_read","content":[{"type":"text","text":"{{cwd}}/task.txt\nfile\n\n1: Reply with the single word DONE after reading this file.\n\n(End of file - total 1 lines)\n"}],"isError":false,"id":"{{sessionId}}"},"meta":{"path":"{{cwd}}/task.txt","offset":1,"lines":[{"number":1,"text":"Reply with the single word DONE after reading this file."}],"totalLines":1}},"sourceEventSeqs":[14],"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":16,"time":0,"data":{"turn":1,"step":1}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":17,"time":0,"data":{"turn":1,"step":2}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":18,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":18,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"developer/message","seq":19,"time":0,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"snapshot_ping"}],"role":"developer","id":"{{sessionId}}"},"headerSeq":18},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":20,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"call_dynamic_ping","name":"snapshot_ping","args":["{}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":21,"time":0,"data":{"turn":1,"step":2,"callId":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":22,"time":0,"data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"call_dynamic_ping"},"toolCallId":"call_dynamic_ping","content":[{"type":"text","text":"pong"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[21],"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":23,"time":0,"data":{"turn":1,"step":2}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":24,"time":0,"data":{"turn":1,"step":3}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":25,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":25,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"developer/message","seq":26,"time":0,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-removal","toolName":"snapshot_ping"}],"role":"developer","id":"{{sessionId}}"}},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":27,"time":0,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10}}},{"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":1,"step":3}}}} diff --git a/snapshots/sdk/dynamic-tool-updates/session.v4.jsonl b/snapshots/sdk/dynamic-tool-updates/session.v4.jsonl index aee79405da..8472750d16 100644 --- a/snapshots/sdk/dynamic-tool-updates/session.v4.jsonl +++ b/snapshots/sdk/dynamic-tool-updates/session.v4.jsonl @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Read task.txt, call the newly available snapshot_ping tool once, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Read task.txt, call the newly","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1790049391235,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790049391235,"index":0,"dt":[],"id":"call_task_read","name":"read","args":["{\"file_path\":\"task.txt\"}"]},{"type":"chunk","time":1790049391235,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}}},{"type":"chunk","time":1790049391235,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1790049391235,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} @@ -17,14 +17,14 @@ {"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_task_read"},"toolCallId":"call_task_read","content":[{"type":"text","text":"{{cwd}}/task.txt\nfile\n\n1: Reply with the single word DONE after reading this file.\n\n(End of file - total 1 lines)\n"}],"isError":false,"id":"{{message:5}}"},"meta":{"path":"{{cwd}}/task.txt","offset":1,"lines":[{"number":1,"text":"Reply with the single word DONE after reading this file."}],"totalLines":1}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"snapshot_ping"}],"role":"developer","id":"{{message:6}}"},"headerSeq":18},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"stream":[{"type":"chunk","time":1790049391255,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790049391255,"index":0,"dt":[],"id":"call_dynamic_ping","name":"snapshot_ping","args":["{}"]},{"type":"chunk","time":1790049391255,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}}},{"type":"chunk","time":1790049391255,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}} {"type":"tool/result","data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"call_dynamic_ping"},"toolCallId":"call_dynamic_ping","content":[{"type":"text","text":"pong"}],"isError":false,"id":"{{message:8}}"}},"sourceEventSeqs":[21],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-removal","toolName":"snapshot_ping"}],"role":"developer","id":"{{message:9}}"}},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10},"stream":[{"type":"chunk","time":1790049391270,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1790049391270,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1790049391270,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1790049391270,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10}}},{"type":"chunk","time":1790049391270,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} diff --git a/snapshots/sdk/persistent-tools/cordis.yml b/snapshots/sdk/persistent-tools/cordis.yml index 6262d2fb44..e2ee87e8a7 100644 --- a/snapshots/sdk/persistent-tools/cordis.yml +++ b/snapshots/sdk/persistent-tools/cordis.yml @@ -84,6 +84,10 @@ name: '@deepseek-ai/dsh-tool-subagent-control' disabled: true +- id: tool-subagent-list-models + name: '@deepseek-ai/dsh-tool-subagent/list-models' + disabled: true + - id: tool-subagent-list-agents name: '@deepseek-ai/dsh-tool-subagent-control/list-agents' disabled: true diff --git a/snapshots/sdk/subagent-activation-limit/tool-schemas.1.expected.json b/snapshots/sdk/subagent-activation-limit/tool-schemas.1.expected.json index 2b4b1f069f..2dd1f341df 100644 --- a/snapshots/sdk/subagent-activation-limit/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-activation-limit/tool-schemas.1.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -364,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -379,6 +408,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json b/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json index 2b4b1f069f..2dd1f341df 100644 --- a/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -364,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -379,6 +408,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json b/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json index 2b4b1f069f..2dd1f341df 100644 --- a/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -364,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -379,6 +408,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json b/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json index ed6f50a025..17dff0816f 100644 --- a/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json +++ b/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -364,7 +393,7 @@ }, { "name": "subagent_dsh_sdk", - "description": "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. The runtime notifies you when it finishes. The completion notice includes its final answer. This backend does not accept follow-up messages.", + "description": "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. The runtime notifies you when it finishes. The completion notice includes its final answer. This backend does not accept follow-up messages. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and this provider's route defaults. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -379,6 +408,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or this provider's route defaults." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or this provider's route defaults." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to use a compatible configured effort or the selected model's default." } }, "required": [ @@ -389,7 +430,7 @@ }, { "name": "subagent_dsh_sdk_second", - "description": "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. The runtime notifies you when it finishes. The completion notice includes its final answer. This backend does not accept follow-up messages.", + "description": "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. The runtime notifies you when it finishes. The completion notice includes its final answer. This backend does not accept follow-up messages. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and this provider's route defaults. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -404,6 +445,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or this provider's route defaults." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or this provider's route defaults." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to use a compatible configured effort or the selected model's default." } }, "required": [ @@ -414,7 +467,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -429,6 +482,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json index 2b4b1f069f..2dd1f341df 100644 --- a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -364,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -379,6 +408,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json index ce1cf8dfd5..87515e6d78 100644 --- a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json +++ b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json @@ -262,7 +262,7 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its advertised models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", "parameters": { "type": "object", "properties": { @@ -272,7 +272,7 @@ }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's advertised models." + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." } } } @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -408,6 +408,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json b/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json index 2b4b1f069f..2dd1f341df 100644 --- a/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -364,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -379,6 +408,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json b/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json index 2b4b1f069f..2dd1f341df 100644 --- a/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -364,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -379,6 +408,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/sdk/text-turn/tool-schemas.expected.json b/snapshots/sdk/text-turn/tool-schemas.expected.json index 2b4b1f069f..2dd1f341df 100644 --- a/snapshots/sdk/text-turn/tool-schemas.expected.json +++ b/snapshots/sdk/text-turn/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -364,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -379,6 +408,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/sdk/tool-error-details/notifications.expected.jsonl b/snapshots/sdk/tool-error-details/notifications.expected.jsonl index e6d6e7fde2..8d30d3d16c 100644 --- a/snapshots/sdk/tool-error-details/notifications.expected.jsonl +++ b/snapshots/sdk/tool-error-details/notifications.expected.jsonl @@ -6,7 +6,7 @@ {"method":"session.event","params":{"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"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Exercise a native denial and a caught PTC inner denial, then reply ERROR_DETAILS_OK."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} {"method":"session.event","params":{"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: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["bash","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"Exercise a native denial and","messageSeqs":[8],"source":{"kind":"fallback"}}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":13,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"denied-native","name":"bash","arguments":"{\"command\":\"echo AUTO_DENIED\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"denied-native","name":"bash","args":["{\"command\":\"echo AUTO_DENIED\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"denied-native","name":"bash","arguments":"{\"command\":\"echo AUTO_DENIED\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}} @@ -16,7 +16,7 @@ {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":17,"time":0,"data":{"turn":1,"step":2}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"system/message","seq":18,"time":0,"data":{"turn":1,"step":2,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[7],"surfaceOp":{"op":"replace","startSeq":7,"endSeq":7}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":19,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["run_code"]},"reason":"change","startsSeries":true}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"developer/message","seq":20,"time":0,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"run_code"},{"type":"tool-removal","toolName":"bash"},{"type":"tool-removal","toolName":"create_goal"},{"type":"tool-removal","toolName":"edit"},{"type":"tool-removal","toolName":"exit_plan_mode"},{"type":"tool-removal","toolName":"get_goal"},{"type":"tool-removal","toolName":"glob"},{"type":"tool-removal","toolName":"grep"},{"type":"tool-removal","toolName":"interrupt_agent"},{"type":"tool-removal","toolName":"job_kill"},{"type":"tool-removal","toolName":"job_list"},{"type":"tool-removal","toolName":"job_output"},{"type":"tool-removal","toolName":"list_agents"},{"type":"tool-removal","toolName":"read"},{"type":"tool-removal","toolName":"read_image"},{"type":"tool-removal","toolName":"send_message"},{"type":"tool-removal","toolName":"skill"},{"type":"tool-removal","toolName":"subagent"},{"type":"tool-removal","toolName":"subagent_fork"},{"type":"tool-removal","toolName":"todo_write"},{"type":"tool-removal","toolName":"update_goal"},{"type":"tool-removal","toolName":"web_fetch"},{"type":"tool-removal","toolName":"web_search"},{"type":"tool-removal","toolName":"workflow"},{"type":"tool-removal","toolName":"working_directory"},{"type":"tool-removal","toolName":"write"}],"role":"developer","id":"{{sessionId}}"},"headerSeq":19},"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"developer/message","seq":20,"time":0,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"run_code"},{"type":"tool-removal","toolName":"bash"},{"type":"tool-removal","toolName":"create_goal"},{"type":"tool-removal","toolName":"edit"},{"type":"tool-removal","toolName":"exit_plan_mode"},{"type":"tool-removal","toolName":"get_goal"},{"type":"tool-removal","toolName":"glob"},{"type":"tool-removal","toolName":"grep"},{"type":"tool-removal","toolName":"interrupt_agent"},{"type":"tool-removal","toolName":"job_kill"},{"type":"tool-removal","toolName":"job_list"},{"type":"tool-removal","toolName":"job_output"},{"type":"tool-removal","toolName":"list_agents"},{"type":"tool-removal","toolName":"list_subagent_models"},{"type":"tool-removal","toolName":"read"},{"type":"tool-removal","toolName":"read_image"},{"type":"tool-removal","toolName":"send_message"},{"type":"tool-removal","toolName":"skill"},{"type":"tool-removal","toolName":"subagent"},{"type":"tool-removal","toolName":"subagent_fork"},{"type":"tool-removal","toolName":"todo_write"},{"type":"tool-removal","toolName":"update_goal"},{"type":"tool-removal","toolName":"web_fetch"},{"type":"tool-removal","toolName":"web_search"},{"type":"tool-removal","toolName":"workflow"},{"type":"tool-removal","toolName":"working_directory"},{"type":"tool-removal","toolName":"write"}],"role":"developer","id":"{{sessionId}}"},"headerSeq":19},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":21,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"denied-code","name":"run_code","arguments":"{\"code\":\"try { await tools.bash({command: \\\"echo AUTO_DENIED\\\"}) } catch (error) { return error.message }\",\"description\":\"Catch one denied inner call\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"denied-code","name":"run_code","args":["{\"code\":\"try { await tools.bash({command: \\\"echo AUTO_DENIED\\\"}) } catch (error) { return error.message }\",\"description\":\"Catch one denied inner call\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"denied-code","name":"run_code","arguments":"{\"code\":\"try { await tools.bash({command: \\\"echo AUTO_DENIED\\\"}) } catch (error) { return error.message }\",\"description\":\"Catch one denied inner call\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":22,"time":0,"data":{"turn":1,"step":2,"callId":"denied-code","name":"run_code","arguments":"{\"code\":\"try { await tools.bash({command: \\\"echo AUTO_DENIED\\\"}) } catch (error) { return error.message }\",\"description\":\"Catch one denied inner call\"}"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/ptc-dispatch-start","seq":23,"time":0,"data":{"rootCallId":"denied-code","parentCallId":"denied-code","subCallId":"denied-code:ptc:1","name":"bash","arguments":{"command":"echo AUTO_DENIED"}}}}} diff --git a/snapshots/sdk/tool-error-details/session.v4.jsonl b/snapshots/sdk/tool-error-details/session.v4.jsonl index 871095019b..806aa48ff4 100644 --- a/snapshots/sdk/tool-error-details/session.v4.jsonl +++ b/snapshots/sdk/tool-error-details/session.v4.jsonl @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Exercise a native denial and a caught PTC inner denial, then reply ERROR_DETAILS_OK."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["bash","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Exercise a native denial and","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"denied-native","name":"bash","arguments":"{\"command\":\"echo AUTO_DENIED\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"stream":[{"type":"chunk","time":1791315723125,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791315723125,"index":0,"dt":[],"id":"denied-native","name":"bash","args":["{\"command\":\"echo AUTO_DENIED\"}"]},{"type":"chunk","time":1791315723125,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"denied-native","name":"bash","arguments":"{\"command\":\"echo AUTO_DENIED\"}"}}},{"type":"chunk","time":1791315723125,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} @@ -19,7 +19,7 @@ {"type":"step/start","data":{"turn":1,"step":2}} {"type":"system/message","data":{"turn":1,"step":2,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:6}}"}},"sourceEventSeqs":[7],"surfaceOp":{"op":"replace","startSeq":7,"endSeq":7}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["run_code"]},"reason":"change","startsSeries":true}} -{"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"run_code"},{"type":"tool-removal","toolName":"bash"},{"type":"tool-removal","toolName":"create_goal"},{"type":"tool-removal","toolName":"edit"},{"type":"tool-removal","toolName":"exit_plan_mode"},{"type":"tool-removal","toolName":"get_goal"},{"type":"tool-removal","toolName":"glob"},{"type":"tool-removal","toolName":"grep"},{"type":"tool-removal","toolName":"interrupt_agent"},{"type":"tool-removal","toolName":"job_kill"},{"type":"tool-removal","toolName":"job_list"},{"type":"tool-removal","toolName":"job_output"},{"type":"tool-removal","toolName":"list_agents"},{"type":"tool-removal","toolName":"read"},{"type":"tool-removal","toolName":"read_image"},{"type":"tool-removal","toolName":"send_message"},{"type":"tool-removal","toolName":"skill"},{"type":"tool-removal","toolName":"subagent"},{"type":"tool-removal","toolName":"subagent_fork"},{"type":"tool-removal","toolName":"todo_write"},{"type":"tool-removal","toolName":"update_goal"},{"type":"tool-removal","toolName":"web_fetch"},{"type":"tool-removal","toolName":"web_search"},{"type":"tool-removal","toolName":"workflow"},{"type":"tool-removal","toolName":"working_directory"},{"type":"tool-removal","toolName":"write"}],"role":"developer","id":"{{message:7}}"},"headerSeq":19},"surfaceOp":"append"} +{"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"run_code"},{"type":"tool-removal","toolName":"bash"},{"type":"tool-removal","toolName":"create_goal"},{"type":"tool-removal","toolName":"edit"},{"type":"tool-removal","toolName":"exit_plan_mode"},{"type":"tool-removal","toolName":"get_goal"},{"type":"tool-removal","toolName":"glob"},{"type":"tool-removal","toolName":"grep"},{"type":"tool-removal","toolName":"interrupt_agent"},{"type":"tool-removal","toolName":"job_kill"},{"type":"tool-removal","toolName":"job_list"},{"type":"tool-removal","toolName":"job_output"},{"type":"tool-removal","toolName":"list_agents"},{"type":"tool-removal","toolName":"list_subagent_models"},{"type":"tool-removal","toolName":"read"},{"type":"tool-removal","toolName":"read_image"},{"type":"tool-removal","toolName":"send_message"},{"type":"tool-removal","toolName":"skill"},{"type":"tool-removal","toolName":"subagent"},{"type":"tool-removal","toolName":"subagent_fork"},{"type":"tool-removal","toolName":"todo_write"},{"type":"tool-removal","toolName":"update_goal"},{"type":"tool-removal","toolName":"web_fetch"},{"type":"tool-removal","toolName":"web_search"},{"type":"tool-removal","toolName":"workflow"},{"type":"tool-removal","toolName":"working_directory"},{"type":"tool-removal","toolName":"write"}],"role":"developer","id":"{{message:7}}"},"headerSeq":19},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"denied-code","name":"run_code","arguments":"{\"code\":\"try { await tools.bash({command: \\\"echo AUTO_DENIED\\\"}) } catch (error) { return error.message }\",\"description\":\"Catch one denied inner call\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"stream":[{"type":"chunk","time":1791315723151,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791315723151,"index":0,"dt":[],"id":"denied-code","name":"run_code","args":["{\"code\":\"try { await tools.bash({command: \\\"echo AUTO_DENIED\\\"}) } catch (error) { return error.message }\",\"description\":\"Catch one denied inner call\"}"]},{"type":"chunk","time":1791315723151,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"denied-code","name":"run_code","arguments":"{\"code\":\"try { await tools.bash({command: \\\"echo AUTO_DENIED\\\"}) } catch (error) { return error.message }\",\"description\":\"Catch one denied inner call\"}"}}},{"type":"chunk","time":1791315723151,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"denied-code","name":"run_code","arguments":"{\"code\":\"try { await tools.bash({command: \\\"echo AUTO_DENIED\\\"}) } catch (error) { return error.message }\",\"description\":\"Catch one denied inner call\"}"}} {"type":"tool/ptc-dispatch-start","data":{"rootCallId":"denied-code","parentCallId":"denied-code","subCallId":"denied-code:ptc:1","name":"bash","arguments":{"command":"echo AUTO_DENIED"}}} diff --git a/snapshots/sdk/tool-error-details/system-prompt.expected.md b/snapshots/sdk/tool-error-details/system-prompt.expected.md index 9070405e5b..b0a8db4970 100644 --- a/snapshots/sdk/tool-error-details/system-prompt.expected.md +++ b/snapshots/sdk/tool-error-details/system-prompt.expected.md @@ -173,6 +173,13 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; + /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + list_subagent_models: { + /** Registered LLM provider id. Omit to list providers. */ + provider?: string; + /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + model?: string; + } & Record; /** Read a UTF-8 text file and return line-numbered content. */ read: { /** Path to read, resolved by the filesystem backend. */ @@ -199,7 +206,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; 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 id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -207,8 +214,14 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -216,6 +229,12 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; /** Record and update a task list to plan multi-step work and show progress; skip it for trivial single-step tasks. Add one todo per concrete step before you start. While work remains, keep the todos being worked on `in_progress`, several only when work runs in parallel. Mark each todo `completed` as soon as it is done. */ todo_write: { @@ -440,6 +459,7 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; + list_subagent_models: string; read: { /** Canonical absolute path in the filesystem execution world. */ path: string; diff --git a/snapshots/sdk/tool-error-details/tool-schemas.expected.json b/snapshots/sdk/tool-error-details/tool-schemas.expected.json index e150d82593..5e9eaf2f00 100644 --- a/snapshots/sdk/tool-error-details/tool-schemas.expected.json +++ b/snapshots/sdk/tool-error-details/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -364,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -379,6 +408,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/advanced-toolchain/session.v4.jsonl b/snapshots/session/advanced-toolchain/session.v4.jsonl index c0f1a22b8d..b9591d4802 100644 --- a/snapshots/session/advanced-toolchain/session.v4.jsonl +++ b/snapshots/session/advanced-toolchain/session.v4.jsonl @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Run this advanced flow exactly once: list runtime inspect providers; discover providers through run_code when available; delegate once to a direct spawn child; run one workflow that delegates to another spawn child; list providers again; then reply with exactly ADVANCED_HEADLESS_OK."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["bash","cordis_inspect_list","cordis_inspect_query","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["bash","cordis_inspect_list","cordis_inspect_query","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Run this advanced flow exactly","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-inspect-before","name":"cordis_inspect_list","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1791315715021,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791315715022,"index":0,"dt":[],"id":"advanced-inspect-before","name":"cordis_inspect_list","args":["{}"]},{"type":"chunk","time":1791315715022,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-inspect-before","name":"cordis_inspect_list","arguments":"{}"}}},{"type":"chunk","time":1791315715022,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1791315715022,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} @@ -19,7 +19,7 @@ {"type":"step/start","data":{"turn":1,"step":2}} {"type":"system/message","data":{"turn":1,"step":2,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:6}}"}},"sourceEventSeqs":[7],"surfaceOp":{"op":"replace","startSeq":7,"endSeq":7}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["run_code"]},"reason":"change","startsSeries":true}} -{"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"run_code"},{"type":"tool-removal","toolName":"bash"},{"type":"tool-removal","toolName":"cordis_inspect_list"},{"type":"tool-removal","toolName":"cordis_inspect_query"},{"type":"tool-removal","toolName":"create_goal"},{"type":"tool-removal","toolName":"edit"},{"type":"tool-removal","toolName":"exit_plan_mode"},{"type":"tool-removal","toolName":"get_goal"},{"type":"tool-removal","toolName":"glob"},{"type":"tool-removal","toolName":"grep"},{"type":"tool-removal","toolName":"interrupt_agent"},{"type":"tool-removal","toolName":"job_kill"},{"type":"tool-removal","toolName":"job_list"},{"type":"tool-removal","toolName":"job_output"},{"type":"tool-removal","toolName":"list_agents"},{"type":"tool-removal","toolName":"read"},{"type":"tool-removal","toolName":"read_image"},{"type":"tool-removal","toolName":"send_message"},{"type":"tool-removal","toolName":"skill"},{"type":"tool-removal","toolName":"subagent"},{"type":"tool-removal","toolName":"subagent_fork"},{"type":"tool-removal","toolName":"todo_write"},{"type":"tool-removal","toolName":"update_goal"},{"type":"tool-removal","toolName":"web_fetch"},{"type":"tool-removal","toolName":"web_search"},{"type":"tool-removal","toolName":"workflow"},{"type":"tool-removal","toolName":"working_directory"},{"type":"tool-removal","toolName":"write"}],"role":"developer","id":"{{message:7}}"},"headerSeq":19},"surfaceOp":"append"} +{"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"run_code"},{"type":"tool-removal","toolName":"bash"},{"type":"tool-removal","toolName":"cordis_inspect_list"},{"type":"tool-removal","toolName":"cordis_inspect_query"},{"type":"tool-removal","toolName":"create_goal"},{"type":"tool-removal","toolName":"edit"},{"type":"tool-removal","toolName":"exit_plan_mode"},{"type":"tool-removal","toolName":"get_goal"},{"type":"tool-removal","toolName":"glob"},{"type":"tool-removal","toolName":"grep"},{"type":"tool-removal","toolName":"interrupt_agent"},{"type":"tool-removal","toolName":"job_kill"},{"type":"tool-removal","toolName":"job_list"},{"type":"tool-removal","toolName":"job_output"},{"type":"tool-removal","toolName":"list_agents"},{"type":"tool-removal","toolName":"list_subagent_models"},{"type":"tool-removal","toolName":"read"},{"type":"tool-removal","toolName":"read_image"},{"type":"tool-removal","toolName":"send_message"},{"type":"tool-removal","toolName":"skill"},{"type":"tool-removal","toolName":"subagent"},{"type":"tool-removal","toolName":"subagent_fork"},{"type":"tool-removal","toolName":"todo_write"},{"type":"tool-removal","toolName":"update_goal"},{"type":"tool-removal","toolName":"web_fetch"},{"type":"tool-removal","toolName":"web_search"},{"type":"tool-removal","toolName":"workflow"},{"type":"tool-removal","toolName":"working_directory"},{"type":"tool-removal","toolName":"write"}],"role":"developer","id":"{{message:7}}"},"headerSeq":19},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"const inspected = await tools.cordis_inspect_list({}); return inspected.providers.map(provider => provider.id);\",\"description\":\"Discover runtime APIs through PTC\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1791315715055,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791315715055,"index":0,"dt":[],"id":"advanced-code","name":"run_code","args":["{\"code\":\"const inspected = await tools.cordis_inspect_list({}); return inspected.providers.map(provider => provider.id);\",\"description\":\"Discover runtime APIs through PTC\"}"]},{"type":"chunk","time":1791315715055,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"const inspected = await tools.cordis_inspect_list({}); return inspected.providers.map(provider => provider.id);\",\"description\":\"Discover runtime APIs through PTC\"}"}}},{"type":"chunk","time":1791315715055,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1791315715055,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"advanced-code","name":"run_code","arguments":"{\"code\":\"const inspected = await tools.cordis_inspect_list({}); return inspected.providers.map(provider => provider.id);\",\"description\":\"Discover runtime APIs through PTC\"}"}} {"type":"tool/ptc-dispatch-start","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:ptc:1","name":"cordis_inspect_list","arguments":{}}} @@ -28,8 +28,8 @@ {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} {"type":"system/message","data":{"turn":1,"step":3,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:10}}"}},"sourceEventSeqs":[18],"surfaceOp":{"op":"replace","startSeq":18,"endSeq":18}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["bash","cordis_inspect_list","cordis_inspect_query","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change","startsSeries":true}} -{"type":"developer/message","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"bash"},{"type":"tool-addition","toolName":"cordis_inspect_list"},{"type":"tool-addition","toolName":"cordis_inspect_query"},{"type":"tool-addition","toolName":"create_goal"},{"type":"tool-addition","toolName":"edit"},{"type":"tool-addition","toolName":"exit_plan_mode"},{"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":"web_fetch"},{"type":"tool-addition","toolName":"web_search"},{"type":"tool-addition","toolName":"workflow"},{"type":"tool-addition","toolName":"working_directory"},{"type":"tool-addition","toolName":"write"},{"type":"tool-removal","toolName":"run_code"}],"role":"developer","id":"{{message:11}}"},"headerSeq":29},"surfaceOp":"append"} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["bash","cordis_inspect_list","cordis_inspect_query","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change","startsSeries":true}} +{"type":"developer/message","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"bash"},{"type":"tool-addition","toolName":"cordis_inspect_list"},{"type":"tool-addition","toolName":"cordis_inspect_query"},{"type":"tool-addition","toolName":"create_goal"},{"type":"tool-addition","toolName":"edit"},{"type":"tool-addition","toolName":"exit_plan_mode"},{"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":"list_subagent_models"},{"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":"web_fetch"},{"type":"tool-addition","toolName":"web_search"},{"type":"tool-addition","toolName":"workflow"},{"type":"tool-addition","toolName":"working_directory"},{"type":"tool-addition","toolName":"write"},{"type":"tool-removal","toolName":"run_code"}],"role":"developer","id":"{{message:11}}"},"headerSeq":29},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:12}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1791315715200,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791315715200,"index":0,"dt":[],"id":"advanced-direct-child","name":"subagent","args":["{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"]},{"type":"chunk","time":1791315715200,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1791315715200,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1791315715200,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":3,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}} {"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1791315715221,"mode":"continuable","label":"Check direct child"}} diff --git a/snapshots/session/advanced-toolchain/system-prompt.expected.md b/snapshots/session/advanced-toolchain/system-prompt.expected.md index cf4dcd9c3f..68033a12e2 100644 --- a/snapshots/session/advanced-toolchain/system-prompt.expected.md +++ b/snapshots/session/advanced-toolchain/system-prompt.expected.md @@ -186,6 +186,13 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; + /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + list_subagent_models: { + /** Registered LLM provider id. Omit to list providers. */ + provider?: string; + /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + model?: string; + } & Record; /** Read a UTF-8 text file and return line-numbered content. */ read: { /** Path to read, resolved by the filesystem backend. */ @@ -212,7 +219,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; 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 id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -220,8 +227,14 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -229,6 +242,12 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; /** Record and update a task list to plan multi-step work and show progress; skip it for trivial single-step tasks. Add one todo per concrete step before you start. While work remains, keep the todos being worked on `in_progress`, several only when work runs in parallel. Mark each todo `completed` as soon as it is done. */ todo_write: { @@ -455,6 +474,7 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; + list_subagent_models: string; read: { /** Canonical absolute path in the filesystem execution world. */ path: string; diff --git a/snapshots/session/advanced-toolchain/tool-schemas.expected.json b/snapshots/session/advanced-toolchain/tool-schemas.expected.json index cb349d3ca4..c0890029de 100644 --- a/snapshots/session/advanced-toolchain/tool-schemas.expected.json +++ b/snapshots/session/advanced-toolchain/tool-schemas.expected.json @@ -301,6 +301,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -380,7 +397,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -395,6 +412,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -405,7 +434,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -420,6 +449,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -1014,6 +1055,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -1093,7 +1151,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -1108,6 +1166,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -1118,7 +1188,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -1133,6 +1203,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/agent-instructions/tool-schemas.expected.json b/snapshots/session/agent-instructions/tool-schemas.expected.json index 00e71002a7..472458e753 100644 --- a/snapshots/session/agent-instructions/tool-schemas.expected.json +++ b/snapshots/session/agent-instructions/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -364,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -379,6 +408,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -893,6 +934,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -972,7 +1030,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -987,6 +1045,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -997,7 +1067,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -1012,6 +1082,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/background-confinement-failure/tool-schemas.expected.json b/snapshots/session/background-confinement-failure/tool-schemas.expected.json index 2b4b1f069f..2dd1f341df 100644 --- a/snapshots/session/background-confinement-failure/tool-schemas.expected.json +++ b/snapshots/session/background-confinement-failure/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -364,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -379,6 +408,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/both-mode-turn/system-prompt.expected.md b/snapshots/session/both-mode-turn/system-prompt.expected.md index 329e451ce8..703d69b43f 100644 --- a/snapshots/session/both-mode-turn/system-prompt.expected.md +++ b/snapshots/session/both-mode-turn/system-prompt.expected.md @@ -140,6 +140,13 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; + /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + list_subagent_models: { + /** Registered LLM provider id. Omit to list providers. */ + provider?: string; + /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + model?: string; + } & Record; /** Read a UTF-8 text file and return line-numbered content. */ read: { /** Path to read, resolved by the filesystem backend. */ @@ -166,7 +173,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; 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 id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -174,8 +181,14 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -183,6 +196,12 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; /** Record and update a task list to plan multi-step work and show progress; skip it for trivial single-step tasks. Add one todo per concrete step before you start. While work remains, keep the todos being worked on `in_progress`, several only when work runs in parallel. Mark each todo `completed` as soon as it is done. */ todo_write: { @@ -407,6 +426,7 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; + list_subagent_models: string; read: { /** Canonical absolute path in the filesystem execution world. */ path: string; diff --git a/snapshots/session/browser-use-chrome-devtools-mcp/tool-schemas.expected.json b/snapshots/session/browser-use-chrome-devtools-mcp/tool-schemas.expected.json index cd473bea70..a8ef352d5e 100644 --- a/snapshots/session/browser-use-chrome-devtools-mcp/tool-schemas.expected.json +++ b/snapshots/session/browser-use-chrome-devtools-mcp/tool-schemas.expected.json @@ -300,6 +300,23 @@ ] } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "mcp__chrome-devtools-mcp__take_screenshot", "description": "Take a screenshot of the page or element.", @@ -446,7 +463,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -461,6 +478,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -471,7 +500,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -486,6 +515,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/browser-use-playwright-mcp/tool-schemas.expected.json b/snapshots/session/browser-use-playwright-mcp/tool-schemas.expected.json index 2d24d7c17d..3d46faeef4 100644 --- a/snapshots/session/browser-use-playwright-mcp/tool-schemas.expected.json +++ b/snapshots/session/browser-use-playwright-mcp/tool-schemas.expected.json @@ -300,6 +300,23 @@ ] } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "mcp__playwright-mcp__browser_take_screenshot", "description": "Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.", @@ -448,7 +465,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -463,6 +480,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -473,7 +502,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -488,6 +517,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/browser-use-stagehand-native/tool-schemas.expected.json b/snapshots/session/browser-use-stagehand-native/tool-schemas.expected.json index e1acbc8206..33c0bdbb5a 100644 --- a/snapshots/session/browser-use-stagehand-native/tool-schemas.expected.json +++ b/snapshots/session/browser-use-stagehand-native/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -552,7 +569,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -567,6 +584,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -577,7 +606,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -592,6 +621,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/claude-code-mods/tool-schemas.expected.json b/snapshots/session/claude-code-mods/tool-schemas.expected.json index 2b4b1f069f..2dd1f341df 100644 --- a/snapshots/session/claude-code-mods/tool-schemas.expected.json +++ b/snapshots/session/claude-code-mods/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -364,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -379,6 +408,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/compaction-output-reserve/cordis.snapshot.yml b/snapshots/session/compaction-output-reserve/cordis.snapshot.yml index 3dce48221f..15f69e1913 100644 --- a/snapshots/session/compaction-output-reserve/cordis.snapshot.yml +++ b/snapshots/session/compaction-output-reserve/cordis.snapshot.yml @@ -1,5 +1,5 @@ # With 1,800 tokens of headroom, a 1,000-token output reservation lowers the -# trigger from 6,782 to 5,782, between the history and its condensed replacement. +# trigger from 7,470 to 6,470, between the history and its condensed replacement. - id: agent-instructions disabled: true @@ -31,7 +31,7 @@ name: DeepSeek models: - id: deepseek-v4-flash - contextWindow: 8582 + contextWindow: 9270 defaultMaxTokens: 1000 - id: compaction-working-directory name: '../../../packages/test-support/session-snapshot/tests/fixtures/compaction-working-directory.ts' diff --git a/snapshots/session/compaction-output-reserve/cordis.yml b/snapshots/session/compaction-output-reserve/cordis.yml index a8d2088083..ea561fc74f 100644 --- a/snapshots/session/compaction-output-reserve/cordis.yml +++ b/snapshots/session/compaction-output-reserve/cordis.yml @@ -7,7 +7,7 @@ config: models: - id: deepseek-v4-flash - contextWindow: 8582 + contextWindow: 9270 maxTokens: 1000 - id: compaction-basic diff --git a/snapshots/session/compaction-output-reserve/session.v4.jsonl b/snapshots/session/compaction-output-reserve/session.v4.jsonl index 4b9830d6ce..ed3f0c8a16 100644 --- a/snapshots/session/compaction-output-reserve/session.v4.jsonl +++ b/snapshots/session/compaction-output-reserve/session.v4.jsonl @@ -11,7 +11,7 @@ {"type":"user/message","data":{"content":[{"type":"text","text":"Establish a durable compaction premise before continuing. Record every part of this historical evidence: the snapshot uses keyless replay; persistence uses JSONL; the assembled headless application loads its real Cordis composition; model-visible inputs remain logged; tool calls and results remain paired and ordered; output reservation reduces the message budget before proactive compaction; compaction opens with compaction/start and closes with compaction/end; a successful auxiliary summary records compaction/summary source metadata; the replacement surface shadows only an older balanced range; the checkpoint remains smaller than the history it replaces; the newest tool result remains verbatim; the next request sees that checkpoint; the final response proves the same turn continued; deterministic snapshot evidence stays separate from the live-provider smoke; and no external API key is needed. Emit one alpha marker through bash, then finish the task after proactive compaction with the exact words PROACTIVE COMPACTION COMPLETE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"/tmp\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"/tmp\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":1000},"adapterDefaults":{"maxTokens":true},"tools":"{{tools}}"},"reason":"initial"}} -{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":8582}} +{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":9270}} {"type":"session/title","data":{"title":"Establish a durable compaction premise","messageSeqs":[9],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_compaction_marker","name":"bash","arguments":"{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":24,"outputTokens":6},"stream":[{"type":"chunk","time":1791185986560,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791185986560,"index":0,"dt":[],"id":"call_compaction_marker","name":"bash","args":["{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"]},{"type":"chunk","time":1791185986560,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_compaction_marker","name":"bash","arguments":"{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"}}},{"type":"chunk","time":1791185986560,"chunk":{"type":"usage","usage":{"inputTokens":24,"outputTokens":6}}},{"type":"chunk","time":1791185986560,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_compaction_marker","name":"bash","arguments":"{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"}} diff --git a/snapshots/session/compaction-recovery/tool-schemas.expected.json b/snapshots/session/compaction-recovery/tool-schemas.expected.json index 00e71002a7..472458e753 100644 --- a/snapshots/session/compaction-recovery/tool-schemas.expected.json +++ b/snapshots/session/compaction-recovery/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -364,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -379,6 +408,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -893,6 +934,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -972,7 +1030,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -987,6 +1045,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -997,7 +1067,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -1012,6 +1082,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/compaction-summary-headroom/cordis.snapshot.yml b/snapshots/session/compaction-summary-headroom/cordis.snapshot.yml index 765a3f7e8a..76355f3bb4 100644 --- a/snapshots/session/compaction-summary-headroom/cordis.snapshot.yml +++ b/snapshots/session/compaction-summary-headroom/cordis.snapshot.yml @@ -1,5 +1,5 @@ # The summary output cap inherits the 1,800-token headroom. A 1,000-token -# request output reservation lowers the proactive trigger to 5,782 tokens. +# request output reservation lowers the proactive trigger to 6,470 tokens. - id: agent-instructions disabled: true @@ -30,7 +30,7 @@ name: DeepSeek models: - id: deepseek-v4-flash - contextWindow: 8582 + contextWindow: 9270 defaultMaxTokens: 1000 - id: compaction-working-directory name: '../../../packages/test-support/session-snapshot/tests/fixtures/compaction-working-directory.ts' diff --git a/snapshots/session/compaction-summary-headroom/cordis.yml b/snapshots/session/compaction-summary-headroom/cordis.yml index f65c90899e..da55c9afc7 100644 --- a/snapshots/session/compaction-summary-headroom/cordis.yml +++ b/snapshots/session/compaction-summary-headroom/cordis.yml @@ -7,7 +7,7 @@ config: models: - id: deepseek-v4-flash - contextWindow: 8582 + contextWindow: 9270 maxTokens: 1000 - id: compaction-basic diff --git a/snapshots/session/compaction-summary-headroom/session.v4.jsonl b/snapshots/session/compaction-summary-headroom/session.v4.jsonl index 2891a589e1..7d2da30924 100644 --- a/snapshots/session/compaction-summary-headroom/session.v4.jsonl +++ b/snapshots/session/compaction-summary-headroom/session.v4.jsonl @@ -11,7 +11,7 @@ {"type":"user/message","data":{"content":[{"type":"text","text":"Establish a durable compaction premise before continuing. Record every part of this historical evidence: the snapshot uses keyless replay; persistence uses JSONL; the assembled headless application loads its real Cordis composition; model-visible inputs remain logged; tool calls and results remain paired and ordered; output reservation reduces the message budget before proactive compaction; compaction opens with compaction/start and closes with compaction/end; a successful auxiliary summary records compaction/summary source metadata; the replacement surface shadows only an older balanced range; the checkpoint remains smaller than the history it replaces; the newest tool result remains verbatim; the next request sees that checkpoint; the final response proves the same turn continued; deterministic snapshot evidence stays separate from the live-provider smoke; and no external API key is needed. Emit one alpha marker through bash, then finish the task after proactive compaction with the exact words PROACTIVE COMPACTION COMPLETE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"/tmp\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"/tmp\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":1000},"adapterDefaults":{"maxTokens":true},"tools":"{{tools}}"},"reason":"initial"}} -{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":8582}} +{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":9270}} {"type":"session/title","data":{"title":"Establish a durable compaction premise","messageSeqs":[9],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_compaction_marker","name":"bash","arguments":"{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":24,"outputTokens":6},"stream":[{"type":"chunk","time":1791187853875,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791187853875,"index":0,"dt":[],"id":"call_compaction_marker","name":"bash","args":["{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"]},{"type":"chunk","time":1791187853875,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_compaction_marker","name":"bash","arguments":"{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"}}},{"type":"chunk","time":1791187853875,"chunk":{"type":"usage","usage":{"inputTokens":24,"outputTokens":6}}},{"type":"chunk","time":1791187853875,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_compaction_marker","name":"bash","arguments":"{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"}} diff --git a/snapshots/session/computer-use-cua-driver-mcp/tool-schemas.expected.json b/snapshots/session/computer-use-cua-driver-mcp/tool-schemas.expected.json index 4b839adb31..9a7dc19de7 100644 --- a/snapshots/session/computer-use-cua-driver-mcp/tool-schemas.expected.json +++ b/snapshots/session/computer-use-cua-driver-mcp/tool-schemas.expected.json @@ -300,6 +300,23 @@ ] } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "mcp__cua-driver-mcp__disconnect", "description": "Disconnect the fixture.", @@ -425,7 +442,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -440,6 +457,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -450,7 +479,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -465,6 +494,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/computer-use-cua-driver-native/tool-schemas.expected.json b/snapshots/session/computer-use-cua-driver-native/tool-schemas.expected.json index 802ed2fe0d..a95daa24a6 100644 --- a/snapshots/session/computer-use-cua-driver-native/tool-schemas.expected.json +++ b/snapshots/session/computer-use-cua-driver-native/tool-schemas.expected.json @@ -306,6 +306,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -385,7 +402,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -400,6 +417,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -410,7 +439,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -425,6 +454,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json b/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json index 5d880fdde7..da58565d4e 100644 --- a/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json +++ b/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json @@ -301,6 +301,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -380,7 +397,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -395,6 +412,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -405,7 +434,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -420,6 +449,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/deepseek-messages-system-prompt/tool-schemas.expected.json b/snapshots/session/deepseek-messages-system-prompt/tool-schemas.expected.json index 2b4b1f069f..2dd1f341df 100644 --- a/snapshots/session/deepseek-messages-system-prompt/tool-schemas.expected.json +++ b/snapshots/session/deepseek-messages-system-prompt/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -364,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -379,6 +408,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/deepseek-protocol-system-prompt/tool-schemas.expected.json b/snapshots/session/deepseek-protocol-system-prompt/tool-schemas.expected.json index 2b4b1f069f..2dd1f341df 100644 --- a/snapshots/session/deepseek-protocol-system-prompt/tool-schemas.expected.json +++ b/snapshots/session/deepseek-protocol-system-prompt/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -364,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -379,6 +408,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/dynamic-tool-prompt-updates/session.v4.jsonl b/snapshots/session/dynamic-tool-prompt-updates/session.v4.jsonl index cff0e5a329..53e2ccd74b 100644 --- a/snapshots/session/dynamic-tool-prompt-updates/session.v4.jsonl +++ b/snapshots/session/dynamic-tool-prompt-updates/session.v4.jsonl @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Read task.txt, call the newly available snapshot_ping tool once, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","systemPromptUpdate":"in-history"}} {"type":"session/title","data":{"title":"Read task.txt, call the newly","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1790149644504,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790149644504,"index":0,"dt":[],"id":"call_task_read","name":"read","args":["{\"file_path\":\"task.txt\"}"]},{"type":"chunk","time":1790149644504,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}}},{"type":"chunk","time":1790149644504,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1790149644504,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} @@ -18,14 +18,14 @@ {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} {"type":"system/message","data":{"turn":1,"step":2,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:6}}"}},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"snapshot_ping"}],"role":"developer","id":"{{message:7}}"},"headerSeq":19},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"stream":[{"type":"chunk","time":1790149644534,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790149644534,"index":0,"dt":[],"id":"call_dynamic_ping","name":"snapshot_ping","args":["{}"]},{"type":"chunk","time":1790149644534,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}}},{"type":"chunk","time":1790149644535,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}} {"type":"tool/result","data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"call_dynamic_ping"},"toolCallId":"call_dynamic_ping","content":[{"type":"text","text":"pong"}],"isError":false,"id":"{{message:9}}"}},"sourceEventSeqs":[22],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-removal","toolName":"snapshot_ping"}],"role":"developer","id":"{{message:10}}"}},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10},"stream":[{"type":"chunk","time":1790149644557,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1790149644557,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1790149644557,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1790149644557,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10}}},{"type":"chunk","time":1790149644557,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} diff --git a/snapshots/session/dynamic-tool-updates/session.v4.jsonl b/snapshots/session/dynamic-tool-updates/session.v4.jsonl index aee79405da..8472750d16 100644 --- a/snapshots/session/dynamic-tool-updates/session.v4.jsonl +++ b/snapshots/session/dynamic-tool-updates/session.v4.jsonl @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Read task.txt, call the newly available snapshot_ping tool once, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Read task.txt, call the newly","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1790049391235,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790049391235,"index":0,"dt":[],"id":"call_task_read","name":"read","args":["{\"file_path\":\"task.txt\"}"]},{"type":"chunk","time":1790049391235,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}}},{"type":"chunk","time":1790049391235,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1790049391235,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} @@ -17,14 +17,14 @@ {"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_task_read"},"toolCallId":"call_task_read","content":[{"type":"text","text":"{{cwd}}/task.txt\nfile\n\n1: Reply with the single word DONE after reading this file.\n\n(End of file - total 1 lines)\n"}],"isError":false,"id":"{{message:5}}"},"meta":{"path":"{{cwd}}/task.txt","offset":1,"lines":[{"number":1,"text":"Reply with the single word DONE after reading this file."}],"totalLines":1}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"snapshot_ping"}],"role":"developer","id":"{{message:6}}"},"headerSeq":18},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"stream":[{"type":"chunk","time":1790049391255,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790049391255,"index":0,"dt":[],"id":"call_dynamic_ping","name":"snapshot_ping","args":["{}"]},{"type":"chunk","time":1790049391255,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}}},{"type":"chunk","time":1790049391255,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}} {"type":"tool/result","data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"call_dynamic_ping"},"toolCallId":"call_dynamic_ping","content":[{"type":"text","text":"pong"}],"isError":false,"id":"{{message:8}}"}},"sourceEventSeqs":[21],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-removal","toolName":"snapshot_ping"}],"role":"developer","id":"{{message:9}}"}},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10},"stream":[{"type":"chunk","time":1790049391270,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1790049391270,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1790049391270,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1790049391270,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10}}},{"type":"chunk","time":1790049391270,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} diff --git a/snapshots/session/dynamic-tool-updates/tool-schemas.expected.json b/snapshots/session/dynamic-tool-updates/tool-schemas.expected.json index 87bccafb3f..5268b0e9d8 100644 --- a/snapshots/session/dynamic-tool-updates/tool-schemas.expected.json +++ b/snapshots/session/dynamic-tool-updates/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -364,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -379,6 +408,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -893,6 +934,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -980,7 +1038,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -995,6 +1053,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -1005,7 +1075,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -1020,6 +1090,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -1533,6 +1615,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -1612,7 +1711,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -1627,6 +1726,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -1637,7 +1748,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -1652,6 +1763,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/foreground-confinement-timeout/tool-schemas.expected.json b/snapshots/session/foreground-confinement-timeout/tool-schemas.expected.json index 2b4b1f069f..2dd1f341df 100644 --- a/snapshots/session/foreground-confinement-timeout/tool-schemas.expected.json +++ b/snapshots/session/foreground-confinement-timeout/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -364,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -379,6 +408,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/fs-glob-sampling/tool-schemas.expected.json b/snapshots/session/fs-glob-sampling/tool-schemas.expected.json index 3fb3980c3b..840733798a 100644 --- a/snapshots/session/fs-glob-sampling/tool-schemas.expected.json +++ b/snapshots/session/fs-glob-sampling/tool-schemas.expected.json @@ -180,6 +180,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -243,7 +260,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -258,6 +275,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -268,7 +297,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -283,6 +312,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/lsp-definition/tool-schemas.expected.json b/snapshots/session/lsp-definition/tool-schemas.expected.json index 76087bb96f..473b6cc111 100644 --- a/snapshots/session/lsp-definition/tool-schemas.expected.json +++ b/snapshots/session/lsp-definition/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "lsp", "description": "Query a language server for precise code navigation. operation is one of goToDefinition, findReferences, goToImplementation, hover. line and character are one-based UTF-16 cursor coordinates. findReferences includes the declaration.", @@ -376,7 +393,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -391,6 +408,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -401,7 +430,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -416,6 +445,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/mcp-resources-ptc/system-prompt.expected.md b/snapshots/session/mcp-resources-ptc/system-prompt.expected.md index a6b0b07911..2776836c5d 100644 --- a/snapshots/session/mcp-resources-ptc/system-prompt.expected.md +++ b/snapshots/session/mcp-resources-ptc/system-prompt.expected.md @@ -162,6 +162,13 @@ interface ToolArgsMap { /** Continuation cursor returned by this server. */ cursor?: string; } & Record; + /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + list_subagent_models: { + /** Registered LLM provider id. Omit to list providers. */ + provider?: string; + /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + model?: string; + } & Record; /** Read a UTF-8 text file and return line-numbered content. */ read: { /** Path to read, resolved by the filesystem backend. */ @@ -195,7 +202,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; 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 id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -203,8 +210,14 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -212,6 +225,12 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; /** Record and update a task list to plan multi-step work and show progress; skip it for trivial single-step tasks. Add one todo per concrete step before you start. While work remains, keep the todos being worked on `in_progress`, several only when work runs in parallel. Mark each todo `completed` as soon as it is done. */ todo_write: { @@ -438,6 +457,7 @@ interface ToolOutputMap { })[]; list_mcp_resource_templates: JsonValue; list_mcp_resources: JsonValue; + list_subagent_models: string; read: { /** Canonical absolute path in the filesystem execution world. */ path: string; diff --git a/snapshots/session/mcp-resources/tool-schemas.expected.json b/snapshots/session/mcp-resources/tool-schemas.expected.json index 4e5e9d557c..5a7f33f534 100644 --- a/snapshots/session/mcp-resources/tool-schemas.expected.json +++ b/snapshots/session/mcp-resources/tool-schemas.expected.json @@ -300,6 +300,23 @@ ] } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -400,7 +417,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -415,6 +432,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -425,7 +454,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -440,6 +469,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/multimodal-spill-ends/tool-schemas.expected.json b/snapshots/session/multimodal-spill-ends/tool-schemas.expected.json index 637c069b74..dec41c176b 100644 --- a/snapshots/session/multimodal-spill-ends/tool-schemas.expected.json +++ b/snapshots/session/multimodal-spill-ends/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "mcp__fixture__inspect", "description": "Read a fixed window snapshot.", @@ -358,7 +375,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -373,6 +390,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -383,7 +412,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -398,6 +427,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/office-skills/tool-schemas.expected.json b/snapshots/session/office-skills/tool-schemas.expected.json index 79d6891abc..7269632ffd 100644 --- a/snapshots/session/office-skills/tool-schemas.expected.json +++ b/snapshots/session/office-skills/tool-schemas.expected.json @@ -215,6 +215,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -294,7 +311,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -309,6 +326,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -319,7 +348,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -334,6 +363,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/plugin-manager-mcp/session.v4.jsonl b/snapshots/session/plugin-manager-mcp/session.v4.jsonl index 794d19b23f..0036cc1272 100644 --- a/snapshots/session/plugin-manager-mcp/session.v4.jsonl +++ b/snapshots/session/plugin-manager-mcp/session.v4.jsonl @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Use plugin_manager with action set_plugin, target include:demo, enabled true to activate the configured MCP entry. Then call its newly discovered mcp__demo__ping tool and report the result. Do not use other tools."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"max"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","plugin_manager","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"max"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","plugin_manager","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":1000000}} {"type":"session/title","data":{"title":"Use plugin_manager with action set_plugi","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to activate a plugin via plugin_manager, then call an MCP tool. Let me do the plugin_manager call first.\n\nNote: they say \"Do not use other tools\" other than plugin_manager and the mcp tool. But I need to first enable it. Let's call plugin_manager set_plugin.\n\nLet me also list plugins first? The instruction says use plugin_manager with action set_plugin, target include:demo, enabled true. Let's just do that."},{"type":"tool-call","id":"call_00_xLhr9wia3A5m7wuSRWwD0436","name":"plugin_manager","arguments":"{\"action\": \"set_plugin\", \"target\": \"include:demo\", \"enabled\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"reasoning","signature":"75d99e21-1d47-4a33-bb88-340a1e84b749"},{"type":"tool-call"}]}},"id":"{{message:4}}"},"usage":{"inputTokens":7220,"outputTokens":174,"cacheReadTokens":0,"cacheWriteTokens":0,"totalTokens":7394},"stream":[{"type":"chunk","time":1789984418848,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1789984418848,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," activate"," a"," plugin"," via"," plugin","_manager",","," then"," call"," an"," M","CP"," tool","."," Let"," me"," do"," the"," plugin","_manager"," call"," first",".\n\n","Note",":"," they"," say"," \"","Do"," not"," use"," other"," tools","\""," other"," than"," plugin","_manager"," and"," the"," m","cp"," tool","."," But"," I"," need"," to"," first"," enable"," it","."," Let","'s"," call"," plugin","_manager"," set","_","plugin",".\n\n","Let"," me"," also"," list"," plugins"," first","?"," The"," instruction"," says"," use"," plugin","_manager"," with"," action"," set","_","plugin",","," target"," include",":","demo",","," enabled"," true","."," Let","'s"," just"," do"," that","."]},{"type":"chunk","time":1789984418849,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to activate a plugin via plugin_manager, then call an MCP tool. Let me do the plugin_manager call first.\n\nNote: they say \"Do not use other tools\" other than plugin_manager and the mcp tool. But I need to first enable it. Let's call plugin_manager set_plugin.\n\nLet me also list plugins first? The instruction says use plugin_manager with action set_plugin, target include:demo, enabled true. Let's just do that."}}},{"type":"chunk","time":1789984418849,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1789984418849,"index":1,"dt":[],"id":"call_00_xLhr9wia3A5m7wuSRWwD0436","name":"plugin_manager","args":[""]},{"type":"tool-call-chunks","time0":1789984418849,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_xLhr9wia3A5m7wuSRWwD0436","args":["{","\"","action","\"",": ","\"","set","_","plugin","\"",", ","\"","target","\"",": ","\"","include",":","demo","\"",", ","\"","en","abled","\"",": ","true","}"]},{"type":"chunk","time":1789984418849,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_xLhr9wia3A5m7wuSRWwD0436","name":"plugin_manager","arguments":"{\"action\": \"set_plugin\", \"target\": \"include:demo\", \"enabled\": true}"}}},{"type":"chunk","time":1789984418849,"chunk":{"type":"usage","usage":{"inputTokens":7220,"outputTokens":174,"cacheReadTokens":0,"cacheWriteTokens":0,"totalTokens":7394}}},{"type":"chunk","time":1789984418849,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"reasoning","signature":"75d99e21-1d47-4a33-bb88-340a1e84b749"},{"type":"tool-call"}]}}}]},"surfaceOp":"append"} @@ -18,7 +18,7 @@ {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} {"type":"system/message","data":{"turn":1,"step":2,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:6}}"}},"sourceEventSeqs":[7],"surfaceOp":{"op":"replace","startSeq":7,"endSeq":7}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"max"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_mcp_resource_templates","list_mcp_resources","mcp__demo__ping","plugin_manager","read","read_image","read_mcp_resource","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change","startsSeries":true}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"max"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_mcp_resource_templates","list_mcp_resources","list_subagent_models","mcp__demo__ping","plugin_manager","read","read_image","read_mcp_resource","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change","startsSeries":true}} {"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"list_mcp_resource_templates"},{"type":"tool-addition","toolName":"list_mcp_resources"},{"type":"tool-addition","toolName":"mcp__demo__ping"},{"type":"tool-addition","toolName":"read_mcp_resource"}],"role":"developer","id":"{{message:7}}"},"headerSeq":19},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The plugin was enabled. Now call mcp__demo__ping.\n\nWait — the tool was already listed in my tools before enabling. Let's call it anyway."},{"type":"tool-call","id":"call_00_miuKfLCTz5Cb38gdp8kK5186","name":"mcp__demo__ping","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"reasoning","signature":"b1ec1efe-4be6-4836-a10c-e5d6bfdce520"},{"type":"tool-call"}]}},"id":"{{message:8}}"},"usage":{"inputTokens":7035,"outputTokens":59,"cacheReadTokens":768,"cacheWriteTokens":0,"totalTokens":7862},"stream":[{"type":"chunk","time":1789984418998,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1789984418998,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," plugin"," was"," enabled","."," Now"," call"," m","cp","__","demo","__","ping",".\n\n","Wait"," —"," the"," tool"," was"," already"," listed"," in"," my"," tools"," before"," enabling","."," Let","'s"," call"," it"," anyway","."]},{"type":"chunk","time":1789984418998,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The plugin was enabled. Now call mcp__demo__ping.\n\nWait — the tool was already listed in my tools before enabling. Let's call it anyway."}}},{"type":"chunk","time":1789984418998,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1789984418998,"index":1,"dt":[],"id":"call_00_miuKfLCTz5Cb38gdp8kK5186","name":"mcp__demo__ping","args":[""]},{"type":"tool-call-chunks","time0":1789984418998,"index":1,"dt":[],"id":"call_00_miuKfLCTz5Cb38gdp8kK5186","args":["{}"]},{"type":"chunk","time":1789984418998,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_miuKfLCTz5Cb38gdp8kK5186","name":"mcp__demo__ping","arguments":"{}"}}},{"type":"chunk","time":1789984418998,"chunk":{"type":"usage","usage":{"inputTokens":7035,"outputTokens":59,"cacheReadTokens":768,"cacheWriteTokens":0,"totalTokens":7862}}},{"type":"chunk","time":1789984418998,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"reasoning","signature":"b1ec1efe-4be6-4836-a10c-e5d6bfdce520"},{"type":"tool-call"}]}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_00_miuKfLCTz5Cb38gdp8kK5186","name":"mcp__demo__ping","arguments":"{}"}} diff --git a/snapshots/session/plugin-manager-mcp/tool-schemas.expected.json b/snapshots/session/plugin-manager-mcp/tool-schemas.expected.json index f44f52640a..bc73a4b1fb 100644 --- a/snapshots/session/plugin-manager-mcp/tool-schemas.expected.json +++ b/snapshots/session/plugin-manager-mcp/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "plugin_manager", "description": "List plugins or bundles in the current profile, enable or disable them, install a bundle, or remove an installed bundle. Every action requires danger-full-access permission or approval for this call. Approval does not change the session permission mode. Changes affect every session in this profile. List first to obtain exact identifiers. Package installation can execute allowed build scripts. Live profiles apply changes immediately; startup profiles require restart. Incompatible DSH peer dependencies block installation and activation. Version exemptions risk crashes and data loss: warn the user and obtain explicit permission for the exact plugin and runtime versions before granting one.", @@ -400,7 +417,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -415,6 +432,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -425,7 +454,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -440,6 +469,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -994,6 +1035,23 @@ ] } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "mcp__demo__ping", "description": "Replies pong.", @@ -1164,7 +1222,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -1179,6 +1237,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -1189,7 +1259,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -1204,6 +1274,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/plugin-manager/tool-schemas.expected.json b/snapshots/session/plugin-manager/tool-schemas.expected.json index 4596dae7a1..47b4c83859 100644 --- a/snapshots/session/plugin-manager/tool-schemas.expected.json +++ b/snapshots/session/plugin-manager/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "plugin_manager", "description": "List plugins or bundles in the current profile, enable or disable them, install a bundle, or remove an installed bundle. Every action requires danger-full-access permission or approval for this call. Approval does not change the session permission mode. Changes affect every session in this profile. List first to obtain exact identifiers. Package installation can execute allowed build scripts. Live profiles apply changes immediately; startup profiles require restart. Incompatible DSH peer dependencies block installation and activation. Version exemptions risk crashes and data loss: warn the user and obtain explicit permission for the exact plugin and runtime versions before granting one.", @@ -400,7 +417,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -415,6 +432,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -425,7 +454,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -440,6 +469,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/product-subagent-both/tool-schemas.expected.json b/snapshots/session/product-subagent-both/tool-schemas.expected.json index 2b36308f92..779cf01085 100644 --- a/snapshots/session/product-subagent-both/tool-schemas.expected.json +++ b/snapshots/session/product-subagent-both/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -464,7 +493,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -479,6 +508,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/product-subagent-codex/tool-schemas.expected.json b/snapshots/session/product-subagent-codex/tool-schemas.expected.json index d9f3cfa9d0..40da027b47 100644 --- a/snapshots/session/product-subagent-codex/tool-schemas.expected.json +++ b/snapshots/session/product-subagent-codex/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -414,7 +443,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -429,6 +458,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json b/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json index 11f07197a2..d202776234 100644 --- a/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json +++ b/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -389,7 +418,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -404,6 +433,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/provider-cwd/tool-schemas.expected.json b/snapshots/session/provider-cwd/tool-schemas.expected.json index 2b4b1f069f..2dd1f341df 100644 --- a/snapshots/session/provider-cwd/tool-schemas.expected.json +++ b/snapshots/session/provider-cwd/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -364,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -379,6 +408,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/ptc-node-read-only/system-prompt.expected.md b/snapshots/session/ptc-node-read-only/system-prompt.expected.md index 39529ce115..ec62c1bdaa 100644 --- a/snapshots/session/ptc-node-read-only/system-prompt.expected.md +++ b/snapshots/session/ptc-node-read-only/system-prompt.expected.md @@ -140,6 +140,13 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; + /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + list_subagent_models: { + /** Registered LLM provider id. Omit to list providers. */ + provider?: string; + /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + model?: string; + } & Record; /** Read a UTF-8 text file and return line-numbered content. */ read: { /** Path to read, resolved by the filesystem backend. */ @@ -166,7 +173,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; 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 id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -174,8 +181,14 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -183,6 +196,12 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; /** Echo template text such as {{item}}, {{model}}, or {{ item }} without substitution. */ template_echo: { @@ -411,6 +430,7 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; + list_subagent_models: string; read: { /** Canonical absolute path in the filesystem execution world. */ path: string; diff --git a/snapshots/session/ptc-node-workspace/system-prompt.expected.md b/snapshots/session/ptc-node-workspace/system-prompt.expected.md index 39529ce115..ec62c1bdaa 100644 --- a/snapshots/session/ptc-node-workspace/system-prompt.expected.md +++ b/snapshots/session/ptc-node-workspace/system-prompt.expected.md @@ -140,6 +140,13 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; + /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + list_subagent_models: { + /** Registered LLM provider id. Omit to list providers. */ + provider?: string; + /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + model?: string; + } & Record; /** Read a UTF-8 text file and return line-numbered content. */ read: { /** Path to read, resolved by the filesystem backend. */ @@ -166,7 +173,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; 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 id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -174,8 +181,14 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -183,6 +196,12 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; /** Echo template text such as {{item}}, {{model}}, or {{ item }} without substitution. */ template_echo: { @@ -411,6 +430,7 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; + list_subagent_models: string; read: { /** Canonical absolute path in the filesystem execution world. */ path: string; diff --git a/snapshots/session/ptc-python-turn/system-prompt.expected.md b/snapshots/session/ptc-python-turn/system-prompt.expected.md index 23dc2e1494..6f94c095fb 100644 --- a/snapshots/session/ptc-python-turn/system-prompt.expected.md +++ b/snapshots/session/ptc-python-turn/system-prompt.expected.md @@ -287,6 +287,13 @@ class ListAgentsOutput2(TypedDict): parent: NotRequired[str] depth: NotRequired[float] +class ListSubagentModelsArgs(TypedDict): + # Registered LLM provider id. Omit to list providers. + provider: NotRequired[str] + # Exact model id to inspect. Requires provider; omit to list that provider's allowed models. + model: NotRequired[str] + # Additional keys beyond those declared are allowed. + class ReadArgs(TypedDict): # Path to read, resolved by the filesystem backend. file_path: str @@ -370,6 +377,12 @@ class SubagentArgs(TypedDict): description: str # The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. prompt: str + # LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. + provider: NotRequired[str] + # Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. + model: NotRequired[str] + # Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. + reasoning_effort: NotRequired[str] # Additional keys beyond those declared are allowed. class SubagentOutput(TypedDict): @@ -383,6 +396,12 @@ class SubagentForkArgs(TypedDict): description: str # The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. prompt: str + # LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. + provider: NotRequired[str] + # Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. + model: NotRequired[str] + # Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. + reasoning_effort: NotRequired[str] # Additional keys beyond those declared are allowed. class SubagentForkOutput(TypedDict): @@ -535,6 +554,8 @@ class Tools(Protocol): """Read a background job: output since the previous read for stream jobs, or the result of a finished final-output job.""" async def list_agents(self, args: ListAgentsArgs) -> list[ListAgentsOutput1 | ListAgentsOutput2]: """List subagents you started, with their ids, labels, and status. running means it is working; inactive means it is not currently working. You will be notified when a subagent finishes; there is no need to keep checking its status. Use send_message to continue the conversation.""" + async def list_subagent_models(self, args: ListSubagentModelsArgs) -> str: + """Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.""" async def read(self, args: ReadArgs) -> ReadOutput: """Read a UTF-8 text file and return line-numbered content.""" async def read_image(self, args: ReadImageArgs) -> ReadImageOutput: @@ -544,9 +565,9 @@ class Tools(Protocol): async def skill(self, args: SkillArgs) -> SkillOutput: """Load the full instructions for a skill. Call it before acting on a task that names or clearly matches a skill in the session skill catalog.""" async def subagent(self, args: SubagentArgs) -> SubagentOutput: - """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. The runtime notifies you when it finishes. The child reports results with `send_message`; 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 id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.""" async def subagent_fork(self, args: SubagentForkArgs) -> SubagentForkOutput: - """Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.""" + """Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.""" async def todo_write(self, args: TodoWriteArgs) -> TodoWriteOutput: """Record and update a task list to plan multi-step work and show progress; skip it for trivial single-step tasks. Add one todo per concrete step before you start. While work remains, keep the todos being worked on `in_progress`, several only when work runs in parallel. Mark each todo `completed` as soon as it is done.""" async def update_goal(self, args: UpdateGoalArgs) -> UpdateGoalOutput1 | UpdateGoalOutput2: diff --git a/snapshots/session/ptc-read-image/system-prompt.expected.md b/snapshots/session/ptc-read-image/system-prompt.expected.md index f0a3c2426f..62aab7eb38 100644 --- a/snapshots/session/ptc-read-image/system-prompt.expected.md +++ b/snapshots/session/ptc-read-image/system-prompt.expected.md @@ -140,6 +140,13 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; + /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + list_subagent_models: { + /** Registered LLM provider id. Omit to list providers. */ + provider?: string; + /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + model?: string; + } & Record; /** Read a UTF-8 text file and return line-numbered content. */ read: { /** Path to read, resolved by the filesystem backend. */ @@ -166,7 +173,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; 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 id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -174,8 +181,14 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -183,6 +196,12 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; /** Record and update a task list to plan multi-step work and show progress; skip it for trivial single-step tasks. Add one todo per concrete step before you start. While work remains, keep the todos being worked on `in_progress`, several only when work runs in parallel. Mark each todo `completed` as soon as it is done. */ todo_write: { @@ -407,6 +426,7 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; + list_subagent_models: string; read: { /** Canonical absolute path in the filesystem execution world. */ path: string; diff --git a/snapshots/session/ptc-turn/system-prompt.expected.md b/snapshots/session/ptc-turn/system-prompt.expected.md index 39529ce115..ec62c1bdaa 100644 --- a/snapshots/session/ptc-turn/system-prompt.expected.md +++ b/snapshots/session/ptc-turn/system-prompt.expected.md @@ -140,6 +140,13 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; + /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + list_subagent_models: { + /** Registered LLM provider id. Omit to list providers. */ + provider?: string; + /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + model?: string; + } & Record; /** Read a UTF-8 text file and return line-numbered content. */ read: { /** Path to read, resolved by the filesystem backend. */ @@ -166,7 +173,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; 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 id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -174,8 +181,14 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -183,6 +196,12 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; /** Echo template text such as {{item}}, {{model}}, or {{ item }} without substitution. */ template_echo: { @@ -411,6 +430,7 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; + list_subagent_models: string; read: { /** Canonical absolute path in the filesystem execution world. */ path: string; diff --git a/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json b/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json index 95ffc0255b..d33cb92a78 100644 --- a/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json +++ b/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -364,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -379,6 +408,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/ralph-loop/tool-schemas.1.expected.json b/snapshots/session/ralph-loop/tool-schemas.1.expected.json index 02ebdc4baf..93d4b861a1 100644 --- a/snapshots/session/ralph-loop/tool-schemas.1.expected.json +++ b/snapshots/session/ralph-loop/tool-schemas.1.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", @@ -402,7 +419,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -417,6 +434,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -427,7 +456,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -442,6 +471,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/ralph-loop/tool-schemas.2.expected.json b/snapshots/session/ralph-loop/tool-schemas.2.expected.json index 02ebdc4baf..93d4b861a1 100644 --- a/snapshots/session/ralph-loop/tool-schemas.2.expected.json +++ b/snapshots/session/ralph-loop/tool-schemas.2.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", @@ -402,7 +419,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -417,6 +434,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -427,7 +456,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -442,6 +471,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/ralph-loop/tool-schemas.expected.json b/snapshots/session/ralph-loop/tool-schemas.expected.json index deabaf4a0d..44c18da093 100644 --- a/snapshots/session/ralph-loop/tool-schemas.expected.json +++ b/snapshots/session/ralph-loop/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", @@ -359,7 +376,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -374,6 +391,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -384,7 +413,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -399,6 +428,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/session-query-spill/tool-schemas.expected.json b/snapshots/session/session-query-spill/tool-schemas.expected.json index 9d66381297..dd5544d7d4 100644 --- a/snapshots/session/session-query-spill/tool-schemas.expected.json +++ b/snapshots/session/session-query-spill/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -543,7 +560,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -558,6 +575,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -568,7 +597,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -583,6 +612,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json b/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json index 46abe32771..f61fc21202 100644 --- a/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json +++ b/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -389,7 +418,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -404,6 +433,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json b/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json index b8dc7c20a8..cd84466ebf 100644 --- a/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json +++ b/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json @@ -323,6 +323,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -402,7 +419,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -417,6 +434,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -427,7 +456,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -442,6 +471,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/subagent-no-message-tool/tool-schemas.1.expected.json b/snapshots/session/subagent-no-message-tool/tool-schemas.1.expected.json index e48983205c..960e60afd0 100644 --- a/snapshots/session/subagent-no-message-tool/tool-schemas.1.expected.json +++ b/snapshots/session/subagent-no-message-tool/tool-schemas.1.expected.json @@ -174,6 +174,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -232,7 +249,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -247,6 +264,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -257,7 +286,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -272,6 +301,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/subagent-structured-parent/tool-schemas.1.expected.json b/snapshots/session/subagent-structured-parent/tool-schemas.1.expected.json index d217c78441..3e450f980e 100644 --- a/snapshots/session/subagent-structured-parent/tool-schemas.1.expected.json +++ b/snapshots/session/subagent-structured-parent/tool-schemas.1.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -354,7 +371,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -369,6 +386,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -379,7 +408,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -394,6 +423,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/subagent-structured-parent/tool-schemas.expected.json b/snapshots/session/subagent-structured-parent/tool-schemas.expected.json index 25793c1cd4..8ebad49ed3 100644 --- a/snapshots/session/subagent-structured-parent/tool-schemas.expected.json +++ b/snapshots/session/subagent-structured-parent/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -364,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -379,6 +408,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/subagent-tool-filter/tool-schemas.1.expected.json b/snapshots/session/subagent-tool-filter/tool-schemas.1.expected.json index 45ab57c7c0..c38ea849b9 100644 --- a/snapshots/session/subagent-tool-filter/tool-schemas.1.expected.json +++ b/snapshots/session/subagent-tool-filter/tool-schemas.1.expected.json @@ -174,6 +174,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -253,7 +270,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -268,6 +285,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -278,7 +307,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -293,6 +322,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/system-prompt-in-history/tool-schemas.expected.json b/snapshots/session/system-prompt-in-history/tool-schemas.expected.json index 2b4b1f069f..2dd1f341df 100644 --- a/snapshots/session/system-prompt-in-history/tool-schemas.expected.json +++ b/snapshots/session/system-prompt-in-history/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -364,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -379,6 +408,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/team-targets/tool-schemas.expected.json b/snapshots/session/team-targets/tool-schemas.expected.json index 4926edb120..3409e4753a 100644 --- a/snapshots/session/team-targets/tool-schemas.expected.json +++ b/snapshots/session/team-targets/tool-schemas.expected.json @@ -251,6 +251,23 @@ "properties": {} } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", diff --git a/snapshots/session/text-turn/tool-schemas.expected.json b/snapshots/session/text-turn/tool-schemas.expected.json index 2b4b1f069f..2dd1f341df 100644 --- a/snapshots/session/text-turn/tool-schemas.expected.json +++ b/snapshots/session/text-turn/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -364,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -379,6 +408,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/web-fetch/tool-schemas.expected.json b/snapshots/session/web-fetch/tool-schemas.expected.json index 5ef902c18a..11dba4b997 100644 --- a/snapshots/session/web-fetch/tool-schemas.expected.json +++ b/snapshots/session/web-fetch/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -364,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -379,6 +408,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/workflow-confinement/tool-schemas.expected.json b/snapshots/session/workflow-confinement/tool-schemas.expected.json index 2b4b1f069f..2dd1f341df 100644 --- a/snapshots/session/workflow-confinement/tool-schemas.expected.json +++ b/snapshots/session/workflow-confinement/tool-schemas.expected.json @@ -260,6 +260,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -339,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -354,6 +371,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -364,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -379,6 +408,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/workspace-dependencies/tool-schemas.expected.json b/snapshots/session/workspace-dependencies/tool-schemas.expected.json index 4d3db178b9..cca2b4d36a 100644 --- a/snapshots/session/workspace-dependencies/tool-schemas.expected.json +++ b/snapshots/session/workspace-dependencies/tool-schemas.expected.json @@ -215,6 +215,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "load_workspace_dependencies", "description": "Get absolute paths to bundled Python and library directories, plus bundled Python distribution versions. Node.js and pnpm paths are included when the payload provides them. Python includes numpy, pandas, python-docx, python-pptx, openpyxl, Pillow, lxml, and XlsxWriter. Use these libraries for Office files unless the user or workspace instructions select another environment. When Node.js and pnpm paths are returned, run pnpm with that Node executable and pnpm script path. This does not change PATH or package-manager settings.", @@ -302,7 +319,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -317,6 +334,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -327,7 +356,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -342,6 +371,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/worktree/tool-schemas.expected.json b/snapshots/session/worktree/tool-schemas.expected.json index 6641f8f0b2..a18f81001c 100644 --- a/snapshots/session/worktree/tool-schemas.expected.json +++ b/snapshots/session/worktree/tool-schemas.expected.json @@ -277,6 +277,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +373,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -371,6 +388,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -381,7 +410,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -396,6 +425,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/tsconfig.base.json b/tsconfig.base.json index 86f3ef50fd..0a540ba263 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -185,6 +185,7 @@ "@deepseek-ai/dsh-tools/presentation": ["./packages/core/tools/src/presentation.ts"], "@deepseek-ai/dsh-tools/types": ["./packages/core/tools/src/types.ts"], "@deepseek-ai/dsh-tool-subagent-control/list-agents": ["./packages/subagent/tool-subagent-control/src/list-agents.ts"], + "@deepseek-ai/dsh-tool-subagent/list-models": ["./packages/subagent/tool-subagent/src/list-models.ts"], "@deepseek-ai/dsh-tool-subagent/model-selection-settings": ["./packages/subagent/tool-subagent/src/model-selection-settings.ts"], "@deepseek-ai/dsh-user-approval/types": ["./packages/interaction/user-approval/src/types.ts"], "@deepseek-ai/dsh-user-questions/types": ["./packages/interaction/user-questions/src/types.ts"], From 11c9d75ca34dceffd3928cde0cafad44836b57d5 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Sun, 27 Sep 2026 20:02:13 +0800 Subject: [PATCH 020/369] test(subagent): verify policy capture failure rolls back creation --- .../agent-loop/tests/serial-listener-review.spec.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/core/agent-loop/tests/serial-listener-review.spec.ts b/packages/core/agent-loop/tests/serial-listener-review.spec.ts index 5246d2cab4..032c286840 100644 --- a/packages/core/agent-loop/tests/serial-listener-review.spec.ts +++ b/packages/core/agent-loop/tests/serial-listener-review.spec.ts @@ -8,7 +8,6 @@ import AgentLoop from '@deepseek-ai/dsh-agent-loop' import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' import { SessionId } from '@deepseek-ai/dsh-session' -import { defineContentToolFixture } from '@deepseek-ai/dsh-tools' import { bindScopeParent, createScope, scopeOf } from '@deepseek-ai/dsh-scope' import LocalFileReference from '@deepseek-ai/dsh-file-reference-local' import SubagentRuntime from '@deepseek-ai/dsh-subagent' @@ -99,11 +98,9 @@ describe('serial creation listener integrations', () => { } }) - it('rolls back creation when a shared preset tool installation fails', async () => { + it('rolls back creation when the shared preset cannot capture model-selection policy', async () => { const ctx = await core() - const errors: string[] = [] - ctx.logger.error = ((error: unknown) => { errors.push(String(error)) }) as typeof ctx.logger.error - await ctx.plugin(Selection) + await ctx.plugin(Selection, { enabled: true, allowedModels: [] }) await ctx.plugin(SubagentRuntime) await ctx.plugin(SubagentSpawn, { providerName: 'spawn' }) const preset = createScope(ctx, { preset: 'review' }) @@ -113,10 +110,8 @@ describe('serial creation listener integrations', () => { sessionId: SessionId('review-tool'), setup(agentCtx) { bindScopeParent(scopeOf(agentCtx)!, scopeOf(preset.ctx)!) - agentCtx.tools.register(defineContentToolFixture({ name: 'subagent', description: 'occupied', parameters: {}, execute: () => Promise.resolve([]) })) }, - })).rejects.toThrow('subagent') - expect(errors.some(error => error.includes('subagent'))).toBe(true) + })).rejects.toThrow('subagent model selection requires at least one allowed model') expect(ctx.agents.list()).toEqual([]) expect(ctx.sessions.list()).toEqual([]) } finally { From 7cf871d39aedf8c2bb1a742c9334ae372f586246 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Sun, 27 Sep 2026 20:12:16 +0800 Subject: [PATCH 021/369] test(subagent): refresh PowerShell and Python SDK tool snapshots --- .../dynamic-tools/tool-history.json | 3 ++ .../restart/requests.json | 2 + .../restart/session.1.v4.jsonl | 2 +- .../restart/session.2.v4.jsonl | 2 +- .../tool-schemas.expected.json | 45 ++++++++++++++++++- .../pwsh-tool-turn/tool-schemas.expected.json | 45 ++++++++++++++++++- 6 files changed, 93 insertions(+), 6 deletions(-) diff --git a/scripts/snapshots/python-sdk-single-exe/dynamic-tools/tool-history.json b/scripts/snapshots/python-sdk-single-exe/dynamic-tools/tool-history.json index 7a8a75aa85..d22dcd9dd6 100644 --- a/scripts/snapshots/python-sdk-single-exe/dynamic-tools/tool-history.json +++ b/scripts/snapshots/python-sdk-single-exe/dynamic-tools/tool-history.json @@ -56,6 +56,7 @@ "job_list", "job_output", "list_agents", + "list_subagent_models", "load_workspace_dependencies", "read", "read_image", @@ -113,6 +114,7 @@ "job_list", "job_output", "list_agents", + "list_subagent_models", "load_workspace_dependencies", "read", "read_image", @@ -185,6 +187,7 @@ "job_list", "job_output", "list_agents", + "list_subagent_models", "load_workspace_dependencies", "read", "read_image", diff --git a/scripts/snapshots/python-sdk-single-exe/restart/requests.json b/scripts/snapshots/python-sdk-single-exe/restart/requests.json index 06bbf97d4f..3c4c8b10d3 100644 --- a/scripts/snapshots/python-sdk-single-exe/restart/requests.json +++ b/scripts/snapshots/python-sdk-single-exe/restart/requests.json @@ -23,6 +23,7 @@ "job_kill", "job_list", "job_output", + "list_subagent_models", "subagent", "workflow", "working_directory" @@ -52,6 +53,7 @@ "job_kill", "job_list", "job_output", + "list_subagent_models", "subagent", "workflow", "working_directory" diff --git a/scripts/snapshots/python-sdk-single-exe/restart/session.1.v4.jsonl b/scripts/snapshots/python-sdk-single-exe/restart/session.1.v4.jsonl index 59d1880f27..3ce01a7a4d 100644 --- a/scripts/snapshots/python-sdk-single-exe/restart/session.1.v4.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/restart/session.1.v4.jsonl @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{messageId}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Complete the first isolated Python SDK process turn."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","subagent","workflow","working_directory"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","list_subagent_models","subagent","workflow","working_directory"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} {"type":"session/title","data":{"title":"Complete the first isolated Python","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session-1}}","sessionFormatVersion":4,"throughSeq":12}} diff --git a/scripts/snapshots/python-sdk-single-exe/restart/session.2.v4.jsonl b/scripts/snapshots/python-sdk-single-exe/restart/session.2.v4.jsonl index be7dd18e39..cc12894292 100644 --- a/scripts/snapshots/python-sdk-single-exe/restart/session.2.v4.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/restart/session.2.v4.jsonl @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{messageId}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Complete the second isolated Python SDK process turn."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","subagent","workflow","working_directory"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","list_subagent_models","subagent","workflow","working_directory"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} {"type":"session/title","data":{"title":"Complete the second isolated Python","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session-2}}","sessionFormatVersion":4,"throughSeq":12}} diff --git a/snapshots/session/persistent-pwsh-tool-turn/tool-schemas.expected.json b/snapshots/session/persistent-pwsh-tool-turn/tool-schemas.expected.json index d1b4eef213..bee9fc050c 100644 --- a/snapshots/session/persistent-pwsh-tool-turn/tool-schemas.expected.json +++ b/snapshots/session/persistent-pwsh-tool-turn/tool-schemas.expected.json @@ -135,6 +135,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "pwsh", "description": "Run commands in a persistent PowerShell shell. State, including the current directory and exported environment variables, persists across calls for this agent.", @@ -214,7 +231,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -229,6 +246,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -239,7 +268,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -254,6 +283,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/pwsh-tool-turn/tool-schemas.expected.json b/snapshots/session/pwsh-tool-turn/tool-schemas.expected.json index d1a1729417..dcf23f6b31 100644 --- a/snapshots/session/pwsh-tool-turn/tool-schemas.expected.json +++ b/snapshots/session/pwsh-tool-turn/tool-schemas.expected.json @@ -187,6 +187,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "pwsh", "description": "Execute a PowerShell command (`pwsh -Command`) and return its stdout/stderr. Each call runs in a fresh pwsh process; pass `workdir` instead of using `cd`. Paths use native Windows form (`C:\\...`); read environment variables with `$env:NAME`. Managed `$env:DSH_*` variables expose current harness environment facts. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. On Windows a force-killed command settles as `[exit code: 1]` without a signal marker — treat it as an interruption, not a command failure. Provide `description` before `command` in the arguments. Before any delete or move, verify that the resolved absolute target path is the intended one; never run it against a computed path you have not checked. Do not assign to automatic variables such as `$HOME`; variable names are case-insensitive, so `$home` is the same read-only variable. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]`, a policy denial: do not retry another way. Under the Windows sandbox, read-only pwsh runs in PowerShell ConstrainedLanguage mode, while workspace-write stays in FullLanguage unless host policy says otherwise. In read-only, prefer cmdlets and core types (`[string]`, `[datetime]`, `[regex]`, `[guid]`); .NET static calls (`[System.IO.*]::`, `[math]::`), `Add-Type`, COM objects, and reflection fail with \"only core types\" errors. `-f` formatting, property access, and core cmdlets work. In both confined modes, programs cannot open named pipes, so a command that captures another program's output through piped stdio (Node.js `child_process.spawn`/`exec` with the default `stdio: 'pipe'`) fails with EPERM, while `stdio: 'inherit'` and `stdio: 'ignore'` spawns work and PowerShell's own pipelines are unaffected. That EPERM is the documented boundary: do not retry the command another way — escalate the exact command once or restructure it to avoid capturing output.", @@ -295,7 +312,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -310,6 +327,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -320,7 +349,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -335,6 +364,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ From 648eb70e906487c1a8cb46ed13c85ebbbc2d5e69 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Sun, 27 Sep 2026 20:19:44 +0800 Subject: [PATCH 022/369] test(subagent): refresh inherited parent tool list --- .../tests/expected/subagent-inheritance/parent.expected.jsonl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 1cee326a29..01674e2a6e 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 @@ -17,8 +17,8 @@ {"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 policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"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 policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"low"},"tools":["bash","create_goal","edit","exit_plan_mode","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","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":"bash"},{"type":"tool-addition","toolName":"create_goal"},{"type":"tool-addition","toolName":"edit"},{"type":"tool-addition","toolName":"exit_plan_mode"},{"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":"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/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"low"},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","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":"bash"},{"type":"tool-addition","toolName":"create_goal"},{"type":"tool-addition","toolName":"edit"},{"type":"tool-addition","toolName":"exit_plan_mode"},{"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":"list_subagent_models"},{"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":"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"} From 3cd55996176a57e580109cba48786badfde74494 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Sun, 27 Sep 2026 20:30:23 +0800 Subject: [PATCH 023/369] test(subagent): refresh Web model-selection tool snapshots --- .../plugin-instances.expected.md | 4 ++ apps/web/tests/shipped-composition.e2e.ts | 1 + .../tool-schemas.expected.json | 45 ++++++++++++++++++- .../web/ptc-round/system-prompt.expected.md | 24 +++++++++- 4 files changed, 70 insertions(+), 4 deletions(-) diff --git a/apps/web/tests/expected/settings-chrome/plugin-instances.expected.md b/apps/web/tests/expected/settings-chrome/plugin-instances.expected.md index b4584b0486..bafdc63177 100644 --- a/apps/web/tests/expected/settings-chrome/plugin-instances.expected.md +++ b/apps/web/tests/expected/settings-chrome/plugin-instances.expected.md @@ -3,6 +3,10 @@ - button "tool-subagent-control, 已启用": - strong: tool-subagent-control - text: Globally named send_message, interrupt_agent, and list_agents tools over ctx.subagents continuations + - listitem: + - button "tool-subagent/list-models, tool-subagent-list-models, 已启用": + - strong: tool-subagent/list-models + - code: tool-subagent-list-models - listitem: - button "tool-subagent-control/list-agents, tool-subagent-list-agents, 已启用": - strong: tool-subagent-control/list-agents diff --git a/apps/web/tests/shipped-composition.e2e.ts b/apps/web/tests/shipped-composition.e2e.ts index 6eefe81171..74e4a636b1 100644 --- a/apps/web/tests/shipped-composition.e2e.ts +++ b/apps/web/tests/shipped-composition.e2e.ts @@ -484,6 +484,7 @@ const EXPECTED_TOOLS = [ 'job_list', 'job_output', 'list_agents', + 'list_subagent_models', 'present', 'read', 'read_image', diff --git a/snapshots/web/fresh-round-trip/tool-schemas.expected.json b/snapshots/web/fresh-round-trip/tool-schemas.expected.json index 557f8adc57..02a74c4f52 100644 --- a/snapshots/web/fresh-round-trip/tool-schemas.expected.json +++ b/snapshots/web/fresh-round-trip/tool-schemas.expected.json @@ -323,6 +323,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "present", "description": "Declare existing files as final deliverables for the user. Use it when the user needs a separate file, especially Office documents, spreadsheets, and slide decks; prefer your final response when that suffices. The user opens the current files; their contents are not copied.", @@ -702,7 +719,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -717,6 +734,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -727,7 +756,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -742,6 +771,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/web/ptc-round/system-prompt.expected.md b/snapshots/web/ptc-round/system-prompt.expected.md index 47cc71cb02..c5c077eb85 100644 --- a/snapshots/web/ptc-round/system-prompt.expected.md +++ b/snapshots/web/ptc-round/system-prompt.expected.md @@ -158,6 +158,13 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; + /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + list_subagent_models: { + /** Registered LLM provider id. Omit to list providers. */ + provider?: string; + /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + model?: string; + } & Record; /** Declare existing files as final deliverables for the user. Use it when the user needs a separate file, especially Office documents, spreadsheets, and slide decks; prefer your final response when that suffices. The user opens the current files; their contents are not copied. */ present: { /** Usually the 1-2 most important deliverables; at most 4 per call. */ @@ -281,7 +288,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; 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 id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -289,8 +296,14 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -298,6 +311,12 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; /** Record and update a task list to plan multi-step work and show progress; skip it for trivial single-step tasks. Add one todo per concrete step before you start. While work remains, keep the todos being worked on `in_progress`, several only when work runs in parallel. Mark each todo `completed` as soon as it is done. */ todo_write: { @@ -500,6 +519,7 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; + list_subagent_models: string; present: { turn: number; files: { From b66aa70051c572ff57355e35bb504c1e458e9ec2 Mon Sep 17 00:00:00 2001 From: turtle1999 Date: Sat, 10 Oct 2026 13:29:58 +0800 Subject: [PATCH 024/369] test(coverage): scope the fork exit ordering claim to the clean exit report Vitest reports an unexpected worker exit either as "Worker exited unexpectedly" from the exit event or as "Worker coverage-forks emitted error" when the channel closes under a send. The record describes the exit, and the error is logged before it, so only the first form can order the record ahead of the report. The previous assertion named one hardcoded message; #5900 widened it to whichever form appeared and so claimed an order the error form cannot satisfy. The case name no longer claims that order, and the report lookup relies on the assertion above it instead of repeating the check. Refs #5813 --- scripts/coverage-fork-diagnostics.spec.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/coverage-fork-diagnostics.spec.ts b/scripts/coverage-fork-diagnostics.spec.ts index be555a3003..3a6557b15a 100644 --- a/scripts/coverage-fork-diagnostics.spec.ts +++ b/scripts/coverage-fork-diagnostics.spec.ts @@ -159,7 +159,7 @@ function workerExitReport(output: string, pool: string): string | undefined { } describe('coverage fork configuration entry', () => { - it('preserves the real Vitest failure and adds metadata before its error report', async ({ signal }) => { + it('preserves the real Vitest failure and attributes the same exit Vitest reports', async ({ signal }) => { const [stock, diagnostic] = await Promise.all([ configuredPool('stock', 'exit', signal), configuredPool('diagnostic', 'exit', signal), @@ -196,8 +196,12 @@ describe('coverage fork configuration entry', () => { }) expect(record.files.map(file => file.replaceAll('\\', '/'))) .toEqual([resolve('scripts/fixtures/coverage-fork.fixture.ts').replaceAll('\\', '/')]) - expect(output.indexOf('coverage-worker-exit:')) - .toBeLessThan(output.indexOf(workerExitReport(output, 'coverage-forks')!)) + const report = workerExitReport(output, 'coverage-forks')! + // A worker error is logged when the channel closes under a send, which precedes the exit the + // record describes. Vitest then reports no exit text, and the record cannot come first. + if (report === 'Worker exited unexpectedly') { + expect(output.indexOf('coverage-worker-exit:')).toBeLessThan(output.indexOf(report)) + } }) it('keeps a successful configured run silent and successful', async ({ signal }) => { From 4fe62777b373ad97ca0b56f56b29f36e5757cc92 Mon Sep 17 00:00:00 2001 From: turtle1999 Date: Sun, 11 Oct 2026 00:36:53 +0800 Subject: [PATCH 025/369] test(web): pin the collapse-timing clocks to the fixture day The scenario stamps its own prompt times from the Host wall clock, which `support.ts` pairs with `pinHostClock`/`pinBrowserClock` and a fixture `createdAt` so the calendar fact belongs to the scenario instead of the run, as `seeded-history.e2e.ts` already does on the same fixture. With both clocks on the fixture day the message and render instants cannot land on different days, so the snapshot no longer depends on which side of local midnight the run falls. Refs #5997 --- apps/web/tests/collapse-timing.e2e.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/apps/web/tests/collapse-timing.e2e.ts b/apps/web/tests/collapse-timing.e2e.ts index a33867b404..5c3a7489fe 100644 --- a/apps/web/tests/collapse-timing.e2e.ts +++ b/apps/web/tests/collapse-timing.e2e.ts @@ -12,7 +12,7 @@ import { assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts, launchWebScaffold, parseSeedFixture, readPersistedEvents, seedSession, watchConsole, webSnapshotMode, type WebScaffold, } from './scaffold.ts' -import { newEnglishPage, openSettings, saveFailureShot, writeComposerDraft } from './support.ts' +import { newEnglishPage, openSettings, pinBrowserClock, pinHostClock, saveFailureShot, WEB_FIXTURE_TIME, writeComposerDraft } from './support.ts' const SNAPSHOT_DIR = fileURLToPath(new URL('../../../snapshots/web/collapse-timing', import.meta.url)) const SEED = fileURLToPath(new URL('../../../snapshots/web/seeded-history/session.v3.jsonl', import.meta.url)) @@ -38,8 +38,13 @@ describe.skipIf(MODE === 'record')('web e2e: recorded collapse timing', () => { let fixture: string let prompt: string let replayDir: string + let hostClock: ReturnType | undefined + let unpinBrowserClock: (() => void) | undefined beforeAll(async () => { + // Seeded and live prompt timestamps share the renderer's fixture day, so a run + // that spans Asia/Shanghai midnight renders no date prefix in the aria. + if (MODE !== 'record') hostClock = pinHostClock() fixture = await readFile(SEED, 'utf8') const prompts = fixtureUserPrompts(fixture) expect(prompts).toHaveLength(1) @@ -56,9 +61,10 @@ describe.skipIf(MODE === 'record')('web e2e: recorded collapse timing', () => { // This rendering borrows a completed turn and replays only its final response as a continuation. compareReplaySession: false, }) - await seedSession(scaffold, fixture, SEED_ID) + await seedSession(scaffold, fixture, SEED_ID, undefined, { createdAt: WEB_FIXTURE_TIME }) browser = await chromium.launch({ headless: true }) page = await newEnglishPage(browser) + if (MODE !== 'record') unpinBrowserClock = await pinBrowserClock(page) tripwire = watchConsole(page) await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' }) await page.getByRole('button', { name: 'Settings', exact: true }).waitFor() @@ -66,8 +72,13 @@ describe.skipIf(MODE === 'record')('web e2e: recorded collapse timing', () => { afterAll(async () => { const failures: unknown[] = [] - await browser?.close().catch((error: unknown) => failures.push(error)) - await scaffold?.close().catch((error: unknown) => failures.push(error)) + try { + await browser?.close().catch((error: unknown) => failures.push(error)) + await scaffold?.close().catch((error: unknown) => failures.push(error)) + } finally { + unpinBrowserClock?.() + hostClock?.mockRestore() + } if (replayDir !== undefined) { await rm(replayDir, { recursive: true, force: true }).catch((error: unknown) => failures.push(error)) } From cde1086c0a9161eb3b8e405ba4efe35aef624946 Mon Sep 17 00:00:00 2001 From: turtle1999 Date: Sun, 11 Oct 2026 03:28:04 +0800 Subject: [PATCH 026/369] test(coverage): name the missing worker-exit line in the ordering comment --- scripts/coverage-fork-diagnostics.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/coverage-fork-diagnostics.spec.ts b/scripts/coverage-fork-diagnostics.spec.ts index 3a6557b15a..4450245255 100644 --- a/scripts/coverage-fork-diagnostics.spec.ts +++ b/scripts/coverage-fork-diagnostics.spec.ts @@ -198,7 +198,8 @@ describe('coverage fork configuration entry', () => { .toEqual([resolve('scripts/fixtures/coverage-fork.fixture.ts').replaceAll('\\', '/')]) const report = workerExitReport(output, 'coverage-forks')! // A worker error is logged when the channel closes under a send, which precedes the exit the - // record describes. Vitest then reports no exit text, and the record cannot come first. + // record describes. Vitest then prints no "Worker exited unexpectedly" line, so only that + // line states an order the record can hold. if (report === 'Worker exited unexpectedly') { expect(output.indexOf('coverage-worker-exit:')).toBeLessThan(output.indexOf(report)) } From 5ecf33aab1e493ca355a34f1ba0bcaaef5541b27 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Mon, 12 Oct 2026 12:03:36 +0800 Subject: [PATCH 027/369] refactor(session-projection): acquire retained readers during activation --- 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 | 12 +- packages/core/agent-loop/src/inbox.ts | 28 ++--- packages/core/agent-loop/src/index.ts | 108 +++++----------- packages/core/agent-loop/tests/inbox.spec.ts | 33 ++--- packages/core/agent-loop/tests/loop.spec.ts | 6 +- .../agent-loop/tests/request-freeze.spec.ts | 11 +- .../tests/resource-lifecycle.spec.ts | 8 +- .../agent-loop/tests/scope-lifecycle.spec.ts | 12 +- packages/core/agent/package.json | 3 + packages/core/agent/src/index.ts | 2 +- packages/core/agent/src/projection.ts | 56 ++++++++- .../tests/cache.spec.ts | 18 +-- .../tests/fixtures.spec.ts | 4 +- .../session-projection/README.i18n.yaml | 8 +- packages/session/session-projection/README.md | 7 +- .../session/session-projection/README.zh.md | 7 +- .../session/session-projection/src/index.ts | 99 ++++++++------- .../tests/registration-reader.spec.ts | 113 +++++++++++++++++ .../session-projection/tests/registry.spec.ts | 115 +++++++++--------- 22 files changed, 388 insertions(+), 270 deletions(-) create mode 100644 packages/session/session-projection/tests/registration-reader.spec.ts diff --git a/packages/core/agent-loop/README.i18n.yaml b/packages/core/agent-loop/README.i18n.yaml index 0bbdb05d37..ab692943de 100644 --- a/packages/core/agent-loop/README.i18n.yaml +++ b/packages/core/agent-loop/README.i18n.yaml @@ -39,8 +39,8 @@ en: 2a8b93869eb5a2f9 zh: ddab8e738d188a61 /deepseek-ai-dsh-agent-loop/understand-the-implementation/creation-and-teardown: - en: d079cf181ece9267 - zh: 047f76fecfc5497c + en: 79aa89d1db536612 + zh: a86635bf4aa54b59 /deepseek-ai-dsh-agent-loop/understand-the-implementation/persistence-integration: en: 3c5721bc00a0bbf4 zh: 711f832343ffbb75 diff --git a/packages/core/agent-loop/README.md b/packages/core/agent-loop/README.md index 62dcd35a92..7308c64c73 100644 --- a/packages/core/agent-loop/README.md +++ b/packages/core/agent-loop/README.md @@ -109,7 +109,7 @@ The loop deep-freezes each derived message identity on its first request and reu Creation is one rollback-covered transaction: construct a private session, concrete agent, and scoped context; await optional setup; enter both registries; announce `session/created`; await serial `agent/created` listeners; then release queued input. A caller creating a runtime child sets `options.parentAgent`; the caller Context separately owns the transaction and live handle. Setup, commit, or listener failure and owner disposal roll back the prepared resources. Announcements already delivered remain observable and receive matching disposal notifications. Teardown stops and drains the driver, unwinds the scope, closes the session's write path, detaches the agent, then detaches the session. Every detach binds the exact entered object, so a stale disposer cannot remove a later same-id replacement. -Factory unload retains the inbox and turn-boundary projections through scope cleanup, including cancellation from scoped abort listeners, until every Agent teardown and startup task settles. Cleanup failures do not release those projections early. Caller unload, factory unload, and handle disposal await the same teardown. +The loop awaits inbox and turn-boundary registration before publishing its factory and passes those handles into synchronous Agent construction. During factory unload, global registrations may disappear before teardown finishes; retained handles remain readable through scope cleanup, including cancellation from scoped abort listeners. Caller unload, factory unload, and handle disposal await the same teardown. ### Persistence integration diff --git a/packages/core/agent-loop/README.zh.md b/packages/core/agent-loop/README.zh.md index ee355a328d..714f3f3839 100644 --- a/packages/core/agent-loop/README.zh.md +++ b/packages/core/agent-loop/README.zh.md @@ -109,7 +109,7 @@ const handle = await ctx.agents.create({ 创建是同一个受回滚保护的事务:构造私有会话、具象 agent 与带作用域上下文;等待可选 setup;进入两个注册表;宣告 `session/created`;等待串行 `agent/created` 监听器;随后释放已排队输入。创建运行时子 Agent 的调用方设置 `options.parentAgent`;调用方 Context 则单独拥有事务和存活句柄。Setup、commit、监听器失败或所有者 dispose 都会回滚已准备的资源。已送达的宣告仍可被观察,并有配对的销毁通知。Teardown 停止并排空驱动器、撤销作用域、关闭会话写路径、detach agent,再 detach 会话。每次 detach 都绑定到确切进入的对象,因此陈旧 disposer 无法移除之后出现的同 id 替代项。 -工厂卸载时在整个作用域清理期间保留 inbox 投影与轮次边界投影,包括作用域内 abort 监听器发起取消的时刻,直到所有 Agent teardown 和启动任务结束。清理失败不会使这些投影提前释放。调用方卸载、工厂卸载和句柄释放等待同一次 teardown。 +loop 在发布工厂前等待 inbox 与轮次边界注册完成,并将句柄传入 Agent 的同步构造过程。工厂卸载时,全局注册可以先于 teardown 完成而消失;保留句柄在整个作用域清理期间仍可读取,包括作用域内 abort 监听器发起取消的时刻。调用方卸载、工厂卸载和句柄释放等待同一次 teardown。 ### 持久化集成 diff --git a/packages/core/agent-loop/src/agent.ts b/packages/core/agent-loop/src/agent.ts index adf2e33ddc..65797b988f 100644 --- a/packages/core/agent-loop/src/agent.ts +++ b/packages/core/agent-loop/src/agent.ts @@ -12,6 +12,8 @@ import type { AgentStatus, CancelOptions, InboxTarget, + InboxState, + TurnBoundaryProjection, PreStepDecision, RequestErrorAction, } from '@deepseek-ai/dsh-agent' @@ -31,7 +33,7 @@ import type { EpochHeader, RequestContext, Session, SessionId, SessionSeq, TurnE import { canonicalHeader, headerEquals, ToolCallRecovery } from '@deepseek-ai/dsh-session' import { joinContextSections, renderContextSections, renderPrompt } from '@deepseek-ai/dsh-system-prompt' import type { PromptAssembly } from '@deepseek-ai/dsh-system-prompt' -import type {} from '@deepseek-ai/dsh-session-projection' +import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection' import type { Context } from '@deepseek-ai/cordis' import { ReactLoopInbox } from './inbox.ts' import { RuntimeContextProjection } from './runtime-context.ts' @@ -124,15 +126,15 @@ export class ReactLoopAgent implements Agent { public readonly id: SessionId, public readonly options: AgentOptions, public readonly session: Session, + inbox: ProjectionHandle, + boundary: ProjectionHandle, ) { this.requestSurfaceGeneration = session.surface.contentGeneration this.dispatch = agentEvents(loopCtx, this) this.scope = createScope(loopCtx, this) this.ctx = this.scope.ctx - this.inbox = new ReactLoopInbox(this.ctx.sessionProjections, session, this.dispatch) - /* v8 ignore next -- the loop registers its own turnBoundary unit, so the key is always present */ - const lastTurn = this.loopCtx.sessionProjections.stateOf(session, 'turnBoundary')?.lastTurn ?? 0 - this.phase = { kind: 'idle', lastTurn } + this.inbox = new ReactLoopInbox(() => inbox.stateOf(session), session, this.dispatch) + this.phase = { kind: 'idle', lastTurn: boundary.stateOf(session).lastTurn } this.runtimeContext = new RuntimeContextProjection(this.ctx, session) this.systemPrompt = new SystemPromptProjection(session) } diff --git a/packages/core/agent-loop/src/inbox.ts b/packages/core/agent-loop/src/inbox.ts index ef00887a54..affe0c6b2f 100644 --- a/packages/core/agent-loop/src/inbox.ts +++ b/packages/core/agent-loop/src/inbox.ts @@ -6,7 +6,6 @@ import type { MessageId } from '@deepseek-ai/dsh-llm' import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection' -import type SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Session, SessionEventMap, UserMessage } from '@deepseek-ai/dsh-session' import type { AgentEventDispatch, @@ -67,30 +66,30 @@ export const inboxProjectionDefinition = { /** * Driver-owned durable Inbox implementation used by ReactLoopAgent and focused * provider tests. - * @param projections - registry with the standard Inbox projection registered by AgentLoop. + * @param readState - reader for the live Inbox projection state. * @param session - session whose durable events store pending input. * @param dispatch - agent-scoped notifications for Inbox lifecycle events. */ export class ReactLoopInbox implements InboxContract { constructor( - private readonly projections: SessionProjectionRegistry, + private readonly readState: () => InboxState, private readonly session: Session, private readonly dispatch: AgentEventDispatch, ) {} /** Prompts awaiting individual turns. */ get nextTurn(): readonly UserMessage[] { - return this.current()['next-turn'] + return this.readState()['next-turn'] } /** Input awaiting the next step boundary. */ get nextStep(): readonly UserMessage[] { - return this.current()['next-step'] + return this.readState()['next-step'] } /** Whether either pending-message list contains work. */ get hasPending(): boolean { - const state = this.current() + const state = this.readState() return state['next-turn'].length > 0 || state['next-step'].length > 0 } @@ -119,7 +118,7 @@ export class ReactLoopInbox implements InboxContract { * @param message - message to append. */ append(target: InboxTarget, message: UserMessage): void { - this.splice(target, this.current()[target].length, 0, [message]) + this.splice(target, this.readState()[target].length, 0, [message]) } /** @@ -175,7 +174,7 @@ export class ReactLoopInbox implements InboxContract { /** Locate one pending identity across both owned lists. */ private locate(messageId: MessageId): { target: InboxTarget; index: number } | undefined { - const state = this.current() + const state = this.readState() for (const target of ['next-turn', 'next-step'] as const) { const index = state[target].findIndex(message => message.id === messageId) if (index >= 0) return { target, index } @@ -183,17 +182,6 @@ export class ReactLoopInbox implements InboxContract { return undefined } - /** Read the current durable projection state. */ - private current(): InboxState { - const state = this.projections.stateOf(this.session, 'inbox') - if (state === undefined) { - throw new Error( - `agent "${this.session.id}" cannot read inbox state: its projection registration is not active`, - ) - } - return state - } - /** Commit one normalized mutation and publish its live events. */ private mutate( target: InboxTarget, @@ -202,7 +190,7 @@ export class ReactLoopInbox implements InboxContract { inserted: UserMessage[], discardRemoved: boolean, ): UserMessage[] { - const state = this.current() + const state = this.readState() const inbox = state[target] const truncatedStart = Math.trunc(start) const offset = Number.isNaN(truncatedStart) ? 0 : truncatedStart diff --git a/packages/core/agent-loop/src/index.ts b/packages/core/agent-loop/src/index.ts index ca4532af8c..60d4e2e8c5 100644 --- a/packages/core/agent-loop/src/index.ts +++ b/packages/core/agent-loop/src/index.ts @@ -9,26 +9,26 @@ import type { Volatile } from '@deepseek-ai/cosmokit' import { Context, FiberState, Service } from '@deepseek-ai/cordis' import { randomUUID } from 'node:crypto' import z from '@deepseek-ai/schemastery' -import { z as zod } from 'zod' import { brandString } from '@deepseek-ai/dsh-brand' -import type { - Agent, - AgentFactory, - AgentHandle, - AgentOptions, - AgentSetup, - CreateAgentOptions, - ResumeAgentOptions, - SessionStartSource, - TurnBoundaryProjection, +import { + turnBoundaryProjectionDefinition, + type Agent, + type AgentFactory, + type AgentHandle, + type AgentOptions, + type AgentSetup, + type CreateAgentOptions, + type ResumeAgentOptions, + type SessionStartSource, + type InboxState, + type TurnBoundaryProjection, } from '@deepseek-ai/dsh-agent' import { errorChain, ReasoningEffortId } from '@deepseek-ai/dsh-llm' -import { interruptedTurnClosers, SessionLogOffset, SessionPreparation, SessionSeq } from '@deepseek-ai/dsh-session' +import { interruptedTurnClosers, SessionLogOffset, SessionPreparation } from '@deepseek-ai/dsh-session' import type { Session, SessionHeader, SessionId } from '@deepseek-ai/dsh-session' import type {} from '@deepseek-ai/dsh-system-prompt' import type {} from '@deepseek-ai/dsh-tools' -import type {} from '@deepseek-ai/dsh-session-projection' -import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection' +import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection' import { SessionPersistenceNotFoundError } from '@deepseek-ai/dsh-session-persistence' import type { SessionHandle, SessionPersistence } from '@deepseek-ai/dsh-session-persistence' import { ReactLoopAgent } from './agent.ts' @@ -36,6 +36,8 @@ import { inboxProjectionDefinition } from './inbox.ts' import { DEFAULT_MAX_PARALLEL_TOOL_CALLS } from './constants.ts' import type {} from './runtime-context.ts' +export { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent' + /** Fiber states that cannot own or serve a new lifecycle. */ const INACTIVE_STATES: ReadonlySet = new Set([ FiberState.UNLOADING, @@ -43,57 +45,6 @@ const INACTIVE_STATES: ReadonlySet = new Set([ FiberState.FAILED, ]) -const turnBoundaryProjectionSchema: zod.ZodType = zod.object({ - openTurnStartSeq: zod.number().int().nonnegative().transform(SessionSeq).nullable(), - lastStepStartSeq: zod.number().int().nonnegative().transform(SessionSeq).nullable(), - lastStepBoundary: zod.object({ - kind: zod.union([zod.literal('start'), zod.literal('end')]), - seq: zod.number().int().nonnegative().transform(SessionSeq), - }).nullable(), - lastTurn: zod.number().int().nonnegative(), -}) - -/** Host projection of agent turn and step boundaries. */ -export const turnBoundaryProjectionDefinition = { - key: 'turnBoundary', - stateVersion: 2, - stateSchema: turnBoundaryProjectionSchema, - init: () => ({ - openTurnStartSeq: null, - lastStepStartSeq: null, - lastStepBoundary: null, - lastTurn: 0, - }), - apply: (state, event) => { - switch (event.type) { - case 'turn/start': - return { - ...state, - openTurnStartSeq: event.seq, - lastTurn: event.data.turn, - } - case 'turn/end': - return { - ...state, - openTurnStartSeq: null, - } - case 'step/start': - return { - ...state, - lastStepStartSeq: event.seq, - lastStepBoundary: { kind: 'start', seq: event.seq }, - } - case 'step/end': - return { - ...state, - lastStepBoundary: { kind: 'end', seq: event.seq }, - } - default: - return state - } - }, -} satisfies ProjectionDefinition<'turnBoundary', TurnBoundaryProjection> - /** Re-throw one collected failure as itself, or several as one `AggregateError`. */ function throwCollectedFailures(failures: readonly unknown[], message: string): void { if (failures.length === 1) throw failures[0] @@ -361,6 +312,8 @@ export class AgentLoop extends Service implements AgentFactory { private readonly ownership: FactoryOwnership /** Plain holder prevents Cordis from re-tracing the factory's dependency context through a caller shadow. */ private readonly runtime: { ctx: Context } + private inbox!: ProjectionHandle + private turnBoundary!: ProjectionHandle constructor(ctx: Context, config: Config) { super(ctx, 'agentLoop') @@ -372,19 +325,17 @@ export class AgentLoop extends Service implements AgentFactory { validateConfiguredAgents(this.config.agents) // Register only after every config validation above has passed, so a // rejected constructor leaves no projection unit behind. - const ownership = this.ownership = new FactoryOwnership(ctx.fiber) + this.ownership = new FactoryOwnership(ctx.fiber) this.runtime = { ctx } - ctx.effect(function* () { - const unregisterTurnBoundary = ctx.sessionProjections.register(turnBoundaryProjectionDefinition) - yield unregisterTurnBoundary - const unregisterInbox = ctx.sessionProjections.register(inboxProjectionDefinition) - yield unregisterInbox - // Cordis skips later disposers after a rejection; finally still releases projections. - yield () => ownership.dispose().finally(() => { - unregisterInbox() - unregisterTurnBoundary() - }) - }, 'agentLoop.transactions()') + } + + protected async [Service.init](): Promise { + const ctx = this.runtime.ctx + const ownership = this.ownership + // Agents retain readers while fiber disposal removes global projection registrations. + ctx.effect(() => () => ownership.dispose(), 'agentLoop.transactions()') + this.turnBoundary = await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + this.inbox = await ctx.sessionProjections.register(inboxProjectionDefinition) ctx.effect(() => ctx.agents.setFactory(this), 'agentLoop.setFactory()') ctx.systemPrompt.variable('provider', context => context.agent?.options.provider) ctx.systemPrompt.variable('model', context => context.agent?.options.model) @@ -599,9 +550,10 @@ export class AgentLoop extends Service implements AgentFactory { } } const untrack = this.ownership.track(dispose) + const { inbox, turnBoundary } = this try { unfollowOwner = ownerCtx.effect(function* () { - const created = machine = new ReactLoopAgent(loopCtx, id, options, session) + const created = machine = new ReactLoopAgent(loopCtx, id, options, session, inbox, turnBoundary) machineReady.resolve() // Adopt the scope on its original fiber so it cannot unload before teardown. // A loop already unloading during minting owns the provisional scope itself. diff --git a/packages/core/agent-loop/tests/inbox.spec.ts b/packages/core/agent-loop/tests/inbox.spec.ts index 739d18b990..ba908715e0 100644 --- a/packages/core/agent-loop/tests/inbox.spec.ts +++ b/packages/core/agent-loop/tests/inbox.spec.ts @@ -49,10 +49,10 @@ async function inboxAgent(rawId: string): Promise<{ onTestFinished(() => ctx.fiber.dispose()) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(inboxProjectionDefinition) + const projection = await ctx.sessionProjections.register(inboxProjectionDefinition) const session = ctx.sessions.create(SessionId(rawId)) const agent = stubAgent(rawId, { ctx, session }) - const inbox = new ReactLoopInbox(ctx.sessionProjections, session, agentEvents(ctx, agent)) + const inbox = new ReactLoopInbox(() => projection.stateOf(session), session, agentEvents(ctx, agent)) Object.assign(agent, { inbox }) return { ctx, session, agent, inbox } } @@ -67,9 +67,9 @@ async function reconstructPersistedInbox( const session = ctx.sessions.create(SessionId(rawId)) populate(session) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(inboxProjectionDefinition) + const projection = await ctx.sessionProjections.register(inboxProjectionDefinition) const agent = stubAgent(rawId, { ctx, session }) - const inbox = new ReactLoopInbox(ctx.sessionProjections, session, agentEvents(ctx, agent)) + const inbox = new ReactLoopInbox(() => projection.stateOf(session), session, agentEvents(ctx, agent)) try { void inbox.nextTurn } catch (error: unknown) { @@ -80,12 +80,12 @@ async function reconstructPersistedInbox( } describe('ReactLoopInbox', () => { - it('reads the shared projection without owning its registration', async () => { + it('retains inbox reads after the shared registration is released', async () => { const ctx = new Context() onTestFinished(() => ctx.fiber.dispose()) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - const unregister = ctx.sessionProjections.register(inboxProjectionDefinition) + const projection = await ctx.sessionProjections.register(inboxProjectionDefinition) const session = ctx.sessions.create(SessionId('inbox-projection')) const pending = createUserMessage({ content: [{ type: 'text', text: 'pending' }], @@ -96,8 +96,8 @@ describe('ReactLoopInbox', () => { }) const agent = stubAgent('inbox-projection', { ctx, session }) const dispatch = agentEvents(ctx, agent) - const first = new ReactLoopInbox(ctx.sessionProjections, session, dispatch) - const second = new ReactLoopInbox(ctx.sessionProjections, session, dispatch) + const first = new ReactLoopInbox(() => projection.stateOf(session), session, dispatch) + const second = new ReactLoopInbox(() => projection.stateOf(session), session, dispatch) expect(first.nextTurn).toEqual([pending]) expect(second.nextTurn).toEqual([pending]) @@ -106,9 +106,12 @@ describe('ReactLoopInbox', () => { 'next-step': [], }) - unregister() - expect(ctx.sessionProjections.stateOf(session, 'inbox')).toBeUndefined() - expect(() => first.nextTurn).toThrow('its projection registration is not active') + await projection.dispose() + expect(ctx.sessionProjections.checkpoint(session).inbox?.val).toBeUndefined() + expect(first.nextTurn).toEqual([pending]) + first.clear() + expect(second.nextTurn).toEqual([]) + expect(ctx.sessionProjections.snapshot(session).values.inbox).toBeUndefined() }) it('rejects invalid durable coordinates and duplicate identities during reconstruction', async () => { @@ -141,10 +144,10 @@ describe('ReactLoopInbox', () => { onTestFinished(() => ctx.fiber.dispose()) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(inboxProjectionDefinition) + const projection = await ctx.sessionProjections.register(inboxProjectionDefinition) const parent = ctx.sessions.create(SessionId('inbox-fork-parent')) const parentAgent = stubAgent('inbox-fork-parent', { ctx, session: parent }) - const parentInbox = new ReactLoopInbox(ctx.sessionProjections, parent, agentEvents(ctx, parentAgent)) + const parentInbox = new ReactLoopInbox(() => projection.stateOf(parent), parent, agentEvents(ctx, parentAgent)) const inherited = createUserMessage({ content: [{ type: 'text', text: 'parent pending' }], source: { kind: 'user' }, @@ -152,7 +155,7 @@ describe('ReactLoopInbox', () => { parentInbox.append('next-turn', inherited) const child = ctx.sessions.fork(parent, undefined, SessionId('inbox-fork-child')) const childAgent = stubAgent('inbox-fork-child', { ctx, session: child }) - const childInbox = new ReactLoopInbox(ctx.sessionProjections, child, agentEvents(ctx, childAgent)) + const childInbox = new ReactLoopInbox(() => projection.stateOf(child), child, agentEvents(ctx, childAgent)) expect(child.inheritedEventCount).toBe(parent.snapshotEvents().length) expect(childInbox.nextTurn).toEqual([inherited]) @@ -175,7 +178,7 @@ describe('ReactLoopInbox', () => { let observed: readonly UserMessage[] | undefined ctx.on('session/event', (subject, event) => { if (subject === session && event.type === 'agent/inbox/spliced') { - observed = ctx.sessionProjections.stateOf(session, 'inbox')?.['next-turn'] + observed = inbox.nextTurn } }) diff --git a/packages/core/agent-loop/tests/loop.spec.ts b/packages/core/agent-loop/tests/loop.spec.ts index 9ae652df61..0dccc2a43b 100644 --- a/packages/core/agent-loop/tests/loop.spec.ts +++ b/packages/core/agent-loop/tests/loop.spec.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from 'vitest' +import { describe, expect, it, onTestFinished, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import LlmRuntime, { createUserMessage, ToolCallId, LlmError, ReasoningEffortId, StreamChunk, expandAssistantStream } from '@deepseek-ai/dsh-llm' import type { ContextFormed } from '@deepseek-ai/dsh-llm' @@ -1912,6 +1912,7 @@ describe('agent loop', () => { defaultEffort: effort, }) const ctx = new Context() + onTestFinished(async () => { await ctx.fiber.dispose() }) await ctx.plugin(LlmRuntime) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) @@ -1923,6 +1924,7 @@ describe('agent loop', () => { }) ctx.llm.registerAdapter(['mock'], adapter) + await vi.waitFor(() => { expect(ctx.agents.list()).toHaveLength(1) }) const agent = ctx.agents.list()[0]! expect(agent).toBeDefined() expect(agent.id).toBe(agent.session.id) @@ -1940,6 +1942,7 @@ describe('agent loop', () => { it('attaches config agent cwd to the fresh session header', async () => { const ctx = new Context() + onTestFinished(async () => { await ctx.fiber.dispose() }) await ctx.plugin(LlmRuntime) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) @@ -1950,6 +1953,7 @@ describe('agent loop', () => { agents: [{ id: SessionId('config-agent'), provider: 'mock', model: 'mock', cwd: '/work/project' }], }) + await vi.waitFor(() => { expect(ctx.agents.list()).toHaveLength(1) }) const agent = ctx.agents.list()[0]! expect(agent.session.header.cwd).toBe('/work/project') }) diff --git a/packages/core/agent-loop/tests/request-freeze.spec.ts b/packages/core/agent-loop/tests/request-freeze.spec.ts index 560be88627..ccb4c89770 100644 --- a/packages/core/agent-loop/tests/request-freeze.spec.ts +++ b/packages/core/agent-loop/tests/request-freeze.spec.ts @@ -3,12 +3,13 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import AgentLoop from '@deepseek-ai/dsh-agent-loop' -import type { Agent } from '@deepseek-ai/dsh-agent' +import { turnBoundaryProjectionDefinition, type Agent } from '@deepseek-ai/dsh-agent' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' import { createAssistantMessage, createUserMessage, isAgentLoopRequest } from '@deepseek-ai/dsh-llm' import type { GenerateOptions, ToolSchema } from '@deepseek-ai/dsh-llm' import { Session, SessionId, SessionLogOffset, SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session' import * as values from '@deepseek-ai/dsh-util-values' +import { inboxProjectionDefinition } from '../src/inbox.ts' import { ReactLoopAgent } from '../src/agent.ts' import { MockAdapter, textResponse } from './mock-adapter.ts' @@ -73,7 +74,9 @@ describe('loop-owned request freezing', () => { expect(Object.isFrozen(userEvent.data.content)).toBe(false) expect(Object.isFrozen(assistantEvent.data.message)).toBe(false) ctx.effect(() => ctx.sessions.enter(session)) - const agent = new ReactLoopAgent(loopCtx, id, { provider: 'mock', model: 'mock' }, session) + const agent = new ReactLoopAgent(loopCtx, id, { provider: 'mock', model: 'mock' }, session, + await loopCtx.sessionProjections.register(inboxProjectionDefinition), + await loopCtx.sessionProjections.register(turnBoundaryProjectionDefinition)) cleanups.push(async () => { agent.cancel({ kind: 'disposed' }) await agent.whenIdle() @@ -122,7 +125,9 @@ describe('loop-owned request freezing', () => { expect(Object.isFrozen(session.deriveMessages())).toBe(false) expect(freeze.mock.calls.filter(([value]) => value === userEvent.data)).toHaveLength(1) expect(freeze.mock.calls.filter(([value]) => value === replacement.data)).toHaveLength(1) - const resumed = new ReactLoopAgent(loopCtx, id, { provider: 'mock', model: 'mock' }, session) + const resumed = new ReactLoopAgent(loopCtx, id, { provider: 'mock', model: 'mock' }, session, + await loopCtx.sessionProjections.register(inboxProjectionDefinition), + await loopCtx.sessionProjections.register(turnBoundaryProjectionDefinition)) cleanups.push(async () => { resumed.cancel({ kind: 'disposed' }) await resumed.whenIdle() diff --git a/packages/core/agent-loop/tests/resource-lifecycle.spec.ts b/packages/core/agent-loop/tests/resource-lifecycle.spec.ts index 4ed14d48a4..cb4e272ecf 100644 --- a/packages/core/agent-loop/tests/resource-lifecycle.spec.ts +++ b/packages/core/agent-loop/tests/resource-lifecycle.spec.ts @@ -38,7 +38,6 @@ it.each([0, 1, 2])('retains Agent resources until all work exits with %i failed const aborted = Promise.withResolvers() let workExited = false let scopeDisposedBeforeWorkExit = false - let projectionPresentAtWorkExit = false let scopeDisposed = false let abortHandled = 0 const controller = new AbortController() @@ -57,8 +56,6 @@ it.each([0, 1, 2])('retains Agent resources until all work exits with %i failed const work = agent.runMaintenance(async (signal) => { signal.addEventListener('abort', () => { aborted.resolve(undefined) }, { once: true }) await release.promise - projectionPresentAtWorkExit = projections.stateOf(agent.session, 'inbox') !== undefined - && projections.stateOf(agent.session, 'turnBoundary') !== undefined workExited = true }) const disposal = loop.dispose() @@ -71,11 +68,10 @@ it.each([0, 1, 2])('retains Agent resources until all work exits with %i failed await Promise.all([disposal, ownerDisposal]) for (const { handle, error } of failed) await expect(handle.dispose()).rejects.toBe(error) expect(scopeDisposedBeforeWorkExit).toBe(false) - expect(projectionPresentAtWorkExit).toBe(true) expect(scopeDisposed).toBe(true) expect(abortHandled).toBe(1) - expect(projections.stateOf(agent.session, 'inbox')).toBeUndefined() - expect(projections.stateOf(agent.session, 'turnBoundary')).toBeUndefined() + expect(projections.checkpoint(agent.session).inbox).toBeUndefined() + expect(projections.checkpoint(agent.session).turnBoundary).toBeUndefined() expect(ctx.agents.get(agent.id)).toBeUndefined() expect(ctx.sessions.get(agent.session.id)).toBeUndefined() }) diff --git a/packages/core/agent-loop/tests/scope-lifecycle.spec.ts b/packages/core/agent-loop/tests/scope-lifecycle.spec.ts index 2addf9cba2..02d84f2fba 100644 --- a/packages/core/agent-loop/tests/scope-lifecycle.spec.ts +++ b/packages/core/agent-loop/tests/scope-lifecycle.spec.ts @@ -288,7 +288,7 @@ describe('agent scope lifecycle', () => { const { ctx, loopFiber } = await harnessWithLoop() onTestFinished(() => ctx.fiber.dispose()) const cold = ctx.sessions.create(SessionId('projection-before-any-agent')) - expect(ctx.sessionProjections.stateOf(cold, 'inbox')).toEqual({ 'next-turn': [], 'next-step': [] }) + expect(ctx.sessionProjections.checkpoint(cold).inbox?.val).toEqual({ 'next-turn': [], 'next-step': [] }) let first!: Awaited> let second!: Awaited> const firstOwner = await ctx.plugin(Object.assign(async (inner: Context) => { @@ -304,15 +304,15 @@ describe('agent scope lifecycle', () => { }) }, { inject: ['agents'] })) - expect(ctx.sessionProjections.stateOf(first.agent.session, 'inbox')).toBeDefined() + expect(ctx.sessionProjections.checkpoint(first.agent.session).inbox?.val).toBeDefined() await firstOwner.dispose() - expect(ctx.sessionProjections.stateOf(second.agent.session, 'inbox')).toBeDefined() + expect(ctx.sessionProjections.checkpoint(second.agent.session).inbox?.val).toBeDefined() await secondOwner.dispose() - expect(ctx.sessionProjections.stateOf(second.agent.session, 'inbox')).toBeDefined() + expect(ctx.sessionProjections.checkpoint(second.agent.session).inbox?.val).toBeDefined() await Promise.all([first.dispose(), second.dispose()]) await loopFiber.dispose() - expect(ctx.sessionProjections.stateOf(second.agent.session, 'inbox')).toBeUndefined() + expect(ctx.sessionProjections.checkpoint(second.agent.session).inbox?.val).toBeUndefined() }) it('agent.ctx listeners hear only their own agent (scoped dispatch end to end)', async () => { @@ -567,7 +567,7 @@ describe('agent scope lifecycle', () => { await ctx.fiber.dispose() }) - it('factory unload during scope minting skips setup and awaits provisional cleanup', async () => { + it('factory unload during scope minting drains provisional setup and cleanup', async () => { const { ctx, loopFiber } = await harnessWithLoop() let unloaded = false let setupCalls = 0 diff --git a/packages/core/agent/package.json b/packages/core/agent/package.json index deb3a08154..a8cd2318c6 100644 --- a/packages/core/agent/package.json +++ b/packages/core/agent/package.json @@ -60,5 +60,8 @@ "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/dsh-workspace": "workspace:*", "@deepseek-ai/cordis": "workspace:~" + }, + "dependencies": { + "zod": "4.4.3" } } diff --git a/packages/core/agent/src/index.ts b/packages/core/agent/src/index.ts index 54dd8a805d..b4ba4e77be 100644 --- a/packages/core/agent/src/index.ts +++ b/packages/core/agent/src/index.ts @@ -18,7 +18,7 @@ import type { AgentOptions, SessionStartSource } from './runtime-types.ts' export * from './runtime-types.ts' export * from './types.ts' -export type * from './projection.ts' +export { turnBoundaryProjectionDefinition } from './projection.ts' export * from './consumed-work.ts' export * from './model-selection.ts' export { agentCarrier, agentEvents, assembleContextFor, emitAgentEvent } from './dispatch.ts' diff --git a/packages/core/agent/src/projection.ts b/packages/core/agent/src/projection.ts index 89c10ac858..64333a692f 100644 --- a/packages/core/agent/src/projection.ts +++ b/packages/core/agent/src/projection.ts @@ -1,5 +1,8 @@ +/** Shared pure fold of the Session's turn and step events. */ import type { TurnBoundaryProjection } from './types.ts' -import type {} from '@deepseek-ai/dsh-session-projection' +import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection' +import { SessionSeq } from '@deepseek-ai/dsh-session' +import { z as zod } from 'zod' declare module '@deepseek-ai/dsh-session-projection/types' { interface SessionProjectionStateMap { @@ -8,4 +11,53 @@ declare module '@deepseek-ai/dsh-session-projection/types' { } } -export {} +const turnBoundaryProjectionSchema: zod.ZodType = zod.object({ + openTurnStartSeq: zod.number().int().nonnegative().transform(SessionSeq).nullable(), + lastStepStartSeq: zod.number().int().nonnegative().transform(SessionSeq).nullable(), + lastStepBoundary: zod.object({ + kind: zod.union([zod.literal('start'), zod.literal('end')]), + seq: zod.number().int().nonnegative().transform(SessionSeq), + }).nullable(), + lastTurn: zod.number().int().nonnegative(), +}) + +/** Host projection of agent turn and step boundaries. */ +export const turnBoundaryProjectionDefinition = { + key: 'turnBoundary', + stateVersion: 2, + stateSchema: turnBoundaryProjectionSchema, + init: () => ({ + openTurnStartSeq: null, + lastStepStartSeq: null, + lastStepBoundary: null, + lastTurn: 0, + }), + apply: (state, event) => { + switch (event.type) { + case 'turn/start': + return { + ...state, + openTurnStartSeq: event.seq, + lastTurn: event.data.turn, + } + case 'turn/end': + return { + ...state, + openTurnStartSeq: null, + } + case 'step/start': + return { + ...state, + lastStepStartSeq: event.seq, + lastStepBoundary: { kind: 'start', seq: event.seq }, + } + case 'step/end': + return { + ...state, + lastStepBoundary: { kind: 'end', seq: event.seq }, + } + default: + return state + } + }, +} satisfies ProjectionDefinition<'turnBoundary', TurnBoundaryProjection> diff --git a/packages/session/session-projection-cache/tests/cache.spec.ts b/packages/session/session-projection-cache/tests/cache.spec.ts index 87860844ee..77d2f81ab3 100644 --- a/packages/session/session-projection-cache/tests/cache.spec.ts +++ b/packages/session/session-projection-cache/tests/cache.spec.ts @@ -149,7 +149,7 @@ async function harness(options: HarnessOptions = {}) { await ctx.plugin({ name: storageDomainName, inject: storageDomainInject, apply: storageDomainApply, Config: storageDomainConfig }, { backend: 'json' }) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(marksUnit(options.stateVersion)) + await ctx.sessionProjections.register(marksUnit(options.stateVersion)) const fiber = await ctx.plugin(SessionProjectionCache, options.config ?? { writeEveryEvents: 100, writeIntervalMs: 60_000 }) return { ctx, root, fiber, cache: ctx.sessionProjectionCache } } @@ -300,7 +300,7 @@ describe('SessionProjectionCache write policy', () => { await ctx.sessionProjectionCache.write(clean) expect((await storedRows(root, clean.id))?.['cache-test/marks']).toEqual({ ver: 1, seq: -1, val: null }) // A unit whose state violates the plain-JSON contract fails the write loud. - ctx.sessionProjections.register({ + await ctx.sessionProjections.register({ key: 'cache-test/marks2', stateSchema: z.custom>(() => true), init: () => new Map(), @@ -336,7 +336,7 @@ describe('SessionProjectionCache write policy', () => { await ctx.plugin({ name: storageDomainName, inject: storageDomainInject, apply: storageDomainApply, Config: storageDomainConfig }, { backend: 'json' }) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(marksUnit()) + await ctx.sessionProjections.register(marksUnit()) await ctx.plugin(SessionProjectionCache, { writeEveryEvents: 100, writeIntervalMs: 60_000 }) const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {}) // A directory where the record document must land makes the atomic @@ -387,7 +387,7 @@ describe('SessionProjectionCache listing read', () => { 'cache-test/secondary-marks': { ver: 1, seq: SessionSeq(6), val: { marks: ['secondary'] } }, }) const { ctx, cache } = await harness({ root }) - ctx.sessionProjections.register(secondaryMarksUnit) + await ctx.sessionProjections.register(secondaryMarksUnit) // The block carries one watermark: the seq every served value has folded // through at least, so the lower of the two rows names the block. @@ -457,7 +457,7 @@ describe('SessionProjectionCache listing read', () => { }, ) const { ctx, cache } = await harness({ root }) - ctx.sessionProjections.register(titleUnit) + await ctx.sessionProjections.register(titleUnit) const seededHeader = { ...headerOf(id), isSeeded: true } // An older format generation never serves the full block, but its title @@ -509,7 +509,7 @@ describe('SessionProjectionCache listing read', () => { 'cache-test/secret': { ver: 1, seq: SessionSeq(4), val: 'private prompt text' }, }) const { ctx, cache } = await harness({ root }) - ctx.sessionProjections.register(secretUnit) + await ctx.sessionProjections.register(secretUnit) const header = headerOf(SessionId('host-state')) expect(cache.cachedSnapshot(header)).toEqual({ @@ -546,7 +546,7 @@ describe('SessionProjectionCache listing read', () => { 'cache-test/marks3': { ver: 1, seq: SessionSeq(4), val: { marks: ['b'] } }, }) const { ctx, cache } = await harness({ root }) - ctx.sessionProjections.register(marks3Unit) + await ctx.sessionProjections.register(marks3Unit) const block = cache.cachedSnapshot(headerOf(SessionId('multi-row'))) expect(block?.values).toEqual({ 'cache-test/marks': { marks: ['a'] }, @@ -752,7 +752,7 @@ describe('SessionProjectionCache cold-read seeding', () => { }) const { cache, ctx } = await harness({ root }) const apply = vi.fn((_state: number, _event: SessionEvent) => 1) - ctx.sessionProjections.register({ + await ctx.sessionProjections.register({ key: 'cache-test/count', stateSchema: z.number().int().nonnegative(), init: () => 0, @@ -794,7 +794,7 @@ describe('SessionProjectionCache cold-read seeding', () => { await ctx.plugin({ name: storageDomainName, inject: storageDomainInject, apply: storageDomainApply, Config: storageDomainConfig }, { backend: 'json' }) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(marksUnit()) + await ctx.sessionProjections.register(marksUnit()) await ctx.plugin(SessionProjectionCache, { writeEveryEvents: 100, writeIntervalMs: 60_000 }) const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {}) // A directory where the record document must land makes the write-back diff --git a/packages/session/session-projection-cache/tests/fixtures.spec.ts b/packages/session/session-projection-cache/tests/fixtures.spec.ts index 2849ea1b81..1fd5126eeb 100644 --- a/packages/session/session-projection-cache/tests/fixtures.spec.ts +++ b/packages/session/session-projection-cache/tests/fixtures.spec.ts @@ -110,7 +110,7 @@ async function harness(root: string) { const ctx = await storageHarness(root) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(titleUnit) + await ctx.sessionProjections.register(titleUnit) await ctx.plugin(SessionProjectionCache, { writeEveryEvents: 100, writeIntervalMs: 60_000 }) return { ctx, cache: ctx.sessionProjectionCache } } @@ -324,7 +324,7 @@ describe('archived version recovery', () => { await ctx.plugin({ name: storageDomainName, inject: storageDomainInject, apply: storageDomainApply, Config: storageDomainConfig }, { backend: 'json' }) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(titleUnit) + await ctx.sessionProjections.register(titleUnit) const error = vi.spyOn(ctx.logger, 'error').mockImplementation(() => {}) // The boot survives the broken record — this line rejecting IS the fixed bug. await ctx.plugin(SessionProjectionCache, { writeEveryEvents: 100, writeIntervalMs: 60_000 }) diff --git a/packages/session/session-projection/README.i18n.yaml b/packages/session/session-projection/README.i18n.yaml index c6305ff877..cf0fd8eeeb 100644 --- a/packages/session/session-projection/README.i18n.yaml +++ b/packages/session/session-projection/README.i18n.yaml @@ -18,14 +18,14 @@ en: 3a15581f0b043e79 zh: 302fc8afe8fb45fa /deepseek-ai-dsh-session-projection/use-this-package/when-to-choose-it: - en: 90b7c26cb45be52a - zh: 386e294ec09607c1 + en: 7ea79ed69e0ea085 + zh: b7791be4c547495c /deepseek-ai-dsh-session-projection/use-this-package/define-a-projection-unit: en: 09758d1475fad86e zh: 473637cb34aebe72 /deepseek-ai-dsh-session-projection/use-this-package/register-and-read: - en: 6a19c075f826b3f7 - zh: c200277cb58062be + en: e10e44c46035dbdb + zh: dff312b52f4ec2ce /deepseek-ai-dsh-session-projection/use-this-package/persisted-checkpoints: en: 45f1c05fb7e08180 zh: f8d62c749899651a diff --git a/packages/session/session-projection/README.md b/packages/session/session-projection/README.md index 79c5e0b213..61e2677e47 100644 --- a/packages/session/session-projection/README.md +++ b/packages/session/session-projection/README.md @@ -29,7 +29,7 @@ Mount `dsh-session-projection` wherever client carriers need current values of l ### When to choose it -Choose it when a domain keeps state that clients should see without re-deriving it — a todo list, a goal snapshot, conversation statistics. The registry drives units eagerly over committed events, so any registered unit's value is current by construction. Skip it for host-only bookkeeping that no client reads: a unit without a `wire` block stays host-only. A host reader either declares `sessionProjections` in its plugin `inject` or fails explicitly when the registry or required key is absent. Contributors may preserve optional registration through `ctx.inject(['sessionProjections'], ...)`. +Choose it when a domain keeps state that clients should see without re-deriving it — a todo list, a goal snapshot, conversation statistics. The registry drives units eagerly over committed events, so any registered unit's value is current by construction. A unit without a `wire` block keeps its state host-only. Host readers declare `sessionProjections` in their plugin dependencies and await registration before exposing synchronous reads. ### Define a projection unit @@ -55,10 +55,11 @@ const definition = { ### Register and read -`register(definition)` installs the unit; registrants with the same key and `stateVersion` share its cells, while an incompatible version or invalid `stateVersion` throws. Registration is an effect on the calling fiber, so the last unload removes the key and its cached cells. The returned disposer can be yielded into a composite `ctx.effect()` on the same fiber to keep the unit available through dependent cleanup. Carriers read a consistent synchronous cut over every client-visible unit with `snapshot(session)` — `{ asOfSeq, values }`, where `asOfSeq` is the seq of the last event every value reflects — and subscribe to per-change notifications with `onChanged(listener)`. `stateOf(session, key)` reads one unit's live read-only host state without computing unrelated views. +`await register(definition)` installs the unit and returns a handle with synchronous `stateOf(session)` reads and `dispose()`. Registrants with the same key and `stateVersion` share cells; incompatible or invalid versions reject registration. Registration belongs to the calling fiber. The last unregister removes the unit from global event drives, snapshots, checkpoints, and change notifications. Existing handles remain readable: each retains its original unit and folds new Session events on demand, even if another version later registers the same key. Reading a handle does not initialize unrelated units or register it again. Carriers use `snapshot(session)` for a consistent client-view cut and `onChanged(listener)` for changes to registered units. ```text -const dispose = ctx.sessionProjections.register(definition) +const projection = await ctx.sessionProjections.register(definition) +const state = projection.stateOf(session) const { asOfSeq, values } = ctx.sessionProjections.snapshot(session) ``` diff --git a/packages/session/session-projection/README.zh.md b/packages/session/session-projection/README.zh.md index cb44e32ef9..2a0ac169d7 100644 --- a/packages/session/session-projection/README.zh.md +++ b/packages/session/session-projection/README.zh.md @@ -29,7 +29,7 @@ kind: "package-reference" ### 何时选择 -当领域保存客户端应看到、但不应自行重新派生的状态——todo 清单、goal 快照、对话统计——时选择本包。注册表在已提交事件上主动驱动单元,因此任何已注册单元的值按构造即为当前值。当维护的是无客户端读取的 host-only 记账时跳过:不带 `wire` 块的单元保持 host-only。host 读取方要么在插件 `inject` 中声明 `sessionProjections`,要么在注册表或必需 key 缺席时明确失败。贡献方可以继续通过 `ctx.inject(['sessionProjections'], ...)` 保持可选注册。 +当领域保存客户端应看到、但不应自行重新派生的状态——todo 清单、goal 快照、对话统计——时选择本包。注册表在已提交事件上主动驱动单元,因此任何已注册单元的值按构造即为当前值。不带 `wire` 块的单元将状态保留在 host。host 读取方在插件依赖中声明 `sessionProjections`,并等待注册完成后才提供同步读取。 ### 定义投影单元 @@ -55,10 +55,11 @@ const definition = { ### 注册与读取 -`register(definition)` 安装单元;具有相同 key 和 `stateVersion` 的注册方共享其 cell,版本不兼容或 `stateVersion` 非法时会 throw。注册是挂在调用方 fiber 上的 effect,因此最后一个注册方卸载后会移除 key 及其缓存 cell。返回的 disposer 可以 yield 到同一 fiber 上的复合 `ctx.effect()` 中,使单元在依赖它的清理完成前保持可用。载体用 `snapshot(session)` 对每个客户端可见单元读取一致的同步切面——`{ asOfSeq, values }`,其中 `asOfSeq` 是所有值共同反映到的最后一个事件的 seq——并用 `onChanged(listener)` 订阅逐变更通知。`stateOf(session, key)` 读取一个单元的实时只读 host 状态,不计算无关视图。 +`await register(definition)` 安装单元,返回提供同步 `stateOf(session)` 读取和 `dispose()` 的 handle。具有相同 key 和 `stateVersion` 的注册方共享 cell;版本不兼容或非法时注册会拒绝。注册归调用方 fiber 所有。最后一个注册方注销后,单元退出全局事件驱动、快照、检查点和变更通知。已有 handle 仍可读取:它保留原有单元,按需折叠新增的 Session 事件,即使同一 key 后来注册了其他版本也不受影响。读取 handle 不会初始化无关单元,也不会重新注册。载体用 `snapshot(session)` 读取一致的客户端视图切面,用 `onChanged(listener)` 订阅已注册单元的变更。 ```text -const dispose = ctx.sessionProjections.register(definition) +const projection = await ctx.sessionProjections.register(definition) +const state = projection.stateOf(session) const { asOfSeq, values } = ctx.sessionProjections.snapshot(session) ``` diff --git a/packages/session/session-projection/src/index.ts b/packages/session/session-projection/src/index.ts index ac6e63458f..9a27c2b98a 100644 --- a/packages/session/session-projection/src/index.ts +++ b/packages/session/session-projection/src/index.ts @@ -37,6 +37,19 @@ import type { SessionProjectionMap, SessionProjectionStateMap } from './types.ts export type { SessionProjectionMap, SessionProjectionStateMap } from './types.ts' +/** An effect-owned registration with a reader retained independently of registration. */ +export interface ProjectionHandle { + /** + * Read the current state, including after unregistering, by folding new + * Session events with this handle's original unit. Do not mutate the live value. + * @param session - Session whose committed events are folded. + * @returns the projection state at the Session's current cursor. + */ + stateOf(session: Session): S + /** Unregister from global drives and snapshots without invalidating this reader. */ + dispose(): void | Promise +} + /** * One domain's state-driven computation unit: a pure synchronous fold plus * declarations and an optional client view — never an opaque getter. The framework drives @@ -156,8 +169,8 @@ interface UnitCell { } /** - * One live registration: the unit plus its per-session cells (dropped whole - * once the last registrant releases it). + * One shared unit and its per-session cells, retained by acquired readers after + * the final registration leaves the live table. * * `refs` exists because one unit definition already serves every session — the * cells are keyed by `Session` — while registrants are per-session: @@ -186,15 +199,11 @@ function cursorBefore(offset: SessionLogOffset): SessionSeqCursor { * raw result changes by `Object.is`. * Cells build lazily — a unit registered after events flowed, or a session * older than the registry, folds `init` over the in-memory log on first - * touch (event or read). Registration is an effect (disposer rides the - * calling fiber): an unloaded domain plugin's key disappears from snapshots - * and clients read it as capability absence. A host reader either declares - * `sessionProjections` in its plugin `inject` or fails explicitly when the - * registry or required key is absent. Contributors may preserve optional - * registration through `ctx.inject(['sessionProjections'], ...)`. Registrants sharing a key - * share one unit and are counted: the same tool package mounted in N agent - * presets registers N times, and the key survives until the last one - * unloads. + * touch (event or read). Consumers declare `sessionProjections` as a required + * injection, await registration during activation, and retain its ready handle. + * Registrants sharing a key share one unit and are counted. The last release + * removes the unit from live drives, checkpoints, and client snapshots; retained + * handles still fold new events on demand using the original unit. */ export class SessionProjectionRegistry extends Service { private readonly registrations = new Map() @@ -223,12 +232,12 @@ export class SessionProjectionRegistry extends Service { } /** - * Register one domain's unit. The registration is an effect on the calling - * context's fiber: disposing the fiber (or calling the returned disposer) - * removes the key — and the unit's cached cells — from subsequent drives - * and snapshots. + * Register one domain's unit on the calling context's fiber. Fiber or handle + * disposal releases one reference; the final release removes live participation. + * Retained handles remain readable after release. + * Callers must await registration before publishing consumers; registration may perform asynchronous work. * @param definition - key, state schema, pure unit functions, and stateVersion. - * @returns the exact disposer that unregisters this unit. + * @returns a ready handle with synchronous state reads and the effect disposer. */ register< K extends keyof SessionProjectionMap, @@ -237,22 +246,23 @@ export class SessionProjectionRegistry extends Service { definition: Omit, 'wire'> & { wire: NonNullable['wire']> }, - ): () => void + ): Promise> /** * Register one host-only unit. Its state is omitted from client snapshots * and always checkpointed like every other unit. + * Callers must await registration before publishing consumers; registration may perform asynchronous work. * @param definition - key, state schema, pure unit functions, and stateVersion. - * @returns the exact disposer that unregisters this unit. + * @returns a ready handle with synchronous state reads and the effect disposer. */ register< K extends Exclude, S extends SessionProjectionStateMap[K], >( definition: Omit, 'wire'>, - ): () => void - register( + ): Promise> + async register( definition: ProjectionDefinition, - ): () => void { + ): Promise> { const wire = definition.wire as { viewSchema: ZodType view(state: S): unknown @@ -270,27 +280,34 @@ export class SessionProjectionRegistry extends Service { if (!Number.isSafeInteger(definition.stateVersion) || definition.stateVersion < 0) { throw new Error(`session projection ${JSON.stringify(definition.key)} stateVersion must be a non-negative integer, got ${String(definition.stateVersion)}`) } - const dispose = this.ctx.effect(function* (this: SessionProjectionRegistry) { + let registration!: Registration + const dispose = this.ctx.effect(() => { const key = erased.key const existing = this.registrations.get(key) if (existing === undefined) { - this.registrations.set(key, { def: erased, cells: new WeakMap(), refs: 1 }) + registration = { def: erased, cells: new WeakMap(), refs: 1 } + this.registrations.set(key, registration) } else { if (existing.def.stateVersion !== erased.stateVersion) { throw new Error(`session projection key ${JSON.stringify(key)} is already registered at stateVersion ${String(existing.def.stateVersion)}; refusing to share it with stateVersion ${String(erased.stateVersion)}`) } existing.refs += 1 + registration = existing } - yield () => { - const live = this.registrations.get(key) - /* v8 ignore next -- the disposer runs once per successful registration, so the entry it counted is still here */ - if (live === undefined) return - live.refs -= 1 - if (live.refs === 0) this.registrations.delete(key) + return () => { + registration.refs -= 1 + if (registration.refs === 0) this.registrations.delete(key) } - }.bind(this), 'sessionProjections.register()') - // oxlint-disable-next-line typescript/no-misused-promises -- synchronous cleanup; keep the Cordis disposer so effects can adopt it. - return dispose + }, 'sessionProjections.register()') + // Observe disposal during the asynchronous registration handoff before exposing a reader. + await Promise.resolve() + this.ctx.fiber.assertActive() + return { + dispose, + stateOf: (session) => { + return this.cellFor(registration, session).state as S + }, + } } /** @@ -309,24 +326,6 @@ export class SessionProjectionRegistry extends Service { return () => void dispose() } - /** - * Read one unit's current host state after materializing every registered - * unit at the Session cursor. Unrelated wire views are not produced. - * The returned value is live; callers must not mutate it. - * @param session - the session whose state is read. - * @param key - the registered unit key. - * @returns current state, or `undefined` when the key is not registered. - */ - stateOf( - session: Session, - key: K, - ): SessionProjectionStateMap[K] | undefined { - const registration = this.registrations.get(key) - if (registration === undefined) return undefined - this.materializeCells(session) - return this.cellFor(registration, session).state as SessionProjectionStateMap[K] - } - /** * One consistent cut over every registered client-visible unit for one session, read from * the watermark cache (missing cells fold lazily over the in-memory log). diff --git a/packages/session/session-projection/tests/registration-reader.spec.ts b/packages/session/session-projection/tests/registration-reader.spec.ts new file mode 100644 index 0000000000..3ee393b85a --- /dev/null +++ b/packages/session/session-projection/tests/registration-reader.spec.ts @@ -0,0 +1,113 @@ +/** Retained readers follow Session events independently of registry participation. */ +import { Context } from '@deepseek-ai/cordis' +import SessionStore from '@deepseek-ai/dsh-session' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' +import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection' +import { describe, expect, it, onTestFinished, vi } from 'vitest' +import { z } from 'zod' + +declare module '@deepseek-ai/dsh-session-projection/types' { + interface SessionProjectionStateMap { + 'test/reader': number + 'test/unrelated-reader': number + } + interface SessionProjectionMap { + 'test/reader': number + } +} + +const definition = { + key: 'test/reader', + stateSchema: z.number(), + stateVersion: 1, + init: () => 0, + apply: (state: number) => state + 1, + wire: { viewSchema: z.number(), view: (state: number) => state }, +} satisfies ProjectionDefinition<'test/reader', number> + +async function setup() { + const ctx = new Context() + onTestFinished(async () => { await ctx.fiber.dispose() }) + await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) + const owner = await ctx.plugin(Object.assign(function registrationOwner() {}, { inject: ['sessionProjections'] })) + const session = ctx.sessions.create() + return { ctx, owner, session } +} + +describe('retained projection readers', () => { + it.each(['handle', 'fiber'] as const)('reads new events after %s disposal without rejoining snapshots or notifications', async (release) => { + const { ctx, owner, session } = await setup() + const apply = vi.fn(definition.apply) + const handle = await owner.ctx.sessionProjections.register({ ...definition, apply }) + const changed = vi.fn() + ctx.sessionProjections.onChanged(changed) + expect(handle.stateOf(session)).toBe(0) + session.append('turn/start', { turn: 1 }) + expect(handle.stateOf(session)).toBe(1) + expect(changed).toHaveBeenCalledOnce() + if (release === 'handle') await handle.dispose() + else await owner.dispose() + session.append('turn/start', { turn: 2 }) + expect(apply).toHaveBeenCalledTimes(1) + expect(handle.stateOf(session)).toBe(2) + expect(apply).toHaveBeenCalledTimes(2) + expect(changed).toHaveBeenCalledOnce() + expect(ctx.sessionProjections.snapshot(session).values).toEqual({}) + expect(ctx.sessionProjections.checkpoint(session)).toEqual({}) + const later = ctx.sessions.create() + later.append('turn/start', { turn: 1 }) + expect(handle.stateOf(later)).toBe(1) + }) + + it('keeps the original unit when the same key is registered with a new version', async () => { + const { ctx, owner, session } = await setup() + const original = await owner.ctx.sessionProjections.register(definition) + expect(original.stateOf(session)).toBe(0) + await original.dispose() + const replacement = await ctx.sessionProjections.register({ ...definition, stateVersion: 2, apply: state => state + 10 }) + session.append('turn/start', { turn: 1 }) + expect(original.stateOf(session)).toBe(1) + expect(replacement.stateOf(session)).toBe(10) + expect(ctx.sessionProjections.snapshot(session).values).toEqual({ 'test/reader': 10 }) + }) + + it('reads only the selected unit without materializing unrelated registrations', async () => { + const { ctx, owner, session } = await setup() + const unrelatedInit = vi.fn(() => 0) + await ctx.sessionProjections.register({ + key: 'test/unrelated-reader', stateSchema: z.number(), stateVersion: 1, + init: unrelatedInit, apply: state => state, + }) + const handle = await owner.ctx.sessionProjections.register(definition) + expect(handle.stateOf(session)).toBe(0) + expect(unrelatedInit).not.toHaveBeenCalled() + }) + + it('rejects initialization when the owner unloads before the handle is returned', async () => { + const { ctx, owner, session } = await setup() + const registration = owner.ctx.sessionProjections.register(definition) + const rejected = expect(registration).rejects.toThrow() + await owner.dispose() + await rejected + expect(ctx.sessionProjections.checkpoint(session)).toEqual({}) + }) + + it.each(['twice', 'owner-unload'] as const)('releases one reference once under %s disposal', async (release) => { + const { ctx, owner, session } = await setup() + const retained = await ctx.sessionProjections.register(definition) + const handle = await owner.ctx.sessionProjections.register(definition) + if (release === 'twice') { + await handle.dispose() + await handle.dispose() + } else { + await Promise.all([owner.dispose(), Promise.resolve(handle.dispose())]) + } + session.append('turn/start', { turn: 1 }) + expect(ctx.sessionProjections.snapshot(session).values).toEqual({ 'test/reader': 1 }) + expect(handle.stateOf(session)).toBe(1) + await retained.dispose() + expect(ctx.sessionProjections.snapshot(session).values).toEqual({}) + }) + +}) diff --git a/packages/session/session-projection/tests/registry.spec.ts b/packages/session/session-projection/tests/registry.spec.ts index de7ecdde60..189bcf96f2 100644 --- a/packages/session/session-projection/tests/registry.spec.ts +++ b/packages/session/session-projection/tests/registry.spec.ts @@ -165,7 +165,7 @@ describe('SessionProjectionRegistry drive', () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(cutUnit()) + const cut = await ctx.sessionProjections.register(cutUnit()) const inherited: SessionEvent[] = [ { type: 'turn/start', seq: SessionSeq(0), time: 1, data: { turn: 1 } }, { type: 'turn/end', seq: SessionSeq(1), time: 2, data: { turn: 1, reason: { kind: 'completed' } } }, @@ -176,7 +176,7 @@ describe('SessionProjectionRegistry drive', () => { meta: { isSeeded: true }, }) - expect(ctx.sessionProjections.stateOf(session, 'test/cut')).toBe(inherited.length) + expect(cut.stateOf(session)).toBe(inherited.length) const restored = ctx.sessionProjections.restore( {}, inherited, @@ -197,12 +197,12 @@ describe('SessionProjectionRegistry drive', () => { inherited, SessionLogOffset(0), ).asOfSeq).toBe(1) - expect(ctx.sessionProjections.stateOf(prepared, 'test/cut')).toBe(inherited.length) + expect(cut.stateOf(prepared)).toBe(inherited.length) }) it('drives a registered unit over committed events and snapshots the current value', async () => { const { ctx, session } = await harness() - ctx.sessionProjections.register(marksUnit()) + await ctx.sessionProjections.register(marksUnit()) mark(session, ['a']) mark(session, ['a', 'b']) const snapshot = ctx.sessionProjections.snapshot(session) @@ -213,7 +213,7 @@ describe('SessionProjectionRegistry drive', () => { it('builds the cell lazily from the full log for a unit registered after events flowed', async () => { const { ctx, session } = await harness() mark(session, ['pre-registration']) - ctx.sessionProjections.register(marksUnit()) + await ctx.sessionProjections.register(marksUnit()) expect(ctx.sessionProjections.snapshot(session).values['test/marks']).toEqual({ marks: ['pre-registration'] }) // The lazily-built cell then continues on the live drive path. mark(session, ['after']) @@ -222,7 +222,7 @@ describe('SessionProjectionRegistry drive', () => { it('serves init-derived state and asOfSeq -1 for an empty log', async () => { const { ctx, session } = await harness() - ctx.sessionProjections.register(marksUnit()) + await ctx.sessionProjections.register(marksUnit()) const snapshot = ctx.sessionProjections.snapshot(session) expect(snapshot.asOfSeq).toBe(-1) expect(snapshot.values['test/marks']).toEqual({ marks: [] }) @@ -230,7 +230,7 @@ describe('SessionProjectionRegistry drive', () => { it('notifies onChanged with the validated view and the causing seq, and skips same-reference applies', async () => { const { ctx, session } = await harness() - ctx.sessionProjections.register(marksUnit()) + await ctx.sessionProjections.register(marksUnit()) const seen: { key: string; value: unknown; seq: SessionSeq; sessionId: string }[] = [] ctx.sessionProjections.onChanged((changedSession, key, value, seq) => { seen.push({ key, value, seq, sessionId: String(changedSession.id) }) @@ -244,19 +244,19 @@ describe('SessionProjectionRegistry drive', () => { it('does not compute a view while no change listener exists', async () => { const { ctx, session } = await harness() const view = vi.fn((state: StableViewState) => state.value) - ctx.sessionProjections.register(stableViewUnit(view)) + const handle = await ctx.sessionProjections.register(stableViewUnit(view)) session.append('turn/start', { turn: 1 }) session.append('turn/start', { turn: 2 }) - expect(ctx.sessionProjections.stateOf(session, 'test/stable-view')?.revision).toBe(2) + expect(handle.stateOf(session).revision).toBe(2) expect(view).not.toHaveBeenCalled() }) it('publishes the first observed view and suppresses later same-reference views', async () => { const { ctx, session } = await harness() const view = vi.fn((state: StableViewState) => state.value) - ctx.sessionProjections.register(stableViewUnit(view)) + await ctx.sessionProjections.register(stableViewUnit(view)) const seen: unknown[] = [] ctx.sessionProjections.onChanged((_session, key, value) => { @@ -277,7 +277,7 @@ describe('SessionProjectionRegistry drive', () => { it('publishes the first view after an unobserved state change', async () => { const { ctx, session } = await harness() const view = vi.fn((state: StableViewState) => state.value) - ctx.sessionProjections.register(stableViewUnit(view)) + await ctx.sessionProjections.register(stableViewUnit(view)) const first: unknown[] = [] const stop = ctx.sessionProjections.onChanged((_session, key, value) => { if (key === 'test/stable-view') first.push(value) @@ -305,7 +305,7 @@ describe('SessionProjectionRegistry drive', () => { const stateByEvent = new Map() const viewByState = new Map() const computedViews: MarksView[] = [] - ctx.sessionProjections.register({ + await ctx.sessionProjections.register({ key: 'test/marks', stateSchema: marksViewSchema.nullable(), init: () => initialState, @@ -431,7 +431,7 @@ describe('SessionProjectionRegistry drive', () => { it('drives independently per session (cells are per-session watermarks)', async () => { const { ctx, session } = await harness() const other = ctx.sessions.create() - ctx.sessionProjections.register(marksUnit()) + await ctx.sessionProjections.register(marksUnit()) mark(session, ['one']) mark(other, ['two']) expect(ctx.sessionProjections.snapshot(session).values['test/marks']).toEqual({ marks: ['one'] }) @@ -440,70 +440,71 @@ describe('SessionProjectionRegistry drive', () => { it('updates host-only units without publishing them to wire listeners', async () => { const { ctx, session } = await harness() - ctx.sessionProjections.register(marksUnit()) - ctx.sessionProjections.register(countUnit()) + await ctx.sessionProjections.register(marksUnit()) + const count = await ctx.sessionProjections.register(countUnit()) const changedKeys: string[] = [] ctx.sessionProjections.onChanged((_session, key) => { changedKeys.push(key) }) session.append('turn/start', { turn: 1 }) expect(changedKeys).toEqual([]) - expect(ctx.sessionProjections.stateOf(session, 'test/count')).toBe(1) + expect(count.stateOf(session)).toBe(1) expect(ctx.sessionProjections.snapshot(session).values).toEqual({ 'test/marks': { marks: [] } }) }) it('shares one unit between registrants of the same key', async () => { const { ctx, session } = await harness() - ctx.sessionProjections.register(marksUnit()) + await ctx.sessionProjections.register(marksUnit()) // One definition already serves every session (cells are keyed by // Session), and registrants are per-session now: an agent preset mounts // the same tool package once per agent. - expect(() => ctx.sessionProjections.register(marksUnit())).not.toThrow() + const handle = await ctx.sessionProjections.register(marksUnit()) + expect(typeof handle.stateOf).toBe('function') mark(session, ['kept']) expect(ctx.sessionProjections.snapshot(session).values['test/marks']).toEqual({ marks: ['kept'] }) }) it('keeps the unit until the last registrant releases it', async () => { const { ctx, session } = await harness() - const first = ctx.sessionProjections.register(marksUnit()) - const second = ctx.sessionProjections.register(marksUnit()) + const first = await ctx.sessionProjections.register(marksUnit()) + const second = await ctx.sessionProjections.register(marksUnit()) mark(session, ['kept']) - first() + await first.dispose() // The regression this counts against: without last-release semantics, one // session ending strips the projection from every other live session, // because the first registrant owns the only disposer. expect(ctx.sessionProjections.snapshot(session).values['test/marks']).toEqual({ marks: ['kept'] }) - second() + await second.dispose() expect(ctx.sessionProjections.snapshot(session).values).toEqual({}) }) it('refuses to share a key across a stateVersion change', async () => { const { ctx } = await harness() - ctx.sessionProjections.register(marksUnit()) + await ctx.sessionProjections.register(marksUnit()) // The one incompatibility a runtime comparison can name: the versioned // contract says the cached state shape differs, so the two cannot share // cells. Everything else about a definition is functions. - expect(() => ctx.sessionProjections.register({ ...marksUnit(), stateVersion: 9 })) - .toThrow(/already registered at stateVersion 1; refusing to share it with stateVersion 9/) + await expect(ctx.sessionProjections.register({ ...marksUnit(), stateVersion: 9 })) + .rejects.toThrow(/already registered at stateVersion 1; refusing to share it with stateVersion 9/) }) it('rejects a non-integer or negative stateVersion at register time', async () => { const { ctx } = await harness() - expect(() => ctx.sessionProjections.register({ ...marksUnit(), stateVersion: -1 })).toThrow(/stateVersion/) - expect(() => ctx.sessionProjections.register({ ...marksUnit(), stateVersion: 1.5 })).toThrow(/stateVersion/) + await expect(ctx.sessionProjections.register({ ...marksUnit(), stateVersion: -1 })).rejects.toThrow(/stateVersion/) + await expect(ctx.sessionProjections.register({ ...marksUnit(), stateVersion: 1.5 })).rejects.toThrow(/stateVersion/) }) it('register() disposer removes the key (with its cells) and frees it for re-registration', async () => { const { ctx, session } = await harness() - const dispose = ctx.sessionProjections.register(marksUnit()) + const dispose = await ctx.sessionProjections.register(marksUnit()) mark(session, ['cached']) - dispose() + await dispose.dispose() expect(ctx.sessionProjections.snapshot(session).values).toEqual({}) - ctx.sessionProjections.register(marksUnit()) + await ctx.sessionProjections.register(marksUnit()) // Fresh registration rebuilds from the log, not from a stale cell. expect(ctx.sessionProjections.snapshot(session).values['test/marks']).toEqual({ marks: ['cached'] }) }) @@ -511,8 +512,8 @@ describe('SessionProjectionRegistry drive', () => { it('removes registrations and change listeners when their owning fiber unloads (HMR safety)', async () => { const { ctx, session } = await harness() const notifications: string[] = [] - const fiber = await ctx.plugin(Object.assign((inner: Context) => { - inner.sessionProjections.register(marksUnit()) + const fiber = await ctx.plugin(Object.assign(async (inner: Context) => { + await inner.sessionProjections.register(marksUnit()) inner.sessionProjections.onChanged((_session, key) => { notifications.push(key) }) @@ -525,38 +526,36 @@ describe('SessionProjectionRegistry drive', () => { expect(ctx.sessionProjections.snapshot(session).values).toEqual({}) }) - it('keeps a yielded registration live until the composite finishes using it', async () => { + it('retains readable state during owner teardown', async () => { const { ctx, session } = await harness() onTestFinished(async () => { await ctx.fiber.dispose() }) let observed: MarksState | undefined - const fiber = await ctx.plugin(Object.assign((inner: Context) => { - inner.effect(function* () { - yield inner.sessionProjections.register(marksUnit()) - yield () => { observed = inner.sessionProjections.stateOf(session, 'test/marks') } - }) + const fiber = await ctx.plugin(Object.assign(async (inner: Context) => { + const handle = await inner.sessionProjections.register(marksUnit()) + inner.effect(() => () => { observed = handle.stateOf(session) }) }, { inject: ['sessionProjections'] })) mark(session, ['live']) await fiber.dispose() expect(observed).toEqual({ marks: ['live'] }) - expect(ctx.sessionProjections.stateOf(session, 'test/marks')).toBeUndefined() + expect(ctx.sessionProjections.snapshot(session).values['test/marks']).toBeUndefined() }) it('snapshot serves client views and excludes host-only state', async () => { const { ctx, session } = await harness() - ctx.sessionProjections.register(marksUnit()) - ctx.sessionProjections.register(countUnit()) + await ctx.sessionProjections.register(marksUnit()) + const count = await ctx.sessionProjections.register(countUnit()) mark(session, ['a', 'b']) const values = ctx.sessionProjections.snapshot(session).values expect(values['test/marks']).toEqual({ marks: ['a', 'b'] }) expect('test/count' in values).toBe(false) - expect(ctx.sessionProjections.stateOf(session, 'test/count')).toBe(1) + expect(count.stateOf(session)).toBe(1) expect('test/unregistered' in values).toBe(false) }) it('checkpoints every persisted unit with its stateVersion and per-cell watermark', async () => { const { ctx, session } = await harness() - ctx.sessionProjections.register(marksUnit()) - ctx.sessionProjections.register({ ...countUnit(), stateVersion: 7 }) + await ctx.sessionProjections.register(marksUnit()) + await ctx.sessionProjections.register({ ...countUnit(), stateVersion: 7 }) const markEvent = mark(session, ['a']) const rows = ctx.sessionProjections.checkpoint(session) expect(rows['test/marks']).toEqual({ ver: 1, seq: markEvent.seq, val: { marks: ['a'] } }) @@ -568,7 +567,7 @@ describe('SessionProjectionRegistry drive', () => { it('checkpoint states are detached clones — mutating them cannot corrupt the watermark cache', async () => { const { ctx, session } = await harness() - ctx.sessionProjections.register(marksUnit()) + await ctx.sessionProjections.register(marksUnit()) mark(session, ['a']) const rows = ctx.sessionProjections.checkpoint(session) // Hostile (or merely careless) consumer mutates the handed-out state. @@ -582,8 +581,8 @@ describe('SessionProjectionRegistry drive', () => { it('restoreFloor anchors one below the lowest usable watermark and at 0 for missing or mismatched rows', async () => { const { ctx } = await harness() expect(ctx.sessionProjections.restoreFloor({})).toBeUndefined() // no unit registered - ctx.sessionProjections.register(marksUnit()) - ctx.sessionProjections.register(countUnit()) + await ctx.sessionProjections.register(marksUnit()) + await ctx.sessionProjections.register(countUnit()) expect(ctx.sessionProjections.restoreFloor({})).toBe(0) // Lowest usable watermark is count's 5 → the anchored tail starts AT 5 // (one below the first needed seq 6), so the read proves seq 5 still exists. @@ -605,8 +604,8 @@ describe('SessionProjectionRegistry drive', () => { it('restore folds the tail past each usable row and refolds from init on version mismatch', async () => { const { ctx } = await harness() - ctx.sessionProjections.register(marksUnit()) - ctx.sessionProjections.register(countUnit()) + await ctx.sessionProjections.register(marksUnit()) + await ctx.sessionProjections.register(countUnit()) const tail: SessionEvent[] = [ { type: 'test/mark', seq: SessionSeq(3), time: 3, data: { marks: ['new'] } }, { type: 'turn/end', seq: SessionSeq(4), time: 4, data: { turn: 1, reason: { kind: 'completed' } } }, @@ -639,8 +638,8 @@ describe('SessionProjectionRegistry drive', () => { it('restore over a suffix folds only past each row watermark and serves an exact empty-tail cut', async () => { const { ctx } = await harness() - ctx.sessionProjections.register(marksUnit()) - ctx.sessionProjections.register(countUnit()) + await ctx.sessionProjections.register(marksUnit()) + await ctx.sessionProjections.register(countUnit()) const rows = { 'test/marks': { ver: 1, seq: SessionSeq(4), val: { marks: ['done'] } }, 'test/count': { ver: 1, seq: SessionSeq(2), val: 3 }, @@ -675,8 +674,8 @@ describe('SessionProjectionRegistry drive', () => { it('viewCheckpoint serves version-matching rows without any log and skips mismatched keys', async () => { const { ctx } = await harness() - ctx.sessionProjections.register(marksUnit()) - ctx.sessionProjections.register(countUnit()) + await ctx.sessionProjections.register(marksUnit()) + await ctx.sessionProjections.register(countUnit()) const values = ctx.sessionProjections.viewCheckpoint({ 'test/marks': { ver: 1, seq: SessionSeq(4), val: { marks: ['stored'] } }, 'test/count': { ver: 99, seq: SessionSeq(4), val: 5 }, // mismatched: absent @@ -688,8 +687,8 @@ describe('SessionProjectionRegistry drive', () => { it('viewCheckpoint and restore exclude host-only state while retaining its checkpoint', async () => { const { ctx } = await harness() - ctx.sessionProjections.register(marksUnit()) - ctx.sessionProjections.register(countUnit()) + await ctx.sessionProjections.register(marksUnit()) + await ctx.sessionProjections.register(countUnit()) const rows = { 'test/marks': { ver: 1, seq: SessionSeq(4), val: { marks: ['stored'] } }, 'test/count': { ver: 1, seq: SessionSeq(4), val: 5 }, @@ -713,7 +712,7 @@ describe('SessionProjectionRegistry drive', () => { it('rejects version-matching rows whose state no longer matches the registered schema', async () => { const { ctx } = await harness() - ctx.sessionProjections.register(marksUnit()) + await ctx.sessionProjections.register(marksUnit()) const drifted = { 'test/marks': { ver: 1, seq: SessionSeq(2), val: { marks: 'not-an-array' } }, } @@ -730,7 +729,7 @@ describe('SessionProjectionRegistry drive', () => { it('restore rejects a row claiming events past the supplied log end (shrunk log ⇒ re-read)', async () => { const { ctx } = await harness() - ctx.sessionProjections.register(countUnit()) + await ctx.sessionProjections.register(countUnit()) const rows = { 'test/count': { ver: 1, seq: SessionSeq(9), val: 10 } } // The anchored floor sits ON the watermark, so the tail read must return // at least seq 9 from an intact log… @@ -775,7 +774,7 @@ describe('SessionProjectionRegistry drive', () => { it('fails loud when a unit view violates its own schema (async unit output is unrepresentable)', async () => { const { ctx, session } = await harness() - ctx.sessionProjections.register({ + await ctx.sessionProjections.register({ key: 'test/marks', stateSchema: z.object({ marks: z.array(z.string()) }).nullable(), init: () => null as MarksState, From 08e48f34b583f19453305f78dbc0a554c2099890 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Mon, 12 Oct 2026 12:03:36 +0800 Subject: [PATCH 028/369] refactor(plugins): migrate projection consumers to retained handles --- .../api/session-controller/README.i18n.yaml | 4 +- packages/api/session-controller/README.md | 2 + packages/api/session-controller/README.zh.md | 2 + packages/api/session-controller/src/agent.ts | 30 +- packages/api/session-controller/src/index.ts | 37 +- packages/api/session-controller/src/list.ts | 32 +- .../src/model-selection-projection.ts | 12 +- .../tests/agent.host.spec.ts | 16 +- .../commands-queue-attachment.host.spec.ts | 2 +- .../tests/controller.host.spec.ts | 6 +- .../tests/list-scheduling.host.spec.ts | 8 +- .../tests/projection-discovery.host.spec.ts | 6 +- .../tests/session-cold.host.spec.ts | 42 +- .../tests/session-fork.host.spec.ts | 44 +- .../session-history-journal.host.spec.ts | 12 +- .../tests/session-list-blank.host.spec.ts | 2 +- .../tests/session-models.host.spec.ts | 44 +- .../session-open-workspace-path.host.spec.ts | 36 +- .../tests/session-presets.host.spec.ts | 4 +- .../tests/session-projections.host.spec.ts | 86 ++-- .../tests/session-rename.host.spec.ts | 10 +- .../tests/session-search.host.spec.ts | 56 +-- .../tests/subagent-catalog.host.spec.ts | 4 +- .../session-controller/tests/test-remote.ts | 33 +- .../tests/transport.host.spec.ts | 2 +- .../agent-instructions/README.i18n.yaml | 4 +- packages/context/agent-instructions/README.md | 2 +- .../context/agent-instructions/README.zh.md | 2 +- .../context/agent-instructions/src/index.ts | 23 +- .../tests/agent-instructions.spec.ts | 33 +- .../harness-dev-context/README.i18n.yaml | 4 +- .../context/harness-dev-context/README.md | 2 +- .../context/harness-dev-context/README.zh.md | 2 +- .../context/harness-dev-context/src/index.ts | 8 +- packages/context/time-context/src/index.ts | 8 +- .../time-context/tests/time-context.spec.ts | 6 +- packages/context/tmux-context/src/index.ts | 12 +- .../deliverables/tool-present/src/index.ts | 11 +- .../tool-present/tests/built-errors.e2e.ts | 6 +- .../tool-present/tests/present.spec.ts | 9 +- .../experimental/agent-team/README.i18n.yaml | 8 +- packages/experimental/agent-team/README.md | 4 +- packages/experimental/agent-team/README.zh.md | 4 +- packages/experimental/agent-team/src/index.ts | 26 +- .../experimental/agent-team/src/journal.ts | 9 +- .../tests/projection-events.spec.ts | 6 +- .../agent-team/tests/team.spec.ts | 60 ++- .../claude-code-mods/README.i18n.yaml | 4 +- .../experimental/claude-code-mods/README.md | 2 +- .../claude-code-mods/README.zh.md | 2 +- .../claude-code-mods/src/host-ops.ts | 34 +- .../claude-code-mods/src/index.ts | 27 +- .../claude-code-mods/tests/bridge.spec.ts | 4 +- .../claude-code-mods/tests/define-mod.spec.ts | 8 +- .../claude-code-mods/tests/host-ops.spec.ts | 42 +- .../tests/activation.host.spec.ts | 6 +- .../hooks-claude-code/src/index.ts | 34 +- .../hooks-claude-code/tests/coverage-cases.ts | 2 +- .../experimental/hooks-codex/src/index.ts | 71 +-- .../hooks-codex/tests/coverage-cases.ts | 2 +- .../tests/provider.spec.ts | 4 +- .../tool-session-query/src/index.ts | 25 +- .../tool-session-query/src/operations.ts | 423 ++++++++++-------- .../src/workspace-access.ts | 21 +- .../tests/sqlite-integration.spec.ts | 8 +- .../tests/tool-session-query.spec.ts | 24 +- packages/fs/tool-fs/tests/tools.spec.ts | 2 +- packages/goal/goal/src/index.ts | 19 +- packages/goal/goal/tests/goal.spec.ts | 12 +- packages/goal/goal/tests/projection.spec.ts | 5 +- packages/goal/tool-goal/README.i18n.yaml | 4 +- packages/goal/tool-goal/README.md | 1 + packages/goal/tool-goal/README.zh.md | 1 + packages/goal/tool-goal/package.json | 3 +- packages/goal/tool-goal/src/authority.ts | 140 +++--- packages/goal/tool-goal/src/index.ts | 13 +- .../goal/tool-goal/tests/tool-goal.spec.ts | 49 +- .../permission-presets/README.i18n.yaml | 8 +- .../interaction/permission-presets/README.md | 4 +- .../permission-presets/README.zh.md | 4 +- .../permission-presets/src/index.ts | 14 +- .../tests/permission-presets.spec.ts | 19 +- .../interaction/user-questions/src/index.ts | 21 +- .../tests/user-questions.spec.ts | 2 +- packages/llm/llm-retry/src/index.ts | 20 +- packages/llm/llm-retry/tests/retry.spec.ts | 29 +- packages/llm/token-meter/src/index.ts | 10 +- .../context-breakdown-projection.spec.ts | 3 +- .../token-meter/tests/route-pricing.spec.ts | 6 +- .../llm/token-meter/tests/token-meter.spec.ts | 8 +- packages/plan/plan-mode/README.i18n.yaml | 8 +- packages/plan/plan-mode/README.md | 4 +- packages/plan/plan-mode/README.zh.md | 4 +- packages/plan/plan-mode/src/index.ts | 18 +- .../plan/plan-mode/tests/integration.spec.ts | 3 +- .../plan/plan-mode/tests/plan-mode.spec.ts | 21 +- .../plan/plan-mode/tests/projection.spec.ts | 8 +- .../preset/agent-preset-registry/src/index.ts | 16 +- .../tests/registry.spec.ts | 6 +- packages/sandbox/sandbox-policy/src/index.ts | 11 +- .../sandbox-policy/tests/policy.spec.ts | 4 +- packages/session/session-stats/src/index.ts | 8 +- .../tests/provider.spec.ts | 4 +- packages/session/session-title/src/index.ts | 19 +- .../session-title/tests/projection.spec.ts | 6 +- .../session-title/tests/provider.spec.ts | 4 +- packages/session/session-title/tsconfig.json | 1 - .../session/session-turn-outline/src/index.ts | 8 +- .../tests/projection.spec.ts | 2 +- .../session/working-directory/src/index.ts | 13 +- packages/shell/tool-bash/tests/tools.spec.ts | 2 +- packages/shell/tool-pwsh/tests/tools.spec.ts | 2 +- .../fixtures/loader/scoped-tool-subagent.ts | 4 +- .../tests/subagent-dsh-sdk.spec.ts | 2 + packages/subagent/subagent/README.i18n.yaml | 4 +- packages/subagent/subagent/README.md | 2 +- packages/subagent/subagent/README.zh.md | 2 +- packages/subagent/subagent/package.json | 3 - packages/subagent/subagent/src/index.ts | 16 +- .../subagent/subagent/tests/catalog.spec.ts | 4 +- .../tests/continuation-inheritance.spec.ts | 2 +- .../subagent/tests/continuation.spec.ts | 5 + .../subagent/subagent/tests/control.spec.ts | 2 + .../subagent/tests/list-children.spec.ts | 9 +- .../subagent/subagent/tests/service.spec.ts | 4 +- .../subagent/tests/timing-projection.spec.ts | 8 +- packages/subagent/tool-subagent/src/index.ts | 14 +- .../src/model-selection-state.ts | 14 +- .../subagent/tool-subagent/tests/harness.ts | 7 +- .../tests/model-selection-settings.spec.ts | 41 +- .../tool-subagent/tests/tool-subagent.spec.ts | 20 +- packages/terminal/terminal-bash/src/index.ts | 9 +- .../terminal-bash/tests/index.spec.ts | 2 +- .../tests/agent-loop-testkit.spec.ts | 2 +- packages/todo/tool-todo/src/index.ts | 6 +- pnpm-lock.yaml | 7 + 136 files changed, 1253 insertions(+), 1029 deletions(-) diff --git a/packages/api/session-controller/README.i18n.yaml b/packages/api/session-controller/README.i18n.yaml index 94dd4e518f..5dc133470c 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: d26a58a8f2406cd5 - zh: 82135eceb9adb628 + en: 5209c0658a6f643b + zh: bb2f9575215bac7a /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 ca922877f8..94daea954c 100644 --- a/packages/api/session-controller/README.md +++ b/packages/api/session-controller/README.md @@ -27,6 +27,8 @@ Desktop analytics follows the [product collection policy](../../client/product-a ## Use this package +The controller registers the `agentPreset` projection independently of preset execution support. Its value is the recorded preset id or null; key presence does not indicate that `agentPresets` is composed. + History pages and follow opening snapshots carry one `{ type: 'event', event: SessionWireEvent }` record per durable Session event. The Client retains each accepted record as one durable `SessionEventLikeEntry`; Assistant token boundaries remain inside the compact stream on `assistant/message` or `assistant/attempt`. Tool arguments, result content, failures, and `tool/result.data.meta` pass through unchanged; the controller does not resolve a Tool definition, run a presenter, or attach UI data. The Client journal validates current Session event envelopes before publishing follow snapshots, live entries, or history pages. It reuses the browser-safe Session validators for required surface markers, exact replacement endpoints, earlier unique source seqs, embedded Assistant provider metadata, request-header omissions, and tool-error consistency. Invalid records fail without field stripping or normalization; range membership and source existence remain durable-log checks on the Host. diff --git a/packages/api/session-controller/README.zh.md b/packages/api/session-controller/README.zh.md index 585c4e0363..79773c94fb 100644 --- a/packages/api/session-controller/README.zh.md +++ b/packages/api/session-controller/README.zh.md @@ -27,6 +27,8 @@ kind: "package-reference" ## 使用本包 +控制器独立于预设执行能力注册 `agentPreset` 投影。其值为已记录的预设 id 或 null;键存在不代表组合中包含 `agentPresets`。 + 历史页与 follow opening 快照为每个持久 Session 事件携带一条 `{ type: 'event', event: SessionWireEvent }` record。Client 把每条已接受 record 保留为一个持久 `SessionEventLikeEntry`;Assistant token 边界保留在 `assistant/message` 或 `assistant/attempt` 的紧凑流内。工具参数、结果内容、失败信息和 `tool/result.data.meta` 原样通过;控制器不解析工具定义、不运行展示转换器,也不附加 UI 数据。 Client journal 在发布 follow 快照、live entry 或历史页之前验证当前 Session 事件 envelope。它复用浏览器安全的 Session validator,检查必需的 surface marker、精确的 replacement endpoint、更早且唯一的 source seq、内嵌 Assistant 提供方元数据、request header 可选字段的省略规则以及工具错误一致性。无效 record 直接失败,不删除字段或归一化;范围成员与来源存在性仍由 Host 的持久日志检查。 diff --git a/packages/api/session-controller/src/agent.ts b/packages/api/session-controller/src/agent.ts index 3f0fa77944..41f56e4f14 100644 --- a/packages/api/session-controller/src/agent.ts +++ b/packages/api/session-controller/src/agent.ts @@ -7,14 +7,16 @@ import type { Agent, AgentOptions, AgentSetup, ModelSelection as AgentModelSelection, ModelSelectionRef, } from '@deepseek-ai/dsh-agent' import type {} from '@deepseek-ai/dsh-agent-default-model' -import type {} from '@deepseek-ai/dsh-agent-preset-registry' +import { agentPresetProjectionDefinition } from '@deepseek-ai/dsh-agent-preset-registry' import { ReasoningEffortId } from '@deepseek-ai/dsh-llm' import type { Session, SessionId } from '@deepseek-ai/dsh-session' import type { SessionInspection } from '@deepseek-ai/dsh-session-persistence' import { SessionQueryError, type SessionObservation } from '@deepseek-ai/dsh-session-query' import { RemoteError } from '@deepseek-ai/dsh-typert-protocol' import type {} from '@deepseek-ai/dsh-typert-registry' -import type { ModelSelection } from './types.ts' +import type { ModelSelection, ModelSelectionProjectionState } from './types.ts' +import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection' +import { modelSelectionProjection } from './model-selection-projection.ts' /** Cold Session identity absent from persistence. */ export class ApiSessionNotFound extends Error {} @@ -138,13 +140,24 @@ export async function inspectApiSession( /** Owns every operation that may create, resume, or configure a Web Agent. */ export class ApiSessionAgentController { + private modelSelection!: ProjectionHandle + private agentPreset!: ProjectionHandle private readonly resumes = new Map>() private readonly creations = new Map>() private readonly selections = new WeakMap() private readonly imageAdmissionChains = new WeakMap>() /** @param ctx - Host context carrying Agent, model, persistence, and Typert services. */ - constructor(private readonly ctx: Context) { + constructor(private readonly ctx: Context) {} + + /** + * Acquire projection readers before exposing dependent operations. + * @returns when all required registrations are ready. + */ + async initialize(): Promise { + const ctx = this.ctx + this.modelSelection = await ctx.sessionProjections.register(modelSelectionProjection) + this.agentPreset = await ctx.sessionProjections.register(agentPresetProjectionDefinition) ctx.typert.lookups.configure('agent', async (sessionId: SessionId) => { const found = await this.resolveAgent(sessionId) if ('error' in found) throw found.error @@ -283,10 +296,7 @@ export class ApiSessionAgentController { selectionFor(agent: Agent): InstalledSelection { const installed = this.selections.get(agent) if (installed !== undefined) return installed - const projectionState = this.ctx.sessionProjections.stateOf(agent.session, 'modelSelection') - if (projectionState === undefined) { - throw new Error('api-session: required modelSelection projection is not registered') - } + const projectionState = this.modelSelection.stateOf(agent.session) let picked = projectionState.pending === null ? undefined : agentModelSelection(projectionState.pending) @@ -355,10 +365,10 @@ export class ApiSessionAgentController { /** * Read the current Agent preset from the Session projection. * @param session - live Session whose projection state is available. - * @returns the current preset, or undefined when the capability is absent. + * @returns the current preset, or undefined when the Session has no recorded preset. */ presetForSession(session: Session): string | undefined { - return this.ctx.sessionProjections.stateOf(session, 'agentPreset') ?? undefined + return this.agentPreset.stateOf(session) ?? undefined } /** @@ -506,7 +516,7 @@ export class ApiSessionAgentController { /** * Read the current Agent preset from an all-projections observation. * @param observation - exact Session observation carrying its projection snapshot. - * @returns the current preset, or undefined when the capability is absent. + * @returns the current preset, or undefined when the Session has no recorded preset. */ presetForObservation(observation: SessionObservation): string | undefined { if (observation.projections === undefined) { diff --git a/packages/api/session-controller/src/index.ts b/packages/api/session-controller/src/index.ts index 0ba5bf8ff0..fbb4234fdf 100644 --- a/packages/api/session-controller/src/index.ts +++ b/packages/api/session-controller/src/index.ts @@ -4,7 +4,7 @@ import { hostname } from 'node:os' import { resolve } from 'node:path' import type { Agent } from '@deepseek-ai/dsh-agent' import type {} from '@deepseek-ai/dsh-fs' -import { Context } from '@deepseek-ai/cordis' +import { Context, Service } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import { errorChain, ReasoningEffortId } from '@deepseek-ai/dsh-llm' import type {} from '@deepseek-ai/dsh-client-file-upload' @@ -25,7 +25,6 @@ import { SessionHistoryController } from './history.ts' import { SessionFileReferences } from './file-references.ts' import { ApiSessionList } from './list.ts' import { buildModelCatalog, hasProviderApiKey } from './catalog.ts' -import { installModelSelectionProjection } from './model-selection-projection.ts' import { SessionSkillCatalog } from './skill-catalog.ts' import { SessionMediaReferences } from './media-references.ts' import { ArchivedSessionGate } from './archived-session-gate.ts' @@ -122,16 +121,17 @@ export class SessionController extends TypertRemoteService { listWorkSliceMs: z.natural().min(1).default(16), }) - private readonly agents: ApiSessionAgentController - private readonly commands: SessionCommandController - private readonly controlState: SessionControlController - private readonly history: SessionHistoryController - private readonly listState: ApiSessionList + private agents!: ApiSessionAgentController + private commands!: SessionCommandController + private controlState!: SessionControlController + private history!: SessionHistoryController + private listState!: ApiSessionList private readonly openPath: (path: string, signal: AbortSignal) => Promise private readonly fileApplications: typeof nativeFileApplications private readonly openFileApplication: typeof openNativeFileApplication private readonly revealPath: (path: string, signal: AbortSignal) => Promise private readonly canOpenPath: () => boolean + private readonly listWorkSliceMs: number private readonly promotions = new Set>() /** @@ -141,9 +141,19 @@ export class SessionController extends TypertRemoteService { */ constructor(ctx: Context, config: Config, internals: SessionControllerInternals = {}) { super(ctx, 'sessionController', { namespace: 'session' }) - const resolved = SessionController.Config(config) - installModelSelectionProjection(ctx) + this.listWorkSliceMs = SessionController.Config(config).listWorkSliceMs + this.fileApplications = internals.fileApplications ?? nativeFileApplications + this.openFileApplication = internals.openFileApplication ?? openNativeFileApplication + this.openPath = internals.openPath ?? openNativeAssociatedPath + this.revealPath = internals.revealPath ?? revealNativePath + this.canOpenPath = internals.canOpenPath + ?? (() => config.nativeOpen ?? (internals.openPath !== undefined || canOpenNativePath())) + } + + protected async [Service.init](): Promise { + const ctx = this.ctx this.agents = new ApiSessionAgentController(ctx) + await this.agents.initialize() this.commands = new SessionCommandController(ctx, this.agents, process.cwd()) ctx.effect(() => ctx.fileUploads.registerAgentResolver(async (sessionId) => { const result = await this.agents.resolveAgent(sessionId) @@ -157,13 +167,8 @@ export class SessionController extends TypertRemoteService { await Promise.allSettled([...this.promotions]) }, 'session-controller.promotions') this.history = new SessionHistoryController(ctx, (observation) => { this.promote(observation) }) - this.listState = new ApiSessionList(ctx, resolved.listWorkSliceMs) - this.fileApplications = internals.fileApplications ?? nativeFileApplications - this.openFileApplication = internals.openFileApplication ?? openNativeFileApplication - this.openPath = internals.openPath ?? openNativeAssociatedPath - this.revealPath = internals.revealPath ?? revealNativePath - this.canOpenPath = internals.canOpenPath - ?? (() => config.nativeOpen ?? (internals.openPath !== undefined || canOpenNativePath())) + this.listState = new ApiSessionList(ctx, this.listWorkSliceMs) + await this.listState.initialize() ctx.plugin(SessionFileReferences) ctx.plugin(SessionMediaReferences) ctx.plugin(SessionSkillCatalog) diff --git a/packages/api/session-controller/src/list.ts b/packages/api/session-controller/src/list.ts index d7ef1ad43c..f109fee5e0 100644 --- a/packages/api/session-controller/src/list.ts +++ b/packages/api/session-controller/src/list.ts @@ -80,8 +80,14 @@ export class ApiSessionList { * @param ctx - Host context carrying Session, query, persistence, and projection services. * @param workSliceMs - Resolved positive integral list-work budget in milliseconds. */ - constructor(private readonly ctx: Context, private readonly workSliceMs: number) { - ctx.sessionProjections.register<'sessionListMetadata', SessionListMetadata>({ + constructor(private readonly ctx: Context, private readonly workSliceMs: number) {} + + /** + * Acquire projection readers before exposing dependent operations. + * @returns when all required registrations are ready. + */ + async initialize(): Promise { + await this.ctx.sessionProjections.register<'sessionListMetadata', SessionListMetadata>({ key: 'sessionListMetadata', stateSchema: sessionListMetadataSchema, init: () => ({ blank: true, lastPromptAt: null }), @@ -89,18 +95,16 @@ export class ApiSessionList { wire: { viewSchema: sessionListMetadataSchema, view: state => state }, stateVersion: 1, }) - ctx.inject(['attachments'], (attachmentCtx) => { - ctx.sessionProjections.register<'imageLimits', null>({ - key: 'imageLimits', - stateSchema: z.null(), - init: () => null, - apply: state => state, - wire: { - viewSchema: imageLimitsSchema, - view: () => attachmentCtx.attachments.imageLimits, - }, - stateVersion: 1, - }) + await this.ctx.sessionProjections.register<'imageLimits', null>({ + key: 'imageLimits', + stateSchema: z.null(), + init: () => null, + apply: state => state, + wire: { + viewSchema: imageLimitsSchema, + view: () => this.ctx.attachments.imageLimits, + }, + stateVersion: 1, }) } diff --git a/packages/api/session-controller/src/model-selection-projection.ts b/packages/api/session-controller/src/model-selection-projection.ts index 47eddde181..071cf5e685 100644 --- a/packages/api/session-controller/src/model-selection-projection.ts +++ b/packages/api/session-controller/src/model-selection-projection.ts @@ -1,6 +1,5 @@ /** Durable model-selection intent and request-use projection. */ -import type { Context } from '@deepseek-ai/cordis' import type { SessionEvent } from '@deepseek-ai/dsh-session' import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection' import { z } from 'zod' @@ -55,7 +54,8 @@ function applyModelSelectionProjection( : { lastUsed, pending } } -const modelSelectionProjection = { +/** Last-used and pending model selections shared by Session controller readers. */ +export const modelSelectionProjection = { key: 'modelSelection', stateSchema: modelSelectionProjectionStateSchema, init: () => ({ lastUsed: null, pending: null }), @@ -73,11 +73,3 @@ function sameSelection(left: ModelSelection | null, right: ModelSelection | null && left.model === right.model && left.reasoningEffort === right.reasoningEffort) } - -/** - * Register the durable model-selection projection when the registry is present. - * @param ctx - Session Controller context. - */ -export function installModelSelectionProjection(ctx: Context): void { - ctx.sessionProjections.register(modelSelectionProjection) -} diff --git a/packages/api/session-controller/tests/agent.host.spec.ts b/packages/api/session-controller/tests/agent.host.spec.ts index 4756ae45b0..10a062d87e 100644 --- a/packages/api/session-controller/tests/agent.host.spec.ts +++ b/packages/api/session-controller/tests/agent.host.spec.ts @@ -18,7 +18,6 @@ import { ApiSessionSubagentOwnership, inspectApiSession, } from '../src/agent.ts' -import { installModelSelectionProjection } from '../src/model-selection-projection.ts' import { installSessionReadTestServices, testSessionPersistence } from './test-remote.ts' const roots: Context[] = [] @@ -38,13 +37,14 @@ async function harness(): Promise<{ ctx: Context; agents: ApiSessionAgentControl await ctx.plugin(SessionStore) await ctx.plugin(AgentRegistry) installSessionReadTestServices(ctx) - ctx.sessionProjections.register(agentPresetProjectionDefinition) - installModelSelectionProjection(ctx) + await ctx.sessionProjections.register(agentPresetProjectionDefinition) ctx.provide('agentDefaultModel', { currentSelection: () => ({ provider: 'fixture', model: 'fixture-model' }), saveSelection: () => Promise.resolve(), } as never) - return { ctx, agents: new ApiSessionAgentController(ctx) } + const agents = new ApiSessionAgentController(ctx) + await agents.initialize() + return { ctx, agents } } function header(id: string, cwd: string | null = '/workspace'): SessionHeader { @@ -288,14 +288,6 @@ describe('ApiSession Agent lookup and recovery', () => { }) describe('ApiSession model selection', () => { - it('requires the model-selection projection', async () => { - const { ctx, agents } = await harness() - const live = agent(ctx, header('missing-model-projection')) - vi.spyOn(ctx.sessionProjections, 'stateOf').mockReturnValue(undefined) - - expect(() => agents.selectionFor(live)).toThrow('required modelSelection projection') - }) - it('reads a reasoning-free request and consumes only the exact pending selection', async () => { const { ctx, agents } = await harness() const logged = agent(ctx, header('logged-model')) diff --git a/packages/api/session-controller/tests/commands-queue-attachment.host.spec.ts b/packages/api/session-controller/tests/commands-queue-attachment.host.spec.ts index 5a4d946bf0..ff5f980250 100644 --- a/packages/api/session-controller/tests/commands-queue-attachment.host.spec.ts +++ b/packages/api/session-controller/tests/commands-queue-attachment.host.spec.ts @@ -38,7 +38,7 @@ async function commandHarness( await ctx.plugin(SessionStore) await ctx.plugin(AgentRegistry) installSessionReadTestServices(ctx) - ctx.sessionProjections.register(subagentIdentityProjectionDefinition) + await ctx.sessionProjections.register(subagentIdentityProjectionDefinition) const sessionId = SessionId('commands-session') const ancestor = Session.create(SessionId('ancestor')) ancestor.append('subagent/descriptor', snapshotSubagentDescriptor({ diff --git a/packages/api/session-controller/tests/controller.host.spec.ts b/packages/api/session-controller/tests/controller.host.spec.ts index 43d1394851..45d3a523ec 100644 --- a/packages/api/session-controller/tests/controller.host.spec.ts +++ b/packages/api/session-controller/tests/controller.host.spec.ts @@ -52,7 +52,7 @@ describe('SessionController facade', () => { bindPrompt: () => ({ commit: () => {}, [Symbol.dispose]: () => {} }), retirePrompt: () => {}, } as never) - const controller = createSessionTestController(ctx, defaults) + const controller = await createSessionTestController(ctx, defaults) const status = vi.fn() const failure = vi.fn() const activity = vi.fn() @@ -157,7 +157,7 @@ describe('SessionController facade', () => { events: [], }), }) as never) - const controller = createSessionTestController(ctx, defaults) + const controller = await createSessionTestController(ctx, defaults) const agents = (controller as unknown as { agents: ApiSessionAgentController }).agents const apiError = vi.fn() ctx.on('api-session/error', apiError) @@ -213,7 +213,7 @@ describe('SessionController facade', () => { events: [], }), }) as never) - const controller = createSessionTestController(ctx, defaults) + const controller = await createSessionTestController(ctx, defaults) const agents = (controller as unknown as { agents: ApiSessionAgentController }).agents const started = Promise.withResolvers() const release = Promise.withResolvers() diff --git a/packages/api/session-controller/tests/list-scheduling.host.spec.ts b/packages/api/session-controller/tests/list-scheduling.host.spec.ts index eefea76713..eeae34ff03 100644 --- a/packages/api/session-controller/tests/list-scheduling.host.spec.ts +++ b/packages/api/session-controller/tests/list-scheduling.host.spec.ts @@ -9,6 +9,7 @@ import { resolve } from 'node:path' import { performance } from 'node:perf_hooks' import { scheduler } from 'node:timers/promises' import SessionController from '../src/index.ts' +import { createSessionTestController, TEST_IMAGE_LIMITS } from './test-remote.ts' import { afterEach, describe, expect, it, vi } from 'vitest' import { ApiSessionList } from '../src/list.ts' @@ -47,7 +48,9 @@ async function harness(rows: readonly Row[], summaryMs = 8, workSliceMs = 8) { await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) + ctx.provide('attachments', { imageLimits: TEST_IMAGE_LIMITS } as never) const list = new ApiSessionList(ctx, workSliceMs) + await list.initialize() const clock = { now: 0 } const prepared = new Map() const agents = new Map() @@ -185,7 +188,10 @@ describe('Session-list work slices', () => { h.ctx.provide('fileUploads', { registerAgentResolver: () => dispose } as never) h.ctx.provide('agentDefaultModel', { currentSelection: () => ({ provider: 'test', model: 'test' }) } as never) h.ctx.provide('workspaceRegistry', { list: () => [], archivedSessionIds: [] } as never) - const controller = new SessionController(h.ctx, { nativeOpen: false, listWorkSliceMs: 4 }, { canOpenPath: () => false }) + const controller = await createSessionTestController(h.ctx, { + nativeOpen: false, listWorkSliceMs: 4, canOpenPath: () => false, + defaultModelSelection: () => ({ provider: 'test', model: 'test' }), cwd: '/tmp', + }) const yieldWork = vi.spyOn(scheduler, 'yield') const result = await controller.list({}, new AbortController().signal) expect(result.items).toHaveLength(3) diff --git a/packages/api/session-controller/tests/projection-discovery.host.spec.ts b/packages/api/session-controller/tests/projection-discovery.host.spec.ts index 0a14c90f0a..ce51110107 100644 --- a/packages/api/session-controller/tests/projection-discovery.host.spec.ts +++ b/packages/api/session-controller/tests/projection-discovery.host.spec.ts @@ -49,8 +49,8 @@ async function harness(withPersistence = true) { await ctx.plugin(SessionStore) await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(subagentCatalogProjectionDefinition) - ctx.sessionProjections.register(titleProjectionDefinition) + await ctx.sessionProjections.register(subagentCatalogProjectionDefinition) + await ctx.sessionProjections.register(titleProjectionDefinition) if (withPersistence) { await ctx.plugin(JsonlSessionPersistence, { root: join(root, 'logs'), compression: 'none' }) const list = vi.spyOn(ctx.sessionPersistence, 'list').mockRejectedValue(new Error('unexpected corpus listing')) @@ -58,7 +58,7 @@ async function harness(withPersistence = true) { expect(list).not.toHaveBeenCalled() }) } - const controller = createSessionTestController(ctx, { + const controller = await createSessionTestController(ctx, { defaultModelSelection: () => ({ provider: 'test', model: 'test' }), cwd: root, }) 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 a6b7a033dd..dd1a68f610 100644 --- a/packages/api/session-controller/tests/session-cold.host.spec.ts +++ b/packages/api/session-controller/tests/session-cold.host.spec.ts @@ -97,7 +97,7 @@ describe('sessions.list cold merge', () => { const stat = vi.fn() const open = vi.fn() ctx.provide('sessionPersistence', { list, stat, open } as never) - const remote = createSessionTestRemote(ctx, { + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp', }) @@ -147,7 +147,7 @@ describe('sessions.list cold merge', () => { ? { asOfSeq: 2, values: { title: 'Cached predecessor title' } } : undefined, } as never) - const remote = createSessionTestRemote(ctx, { + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp', }) @@ -209,7 +209,7 @@ describe('sessions.list cold merge', () => { }, cachedPredecessorTitle: () => undefined, } as never) - const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) const response = await remote.list(request({})) expect(response.ok).toBe(true) @@ -247,7 +247,7 @@ describe('attached updatedAt tracks human prompts', () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(AgentRegistry) - const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) await new Promise(resolve => setTimeout(resolve, 0)) // Old work, resumed just now: the log tail would report the pickup. @@ -306,7 +306,7 @@ describe('cold history recovery view', () => { list: () => Promise.resolve([structuredClone(meta)]), inspect: () => Promise.resolve({ meta: structuredClone(meta), events: structuredClone(events) }), }) - const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) const history = await remote.page({ address: { kind: 'session', sessionId }, @@ -365,7 +365,7 @@ describe('Remote Agent and Session lookup policy', () => { }), }) const resume = vi.spyOn(ctx.agents, 'resume') - const remote = createSessionTestRemote(ctx, { + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp', }) @@ -391,7 +391,7 @@ describe('Remote Agent and Session lookup policy', () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(AgentRegistry) - const remote = createSessionTestRemote(ctx, { + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp', }) @@ -427,7 +427,7 @@ describe('Remote Agent and Session lookup policy', () => { }) const defaultAgentLookup = ctx.typert.lookups.get('agent') const defaultSessionLookup = ctx.typert.lookups.get('session') - createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) + await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) await vi.waitFor(() => { expect(ctx.typert.lookups.get('agent')).not.toBe(defaultAgentLookup) expect(ctx.typert.lookups.get('session')).not.toBe(defaultSessionLookup) @@ -469,7 +469,7 @@ describe('Remote Agent and Session lookup policy', () => { const resume = vi.spyOn(ctx.agents, 'resume') const defaultAgentLookup = ctx.typert.lookups.get('agent') const defaultSessionLookup = ctx.typert.lookups.get('session') - createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) + await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) await vi.waitFor(() => { expect(ctx.typert.lookups.get('agent')).not.toBe(defaultAgentLookup) expect(ctx.typert.lookups.get('session')).not.toBe(defaultSessionLookup) @@ -511,7 +511,7 @@ describe('Remote Agent and Session lookup policy', () => { return { agent: published, dispose: () => Promise.resolve() } }) const defaultLookup = ctx.typert.lookups.get('agent') - createSessionTestRemote(ctx, { + await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp', }) @@ -562,8 +562,8 @@ describe('subagent ownership fence', () => { inspect, }) const resume = vi.spyOn(ctx.agents, 'resume') - const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) - ctx.sessionProjections.register(subagentIdentityProjectionDefinition) + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) + await ctx.sessionProjections.register(subagentIdentityProjectionDefinition) const history = await new SessionHistoryController( ctx, @@ -629,7 +629,7 @@ describe('subagent ownership fence', () => { // answering `agent-busy`. const resume = vi.spyOn(ctx.agents, 'resume') .mockRejectedValue(new Error('registry unavailable in this bench')) - const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) const prompt = await remote.prompt(promptRequest({ sessionId, @@ -669,7 +669,7 @@ describe('subagent ownership fence', () => { }) const startingChild = { id: startingSession.id, session: startingSession, status: 'idle', ctx } as Agent ctx.agents.enter(startingChild, parent) - const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) const stopped = await remote.cancel(request({ sessionId: originChild.id })) expect(stopped.ok).toBe(false) @@ -719,7 +719,7 @@ describe('subagent ownership fence', () => { id: session.id, session, inbox: inboxFor(), status: 'idle', ctx, followup, } as unknown as Agent await ctx.agents.register(agent) - const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) const response = await remote.prompt(promptRequest({ sessionId: agent.id, @@ -740,7 +740,7 @@ describe('subagent ownership fence', () => { id: session.id, session, inbox: inboxFor(), status: 'idle', ctx, followup, } as unknown as Agent await ctx.agents.register(agent) - const remote = createSessionTestRemote(ctx, { + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp', }) @@ -805,7 +805,7 @@ describe('degenerate composition (no persistence, no factory)', () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(AgentRegistry) - const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) const listed = await remote.list(request({})) expect(listed.ok).toBe(true) @@ -833,7 +833,7 @@ describe('degenerate composition (no persistence, no factory)', () => { stat, inspect, }) - const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) const response = await remote.page({ address: { kind: 'session', sessionId: sid('session-missing') }, @@ -883,7 +883,7 @@ describe('sessions.prompt synchronous rejection', () => { provider: 'p', id: 'm', name: 'Model', inputModalities: ['text', 'image'], }), } as never) - const remote = createSessionTestRemote(ctx, { + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp', }) @@ -960,7 +960,7 @@ describe('sessions.prompt synchronous rejection', () => { followup: () => { throw new Error('agent "session-throwing" lifecycle disposed') }, steer: () => { throw new Error('agent "session-throwing" lifecycle disposed') }, } as unknown as Agent) - const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) for (const mode of ['queue', 'steer'] as const) { const response = await remote.prompt(promptRequest({ @@ -1001,7 +1001,7 @@ describe('sessions.prompt synchronous rejection', () => { await ctx.agents.register(child) throw new Error('session id already published') }) - const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) const selection = await remote.selectModel(request({ sessionId, provider: 'p', model: 'm' })) expect(selection.ok).toBe(false) diff --git a/packages/api/session-controller/tests/session-fork.host.spec.ts b/packages/api/session-controller/tests/session-fork.host.spec.ts index e6ba8321ce..87fbd2456e 100644 --- a/packages/api/session-controller/tests/session-fork.host.spec.ts +++ b/packages/api/session-controller/tests/session-fork.host.spec.ts @@ -90,7 +90,7 @@ async function liveAgent( return session } -const remote = (ctx: Context) => createSessionTestRemote(ctx, { +const remote = async (ctx: Context) => await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'default-provider', model: 'default-model' }), cwd: '/tmp', }) @@ -120,7 +120,7 @@ describe('sessions.fork', () => { }) as never) const open = vi.spyOn(ctx.sessionPersistence, 'open') - const response = await remote(ctx).fork({ + const response = await (await remote(ctx)).fork({ sessionId: source.id, ...allowMigration === undefined ? {} : { allowMigration }, }) @@ -150,7 +150,7 @@ describe('sessions.fork', () => { const observe = vi.spyOn(ctx.sessionQuery, 'observeSession') const create = vi.spyOn(ctx.agents, 'create') - await expect(remote(ctx).fork({ sessionId: sourceId, allowMigration: false })).resolves.toMatchObject({ + await expect((await remote(ctx)).fork({ sessionId: sourceId, allowMigration: false })).resolves.toMatchObject({ ok: false, error: { code: 'session/migration-required', details: { sessionId: sourceId } }, }) expect(stat).toHaveBeenCalledExactlyOnceWith(sourceId) @@ -171,7 +171,7 @@ describe('sessions.fork', () => { const stat = vi.fn(() => Promise.reject(new Error('unexpected metadata read'))) ctx.provide('sessionPersistence', testSessionPersistence(ctx, { stat }) as never) - const result = await remote(ctx).fork({ sessionId: source.id, allowMigration: false }) + const result = await (await remote(ctx)).fork({ sessionId: source.id, allowMigration: false }) if (!result.ok) throw result.error expect(stat).not.toHaveBeenCalled() @@ -199,7 +199,7 @@ describe('sessions.fork', () => { return finish.promise }) ctx.provide('sessionPersistence', testSessionPersistence(ctx, { stat }) as never) - const operation = remote(ctx).fork({ sessionId: sourceId, allowMigration: false }) + const operation = (await remote(ctx)).fork({ sessionId: sourceId, allowMigration: false }) try { await entered.promise const live = ctx.sessions.create(sourceId, { meta: source.header }) @@ -229,7 +229,7 @@ describe('sessions.fork', () => { const observe = vi.spyOn(ctx.sessionQuery, 'observeSession') const create = vi.spyOn(ctx.agents, 'create') - await expect(remote(ctx).fork({ sessionId: sourceId, allowMigration: false })).resolves.toMatchObject({ + await expect((await remote(ctx)).fork({ sessionId: sourceId, allowMigration: false })).resolves.toMatchObject({ ok: false, error: { code: 'gateway/internal', message: 'source metadata unavailable' }, }) @@ -262,9 +262,9 @@ describe('sessions.fork', () => { expect(compacted.at(-1)).toMatchObject({ type: 'compaction/end', data: { turn: null } }) source.inbox.append('next-turn', message('queued after compaction')) - const response = await createSessionTestRemote(ctx, { + const response = await (await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'mock', model: 'mock' }), cwd: '/tmp', - }).fork({ sessionId: source.id }) + })).fork({ sessionId: source.id }) if (!response.ok) throw response.error const child = ctx.agents.get(response.value.sessionId)! @@ -304,9 +304,9 @@ describe('sessions.fork', () => { } const original = source.session.snapshotEvents() const atSeq = anchor === 'omitted' ? undefined : boundary - const response = await createSessionTestRemote(ctx, { + const response = await (await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'mock', model: 'mock' }), cwd: '/tmp', - }).fork({ sessionId: source.id, ...(atSeq === undefined ? {} : { atSeq }) }) + })).fork({ sessionId: source.id, ...(atSeq === undefined ? {} : { atSeq }) }) if (!response.ok) throw response.error const child = ctx.agents.get(response.value.sessionId)! const requestCount = adapter.requests.length @@ -327,7 +327,7 @@ describe('sessions.fork', () => { it('cuts exactly at a completed turn and records lineage and cwd', async () => { const ctx = await composed() const source = await liveAgent(ctx, 'session-source', 2) - const response = await remote(ctx).fork(request({ sessionId: source.id, atSeq: 2 })) + const response = await (await remote(ctx)).fork(request({ sessionId: source.id, atSeq: 2 })) expect(response.ok ? null : response.error).toBeNull() if (!response.ok) return const child = ctx.sessions.get(response.value.sessionId) @@ -369,7 +369,7 @@ describe('sessions.fork', () => { root: { header: owner.header, live: true, persisted: false }, }) - const response = await remote(ctx).fork(request({ sessionId: grandchild.id })) + const response = await (await remote(ctx)).fork(request({ sessionId: grandchild.id })) expect(response.ok ? undefined : response.error).toBeUndefined() if (!response.ok) return @@ -424,7 +424,7 @@ describe('sessions.fork', () => { }) as never) const resume = vi.spyOn(ctx.agents, 'resume') - const response = await remote(ctx).fork(request({ sessionId: sourceId })) + const response = await (await remote(ctx)).fork(request({ sessionId: sourceId })) expect(response.ok ? undefined : response.error).toBeUndefined() if (!response.ok) return @@ -441,7 +441,7 @@ describe('sessions.fork', () => { it('uses the latest completed prefix only when atSeq is omitted', async () => { const ctx = await composed() const source = await liveAgent(ctx, 'session-tail', 2, 'open') - const proxy = remote(ctx) + const proxy = await remote(ctx) const omitted = await proxy.fork(request({ sessionId: source.id })) expect(omitted.ok).toBe(true) if (omitted.ok) { @@ -464,7 +464,7 @@ describe('sessions.fork', () => { it('rejects an omitted boundary when no turn has completed', async () => { const ctx = await composed() const source = await liveAgent(ctx, 'session-never-completed', 0, 'open') - const response = await remote(ctx).fork(request({ sessionId: source.id })) + const response = await (await remote(ctx)).fork(request({ sessionId: source.id })) expect(response).toMatchObject({ ok: false, error: { code: 'session/fork-unavailable', details: { sessionId: source.id } }, @@ -481,7 +481,7 @@ describe('sessions.fork', () => { const inherited = source.snapshotEvents() if (openNextTurn) source.append('turn/start', { turn: 2 }) - const response = await remote(ctx).fork({ sessionId: source.id }) + const response = await (await remote(ctx)).fork({ sessionId: source.id }) if (!response.ok) throw response.error const child = ctx.sessions.get(response.value.sessionId)! @@ -496,7 +496,7 @@ describe('sessions.fork', () => { it('rejects invalid fork anchors before reading or creating a Session', async () => { const ctx = await composed() - const proxy = remote(ctx) + const proxy = await remote(ctx) for (const atSeq of [-1, 0.5]) { await expect(proxy.fork(request({ sessionId: sid('missing'), atSeq }))) @@ -510,7 +510,7 @@ describe('sessions.fork', () => { const ctx = await composed() const source = await liveAgent(ctx, 'session-aborted', 1, 'aborted') const anchor = (source.snapshotEvents().at(-1)?.seq ?? 0) - 1 - const response = await remote(ctx).fork(request({ sessionId: source.id, atSeq: anchor })) + const response = await (await remote(ctx)).fork(request({ sessionId: source.id, atSeq: anchor })) expect(response.ok ? undefined : response.error).toBeUndefined() if (!response.ok) return const child = ctx.sessions.get(response.value.sessionId) @@ -527,7 +527,7 @@ describe('sessions.fork', () => { const ctx = await composed() const source = await liveAgent(ctx, 'session-open', 1, 'open') const boundary = source.snapshotEvents().at(-1)?.seq ?? 0 - const response = await remote(ctx).fork(request({ sessionId: source.id, atSeq: boundary })) + const response = await (await remote(ctx)).fork(request({ sessionId: source.id, atSeq: boundary })) expect(response.ok ? undefined : response.error).toBeUndefined() if (!response.ok) return const child = ctx.sessions.get(response.value.sessionId) @@ -548,7 +548,7 @@ describe('sessions.fork', () => { const boundary = source.snapshotEvents().at(-1)?.seq ?? 0 source.append('session/title', { title: 'after boundary', messageSeqs: [], source: { kind: 'fallback' } }) - const response = await remote(ctx).fork(request({ sessionId: source.id, atSeq: boundary })) + const response = await (await remote(ctx)).fork(request({ sessionId: source.id, atSeq: boundary })) expect(response.ok ? undefined : response.error).toBeUndefined() if (!response.ok) return @@ -604,7 +604,7 @@ describe('sessions.fork', () => { }) as never) const resume = vi.spyOn(ctx.agents, 'resume') - const response = await remote(ctx).fork(request({ sessionId: sourceId, atSeq: 4 })) + const response = await (await remote(ctx)).fork(request({ sessionId: sourceId, atSeq: 4 })) expect(response.ok ? undefined : response.error).toBeUndefined() if (!response.ok) return @@ -656,7 +656,7 @@ describe('sessions.fork', () => { reason: 'change', }) source.append('turn/end', { turn: 2, reason: { kind: 'completed' } }) - const response = await remote(ctx).fork(request({ sessionId: source.id, atSeq: boundary.seq })) + const response = await (await remote(ctx)).fork(request({ sessionId: source.id, atSeq: boundary.seq })) expect(response.ok ? undefined : response.error).toBeUndefined() if (!response.ok) return const child = ctx.agents.get(response.value.sessionId) diff --git a/packages/api/session-controller/tests/session-history-journal.host.spec.ts b/packages/api/session-controller/tests/session-history-journal.host.spec.ts index 86ed42a678..bf0c163914 100644 --- a/packages/api/session-controller/tests/session-history-journal.host.spec.ts +++ b/packages/api/session-controller/tests/session-history-journal.host.spec.ts @@ -719,7 +719,7 @@ describe('Session history raw journal', () => { it('serves raw call and result entries without parsing tool arguments', async () => { const { ctx } = await harness() - const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) const session = ctx.sessions.create(undefined, { meta: { cwd: '/workspace' } }) const start = session.append('turn/start', { turn: 1 }) const call = session.append('tool/call', { @@ -760,7 +760,7 @@ describe('Session history raw journal', () => { ])('paginates $name in one contiguous Turn window', async ({ replies, beforeMessage, firstTurn, steering, minMessages = 50 }) => { const { ctx } = await harness() onTestFinished(() => ctx.fiber.dispose()) - const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) const session = ctx.sessions.create(undefined, { meta: { cwd: '/workspace' } }) const starts: SessionEvent[] = [] const messages: SessionEvent[][] = [] @@ -818,7 +818,7 @@ describe('Session history raw journal', () => { it('counts only append-origin messages toward maxMessages and keeps each compaction summary with its replacement', async () => { const { ctx } = await harness() - const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) const session = ctx.sessions.create(undefined, { meta: { cwd: '/workspace' } }) session.append('turn/start', { turn: 1 }) const first = appendUserText(session, 'first prompt') @@ -872,7 +872,7 @@ describe('Session history raw journal', () => { it('paginates a message with a large embedded stream without expanding physical records', async () => { const { ctx } = await harness() - const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) const session = ctx.sessions.create(undefined, { meta: { cwd: '/workspace' } }) session.append('turn/start', { turn: 1 }) session.append('step/start', { turn: 1, step: 1 }) @@ -910,7 +910,7 @@ describe('Session history raw journal', () => { it('keeps an earlier declared source on the same message-aligned page', async () => { const { ctx } = await harness() - const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) const session = ctx.sessions.create(undefined, { meta: { cwd: '/workspace' } }) const source = session.append('request/context', { provider: 'p', model: 'm' }) const laterSource = session.append('request/context', { provider: 'p', model: 'm' }) @@ -931,7 +931,7 @@ describe('Session history raw journal', () => { it('keeps compact reasoning and tool-call runs nested in one attempt event', async () => { const { ctx } = await harness() - const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) const session = ctx.sessions.create(undefined, { meta: { cwd: '/workspace' } }) const callId = ToolCallId('packed-call') const attempt = session.append('assistant/attempt', { diff --git a/packages/api/session-controller/tests/session-list-blank.host.spec.ts b/packages/api/session-controller/tests/session-list-blank.host.spec.ts index fce236d3e3..7ea81ea4bc 100644 --- a/packages/api/session-controller/tests/session-list-blank.host.spec.ts +++ b/packages/api/session-controller/tests/session-list-blank.host.spec.ts @@ -25,7 +25,7 @@ async function harness(): Promise<{ ctx: Context; remote: TestSessionRemote; att await ctx.plugin(AgentRegistry) return { ctx, - remote: createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }), + remote: await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }), attach: async (session) => { await ctx.agents.register({ id: session.id, session, status: 'idle', ctx } as Agent) }, diff --git a/packages/api/session-controller/tests/session-models.host.spec.ts b/packages/api/session-controller/tests/session-models.host.spec.ts index e852460e93..6af6983f70 100644 --- a/packages/api/session-controller/tests/session-models.host.spec.ts +++ b/packages/api/session-controller/tests/session-models.host.spec.ts @@ -183,7 +183,7 @@ describe('Web session model selection', () => { ctx.provide('attachments', Object.setPrototypeOf(attachments, AttachmentStore.prototype) as never) const followup = vi.fn() Object.assign(agent, { followup }) - const remote = createSessionTestRemote(ctx, { + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), cwd: '/tmp', }) @@ -251,7 +251,7 @@ describe('Web session model selection', () => { const steer = vi.fn() const followup = vi.fn() Object.assign(agent, { steer, followup }) - const remote = createSessionTestRemote(ctx, { + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), cwd: '/tmp', }) @@ -280,7 +280,7 @@ describe('Web session model selection', () => { it('allows a text-only selection while durable or pending images remain available for later models', async () => { const { ctx, agent, sessionId } = await harness() registerTextOnly(ctx) - const remote = createSessionTestRemote(ctx, { + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), cwd: '/tmp', }) @@ -318,7 +318,7 @@ describe('Web session model selection', () => { } const readImage = vi.fn(() => Promise.resolve({ ref, data: Uint8Array.of(1, 2) })) ctx.provide('attachments', { readImage } as never) - const remote = createSessionTestRemote(ctx, { + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), cwd: '/tmp', }) @@ -394,7 +394,7 @@ describe('Web session model selection', () => { value: { providers: { custom: { apiKeyEnv: 'CUSTOM_API_KEY', models: [] } } }, }]) await ctx.agentDefaultModel.saveSelection({ provider: 'removed', model: 'saved' }) - const controller = createSessionTestController(ctx, { + const controller = await createSessionTestController(ctx, { defaultModelSelection: () => ctx.agentDefaultModel.currentSelection(), cwd: '/tmp', }) await controller.initializeDefaultModel() @@ -411,7 +411,7 @@ describe('Web session model selection', () => { model: 'private-preview', reasoningEffort: ReasoningEffortId('max'), }) - const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), cwd: '/tmp' }) + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), cwd: '/tmp' }) const catalog = expectValue(await remote.modelCatalog()) expect(currentSelection(ctx, sessionId)).toEqual({ @@ -460,7 +460,7 @@ describe('Web session model selection', () => { return Promise.reject('string catalog failure') } }('String Failure', [])) - createSessionTestRemote(ctx, { + await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), cwd: '/tmp', }) @@ -488,7 +488,7 @@ describe('Web session model selection', () => { it('rejects unlisted models and switches available models only after the next assembly', async () => { const { ctx, agent, sessionId } = await harness() - const remote = createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), cwd: '/tmp' }) + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), cwd: '/tmp' }) const seed: LlmCallConfig = { provider: 'seed', model: 'seed', temperature: 0.2 } const signal = new AbortController().signal @@ -567,7 +567,7 @@ describe('Web session model selection', () => { it('reads the Agent default live for a session whose log names no selection', async () => { const { ctx, sessionId } = await harness() let stored = { provider: 'deepseek-official', model: 'deepseek-chat' } - createSessionTestRemote(ctx, { + await createSessionTestRemote(ctx, { defaultModelSelection: () => stored, cwd: '/tmp', }) @@ -589,7 +589,7 @@ describe('Web session model selection', () => { model: 'deepseek-chat', }) let stored = { provider: 'deepseek-official', model: 'deepseek-chat' } - createSessionTestRemote(ctx, { + await createSessionTestRemote(ctx, { defaultModelSelection: () => stored, cwd: '/tmp', }) @@ -607,12 +607,14 @@ describe('Web session model selection', () => { reasoningEffort: ReasoningEffortId('high'), adapterDefaults: { reasoningEffort: true }, }) - createSessionTestRemote(ctx, { + await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'duplicate', model: 'same' }), cwd: '/tmp', }) - expect(new ApiSessionAgentController(ctx).selectionFor(agent).current) + const controller = new ApiSessionAgentController(ctx) + await controller.initialize() + expect(controller.selectionFor(agent).current) .toEqual({ provider: 'deepseek-official', model: 'deepseek-chat' }) await ctx.fiber.dispose() }) @@ -627,7 +629,7 @@ describe('Web session model selection', () => { await Promise.allSettled([...operations, ...saves]) await ctx.fiber.dispose() }) - const remote = createSessionTestRemote(ctx, { + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), saveDefaultModelSelection: () => { saves.push(release.promise) @@ -653,7 +655,7 @@ describe('Web session model selection', () => { onTestFinished(() => { warn.mockRestore() }) const saved: unknown[] = [] let reject = false - const remote = createSessionTestRemote(ctx, { + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), saveDefaultModelSelection: (selection) => { saved.push(selection) @@ -692,7 +694,7 @@ describe('Web session model selection', () => { const { ctx, sessionId, agent } = await harness() const followup = vi.fn() Object.assign(agent, { followup }) - const remote = createSessionTestRemote(ctx, { + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), cwd: '/tmp', }) const list = vi.spyOn(ctx.llm, 'listModels') @@ -716,7 +718,7 @@ describe('Web session model selection', () => { }()) try { await expect(ctx.llm.resolveModelInfo('uncatalogued', 'custom-model')).resolves.toMatchObject({ id: 'custom-model' }) - const remote = createSessionTestRemote(ctx, { + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'uncatalogued', model: 'custom-model' }), cwd: '/tmp', }) expect(await remote.selectModel({ sessionId, provider: 'uncatalogued', model: 'custom-model' })) @@ -730,7 +732,7 @@ describe('Web session model selection', () => { const { ctx, sessionId, agent } = await harness() const followup = vi.fn() Object.assign(agent, { followup }) - const remote = createSessionTestRemote(ctx, { + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'deleted-gateway', model: 'deleted-model' }), cwd: '/tmp', }) @@ -752,7 +754,7 @@ describe('Web session model selection', () => { it('admits a removed model without changing its saved selection', async () => { const { ctx, sessionId, agent } = await harness({ provider: 'deepseek-official', model: 'removed-model' }) - const remote = createSessionTestRemote(ctx, { + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), cwd: '/tmp', }) const events = [...agent.session.snapshotEvents()] @@ -768,7 +770,7 @@ describe('Web session model selection', () => { it('serves a session and its catalog when the stored default names a route that is gone', async () => { const { ctx, sessionId } = await harness() - createSessionTestRemote(ctx, { + await createSessionTestRemote(ctx, { // What a Models-page removal leaves behind: the settings document still // names the route the user last picked, and nothing serves it. defaultModelSelection: () => ({ provider: 'deleted-gateway', model: 'deleted-model' }), @@ -816,7 +818,7 @@ describe('Web session model selection', () => { }, AttachmentStore.prototype) as never) const followup = vi.fn() Object.assign(agent, { followup }) - const remote = createSessionTestRemote(ctx, { + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'deepseek-official', model: 'deepseek-chat' }), cwd: '/tmp', }) @@ -887,7 +889,7 @@ it('initializes the account model without reasoning metadata and rejects an empt const dispose = ctx.llm.registerAdapter(['deepseek-account'], new CatalogAdapter('Account', [ { provider: 'deepseek-account', id: 'basic', name: 'Basic' }, ])) - const controller = createSessionTestController(ctx, { + const controller = await createSessionTestController(ctx, { defaultModelSelection: () => ctx.agentDefaultModel.currentSelection(), cwd: '/tmp', }) await controller.initializeDefaultModel() diff --git a/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts b/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts index e8216b5214..a2ab39254a 100644 --- a/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts +++ b/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts @@ -23,7 +23,7 @@ async function context(): Promise { describe('session/openWorkspacePath', () => { it('reports the deployment opener capability independently of a Session', async () => { const ctx = await context() - const remote = createSessionTestRemote(ctx, { + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/default', canOpenPath: () => false, @@ -33,21 +33,21 @@ describe('session/openWorkspacePath', () => { }) it('derives opener availability from config, an injected opener, or the platform probe', async () => { - const configured = createSessionTestRemote(await context(), { + const configured = await createSessionTestRemote(await context(), { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/default', nativeOpen: false, }) await expect(configured.canOpenWorkspacePath()).resolves.toEqual({ ok: true, value: false }) - const injected = createSessionTestRemote(await context(), { + const injected = await createSessionTestRemote(await context(), { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/default', openPath: () => Promise.resolve(), }) await expect(injected.canOpenWorkspacePath()).resolves.toEqual({ ok: true, value: true }) - const detected = createSessionTestRemote(await context(), { + const detected = await createSessionTestRemote(await context(), { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/default', }) @@ -57,7 +57,7 @@ describe('session/openWorkspacePath', () => { it('hands a Client-resolved workspace path to the Host opener unchanged', async () => { const ctx = await context() const openPath = vi.fn((_path: string, _signal: AbortSignal) => Promise.resolve()) - const remote = createSessionTestRemote(ctx, { + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/default', openPath, @@ -73,7 +73,7 @@ describe('session/openWorkspacePath', () => { it('normalizes relative and absolute Host-resolvable paths', async () => { const ctx = await context() const openPath = vi.fn((_path: string, _signal: AbortSignal) => Promise.resolve()) - const remote = createSessionTestRemote(ctx, { + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/default', openPath, @@ -87,7 +87,7 @@ describe('session/openWorkspacePath', () => { it('rejects empty paths before opening anything', async () => { const ctx = await context() const openPath = vi.fn((_path: string, _signal: AbortSignal) => Promise.resolve()) - const remote = createSessionTestRemote(ctx, { + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/default', openPath, @@ -102,7 +102,7 @@ describe('session/openWorkspacePath', () => { const ctx = await context() const openPath = vi.fn((_path: string, _signal: AbortSignal) => Promise.reject(new Error('Command failed: powershell.exe -EncodedCommand private-script-text'))) - const remote = createSessionTestRemote(ctx, { + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/default', openPath, @@ -129,7 +129,7 @@ describe('session/openWorkspacePath', () => { throw new Error('opening stopped') }) .mockRejectedValueOnce('desktop unavailable') - const controller = createSessionTestController(ctx, { + const controller = await createSessionTestController(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/default', openPath, @@ -150,7 +150,7 @@ it('reports Host file-manager metadata and dispatches reveal separately from def const ctx = await context() const revealPath = vi.fn(async (_path: string, _signal: AbortSignal) => {}) const openPath = vi.fn(async (_path: string, _signal: AbortSignal) => {}) - const controller = createSessionTestController(ctx, { + const controller = await createSessionTestController(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/default', openPath, revealPath, }) try { @@ -167,7 +167,7 @@ it('uses the native reveal adapter without a test override and respects unsuppor const reveal = vi.spyOn(nativeCommand, 'revealNativePath').mockResolvedValue(undefined) const manager = vi.spyOn(nativeCommand, 'nativeFileManager').mockReturnValue(null) try { - const controller = createSessionTestController(ctx, { + const controller = await createSessionTestController(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/default', nativeOpen: true, }) expect(controller.workspaceDesktop()).toMatchObject({ available: false, fileManager: null }) @@ -183,7 +183,7 @@ it.each(['open', 'reveal'] as const)('rejects an unmapped remote path before nat onTestFinished(() => { mapping.mockRestore() }) const openPath = vi.fn(async () => {}) const revealPath = vi.fn(async () => {}) - const controller = createSessionTestController(ctx, { + const controller = await createSessionTestController(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/default', openPath, revealPath, }) await expect(controller.openWorkspacePath({ path: '/remote/report.html', ...(action === 'reveal' ? { action } : {}) }, new AbortController().signal)) @@ -197,7 +197,7 @@ it('rejects a filesystem mapping that resolves to another process path', async ( const mapping = vi.spyOn(ctx.fs, 'processPath').mockReturnValue('/different/report.html') onTestFinished(() => { mapping.mockRestore() }) const openPath = vi.fn(async () => {}) - const controller = createSessionTestController(ctx, { + const controller = await createSessionTestController(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/default', openPath, }) await expect(controller.openWorkspacePath({ path: '/report.html' }, new AbortController().signal)) @@ -209,7 +209,7 @@ it('dispatches default-app opening to the association adapter', async () => { const ctx = await context() const open = vi.spyOn(nativeCommand, 'openNativeAssociatedPath').mockResolvedValue(undefined) onTestFinished(() => { open.mockRestore() }) - const controller = createSessionTestController(ctx, { + const controller = await createSessionTestController(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/default', nativeOpen: true, }) const signal = new AbortController().signal @@ -223,7 +223,7 @@ it('uses the same path verification for handler queries and explicit application const applications = [{ id: '/Applications/Music.app', name: 'Music', default: true, icon: null }] const fileApplications = vi.fn(async () => applications) const openFileApplication = vi.fn(async () => {}) - const controller = createSessionTestController(ctx, { + const controller = await createSessionTestController(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/default', nativeOpen: true, fileApplications, openFileApplication, }) @@ -242,11 +242,11 @@ it('uses the same path verification for handler queries and explicit application it('avoids desktop queries when unavailable and rejects an empty query path', async () => { const fileApplications = vi.fn(async () => []) - const unavailable = createSessionTestController(await context(), { + const unavailable = await createSessionTestController(await context(), { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/default', nativeOpen: false, fileApplications, }) expect(await unavailable.workspacePathApplications({ path: '/file.mp3' }, new AbortController().signal)).toEqual([]) - const available = createSessionTestController(await context(), { + const available = await createSessionTestController(await context(), { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/default', nativeOpen: true, fileApplications, }) await expect(available.workspacePathApplications({ path: '' }, new AbortController().signal)).rejects.toMatchObject({ code: 'gateway/bad-request' }) @@ -262,7 +262,7 @@ it('returns bounded query failures and classifies cancellation through the Remot lifetime.abort() throw new Error('native query stopped') }) - const remote = createSessionTestRemote(await context(), { + const remote = await createSessionTestRemote(await context(), { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/default', nativeOpen: true, fileApplications, }) await expect(remote.workspacePathApplications({ path: '/file.mp3' })).resolves.toMatchObject({ diff --git a/packages/api/session-controller/tests/session-presets.host.spec.ts b/packages/api/session-controller/tests/session-presets.host.spec.ts index 5406d235f8..e39c7ea7d2 100644 --- a/packages/api/session-controller/tests/session-presets.host.spec.ts +++ b/packages/api/session-controller/tests/session-presets.host.spec.ts @@ -75,11 +75,11 @@ async function harness(presets?: readonly string[]) { }, } ctx.agents.setFactory(factory) - const remote = createSessionTestRemote(ctx, { + const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'test', model: 'test-model' }), cwd, }) - if (presets !== undefined) ctx.sessionProjections.register(agentPresetProjectionDefinition) + if (presets !== undefined) await ctx.sessionProjections.register(agentPresetProjectionDefinition) return { ctx, remote } } diff --git a/packages/api/session-controller/tests/session-projections.host.spec.ts b/packages/api/session-controller/tests/session-projections.host.spec.ts index ed4f4a6b0c..0b565dfd33 100644 --- a/packages/api/session-controller/tests/session-projections.host.spec.ts +++ b/packages/api/session-controller/tests/session-projections.host.spec.ts @@ -160,7 +160,7 @@ function seedMessages(session: Session, count: number): void { } } -const remote = (ctx: Context) => createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) +const remote = async (ctx: Context) => await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) describe('session.history projections block', () => { it('keeps the v0 numeric seed cut on the wire while logical headers expose only lineage', async () => { @@ -182,7 +182,7 @@ describe('session.history projections block', () => { }, }) - const snapshot = await opening(remote(ctx), child.id) + const snapshot = await opening(await remote(ctx), child.id) expect(snapshot.header).toEqual({ version: SESSION_FORMAT_VERSION, @@ -197,7 +197,7 @@ describe('session.history projections block', () => { it('tracks pending and used model selections across repeated request headers', async () => { const { ctx, session } = await harness(true) - remote(ctx) + await remote(ctx) await new Promise(resolve => setTimeout(resolve, 0)) const selected = { provider: 'p', model: 'next' } session.append('model/selection', selected) @@ -225,9 +225,9 @@ describe('session.history projections block', () => { it('serves the unit value on the tail page with asOfSeq = last event seq', async () => { const { ctx, session } = await harness(true) - ctx.sessionProjections.register(lastUserUnit()) + await ctx.sessionProjections.register(lastUserUnit()) seedMessages(session, 3) - const snapshot = await opening(remote(ctx), session.id) + const snapshot = await opening(await remote(ctx), session.id) const { records, projections } = snapshot expect(projections.asOfSeq).toBe(session.seq - 1) expect(projections.values['test/last-user']).toEqual({ text: 'm2' }) @@ -257,7 +257,7 @@ describe('session.history projections block', () => { list: () => Promise.resolve([meta]), inspect: () => Promise.resolve({ meta, events, inheritedEventCount: SessionLogOffset(0) }), }) as never) - const snapshot = await opening(remote(ctx), coldId) + const snapshot = await opening(await remote(ctx), coldId) expect(snapshot.projections.values.inbox).toEqual({ 'next-turn': [message], @@ -269,7 +269,7 @@ describe('session.history projections block', () => { it('removes claimed steering from the pending Inbox projection immediately', async () => { const { ctx, session, claim } = await harness(true) - const proxy = remote(ctx) + const proxy = await remote(ctx) const message = createUserMessage({ content: [{ type: 'text', text: 'apply this now' }], source: { kind: 'user' }, @@ -309,10 +309,10 @@ describe('session.history projections block', () => { it('returns a complete current replacement cut on each follow generation', async () => { const { ctx, session } = await harness(true) - ctx.sessionProjections.register(lastUserUnit()) + await ctx.sessionProjections.register(lastUserUnit()) seedMessages(session, 2) - const snapshot = await opening(remote(ctx), session.id) + const snapshot = await opening(await remote(ctx), session.id) expect(snapshot.records.map(record => record.event.seq)).toEqual([0, 1]) expect(snapshot.projections.asOfSeq).toBe(1) @@ -323,9 +323,9 @@ describe('session.history projections block', () => { it('projects an empty log at cursor -1', async () => { const { ctx, session } = await harness(true) - ctx.sessionProjections.register(lastUserUnit()) + await ctx.sessionProjections.register(lastUserUnit()) - const snapshot = await opening(remote(ctx), session.id) + const snapshot = await opening(await remote(ctx), session.id) expect(snapshot.records).toEqual([]) expect(snapshot.projections.asOfSeq).toBe(-1) @@ -350,7 +350,7 @@ describe('session.history projections block', () => { saveImage(): Promise { return Promise.reject(new Error('unused')) } readImage(): Promise { return Promise.reject(new Error('unused')) } }) - const gateway = remote(ctx) + const gateway = await remote(ctx) await new Promise(resolve => setTimeout(resolve, 0)) seedMessages(session, 2) const snapshot = await opening(gateway, session.id) @@ -377,18 +377,18 @@ describe('session.history projections block', () => { await expect(extra).resolves.toEqual({ done: true, value: undefined }) }) - it('leaves the imageLimits key absent while no attachment service is composed', async () => { + it("publishes image limits from the Controller's attachment dependency", async () => { const { ctx, session } = await harness(true) seedMessages(session, 1) - const snapshot = await opening(remote(ctx), session.id) - expect('imageLimits' in snapshot.projections.values).toBe(false) + const snapshot = await opening(await remote(ctx), session.id) + expect(snapshot.projections.values.imageLimits).toEqual(ctx.attachments.imageLimits) }) it('never carries the block on loadOlder pages (beforeSeq present)', async () => { const { ctx, session } = await harness(true) - ctx.sessionProjections.register(lastUserUnit()) + await ctx.sessionProjections.register(lastUserUnit()) seedMessages(session, 5) - const older = await page(remote(ctx), request({ + const older = await page(await remote(ctx), request({ sessionId: session.id, throughSeq: session.seq - 1, beforeSeq: 3, maxMessages: 2, })) expect(older.ok).toBe(true) @@ -399,7 +399,7 @@ describe('session.history projections block', () => { it('serves no block when the composition has no projection registry', async () => { const { ctx, session } = await harness(false) seedMessages(session, 2) - const response = await page(remote(ctx), request({ sessionId: session.id, throughSeq: session.seq - 1 })) + const response = await page(await remote(ctx), request({ sessionId: session.id, throughSeq: session.seq - 1 })) expect(response.ok).toBe(true) if (!response.ok) throw new Error('unreachable') expect('projections' in response.value).toBe(false) @@ -407,8 +407,8 @@ describe('session.history projections block', () => { it('never exposes a host-only unit through history, listing, or push frames', async () => { const { ctx, session } = await harness(true) - ctx.sessionProjections.register(internalCountUnit()) - const proxy = remote(ctx) + await ctx.sessionProjections.register(internalCountUnit()) + const proxy = await remote(ctx) await new Promise(resolve => setTimeout(resolve, 0)) const abort = new AbortController() const iterator = proxy.control(abort.signal)[Symbol.asyncIterator]() @@ -438,13 +438,13 @@ describe('session.history projections block', () => { it('drops a disposed registration from subsequent tail pages (empty block, key absent)', async () => { const { ctx, session } = await harness(true) - const dispose = ctx.sessionProjections.register(lastUserUnit()) + const dispose = await ctx.sessionProjections.register(lastUserUnit()) seedMessages(session, 1) - const proxy = remote(ctx) + const proxy = await remote(ctx) const before = await opening(proxy, session.id) expect(before.projections.values['test/last-user']).toEqual({ text: 'm0' }) - dispose() + await dispose.dispose() const after = await opening(proxy, session.id) // The registry stays mounted; only the disposed key leaves while the // gateway-owned Session-list unit remains. @@ -459,8 +459,8 @@ describe('session.history projections block', () => { it('removes the gateway-owned Session-list unit when the gateway fiber unloads', async () => { const { ctx, session } = await harness(true) expect('sessionListMetadata' in ctx.sessionProjections.snapshot(session).values).toBe(false) - const fiber = ctx.plugin(Object.assign((gatewayCtx: Context) => { - createSessionTestRemote(gatewayCtx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) + const fiber = ctx.plugin(Object.assign(async (gatewayCtx: Context) => { + await createSessionTestRemote(gatewayCtx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) }, { inject: ['sessions', 'agents', 'sessionProjections'] })) await fiber.await() await vi.waitFor(() => { @@ -475,8 +475,8 @@ describe('session.history projections block', () => { describe('session.list projections column', () => { it('serves every already-materialized wire value from the live registry without folding', async () => { const { ctx, session } = await harness(true) - ctx.sessionProjections.register(lastUserUnit()) - const gateway = remote(ctx) + await ctx.sessionProjections.register(lastUserUnit()) + const gateway = await remote(ctx) await new Promise(resolve => setTimeout(resolve, 0)) session.append('turn/start', { turn: 1 }) seedMessages(session, 1) @@ -496,8 +496,8 @@ describe('session.list projections column', () => { const session = ctx.sessions.create(SessionId('preset-list'), { meta: { cwd: '/workspace', agentPreset: 'standard' }, }) - ctx.sessionProjections.register(agentPresetProjectionDefinition) - const gateway = remote(ctx) + await ctx.sessionProjections.register(agentPresetProjectionDefinition) + const gateway = await remote(ctx) await new Promise(resolve => setTimeout(resolve, 0)) session.append('agent-preset/selected', { agentPreset: 'minimal' }) @@ -512,9 +512,9 @@ describe('session.list projections column', () => { seedMessages(session, 1) const unit = lastUserUnit() const apply = vi.fn(unit.apply) - ctx.sessionProjections.register({ ...unit, apply }) + await ctx.sessionProjections.register({ ...unit, apply }) - const response = await remote(ctx).list(request({})) + const response = await (await remote(ctx)).list(request({})) if (!response.ok) throw new Error('unreachable') const row = response.value.items.find(item => item.sessionId === session.id) expect(row).toBeDefined() @@ -525,7 +525,7 @@ describe('session.list projections column', () => { it('omits the column entirely when no registry is mounted', async () => { const { ctx, session } = await harness(false) seedMessages(session, 1) - const response = await remote(ctx).list(request({})) + const response = await (await remote(ctx)).list(request({})) if (!response.ok) throw new Error('unreachable') const row = response.value.items.find(item => item.sessionId === session.id) expect(row).toBeDefined() @@ -555,7 +555,7 @@ describe('session.list projections column', () => { } : undefined), } as never) - const response = await remote(ctx).list(request({})) + const response = await (await remote(ctx)).list(request({})) if (!response.ok) throw new Error('unreachable') const row = response.value.items.find(item => item.sessionId === coldId) expect(row?.running).toBe(false) @@ -580,9 +580,9 @@ describe('session.list projections column', () => { await ctx.plugin(SessionStore) await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(privatePromptUnit()) + await ctx.sessionProjections.register(privatePromptUnit()) await ctx.plugin(SessionProjectionCache, { writeEveryEvents: 100, writeIntervalMs: 60_000 }) - const gateway = remote(ctx) + const gateway = await remote(ctx) await new Promise(resolve => setTimeout(resolve, 0)) const id = SessionId('session-cold-host-state') @@ -635,7 +635,7 @@ describe('session.list projections column', () => { await ctx.plugin(SessionStore) await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(titleProjectionDefinition) + await ctx.sessionProjections.register(titleProjectionDefinition) await ctx.plugin(SessionProjectionCache, { writeEveryEvents: 100, writeIntervalMs: 60_000 }) return ctx } @@ -647,7 +647,7 @@ describe('session.list projections column', () => { let lastSeq: number | undefined let promptTime: number | undefined try { - remote(first) + await remote(first) await new Promise(resolve => setTimeout(resolve, 0)) let child: Session | undefined const owner = await first.plugin(Object.assign((sessionCtx: Context) => { @@ -679,7 +679,7 @@ describe('session.list projections column', () => { // record, and nothing opens the log. const second = await boot() try { - const gateway = remote(second) + const gateway = await remote(second) await new Promise(resolve => setTimeout(resolve, 0)) const load = () => { throw new Error('a cold fork listing must not load the event log') } second.provide('sessionPersistence', testSessionPersistence(second, { @@ -712,7 +712,7 @@ describe('session.list projections column', () => { ctx.provide('sessionPersistence', testSessionPersistence(ctx, { list: async () => [{ version: SESSION_FORMAT_VERSION, id: coldId, createdAt: 5, isSeeded: false, cwd: '/tmp' }], }) as never) - const response = await remote(ctx).list(request({})) + const response = await (await remote(ctx)).list(request({})) if (!response.ok) throw new Error('unreachable') const row = response.value.items.find(item => item.sessionId === coldId) expect(row).toBeDefined() @@ -721,7 +721,7 @@ describe('session.list projections column', () => { it('a throwing column read degrades that row, never the listing', async () => { const { ctx, session } = await harness(true) - ctx.sessionProjections.register({ + await ctx.sessionProjections.register({ ...lastUserUnit(), wire: { viewSchema: z.union([z.object({ text: z.string() }), z.null()]), @@ -729,7 +729,7 @@ describe('session.list projections column', () => { }, }) seedMessages(session, 1) - const response = await remote(ctx).list(request({})) + const response = await (await remote(ctx)).list(request({})) if (!response.ok) throw new Error('unreachable') const row = response.value.items.find(item => item.sessionId === session.id) expect(row).toBeDefined() @@ -754,8 +754,8 @@ describe('Session control projection frames', () => { it('broadcasts changed view references with the causing seq and skips same-reference applies', async () => { const { ctx, session } = await harness(true) - ctx.sessionProjections.register(lastUserUnit()) - const proxy = remote(ctx) + await ctx.sessionProjections.register(lastUserUnit()) + const proxy = await remote(ctx) // The controller's onChanged subscription lives in an inject child whose // fiber activates asynchronously; yield until it lands before appending. await new Promise(resolve => setTimeout(resolve, 0)) diff --git a/packages/api/session-controller/tests/session-rename.host.spec.ts b/packages/api/session-controller/tests/session-rename.host.spec.ts index 0e4754d8b0..c9f9502331 100644 --- a/packages/api/session-controller/tests/session-rename.host.spec.ts +++ b/packages/api/session-controller/tests/session-rename.host.spec.ts @@ -65,14 +65,14 @@ async function liveAgent(ctx: Context, id: string, turns: number): Promise createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) +const remote = async (ctx: Context) => await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) describe('sessions.rename', () => { it('accepts through the composed title service: normalized user-source event, echoed seq', async () => { const ctx = await composed() const source = await liveAgent(ctx, 'session-rename', 1) - const renamed = await remote(ctx).rename(request({ sessionId: source.id, title: ' new name ' })) + const renamed = await (await remote(ctx)).rename(request({ sessionId: source.id, title: ' new name ' })) expect(renamed.ok).toBe(true) if (!renamed.ok) return expect(renamed.value.title).toBe('new name') @@ -86,7 +86,7 @@ describe('sessions.rename', () => { const source = await liveAgent(ctx, 'session-rename-bad', 1) // U+200B passes a client-side trim gate but normalizes to empty host-side. - const response = await remote(ctx).rename(request({ sessionId: source.id, title: ' ​ ' })) + const response = await (await remote(ctx)).rename(request({ sessionId: source.id, title: ' ​ ' })) expect(response.ok).toBe(false) if (!response.ok) { expect(response.error).toMatchObject({ @@ -107,7 +107,7 @@ describe('sessions.rename', () => { const stale = await liveAgent(foreign, 'session-rename-stale', 1) await ctx.agents.register({ id: stale.id, session: stale, status: 'idle', ctx } as Agent) - const response = await remote(ctx).rename(request({ sessionId: stale.id, title: 'name' })) + const response = await (await remote(ctx)).rename(request({ sessionId: stale.id, title: 'name' })) expect(response.ok).toBe(false) if (!response.ok) expect(response.error.code).toBe('gateway/internal') }) @@ -116,7 +116,7 @@ describe('sessions.rename', () => { const ctx = await composed(false) const source = await liveAgent(ctx, 'session-no-titles', 1) - const response = await remote(ctx).rename(request({ sessionId: source.id, title: 'name' })) + const response = await (await remote(ctx)).rename(request({ sessionId: source.id, title: 'name' })) expect(response.ok).toBe(false) if (!response.ok) { expect(response.error.code).toBe('gateway/internal') diff --git a/packages/api/session-controller/tests/session-search.host.spec.ts b/packages/api/session-controller/tests/session-search.host.spec.ts index d8690ed0a0..b96b6e9463 100644 --- a/packages/api/session-controller/tests/session-search.host.spec.ts +++ b/packages/api/session-controller/tests/session-search.host.spec.ts @@ -17,7 +17,7 @@ import { type SessionSearchHit, type SessionSearchRequest, } from '@deepseek-ai/dsh-session-query' -import { createSessionTestRemote, testSessionPersistence } from './test-remote.ts' +import { createSessionTestRemote, testSessionPersistence, TEST_IMAGE_LIMITS } from './test-remote.ts' import { ApiSessionList } from '../src/list.ts' const sid = (value: string): SessionId => value as SessionId @@ -96,7 +96,9 @@ function installSearchQuery( describe('session.search', () => { it('rejects search when the query service is absent', async () => { const ctx = await baseContext() + ctx.provide('attachments', { imageLimits: TEST_IMAGE_LIMITS } as never) const list = new ApiSessionList(ctx, 8) + await list.initialize() await expect(list.search('query', new AbortController().signal)).rejects.toMatchObject({ code: 'gateway/internal', @@ -151,7 +153,7 @@ describe('session.search', () => { ], })) installSearchQuery(ctx, searchSessions) - const remote = createSessionTestRemote(ctx, defaults) + const remote = await createSessionTestRemote(ctx, defaults) const signal = new AbortController().signal const response = await remote.search(request(' matching answer '), signal) @@ -187,7 +189,7 @@ describe('session.search', () => { ctx.sessions.create(sid('visible'), { meta: header('visible') }) const searchSessions = vi.fn() installSearchQuery(ctx, searchSessions) - const remote = createSessionTestRemote(ctx, defaults) + const remote = await createSessionTestRemote(ctx, defaults) for (const query of ['', ' ', 'contains\0nul', 'x'.repeat(501)]) { await expect(remote.search(request(query), new AbortController().signal)) @@ -201,7 +203,7 @@ describe('session.search', () => { const ctx = await baseContext() const searchSessions = vi.fn() installSearchQuery(ctx, searchSessions) - const remote = createSessionTestRemote(ctx, defaults) + const remote = await createSessionTestRemote(ctx, defaults) const response = await remote.search( request('anything'), @@ -235,7 +237,7 @@ describe('session.search', () => { ], })) - const response = await createSessionTestRemote(ctx, defaults).search( + const response = await (await createSessionTestRemote(ctx, defaults)).search( request('match'), new AbortController().signal, ) @@ -262,7 +264,7 @@ describe('session.search', () => { }) .mockResolvedValueOnce({ items: items.slice(19) }) installSearchQuery(ctx, searchSessions) - const response = await createSessionTestRemote(ctx, defaults).search( + const response = await (await createSessionTestRemote(ctx, defaults)).search( request('match'), new AbortController().signal, ) @@ -303,7 +305,7 @@ describe('session.search', () => { }) installSearchQuery(ctx, searchSessions) - const response = await createSessionTestRemote(ctx, defaults).search( + const response = await (await createSessionTestRemote(ctx, defaults)).search( request('adaptive-page-limit'), new AbortController().signal, ) @@ -346,7 +348,7 @@ describe('session.search', () => { }) installSearchQuery(ctx, searchSessions) - const response = await createSessionTestRemote(ctx, defaults).search( + const response = await (await createSessionTestRemote(ctx, defaults)).search( request('endless-pages'), new AbortController().signal, ) @@ -411,7 +413,7 @@ describe('session.search', () => { }) installSearchQuery(ctx, searchSessions) - const response = await createSessionTestRemote(ctx, defaults).search( + const response = await (await createSessionTestRemote(ctx, defaults)).search( request('stale-restart'), new AbortController().signal, ) @@ -450,7 +452,7 @@ describe('session.search', () => { }) installSearchQuery(ctx, searchSessions) - const response = await createSessionTestRemote(ctx, defaults).search( + const response = await (await createSessionTestRemote(ctx, defaults)).search( request('stale-churn'), new AbortController().signal, ) @@ -479,7 +481,7 @@ describe('session.search', () => { }) installSearchQuery(ctx, searchSessions) - const response = await createSessionTestRemote(ctx, defaults).search( + const response = await (await createSessionTestRemote(ctx, defaults)).search( request('abort-stale'), controller.signal, ) @@ -500,7 +502,7 @@ describe('session.search', () => { ))) installSearchQuery(ctx, searchSessions) - const response = await createSessionTestRemote(ctx, defaults).search( + const response = await (await createSessionTestRemote(ctx, defaults)).search( request('first-page-stale'), new AbortController().signal, ) @@ -524,7 +526,7 @@ describe('session.search', () => { )) installSearchQuery(ctx, searchSessions) - const response = await createSessionTestRemote(ctx, defaults).search( + const response = await (await createSessionTestRemote(ctx, defaults)).search( request('continuation-invalid-limit'), new AbortController().signal, ) @@ -551,7 +553,7 @@ describe('session.search', () => { )) installSearchQuery(ctx, searchSessions) - const response = await createSessionTestRemote(ctx, defaults).search( + const response = await (await createSessionTestRemote(ctx, defaults)).search( request('minimum-page-limit'), new AbortController().signal, ) @@ -577,7 +579,7 @@ describe('session.search', () => { }) installSearchQuery(ctx, searchSessions) - const response = await createSessionTestRemote(ctx, defaults).search( + const response = await (await createSessionTestRemote(ctx, defaults)).search( request('abort-invalid-limit'), controller.signal, ) @@ -596,7 +598,7 @@ describe('session.search', () => { const searchSessions = vi.fn(() => Promise.resolve({ items: oversized })) installSearchQuery(ctx, searchSessions) - const response = await createSessionTestRemote(ctx, defaults).search( + const response = await (await createSessionTestRemote(ctx, defaults)).search( request('oversized-page'), new AbortController().signal, ) @@ -622,7 +624,7 @@ describe('session.search', () => { }) installSearchQuery(ctx, searchSessions) - const response = await createSessionTestRemote(ctx, defaults).search( + const response = await (await createSessionTestRemote(ctx, defaults)).search( request('adapted-oversized-page'), new AbortController().signal, ) @@ -648,7 +650,7 @@ describe('session.search', () => { } installSearchQuery(ctx, () => Promise.resolve({ items: [overlong] })) - const response = await createSessionTestRemote(ctx, defaults).search( + const response = await (await createSessionTestRemote(ctx, defaults)).search( request('bounded-snippet'), new AbortController().signal, ) @@ -670,7 +672,7 @@ describe('session.search', () => { .mockResolvedValueOnce({ items: [], nextCursor: 'repeated' }) installSearchQuery(ctx, searchSessions) - const response = await createSessionTestRemote(ctx, defaults).search( + const response = await (await createSessionTestRemote(ctx, defaults)).search( request('repeated-cursor'), new AbortController().signal, ) @@ -693,7 +695,7 @@ describe('session.search', () => { .mockResolvedValueOnce({ items: items.slice(20), nextCursor: 'repeated' }) installSearchQuery(ctx, searchSessions) - const response = await createSessionTestRemote(ctx, defaults).search( + const response = await (await createSessionTestRemote(ctx, defaults)).search( request('repeated-lookahead-cursor'), new AbortController().signal, ) @@ -720,7 +722,7 @@ describe('session.search', () => { .mockResolvedValueOnce({ items: items.slice(20) }) installSearchQuery(ctx, searchSessions) - const response = await createSessionTestRemote(ctx, defaults).search( + const response = await (await createSessionTestRemote(ctx, defaults)).search( request('duplicate-pages'), new AbortController().signal, ) @@ -748,7 +750,7 @@ describe('session.search', () => { }) installSearchQuery(ctx, searchSessions) - const response = await createSessionTestRemote(ctx, defaults).search( + const response = await (await createSessionTestRemote(ctx, defaults)).search( request('cancel-continuation'), controller.signal, ) @@ -777,7 +779,7 @@ describe('session.search', () => { })) installSearchQuery(ctx, searchSessions) - const response = await createSessionTestRemote(ctx, defaults).search( + const response = await (await createSessionTestRemote(ctx, defaults)).search( request('large corpus'), new AbortController().signal, ) @@ -813,7 +815,7 @@ describe('session.search', () => { const searchSessions = vi.fn() installSearchQuery(ctx, searchSessions) - const response = await createSessionTestRemote(ctx, defaults).search( + const response = await (await createSessionTestRemote(ctx, defaults)).search( request('cancel-during-visibility'), controller.signal, ) @@ -840,7 +842,7 @@ describe('session.search', () => { const searchSessions = vi.fn(() => Promise.resolve({ items: [] })) installSearchQuery(ctx, searchSessions) - const response = await createSessionTestRemote(ctx, defaults).search( + const response = await (await createSessionTestRemote(ctx, defaults)).search( request('header-only-visibility'), new AbortController().signal, ) @@ -856,7 +858,7 @@ describe('session.search', () => { it('maps preflight cancellation, query cancellation, and provider failure', async () => { const missingCtx = await baseContext() missingCtx.sessions.create(sid('visible'), { meta: header('visible') }) - const missingApi = createSessionTestRemote(missingCtx, defaults) + const missingApi = await createSessionTestRemote(missingCtx, defaults) const preAborted = new AbortController() preAborted.abort() const cancelledBeforeLookup = await missingApi.search( @@ -875,7 +877,7 @@ describe('session.search', () => { .mockRejectedValueOnce(aborted) .mockRejectedValueOnce(new Error('database unavailable')) installSearchQuery(ctx, searchSessions) - const remote = createSessionTestRemote(ctx, defaults) + const remote = await createSessionTestRemote(ctx, defaults) const cancelled = await remote.search( request('first'), diff --git a/packages/api/session-controller/tests/subagent-catalog.host.spec.ts b/packages/api/session-controller/tests/subagent-catalog.host.spec.ts index de1852991a..7d7d5b384f 100644 --- a/packages/api/session-controller/tests/subagent-catalog.host.spec.ts +++ b/packages/api/session-controller/tests/subagent-catalog.host.spec.ts @@ -59,7 +59,7 @@ async function bench() { installSessionReadTestServices(ctx) provideWorkingDirectoryFixture(ctx) await ctx.plugin(SubagentRuntime) - const controller = createSessionTestController(ctx, defaults) + const controller = await createSessionTestController(ctx, defaults) return { ctx, controller } } @@ -195,7 +195,7 @@ describe('SessionController subagent catalog', () => { await ctx.plugin(SessionStore) await ctx.plugin(AgentRegistry) installSessionReadTestServices(ctx) - const controller = createSessionTestController(ctx, defaults) + const controller = await createSessionTestController(ctx, defaults) const session = ctx.sessions.create(PARENT, { meta: { cwd: '/workspace' } }) const expected = ctx.sessionProjections.snapshot(session) expect(expected.values.subagentCatalog).toBeUndefined() diff --git a/packages/api/session-controller/tests/test-remote.ts b/packages/api/session-controller/tests/test-remote.ts index 71db06b22b..3c4a7afa79 100644 --- a/packages/api/session-controller/tests/test-remote.ts +++ b/packages/api/session-controller/tests/test-remote.ts @@ -2,7 +2,7 @@ import type { SessionControllerInternals } from '../src/index.ts' import { SessionLogOffset } from '@deepseek-ai/dsh-session' -import type { Context } from '@deepseek-ai/cordis' +import { Service, type Context } from '@deepseek-ai/cordis' import type { ModelSelection as AgentModelSelection } from '@deepseek-ai/dsh-agent' import type { AdmittedPromptContentPart, @@ -103,11 +103,13 @@ export interface TestSessionRemoteDefaults { readonly openFileApplication?: SessionControllerInternals['openFileApplication'] readonly revealPath?: (path: string, signal: AbortSignal) => Promise readonly canOpenPath?: () => boolean + readonly listWorkSliceMs?: number } const installed = new WeakMap() -const TEST_IMAGE_LIMITS: ImageAttachmentLimits = Object.freeze({ +/** Image policy for controller fixtures without attachment I/O. */ +export const TEST_IMAGE_LIMITS: ImageAttachmentLimits = Object.freeze({ maxImageBytes: 5 * 1024 * 1024, maxImagesPerMessage: 20, maxMessageImageBytes: 100 * 1024 * 1024, @@ -231,10 +233,10 @@ export function installSessionReadTestServices(ctx: Context): void { if (ctx.get('sessionQuery') === undefined) new TestSessionQuery(ctx) } -function installControllers( +async function installControllers( ctx: Context, defaults: TestSessionRemoteDefaults, -): SessionController { +): Promise { const found = installed.get(ctx) if (found !== undefined) return found @@ -290,12 +292,18 @@ function installControllers( } installSessionReadTestServices(ctx) const cwd = vi.spyOn(process, 'cwd').mockReturnValue(defaults.cwd) - let controller: SessionController + class TestController extends SessionController { + initialize(): Promise { + return this[Service.init]() + } + } + let controller: TestController try { - controller = new SessionController( + controller = new TestController( ctx, { ...defaults.nativeOpen === undefined ? {} : { nativeOpen: defaults.nativeOpen }, + ...defaults.listWorkSliceMs === undefined ? {} : { listWorkSliceMs: defaults.listWorkSliceMs }, }, { ...defaults.openPath === undefined ? {} : { openPath: defaults.openPath }, @@ -305,6 +313,7 @@ function installControllers( ...defaults.canOpenPath === undefined ? {} : { canOpenPath: defaults.canOpenPath }, }, ) + await controller.initialize() } finally { cwd.mockRestore() } @@ -313,11 +322,11 @@ function installControllers( } /** Build or return the production Session Controller for a direct unit harness. */ -export function createSessionTestController( +export async function createSessionTestController( ctx: Context, defaults: TestSessionRemoteDefaults, -): SessionController { - return installControllers(ctx, defaults) +): Promise { + return await installControllers(ctx, defaults) } function remoteResult( @@ -341,11 +350,11 @@ function remoteResult( } /** Build the generated Session Remote's unary result semantics without a carrier. */ -export function createSessionTestRemote( +export async function createSessionTestRemote( ctx: Context, defaults: TestSessionRemoteDefaults, -): TestSessionRemote { - const direct = createSessionTestController(ctx, defaults) +): Promise { + const direct = await createSessionTestController(ctx, defaults) return { workspacePathApplications: (request, signal = new AbortController().signal) => remoteResult( () => direct.workspacePathApplications(request, signal), signal, diff --git a/packages/api/session-controller/tests/transport.host.spec.ts b/packages/api/session-controller/tests/transport.host.spec.ts index 890000a0ac..66b697fcef 100644 --- a/packages/api/session-controller/tests/transport.host.spec.ts +++ b/packages/api/session-controller/tests/transport.host.spec.ts @@ -76,7 +76,7 @@ async function setup(): Promise<{ ctx: Context; transport: SessionHistoryControl const ctx = new Context() await ctx.plugin(SessionStore) installSessionReadTestServices(ctx) - ctx.sessionProjections.register(subagentIdentityProjectionDefinition) + await ctx.sessionProjections.register(subagentIdentityProjectionDefinition) const transport = new SessionHistoryController(ctx, (observation) => { observation[Symbol.dispose]() }) return { ctx, transport } } diff --git a/packages/context/agent-instructions/README.i18n.yaml b/packages/context/agent-instructions/README.i18n.yaml index 9fc0cb6eae..818bcda27b 100644 --- a/packages/context/agent-instructions/README.i18n.yaml +++ b/packages/context/agent-instructions/README.i18n.yaml @@ -30,8 +30,8 @@ en: 724df7ff6d1d9fea zh: 91fb1013eb9178ec /deepseek-ai-dsh-agent-instructions/understand-the-implementation/design-concept: - en: 4708b51bf8fe86f7 - zh: 869fc4b1f78383ee + en: 566f3518b2d1d100 + zh: c7d0a22b19e7697a /deepseek-ai-dsh-agent-instructions/understand-the-implementation/source-map: en: cb4c1f929ef5a835 zh: 3f0fcb7f568a0a5c diff --git a/packages/context/agent-instructions/README.md b/packages/context/agent-instructions/README.md index 25c48ba1ac..e935cedf8d 100644 --- a/packages/context/agent-instructions/README.md +++ b/packages/context/agent-instructions/README.md @@ -85,7 +85,7 @@ This section explains the design decisions behind the plugin; the observable beh ### Design concept -The plugin is built on one principle: workspace instructions are durable conversation content, owned per agent and per session. Baseline and refresh messages are ordinary sourced `user/message` events, so they replay, compact, and resume exactly like other history, and model-visible state is always reconstructable from the session log. The plugin owns the complete `` framing and every injected message reaches the model verbatim. +The plugin is built on one principle: workspace instructions are durable conversation content, owned per agent and per session. Baseline and refresh messages are ordinary sourced `user/message` events, so they replay, compact, and resume exactly like other history, and model-visible state is always reconstructable from the session log. The plugin owns the complete `` framing and every injected message reaches the model verbatim. The package exports `AgentInstructionSource` for typed consumers of these message sources. ### Source map diff --git a/packages/context/agent-instructions/README.zh.md b/packages/context/agent-instructions/README.zh.md index a87cfae53a..cf193f2a28 100644 --- a/packages/context/agent-instructions/README.zh.md +++ b/packages/context/agent-instructions/README.zh.md @@ -85,7 +85,7 @@ export interface Config { ### 设计理念 -该插件建立在一个原则上:工作区指令是持久的对话内容,按 agent 与会话分别归属。基线消息与刷新消息都是普通的带来源 `user/message` 事件,因此与其他历史一样可回放、可压缩(compaction)、可恢复,模型可见状态总能从会话日志重建。插件拥有完整的 `` 框架,每条注入消息都原样到达模型。 +该插件建立在一个原则上:工作区指令是持久的对话内容,按 agent 与会话分别归属。基线消息与刷新消息都是普通的带来源 `user/message` 事件,因此与其他历史一样可回放、可压缩(compaction)、可恢复,模型可见状态总能从会话日志重建。插件拥有完整的 `` 框架,每条注入消息都原样到达模型。 本包导出 `AgentInstructionSource`,供这些消息来源的消费者进行类型检查。 ### 源码地图 diff --git a/packages/context/agent-instructions/src/index.ts b/packages/context/agent-instructions/src/index.ts index 74d6e5fa22..b32a5e6bb7 100644 --- a/packages/context/agent-instructions/src/index.ts +++ b/packages/context/agent-instructions/src/index.ts @@ -9,6 +9,7 @@ * @module @deepseek-ai/dsh-agent-instructions */ +import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent' import type {} from '@deepseek-ai/dsh-working-directory' import type { Context } from '@deepseek-ai/cordis' import { isDeepStrictEqual } from 'node:util' @@ -23,7 +24,6 @@ import { trimmedInstructionDigest } from './digest.ts' import { applyInstructionVersionUpdates, baselineInstructionState, - name, reconcileInstructionContext, agentInstructionsMessage, type InstructionVersionCache, @@ -31,9 +31,9 @@ import { } from './state.ts' import { instructionCandidateGroup, instructionScopeKey, scopeForDisplayPath, type AgentInstructionChange } from './render.ts' -export { Config, name } -/** Services required by workspace instruction projection. */ -export const inject = ['sessionProjections', 'workingDirectory'] +export { Config } +export { name } from './state.ts' +export type { AgentInstructionSource } from './state.ts' export { discoverBaselineInstructionFiles, loadBaselineInstructions, @@ -45,6 +45,8 @@ export type { export { renderAgentInstructions } from './render.ts' export type { RenderedAgentInstructions, TruncatedInstruction } from './render.ts' +export const inject = ['sessionProjections', 'workingDirectory'] + function visibleBaselineSource( agent: Agent, authorityMessages: readonly UserMessage[], @@ -83,7 +85,13 @@ function filePathFromExecution(exec: ToolExecution): string | undefined { return filePath.length > 0 ? filePath : undefined } -export function apply(ctx: Context, config: Config): void { +/** Register the agent-instructions plugin after its projections are ready. + * @param ctx - plugin context. + * @param config - workspace instruction policy. + * @returns when the plugin is ready. + */ +export async function apply(ctx: Context, config: Config): Promise { + const turnBoundary = await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) const resolved: ResolvedConfig = resolveConfig(config) const instructionVersions: InstructionVersionCache = new WeakMap() const baselinePreparations = new WeakMap { - const boundary = ctx.sessionProjections.stateOf(session, 'turnBoundary') - if (boundary === undefined) { - throw new Error('agent-instructions requires the turnBoundary session projection') - } + const boundary = turnBoundary.stateOf(session) return boundary.openTurnStartSeq !== null && boundary.lastStepBoundary?.kind === 'start' && boundary.lastStepBoundary.seq > boundary.openTurnStartSeq diff --git a/packages/context/agent-instructions/tests/agent-instructions.spec.ts b/packages/context/agent-instructions/tests/agent-instructions.spec.ts index 721866df73..a4df1f6d21 100644 --- a/packages/context/agent-instructions/tests/agent-instructions.spec.ts +++ b/packages/context/agent-instructions/tests/agent-instructions.spec.ts @@ -5,6 +5,7 @@ import { tmpdir } from 'node:os' import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' +import type { Config as AgentInstructionsConfig } from '@deepseek-ai/dsh-agent-instructions' import * as AgentInstructions from '@deepseek-ai/dsh-agent-instructions' import LlmRuntime, { createUserMessage, ToolCallId, type Message, type MessageSource, type StreamChunk } from '@deepseek-ai/dsh-llm' import type { ContextFormed } from '@deepseek-ai/dsh-llm' @@ -236,19 +237,19 @@ class BlockingReadFileSystem extends RecordingFileSystem { } } -async function mountAgentInstructionsPlugin(ctx: Context, config: AgentInstructions.Config): Promise>> { +async function mountAgentInstructionsPlugin(ctx: Context, config: AgentInstructionsConfig): Promise>> { if (ctx.get('workingDirectory') === undefined) provideWorkingDirectoryFixture(ctx) if (ctx.get('sessionProjections') === undefined) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) return ctx.plugin(AgentInstructions, config) } -async function mountAgentInstructions(ctx: Context, config: AgentInstructions.Config): Promise>> { +async function mountAgentInstructions(ctx: Context, config: AgentInstructionsConfig): Promise>> { await ctx.plugin(LocalFileSystem, { cwd: '/' }) return mountAgentInstructionsPlugin(ctx, config) } -async function mountFileToolsAndAgentInstructions(ctx: Context, config: AgentInstructions.Config): Promise>> { +async function mountFileToolsAndAgentInstructions(ctx: Context, config: AgentInstructionsConfig): Promise>> { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) await ctx.plugin(LocalFileSystem, { cwd: '/' }) @@ -1234,7 +1235,7 @@ describe('workspace context request injection', () => { const ctx = new Context() provideWorkingDirectoryFixture(ctx) - await expect(mountAgentInstructionsPlugin(ctx, {} as AgentInstructions.Config)).rejects.toThrow(/maxBytes/) + await expect(mountAgentInstructionsPlugin(ctx, {} as AgentInstructionsConfig)).rejects.toThrow(/maxBytes/) }) it('mounts without requiring a filesystem provider', async () => { @@ -1251,28 +1252,6 @@ describe('workspace context request injection', () => { expect(AgentInstructions.inject).toEqual(['sessionProjections', 'workingDirectory']) }) - it('rejects a file-touch projection when the turn boundary unit is absent', async () => { - const ctx = new Context() - provideWorkingDirectoryFixture(ctx) - await ctx.plugin(SessionProjectionRegistry) - await ctx.plugin(AgentInstructions, { maxBytes: 65536 }) - const exec = stubToolExecution({ - callId: ToolCallId('missing-turn-boundary'), - name: 'read', - arguments: { file_path: 'file.txt' }, - agent: await stubAgent('/virtual/repo'), - signal: testToolSignal, - }) - - expect(() => { - ctx.emit('tools/result', exec, { - content: [{ type: 'text', text: 'ok' }], - isError: false, - value: null, - }) - }).toThrow('agent-instructions requires the turnBoundary session projection') - }) - it('does not inject baseline context when no filesystem provider is present', async () => { const ctx = new Context() provideWorkingDirectoryFixture(ctx) diff --git a/packages/context/harness-dev-context/README.i18n.yaml b/packages/context/harness-dev-context/README.i18n.yaml index adc0b95c0e..5a9ee319b8 100644 --- a/packages/context/harness-dev-context/README.i18n.yaml +++ b/packages/context/harness-dev-context/README.i18n.yaml @@ -33,8 +33,8 @@ en: 4696b6c3a1f91f96 zh: 3a56d257ec768a5e /deepseek-ai-dsh-harness-dev-context/understand-the-implementation/main-flow: - en: f3844a1825b2e329 - zh: bdcafbcadbc6d316 + en: 55c4cdd475f9f851 + zh: ac4fb96e7be88810 /deepseek-ai-dsh-harness-dev-context/further-exploration: en: f291741c499581ac zh: 8ee0baef51e69ef0 diff --git a/packages/context/harness-dev-context/README.md b/packages/context/harness-dev-context/README.md index 04d3925181..3d4ca78246 100644 --- a/packages/context/harness-dev-context/README.md +++ b/packages/context/harness-dev-context/README.md @@ -57,7 +57,7 @@ A host-only session projection, `harnessDevLegacyPrompt`, folds every committed ### Main flow -Activation registers the projection unit, both sections, and both contexts. At each assembly the sections choose the admitted earlier paragraph or the current guidance; at each request admission the contexts read the advertised URL. Disposing the plugin fiber removes every registration. +Activation awaits the projection handle before registering both sections and both contexts. At each assembly the sections read the handle to choose the admitted earlier paragraph or the current guidance; at each request admission the contexts read the advertised URL. Disposing the plugin fiber removes every registration.
diff --git a/packages/context/harness-dev-context/README.zh.md b/packages/context/harness-dev-context/README.zh.md index e108c6f161..b25f5eea66 100644 --- a/packages/context/harness-dev-context/README.zh.md +++ b/packages/context/harness-dev-context/README.zh.md @@ -57,7 +57,7 @@ kind: "package-reference" ### 主流程 -激活时注册投影单元、两个段落与两个上下文。每次组装时,段落在已提交的早期段落与当前说明之间选择;每次请求准入时,上下文读取公告 URL。销毁插件 fiber 会移除全部注册。 +激活时等待取得投影 handle,再注册两个段落与两个上下文。每次组装时,段落读取 handle,在已提交的早期段落与当前说明之间选择;每次请求准入时,上下文读取公告 URL。销毁插件 fiber 会移除全部注册。
diff --git a/packages/context/harness-dev-context/src/index.ts b/packages/context/harness-dev-context/src/index.ts index becb4a6005..d823735a2a 100644 --- a/packages/context/harness-dev-context/src/index.ts +++ b/packages/context/harness-dev-context/src/index.ts @@ -124,9 +124,10 @@ const ADMITS_NOTHING: LegacyPrompt = { source: null, web: null } * Register the two sections, their runtime-context values, and the * projection that keeps a Session's admitted paragraphs. * @param ctx - plugin context with every {@link inject} service. + * @returns after the projection reader and prompt registrations are ready. */ -export function apply(ctx: Context): void { - ctx.sessionProjections.register({ +export async function apply(ctx: Context): Promise { + const legacyPrompt = await ctx.sessionProjections.register({ key: LEGACY_PROMPT, stateVersion: 1, stateSchema: legacyPromptSchema, @@ -135,8 +136,7 @@ export function apply(ctx: Context): void { }) const admitted = (context: AssembleContext): LegacyPrompt => { const session = context.agent?.session - // This plugin registers the unit above, so its state exists for every Session. - return session === undefined ? ADMITS_NOTHING : ctx.sessionProjections.stateOf(session, LEGACY_PROMPT) as LegacyPrompt + return session === undefined ? ADMITS_NOTHING : legacyPrompt.stateOf(session) } ctx.systemPrompt.section({ name: HARNESS_SOURCE, diff --git a/packages/context/time-context/src/index.ts b/packages/context/time-context/src/index.ts index 493630a811..d1049f4203 100644 --- a/packages/context/time-context/src/index.ts +++ b/packages/context/time-context/src/index.ts @@ -49,7 +49,6 @@ const timeContextStateSchema = zod.object({ /** Folded time-context readings. */ type TimeContextProjection = zod.infer -/** The agent registry that owns pre-step processing. */ export const inject = ['agents', 'sessionProjections'] /** Request-preparation clock formatting and append scheduling. Invalid values fail plugin load. */ @@ -128,8 +127,9 @@ function validateRefreshInterval(refreshIntervalMs: number): void { * @param ctx - plugin context; the listener is disposed with it. * @param config - time zone and durable refresh scheduling configuration. * @throws when the refresh interval is invalid or the configured or process time zone cannot be resolved. + * @returns when the plugin is ready. */ -export function apply(ctx: Context, config: Config): void { +export async function apply(ctx: Context, config: Config): Promise { const timeZone = config.timeZone const refreshIntervalMs = config.refreshIntervalMs ?? 600_000 validateRefreshInterval(refreshIntervalMs) @@ -154,7 +154,7 @@ export function apply(ctx: Context, config: Config): void { return created } - ctx.sessionProjections.register({ + const projection = await ctx.sessionProjections.register({ key: 'timeContext', stateVersion: 2, stateSchema: timeContextStateSchema, @@ -189,7 +189,7 @@ export function apply(ctx: Context, config: Config): void { const decision = await next() if (decision.kind === 'reject' || signal.aborted) return decision const now = Date.now() - const state = ctx.sessionProjections.stateOf(agent.session, 'timeContext') as TimeContextProjection + const state = projection.stateOf(agent.session) if (refreshIntervalMs > 0) { const lastInjection = state.lastInjectionTime if (lastInjection != null diff --git a/packages/context/time-context/tests/time-context.spec.ts b/packages/context/time-context/tests/time-context.spec.ts index 92499007d1..a795935f2e 100644 --- a/packages/context/time-context/tests/time-context.spec.ts +++ b/packages/context/time-context/tests/time-context.spec.ts @@ -429,10 +429,10 @@ describe('time-context projection fold edges', () => { content: [{ type: 'text', text: 'reading' }], source: { kind: 'time-context' }, }), { surfaceOp: 'append' }) - expect(typeof ctx.sessionProjections.stateOf(session, 'timeContext')?.lastTurnInjectionTime).toBe('number') + expect(ctx.sessionProjections.checkpoint(session).timeContext?.val).toHaveProperty('lastTurnInjectionTime', expect.any(Number)) session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) session.append('turn/start', { turn: 2 }) - expect(ctx.sessionProjections.stateOf(session, 'timeContext')).toMatchObject({ + expect(ctx.sessionProjections.checkpoint(session).timeContext?.val).toMatchObject({ lastTurnInjectionTime: null, }) }) @@ -441,7 +441,7 @@ describe('time-context projection fold edges', () => { const { ctx } = await mount() const session = Session.create(SessionId('end-without-start')) session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) - expect(ctx.sessionProjections.stateOf(session, 'timeContext')).toMatchObject({ + expect(ctx.sessionProjections.checkpoint(session).timeContext?.val).toMatchObject({ lastTurnInjectionTime: null, }) }) diff --git a/packages/context/tmux-context/src/index.ts b/packages/context/tmux-context/src/index.ts index af6d636e93..fec71d8d69 100644 --- a/packages/context/tmux-context/src/index.ts +++ b/packages/context/tmux-context/src/index.ts @@ -36,11 +36,9 @@ declare module '@deepseek-ai/dsh-llm' { } } - /** Cordis plugin name used by loader diagnostics. */ export const name = 'tmux-context' -/** The agent registry that owns pre-step processing. */ export const inject = ['agents', 'sessionProjections'] /** Per-turn tmux-location scheduling. Invalid values fail plugin load. */ @@ -223,11 +221,15 @@ declare module '@deepseek-ai/dsh-session-projection/types' { } } -export function apply(ctx: Context, config: Config): void { +/** Register the tmux-context plugin after its projections are ready. + * @param ctx - plugin context. + * @returns when the plugin is ready. + */ +export async function apply(ctx: Context, config: Config): Promise { const refreshIntervalMs = config.refreshIntervalMs validateRefreshInterval(refreshIntervalMs) - ctx.sessionProjections.register({ + const projection = await ctx.sessionProjections.register({ key: 'tmuxContext', stateVersion: 1, stateSchema: tmuxContextStateSchema, @@ -251,7 +253,7 @@ export function apply(ctx: Context, config: Config): void { if (decision.kind === 'reject' || signal.aborted || step !== 1) return decision const bash = ctx.get('shell') if (bash === undefined) return decision - const previous = ctx.sessionProjections.stateOf(agent.session, 'tmuxContext') as TmuxContextState + const previous = projection.stateOf(agent.session) if (refreshIntervalMs !== undefined && refreshIntervalMs > 0 && previous !== null) { const now = Date.now() if (now >= previous.time && now - previous.time < refreshIntervalMs) return decision diff --git a/packages/deliverables/tool-present/src/index.ts b/packages/deliverables/tool-present/src/index.ts index 8b47748b03..afc6ab82a8 100644 --- a/packages/deliverables/tool-present/src/index.ts +++ b/packages/deliverables/tool-present/src/index.ts @@ -1,4 +1,5 @@ /** Scoped tool that declares filesystem deliveries in their owning Session. */ +import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent' import type {} from '@deepseek-ai/dsh-working-directory' import type { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' @@ -9,7 +10,6 @@ import type {} from '@deepseek-ai/dsh-session-projection' import type { Session } from '@deepseek-ai/dsh-session' import type { PresentedFile } from './types.ts' -/** Stable Loader identity. */ export const name = 'tool-present' /** Per-call delivery limit. */ @@ -23,15 +23,16 @@ export const Config: z = z.object({ maxFiles: z.number().default(8), }) -/** Services used by the scoped delivery tool. */ export const inject = ['tools', 'fs', 'sessionProjections', 'workingDirectory'] /** * Register present with durable file references in its tool result. * @param ctx - agent-scoped services. * @param config - maximum files per call. + * @returns when the plugin is ready. */ -export function apply(ctx: Context, config: Config): void { +export async function apply(ctx: Context, config: Config): Promise { + const turnBoundary = await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) if (!Number.isSafeInteger(config.maxFiles) || config.maxFiles < 1) { throw new Error('present requires a positive integer maxFiles') } @@ -76,8 +77,8 @@ export function apply(ctx: Context, config: Config): void { }, async execute(args, exec) { if (exec.agent === undefined) throw new Error('present requires an agent Session') - const boundary = ctx.sessionProjections.stateOf(exec.agent.session, 'turnBoundary') - if (boundary === undefined || boundary.openTurnStartSeq === null) throw new Error('present requires an open turn') + const boundary = turnBoundary.stateOf(exec.agent.session) + if (boundary.openTurnStartSeq === null) throw new Error('present requires an open turn') if (args.files.length === 0 || args.files.length > config.maxFiles) throw new Error(`present accepts 1 to ${config.maxFiles} files`) const cwd = await ctx.workingDirectory.ensure(exec.agent, exec.signal) const options = { cwd, signal: exec.signal } diff --git a/packages/deliverables/tool-present/tests/built-errors.e2e.ts b/packages/deliverables/tool-present/tests/built-errors.e2e.ts index e8b0bee060..7595872758 100644 --- a/packages/deliverables/tool-present/tests/built-errors.e2e.ts +++ b/packages/deliverables/tool-present/tests/built-errors.e2e.ts @@ -16,7 +16,8 @@ import LocalFileSystem from './packages/fs/fs-local/lib/index.js' import SystemPrompt from './packages/core/system-prompt/lib/index.js' import ToolRuntime from './packages/core/tools/lib/index.js' import { Session, SESSION_FORMAT_VERSION } from './packages/core/session/lib/index.js' -import * as Present from './packages/deliverables/tool-present/lib/index.js' +import Present from './packages/deliverables/tool-present/lib/index.js' +import SessionProjectionRegistry from './packages/session/session-projection/lib/index.js' import { mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -28,10 +29,11 @@ try { await ctx.plugin(ToolRuntime) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalFileSystem, { cwd: root }) - ctx.provide('sessionProjections', { stateOf() { return { openTurnStartSeq: 1, lastTurn: 1 } } }) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(Present, { maxFiles: 2 }) const scope = ctx.plugin(() => {}) const session = Session.create('built-present', [], { version: SESSION_FORMAT_VERSION, id: 'built-present', createdAt: 0, cwd: root, isSeeded: false }) + session.append('turn/start', { turn: 1 }) const owner = { id: 'built-present', session, ctx: scope.ctx, options: {}, status: 'idle' } await ctx.agents.register(owner) const events = [] diff --git a/packages/deliverables/tool-present/tests/present.spec.ts b/packages/deliverables/tool-present/tests/present.spec.ts index afbbf5958a..94fae8366f 100644 --- a/packages/deliverables/tool-present/tests/present.spec.ts +++ b/packages/deliverables/tool-present/tests/present.spec.ts @@ -62,7 +62,7 @@ async function setup() { await ctx.plugin(AgentRegistry) await ctx.plugin(LocalFileSystem, { cwd: root }) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) const fiber = ctx.plugin(Present, { maxFiles: 2 }) await fiber const owner = await agent(ctx, root) @@ -149,9 +149,12 @@ describe('present', () => { }) -it('validates deployment limits before registering the tool', () => { +it('validates deployment limits before registering the tool', async () => { + const ctx = new Context() + cleanups.push(() => ctx.fiber.dispose()) + await ctx.plugin(SessionProjectionRegistry) for (const config of [{ maxFiles: 0 }, { maxFiles: 1.5 }, { maxFiles: Number.POSITIVE_INFINITY }]) { - expect(() => { Present.apply(new Context(), config) }).toThrow('positive integer maxFiles') + await expect(Present.apply(ctx, config)).rejects.toThrow('positive integer maxFiles') } }) diff --git a/packages/experimental/agent-team/README.i18n.yaml b/packages/experimental/agent-team/README.i18n.yaml index e4f3b0666e..0dc6d22bb4 100644 --- a/packages/experimental/agent-team/README.i18n.yaml +++ b/packages/experimental/agent-team/README.i18n.yaml @@ -63,14 +63,14 @@ en: 6922e307e18696b4 zh: 083f4d95587a844b /deepseek-ai-dsh-experimental-agent-team/understand-the-implementation/disposal: - en: a12fb358183cc6d8 - zh: 7fdb0434d3c407fe + en: f32086839d37c9a6 + zh: b40ab7140b314afb /deepseek-ai-dsh-experimental-agent-team/further-exploration: en: 020d07f96ef2077d zh: 9e771d38a02048e1 /deepseek-ai-dsh-experimental-agent-team/further-exploration/browser-projection: - en: 4b3033806e6db528 - zh: 53e0dfef7309840e + en: 5dfc9113cabec7cd + zh: 22eaee731ca3f1b3 /deepseek-ai-dsh-experimental-agent-team/model-experience: en: 215e7ba838619b7b zh: a311da3843709f90 diff --git a/packages/experimental/agent-team/README.md b/packages/experimental/agent-team/README.md index 2d8abc7409..40872142cb 100644 --- a/packages/experimental/agent-team/README.md +++ b/packages/experimental/agent-team/README.md @@ -149,7 +149,7 @@ Mailbox projection and checkpoint admission preserve every decoded JSON field of ### Disposal -The runtime lifecycle tracks sends and complete creation transactions in one operation set. Disposal closes admission, aborts and awaits those operations, then releases the roster’s live children and descendants. Non-Team children remain untouched. Admitted operations share one `disposalTimeoutMs` settlement deadline; each subsequent Team child drain has its own deadline. This value is not a whole-service shutdown bound. Cleanup failures are reported. +The runtime lifecycle tracks sends and complete creation transactions in one operation set. Disposal closes admission, aborts and awaits those operations, then releases the roster’s live children and descendants. Non-Team children remain untouched. Admitted operations share one `disposalTimeoutMs` settlement deadline; each subsequent Team child drain has its own deadline. This value is not a whole-service shutdown bound. Cleanup failures are reported. Team shutdown is independent of projection unregistration; its retained reader follows Session events while admitted work settles. @@ -171,7 +171,7 @@ Read these pages when the package-level contract is not enough. They move from t ### Browser projection -The `agentTeam` Session projection publishes the Lead Session's durable roster identities and phases, member errors, non-deleted task views, and any `failure` beside the last valid state. Its `apply` replaces only the touched collection; mailbox-only changes retain the client view reference and produce no frame. The [subsystem reference](../../../docs/subsystems/agent-team.md#web-projection) defines the wire types. +The `agentTeam` Session projection publishes the Lead Session's durable roster identities and phases, member errors, non-deleted task views, and any `failure` beside the last valid state. Its `apply` replaces only the touched collection; mailbox-only changes retain the client view reference and produce no frame. The [subsystem reference](../../../docs/subsystems/agent-team.md#web-projection) defines the wire types. A failed projection rejects subsequent Team reads and mutations without appending further Team events. The [Web UI](../client-ui-agent-team/README.md) reads the shared Session projections and overlays activity from Session status. Task creation and updates belong to Team agents through the service and model tools. The `./client` export supplies browser-safe roster, task, and projection types. diff --git a/packages/experimental/agent-team/README.zh.md b/packages/experimental/agent-team/README.zh.md index e60d93b255..815d41eb2b 100644 --- a/packages/experimental/agent-team/README.zh.md +++ b/packages/experimental/agent-team/README.zh.md @@ -149,7 +149,7 @@ Mailbox 投影与 checkpoint 准入保留本地声明的校验器之外获准内 ### Dispose -运行时生命周期用同一个操作集合跟踪发送和完整创建事务。dispose 关闭准入,中止并等待这些操作,然后释放 roster 中的 live child 及其后代。非 Team child 不受影响。已获准操作共享一个 `disposalTimeoutMs` 等待期限;随后每个 Team 的 child drain 各有独立期限。此值不是整个服务关闭的总时限。清理失败会明确报告。 +运行时生命周期用同一个操作集合跟踪发送和完整创建事务。dispose 关闭准入,中止并等待这些操作,然后释放 roster 中的 live child 及其后代。非 Team child 不受影响。已获准操作共享一个 `disposalTimeoutMs` 等待期限;随后每个 Team 的 child drain 各有独立期限。此值不是整个服务关闭的总时限。清理失败会明确报告。 Team 停机与投影注销独立;在已获准操作收尾期间,保留的 reader 仍会根据 Session 事件更新读取结果。 @@ -171,7 +171,7 @@ Mailbox 投影与 checkpoint 准入保留本地声明的校验器之外获准内 ### 浏览器投影 -`agentTeam` Session 投影发布 Lead Session 的持久成员身份与阶段、成员错误、未删除任务视图,以及最后有效状态旁的 `failure`。其 `apply` 只替换被触及的集合;仅邮箱的变化保留客户端视图引用,不产生 frame。[子系统参考](../../../docs/subsystems/agent-team.zh.md#web-projection) 定义传输类型。 +`agentTeam` Session 投影发布 Lead Session 的持久成员身份与阶段、成员错误、未删除任务视图,以及最后有效状态旁的 `failure`。其 `apply` 只替换被触及的集合;仅邮箱的变化保留客户端视图引用,不产生 frame。[子系统参考](../../../docs/subsystems/agent-team.zh.md#web-projection) 定义传输类型。 投影失败后,后续 Team 读取和修改均报错,不再追加 Team 事件。 [Web UI](../client-ui-agent-team/README.zh.md) 读取共享 Session 投影,并从 Session 状态叠加活动信息。任务创建与更新由 Team agent 通过服务和模型工具完成。`./client` 导出可供浏览器使用的 roster、任务与投影类型。 diff --git a/packages/experimental/agent-team/src/index.ts b/packages/experimental/agent-team/src/index.ts index 2d35933776..fd1ea84a29 100644 --- a/packages/experimental/agent-team/src/index.ts +++ b/packages/experimental/agent-team/src/index.ts @@ -9,8 +9,8 @@ import { steerHostSubagentPrompt } from '@deepseek-ai/dsh-subagent/internal' import { TeamActivity } from './activity.ts' import { errorMessage, TeamError } from './error.ts' import { TeamJournal } from './journal.ts' -import { TeamRuntimeLifecycle } from './lifecycle.ts' import { teamProjectionDefinition } from './projection.ts' +import { TeamRuntimeLifecycle } from './lifecycle.ts' import { resolveActiveMember, TeamRoster } from './roster.ts' import type { TeamMembership } from './roster.ts' import { TeamTaskBoard } from './task-board.ts' @@ -68,9 +68,9 @@ export class TeamService extends Service { private readonly activity: TeamActivity private readonly lifecycle: TeamRuntimeLifecycle - private readonly journal: TeamJournal - private readonly roster: TeamRoster - private readonly tasks: TeamTaskBoard + private journal!: TeamJournal + private roster!: TeamRoster + private tasks!: TeamTaskBoard constructor(ctx: Context, config: Config = {}) { super(ctx, 'agentTeams') @@ -86,25 +86,21 @@ export class TeamService extends Service { this.activity = new TeamActivity() this.lifecycle = new TeamRuntimeLifecycle(this.config.disposalTimeoutMs) - this.journal = new TeamJournal(ctx, (root) => { this.activity.notify(TeamId(root.id)) }) + } + + protected async [Service.init](): Promise { + const ctx = this.ctx + const projection = await ctx.sessionProjections.register(teamProjectionDefinition) + this.journal = new TeamJournal(ctx, projection, (root) => { this.activity.notify(TeamId(root.id)) }) this.roster = new TeamRoster(ctx, this.journal, this.lifecycle, this.config.maxMembers) this.tasks = new TeamTaskBoard(this.journal, this.config.maxTasks) + ctx.effect(() => () => this.disposeRuntime(), 'agentTeams.runtimeLifecycle()') ctx.on('agent/created', ({ agent }) => { this.scheduleRecovery(agent) }) ctx.on('agent/status', ({ agent }) => { const membership = this.roster.tryMembership(agent) if (membership !== undefined) this.activity.notify(membership.id) }) - ctx.effect(() => { - const disposeProjection = ctx.root.sessionProjections.register(teamProjectionDefinition) - return async () => { - try { - await this.disposeRuntime() - } finally { - disposeProjection() - } - } - }, 'agentTeams.runtimeLifecycle()') for (const agent of ctx.agents.list()) this.scheduleRecovery(agent) } diff --git a/packages/experimental/agent-team/src/journal.ts b/packages/experimental/agent-team/src/journal.ts index f69ac376b8..6a6002bb72 100644 --- a/packages/experimental/agent-team/src/journal.ts +++ b/packages/experimental/agent-team/src/journal.ts @@ -1,9 +1,10 @@ /** Serialized Team transactions over the exact live Lead Session log. */ import type { Agent } from '@deepseek-ai/dsh-agent' +import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection' import type { Context } from '@deepseek-ai/cordis' import type { SessionEventMap, SessionId } from '@deepseek-ai/dsh-session' -import type { TeamEventType, TeamState } from './projection.ts' +import type { TeamEventType, TeamProjectionState, TeamState } from './projection.ts' type AppendTeamEvent = (type: T, data: SessionEventMap[T]) => void type MutableTeamEventType = 'team/member' | 'team/task' @@ -14,10 +15,12 @@ export class TeamJournal { /** * @param ctx - Team service context with the injected Session service. + * @param projection - Team state registration owned by the Team service. * @param onCommit - synchronous notification after the Team event flush succeeds. */ constructor( private readonly ctx: Context, + private readonly projection: ProjectionHandle, private readonly onCommit: (root: Agent) => void, ) {} @@ -25,10 +28,10 @@ export class TeamJournal { * Read authoritative Team state for one exact live Lead. * @param root - exact live Team Lead. * @returns current projected state selected by the Lead Team id. + * @throws when the durable Team projection has failed. */ state(root: Agent): TeamState { - const projection = this.ctx.sessionProjections.stateOf(root.session, 'agentTeam') - if (projection === undefined) throw new Error('Agent Teams projection is not registered') + const projection = this.projection.stateOf(root.session) if (projection.failure !== undefined) throw new Error(projection.failure) return projection } diff --git a/packages/experimental/agent-team/tests/projection-events.spec.ts b/packages/experimental/agent-team/tests/projection-events.spec.ts index 2266b41483..2de9c75ff4 100644 --- a/packages/experimental/agent-team/tests/projection-events.spec.ts +++ b/packages/experimental/agent-team/tests/projection-events.spec.ts @@ -90,7 +90,7 @@ describe('Agent Teams projection events', () => { onTestFinished(() => ctx.fiber.dispose()) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - ctx.effect(() => ctx.sessionProjections.register(teamProjectionDefinition)) + await ctx.sessionProjections.register(teamProjectionDefinition) const session = ctx.sessions.create(ROOT) const restore = (val: unknown) => ctx.sessionProjections.restore({ agentTeam: { ver: teamProjectionDefinition.stateVersion, seq: -1, val }, @@ -359,7 +359,7 @@ describe('Agent Teams projection events', () => { const registry = ctx.plugin(SessionProjectionRegistry) try { await registry - ctx.sessionProjections.register(teamProjectionDefinition) + await ctx.sessionProjections.register(teamProjectionDefinition) const extension = JSON.parse('{"type":"plugin/custom","__proto__":{"saved":true}}') as ContentBlock const queued = event('team/message/queued', { version: 2, teamId: TEAM, message: message({ content: [extension] }), @@ -546,7 +546,7 @@ describe('Agent Teams projection events', () => { onTestFinished(() => ctx.fiber.dispose()) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - ctx.effect(() => ctx.sessionProjections.register(teamProjectionDefinition)) + await ctx.sessionProjections.register(teamProjectionDefinition) const published: Array<{ session: SessionId; key: string; value: unknown }> = [] ctx.sessionProjections.onChanged((session, key, value) => { published.push({ session: session.id, key, value }) diff --git a/packages/experimental/agent-team/tests/team.spec.ts b/packages/experimental/agent-team/tests/team.spec.ts index ec79684726..671cb281f7 100644 --- a/packages/experimental/agent-team/tests/team.spec.ts +++ b/packages/experimental/agent-team/tests/team.spec.ts @@ -1,3 +1,4 @@ +import { Service } from '@deepseek-ai/cordis' import { afterEach, describe, expect, it, vi } from 'vitest' import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' @@ -141,19 +142,20 @@ async function waitRunning(ctx: Context, id: SessionId): Promise { } describe('Team identity and provisioning', () => { - it('rejects missing and failed authoritative Team projections', async () => { - const first = await setup([]) - const journal = teamInternals(first.ctx).journal - const stateOf = first.ctx.sessionProjections.stateOf.bind(first.ctx.sessionProjections) - const stateOfSpy = vi.spyOn(first.ctx.sessionProjections, 'stateOf').mockImplementation((session, key) => ( - key === 'agentTeam' ? undefined : stateOf(session, key) - )) - expect(() => journal.state(first.lead)).toThrow('Agent Teams projection is not registered') - stateOfSpy.mockImplementation((session, key) => key === 'agentTeam' - ? { ...teamProjectionDefinition.init(session.header), failure: 'failed Team projection' } - : stateOf(session, key)) - expect(() => journal.state(first.lead)).toThrow('failed Team projection') - stateOfSpy.mockRestore() + it('rejects reads and mutations after a Team projection fails', async () => { + const { ctx, lead } = await setup([]) + lead.session.append('team/message/delivered', { + version: 2, + teamId: TeamId(lead.id), + messageId: TeamMessageId('unqueued-message'), + targetId: lead.id, + }) + + expect(() => ctx.agentTeams.listMembers(lead)).toThrow('delivered before queueing') + const eventCount = lead.session.snapshotEvents().length + await expect(ctx.agentTeams.createTask(lead, { subject: 'blocked', description: 'blocked' })) + .rejects.toThrow('delivered before queueing') + expect(lead.session.snapshotEvents()).toHaveLength(eventCount) }) it('rejects deployment limits that are not positive safe integers', async () => { @@ -181,7 +183,11 @@ describe('Team identity and provisioning', () => { await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(SubagentService) const lead = await ctx.agentLoop.create(SessionId('preexisting-lead'), {}) - const service = new TeamService(ctx) + class DirectTeamService extends TeamService { + initialize(): Promise { return this[Service.init]() } + } + const service = new DirectTeamService(ctx) + await service.initialize() expect(service.listMembers(lead)).toEqual([expect.objectContaining({ name: 'lead', @@ -1147,6 +1153,32 @@ describe('Team direct messages and waiting', () => { expect(ctx.get('agentTeams')).toBeUndefined() }) + it.each([false, true])('settles admitted work after projection unregistration (failure: %s)', async (fails) => { + const { ctx, lead, teamFiber } = await setup([]) + const internal = teamInternals(ctx) + const state = internal.journal.state(lead) + const finish = Promise.withResolvers() + let readDuringShutdown = false + void internal.lifecycle.track(finish.promise.then(() => { + expect(internal.journal.state(lead)).toBe(state) + readDuringShutdown = true + if (fails) throw new Error('admitted operation failed during shutdown') + })) + const disposal = teamFiber.dispose() + try { + await vi.waitFor(() => { + expect(internal.lifecycle.disposed).toBe(true) + expect(ctx.sessionProjections.checkpoint(lead.session)).not.toHaveProperty('agentTeam') + }) + expect(readDuringShutdown).toBe(false) + } finally { + finish.resolve(undefined) + await disposal + } + expect(readDuringShutdown).toBe(true) + expect(ctx.get('agentTeams')).toBeUndefined() + }) + it('closes creation admission and drains an in-flight spawn before unload completes', async () => { const { ctx, lead, teamFiber } = await setup(['hang']) const service = ctx.agentTeams diff --git a/packages/experimental/claude-code-mods/README.i18n.yaml b/packages/experimental/claude-code-mods/README.i18n.yaml index aa0a5e6116..68c4c306d6 100644 --- a/packages/experimental/claude-code-mods/README.i18n.yaml +++ b/packages/experimental/claude-code-mods/README.i18n.yaml @@ -9,8 +9,8 @@ en: bf54b493565f1624 zh: 6f28a518f7b0dea8 /deepseek-ai-dsh-experimental-claude-code-mods/summary: - en: 4ddb71e280b6b328 - zh: e4775593fd7f232c + en: e21a08cf7ad99a77 + zh: df7634372a42d9b2 /deepseek-ai-dsh-experimental-claude-code-mods/table-of-contents: en: d152484eb41ac6b4 zh: 09388d293f9be9cb diff --git a/packages/experimental/claude-code-mods/README.md b/packages/experimental/claude-code-mods/README.md index 4fd02d3ded..52dc778067 100644 --- a/packages/experimental/claude-code-mods/README.md +++ b/packages/experimental/claude-code-mods/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -Run [Claude Code mods](https://code.claude.com/docs/en/plugins/mods/overview) inside agent runs: wrap a mod's `register(on, options)` with `defineMod`, mount it as a plugin after this bridge, and its hooks guard tool calls, rewrite prompts, add commands and tools, read session facts, and draw a band above the prompt through the same `$`, `e`, `next` chain. The bridge requires `dsh-working-directory`, and each `$` call rides a composed harness service. It is an alpha interface-compatibility demonstration: an unserved event is reported at load, an unserved `$` member fails naming the gap, and [the compatibility page](../../../docs/subsystems/claude-code-mods.md) lists every difference. +Run [Claude Code mods](https://code.claude.com/docs/en/plugins/mods/overview) inside agent runs: wrap a mod's `register(on, options)` with `defineMod`, mount it as a plugin after this bridge, and its hooks guard tool calls, rewrite prompts, add commands and tools, read session facts, and draw a band above the prompt through the same `$`, `e`, `next` chain. The bridge requires `dsh-working-directory` and `dsh-session-projection`, and each `$` call rides a composed harness service. It is an alpha interface-compatibility demonstration: an unserved event is reported at load, an unserved `$` member fails naming the gap, and [the compatibility page](../../../docs/subsystems/claude-code-mods.md) lists every difference. ## Table of Contents diff --git a/packages/experimental/claude-code-mods/README.zh.md b/packages/experimental/claude-code-mods/README.zh.md index 1a2f909626..6b11df8418 100644 --- a/packages/experimental/claude-code-mods/README.zh.md +++ b/packages/experimental/claude-code-mods/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -在智能体运行中执行 [Claude Code 模组](https://code.claude.com/docs/en/plugins/mods/overview):用 `defineMod` 包装模组的 `register(on, options)`,在本桥接之后作为插件挂载,其钩子即可通过同一套 `$`、`e`、`next` 链守卫工具调用、改写提示词、添加命令与工具、读取会话事实,并在提示框上方绘制一条横幅。桥接需要 `dsh-working-directory`,每次 `$` 调用都落在一个已组合的 harness 服务上。它是 alpha 阶段的接口兼容性演示:未服务的事件在加载时报告,未服务的 `$` 成员以指明缺口的消息失败,[兼容性页面](../../../docs/subsystems/claude-code-mods.zh.md) 列出了全部差异。 +在智能体运行中执行 [Claude Code 模组](https://code.claude.com/docs/en/plugins/mods/overview):用 `defineMod` 包装模组的 `register(on, options)`,在本桥接之后作为插件挂载,其钩子即可通过同一套 `$`、`e`、`next` 链守卫工具调用、改写提示词、添加命令与工具、读取会话事实,并在提示框上方绘制一条横幅。桥接需要 `dsh-working-directory` 和 `dsh-session-projection`,每次 `$` 调用都落在一个已组合的 harness 服务上。它是 alpha 阶段的接口兼容性演示:未服务的事件在加载时报告,未服务的 `$` 成员以指明缺口的消息失败,[兼容性页面](../../../docs/subsystems/claude-code-mods.zh.md) 列出了全部差异。 ## 目录 diff --git a/packages/experimental/claude-code-mods/src/host-ops.ts b/packages/experimental/claude-code-mods/src/host-ops.ts index 3d6363b463..7b61a18a4c 100644 --- a/packages/experimental/claude-code-mods/src/host-ops.ts +++ b/packages/experimental/claude-code-mods/src/host-ops.ts @@ -9,20 +9,17 @@ */ import type { Context } from '@deepseek-ai/cordis' -import type { Agent } from '@deepseek-ai/dsh-agent' -// Type-only: activate the `ctx.get('subprocess')` and `ctx.get('sessionProjections')` Context declarations. -import type {} from '@deepseek-ai/dsh-subprocess' import type {} from '@deepseek-ai/dsh-session-projection' -import type { SessionProjectionStateMap } from '@deepseek-ai/dsh-session-projection/types' +import { turnBoundaryProjectionDefinition, type Agent } from '@deepseek-ai/dsh-agent' +// Type-only: activate the `ctx.get('subprocess')` Context declaration. +import type {} from '@deepseek-ai/dsh-subprocess' import type { CommandDefinition } from '@deepseek-ai/dsh-commands' import type { FileSystem, FsTarget } from '@deepseek-ai/dsh-fs' import { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm' import type { ContentBlock, Message } from '@deepseek-ai/dsh-llm' import { scopeOf } from '@deepseek-ai/dsh-scope' -import type { Session } from '@deepseek-ai/dsh-session' import type { WorkingDirectoryService } from '@deepseek-ai/dsh-working-directory' -// Type-only: merges the token-meter projection states into SessionProjectionStateMap. -import type {} from '@deepseek-ai/dsh-token-meter' +import { contextPressureProjectionDefinition } from '@deepseek-ai/dsh-token-meter' import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain' import type { Domain } from '@deepseek-ai/dsh-storage-domain' import { assertObjectJsonSchema } from '@deepseek-ai/dsh-tools' @@ -161,12 +158,14 @@ function sizeOf(info: { size?: number } | undefined): number { } /** - * Build the op table over the composed harness services. + * Build the op table with the required session projection registry and composed harness services. * @param options - the context, aliases, limits, and registration tracking. - * @returns engine behaviors by event name. + * @returns engine behaviors by event name, after both session projections are registered. */ -export function createHostOps(options: HostOpsOptions): OpTable { +export async function createHostOps(options: HostOpsOptions): Promise> { const { ctx, aliases } = options + const turnBoundary = await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + const contextPressure = await ctx.sessionProjections.register(contextPressureProjectionDefinition) let storeDomain: Promise> | undefined function store(): Promise> { @@ -225,11 +224,6 @@ export function createHostOps(options: HostOpsOptions): OpTable { if (agent !== undefined) options.redraw?.(agent.session.id) } - /** One projection's state, or undefined when the registry or the projection's owning plugin is not composed. */ - function projection(session: Session, key: K): SessionProjectionStateMap[K] | undefined { - return ctx.get('sessionProjections')?.stateOf(session, key) - } - return { // ---- ui: no mod drawing surface exists; log-style calls reach the host log ---- 'ui.log': (input, { mod }) => { @@ -411,16 +405,14 @@ export function createHostOps(options: HostOpsOptions): OpTable { }, 'session.turns': (_input, context) => { const agent = requireAgent(context, 'session.turns') - const boundary = projection(agent.session, 'turnBoundary') - if (boundary === undefined) throw new Error('$.session.turns needs the turnBoundary projection, which this deployment did not compose') - return boundary.lastTurn + return turnBoundary.stateOf(agent.session).lastTurn }, 'session.messages': (_input, context) => sessionMessages(requireAgent(context, 'session.messages').session.deriveMessages(), aliases), 'session.usage': (_input, context): SessionUsage => { const agent = requireAgent(context, 'session.usage') - const pressure = projection(agent.session, 'contextPressure') - const tokens = pressure?.pressureTokens - const window = pressure?.contextWindow ?? 0 + const pressure = contextPressure.stateOf(agent.session) + const tokens = pressure.pressureTokens + const window = pressure.contextWindow ?? 0 return { startedAt: agent.session.header.createdAt, context: { diff --git a/packages/experimental/claude-code-mods/src/index.ts b/packages/experimental/claude-code-mods/src/index.ts index 1c97870b73..f715a74d7f 100644 --- a/packages/experimental/claude-code-mods/src/index.ts +++ b/packages/experimental/claude-code-mods/src/index.ts @@ -10,7 +10,7 @@ * @module @deepseek-ai/dsh-experimental-claude-code-mods */ -import type { Context } from '@deepseek-ai/cordis' +import { Service, type Context } from '@deepseek-ai/cordis' import type {} from '@deepseek-ai/dsh-working-directory' import z from '@deepseek-ai/schemastery' import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent' @@ -208,22 +208,25 @@ export class ClaudeCodeMods extends TypertRemoteService { bandRows: z.number().default(10), }) - // Session initialization requires its directory owner; other services are read on demand. - static inject = ['workingDirectory'] + // Session initialization requires its directory owner and projection registry; other services are read on demand. + static inject = ['workingDirectory', 'sessionProjections'] - private readonly engine: ModsEngine + private engine!: ModsEngine private readonly registrations = new Map void>>() - private readonly surfaces: SurfaceTable + private surfaces!: SurfaceTable - constructor(ctx: Context, config: Config) { + constructor(ctx: Context, private readonly config: Config) { super(ctx, 'claudeCodeMods', { namespace: 'claudeCodeMods' }) + for (const key of ['hookTimeoutMs', 'catchTimeoutMs', 'processTimeoutMs', 'bandColumns', 'bandRows'] as const) { + const value = config[key] + if (value !== undefined) assertPositive(key, value) + } + } + + protected async [Service.init](): Promise { + const { ctx, config } = this // The schema supplies every default; a direct construction passes the fields it needs. const { hookTimeoutMs = 10_000, catchTimeoutMs = 1_000, processTimeoutMs = 30_000, bandColumns = 120, bandRows = 10 } = config - assertPositive('hookTimeoutMs', hookTimeoutMs) - assertPositive('catchTimeoutMs', catchTimeoutMs) - assertPositive('processTimeoutMs', processTimeoutMs) - assertPositive('bandColumns', bandColumns) - assertPositive('bandRows', bandRows) const aliases = createToolNameAliases(config.toolAliases) const callOrigins = new Map() const modCommands = new Set() @@ -259,7 +262,7 @@ export class ClaudeCodeMods extends TypertRemoteService { }, 0).unref() } const modSubmissions = new Map() - const ops = createHostOps({ + const ops = await createHostOps({ ctx, aliases, processTimeoutMs, registrations: this.registrations, callOrigins, modCommands, modTools, redraw, submitted: (messageId, mod) => { modSubmissions.set(messageId, mod) }, }) diff --git a/packages/experimental/claude-code-mods/tests/bridge.spec.ts b/packages/experimental/claude-code-mods/tests/bridge.spec.ts index 4ee329ce99..7243febfc6 100644 --- a/packages/experimental/claude-code-mods/tests/bridge.spec.ts +++ b/packages/experimental/claude-code-mods/tests/bridge.spec.ts @@ -535,7 +535,7 @@ describe('the mods API over harness services', () => { expect(out.session).toEqual({ id: 'a1', cwd: h.workspace, root: h.workspace, model: 'mock', turns: 1, version: MODS_API_VERSION, - usage: { startedAt: agent.session.header.createdAt, context: { window: 0 }, rateLimits: [] }, + usage: { startedAt: agent.session.header.createdAt, context: { tokens: 10, window: 0 }, rateLimits: [] }, messages: [ { role: 'user', text: 'hello', toolUses: [] }, { role: 'user', text: `Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: ${JSON.stringify(h.workspace)}.`, toolUses: [] }, @@ -951,7 +951,7 @@ describe('loading diagnostics and configuration', () => { }) it('is a Service plugin the Loader keeps by its default export', () => { - expect(ClaudeCodeMods.inject).toEqual(['workingDirectory']) + expect(ClaudeCodeMods.inject).toEqual(['workingDirectory', 'sessionProjections']) const loader = Object.create(Loader.prototype) as Loader const unwrapped: unknown = loader.unwrapExports({ default: ClaudeCodeMods }) expect(unwrapped).toBe(ClaudeCodeMods) diff --git a/packages/experimental/claude-code-mods/tests/define-mod.spec.ts b/packages/experimental/claude-code-mods/tests/define-mod.spec.ts index d4bc93702d..987c2db123 100644 --- a/packages/experimental/claude-code-mods/tests/define-mod.spec.ts +++ b/packages/experimental/claude-code-mods/tests/define-mod.spec.ts @@ -1,5 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { Context, type Fiber } from '@deepseek-ai/cordis' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import { provideWorkingDirectoryFixture } from '@deepseek-ai/dsh-agent-loop-testkit' import ClaudeCodeMods, { defineMod } from '../src/index.ts' @@ -32,7 +33,10 @@ describe('defineMod', () => { const ctx = new Context() fibers.push(ctx.fiber) provideWorkingDirectoryFixture(ctx) - await ctx.plugin(ClaudeCodeMods, {}) + const bridge = ctx.plugin(ClaudeCodeMods, {}) + expect(ctx.get('claudeCodeMods')).toBeUndefined() + await ctx.plugin(SessionProjectionRegistry) + await bridge const fiber = await ctx.plugin(plugin, { history: 3 }) await fiber.await() expect(seen).toEqual([{ history: 3, unit: 'tokens' }]) @@ -52,6 +56,7 @@ describe('defineMod', () => { const ctx = new Context() fibers.push(ctx.fiber) provideWorkingDirectoryFixture(ctx) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(ClaudeCodeMods, {}) const fiber = ctx.plugin(slow, {}) await vi.waitFor(() => { expect(release).toBeDefined() }) @@ -69,6 +74,7 @@ describe('defineMod', () => { const ctx = new Context() fibers.push(ctx.fiber) provideWorkingDirectoryFixture(ctx) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(ClaudeCodeMods, {}) const fiber = await ctx.plugin(bare, {}) await fiber.await() diff --git a/packages/experimental/claude-code-mods/tests/host-ops.spec.ts b/packages/experimental/claude-code-mods/tests/host-ops.spec.ts index 15400980f4..750a68adf9 100644 --- a/packages/experimental/claude-code-mods/tests/host-ops.spec.ts +++ b/packages/experimental/claude-code-mods/tests/host-ops.spec.ts @@ -6,7 +6,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import type { Agent } from '@deepseek-ai/dsh-agent' import type { Message } from '@deepseek-ai/dsh-llm' -import { SessionId } from '@deepseek-ai/dsh-session' +import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import { mountAgentLoopTestDependencies, mountAgentLoopTestHarness, provideWorkingDirectoryFixture } from '@deepseek-ai/dsh-agent-loop-testkit' import CommandRuntime from '@deepseek-ai/dsh-commands' import LocalFileSystem from '@deepseek-ai/dsh-fs-local' @@ -34,10 +35,11 @@ afterEach(async () => { const mod: LoadedMod = { name: 'unit-mod', version: undefined, root: '/mods/unit-mod', options: {}, order: 0 } -function setup(ctx = new Context()) { +async function setup(ctx = new Context()) { contexts.push(ctx) + if (ctx.get('sessionProjections') === undefined) await ctx.plugin(SessionProjectionRegistry) const registrations = new Map void>>() - const ops = createHostOps({ + const ops = await createHostOps({ ctx, aliases: createToolNameAliases(), processTimeoutMs: 5_000, @@ -72,12 +74,16 @@ function fakeAgent(ctx: Context, overrides: { cwd?: string; model?: string; requ id: 'fake', ctx, options: overrides.model === undefined ? {} : { model: overrides.model }, - session: { - id: 'fake', - header: { id: 'fake', createdAt: 1, ...overrides.cwd === undefined ? {} : { cwd: overrides.cwd } }, + session: Object.assign(Session.create(SessionId('fake'), [], { + version: SESSION_FORMAT_VERSION, + id: SessionId('fake'), + createdAt: 1, + isSeeded: false, + ...overrides.cwd === undefined ? {} : { cwd: overrides.cwd }, + }), { requestHeader: () => overrides.requestModel === undefined ? undefined : { config: { provider: 'p', model: overrides.requestModel } }, deriveMessages: (): Message[] => [], - }, + }), } return facts as Agent } @@ -100,7 +106,7 @@ describe('values', () => { describe('services a deployment did not compose', () => { it('names the missing service in each call', async () => { - const { call, ctx } = setup() + const { call, ctx } = await setup() const agent = fakeAgent(ctx) await expect(call('ui.ask', { question: 'q' }, agent)).rejects.toThrow(/dsh-user-questions/) await expect(call('command.register', { name: 'x', description: 'd' }, agent)).rejects.toThrow(/dsh-commands/) @@ -113,7 +119,7 @@ describe('services a deployment did not compose', () => { await expect(call('store.get', { key: 'k' })).rejects.toThrow(/dsh-storage-domain/) await expect(call('fs.read', { path: 'x' })).rejects.toThrow(/dsh-fs/) await expect(call('process.run', { argv: ['ls'] })).rejects.toThrow(/dsh-subprocess/) - await expect(call('session.turns', {}, agent)).rejects.toThrow(/turnBoundary projection/) + await expect(call('session.turns', {}, agent)).resolves.toBe(0) // Without a drawn surface the ui calls that would redraw the band are plain no-ops. await expect(call('ui.invalidate', {}, agent)).resolves.toBeUndefined() await expect(call('ui.invalidate', {})).resolves.toBeUndefined() @@ -123,7 +129,7 @@ describe('services a deployment did not compose', () => { }) it('requires a session for session-bound calls and validates mod input', async () => { - const { call } = setup() + const { call } = await setup() for (const op of ['ui.ask', 'command.run', 'command.list', 'prompt.submit', 'session.id', 'session.cwd', 'session.root', 'session.model', 'session.turns', 'session.messages', 'session.usage']) { await expect(call(op, {})).rejects.toThrow(`$.${op} needs a session, and this event has none`) } @@ -137,7 +143,7 @@ describe('services a deployment did not compose', () => { describe('session facts from the agent', () => { it('falls back from the logged route to the agent option to an empty model, and from the session cwd to the process cwd', async () => { - const { call, ctx } = setup() + const { call, ctx } = await setup() expect(await call('session.model', {}, fakeAgent(ctx, { requestModel: 'logged', model: 'opt' }))).toBe('logged') expect(await call('session.model', {}, fakeAgent(ctx, { model: 'opt' }))).toBe('opt') expect(await call('session.model', {}, fakeAgent(ctx))).toBe('') @@ -157,7 +163,7 @@ describe('files through the filesystem seam', () => { dirs.push(root) const ctx = new Context() await ctx.plugin(LocalFileSystem, { cwd: root }) - const { call } = setup(ctx) + const { call } = await setup(ctx) provideWorkingDirectoryFixture(ctx) writeFileSync(join(root, 'small.txt'), 'small') writeFileSync(join(root, 'big.txt'), Buffer.alloc(FS_MAX_BYTES + 1, 97)) @@ -182,7 +188,7 @@ describe('processes through the subprocess seam', () => { it('runs argv with an explicit cwd and env, and reports a signal-terminated child', async () => { const ctx = new Context() await ctx.plugin(LocalSubprocessRuntime) - const { call } = setup(ctx) + const { call } = await setup(ctx) const cwd = mkdtempSync(join(tmpdir(), 'dsh-cc-mods-proc-')) dirs.push(cwd) const run = await call('process.run', { @@ -220,7 +226,7 @@ describe('the store limit', () => { close: () => Promise.resolve(), } ctx.provide('storageDomain', { open: () => Promise.resolve(domain) }) - const { call } = setup(ctx) + const { call } = await setup(ctx) expect(await call('store.keys', {})).toEqual([]) await call('store.set', { key: 'ok', value: 1 }) expect(await call('store.keys', {})).toEqual(['ok']) @@ -247,7 +253,7 @@ describe('registrations outside a session', () => { const ctx = new Context() await mountAgentLoopTestDependencies(ctx) await ctx.plugin(CommandRuntime) - const { call, registrations, engine } = setup(ctx) + const { call, registrations, engine } = await setup(ctx) await call('command.register', { name: 'Global', description: 'global command' }) await call('tool.register', { name: 'global_tool', description: 'global tool' }) expect(ctx.tools.schemas().map(tool => tool.name)).toContain('mcp__unit-mod__global_tool') @@ -264,7 +270,7 @@ describe('registrations outside a session', () => { describe('ui.log sinks', () => { it('writes transcript lines at info and debug lines at debug', async () => { - const { call, ctx } = setup() + const { call, ctx } = await setup() const info = vi.fn() const debug = vi.fn() ctx.logger.info = info as never @@ -293,7 +299,7 @@ describe('the committed directory owner', () => { await mountAgentLoopTestDependencies(ctx, { workingDirectory: true }) const harness = await mountAgentLoopTestHarness(ctx) await ctx.plugin(LocalSubprocessRuntime) - const { call } = setup(ctx) + const { call } = await setup(ctx) const agent = await harness.create(SessionId('directory-owner'), {}, { cwd: origin }) await ctx.workingDirectory.set(agent, current) @@ -322,7 +328,7 @@ describe('the committed directory owner', () => { }) it('fails explicitly when a session-bound operation has no directory owner', async () => { - const { ctx, call } = setup() + const { ctx, call } = await setup() await ctx.plugin(LocalFileSystem) await ctx.plugin(LocalSubprocessRuntime) const agent = fakeAgent(ctx) diff --git a/packages/experimental/client-ui-cot-translation/tests/activation.host.spec.ts b/packages/experimental/client-ui-cot-translation/tests/activation.host.spec.ts index 534d6d3408..5ed24ad300 100644 --- a/packages/experimental/client-ui-cot-translation/tests/activation.host.spec.ts +++ b/packages/experimental/client-ui-cot-translation/tests/activation.host.spec.ts @@ -6,7 +6,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { Context } from '@deepseek-ai/cordis' import { mountAgentLoopTestDependencies, mountAgentLoopTestHarness } from '@deepseek-ai/dsh-agent-loop-testkit' -import SessionController from '@deepseek-ai/dsh-api-session-controller' +import { createSessionTestController } from '../../../api/session-controller/tests/test-remote.ts' import Translator from '@deepseek-ai/dsh-experimental-translator' import { SESSION_FORMAT_VERSION, SessionId, SessionSeq } from '@deepseek-ai/dsh-session' import JsonlPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' @@ -66,7 +66,9 @@ async function fixture(origin?: 'subagent') { saveSelection: async () => {}, } as never) ctx.provide('fileUploads', { registerAgentResolver: () => () => {} } as never) - const sessions = new SessionController(ctx, { nativeOpen: false }) + const sessions = await createSessionTestController(ctx, { + nativeOpen: false, cwd: root, defaultModelSelection: () => ({ provider: 'unused', model: 'unused' }), + }) await ctx.plugin(Translator, Translator.Config({ bingEndpoint: `http://127.0.0.1:${address.port}/bing` })) const ui = ctx.plugin(CotTranslationController, {}) await ui diff --git a/packages/experimental/hooks-claude-code/src/index.ts b/packages/experimental/hooks-claude-code/src/index.ts index afbc60de08..1c79429b80 100644 --- a/packages/experimental/hooks-claude-code/src/index.ts +++ b/packages/experimental/hooks-claude-code/src/index.ts @@ -8,12 +8,13 @@ * @module @deepseek-ai/dsh-hooks-claude-code */ +import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent' import type {} from '@deepseek-ai/dsh-working-directory' import { readFileSync } from 'node:fs' import type { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' -import type { Agent, PreStepDecision, TurnBoundaryProjection } from '@deepseek-ai/dsh-agent' import type {} from '@deepseek-ai/dsh-session-projection' +import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent' import { createUserMessage } from '@deepseek-ai/dsh-llm' import type { ContextFormed } from '@deepseek-ai/dsh-llm' declare module '@deepseek-ai/dsh-llm' { @@ -43,10 +44,11 @@ import type { SubagentRunId } from '@deepseek-ai/dsh-subagent' import { parseClaudeCodeConfig, type ClaudeCodeHookConfig } from './config.ts' export const name = 'hooks-claude-code' -// `shell` runs hooks and `sessionProjections` supplies turn numbers; the rest -// are read opportunistically via ctx.get so a deployment can omit them. + export const inject = ['shell', 'sessionProjections', 'workingDirectory'] +// `shell` runs hooks and `sessionProjections` supplies turn numbers; the rest +// are read opportunistically via ctx.get so a deployment can omit them. /** Plugin config: where the CC hook config lives + substitution roots. */ export interface Config { /** @@ -99,7 +101,12 @@ function assertPositiveInteger(name: string, value: number): void { } } -export function apply(ctx: Context, config: Config): void { +/** Register the hooks-claude-code plugin after its projections are ready. + * @param ctx - plugin context. + * @param config - hook configuration path and execution limits. + * @returns when the plugin is ready. + */ +export async function apply(ctx: Context, config: Config): Promise { // Validate before config parsing so a bad value cannot be hidden by its early return. const stderrSummaryMaxChars = config.stderrSummaryMaxChars ?? DEFAULT_STDERR_SUMMARY_MAX_CHARS assertPositiveInteger('stderrSummaryMaxChars', stderrSummaryMaxChars) @@ -121,6 +128,8 @@ export function apply(ctx: Context, config: Config): void { return } + const turnBoundary = await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + // Emit-shaped points run detached, so track their chains; disposal aborts // active hooks and drains continuations before resolving. const detached = createDetachedRuns() @@ -242,7 +251,7 @@ export function apply(ctx: Context, config: Config): void { // --- PreToolUse → PreToolDecision. Matcher subject is the tool name. --- ctx.on('tools/pre-execute', async (exec, next): Promise => { - const turn = lastTurn(ctx, exec.agent) + const turn = lastTurn(exec.agent) const merged = await runPoint('PreToolUse', exec.name, preToolPayload(exec), { ...exec.agent ? { agent: exec.agent } : {}, turn, signal: exec.signal }) if (merged.decision === 'deny') return { kind: 'deny', reason: merged.reason ?? 'blocked by PreToolUse hook' } if (merged.decision === 'ask') return { kind: 'ask', ...merged.reason !== undefined ? { reason: merged.reason } : {} } @@ -251,7 +260,7 @@ export function apply(ctx: Context, config: Config): void { // --- PostToolUse → PostToolDecision. Matcher subject is the tool name. --- ctx.on('tools/post-execute', async (exec, result, next): Promise => { - const turn = lastTurn(ctx, exec.agent) + const turn = lastTurn(exec.agent) const merged = await runPoint('PostToolUse', exec.name, postToolPayload(exec, result), { ...exec.agent ? { agent: exec.agent } : {}, turn, signal: exec.signal }) const context = contextFrom(merged) if (merged.decision === 'deny') { @@ -301,6 +310,12 @@ export function apply(ctx: Context, config: Config): void { detached.track(runPoint('SubagentStop', SUBAGENT_TYPE, subagentPayload('SubagentStop', info, child), { ...child ? { agent: child, directory: 'committed' } : {}, signal: detached.signal }) .catch((error: unknown) => { ctx.logger.warn(`hooks-claude-code: SubagentStop hook failed: ${String(error)}`) })) }) + + function lastTurn(agent: Agent | undefined): number { + if (!agent) return 0 + const boundary = turnBoundary.stateOf(agent.session) + return boundary.lastTurn + } } /** @@ -314,13 +329,6 @@ const SUBAGENT_TYPE = 'general-purpose' // --- Per-event stdin payloads (the CC DIALECT shape). Field names match CC's // hook input schema; this is the part a bridge owns. --- -/** The last open turn number in the agent's log, or 0 without an agent. */ -function lastTurn(ctx: Context, agent: Agent | undefined): number { - if (!agent) return 0 - const boundary = ctx.sessionProjections.stateOf(agent.session, 'turnBoundary') as TurnBoundaryProjection - return boundary.lastTurn -} - /** Flatten content blocks to the text a hook payload carries (the common case). */ function blocksToText(content: ContentBlock[]): string { return content.filter((b): b is Extract => b.type === 'text').map(b => b.text).join('') diff --git a/packages/experimental/hooks-claude-code/tests/coverage-cases.ts b/packages/experimental/hooks-claude-code/tests/coverage-cases.ts index b77aa85c96..dec29257f9 100644 --- a/packages/experimental/hooks-claude-code/tests/coverage-cases.ts +++ b/packages/experimental/hooks-claude-code/tests/coverage-cases.ts @@ -381,7 +381,7 @@ export function defineCoverageCases(group: CoverageGroup): void { // Direct apply with only configPath — bypasses schemastery's defaults, so // the bridge must run on the raw minimal config (the per-hook timeout is // the protocol lib's reference default, not a config knob). - HooksClaude.apply(ctx, { configPath: join(d, 'hooks.json') }) + await HooksClaude.apply(ctx, { configPath: join(d, 'hooks.json') }) ctx.llm.registerAdapter(['mock'], adapter) const agent = await ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })) diff --git a/packages/experimental/hooks-codex/src/index.ts b/packages/experimental/hooks-codex/src/index.ts index 139a99de6d..6159a90dda 100644 --- a/packages/experimental/hooks-codex/src/index.ts +++ b/packages/experimental/hooks-codex/src/index.ts @@ -11,12 +11,13 @@ // Each dialect bridge keeps its complete dependency list visible at the entry // point; a cross-package facade for imports alone would add indirection. /* jscpd:ignore-start */ +import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent' import type {} from '@deepseek-ai/dsh-working-directory' import { readFileSync } from 'node:fs' import type { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' -import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent' import type {} from '@deepseek-ai/dsh-session-projection' +import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent' import { createUserMessage } from '@deepseek-ai/dsh-llm' import type { ContextFormed } from '@deepseek-ai/dsh-llm' declare module '@deepseek-ai/dsh-llm' { @@ -41,11 +42,13 @@ import { type MergedHookOutcome, } from '@deepseek-ai/dsh-hook-protocol' import { parseCodexConfig, type CodexHookConfig } from './config.ts' -/* jscpd:ignore-end */ export const name = 'hooks-codex' + export const inject = ['shell', 'sessionProjections', 'workingDirectory'] +/* jscpd:ignore-end */ + /** Plugin config: where the Codex hooks.json lives + the model name for payloads. */ export interface Config { /** @@ -84,7 +87,12 @@ function assertPositiveInteger(name: string, value: number): void { } } -export function apply(ctx: Context, config: Config): void { +/** Register the hooks-codex plugin after its projections are ready. + * @param ctx - plugin context. + * @param config - hook configuration path and execution limits. + * @returns when the plugin is ready. + */ +export async function apply(ctx: Context, config: Config): Promise { // Validate before config parsing so a bad value cannot be hidden by its early return. const stderrSummaryMaxChars = config.stderrSummaryMaxChars ?? DEFAULT_STDERR_SUMMARY_MAX_CHARS assertPositiveInteger('stderrSummaryMaxChars', stderrSummaryMaxChars) @@ -102,6 +110,7 @@ export function apply(ctx: Context, config: Config): void { return } + const turnBoundary = await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) const model = config.model ?? '' // SessionStart is the one emit-shaped (detached) point Codex has: track its @@ -195,7 +204,6 @@ export function apply(ctx: Context, config: Config): void { .catch((error: unknown) => { ctx.logger.warn(`hooks-codex: SessionStart hook failed: ${String(error)}`) }) detached.track(run) await run - /* jscpd:ignore-end */ }) // UserPromptSubmit → PreStepDecision. Codex supports reject, not rewrite or ask. @@ -226,18 +234,17 @@ export function apply(ctx: Context, config: Config): void { // PreToolUse → PreToolDecision. Codex blocks only (no allow/ask honored). ctx.on('tools/pre-execute', async (exec, next): Promise => { - const turn = lastTurn(ctx, exec.agent) - const merged = await runPoint('PreToolUse', exec.name, preToolPayload(ctx, exec, model), { ...exec.agent ? { agent: exec.agent } : {}, turn, signal: exec.signal }) - /* jscpd:ignore-end */ + const turn = lastTurn(exec.agent) + const merged = await runPoint('PreToolUse', exec.name, preToolPayload(exec, model), { ...exec.agent ? { agent: exec.agent } : {}, turn, signal: exec.signal }) if (merged.decision === 'deny') return { kind: 'deny', reason: merged.reason ?? 'blocked by PreToolUse hook' } return next() }) // PostToolUse → PostToolDecision (block with feedback, or attach context). ctx.on('tools/post-execute', async (exec, result, next): Promise => { - const turn = lastTurn(ctx, exec.agent) + const turn = lastTurn(exec.agent) /* jscpd:ignore-start */ - const merged = await runPoint('PostToolUse', exec.name, postToolPayload(ctx, exec, result, model), { ...exec.agent ? { agent: exec.agent } : {}, turn, signal: exec.signal }) + const merged = await runPoint('PostToolUse', exec.name, postToolPayload(exec, result, model), { ...exec.agent ? { agent: exec.agent } : {}, turn, signal: exec.signal }) const context = contextFrom(merged) if (merged.decision === 'deny') { return { kind: 'block', feedback: [{ type: 'text', text: merged.reason ?? 'blocked by PostToolUse hook' }], ...context ? { additionalContexts: [context] } : {} } @@ -261,8 +268,7 @@ export function apply(ctx: Context, config: Config): void { // avoid continuing the same turn indefinitely. It is always false here, so an // unconditionally blocking hook force-continues every step until it self-limits. ctx.on('agent/turn-stopping', async ({ agent, turn, signal }): Promise => { - const merged = await runPoint('Stop', '', { ...turnBase(ctx, agent, 'Stop', model), stop_hook_active: false, last_assistant_message: null }, { agent, turn, signal }) - /* jscpd:ignore-end */ + const merged = await runPoint('Stop', '', { ...turnBase(agent, 'Stop', model), stop_hook_active: false, last_assistant_message: null }, { agent, turn, signal }) if (merged.decision === 'deny') { // A blocking Stop hook forces continuation; a block with no reason (exit 2, // empty stderr) still forces it — fall back to a generic steering line @@ -271,6 +277,27 @@ export function apply(ctx: Context, config: Config): void { agent.steer(createUserMessage({ content: [{ type: 'text', text }], source: CONTEXT_SOURCE })) } }) + + function lastTurn(agent: Agent | undefined): number { + if (!agent) return 0 + return turnBoundary.stateOf(agent.session).lastTurn + } + + function turnBase(agent: Agent | undefined, event: string, model: string): Record { + return { ...base(agent, event, model), turn_id: String(lastTurn(agent)) } + } + + function preToolPayload(exec: ToolExecution, model: string): Record { + // `tool_name` is the REAL tool name (matching the `exec.name` matcher subject); + // a hardcoded constant would disagree with what the matcher tests and make a + // config's tool matcher never fire. `tool_input` keeps Codex's `{ command }` + // shape (its shell payload), derived from the call's `command` arg when present. + return { ...turnBase(exec.agent, 'PreToolUse', model), tool_name: exec.name, tool_input: { command: commandOf(exec.arguments) }, tool_use_id: exec.callId } + } + + function postToolPayload(exec: ToolExecution, result: ToolExecutionResult, model: string): Record { + return { ...turnBase(exec.agent, 'PostToolUse', model), tool_name: exec.name, tool_input: { command: commandOf(exec.arguments) }, tool_use_id: exec.callId, tool_response: blocksToText(result.content) } + } } // --- Codex DIALECT payloads: snake_case, model on every event, turn_id on @@ -279,11 +306,6 @@ export function apply(ctx: Context, config: Config): void { // These small payload helpers intentionally remain next to the dialect shape; // sharing them would pull bridge-only agent/LLM dependencies into hook-protocol. /* jscpd:ignore-start */ -function lastTurn(ctx: Context, agent: Agent | undefined): number { - if (!agent) return 0 - /* v8 ignore next -- agent-present hook points run inside AgentLoop, which owns this projection. */ - return ctx.sessionProjections.stateOf(agent.session, 'turnBoundary')?.lastTurn ?? 0 -} function blocksToText(content: ContentBlock[]): string { return content.filter((b): b is Extract => b.type === 'text').map(b => b.text).join('') @@ -303,11 +325,6 @@ function base(agent: Agent | undefined, event: string, model: string): Record { - return { ...base(agent, event, model), turn_id: String(lastTurn(ctx, agent)) } -} - /** Extract a `command` string from a tool call's parsed arguments, else ''. */ function commandOf(args: unknown): string { if (typeof args === 'object' && args !== null && 'command' in args) { @@ -316,15 +333,3 @@ function commandOf(args: unknown): string { } return '' } - -function preToolPayload(ctx: Context, exec: ToolExecution, model: string): Record { - // `tool_name` is the REAL tool name (matching the `exec.name` matcher subject); - // a hardcoded constant would disagree with what the matcher tests and make a - // config's tool matcher never fire. `tool_input` keeps Codex's `{ command }` - // shape (its shell payload), derived from the call's `command` arg when present. - return { ...turnBase(ctx, exec.agent, 'PreToolUse', model), tool_name: exec.name, tool_input: { command: commandOf(exec.arguments) }, tool_use_id: exec.callId } -} - -function postToolPayload(ctx: Context, exec: ToolExecution, result: ToolExecutionResult, model: string): Record { - return { ...turnBase(ctx, exec.agent, 'PostToolUse', model), tool_name: exec.name, tool_input: { command: commandOf(exec.arguments) }, tool_use_id: exec.callId, tool_response: blocksToText(result.content) } -} diff --git a/packages/experimental/hooks-codex/tests/coverage-cases.ts b/packages/experimental/hooks-codex/tests/coverage-cases.ts index 499a4a8fc1..d05a64984e 100644 --- a/packages/experimental/hooks-codex/tests/coverage-cases.ts +++ b/packages/experimental/hooks-codex/tests/coverage-cases.ts @@ -326,7 +326,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000 }) ctx.logger.warn = warn as never // Direct apply (schema bypass) → the `model ?? ''` fallback is exercised. - HooksCodex.apply(ctx, { configPath: join(d, 'hooks.json') }) + await HooksCodex.apply(ctx, { configPath: join(d, 'hooks.json') }) ctx.llm.registerAdapter(['mock'], adapter) const agent = await ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })); await waitForIdle(ctx, agent) diff --git a/packages/experimental/session-title-all-prompts-llm/tests/provider.spec.ts b/packages/experimental/session-title-all-prompts-llm/tests/provider.spec.ts index b2bf78ddca..282662b1a0 100644 --- a/packages/experimental/session-title-all-prompts-llm/tests/provider.spec.ts +++ b/packages/experimental/session-title-all-prompts-llm/tests/provider.spec.ts @@ -92,7 +92,7 @@ async function harness( await ctx.plugin(LlmRuntime) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) await ctx.plugin(SessionTitleService, TITLE_CONFIG) const adapter = new RecordingAdapter(script, reasoningEfforts) ctx.llm.registerAdapter(['title-route'], adapter) @@ -215,7 +215,7 @@ describe('all-messages LLM title provider', () => { await ctx.plugin(LlmRuntime) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) // A one-byte fallback cap cannot derive a title from the multibyte message, // so the service supplies no currentTitle to the provider. await ctx.plugin(SessionTitleService, { fallbackMaxWords: 5, fallbackMaxBytes: 1, maxTitleBytes: 80 }) diff --git a/packages/experimental/tool-session-query/src/index.ts b/packages/experimental/tool-session-query/src/index.ts index 61e18cc8f5..8bbf6c3bff 100644 --- a/packages/experimental/tool-session-query/src/index.ts +++ b/packages/experimental/tool-session-query/src/index.ts @@ -9,13 +9,11 @@ import z from '@deepseek-ai/schemastery' import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout' import { defineTool } from '@deepseek-ai/dsh-tools' import { toolInput } from './input.ts' -import { operations } from './operations.ts' +import { SessionQueryOperations } from './operations.ts' import { presentation } from './presentation.ts' -/** Cordis plugin name used by Loader diagnostics. */ export const name = 'tool-session-query' -/** Capability services required by the model-facing consumer. */ export const inject = ['tools', 'systemPrompt', 'sessionQuery', 'sessionProjections'] /** Default maximum number of authorized search hits returned by one call. */ @@ -53,8 +51,15 @@ const PROMPT_TEXT = + 'events in one session. Search results are cursor-free and workspace-scoped. Follow a useful hit with ' + 'session_trace, session_event_trace, or session_event_read when you need lineage, relationships, or exact data.' -/** Register all five tools and their shared model guidance. */ -export function apply(ctx: Context, config: Config): void { +/** + * Register all five tools and their shared model guidance. + * @param ctx - owning plugin context. + * @param config - search limits. + * @returns when the tools and their projection reader are ready. + */ +export async function apply(ctx: Context, config: Config): Promise { + const operations = new SessionQueryOperations(ctx) + await operations.initialize() const resolved = resolveConfig(config) ctx.systemPrompt.section({ name: 'tool:session-query', @@ -68,7 +73,7 @@ export function apply(ctx: Context, config: Config): void { parameters: toolInput.sessionSearchParameters, output: TEXT_OUTPUT, timeoutMs: resolved.searchTimeoutMs, - execute: (args, exec) => operations.executeSessionSearch(ctx, args, exec, resolved.maxSearchResults), + execute: (args, exec) => operations.executeSessionSearch(args, exec, resolved.maxSearchResults), presentCall: presentation.presentSessionSearchCall, })) @@ -78,7 +83,7 @@ export function apply(ctx: Context, config: Config): void { parameters: toolInput.eventSearchParameters, output: TEXT_OUTPUT, timeoutMs: resolved.searchTimeoutMs, - execute: (args, exec) => operations.executeEventSearch(ctx, args, exec, resolved.maxSearchResults), + execute: (args, exec) => operations.executeEventSearch(args, exec, resolved.maxSearchResults), presentCall: presentation.presentEventSearchCall, })) @@ -88,7 +93,7 @@ export function apply(ctx: Context, config: Config): void { parameters: toolInput.targetSessionParameter, output: TEXT_OUTPUT, isConcurrencySafe: () => true, - execute: (args, exec) => operations.executeSessionTrace(ctx, args, exec), + execute: (args, exec) => operations.executeSessionTrace(args, exec), presentCall: presentation.presentSessionTraceCall, })) @@ -101,7 +106,7 @@ export function apply(ctx: Context, config: Config): void { }, output: TEXT_OUTPUT, isConcurrencySafe: () => true, - execute: (args, exec) => operations.executeEventTrace(ctx, args, exec), + execute: (args, exec) => operations.executeEventTrace(args, exec), presentCall: args => presentation.presentEventTargetCall('Trace event', args), })) @@ -116,7 +121,7 @@ export function apply(ctx: Context, config: Config): void { }, output: TEXT_OUTPUT, isConcurrencySafe: () => true, - execute: (args, exec) => operations.executeEventRead(ctx, args, exec), + execute: (args, exec) => operations.executeEventRead(args, exec), presentCall: args => presentation.presentEventTargetCall('Read event', args), })) } diff --git a/packages/experimental/tool-session-query/src/operations.ts b/packages/experimental/tool-session-query/src/operations.ts index 3d5fa17f31..4c05480221 100644 --- a/packages/experimental/tool-session-query/src/operations.ts +++ b/packages/experimental/tool-session-query/src/operations.ts @@ -4,6 +4,8 @@ * @module @deepseek-ai/dsh-tool-session-query/operations */ +import { turnBoundaryProjectionDefinition, type TurnBoundaryProjection } from '@deepseek-ai/dsh-agent' +import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection' import type { Context } from '@deepseek-ai/cordis' import { HarnessError } from '@deepseek-ai/dsh-llm' import { SessionSeq } from '@deepseek-ai/dsh-session' @@ -52,193 +54,249 @@ interface SearchCollection { readonly capped: boolean } -async function executeSessionSearch( - ctx: Context, - args: SessionSearchArgs, - exec: ToolRunContext, - maxResults: number, -): Promise { - const caller = workspaceAccess.callerOf(exec, ctx) - const cwd = caller.header.cwd - if (cwd === undefined) { - throw new HarnessError( - 'cross-session search is unavailable because the caller session has no workspace', - 'SESSION_QUERY_TOOL_UNAUTHORIZED', - ) - } - const query = toolInput.normalizeQuery(args.query) - const sessionFilters = toolInput.buildSessionFilters(args) - const eventFilters = toolInput.buildEventFilters({ - seqFrom: args.event_seq_from, - seqTo: args.event_seq_to, - timeFrom: args.event_time_from, - timeTo: args.event_time_to, - eventTypes: args.event_types, - surfaces: args.event_surfaces, - }) - const requestedParentIds = toolInput.materializeParentSessionIds(args.parent_session_ids) - if (requestedParentIds !== undefined || args.include_root_sessions === true) { - const authorizedParentIds = requestedParentIds === undefined - ? new Set() - : await workspaceAccess.authorizeSessionIds(ctx, caller, requestedParentIds, exec.signal) - const parentValues: Array = requestedParentIds - ?.filter(id => authorizedParentIds.has(id)) ?? [] - if (args.include_root_sessions === true) parentValues.push(null) - if (parentValues.length === 0) return presentation.formatEmptySessionSearch() - sessionFilters.push({ kind: 'parent', values: parentValues }) - } - sessionFilters.push({ kind: 'cwd', values: [cwd] }) - const collected = await collectPages( - maxResults, - exec.signal, - cursor => serviceBoundary.call(ctx, exec.signal, 'session search', () => - ctx.sessionQuery.searchSessions({ - query, - sessionFilters, - eventFilters, - ...cursor === undefined ? {} : { cursor }, - }, { signal: exec.signal })), - hit => hit.header.id !== caller.id && workspaceAccess.recordAuthorized(hit, caller), - ) +/** Session-history tool operations over the caller’s current turn state. */ +export class SessionQueryOperations { + private turnBoundary!: ProjectionHandle - const parentIds = collected.items - .map(hit => hit.header.parentSession) - .filter((id): id is SessionId => id !== undefined) - const authorizedParents = await workspaceAccess.authorizeSessionIds(ctx, caller, parentIds, exec.signal) - const titles = await workspaceAccess.readTitles( - ctx, - caller, - collected.items.map(hit => hit.header.id), - exec.signal, - ) - return presentation.formatSessionSearch(collected, titles, authorizedParents) -} + constructor(private readonly ctx: Context) {} -async function executeEventSearch( - ctx: Context, - args: EventSearchArgs, - exec: ToolRunContext, - maxResults: number, -): Promise { - const caller = workspaceAccess.callerOf(exec, ctx) - const sessionId = workspaceAccess.targetId(args, caller) - await workspaceAccess.authorizeTarget(ctx, caller, sessionId, exec.signal) - const query = toolInput.normalizeQuery(args.query) - const range = toolInput.sequenceRange(args.seq_from, args.seq_to) - if (sessionId === caller.id) { - const stepStartSeq = caller.boundary?.lastStepStartSeq - if (stepStartSeq === undefined) { + /** + * Acquire projection readers before exposing dependent operations. + * @returns when all required registrations are ready. + */ + async initialize(): Promise { + this.turnBoundary = await this.ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + } + + private callerOf(exec: ToolRunContext) { + const agent = exec.agent + if (agent === undefined) { throw new HarnessError( - 'current-session search requires an active step boundary', - 'SESSION_QUERY_TOOL_NO_CURRENT_STEP', + 'session query tools require an agent-bound caller', + 'SESSION_QUERY_TOOL_MISSING_AGENT', ) } - // `null` (no step started yet) caps the range to a degenerate `to` the - // filter validation rejects — the loop never runs tools outside a step. - range.to = Math.min(range.to ?? Number.MAX_SAFE_INTEGER, (stepStartSeq ?? 0) - 1) - } - const title = await workspaceAccess.readTitle(ctx, caller, sessionId, exec.signal) - if (range.from !== undefined && range.to !== undefined && range.from > range.to) { - return presentation.formatEventSearch(sessionId, title, { items: [], capped: false }) - } - const filters = toolInput.buildEventFilters({ - seqFrom: range.from, - seqTo: range.to, - timeFrom: args.time_from, - timeTo: args.time_to, - eventTypes: args.event_types, - surfaces: args.surfaces, - }) - const collected = await collectPages( - maxResults, - exec.signal, - async (cursor): Promise => { - const page = await serviceBoundary.call(ctx, exec.signal, 'event search', () => - ctx.sessionQuery.searchEvents({ - sessionId, - query, - filters, - ...cursor === undefined ? {} : { cursor }, - }, { signal: exec.signal })) - workspaceAccess.assertObservedTargetAuthorized(caller, sessionId, page.session) - return page - }, - () => true, - ) - return presentation.formatEventSearch(sessionId, title, collected) -} - -async function executeSessionTrace( - ctx: Context, - args: SessionTargetArgs, - exec: ToolRunContext, -): Promise { - const caller = workspaceAccess.callerOf(exec, ctx) - const sessionId = workspaceAccess.targetId(args, caller) - await workspaceAccess.authorizeTarget(ctx, caller, sessionId, exec.signal) - const trace = await serviceBoundary.call(ctx, exec.signal, 'session lineage trace', () => - ctx.sessionQuery.traceSession(sessionId, exec.signal)) - workspaceAccess.assertObservedTargetAuthorized(caller, sessionId, trace.target.header) - - const ancestors: SessionRecord[] = [] - let ancestorBoundary = false - for (const ancestor of trace.ancestors) { - if (!workspaceAccess.recordAuthorized(ancestor, caller)) { - ancestorBoundary = true - break + return { + id: agent.session.id, + header: agent.session.header, + boundary: this.turnBoundary.stateOf(agent.session), } - ancestors.push(ancestor) } - if (ancestors.length === trace.ancestors.length && !trace.complete) ancestorBoundary = true - const descendants = workspaceAccess.authorizeDescendants(trace.descendants, caller) - const visibleIds = [ - trace.target.header.id, - ...ancestors.map(record => record.header.id), - ...workspaceAccess.descendantIds(descendants), - ] - const titles = await workspaceAccess.readTitles(ctx, caller, visibleIds, exec.signal) - return presentation.formatSessionTrace(trace, ancestors, ancestorBoundary, descendants, titles) -} -async function executeEventTrace( - ctx: Context, - args: EventTargetArgs, - exec: ToolRunContext, -): Promise { - toolInput.assertNonNegativeSafeInteger('seq', args.seq) - const seq = SessionSeq(args.seq) - const caller = workspaceAccess.callerOf(exec, ctx) - const sessionId = workspaceAccess.targetId(args, caller) - await workspaceAccess.authorizeTarget(ctx, caller, sessionId, exec.signal) - const trace = await serviceBoundary.call(ctx, exec.signal, 'event trace', () => - ctx.sessionQuery.traceEvent({ sessionId, seq }, exec.signal)) - workspaceAccess.assertObservedTargetAuthorized(caller, sessionId, trace.session) - const title = await workspaceAccess.readTitle(ctx, caller, sessionId, exec.signal) - return presentation.formatEventTrace(sessionId, title, trace) -} + /** + * Search other Sessions within the caller’s authorized workspace. + * @param args - Session and event filters. + * @param exec - Agent-bound caller and cancellation signal. + * @param maxResults - Maximum number of authorized matches to return. + * @returns Formatted authorized Session matches and truncation status. + */ + async executeSessionSearch( + args: SessionSearchArgs, + exec: ToolRunContext, + maxResults: number, + ): Promise { + const ctx = this.ctx + const caller = this.callerOf(exec) + const cwd = caller.header.cwd + if (cwd === undefined) { + throw new HarnessError( + 'cross-session search is unavailable because the caller session has no workspace', + 'SESSION_QUERY_TOOL_UNAUTHORIZED', + ) + } + const query = toolInput.normalizeQuery(args.query) + const sessionFilters = toolInput.buildSessionFilters(args) + const eventFilters = toolInput.buildEventFilters({ + seqFrom: args.event_seq_from, + seqTo: args.event_seq_to, + timeFrom: args.event_time_from, + timeTo: args.event_time_to, + eventTypes: args.event_types, + surfaces: args.event_surfaces, + }) + const requestedParentIds = toolInput.materializeParentSessionIds(args.parent_session_ids) + if (requestedParentIds !== undefined || args.include_root_sessions === true) { + const authorizedParentIds = requestedParentIds === undefined + ? new Set() + : await workspaceAccess.authorizeSessionIds(ctx, caller, requestedParentIds, exec.signal) + const parentValues: Array = requestedParentIds + ?.filter(id => authorizedParentIds.has(id)) ?? [] + if (args.include_root_sessions === true) parentValues.push(null) + if (parentValues.length === 0) return presentation.formatEmptySessionSearch() + sessionFilters.push({ kind: 'parent', values: parentValues }) + } + sessionFilters.push({ kind: 'cwd', values: [cwd] }) + const collected = await collectPages( + maxResults, + exec.signal, + cursor => serviceBoundary.call(ctx, exec.signal, 'session search', () => + ctx.sessionQuery.searchSessions({ + query, + sessionFilters, + eventFilters, + ...cursor === undefined ? {} : { cursor }, + }, { signal: exec.signal })), + hit => hit.header.id !== caller.id && workspaceAccess.recordAuthorized(hit, caller), + ) -async function executeEventRead( - ctx: Context, - args: EventReadArgs, - exec: ToolRunContext, -): Promise { - toolInput.assertNonNegativeSafeInteger('seq', args.seq) - const seq = SessionSeq(args.seq) - if (args.before !== undefined) toolInput.assertNonNegativeSafeInteger('before', args.before) - if (args.after !== undefined) toolInput.assertNonNegativeSafeInteger('after', args.after) - const caller = workspaceAccess.callerOf(exec, ctx) - const sessionId = workspaceAccess.targetId(args, caller) - await workspaceAccess.authorizeTarget(ctx, caller, sessionId, exec.signal) - const window = await serviceBoundary.call(ctx, exec.signal, 'event read', () => - ctx.sessionQuery.readEvent({ - sessionId, - seq, - ...args.before === undefined ? {} : { before: args.before }, - ...args.after === undefined ? {} : { after: args.after }, - }, exec.signal)) - workspaceAccess.assertObservedTargetAuthorized(caller, sessionId, window.session) - const title = await workspaceAccess.readTitle(ctx, caller, sessionId, exec.signal) - return presentation.formatEventRead(sessionId, title, window) + const parentIds = collected.items + .map(hit => hit.header.parentSession) + .filter((id): id is SessionId => id !== undefined) + const authorizedParents = await workspaceAccess.authorizeSessionIds(ctx, caller, parentIds, exec.signal) + const titles = await workspaceAccess.readTitles( + ctx, + caller, + collected.items.map(hit => hit.header.id), + exec.signal, + ) + return presentation.formatSessionSearch(collected, titles, authorizedParents) + } + + /** + * Search authorized Session events, excluding the caller’s current step. + * @param args - Target Session and event filters. + * @param exec - Agent-bound caller and cancellation signal. + * @param maxResults - Maximum number of authorized matches to return. + * @returns Formatted matching events and truncation status. + */ + async executeEventSearch( + args: EventSearchArgs, + exec: ToolRunContext, + maxResults: number, + ): Promise { + const ctx = this.ctx + const caller = this.callerOf(exec) + const sessionId = workspaceAccess.targetId(args, caller) + await workspaceAccess.authorizeTarget(ctx, caller, sessionId, exec.signal) + const query = toolInput.normalizeQuery(args.query) + const range = toolInput.sequenceRange(args.seq_from, args.seq_to) + if (sessionId === caller.id) { + const stepStartSeq = caller.boundary.lastStepStartSeq + // `null` (no step started yet) caps the range to a degenerate `to` the + // filter validation rejects — the loop never runs tools outside a step. + range.to = Math.min(range.to ?? Number.MAX_SAFE_INTEGER, (stepStartSeq ?? 0) - 1) + } + const title = await workspaceAccess.readTitle(ctx, caller, sessionId, exec.signal) + if (range.from !== undefined && range.to !== undefined && range.from > range.to) { + return presentation.formatEventSearch(sessionId, title, { items: [], capped: false }) + } + const filters = toolInput.buildEventFilters({ + seqFrom: range.from, + seqTo: range.to, + timeFrom: args.time_from, + timeTo: args.time_to, + eventTypes: args.event_types, + surfaces: args.surfaces, + }) + const collected = await collectPages( + maxResults, + exec.signal, + async (cursor): Promise => { + const page = await serviceBoundary.call(ctx, exec.signal, 'event search', () => + ctx.sessionQuery.searchEvents({ + sessionId, + query, + filters, + ...cursor === undefined ? {} : { cursor }, + }, { signal: exec.signal })) + workspaceAccess.assertObservedTargetAuthorized(caller, sessionId, page.session) + return page + }, + () => true, + ) + return presentation.formatEventSearch(sessionId, title, collected) + } + + /** + * Trace Session lineage within the caller’s workspace permissions. + * @param args - Optional target Session id; defaults to the caller. + * @param exec - Agent-bound caller and cancellation signal. + * @returns Formatted authorized ancestors and descendants. + */ + async executeSessionTrace( + args: SessionTargetArgs, + exec: ToolRunContext, + ): Promise { + const ctx = this.ctx + const caller = this.callerOf(exec) + const sessionId = workspaceAccess.targetId(args, caller) + await workspaceAccess.authorizeTarget(ctx, caller, sessionId, exec.signal) + const trace = await serviceBoundary.call(ctx, exec.signal, 'session lineage trace', () => + ctx.sessionQuery.traceSession(sessionId, exec.signal)) + workspaceAccess.assertObservedTargetAuthorized(caller, sessionId, trace.target.header) + + const ancestors: SessionRecord[] = [] + let ancestorBoundary = false + for (const ancestor of trace.ancestors) { + if (!workspaceAccess.recordAuthorized(ancestor, caller)) { + ancestorBoundary = true + break + } + ancestors.push(ancestor) + } + if (ancestors.length === trace.ancestors.length && !trace.complete) ancestorBoundary = true + const descendants = workspaceAccess.authorizeDescendants(trace.descendants, caller) + const visibleIds = [ + trace.target.header.id, + ...ancestors.map(record => record.header.id), + ...workspaceAccess.descendantIds(descendants), + ] + const titles = await workspaceAccess.readTitles(ctx, caller, visibleIds, exec.signal) + return presentation.formatSessionTrace(trace, ancestors, ancestorBoundary, descendants, titles) + } + + /** + * Trace an event after authorizing its owning Session. + * @param args - Target Session and event sequence. + * @param exec - Agent-bound caller and cancellation signal. + * @returns Formatted event lineage. + */ + async executeEventTrace( + args: EventTargetArgs, + exec: ToolRunContext, + ): Promise { + const ctx = this.ctx + toolInput.assertNonNegativeSafeInteger('seq', args.seq) + const seq = SessionSeq(args.seq) + const caller = this.callerOf(exec) + const sessionId = workspaceAccess.targetId(args, caller) + await workspaceAccess.authorizeTarget(ctx, caller, sessionId, exec.signal) + const trace = await serviceBoundary.call(ctx, exec.signal, 'event trace', () => + ctx.sessionQuery.traceEvent({ sessionId, seq }, exec.signal)) + workspaceAccess.assertObservedTargetAuthorized(caller, sessionId, trace.session) + const title = await workspaceAccess.readTitle(ctx, caller, sessionId, exec.signal) + return presentation.formatEventTrace(sessionId, title, trace) + } + + /** + * Read an event window after authorizing its owning Session. + * @param args - Target event and surrounding event counts. + * @param exec - Agent-bound caller and cancellation signal. + * @returns Formatted event window. + */ + async executeEventRead( + args: EventReadArgs, + exec: ToolRunContext, + ): Promise { + const ctx = this.ctx + toolInput.assertNonNegativeSafeInteger('seq', args.seq) + const seq = SessionSeq(args.seq) + if (args.before !== undefined) toolInput.assertNonNegativeSafeInteger('before', args.before) + if (args.after !== undefined) toolInput.assertNonNegativeSafeInteger('after', args.after) + const caller = this.callerOf(exec) + const sessionId = workspaceAccess.targetId(args, caller) + await workspaceAccess.authorizeTarget(ctx, caller, sessionId, exec.signal) + const window = await serviceBoundary.call(ctx, exec.signal, 'event read', () => + ctx.sessionQuery.readEvent({ + sessionId, + seq, + ...args.before === undefined ? {} : { before: args.before }, + ...args.after === undefined ? {} : { after: args.after }, + }, exec.signal)) + workspaceAccess.assertObservedTargetAuthorized(caller, sessionId, window.session) + const title = await workspaceAccess.readTitle(ctx, caller, sessionId, exec.signal) + return presentation.formatEventRead(sessionId, title, window) + } } async function collectPages( @@ -275,12 +333,3 @@ async function collectPages( cursor = page.nextCursor } } - -/** Five model-facing session-query operation implementations. */ -export const operations = { - executeSessionSearch, - executeEventSearch, - executeSessionTrace, - executeEventTrace, - executeEventRead, -} diff --git a/packages/experimental/tool-session-query/src/workspace-access.ts b/packages/experimental/tool-session-query/src/workspace-access.ts index 9eabee4e46..4f11e64f38 100644 --- a/packages/experimental/tool-session-query/src/workspace-access.ts +++ b/packages/experimental/tool-session-query/src/workspace-access.ts @@ -6,7 +6,6 @@ import type { Context } from '@deepseek-ai/cordis' import { brandString } from '@deepseek-ai/dsh-brand' -import { HarnessError } from '@deepseek-ai/dsh-llm' import { type SessionHeader, type SessionId as SessionIdValue, @@ -16,15 +15,13 @@ import type { SessionLineageNode, SessionRecord, } from '@deepseek-ai/dsh-session-query' -import type { ToolRunContext } from '@deepseek-ai/dsh-tools' -import type {} from '@deepseek-ai/dsh-session-projection' import { serviceBoundary } from './service-boundary.ts' interface Caller { readonly id: SessionIdValue readonly header: SessionHeader /** The caller's own-session boundary fold (the `turnBoundary` projection). */ - readonly boundary: TurnBoundaryProjection | undefined + readonly boundary: TurnBoundaryProjection } interface TitleView { @@ -53,21 +50,6 @@ interface DescendantVisit { readonly next: DescendantVisit | undefined } -function callerOf(exec: ToolRunContext, ctx: Context): Caller { - const agent = exec.agent - if (agent === undefined) { - throw new HarnessError( - 'session query tools require an agent-bound caller', - 'SESSION_QUERY_TOOL_MISSING_AGENT', - ) - } - return { - id: agent.session.id, - header: agent.session.header, - boundary: ctx.sessionProjections.stateOf(agent.session, 'turnBoundary'), - } -} - function targetId(args: { readonly session_id?: string }, caller: Caller): SessionIdValue { return args.session_id === undefined ? caller.id : brandString(args.session_id) } @@ -242,7 +224,6 @@ function titleText(view: TitleView): string { /** Workspace-scoped caller authorization, title access, and lineage projection. */ export const workspaceAccess = { - callerOf, targetId, authorizeTarget, recordAuthorized, diff --git a/packages/experimental/tool-session-query/tests/sqlite-integration.spec.ts b/packages/experimental/tool-session-query/tests/sqlite-integration.spec.ts index ed6402865f..d71079adb7 100644 --- a/packages/experimental/tool-session-query/tests/sqlite-integration.spec.ts +++ b/packages/experimental/tool-session-query/tests/sqlite-integration.spec.ts @@ -33,8 +33,8 @@ function fakeAgent(session: Session): Agent { return { id: session.id, session } as unknown as Agent } -function registerTurnBoundary(ctx: Context): void { - ctx.sessionProjections.register(turnBoundaryProjectionDefinition) +async function registerTurnBoundary(ctx: Context): Promise { + await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) } describe('tool-session-query with the real SQLite provider', () => { @@ -45,7 +45,7 @@ describe('tool-session-query with the real SQLite provider', () => { contexts.push(ctx) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - registerTurnBoundary(ctx) + await registerTurnBoundary(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) await ctx.plugin(JsonlSessionPersistence, { root, compression: 'none' }) @@ -118,7 +118,7 @@ describe('tool-session-query with the real SQLite provider', () => { contexts.push(ctx) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - registerTurnBoundary(ctx) + await registerTurnBoundary(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) await ctx.plugin(JsonlSessionPersistence, { root, compression: 'none' }) diff --git a/packages/experimental/tool-session-query/tests/tool-session-query.spec.ts b/packages/experimental/tool-session-query/tests/tool-session-query.spec.ts index 3d11ad3f85..c5dd7fddfc 100644 --- a/packages/experimental/tool-session-query/tests/tool-session-query.spec.ts +++ b/packages/experimental/tool-session-query/tests/tool-session-query.spec.ts @@ -31,6 +31,7 @@ import SessionQueryEngine, { } from '@deepseek-ai/dsh-session-query' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { type ToolExecutionResult } from '@deepseek-ai/dsh-tools' +import type { Config as ToolSessionQueryConfig } from '@deepseek-ai/dsh-tool-session-query' import * as ToolSessionQuery from '@deepseek-ai/dsh-tool-session-query' declare module '@deepseek-ai/dsh-llm' { @@ -207,12 +208,12 @@ interface Mounted { call(name: string, args: unknown, options?: { agent?: Agent; signal?: AbortSignal }): Promise } -function registerTurnBoundary(ctx: Context): void { - ctx.sessionProjections.register(turnBoundaryProjectionDefinition) +async function registerTurnBoundary(ctx: Context): Promise { + await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) } async function mount( - config: ToolSessionQuery.Config = {}, + config: ToolSessionQueryConfig = {}, callerCwd: string | null = '/work', enforceTimeout = false, ): Promise { @@ -220,7 +221,7 @@ async function mount( activeContexts.push(ctx) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - registerTurnBoundary(ctx) + await registerTurnBoundary(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) if (enforceTimeout) await ctx.plugin(TimeoutPolicy) @@ -329,14 +330,14 @@ describe('registration and schemas', () => { it('fails invalid direct config before registering anything', async () => { const mounted = await mount() for (const maxSearchResults of [0, 1.5, Number.NaN]) { - expect(() => { ToolSessionQuery.apply(mounted.ctx, { maxSearchResults }) }) - .toThrow('maxSearchResults') + await expect(ToolSessionQuery.apply(mounted.ctx, { maxSearchResults })) + .rejects.toThrow('maxSearchResults') } for (const searchTimeoutMs of [0, 1.5, Number.POSITIVE_INFINITY, MAX_TIMER_DELAY_MS + 1]) { - expect(() => { ToolSessionQuery.apply(mounted.ctx, { searchTimeoutMs }) }) - .toThrow(`no greater than ${MAX_TIMER_DELAY_MS}`) + await expect(ToolSessionQuery.apply(mounted.ctx, { searchTimeoutMs })) + .rejects.toThrow(`no greater than ${MAX_TIMER_DELAY_MS}`) } - expect(() => { ToolSessionQuery.apply(new Context(), {}) }).toThrow() + await expect(ToolSessionQuery.apply(new Context(), {})).rejects.toThrow() }) it('expresses the complete Node timer range in the Loader config schema', () => { @@ -1652,7 +1653,7 @@ describe('search paging, prior-history bounds, titles, and cancellation', () => expect(text(result)).toContain('Result cap reached') }) - it('rejects current-session search without the turnBoundary fold', async () => { + it('rejects current-session search before any step has started', async () => { const ctx = new Context() activeContexts.push(ctx) await ctx.plugin(SessionStore) @@ -1669,7 +1670,8 @@ describe('search paging, prior-history bounds, titles, and cancellation', () => signal: new AbortController().signal, agent: fakeAgent(session), }) - expect(errorCode(result)).toBe('SESSION_QUERY_TOOL_NO_CURRENT_STEP') + expect(ctx.sessionProjections.checkpoint(session).turnBoundary).toBeDefined() + expect(errorCode(result)).toBe('SESSION_QUERY_INVALID_FILTER') }) it('preserves base results when a title read fails, annotates the code, and logs the full error', async () => { diff --git a/packages/fs/tool-fs/tests/tools.spec.ts b/packages/fs/tool-fs/tests/tools.spec.ts index f495097df2..6d8c8e9698 100644 --- a/packages/fs/tool-fs/tests/tools.spec.ts +++ b/packages/fs/tool-fs/tests/tools.spec.ts @@ -841,7 +841,7 @@ describe('sandbox escalation API (write/edit)', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) await ctx.plugin(SandboxPolicyService, { mode: 'workspace-write' }) await ctx.plugin(SandboxingFakeFs) await ctx.plugin(FsPolicy) diff --git a/packages/goal/goal/src/index.ts b/packages/goal/goal/src/index.ts index 48a02e485c..cbc0344b95 100644 --- a/packages/goal/goal/src/index.ts +++ b/packages/goal/goal/src/index.ts @@ -5,7 +5,7 @@ */ import { randomUUID } from 'node:crypto' -import { Context } from '@deepseek-ai/cordis' +import { Context, Service } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import { z as zod } from 'zod' import type { ZodType } from 'zod' @@ -14,7 +14,7 @@ import type { Agent } from '@deepseek-ai/dsh-agent' import { SessionSeq } from '@deepseek-ai/dsh-session' import type { Session, SessionEvent, SessionLogOffset } from '@deepseek-ai/dsh-session' import { TypertRemoteService, Remote } from '@deepseek-ai/dsh-typert-protocol' -import type {} from '@deepseek-ai/dsh-session-projection' +import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection' import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection' import { applyGoalEvent, @@ -247,15 +247,21 @@ export class GoalService extends TypertRemoteService { private readonly resolved: ResolvedConfig private readonly runtimeStates = new WeakMap() + private projection!: ProjectionHandle + constructor(ctx: Context, config: Config = {}) { super(ctx, 'goals') this.resolved = { defaultMaxGoalRounds: resolveMaxGoalRounds(config.defaultMaxGoalRounds ?? 256), } + } + + protected async [Service.init](): Promise { + const ctx = this.ctx + this.projection = await ctx.sessionProjections.register(goalProjectionDefinition) ctx.on('agent/created', ({ agent }) => { this.setActivation(agent.session, 'disarmed') }) - ctx.sessionProjections.register(goalProjectionDefinition) ctx.on('session/event', (session, event) => { if (event.type !== 'goal/change') return const runtime = this.runtimeState(session) @@ -474,8 +480,7 @@ export class GoalService extends TypertRemoteService { /** Read the current durable projection maintained by the registry. */ private state(session: Session): GoalProjection | null { - const state = this.ctx.sessionProjections.stateOf(session, 'goal') - if (state === undefined) throw new Error('goal projection is not registered') + const state = this.projection.stateOf(session) if (state.failure !== null) throw new Error(state.failure) return state.current } @@ -497,9 +502,7 @@ export class GoalService extends TypertRemoteService { const runtime = this.runtimeState(session) if (runtime.activation === activation) return runtime.activation = activation - const state = this.ctx.sessionProjections.stateOf(session, 'goal') - /* v8 ignore next -- static inject requires the projection registry before this service activates. */ - if (state === undefined) return + const state = this.projection.stateOf(session) if (state.failure !== null) return const goal = this.view(state.current, runtime) this.ctx.emit('goal/activation-changed', { diff --git a/packages/goal/goal/tests/goal.spec.ts b/packages/goal/goal/tests/goal.spec.ts index fe9c2d0030..462a0f59fa 100644 --- a/packages/goal/goal/tests/goal.spec.ts +++ b/packages/goal/goal/tests/goal.spec.ts @@ -1,3 +1,4 @@ +import { Service } from '@deepseek-ai/cordis' import { describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import AgentRegistry, { agentEvents } from '@deepseek-ai/dsh-agent' @@ -174,8 +175,11 @@ describe('GoalService creation and replay', () => { await ctx.plugin(SessionProjectionRegistry) const stub = stubAgent('goal-direct-construction') await ctx.agents.register(stub.agent) - const goals = new GoalService(ctx) - await new Promise(resolve => setImmediate(resolve)) + class DirectGoalService extends GoalService { + initialize(): Promise { return this[Service.init]() } + } + const goals = new DirectGoalService(ctx) + await goals.initialize() expect(goals.create(stub.agent, { objective: 'direct' })).toMatchObject({ objective: 'direct', maxGoalRounds: 256, }) @@ -277,8 +281,8 @@ describe('GoalService creation and replay', () => { await fiber.dispose() expect(ctx.get('goals')).toBeUndefined() - expect(ctx.sessionProjections.stateOf(stub.session, 'goal')).toBeUndefined() - expect(() => first.get(stub.agent)).toThrow('goal projection is not registered') + expect(ctx.sessionProjections.checkpoint(stub.session).goal?.val).toBeUndefined() + expect(first.get(stub.agent)).toMatchObject({ id: goal.id }) await ctx.plugin(GoalService) expect(ctx.goals).not.toBe(first) diff --git a/packages/goal/goal/tests/projection.spec.ts b/packages/goal/goal/tests/projection.spec.ts index 227da38ad8..64071e4038 100644 --- a/packages/goal/goal/tests/projection.spec.ts +++ b/packages/goal/goal/tests/projection.spec.ts @@ -235,9 +235,10 @@ describe('goal projection unit', () => { const bench = await harness(true) bench.ctx.goals.create(bench.agent, { objective: 'poisoned replay' }) const failure = 'goal replay failed at session event 0: invalid restored goal stream' - const state = bench.ctx.sessionProjections.stateOf(bench.session, 'goal') + const handle = await bench.ctx.sessionProjections.register(goalProjectionDefinition) + const state = handle.stateOf(bench.session) expect(state).toBeDefined() - Object.assign(state!, { failure }) + Object.assign(state, { failure }) await expect(agentEvents(bench.ctx, bench.agent).serial('agent/created', { source: 'resume' })) .resolves.toBeUndefined() diff --git a/packages/goal/tool-goal/README.i18n.yaml b/packages/goal/tool-goal/README.i18n.yaml index 492ae8374e..1d97f69a1b 100644 --- a/packages/goal/tool-goal/README.i18n.yaml +++ b/packages/goal/tool-goal/README.i18n.yaml @@ -30,8 +30,8 @@ en: 4e9fdb5a89f4baa3 zh: 50d182467ae1f3a7 /deepseek-ai-dsh-tool-goal/understand-the-implementation/design: - en: a30f36299c8107f7 - zh: 65e6e1e19aeb5281 + en: 905e5e105efdf7a8 + zh: 3704176a160aabb1 /deepseek-ai-dsh-tool-goal/understand-the-implementation/source-map: en: 4d5c648787409868 zh: b4938bfe660f18aa diff --git a/packages/goal/tool-goal/README.md b/packages/goal/tool-goal/README.md index 158446e156..9ab458268d 100644 --- a/packages/goal/tool-goal/README.md +++ b/packages/goal/tool-goal/README.md @@ -69,6 +69,7 @@ This section explains how the tools enforce authority and render output; the obs ### Design - **Authority at execution.** Every call resolves the exact live agent, its inherited `AgentRegistry` initiator, running status, and an open turn; `create`, `edit`, `pause`, and `resume` additionally require an accepted `{ kind: 'user' }` message or steering event in a runtime-root agent's current turn. A durable paused goal fails the `resume` action with `GOAL_TOOL_RESUME_PAUSED`; the user-facing command or Web control owns that transition. Durable fork lineage does not demote a resumed root; live subagent ownership does. +- **Current-turn input projection.** A host-only projection retains human-input presence and admitted goal-round identifiers, resets at each turn boundary, and reconstructs these facts from committed events when registered. - **Host attestation of human input.** `{ kind: 'user' }` is assigned by `Agent.followup()` and `steer()` when their caller omits a source, so plugins, schedulers, and other non-human producers must pass their own source rather than inheriting human authority. - **System-prompt guidance with the configured threshold.** The package registers one `tool:goal` system-prompt section whose fixed text interpolates `blockedAfterConsecutiveRounds`; the same value is the hard lower bound enforced at execution. - **Wrap-up context for terminal rounds.** A successful autonomous `complete` or `blocked` defers a closing `` or `` instruction so the model addresses the user once before the turn ends; direct-human mutations never defer this context. diff --git a/packages/goal/tool-goal/README.zh.md b/packages/goal/tool-goal/README.zh.md index 1ec0a8e1f8..9e868061c2 100644 --- a/packages/goal/tool-goal/README.zh.md +++ b/packages/goal/tool-goal/README.zh.md @@ -69,6 +69,7 @@ kind: "package-reference" ### 设计 - **执行时权限。** 每次调用都解析精确活跃 agent、其继承的 `AgentRegistry` initiator、running 状态与开放轮次;`create`、`edit`、`pause` 和 `resume` 还要求运行时根 agent 的当前轮次中存在已接受的 `{ kind: 'user' }` 消息或 steering 事件。持久的 paused goal 会让 `resume` 以 `GOAL_TOOL_RESUME_PAUSED` 失败;面向用户的命令或 Web 控件拥有该转换。持久 fork 谱系不会降低已恢复根 agent 的等级;活跃 subagent 所有权会降低。 +- **当前轮输入投影。** 仅供 Host 使用的投影保存是否有人类输入及已接纳的目标轮次标识,在每个轮次边界重置,并在注册时从已提交事件重建这些事实。 - **人类输入的宿主证明。** `Agent.followup()` 与 `steer()` 会在调用方省略 source 时分配 `{ kind: 'user' }`,因此插件、调度器与其他非人类生产方必须传入自己的 source,不能继承人类权限。 - **带配置阈值的系统提示词指引。** 本包注册一个 `tool:goal` 系统提示词章节,其固定文本插入 `blockedAfterConsecutiveRounds`;同一数值就是执行时强制执行的硬下限。 - **终局 Round 的结束上下文。** 成功的自主 `complete` 或 `blocked` 会延后一条 `` 或 `` 结束指令,让模型在轮次结束前向用户做一次交代;人类直接变更绝不会延后该上下文。 diff --git a/packages/goal/tool-goal/package.json b/packages/goal/tool-goal/package.json index e5c0e9d8e7..65a9e48f4b 100644 --- a/packages/goal/tool-goal/package.json +++ b/packages/goal/tool-goal/package.json @@ -39,7 +39,8 @@ "@deepseek-ai/dsh-session-projection": "workspace:*" }, "dependencies": { - "@deepseek-ai/schemastery": "workspace:~" + "@deepseek-ai/schemastery": "workspace:~", + "zod": "4.4.3" }, "devDependencies": { "@deepseek-ai/cordis-plugin-loader": "workspace:~", diff --git a/packages/goal/tool-goal/src/authority.ts b/packages/goal/tool-goal/src/authority.ts index eadc0bd8f9..6214005230 100644 --- a/packages/goal/tool-goal/src/authority.ts +++ b/packages/goal/tool-goal/src/authority.ts @@ -2,17 +2,57 @@ import type { Context } from '@deepseek-ai/cordis' import type { Agent } from '@deepseek-ai/dsh-agent' -import type { GoalView } from '@deepseek-ai/dsh-goal' +import { GoalId, type GoalView } from '@deepseek-ai/dsh-goal' import { HarnessError } from '@deepseek-ai/dsh-llm' -import type { SessionEvent, SessionSeq } from '@deepseek-ai/dsh-session' import type { ToolRunContext } from '@deepseek-ai/dsh-tools' -import type {} from '@deepseek-ai/dsh-session-projection' +import type { ProjectionDefinition, ProjectionHandle } from '@deepseek-ai/dsh-session-projection' +import { z as zod } from 'zod' -/** The calling agent plus the immutable event cut and open-turn start seq used for authority checks. */ +const turnAuthoritySchema = zod.object({ + open: zod.boolean(), + directHuman: zod.boolean(), + goalRounds: zod.array(zod.object({ + goalId: zod.string().transform(GoalId), + revision: zod.number().int().positive(), + round: zod.number().int().positive(), + })), +}) + +/** Accepted input authority for the currently open turn, reset at both turn boundaries. */ +type TurnAuthority = zod.infer + +declare module '@deepseek-ai/dsh-session-projection/types' { + interface SessionProjectionStateMap { + /** Input sources admitted in the current turn for goal-tool authorization. */ + goalToolAuthority: TurnAuthority + } +} + +const turnAuthorityProjection = { + key: 'goalToolAuthority', + stateVersion: 1, + stateSchema: turnAuthoritySchema, + init: (): TurnAuthority => ({ open: false, directHuman: false, goalRounds: [] }), + apply: (state, event) => { + if (event.type === 'turn/start' || event.type === 'turn/end') { + return { open: event.type === 'turn/start', directHuman: false, goalRounds: [] } + } + if (!state.open || event.type !== 'user/message') return state + const source = event.data.source + if (source.kind === 'user') return state.directHuman ? state : { ...state, directHuman: true } + if (source.kind === 'goal') { + const { goalId, revision, round } = source + return { ...state, goalRounds: [...state.goalRounds, { goalId, revision, round }] } + } + // Other message producers do not grant goal-tool authority. + return state + }, +} satisfies ProjectionDefinition<'goalToolAuthority', TurnAuthority> + +/** The authenticated caller and immutable authority facts captured for one tool call. */ export interface GoalToolExecution { readonly agent: Agent - readonly events: readonly SessionEvent[] - readonly openTurnStartSeq: SessionSeq + readonly authority: TurnAuthority } /** Hard authority granted to one state-changing call. */ @@ -25,51 +65,44 @@ function reject(message: string, code = 'GOAL_TOOL_AUTHORITY_REQUIRED'): never { throw new HarnessError(message, code) } -/** Resolve the immutable event cut and open-turn boundary without copying the turn suffix. */ -function openTurnEvents( - ctx: Context, - agent: Agent, -): Pick { - // oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred. - const events = agent.session.snapshotEvents() - const boundary = ctx.sessionProjections.stateOf(agent.session, 'turnBoundary') - if (boundary === undefined || boundary.openTurnStartSeq === null) { - reject('goal tools require an open model turn', 'GOAL_TOOL_DRIVER_REQUIRED') - } - return { events, openTurnStartSeq: boundary.openTurnStartSeq } -} +/** Resolves goal-tool authority from the live driver and its committed turn. */ +export class GoalToolExecutionResolver { + private authority!: ProjectionHandle -/** - * Resolve and authenticate the calling agent and its driver boundary. - * @param ctx - Context carrying the live agent registry. - * @param exec - Tool execution metadata supplied by the registry. - * @returns The authenticated agent, immutable event cut, and open-turn boundary. - */ -export function goalToolExecution(ctx: Context, exec: ToolRunContext): GoalToolExecution { - const agent = exec.agent - if (agent === undefined) { - return reject('goal tools require a calling agent', 'GOAL_TOOL_AGENT_REQUIRED') - } - if (ctx.agents.get(agent.id) !== agent || agent.status !== 'running' - || ctx.agents.currentInitiator() !== agent) { - return reject( - 'goal tools require the exact live calling agent inside its active driver', - 'GOAL_TOOL_DRIVER_REQUIRED', - ) - } - return { agent, ...openTurnEvents(ctx, agent) } -} + /** @param ctx - Context carrying the live agent registry and session projections. */ + constructor(private readonly ctx: Context) {} -/** Whether the captured open turn contains an event accepted by `predicate`. */ -function someOpenTurnEvent( - execution: GoalToolExecution, - predicate: (event: SessionEvent) => boolean, -): boolean { - for (let seq = execution.openTurnStartSeq + 1; seq < execution.events.length; seq += 1) { - const event = execution.events[seq] - if (event !== undefined && predicate(event)) return true + /** + * Acquire projection readers before exposing dependent operations. + * @returns when all required registrations are ready. + */ + async initialize(): Promise { + this.authority = await this.ctx.sessionProjections.register(turnAuthorityProjection) + } + + /** + * Authenticate the caller and capture its current turn's accepted input sources. + * @param exec - Tool execution carrying the live calling agent. + * @returns the caller and projected authority facts. + */ + resolve(exec: ToolRunContext): GoalToolExecution { + const agent = exec.agent + if (agent === undefined) { + return reject('goal tools require a calling agent', 'GOAL_TOOL_AGENT_REQUIRED') + } + if (this.ctx.agents.get(agent.id) !== agent || agent.status !== 'running' + || this.ctx.agents.currentInitiator() !== agent) { + return reject( + 'goal tools require the exact live calling agent inside its active driver', + 'GOAL_TOOL_DRIVER_REQUIRED', + ) + } + const authority = this.authority.stateOf(agent.session) + if (!authority.open) { + reject('goal tools require an open model turn', 'GOAL_TOOL_DRIVER_REQUIRED') + } + return { agent, authority } } - return false } /** @@ -79,17 +112,14 @@ function someOpenTurnEvent( */ function hasDirectHumanInput(ctx: Context, execution: GoalToolExecution): boolean { if (!ctx.agents.roots().includes(execution.agent)) return false - return someOpenTurnEvent(execution, event => - event.type === 'user/message' && event.data.source.kind === 'user') + return execution.authority.directHuman } /** Whether this turn is the current goal's exact admitted round. */ function isMatchingGoalRound(execution: GoalToolExecution, goal: GoalView): boolean { - return someOpenTurnEvent(execution, event => event.type === 'user/message' - && event.data.source.kind === 'goal' - && event.data.source.goalId === goal.id - && event.data.source.revision === goal.revision - && event.data.source.round === goal.roundsStarted) + return execution.authority.goalRounds.some(round => round.goalId === goal.id + && round.revision === goal.revision + && round.round === goal.roundsStarted) } /** diff --git a/packages/goal/tool-goal/src/index.ts b/packages/goal/tool-goal/src/index.ts index 9f47f1a77d..a364dec967 100644 --- a/packages/goal/tool-goal/src/index.ts +++ b/packages/goal/tool-goal/src/index.ts @@ -20,12 +20,13 @@ import { defineTool } from '@deepseek-ai/dsh-tools' import type { GenericCallView } from '@deepseek-ai/dsh-tools' import { completionAuthority, - goalToolExecution, + GoalToolExecutionResolver, requireDirectHuman, } from './authority.ts' import { renderWrapupContext } from './wrapup.ts' export const name = 'tool-goal' + export const inject = ['agents', 'goals', 'tools', 'systemPrompt', 'sessionProjections'] /** Model policy and hard lower bounds for goal-state updates. */ @@ -184,7 +185,9 @@ function present(title: string, kind: 'read' | 'other', rawInput?: unknown): Gen } /** Register the three Codex-shaped goal tools and their shared policy section. */ -export function apply(ctx: Context, config: Config): void { +export async function apply(ctx: Context, config: Config): Promise { + const executionResolver = new GoalToolExecutionResolver(ctx) + await executionResolver.initialize() const resolved = resolveConfig(config) ctx.systemPrompt.section({ name: 'tool:goal', @@ -198,7 +201,7 @@ export function apply(ctx: Context, config: Config): void { parameters: {}, output: GOAL_OUTPUT, execute(_args, exec) { - const execution = goalToolExecution(ctx, exec) + const execution = executionResolver.resolve(exec) return Promise.resolve(goalValue(ctx.goals.get(execution.agent))) }, presentCall: () => present('Read current goal', 'read'), @@ -220,7 +223,7 @@ export function apply(ctx: Context, config: Config): void { }, output: GOAL_OUTPUT, execute(args, exec) { - const execution = goalToolExecution(ctx, exec) + const execution = executionResolver.resolve(exec) requireDirectHuman(ctx, execution) const goal = ctx.goals.create(execution.agent, { objective: args.objective, @@ -253,7 +256,7 @@ export function apply(ctx: Context, config: Config): void { }, output: GOAL_OUTPUT, execute(args, exec) { - const execution = goalToolExecution(ctx, exec) + const execution = executionResolver.resolve(exec) const ref = goalRef(args.goal_id, args.revision) const replacements = { ...hasText(args.objective) ? { objective: args.objective } : {}, diff --git a/packages/goal/tool-goal/tests/tool-goal.spec.ts b/packages/goal/tool-goal/tests/tool-goal.spec.ts index ecc7822bce..0bb6a91a48 100644 --- a/packages/goal/tool-goal/tests/tool-goal.spec.ts +++ b/packages/goal/tool-goal/tests/tool-goal.spec.ts @@ -3,7 +3,6 @@ import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' import AgentRegistry, { agentEvents } from '@deepseek-ai/dsh-agent' import type { Agent, AgentStatus, Inbox } from '@deepseek-ai/dsh-agent' -import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent-loop' import GoalService, { GoalId } from '@deepseek-ai/dsh-goal' import type { GoalRef } from '@deepseek-ai/dsh-goal' import { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm' @@ -19,6 +18,7 @@ import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime from '@deepseek-ai/dsh-tools' import type { ToolExecutionResult } from '@deepseek-ai/dsh-tools' +import * as GoalTools from '@deepseek-ai/dsh-tool-goal' import * as toolGoal from '@deepseek-ai/dsh-tool-goal' import { createInboxStub } from '@deepseek-ai/dsh-agent-loop-testkit' @@ -104,9 +104,8 @@ async function harness(config: toolGoal.Config = {}) { await ctx.plugin(SystemPrompt) await ctx.plugin(AgentRegistry) await ctx.plugin(ToolRuntime) - ctx.sessionProjections.register(turnBoundaryProjectionDefinition) await ctx.plugin(GoalService) - const fiber = await ctx.plugin(toolGoal, config) + const fiber = await ctx.plugin(GoalTools, config) const root = stubAgent(`goal-tool-root-${Math.random()}`, undefined, ctx) await ctx.agents.register(root.agent) return { ctx, fiber, root } @@ -202,25 +201,27 @@ describe('goal tool registration and presentation', () => { it('fails invalid direct config before registering anything', async () => { const ctx = new Context() + await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(SystemPrompt) await ctx.plugin(AgentRegistry) await ctx.plugin(ToolRuntime) await ctx.plugin(GoalService) - expect(() => { - toolGoal.apply(ctx, { blockedAfterConsecutiveRounds: 1.5 }) - }).toThrow( + await expect(GoalTools.apply(ctx, { blockedAfterConsecutiveRounds: 1.5 })).rejects.toThrow( 'blockedAfterConsecutiveRounds must be a positive safe integer', ) expect(ctx.tools.get('get_goal')).toBeUndefined() }) - it('resolves the direct-apply default before registration', async () => { + it('resolves the direct-constructor default before registration', async () => { const ctx = new Context() + await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(SystemPrompt) await ctx.plugin(AgentRegistry) await ctx.plugin(ToolRuntime) await ctx.plugin(GoalService) - toolGoal.apply(ctx, {}) + await GoalTools.apply(ctx, {}) const section = (await ctx.systemPrompt.assemble()).sections.find(item => item.name === 'tool:goal') expect(section?.text).toContain('at least 3 consecutive goal rounds') }) @@ -331,7 +332,27 @@ describe('goal tool execution authority', () => { expect(malformed.error?.info?.code).toBe('GOAL_TOOL_AUTHORITY_REQUIRED') }) - it('accepts direct human steering in a goal-sourced root turn', async () => { + it('does not carry human authority into a later non-human turn', async () => { + const { ctx, root } = await harness() + const turn = openTurn(root, { kind: 'user' }) + await execute(ctx, 'get_goal', {}, root.agent) + closeTurn(root, turn) + openTurn(root, { kind: 'test' }) + const result = await execute(ctx, 'create_goal', { objective: 'unauthorized' }, root.agent) + expect(result.error?.info?.code).toBe('GOAL_TOOL_AUTHORITY_REQUIRED') + expect(ctx.goals.get(root.agent)).toBeUndefined() + }) + + it('reconstructs current-turn authority when the tools mount after input was committed', async () => { + const { ctx, root, fiber } = await harness() + await fiber.dispose() + openTurn(root, { kind: 'user' }) + await ctx.plugin(GoalTools, {}) + const result = await execute(ctx, 'create_goal', { objective: 'accepted before tool load' }, root.agent) + expect(resultGoal(result)).toMatchObject({ objective: 'accepted before tool load' }) + }) + + it.each([1, 2])('accepts %i direct human steering messages in a goal-sourced root turn', async (messageCount) => { const { ctx, root } = await harness() const humanTurn = openTurn(root, { kind: 'user' }) const created = ctx.goals.create(root.agent, { objective: 'steer me' }) @@ -339,10 +360,12 @@ describe('goal tool execution authority', () => { openTurn(root, { kind: 'goal', goalId: created.id, revision: created.revision, round: 1, }) - root.session.append('user/message', createUserMessage({ - content: [{ type: 'text', text: 'pause now' }], - source: { kind: 'user' }, - }), { surfaceOp: 'append' }) + for (let index = 0; index < messageCount; index++) { + root.session.append('user/message', createUserMessage({ + content: [{ type: 'text', text: 'pause now' }], + source: { kind: 'user' }, + }), { surfaceOp: 'append' }) + } const paused = await execute(ctx, 'update_goal', { goal_id: created.id, revision: created.revision, action: 'pause', }, root.agent) diff --git a/packages/interaction/permission-presets/README.i18n.yaml b/packages/interaction/permission-presets/README.i18n.yaml index 949ada7106..46968ede01 100644 --- a/packages/interaction/permission-presets/README.i18n.yaml +++ b/packages/interaction/permission-presets/README.i18n.yaml @@ -39,14 +39,14 @@ en: b01849a5fa73aae9 zh: cbd7fcf50117502b /deepseek-ai-dsh-permission-presets/understand-the-implementation/read-side-and-custom: - en: c9ca792038f44654 - zh: 83cc2641c800c114 + en: 76deeb5fbcdf7525 + zh: e96ad7e107947c23 /deepseek-ai-dsh-permission-presets/understand-the-implementation/session-pinning-and-blank-reuse: en: 893cea88bce22903 zh: 2431e19ced670511 /deepseek-ai-dsh-permission-presets/understand-the-implementation/catalog-projection-and-optional-command: - en: 2e79bec74274b054 - zh: 256526ab1b4b55e9 + en: 341f80202edd66a3 + zh: bd8ebb9d011b547f /deepseek-ai-dsh-permission-presets/further-exploration: en: ca27c3e438cffdc5 zh: ae5d25ae55e5d503 diff --git a/packages/interaction/permission-presets/README.md b/packages/interaction/permission-presets/README.md index e21a41a053..d1179a9926 100644 --- a/packages/interaction/permission-presets/README.md +++ b/packages/interaction/permission-presets/README.md @@ -86,7 +86,7 @@ The observable behavior is covered in [Use this package](#use-this-package); thi ### Read side and `custom` -`current(session)` reads the required `permissions` projection, whose unit folds the three whole-value knob events over the composition defaults (`ctx.shell.sandboxMode` and the approval config). The host state also retains whether `session/end-seed` has occurred, so session pinning distinguishes an explicitly empty restored seed from a genuinely fresh session without rescanning the log. A still-matching last selection wins shared-bundle ties, and a recorded Auto selection also matches the `never` approval policy; otherwise the first configured match wins; otherwise the derived `CUSTOM_PRESET` is returned. A missing projection key fails explicitly. +`current(session)` reads the required `permissions` projection, whose unit folds the three whole-value knob events over the composition defaults (`ctx.shell.sandboxMode` and the approval config). The host state also retains whether `session/end-seed` has occurred, so session pinning distinguishes an explicitly empty restored seed from a genuinely fresh session without rescanning the log. A still-matching last selection wins shared-bundle ties, and a recorded Auto selection also matches the `never` approval policy; otherwise the first configured match wins; otherwise the derived `CUSTOM_PRESET` is returned. The service acquires its state reader during activation. `optionOf(name)` returns a configured entry, the live Auto entry, or the display-only `custom` entry. It throws only when the name matches none of those; a withdrawn Auto entry is unavailable. @@ -96,7 +96,7 @@ Mounting pins every live and future session: a genuinely fresh session gains the ### Catalog, projection, and optional command -The service requires `ctx.sessionProjections` and registers a `permissions` projection containing only `currentValue`. Its process-level `catalog()` Remote returns one complete selectable snapshot. Registering or removing Auto emits the payload-free `permission-presets/catalog-changed` notification, so clients subscribe first and then re-read the catalog without appending a Session event, publishing a Session projection frame, or changing Session sequence. The `/permission` command registers only when a `ctx.commands` registry is composed. Calls that derive the current preset or pin an initial selection fail explicitly when the projection key is absent. +The service requires `ctx.sessionProjections` and awaits registration of `permissions`; its client view contains only `currentValue`. Its process-level `catalog()` Remote returns one complete selectable snapshot. Registering or removing Auto emits the payload-free `permission-presets/catalog-changed` notification, so clients subscribe first and then re-read the catalog without appending a Session event, publishing a Session projection frame, or changing Session sequence. The `/permission` command registers only when a `ctx.commands` registry is composed. Calls that derive the current preset or pin an initial selection use the retained projection handle. diff --git a/packages/interaction/permission-presets/README.zh.md b/packages/interaction/permission-presets/README.zh.md index 0477b4197e..6afa0a0163 100644 --- a/packages/interaction/permission-presets/README.zh.md +++ b/packages/interaction/permission-presets/README.zh.md @@ -86,7 +86,7 @@ kind: "package-reference" ### 读取侧与 `custom` -`current(session)` 读取必需的 `permissions` 投影;该单元在组合默认值(`ctx.shell.sandboxMode` 与审批配置)之上折叠三个全量值旋钮事件。host 状态还会保留 `session/end-seed` 是否已经出现,使会话固定无需重扫日志即可区分显式为空的恢复 seed 与真正的新会话。仍匹配的最近选择在共享捆绑时胜出,已记录的 Auto 选择在 `never` 审批策略下也匹配;否则配置表中的第一个匹配项胜出;否则返回推导出的 `CUSTOM_PRESET`。投影 key 缺失时会显式失败。 +`current(session)` 读取必需的 `permissions` 投影;该单元在组合默认值(`ctx.shell.sandboxMode` 与审批配置)之上折叠三个全量值旋钮事件。host 状态还会保留 `session/end-seed` 是否已经出现,使会话固定无需重扫日志即可区分显式为空的恢复 seed 与真正的新会话。仍匹配的最近选择在共享捆绑时胜出,已记录的 Auto 选择在 `never` 审批策略下也匹配;否则配置表中的第一个匹配项胜出;否则返回推导出的 `CUSTOM_PRESET`。该服务在激活期间取得状态读取句柄。 `optionOf(name)` 返回配置条目、存活的 Auto 条目或仅供显示的 `custom` 条目。只有名称不匹配这三者时才抛错;已撤回的 Auto 条目不可用。 @@ -96,7 +96,7 @@ kind: "package-reference" ### 目录、投影与可选命令 -该服务要求 `ctx.sessionProjections`,并注册一个只含 `currentValue` 的 `permissions` 投影。进程级 `catalog()` Remote 返回一份完整的可选快照。注册或移除 Auto 会发出无 payload 的 `permission-presets/catalog-changed` 通知,因此客户端先订阅再重新读取目录;该过程不会追加 Session 事件、发布 Session 投影帧或改变 Session 序列。`/permission` 命令仅在组合了 `ctx.commands` 注册表时注册。派生当前预设或固定初始选择的调用会在投影 key 缺失时显式失败。 +该服务要求 `ctx.sessionProjections`,并等待 `permissions` 注册完成;其客户端视图只含 `currentValue`。进程级 `catalog()` Remote 返回一份完整的可选快照。注册或移除 Auto 会发出无 payload 的 `permission-presets/catalog-changed` 通知,因此客户端先订阅再重新读取目录;该过程不会追加 Session 事件、发布 Session 投影帧或改变 Session 序列。`/permission` 命令仅在组合了 `ctx.commands` 注册表时注册。派生当前预设或固定初始选择的调用使用保留的投影句柄。 diff --git a/packages/interaction/permission-presets/src/index.ts b/packages/interaction/permission-presets/src/index.ts index 8135d74282..a3df0f8d74 100644 --- a/packages/interaction/permission-presets/src/index.ts +++ b/packages/interaction/permission-presets/src/index.ts @@ -15,7 +15,7 @@ import type {} from '@deepseek-ai/dsh-settings' import type { Volatile } from '@deepseek-ai/cordis' -import { Context } from '@deepseek-ai/cordis' +import { Context, Service } from '@deepseek-ai/cordis' import { CommandDefinitionId } from '@deepseek-ai/dsh-commands/brand' import z from '@deepseek-ai/schemastery' import { z as zod } from 'zod' @@ -29,7 +29,7 @@ import type {} from '@deepseek-ai/dsh-shell' import type { ApprovalPolicy } from '@deepseek-ai/dsh-user-approval' import { APPROVAL_POLICIES, setApprovalPolicy } from '@deepseek-ai/dsh-user-approval' // Type-only: resolves the required projection service and optional settings/command children. -import type {} from '@deepseek-ai/dsh-session-projection' +import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection' import type {} from '@deepseek-ai/dsh-commands' import type { PermissionCatalog, PermissionSelection, PresetOption } from './types.ts' @@ -204,6 +204,8 @@ export class PermissionPresetService extends TypertRemoteService { private autoAdmit: (() => void) | undefined private defaultSettings: () => PermissionSettings + private projection!: ProjectionHandle + constructor(ctx: Context, config: Config) { super(ctx, 'permissionPresets') @@ -230,11 +232,14 @@ export class PermissionPresetService extends TypertRemoteService { if (!Object.hasOwn(this.presets, defaultPreset)) throw new Error(`permission: unknown default preset "${defaultPreset}"`) return { defaultPreset } } + } + protected async [Service.init](): Promise { + const ctx = this.ctx const selectionSchema = zod.object({ currentValue: zod.string().min(1), }) as zod.ZodType - ctx.sessionProjections.register({ + this.projection = await ctx.sessionProjections.register({ key: 'permissions', stateVersion: 2, stateSchema: permissionStateSchema, @@ -326,8 +331,7 @@ export class PermissionPresetService extends TypertRemoteService { } private permissionState(session: Session): PermissionProjectionState { - const state = this.ctx.sessionProjections.stateOf(session, 'permissions') - if (state === undefined) throw new Error('permission: permissions session projection is not registered') + const state = this.projection.stateOf(session) return state } diff --git a/packages/interaction/permission-presets/tests/permission-presets.spec.ts b/packages/interaction/permission-presets/tests/permission-presets.spec.ts index 10de7a4f31..6641d2c259 100644 --- a/packages/interaction/permission-presets/tests/permission-presets.spec.ts +++ b/packages/interaction/permission-presets/tests/permission-presets.spec.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from 'vitest' +import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' import SessionStore, { Session, @@ -65,17 +65,17 @@ describe('permission preset fold', () => { it('folds the latest preset selection and steps over unrelated events', async () => { const ctx = await mounted() const session = freshSession('sess-fold') - const presetOf = () => ctx.sessionProjections.stateOf(session, 'permissions')?.preset ?? null - expect(presetOf()).toBeNull() + const presetOf = () => ctx.sessionProjections.checkpoint(session).permissions?.val + expect(presetOf()).toMatchObject({ preset: null }) session.append('permission/preset', { preset: 'danger-full-access' }) session.append('permission/preset', { preset: 'workspace-write' }) - expect(presetOf()).toBe('workspace-write') + expect(presetOf()).toMatchObject({ preset: 'workspace-write' }) // The knob fold steps over non-preset events to the latest selection. session.append('sandbox/mode', { mode: 'read-only' }) - expect(presetOf()).toBe('workspace-write') + expect(presetOf()).toMatchObject({ preset: 'workspace-write' }) const seeded = Session.create(SessionId('sess-fold-seeded'), []) - expect(ctx.sessionProjections.stateOf(seeded, 'permissions')?.seeded).toBe(true) + expect(ctx.sessionProjections.checkpoint(seeded).permissions?.val).toMatchObject({ seeded: true }) }) }) @@ -85,13 +85,6 @@ describe('PermissionPresetService', () => { expect(ctx.get('permissionPresets')).toBeUndefined() }) - it('fails when the permissions projection key is absent', async () => { - const ctx = await mounted() - vi.spyOn(ctx.sessionProjections, 'stateOf').mockReturnValue(undefined) - expect(() => ctx.permissionPresets.current(freshSession('missing-permission-projection-key'))) - .toThrow('permission: permissions session projection is not registered') - }) - it('advertises the preset table in declaration order and resolves bundles', async () => { const ctx = await mounted() expect(ctx.permissionPresets.names).toEqual(['workspace-write', 'danger-full-access']) diff --git a/packages/interaction/user-questions/src/index.ts b/packages/interaction/user-questions/src/index.ts index 06cb67b8b6..1a93231646 100644 --- a/packages/interaction/user-questions/src/index.ts +++ b/packages/interaction/user-questions/src/index.ts @@ -7,16 +7,16 @@ * @module @deepseek-ai/dsh-user-questions */ -import { Context } from '@deepseek-ai/cordis' +import { Context, Service } from '@deepseek-ai/cordis' import type {} from '@deepseek-ai/dsh-agent' import { createUserMessage, HarnessError, type MessageId, type ToolCallId, type UserMessage } from '@deepseek-ai/dsh-llm' import type { Session } from '@deepseek-ai/dsh-session' import { scopeTarget } from '@deepseek-ai/dsh-scope' import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol' import type { Agent } from '@deepseek-ai/dsh-agent' -import type {} from '@deepseek-ai/dsh-session-projection' +import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection' import z from '@deepseek-ai/schemastery' -import { userQuestionProjectionDefinition } from './projection.ts' +import { userQuestionProjectionDefinition, type UserQuestionProjectionState } from './projection.ts' import { TimedQuestionWait } from './timed-wait.ts' declare module '@deepseek-ai/cordis' { @@ -78,14 +78,12 @@ type QueuedReply = { messageId: MessageId; claimedTurn?: number } /** `ctx.userQuestions`: validation plus the scoped answerer waterfall. */ export class UserQuestionService extends TypertRemoteService { static Config = z.object({}) + private projection: ProjectionHandle | undefined private readonly waits = new Map>() private readonly queuedReplies = new WeakMap>() constructor(ctx: Context) { super(ctx, 'userQuestions') - ctx.inject(['sessionProjections'], (projectionCtx) => { - projectionCtx.sessionProjections.register(userQuestionProjectionDefinition) - }) ctx.effect(() => () => { for (const calls of this.waits.values()) { for (const wait of calls.values()) wait.close(abortedQuestion()) @@ -128,6 +126,14 @@ export class UserQuestionService extends TypertRemoteService { calls.delete(callId) } + protected async [Service.init](): Promise { + const binding = this.ctx.inject(['sessionProjections'], async (ctx) => { + this.projection = await ctx.sessionProjections.register(userQuestionProjectionDefinition) + return () => { this.projection = undefined } + }) + if (this.ctx.get('sessionProjections') !== undefined) await binding + } + private assertLiveRoot(agent: Agent): void { const agents = this.ctx.get('agents') if (agents === undefined || agents.get(agent.id) !== agent) { @@ -144,8 +150,7 @@ export class UserQuestionService extends TypertRemoteService { } private continued(agent: Agent): readonly PendingUserQuestion[] { - const state = this.ctx.get('sessionProjections')?.stateOf(agent.session, 'userQuestions') - return (state?.questions.active ?? []).filter(question => question.state === 'continued') + return this.projection?.stateOf(agent.session).questions.active.filter(question => question.state === 'continued') ?? [] } /** diff --git a/packages/interaction/user-questions/tests/user-questions.spec.ts b/packages/interaction/user-questions/tests/user-questions.spec.ts index f0f744259a..0cbb2a5031 100644 --- a/packages/interaction/user-questions/tests/user-questions.spec.ts +++ b/packages/interaction/user-questions/tests/user-questions.spec.ts @@ -895,7 +895,7 @@ describe('late replies', () => { error: { name: 'SessionFormatError', code: TOOL_OUTCOME_UNKNOWN }, }, { surfaceOp: 'append' }) - expect(ctx.sessionProjections.stateOf(agent.session, 'userQuestions')?.questions).toEqual({ active: [], settled: [] }) + expect(ctx.sessionProjections.checkpoint(agent.session).userQuestions?.val).toMatchObject({ questions: { active: [], settled: [] } }) expect(ctx.userQuestions.answer(agent, timedCallId, { answers: [{ id: 'scope', selected: [] }] })).toBe(false) expect(agent.steer).not.toHaveBeenCalled() await ctx.fiber.dispose() diff --git a/packages/llm/llm-retry/src/index.ts b/packages/llm/llm-retry/src/index.ts index 89ffc14c40..60cca5ba2f 100644 --- a/packages/llm/llm-retry/src/index.ts +++ b/packages/llm/llm-retry/src/index.ts @@ -19,6 +19,7 @@ export type { LlmRetryEventData, LlmRetryStartedEventData } from './types.ts' export { RetryId } from './brand.ts' export const name = 'llm-retry' + export const inject = ['agents', 'sessionProjections'] /** This policy executor has no config; providers own `retryPolicy`. */ @@ -95,12 +96,6 @@ function cancellableDelay(delayMs: number, signal: AbortSignal): Promise { validateConfig(config) - ctx.sessionProjections.register({ + const projection = await ctx.sessionProjections.register({ key: 'llmRetry', stateVersion: 1, stateSchema: llmRetryStateSchema, @@ -217,7 +219,7 @@ export function apply(ctx: Context, config: Config = {}, internals: RetryInterna } const policyKey = retryPolicyKey(policy) - const retryState = ctx.sessionProjections.stateOf(agent.session, 'llmRetry') as LlmRetryState + const retryState = projection.stateOf(agent.session) const previous = retryState[retryStateKey(provider, policyKey)] const previousRetry = previous?.retry ?? 0 if (policy.mode === 'normal' && previousRetry >= policy.maxRetries) return next() diff --git a/packages/llm/llm-retry/tests/retry.spec.ts b/packages/llm/llm-retry/tests/retry.spec.ts index 3adb88de05..43ead12c94 100644 --- a/packages/llm/llm-retry/tests/retry.spec.ts +++ b/packages/llm/llm-retry/tests/retry.spec.ts @@ -20,6 +20,7 @@ import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools' import AgentRegistry from '@deepseek-ai/dsh-agent' import type { Agent, RequestErrorAction } from '@deepseek-ai/dsh-agent' import AgentLoop from '@deepseek-ai/dsh-agent-loop' +import type { Config as retryConfig, RetryInternals as retryRetryInternals } from '../src/index.ts' import * as retry from '../src/index.ts' type ScriptEntry = Error | Iterable | AsyncIterable @@ -103,7 +104,7 @@ async function harness( adapter: ScriptedAdapter, policies: Readonly> = { mock: normalConfig() }, beforeRetry?: (ctx: Context) => void, - internals: retry.RetryInternals = {}, + internals: retryRetryInternals = {}, ): Promise<{ ctx: Context; retryFiber: Fiber; disposeAdapter: () => void }> { const ctx = new Context() await ctx.plugin(LlmRuntime) @@ -114,8 +115,8 @@ async function harness( await ctx.plugin(AgentRegistry) beforeRetry?.(ctx) adapter.configureRetryPolicies(policies) - const retryFiber = await ctx.plugin(Object.assign((inner: Context) => { - retry.apply(inner, {}, internals) + const retryFiber = await ctx.plugin(Object.assign(async (inner: Context) => { + await retry.apply(inner, {}, internals) }, { inject: retry.inject })) await ctx.plugin(AgentLoop, { agents: [] }) const disposeAdapter = ctx.llm.registerAdapter(['mock', 'other'], adapter) @@ -1047,23 +1048,21 @@ describe('provider-routed retry policy', () => { expect(vi.getTimerCount()).toBe(0) }) - it('rejects retry policy configured on the executor instead of a provider', () => { - expectTypeOf<{}>().toExtend() - expectTypeOf<{ retryPolicy: { mode: 'always' } }>().not.toExtend() + it('rejects retry policy configured on the executor instead of a provider', async () => { + expectTypeOf<{}>().toExtend() + expectTypeOf<{ retryPolicy: { mode: 'always' } }>().not.toExtend() const ctx = new Context() - // `apply` registers its projection unit first; the registry is a required - // injection, so the direct-apply path must carry it too. new SessionProjectionRegistry(ctx) - expect(() => { - retry.apply(ctx, { retryPolicy: { mode: 'always' } } as unknown as retry.Config) - }).toThrow(/retryPolicy belongs under each provider/) + await expect(async () => { + await retry.apply(ctx, { retryPolicy: { mode: 'always' } } as unknown as retryConfig) + }).rejects.toThrow(/retryPolicy belongs under each provider/) }) - it('rejects unknown executor config', () => { + it('rejects unknown executor config', async () => { const ctx = new Context() new SessionProjectionRegistry(ctx) - expect(() => { - retry.apply(ctx, { retryPolciy: {} } as unknown as retry.Config) - }).toThrow(/unknown key "retryPolciy"/) + await expect(async () => { + await retry.apply(ctx, { retryPolciy: {} } as unknown as retryConfig) + }).rejects.toThrow(/unknown key "retryPolciy"/) }) }) diff --git a/packages/llm/token-meter/src/index.ts b/packages/llm/token-meter/src/index.ts index f721d26219..7c1ac24432 100644 --- a/packages/llm/token-meter/src/index.ts +++ b/packages/llm/token-meter/src/index.ts @@ -38,6 +38,7 @@ import type { MeterSurfaceNode } from './surface-fold.ts' import { priceSurface } from './route-pricing.ts' export type * from './types.ts' +export { contextPressureProjectionDefinition } from './usage-projection.ts' // Module-edge re-export: forces the emitted index.d.ts to import the // projection-unit modules, so their SessionProjectionStateMap augmentations load // in aggregate programs that only import the package root. @@ -110,10 +111,13 @@ export class TokenMeter extends Service { constructor(ctx: Context, config: TokenMeterConfig = {}) { super(ctx, 'tokenMeter') validateConfigKeys(config) + } - ctx.sessionProjections.register(tokenUsageProjectionDefinition) - ctx.sessionProjections.register(contextPressureProjectionDefinition) - ctx.sessionProjections.register(contextBreakdownProjectionDefinition) + protected async [Service.init](): Promise { + const ctx = this.ctx + await ctx.sessionProjections.register(tokenUsageProjectionDefinition) + await ctx.sessionProjections.register(contextPressureProjectionDefinition) + await ctx.sessionProjections.register(contextBreakdownProjectionDefinition) // Readers catch up independently, while eager observation bounds ordinary // read latency without creating state for sessions no consumer has read. diff --git a/packages/llm/token-meter/tests/context-breakdown-projection.spec.ts b/packages/llm/token-meter/tests/context-breakdown-projection.spec.ts index d6fee3cb04..8eba5db571 100644 --- a/packages/llm/token-meter/tests/context-breakdown-projection.spec.ts +++ b/packages/llm/token-meter/tests/context-breakdown-projection.spec.ts @@ -370,8 +370,9 @@ describe('contextBreakdown session projection', () => { const first = appendUser(session, 'the first of many messages') for (let index = 0; index < 24; index += 1) appendUser(session, `message number ${index} with some text`) const last = appendUser(session, 'the last message before compaction') + const projection = await ctx.sessionProjections.register(contextBreakdownProjectionDefinition) const state = () => { - const current = ctx.sessionProjections.stateOf(session, 'contextBreakdown') + const current = projection.stateOf(session) if (current === undefined) throw new Error('registered context breakdown has no state') return current } diff --git a/packages/llm/token-meter/tests/route-pricing.spec.ts b/packages/llm/token-meter/tests/route-pricing.spec.ts index bf11f1b039..08959f288e 100644 --- a/packages/llm/token-meter/tests/route-pricing.spec.ts +++ b/packages/llm/token-meter/tests/route-pricing.spec.ts @@ -1,5 +1,5 @@ import { imageOffloadProjection } from '@deepseek-ai/dsh-compaction-image-offload/projection' -import { describe, expect, it } from 'vitest' +import { describe, expect, it, onTestFinished } from 'vitest' import { Context } from '@deepseek-ai/cordis' import { LlmRuntime, LlmAdapter, createMessage, createToolResultMessage, createUserMessage, projectFilesToText, ToolCallId, @@ -76,6 +76,7 @@ interface Harness { async function harness(pricing: (model: string) => LlmImageRequestPricing | undefined): Promise { const ctx = new Context() + onTestFinished(async () => { await ctx.fiber.dispose() }) new SessionProjectionRegistry(ctx) ctx.provide('attachments', { fileHostPath: (ref: FileAttachmentRef) => `/host/${ref.name}`, @@ -85,7 +86,8 @@ async function harness(pricing: (model: string) => LlmImageRequestPricing | unde } as never) const llm = new LlmRuntime(ctx) llm.registerAdapter(['mock'], new PricingAdapter(pricing)) - const meter = new TokenMeter(ctx) + await ctx.plugin(TokenMeter) + const meter = ctx.tokenMeter return { ctx, meter, session: Session.create(SessionId('route-priced'), undefined, undefined, undefined, [imageOffloadProjection]) } } diff --git a/packages/llm/token-meter/tests/token-meter.spec.ts b/packages/llm/token-meter/tests/token-meter.spec.ts index f207905573..818fa45921 100644 --- a/packages/llm/token-meter/tests/token-meter.spec.ts +++ b/packages/llm/token-meter/tests/token-meter.spec.ts @@ -106,8 +106,7 @@ function appendSuccessfulCall( function meter(config: TokenMeterConfig = {}): TokenMeter { const ctx = new Context() - // The registry is a required injection of the service (its three projection - // units register in the constructor); mount it synchronously. + // Pure measurement tests use the service without mounting its projection contributions. new SessionProjectionRegistry(ctx) return new TokenMeter(ctx, config) } @@ -137,8 +136,13 @@ describe('TokenMeter configuration and registration', () => { await ctx.plugin(SessionProjectionRegistry) const fiber = await ctx.plugin(TokenMeter) expect(ctx.get('tokenMeter')).toBeInstanceOf(TokenMeter) + const session = ctx.sessions.create() + expect(Object.keys(ctx.sessionProjections.checkpoint(session)).sort()) + .toEqual(['contextBreakdown', 'contextPressure', 'tokenUsage']) await fiber.dispose() expect(ctx.get('tokenMeter')).toBeUndefined() + expect(ctx.sessionProjections.checkpoint(session)).toEqual({}) + await ctx.fiber.dispose() }) }) diff --git a/packages/plan/plan-mode/README.i18n.yaml b/packages/plan/plan-mode/README.i18n.yaml index fcd9bc5201..5123d81ae9 100644 --- a/packages/plan/plan-mode/README.i18n.yaml +++ b/packages/plan/plan-mode/README.i18n.yaml @@ -39,8 +39,8 @@ en: 0acc88b042a99380 zh: e4cf66b77f2fc7ae /deepseek-ai-dsh-plan-mode/understand-the-implementation/durable-state-and-step-boundary-appends: - en: ff946ab59a7b4a0f - zh: b1c79937770f57ad + en: 24166a02b6e81553 + zh: 24313c2153c9a6a6 /deepseek-ai-dsh-plan-mode/understand-the-implementation/the-plan-command: en: a7bf35c2a9da52bc zh: dea9f4b3a41f1a6d @@ -48,8 +48,8 @@ en: 56798d0f45fb5775 zh: 282e4a522ce6dae0 /deepseek-ai-dsh-plan-mode/understand-the-implementation/session-projection-unit: - en: 74cd6bdf50af51f5 - zh: 67609cf7202ececb + en: 42189cbdbd1ef615 + zh: 711142761cc29491 /deepseek-ai-dsh-plan-mode/understand-the-implementation/source-map: en: fa97e858495a8105 zh: 8be1fbd7e420ff8a diff --git a/packages/plan/plan-mode/README.md b/packages/plan/plan-mode/README.md index 35084ddb60..dce61a5e38 100644 --- a/packages/plan/plan-mode/README.md +++ b/packages/plan/plan-mode/README.md @@ -82,7 +82,7 @@ Plan mode is a product package, not a capability seam: there is no swappable bac ### Durable state and step-boundary appends -The package persists one log-only whole-value event, `plan/mode`, and the last logged value is the state. A mode change appends immediately when no turn is open; during an open turn it stays pending until the next accepted in-turn pre-step — the only append point while an agent runs — and an append failure cannot block the turn. The `set`/`get` service methods and their exact return states live in [`src/index.ts`](src/index.ts) and read the registered `plan` projection; the first dependent access fails explicitly when the registry or key is absent. +The package persists one log-only whole-value event, `plan/mode`, and the last logged value is the state. A mode change appends immediately when no turn is open; during an open turn it stays pending until the next accepted in-turn pre-step — the only append point while an agent runs — and an append failure cannot block the turn. The `set`/`get` service methods and their exact return states live in [`src/index.ts`](src/index.ts) and read the retained `plan` projection handle, acquired during activation. ### The `/plan` command @@ -94,7 +94,7 @@ The command child activates only when a commands service is composed. It maps ba ### Session projection unit -When `ctx.sessionProjections` is composed, the package registers the `plan` unit through optional injection. The unit turns logged `/plan` command runs into a candidate target, commits the logged state on `plan/mode`, and derives `{ active, pending }` for `view`, where `pending` is true only while an unsettled or successful selection differs from the logged state — a pure replay quantity recoverable from the log alone. The key merges into `SessionProjectionMap` from [`src/types.ts`](src/types.ts); the framework drives the unit, and unloading the plugin fiber unregisters the key. Plan-mode reads require this unit and the `turnBoundary` unit, and fail explicitly if the registry or either key is absent. +The package requires `ctx.sessionProjections` and awaits registration of the `plan` and `turnBoundary` units during activation. The unit turns logged `/plan` command runs into a candidate target, commits the logged state on `plan/mode`, and derives `{ active, pending }` for `view`, where `pending` is true only while an unsettled or successful selection differs from the logged state — a pure replay quantity recoverable from the log alone. The key merges into `SessionProjectionMap` from [`src/types.ts`](src/types.ts); the framework drives the unit, and unloading the plugin fiber unregisters the key. State reads use the retained handles and remain available after unregistration. ### Source map diff --git a/packages/plan/plan-mode/README.zh.md b/packages/plan/plan-mode/README.zh.md index 9d7023df69..b325573434 100644 --- a/packages/plan/plan-mode/README.zh.md +++ b/packages/plan/plan-mode/README.zh.md @@ -82,7 +82,7 @@ agent 完成计划后,会以 markdown 形式、从标题开头书写计划并 ### 持久状态与步骤边界追加 -本包持久化一条仅记日志、整值替换的事件 `plan/mode`,最后一条已记录值即为状态。没有轮次开启时,模式变更会立即追加;轮次开启期间,它保持待生效,直到下一个被接受的轮内 pre-step——agent 运行时唯一的追加点——且追加失败不能阻塞轮次。`set`/`get` 服务方法及其确切返回状态见 [`src/index.ts`](src/index.ts),并读取已注册的 `plan` 投影;注册表或 key 缺失时,第一次依赖它们的访问会显式失败。 +本包持久化一条仅记日志、整值替换的事件 `plan/mode`,最后一条已记录值即为状态。没有轮次开启时,模式变更会立即追加;轮次开启期间,它保持待生效,直到下一个被接受的轮内 pre-step——agent 运行时唯一的追加点——且追加失败不能阻塞轮次。`set`/`get` 服务方法及其确切返回状态见 [`src/index.ts`](src/index.ts),并通过激活期间取得并保留的 `plan` 投影句柄读取状态。 ### `/plan` 命令 @@ -94,7 +94,7 @@ agent 完成计划后,会以 markdown 形式、从标题开头书写计划并 ### 会话投影单元 -组合了 `ctx.sessionProjections` 时,本包通过可选注入注册 `plan` 单元。该单元把已记录的 `/plan` 命令运行转为候选目标,在 `plan/mode` 上提交已记录状态,并为 `view` 推导 `{ active, pending }`,其中 `pending` 仅在未结算或已成功的选择与已记录状态不同时为 true——这是仅凭日志即可恢复的纯回放量。key 由 [`src/types.ts`](src/types.ts) 的声明合并加入 `SessionProjectionMap`;框架负责驱动该单元,卸载插件 fiber 会注销该 key。plan-mode 读取要求该单元与 `turnBoundary` 单元存在;注册表或任一 key 缺失时都会显式失败。 +本包要求 `ctx.sessionProjections`,并在激活期间等待 `plan` 与 `turnBoundary` 单元注册完成。该单元把已记录的 `/plan` 命令运行转为候选目标,在 `plan/mode` 上提交已记录状态,并为 `view` 推导 `{ active, pending }`,其中 `pending` 仅在未结算或已成功的选择与已记录状态不同时为 true——这是仅凭日志即可恢复的纯回放量。key 由 [`src/types.ts`](src/types.ts) 的声明合并加入 `SessionProjectionMap`;框架负责驱动该单元,卸载插件 fiber 会注销该 key。状态读取使用保留的句柄,注销后仍然可用。 ### 源码地图 diff --git a/packages/plan/plan-mode/src/index.ts b/packages/plan/plan-mode/src/index.ts index a2385491ef..6f05c60d1d 100644 --- a/packages/plan/plan-mode/src/index.ts +++ b/packages/plan/plan-mode/src/index.ts @@ -25,6 +25,7 @@ import { Context, Service } from '@deepseek-ai/cordis' import { brandString } from '@deepseek-ai/dsh-brand' import { z as zod } from 'zod' import type { ZodType } from 'zod' +import { turnBoundaryProjectionDefinition, type TurnBoundaryProjection } from '@deepseek-ai/dsh-agent' import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent' import { createUserMessage } from '@deepseek-ai/dsh-llm' import type { ContextFormed } from '@deepseek-ai/dsh-llm' @@ -33,7 +34,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools' import { UserQuestionError, type AskUserQuestionAnswer } from '@deepseek-ai/dsh-user-questions' import type { CommandDefinitionId, CommandId } from '@deepseek-ai/dsh-commands' import type {} from '@deepseek-ai/dsh-session-projection' -import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection' +import type { ProjectionDefinition, ProjectionHandle } from '@deepseek-ai/dsh-session-projection' import type { PlanProjection, PlanUnitState } from './types.ts' declare module '@deepseek-ai/dsh-llm' { interface MessageSourceMap { @@ -177,6 +178,8 @@ export class PlanModeController extends Service { /** Validated deployment-owned guidance. */ private readonly section: string + private projection!: ProjectionHandle + private turnBoundary!: ProjectionHandle /** * Latest selection per session awaiting the next accepted in-turn pre-step. @@ -188,6 +191,12 @@ export class PlanModeController extends Service { constructor(ctx: Context, config: PlanModeConfig = { section: '' }) { super(ctx, 'planMode') this.section = resolveConfig(config).section + } + + protected async [Service.init](): Promise { + const ctx = this.ctx + this.projection = await ctx.sessionProjections.register(planProjectionDefinition) + this.turnBoundary = await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) let disposed = false // Pre-step is outside Session.append publication, so it can append the // log-only mode event inside an open turn without re-entering the session. @@ -223,7 +232,6 @@ export class PlanModeController extends Service { }, }) - ctx.sessionProjections.register(planProjectionDefinition) // The command child activates only when a command registry is composed. ctx.inject(['commands'], (commandCtx) => { @@ -377,8 +385,7 @@ export class PlanModeController extends Service { } private hasOpenTurn(session: Session): boolean { - const state = this.ctx.sessionProjections.stateOf(session, 'turnBoundary') - if (state === undefined) throw new Error('plan-mode requires the turnBoundary session projection') + const state = this.turnBoundary.stateOf(session) return state.openTurnStartSeq !== null } @@ -388,8 +395,7 @@ export class PlanModeController extends Service { /** Read the required plan projection state or fail at the first service access. */ private planState(session: Session): PlanUnitState { - const state = this.ctx.sessionProjections.stateOf(session, 'plan') - if (state === undefined) throw new Error('plan-mode requires the plan session projection') + const state = this.projection.stateOf(session) return state } diff --git a/packages/plan/plan-mode/tests/integration.spec.ts b/packages/plan/plan-mode/tests/integration.spec.ts index 5c39a792e8..1068721786 100644 --- a/packages/plan/plan-mode/tests/integration.spec.ts +++ b/packages/plan/plan-mode/tests/integration.spec.ts @@ -56,8 +56,7 @@ function waitForIdle(ctx: Context, agent: Agent): Promise { /** Read the plan unit that backs the service in this full composition. */ function planActive(ctx: Context, agent: Agent): boolean { - const state = ctx.sessionProjections.stateOf(agent.session, 'plan') - if (state === undefined) throw new Error('plan projection is not registered') + const state = ctx.planMode.get(agent) return state.active } diff --git a/packages/plan/plan-mode/tests/plan-mode.spec.ts b/packages/plan/plan-mode/tests/plan-mode.spec.ts index 6024269b6d..2ba26b9a09 100644 --- a/packages/plan/plan-mode/tests/plan-mode.spec.ts +++ b/packages/plan/plan-mode/tests/plan-mode.spec.ts @@ -89,7 +89,7 @@ function foldPlanMode(events: readonly SessionEvent[], end = events.length): boo async function mountProjectionSeam(ctx: Context): Promise { await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) } async function setup(config: PlanModeConfig = PLAN_CONFIG): Promise { @@ -231,25 +231,6 @@ describe('ctx.planMode: get/set', () => { expect(ctx.get('planMode')).toBeUndefined() }) - it('fails when the required plan projection key is absent', async () => { - const ctx = await setup() - const agent = await agentWithSession(ctx, 'missing-plan-projection') - vi.spyOn(ctx.sessionProjections, 'stateOf').mockReturnValue(undefined) - expect(() => ctx.planMode.get(agent)).toThrow('plan-mode requires the plan session projection') - }) - - it('registers plan state directly but requires turnBoundary state', async () => { - const ctx = new Context() - await ctx.plugin(SessionProjectionRegistry) - await ctx.plugin(SystemPrompt) - await ctx.plugin(ToolRuntime) - const agent = await agentWithSession(ctx, 'missing-plan-projection-keys') - const planMode = new PlanModeController(ctx, PLAN_CONFIG) - await new Promise(resolve => setImmediate(resolve)) - expect(planMode.get(agent)).toEqual({ active: false }) - expect(() => planMode.set(agent, true)).toThrow('plan-mode requires the turnBoundary session projection') - }) - it('reads the folded state', async () => { const ctx = await setup() const agent = await agentWithSession(ctx) diff --git a/packages/plan/plan-mode/tests/projection.spec.ts b/packages/plan/plan-mode/tests/projection.spec.ts index 3c2fc36b5c..99c2b538ea 100644 --- a/packages/plan/plan-mode/tests/projection.spec.ts +++ b/packages/plan/plan-mode/tests/projection.spec.ts @@ -118,22 +118,22 @@ describe('plan projection unit', () => { const bench = await harness(true) commitPlanMode(bench.session, true, 0) expect(bench.values().plan).toEqual({ active: true, pending: false }) - expect(bench.ctx.sessionProjections.stateOf(bench.session, 'plan')?.activeAtLastHeader).toBeNull() + expect(bench.ctx.sessionProjections.checkpoint(bench.session).plan?.val).toMatchObject({ activeAtLastHeader: null }) bench.session.append('request/header', { header: { config: { provider: 'test', model: 'test-model' } }, reason: 'initial', }) expect(bench.values().plan).toEqual({ active: true, pending: false }) - expect(bench.ctx.sessionProjections.stateOf(bench.session, 'plan')?.activeAtLastHeader).toBe(true) + expect(bench.ctx.sessionProjections.checkpoint(bench.session).plan?.val).toMatchObject({ activeAtLastHeader: true }) commitPlanMode(bench.session, false, 1) expect(bench.values().plan).toEqual({ active: false, pending: false }) - expect(bench.ctx.sessionProjections.stateOf(bench.session, 'plan')?.activeAtLastHeader).toBe(true) + expect(bench.ctx.sessionProjections.checkpoint(bench.session).plan?.val).toMatchObject({ activeAtLastHeader: true }) bench.session.append('request/header', { header: { config: { provider: 'test', model: 'test-model' } }, reason: 'change', }) expect(bench.values().plan).toEqual({ active: false, pending: false }) - expect(bench.ctx.sessionProjections.stateOf(bench.session, 'plan')?.activeAtLastHeader).toBe(false) + expect(bench.ctx.sessionProjections.checkpoint(bench.session).plan?.val).toMatchObject({ activeAtLastHeader: false }) }) it('has no plan key when plan-mode is not composed', async () => { diff --git a/packages/preset/agent-preset-registry/src/index.ts b/packages/preset/agent-preset-registry/src/index.ts index 0500922904..9f870203b5 100644 --- a/packages/preset/agent-preset-registry/src/index.ts +++ b/packages/preset/agent-preset-registry/src/index.ts @@ -1,5 +1,7 @@ /** Declarative Agent capability sets, activation and session binding. */ -import { Context } from '@deepseek-ai/cordis' +import { turnBoundaryProjectionDefinition, type TurnBoundaryProjection } from '@deepseek-ai/dsh-agent' +import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection' +import { Context, Service } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol' import { bindScopeParent, createScope, scopeOf, scopeParentOf, type Scope, type ScopeKey, type ScopeParentBinding } from '@deepseek-ai/dsh-scope' @@ -55,6 +57,7 @@ export class AgentPresetRegistry extends TypertRemoteService { default: z.string().required(), selectedDefault: z.string().volatile(), }) + private turnBoundary!: ProjectionHandle private readonly owner: Context private readonly definitions = new Map() private readonly generations = new Map() @@ -64,7 +67,12 @@ export class AgentPresetRegistry extends TypertRemoteService { constructor(ctx: Context, public config: Config) { super(ctx, 'agentPresets') this.owner = ctx - ctx.sessionProjections.register(agentPresetProjectionDefinition) + } + + protected async [Service.init](): Promise { + const ctx = this.owner + await ctx.sessionProjections.register(agentPresetProjectionDefinition) + this.turnBoundary = await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) ctx.inject(['settings'], (child) => { child.effect(() => child.settings.configure({ auto: false }, ctx.fiber)) }) ctx.on('session/event', (session, event) => { if (event.type === 'agent-preset/selected') ctx.emit('agent-preset/selected', session.id, event.data.agentPreset) @@ -346,8 +354,8 @@ export class AgentPresetRegistry extends TypertRemoteService { @Remote('select') async select(agent: Agent, agentPreset: string): Promise { const turn = (this.switches.get(agent.id) ?? Promise.resolve()).then(async () => { - const boundary = this.owner.sessionProjections.stateOf(agent.session, 'turnBoundary') - if (boundary !== undefined && (boundary.openTurnStartSeq !== null || boundary.lastTurn > 0)) { + const boundary = this.turnBoundary.stateOf(agent.session) + if (boundary.openTurnStartSeq !== null || boundary.lastTurn > 0) { throw new RemoteError('agent-preset/locked', 'This session has already started', { sessionId: agent.id, agentPreset }) } const preset = await this.recompose(agent.ctx, agentPreset) diff --git a/packages/preset/agent-preset-registry/tests/registry.spec.ts b/packages/preset/agent-preset-registry/tests/registry.spec.ts index 04ab9c0096..471998f689 100644 --- a/packages/preset/agent-preset-registry/tests/registry.spec.ts +++ b/packages/preset/agent-preset-registry/tests/registry.spec.ts @@ -108,7 +108,7 @@ describe('declarative preset revisions', () => { await declare(ctx, contribution('minimal')) const agent = await agentOn(ctx, 'selection') expect(await ctx.agentPresets.select(agent, 'minimal')).toBe('minimal') - expect(ctx.sessionProjections.stateOf(agent.session, 'agentPreset')).toBe('minimal') + expect(ctx.sessionProjections.checkpoint(agent.session).agentPreset?.val).toBe('minimal') agent.session.append('turn/start', { turn: 1 }) await expect(ctx.agentPresets.select(agent, 'standard')).rejects.toThrow('already started') }) @@ -276,7 +276,7 @@ it('serializes competing blank-session selections and recovers after a failed se ctx.agentPresets.select(agent, 'absent'), ctx.agentPresets.select(agent, 'minimal'), ctx.agentPresets.select(agent, 'standard'), ]) expect(results.map(row => row.status)).toEqual(['rejected', 'fulfilled', 'fulfilled']) - expect(ctx.sessionProjections.stateOf(agent.session, 'agentPreset')).toBe('standard') + expect(ctx.sessionProjections.checkpoint(agent.session).agentPreset?.val).toBe('standard') }) it('retains a newly acquired generation when a previous activation is replaced', async () => { @@ -305,7 +305,7 @@ it('does not lose the durable selection when a tools observer rejects notificati const agent = await agentOn(ctx, 'observer') ctx.on('tools/change', () => { throw new Error('observer refused') }) expect(await ctx.agentPresets.select(agent, 'minimal')).toBe('minimal') - expect(ctx.sessionProjections.stateOf(agent.session, 'agentPreset')).toBe('minimal') + expect(ctx.sessionProjections.checkpoint(agent.session).agentPreset?.val).toBe('minimal') }) it('inventories failed conditional entries without executing their expressions again', async () => { diff --git a/packages/sandbox/sandbox-policy/src/index.ts b/packages/sandbox/sandbox-policy/src/index.ts index e97796db5c..c966e6a4ea 100644 --- a/packages/sandbox/sandbox-policy/src/index.ts +++ b/packages/sandbox/sandbox-policy/src/index.ts @@ -27,7 +27,7 @@ import z from '@deepseek-ai/schemastery' import type {} from '@deepseek-ai/dsh-agent' import type { SandboxExecutionPolicy, SandboxMode } from '@deepseek-ai/dsh-sandbox' import type { Session } from '@deepseek-ai/dsh-session' -import type {} from '@deepseek-ai/dsh-session-projection' +import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection' import type {} from '@deepseek-ai/dsh-system-prompt' export { SANDBOX_MODES, setSandboxMode } from './session-mode.ts' @@ -122,6 +122,8 @@ export class SandboxPolicyService extends Service { readonly defaultMode: SandboxMode /** The absolute `workspace-write` fallback root for calls without a session cwd. */ readonly workspaceRoot: string + private projection!: ProjectionHandle + constructor(ctx: Context, config: Config) { super(ctx, 'sandboxPolicy') // schemastery (static Config) already filled `mode`; the cast records that @@ -129,8 +131,11 @@ export class SandboxPolicyService extends Service { // the process cwd is real branching, resolved absolute either way. this.defaultMode = config.mode as SandboxMode this.workspaceRoot = resolveWorkspaceRoot(config.workspaceRoot ?? process.cwd()) + } - ctx.sessionProjections.register({ + protected async [Service.init](): Promise { + const ctx = this.ctx + this.projection = await ctx.sessionProjections.register({ key: 'sandboxMode', stateVersion: 1, stateSchema: sandboxModeStateSchema, @@ -176,7 +181,7 @@ export class SandboxPolicyService extends Service { * @returns the last logged mode, or `undefined` without one. */ overrideOf(session: Session): SandboxMode | undefined { - return this.ctx.sessionProjections.stateOf(session, 'sandboxMode') ?? undefined + return this.projection.stateOf(session) ?? undefined } } diff --git a/packages/sandbox/sandbox-policy/tests/policy.spec.ts b/packages/sandbox/sandbox-policy/tests/policy.spec.ts index a6ed4d9fb4..e2279e7fd7 100644 --- a/packages/sandbox/sandbox-policy/tests/policy.spec.ts +++ b/packages/sandbox/sandbox-policy/tests/policy.spec.ts @@ -235,10 +235,10 @@ describe('the sandbox/mode session kit', () => { it('the sandboxMode projection folds to the last switch, or null without one', async () => { const ctx = await mounted() const session = Session.create(SessionId('sess-fold')) - expect(ctx.sessionProjections.stateOf(session, 'sandboxMode')).toBeNull() + expect(ctx.sessionProjections.checkpoint(session).sandboxMode?.val).toBeNull() setSandboxMode(session, 'workspace-write') setSandboxMode(session, 'read-only') - expect(ctx.sessionProjections.stateOf(session, 'sandboxMode')).toBe('read-only') + expect(ctx.sessionProjections.checkpoint(session).sandboxMode?.val).toBe('read-only') }) it('setSandboxMode appends exactly one sandbox/mode event per switch', () => { diff --git a/packages/session/session-stats/src/index.ts b/packages/session/session-stats/src/index.ts index 54679b6e78..6439070f01 100644 --- a/packages/session/session-stats/src/index.ts +++ b/packages/session/session-stats/src/index.ts @@ -14,16 +14,16 @@ import { sessionStatsProjectionDefinition } from './projection.ts' export type * from './types.ts' -/** Cordis plugin name. */ export const name = 'session-stats' -/** The projection registry is the plugin's whole purpose; without it the fiber stays pending. */ + export const inject = ['sessionProjections'] /** * Register the `sessionStats` unit; the registration is an effect on this * plugin's fiber, so unloading removes the key. * @param ctx - registrant context carrying the projection registry. + * @returns when the plugin is ready. */ -export function apply(ctx: Context): void { - ctx.sessionProjections.register(sessionStatsProjectionDefinition) +export async function apply(ctx: Context): Promise { + await ctx.sessionProjections.register(sessionStatsProjectionDefinition) } diff --git a/packages/session/session-title-first-prompt-llm/tests/provider.spec.ts b/packages/session/session-title-first-prompt-llm/tests/provider.spec.ts index 35c297eddc..92565e1cc9 100644 --- a/packages/session/session-title-first-prompt-llm/tests/provider.spec.ts +++ b/packages/session/session-title-first-prompt-llm/tests/provider.spec.ts @@ -76,7 +76,7 @@ async function harness( await ctx.plugin(LlmRuntime) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) await ctx.plugin(SessionTitleService, TITLE_CONFIG) const adapter = new RecordingAdapter(script, reasoningEfforts) ctx.llm.registerAdapter(['title-route'], adapter) @@ -249,7 +249,7 @@ describe('first-prompt LLM title provider', () => { await ctx.plugin(LlmRuntime) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) await ctx.plugin(SessionTitleService, TITLE_CONFIG) // Delegate to the real registration while capturing it, to reach the guard // the service never produces. diff --git a/packages/session/session-title/src/index.ts b/packages/session/session-title/src/index.ts index 6a7e9acdde..3d5dc3d7e0 100644 --- a/packages/session/session-title/src/index.ts +++ b/packages/session/session-title/src/index.ts @@ -3,6 +3,7 @@ * @module @deepseek-ai/dsh-session-title */ +import { turnBoundaryProjectionDefinition, type TurnBoundaryProjection } from '@deepseek-ai/dsh-agent' import { Context, FiberState, Service, type Fiber } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import { z as zod } from 'zod' @@ -17,8 +18,7 @@ import type { } from '@deepseek-ai/dsh-session' import { SessionSeq } from '@deepseek-ai/dsh-session' import type {} from '@deepseek-ai/dsh-session-projection' -import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection' -import type {} from '@deepseek-ai/dsh-agent' +import type { ProjectionDefinition, ProjectionHandle } from '@deepseek-ai/dsh-session-projection' export type { SessionTitleEventData, SessionTitleModelIdentity, @@ -306,6 +306,8 @@ export class SessionTitleService extends Service { maxTitleBytes: z.number().step(1).min(1).required(), }) + private turnBoundary!: ProjectionHandle + private titleInput!: ProjectionHandle private readonly config: ResolvedConfig private readonly ownerFiber: Fiber private registration: ProviderRegistration | undefined @@ -328,6 +330,11 @@ export class SessionTitleService extends Service { throw new Error('session-title: fallbackMaxBytes must not exceed maxTitleBytes') } this.config = deepFreeze({ ...value }) + } + + protected async [Service.init](): Promise { + const ctx = this.ctx + this.turnBoundary = await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) ctx.effect(() => async () => { this.lifetime.abort(new Error('session-title service disposed')) @@ -341,9 +348,9 @@ export class SessionTitleService extends Service { this.work.clear() }, 'sessionTitle lifecycle') - ctx.sessionProjections.register(titleProjectionDefinition) + await ctx.sessionProjections.register(titleProjectionDefinition) - ctx.sessionProjections.register<'titleInput', TitleInputState>({ + this.titleInput = await ctx.sessionProjections.register<'titleInput', TitleInputState>({ key: 'titleInput', stateVersion: 3, stateSchema: titleInputStateSchema, @@ -549,7 +556,7 @@ export class SessionTitleService extends Service { const state = session === undefined ? undefined : this.work.get(session) const pending = state?.pending if (session === undefined || state === undefined || pending === undefined) return - const boundary = this.ctx.sessionProjections.stateOf(session, 'turnBoundary')?.lastStepBoundary + const boundary = this.turnBoundary.stateOf(session).lastStepBoundary const route = session.requestHeader()?.config if (boundary?.kind !== 'start' || route?.provider !== options.provider @@ -730,7 +737,7 @@ export class SessionTitleService extends Service { } private titleInputOf(session: Session): TitleInputState { - return this.ctx.sessionProjections.stateOf(session, 'titleInput') as TitleInputState + return this.titleInput.stateOf(session) } /** Queue detached service work and retain it through service disposal. */ diff --git a/packages/session/session-title/tests/projection.spec.ts b/packages/session/session-title/tests/projection.spec.ts index df2b5f1f91..784df3c69d 100644 --- a/packages/session/session-title/tests/projection.spec.ts +++ b/packages/session/session-title/tests/projection.spec.ts @@ -90,10 +90,8 @@ describe('title projection unit', () => { } await ctx.plugin(SessionTitleService, CONFIG) - const state = ctx.sessionProjections.stateOf(session, 'titleInput') - expect(state?.count).toBe(5_000) - expect(state?.first?.text).toBe('message 0') - expect(state?.lastSeq).toBe(session.seq - 1) + const state = ctx.sessionProjections.checkpoint(session).titleInput?.val + expect(state).toMatchObject({ count: 5_000, first: { text: 'message 0' }, lastSeq: session.seq - 1 }) expect(ctx.sessionProjections.checkpoint(session).titleInput).toBeDefined() }) diff --git a/packages/session/session-title/tests/provider.spec.ts b/packages/session/session-title/tests/provider.spec.ts index 8f22a930fc..f08002e145 100644 --- a/packages/session/session-title/tests/provider.spec.ts +++ b/packages/session/session-title/tests/provider.spec.ts @@ -452,7 +452,7 @@ describe('SessionTitleService Provider lifecycle', () => { await ctx.plugin(LlmRuntime) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) await ctx.plugin(SessionTitleService, CONFIG) const requests: SessionTitleProviderRequest[] = [] ctx.sessionTitle.register({ @@ -508,7 +508,7 @@ describe('SessionTitleService Provider lifecycle', () => { await ctx.plugin(LlmRuntime) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) await ctx.plugin(SessionTitleService, CONFIG) const generate = vi.fn(async (request: SessionTitleProviderRequest): Promise => ({ title: 'Unexpected title', diff --git a/packages/session/session-title/tsconfig.json b/packages/session/session-title/tsconfig.json index 5755ac55e2..e056eda2bd 100644 --- a/packages/session/session-title/tsconfig.json +++ b/packages/session/session-title/tsconfig.json @@ -31,7 +31,6 @@ }, { "path": "../../core/agent" - } ] } diff --git a/packages/session/session-turn-outline/src/index.ts b/packages/session/session-turn-outline/src/index.ts index 0105e7c186..ab2c5f667e 100644 --- a/packages/session/session-turn-outline/src/index.ts +++ b/packages/session/session-turn-outline/src/index.ts @@ -14,16 +14,16 @@ import { turnOutlineProjectionDefinition } from './projection.ts' export type * from './types.ts' -/** Cordis plugin name. */ export const name = 'session-turn-outline' -/** The projection registry is the plugin's whole purpose; without it the fiber stays pending. */ + export const inject = ['sessionProjections'] /** * Register the `turnOutline` unit; the registration is an effect on this * plugin's fiber, so unloading removes the key. * @param ctx - registrant context carrying the projection registry. + * @returns when the plugin is ready. */ -export function apply(ctx: Context): void { - ctx.sessionProjections.register(turnOutlineProjectionDefinition) +export async function apply(ctx: Context): Promise { + await ctx.sessionProjections.register(turnOutlineProjectionDefinition) } diff --git a/packages/session/session-turn-outline/tests/projection.spec.ts b/packages/session/session-turn-outline/tests/projection.spec.ts index c01acf8aa5..8c1d26bc96 100644 --- a/packages/session/session-turn-outline/tests/projection.spec.ts +++ b/packages/session/session-turn-outline/tests/projection.spec.ts @@ -97,7 +97,7 @@ describe('turn outline projection unit', () => { appendPrompt(session, 'prompt') appendAssistant(session, 1, 1, 'streamed but unsettled') expect(outlineOf(ctx, session)[0]?.response).toBe('') - expect(ctx.sessionProjections.stateOf(session, 'turnOutline')?.draft).toBe('streamed but unsettled') + expect(ctx.sessionProjections.checkpoint(session).turnOutline?.val).toMatchObject({ draft: 'streamed but unsettled' }) endTurn(session, 1) expect(outlineOf(ctx, session)[0]?.response).toBe('streamed but unsettled') }) diff --git a/packages/session/working-directory/src/index.ts b/packages/session/working-directory/src/index.ts index 7346feb274..60c8ad9c2a 100644 --- a/packages/session/working-directory/src/index.ts +++ b/packages/session/working-directory/src/index.ts @@ -11,7 +11,7 @@ import type { Agent } from '@deepseek-ai/dsh-agent' import type {} from '@deepseek-ai/dsh-fs' import { createUserMessage } from '@deepseek-ai/dsh-llm' import type { Session } from '@deepseek-ai/dsh-session' -import type {} from '@deepseek-ai/dsh-session-projection' +import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection' import type {} from '@deepseek-ai/dsh-system-prompt' declare module '@deepseek-ai/cordis' { @@ -72,14 +72,19 @@ export class WorkingDirectoryService extends Service { private readonly operations = new Set>() private readonly lifecycle = new AbortController() + private projection!: ProjectionHandle + constructor(ctx: Context, config: Config) { super(ctx, 'workingDirectory') const directory = config.defaultDirectory ?? process.cwd() if (!isAbsolute(directory)) throw new Error('working-directory: defaultDirectory must be absolute') this.defaultDirectory = directory + } + protected async [Service.init](): Promise { + const ctx = this.ctx const directorySchema = zod.string().min(1).refine(isAbsolute).nullable() - ctx.sessionProjections.register({ + this.projection = await ctx.sessionProjections.register({ key: 'workingDirectory', stateVersion: 1, stateSchema: directorySchema, @@ -120,7 +125,7 @@ export class WorkingDirectoryService extends Service { * @returns its effective absolute directory. */ get(session: Session): string { - return this.ctx.sessionProjections.stateOf(session, 'workingDirectory') ?? this.defaultDirectory + return this.projection.stateOf(session) ?? this.defaultDirectory } /** @@ -139,7 +144,7 @@ export class WorkingDirectoryService extends Service { control.throwIfAborted() agent.ctx.fiber.assertActive() if (info?.type === 'directory') { - if (this.ctx.sessionProjections.stateOf(agent.session, 'workingDirectory') === null) { + if (this.projection.stateOf(agent.session) === null) { agent.session.append('working-directory/change', { cwd: current }) } return current diff --git a/packages/shell/tool-bash/tests/tools.spec.ts b/packages/shell/tool-bash/tests/tools.spec.ts index 1d01640cd0..1f2eb7d2e8 100644 --- a/packages/shell/tool-bash/tests/tools.spec.ts +++ b/packages/shell/tool-bash/tests/tools.spec.ts @@ -211,7 +211,7 @@ async function setupSandboxed(withApproval = false) { await ctx.plugin(LocalJobRegistry) await ctx.plugin(ToolJobs) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) await ctx.plugin(SandboxPolicyService, {}) await ctx.plugin(RecordingSandboxExecutor) if (withApproval) await ctx.plugin(ApprovalService) diff --git a/packages/shell/tool-pwsh/tests/tools.spec.ts b/packages/shell/tool-pwsh/tests/tools.spec.ts index d033c52535..aaa82411c5 100644 --- a/packages/shell/tool-pwsh/tests/tools.spec.ts +++ b/packages/shell/tool-pwsh/tests/tools.spec.ts @@ -228,7 +228,7 @@ async function setupSandboxed(withApproval = false) { // The loop's turnBoundary unit (the open-turn fold) is not mounted in this // bench — the loop itself is not composed. Register its open-turn fold so // the approval service's turn-enclosure gate reads the seeded log shape. - ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) await ctx.plugin(SandboxPolicyService, {}) await ctx.plugin(ConfiningFakeBash) if (withApproval) await ctx.plugin(ApprovalService) diff --git a/packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/scoped-tool-subagent.ts b/packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/scoped-tool-subagent.ts index cd30075474..2e348c865f 100644 --- a/packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/scoped-tool-subagent.ts +++ b/packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/scoped-tool-subagent.ts @@ -15,8 +15,8 @@ export const inject = ['agents', 'subagentModelSelection'] */ export function apply(ctx: Context, config: Config): void { const install = (agent: Agent): void => { - agent.ctx.inject(ToolSubagent.inject, (runtimeCtx) => { - ToolSubagent.apply(runtimeCtx, { + agent.ctx.inject(ToolSubagent.inject, async (runtimeCtx) => { + await ToolSubagent.apply(runtimeCtx, { provider: config.provider, modelSelectionSettings: true, ...(config.toolName === undefined ? {} : { toolName: config.toolName }), diff --git a/packages/subagent/subagent-dsh-sdk/tests/subagent-dsh-sdk.spec.ts b/packages/subagent/subagent-dsh-sdk/tests/subagent-dsh-sdk.spec.ts index c4b1781f57..891e860b16 100644 --- a/packages/subagent/subagent-dsh-sdk/tests/subagent-dsh-sdk.spec.ts +++ b/packages/subagent/subagent-dsh-sdk/tests/subagent-dsh-sdk.spec.ts @@ -878,6 +878,7 @@ describe('dsh-subagent-dsh-sdk provider', () => { it('requires an explicit absolute Harness home for nested dsh runtimes', async () => { const ctx = new Context() await mountWorkingDirectoryFixture(ctx) + if (ctx.get('sessionProjections') === undefined) await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(SubagentRuntime) await expect(ctx.plugin(sdk, { providerName: 'sdk', @@ -898,6 +899,7 @@ describe('dsh-subagent-dsh-sdk provider', () => { ])('rejects an invalid $field at load', async ({ field, override }) => { const ctx = new Context() await mountWorkingDirectoryFixture(ctx) + if (ctx.get('sessionProjections') === undefined) await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(SubagentRuntime) await expect(ctx.plugin(sdk, { providerName: 'sdk', diff --git a/packages/subagent/subagent/README.i18n.yaml b/packages/subagent/subagent/README.i18n.yaml index dfb659c93b..c4fef66681 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: 388ae5427c869173 - zh: aff830f4986cf1b3 + en: 025605eddae02478 + zh: 5659cba4340735d8 /deepseek-ai-dsh-subagent/use-this-package/delegation-settings: en: 86c982223fab1a3a zh: 3d220d8ed84eab9a diff --git a/packages/subagent/subagent/README.md b/packages/subagent/subagent/README.md index 2442af74c1..77e2e5cab9 100644 --- a/packages/subagent/subagent/README.md +++ b/packages/subagent/subagent/README.md @@ -31,7 +31,7 @@ This package is the contract every delegation setup shares. You enable it by mou ### Enabling delegation -Mount the service with a provider and the delegation tool. The provider registers under its configured name; each tool row selects one provider. A minimal setup: +Mount the service with `@deepseek-ai/dsh-session-projection`, a provider, and the delegation tool; the service waits for the projection registry before initializing. The provider registers under its configured name; each tool row selects one provider. A minimal setup: ```yaml - name: '@deepseek-ai/dsh-subagent' diff --git a/packages/subagent/subagent/README.zh.md b/packages/subagent/subagent/README.zh.md index 7dccf24550..6506779e83 100644 --- a/packages/subagent/subagent/README.zh.md +++ b/packages/subagent/subagent/README.zh.md @@ -31,7 +31,7 @@ kind: "package-reference" ### 启用委派 -挂载服务、提供方与委派工具。提供方按配置名称注册,每个工具行选择一个提供方。最小配置如下: +一起挂载本服务、`@deepseek-ai/dsh-session-projection`、提供方与委派工具;本服务等待投影注册表可用后才初始化。提供方按配置名称注册,每个工具行选择一个提供方。最小配置如下: ```yaml - name: '@deepseek-ai/dsh-subagent' diff --git a/packages/subagent/subagent/package.json b/packages/subagent/subagent/package.json index 5a802075d1..5645775408 100644 --- a/packages/subagent/subagent/package.json +++ b/packages/subagent/subagent/package.json @@ -94,9 +94,6 @@ "@deepseek-ai/dsh-session-persistence": { "optional": true }, - "@deepseek-ai/dsh-session-projection": { - "optional": true - }, "@deepseek-ai/dsh-session-query": { "optional": true }, diff --git a/packages/subagent/subagent/src/index.ts b/packages/subagent/subagent/src/index.ts index edce6b7ae5..7484cdd5dc 100644 --- a/packages/subagent/subagent/src/index.ts +++ b/packages/subagent/subagent/src/index.ts @@ -29,7 +29,7 @@ */ import type { Volatile } from '@deepseek-ai/cordis' -import { Context } from '@deepseek-ai/cordis' +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' @@ -194,7 +194,7 @@ export class SubagentRuntime extends TypertRemoteService { maxDepth: z.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).default(1).volatile(), maxActiveSubagents: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(8).volatile(), }) - static inject = ['workingDirectory'] + static inject = ['workingDirectory', 'sessionProjections'] private providers = new Map() private manager: SubagentManager | undefined /** @@ -222,16 +222,18 @@ export class SubagentRuntime extends TypertRemoteService { if (this.manager === manager) this.manager = undefined }, 'subagents.managerBinding()') }) - ctx.inject(['sessionProjections'], (projectionCtx) => { - projectionCtx.sessionProjections.register(subagentCatalogProjectionDefinition) - projectionCtx.sessionProjections.register(subagentTimingProjectionDefinition) - projectionCtx.sessionProjections.register(subagentIdentityProjectionDefinition) - }) // Archive admission: this runtime is the owner that knows which live // children descend from a Session and how a parent stops them. ctx.inject(['agents'], (agentsCtx: Context) => { installSubagentArchiveAdmission(agentsCtx) }) } + protected async [Service.init](): Promise { + const { sessionProjections } = this.ctx + await sessionProjections.register(subagentCatalogProjectionDefinition) + await sessionProjections.register(subagentTimingProjectionDefinition) + await sessionProjections.register(subagentIdentityProjectionDefinition) + } + /** * Resolve a delegation tool's depth policy against the current user setting. * @param configured - Explicit tool limit, or provider-managed for external delegation. diff --git a/packages/subagent/subagent/tests/catalog.spec.ts b/packages/subagent/subagent/tests/catalog.spec.ts index c242f1c423..ac576639fa 100644 --- a/packages/subagent/subagent/tests/catalog.spec.ts +++ b/packages/subagent/subagent/tests/catalog.spec.ts @@ -79,7 +79,7 @@ describe('subagent catalog projection', () => { try { await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(subagentCatalogProjectionDefinition) + await ctx.sessionProjections.register(subagentCatalogProjectionDefinition) const parent = ctx.sessions.create(header.id) const changes = vi.fn() ctx.sessionProjections.onChanged(changes) @@ -196,7 +196,7 @@ describe('subagent catalog projection', () => { try { await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(subagentCatalogProjectionDefinition) + await ctx.sessionProjections.register(subagentCatalogProjectionDefinition) const events = [ fact(0, 'valid-child', 0, { mode: 'one-shot' }), { type: 'subagent/catalog', seq: SessionSeq(1), time: 0, data } as unknown as SessionEvent, diff --git a/packages/subagent/subagent/tests/continuation-inheritance.spec.ts b/packages/subagent/subagent/tests/continuation-inheritance.spec.ts index e44c170dae..9a764ad01d 100644 --- a/packages/subagent/subagent/tests/continuation-inheritance.spec.ts +++ b/packages/subagent/subagent/tests/continuation-inheritance.spec.ts @@ -80,7 +80,7 @@ function policyEvents(events: readonly SessionEvent[]) { } function foldedSandboxMode(ctx: Context, id: SessionId, events: readonly SessionEvent[]): unknown { - return ctx.sessionProjections.stateOf(Session.create(id, events), 'sandboxMode') + return ctx.sandboxPolicy.overrideOf(Session.create(id, events)) ?? null } function foldedApprovalPolicy(ctx: Context, id: SessionId, events: readonly SessionEvent[]): unknown { diff --git a/packages/subagent/subagent/tests/continuation.spec.ts b/packages/subagent/subagent/tests/continuation.spec.ts index e4ee8e6c89..d7c4477c5b 100644 --- a/packages/subagent/subagent/tests/continuation.spec.ts +++ b/packages/subagent/subagent/tests/continuation.spec.ts @@ -1,3 +1,4 @@ +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts' import { afterEach, describe, expect, it, vi } from 'vitest' import { mkdtempSync, rmSync } from 'node:fs' @@ -100,6 +101,7 @@ async function setupWith( await ctx.plugin(AgentLoop, { agents: [] }) if (options.sessionQuery !== false) await ctx.plugin(TestSessionQuery) await mountWorkingDirectoryFixture(ctx) + if (ctx.get('sessionProjections') === undefined) await ctx.plugin(SessionProjectionRegistry) subagentConfigs.set(ctx, await liveConfig(ctx, SubagentRuntime, options.maxActiveSubagents === undefined ? {} : { maxActiveSubagents: options.maxActiveSubagents })) await ctx.plugin(SubagentSpawn, { providerName: 'spawn' }) @@ -262,6 +264,7 @@ describe('continuable activation capacity', () => { const ctx = new Context() try { await mountWorkingDirectoryFixture(ctx) + if (ctx.get('sessionProjections') === undefined) await ctx.plugin(SessionProjectionRegistry) await expect(ctx.plugin(SubagentRuntime, { maxActiveSubagents })).rejects.toThrow() } finally { await ctx.fiber.dispose() @@ -272,6 +275,7 @@ describe('continuable activation capacity', () => { const ctx = new Context() try { await mountWorkingDirectoryFixture(ctx) + if (ctx.get('sessionProjections') === undefined) await ctx.plugin(SessionProjectionRegistry) const live = await liveConfig(ctx, SubagentRuntime, { maxDepth: 4 }) subagentConfigs.set(ctx, live) const fiber = live.fiber @@ -3861,6 +3865,7 @@ describe('continuable errors', () => { }) await ctx.plugin(AgentLoop, { agents: [] }) await mountWorkingDirectoryFixture(ctx) + if (ctx.get('sessionProjections') === undefined) await ctx.plugin(SessionProjectionRegistry) const serviceFiber = await ctx.plugin(SubagentRuntime) await ctx.plugin(SubagentSpawn, { providerName: 'spawn' }) ctx.llm.registerAdapter(['mock'], adapter) diff --git a/packages/subagent/subagent/tests/control.spec.ts b/packages/subagent/subagent/tests/control.spec.ts index b6df7c53ed..16263f5b10 100644 --- a/packages/subagent/subagent/tests/control.spec.ts +++ b/packages/subagent/subagent/tests/control.spec.ts @@ -1,3 +1,4 @@ +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts' // The browser-facing prompt and interrupt controls and their stable failure // codes. Session Controller owns catalog observation and transport. @@ -25,6 +26,7 @@ const IMAGE_REF = { attachmentId: 'att', mediaType: 'image/png', bytes: 2, width async function bench(live?: Record) { const ctx = new Context() await mountWorkingDirectoryFixture(ctx) + if (ctx.get('sessionProjections') === undefined) await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(SubagentRuntime) if (live !== undefined) { ctx.provide('agents', { diff --git a/packages/subagent/subagent/tests/list-children.spec.ts b/packages/subagent/subagent/tests/list-children.spec.ts index dd5cf76799..f9983b580c 100644 --- a/packages/subagent/subagent/tests/list-children.spec.ts +++ b/packages/subagent/subagent/tests/list-children.spec.ts @@ -1,3 +1,4 @@ +import { subagentCatalogProjectionDefinition } from '../src/catalog.ts' import { mountWorkingDirectoryFixture } from './working-directory-fixture.ts' import { afterEach, describe, expect, it, vi } from 'vitest' import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs' @@ -49,6 +50,7 @@ async function setup( await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(TestSessionQuery) await mountWorkingDirectoryFixture(ctx) + if (ctx.get('sessionProjections') === undefined) await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(SubagentRuntime) await ctx.plugin(SubagentSpawn, { providerName: 'spawn' }) await ctx.plugin(SubagentFork, { providerName: 'fork' }) @@ -191,6 +193,7 @@ describe('SubagentRuntime.listChildren', () => { await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(TestSessionQuery) await mountWorkingDirectoryFixture(ctx) + if (ctx.get('sessionProjections') === undefined) await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(SubagentRuntime) const ancestor = ctx.sessions.create(SessionId('catalog-ancestor')) ancestor.append('turn/start', { turn: 1 }) @@ -221,6 +224,7 @@ describe('SubagentRuntime.listChildren', () => { await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(TestSessionQuery) await mountWorkingDirectoryFixture(ctx) + if (ctx.get('sessionProjections') === undefined) await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(SubagentRuntime) const parent = ctx.sessions.create(SessionId('chunk-parent')) for (let index = 0; index < 1_001; index += 1) { @@ -235,7 +239,8 @@ describe('SubagentRuntime.listChildren', () => { }) } const listSessions = vi.spyOn(ctx.sessionQuery, 'listSessions') - const state = ctx.sessionProjections.stateOf(parent, 'subagentCatalog') + const projection = await ctx.sessionProjections.register(subagentCatalogProjectionDefinition) + const state = projection.stateOf(parent) const chunkLengths: number[] = [] for (let chunk = state?.head; chunk !== undefined; chunk = chunk.previous) { chunkLengths.push(chunk.values.length) @@ -253,6 +258,7 @@ describe('SubagentRuntime.listChildren', () => { const withoutProjection = new Context() await withoutProjection.plugin(SessionStore) await mountWorkingDirectoryFixture(withoutProjection) + await withoutProjection.plugin(SessionProjectionRegistry) await withoutProjection.plugin(SubagentRuntime) const parent = withoutProjection.sessions.create(SessionId('parent')) await expect(withoutProjection.subagents.listChildren(parent.id)).rejects.toMatchObject({ @@ -441,6 +447,7 @@ describe('SubagentRuntime.listDescendants', () => { it('fails before reading when the Session store or query service is absent', async () => { const ctx = new Context() await mountWorkingDirectoryFixture(ctx) + if (ctx.get('sessionProjections') === undefined) await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(SubagentRuntime) await expect(ctx.subagents.listDescendants(SessionId('root'))).rejects.toMatchObject({ code: 'SUBAGENT_CONTROL_SESSION_STORE_UNAVAILABLE' }) await ctx.plugin(SessionStore) diff --git a/packages/subagent/subagent/tests/service.spec.ts b/packages/subagent/subagent/tests/service.spec.ts index 8025ac0060..30a6cfc932 100644 --- a/packages/subagent/subagent/tests/service.spec.ts +++ b/packages/subagent/subagent/tests/service.spec.ts @@ -70,8 +70,6 @@ const contexts = new WeakMap() async function service(withAgents = true): Promise<{ ctx: Context; subagents: SubagentRuntime }> { const ctx = new Context() - // The registry is a required injection of SubagentRuntime (its projection - // units register in the constructor). if (withAgents) { await mountAgentLoopTestDependencies(ctx) await ctx.plugin(AgentLoop, { agents: [] }) @@ -119,7 +117,7 @@ describe('SubagentRuntime', () => { await fiber.dispose() - expect(ctx.sessionProjections.stateOf(parent, 'subagentCatalog')).toBeUndefined() + expect(ctx.sessionProjections.snapshot(parent).values.subagentCatalog).toBeUndefined() }) it('registers, lists, looks up, starts, and removes providers', async () => { diff --git a/packages/subagent/subagent/tests/timing-projection.spec.ts b/packages/subagent/subagent/tests/timing-projection.spec.ts index 40b21431ec..dd8af27c69 100644 --- a/packages/subagent/subagent/tests/timing-projection.spec.ts +++ b/packages/subagent/subagent/tests/timing-projection.spec.ts @@ -38,12 +38,14 @@ describe('subagent timing projection', () => { expect(subagentIdentityProjectionDefinition.wire.view(state)).toEqual({ mode: 'one-shot', seq: 1 }) }) - it('registers with the optional session projection registry', async () => { + it('waits for the session projection registry and releases its projections on unload', async () => { const ctx = new Context() await ctx.plugin(SessionStore) - await ctx.plugin(SessionProjectionRegistry) await mountWorkingDirectoryFixture(ctx) - const serviceFiber = await ctx.plugin(SubagentRuntime) + const pending = ctx.plugin(SubagentRuntime) + expect(ctx.get('subagents')).toBeUndefined() + await ctx.plugin(SessionProjectionRegistry) + const serviceFiber = await pending const before = ctx.sessionProjections.snapshot(ctx.sessions.create()).values expect(before.subagentTiming).toEqual({ settledMs: 0 }) diff --git a/packages/subagent/tool-subagent/src/index.ts b/packages/subagent/tool-subagent/src/index.ts index 968b68ebf3..11df129bb1 100644 --- a/packages/subagent/tool-subagent/src/index.ts +++ b/packages/subagent/tool-subagent/src/index.ts @@ -33,11 +33,12 @@ import { registerListSubagentModels } from './list-models.ts' import type {} from './model-selection-settings.ts' import { recordSubagentModelSelection, - subagentModelSelectionProjectionDefinition, subagentModelSelectionPolicy, + subagentModelSelectionProjectionDefinition, } from './model-selection-state.ts' export const name = 'tool-subagent' + export const inject = ['tools', 'subagents', 'systemPrompt', 'sessionProjections'] // Definition identity keeps shared guidance scoped to the actual visible tools, @@ -159,8 +160,9 @@ function providerWording(inheritsConversation: boolean): { description: string; * @param ctx - Context that owns the registrations. * @param config - delegation-tool configuration. * @param session - unpublished Session supplied by a direct Agent setup; omit for a standing composition. + * @returns when the plugin is ready. */ -export function apply(ctx: Context, config: Config, session?: Session): void { +export async function apply(ctx: Context, config: Config, session?: Session): Promise { // Direct apply() bypasses Schemastery's numeric constraints. A direct-apply // omission stays capless (the schema default only runs through the loader). if (config.maxDepth !== 'provider-managed') assertSubagentMaxDepth(config.maxDepth) @@ -171,7 +173,7 @@ export function apply(ctx: Context, config: Config, session?: Session): void { const toolName = config.toolName ?? 'subagent' const modelSelectionCapable = config.modelSelectionSettings === true - ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition) + const modelSelectionProjection = await ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition) const assertSubagentProviderConfiguration = (subagentProvider: SubagentProvider): void => { if (ctx.subagents.resolveMaxDepth(config.maxDepth) !== undefined && !subagentProvider.capabilities.depthLimit) { @@ -401,7 +403,7 @@ export function apply(ctx: Context, config: Config, session?: Session): void { const freshSession = target.firstLiveSeq === 0 // oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred. && target.eventAt(SessionSeq(0))?.type !== 'session/end-seed' - let allowedModels = subagentModelSelectionPolicy(ctx.sessionProjections, target) + let allowedModels = subagentModelSelectionPolicy(modelSelectionProjection, target) if (allowedModels === undefined) { const parentId = target.header.origin === 'subagent' ? target.header.parentSession @@ -414,14 +416,14 @@ export function apply(ctx: Context, config: Config, session?: Session): void { const parent = sessions.get(parentId) allowedModels = parent === undefined ? undefined - : subagentModelSelectionPolicy(ctx.sessionProjections, parent) + : subagentModelSelectionPolicy(modelSelectionProjection, parent) } else if (freshSession) { const current = settings.current() allowedModels = current.enabled ? current.allowedModels : undefined } } if (allowedModels !== undefined) { - recordSubagentModelSelection(ctx.sessionProjections, target, allowedModels) + recordSubagentModelSelection(modelSelectionProjection, target, allowedModels) } return allowedModels === undefined ? undefined : { routes: allowedModels } } diff --git a/packages/subagent/tool-subagent/src/model-selection-state.ts b/packages/subagent/tool-subagent/src/model-selection-state.ts index d3b04b828b..9b51f384f7 100644 --- a/packages/subagent/tool-subagent/src/model-selection-state.ts +++ b/packages/subagent/tool-subagent/src/model-selection-state.ts @@ -2,7 +2,7 @@ import { z as zod } from 'zod' import type { Session } from '@deepseek-ai/dsh-session' -import type SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' +import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection' import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection' import { assertAllowedModelRoutes, type AllowedModelRoute } from './model-selection.ts' @@ -52,29 +52,29 @@ export const subagentModelSelectionProjectionDefinition = { /** * Read the exact route list captured for a model-selectable definition. - * @param projections - registry that owns the policy projection. + * @param projection - registered policy state reader. * @param session - session whose durable decision is read. * @returns a detached route list, or undefined for the fixed-route definition. */ export function subagentModelSelectionPolicy( - projections: Pick, + projection: Pick, 'stateOf'>, session: Session, ): AllowedModelRoute[] | undefined { - return projections.stateOf(session, 'subagentModelSelectionPolicy')?.map(route => ({ ...route })) + return projection.stateOf(session)?.map(route => ({ ...route })) } /** * Append the route policy once, before its definition can reach a model request. - * @param projections - registry that owns the policy projection. + * @param projection - registered policy state reader. * @param session - session receiving the model-selectable definition. * @param allowedModels - exact routes the definition may select explicitly. */ export function recordSubagentModelSelection( - projections: Pick, + projection: Pick, 'stateOf'>, session: Session, allowedModels: readonly AllowedModelRoute[], ): void { - if (subagentModelSelectionPolicy(projections, session) !== undefined) return + if (subagentModelSelectionPolicy(projection, session) !== undefined) return session.append('subagent/model-selection-policy', { allowedModels: allowedModels.map(route => ({ ...route })), }) diff --git a/packages/subagent/tool-subagent/tests/harness.ts b/packages/subagent/tool-subagent/tests/harness.ts index 422d65ba0b..7547d77216 100644 --- a/packages/subagent/tool-subagent/tests/harness.ts +++ b/packages/subagent/tool-subagent/tests/harness.ts @@ -8,6 +8,7 @@ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-test import SubagentRuntime from '@deepseek-ai/dsh-subagent' import { SessionId } from '@deepseek-ai/dsh-session' import * as mock from './scripted-provider.ts' +import type { Config as toolConfig } from '../src/index.ts' import * as tool from '../src/index.ts' import SubagentModelSelectionConfig from '../src/model-selection-settings.ts' @@ -32,7 +33,7 @@ const setupProviders = new WeakMap { - const fiber = agentCtx.inject(tool.inject, (runtimeCtx) => { - tool.apply(runtimeCtx, { ...config, modelSelectionSettings: true }, agent.session) + const fiber = agentCtx.inject(tool.inject, async (runtimeCtx) => { + await tool.apply(runtimeCtx, { ...config, modelSelectionSettings: true }, agent.session) }) await fiber.await() }, diff --git a/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts b/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts index 31ab63b973..a20bbaa404 100644 --- a/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts +++ b/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts @@ -104,7 +104,7 @@ describe('SubagentModelSelectionConfig', () => { selectionConfigs.set(ctx, await liveConfig(ctx, SubagentModelSelectionConfig)) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition) + await ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition) // A stored policy the service cannot apply fails when it is read, not when the profile row is written. await selectionConfigs.get(ctx)!.update({ allowedModels: [...ALLOWED_MODELS, ...ALLOWED_MODELS] }) @@ -122,13 +122,14 @@ describe('SubagentModelSelectionConfig', () => { const invalid = Session.create(SessionId('empty-policy')) invalid.append('subagent/model-selection-policy', { allowedModels: [] }) - expect(() => subagentModelSelectionPolicy(ctx.sessionProjections, invalid)).toThrow('requires at least one route') + const policy = await ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition) + expect(() => subagentModelSelectionPolicy(policy, invalid)).toThrow('requires at least one route') const malformed = Session.create(SessionId('malformed-policy')) malformed.append('subagent/model-selection-policy', { allowedModels: [{ provider: 1, model: 'fast-model' }], } as never) - expect(() => subagentModelSelectionPolicy(ctx.sessionProjections, malformed)) + expect(() => subagentModelSelectionPolicy(policy, malformed)) .toThrow('requires non-empty provider and model ids') await ctx.fiber.dispose() }) @@ -137,14 +138,15 @@ describe('SubagentModelSelectionConfig', () => { const ctx = await boot() const disabled = await createAgent(ctx, 'disabled') expect(selectable(ctx, disabled)).toBe(false) - expect(subagentModelSelectionPolicy(ctx.sessionProjections, disabled.session)).toBeUndefined() + const policy = await ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition) + expect(subagentModelSelectionPolicy(policy, disabled.session)).toBeUndefined() await selectionConfigs.get(ctx)!.update({ enabled: true, allowedModels: ALLOWED_MODELS, }) const enabled = await createAgent(ctx, 'enabled') - expect(subagentModelSelectionPolicy(ctx.sessionProjections, enabled.session)).toEqual(ALLOWED_MODELS) + expect(subagentModelSelectionPolicy(policy, enabled.session)).toEqual(ALLOWED_MODELS) expect(selectable(ctx, enabled)).toBe(true) expect(selectable(ctx, disabled)).toBe(false) @@ -171,8 +173,8 @@ describe('SubagentModelSelectionConfig', () => { sessionId: SessionId('direct-agent-setup'), setup: async (agentCtx, agent) => { prepared = agent - const fiber = agentCtx.inject(tool.inject, (runtimeCtx) => { - tool.apply(runtimeCtx, { + const fiber = agentCtx.inject(tool.inject, async (runtimeCtx) => { + await tool.apply(runtimeCtx, { provider: 'spawn', modelSelectionSettings: true, }, agent.session) @@ -336,7 +338,8 @@ describe('SubagentModelSelectionConfig', () => { meta: { parentSession: parent.id, origin: 'subagent' }, }) expect(selectable(ctx, child)).toBe(true) - expect(subagentModelSelectionPolicy(ctx.sessionProjections, child.session)).toEqual(ALLOWED_MODELS) + const policy = await ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition) + expect(subagentModelSelectionPolicy(policy, child.session)).toEqual(ALLOWED_MODELS) const orphan = await createAgent(ctx, 'orphan', { meta: { parentSession: SessionId('missing-parent'), origin: 'subagent' }, @@ -355,11 +358,11 @@ describe('SubagentModelSelectionConfig', () => { }) const resumedEmpty = await createAgent(ctx, 'resumed-empty', { seed: [] }) expect(selectable(ctx, resumedEmpty)).toBe(false) - expect(subagentModelSelectionPolicy(ctx.sessionProjections, resumedEmpty.session)).toBeUndefined() + expect(subagentModelSelectionPolicy(policy, resumedEmpty.session)).toBeUndefined() const resumedDisabled = await createAgent(ctx, 'resumed-disabled', { seed: oldSeed.snapshotEvents() }) expect(selectable(ctx, resumedDisabled)).toBe(false) - expect(subagentModelSelectionPolicy(ctx.sessionProjections, resumedDisabled.session)).toBeUndefined() + expect(subagentModelSelectionPolicy(policy, resumedDisabled.session)).toBeUndefined() await ctx.fiber.dispose() }) @@ -368,22 +371,22 @@ describe('SubagentModelSelectionConfig', () => { await mountAgentLoopTestDependencies(withoutSettings) await mountWorkingDirectoryFixture(withoutSettings) await withoutSettings.plugin(SubagentRuntime) - expect(() => { - tool.apply(withoutSettings, { + await expect(async () => { + await tool.apply(withoutSettings, { provider: 'missing', modelSelectionSettings: true, maxDepth: 'provider-managed', }) - }).toThrow('requires @deepseek-ai/dsh-tool-subagent/model-selection-settings') + }).rejects.toThrow('requires @deepseek-ai/dsh-tool-subagent/model-selection-settings') await withoutSettings.fiber.dispose() const withoutAgent = await boot(false) - expect(() => { - tool.apply(withoutAgent, { + await expect(async () => { + await tool.apply(withoutAgent, { provider: 'spawn', modelSelectionSettings: true, }) - }).toThrow('requires a scoped preset Context') + }).rejects.toThrow('requires a scoped preset Context') await withoutAgent.fiber.dispose() }) @@ -405,13 +408,13 @@ describe('SubagentModelSelectionConfig', () => { parentSession: SessionId('missing-parent'), }) - expect(() => { - tool.apply(ctx, { + await expect(async () => { + await tool.apply(ctx, { provider: 'missing', modelSelectionSettings: true, maxDepth: 'provider-managed', }, child) - }).toThrow('child model-selection inheritance requires the Session registry') + }).rejects.toThrow('child model-selection inheritance requires the Session registry') } finally { await ctx.fiber.dispose() } diff --git a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts index ea090fa0a3..193b7bece0 100644 --- a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts +++ b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts @@ -18,6 +18,7 @@ import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process' import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' import { loadStoredSession } from '../../subagent/tests/persistence-helpers.ts' import * as mock from './scripted-provider.ts' +import type { Config as toolConfig } from '../src/index.ts' import * as tool from '../src/index.ts' import { SessionId } from '@deepseek-ai/dsh-session' import { @@ -325,7 +326,7 @@ describe('dsh-tool-subagent', () => { }, }) // Direct apply with only `provider` — no toolName, no agentOptions. - tool.apply(ctx, { maxDepth: 'provider-managed', provider: 'bare' }) + await tool.apply(ctx, { maxDepth: 'provider-managed', provider: 'bare' }) await new Promise(r => setTimeout(r, 10)) expect(ctx.tools.schemas().some(s => s.name === 'subagent')).toBe(true) @@ -347,7 +348,7 @@ describe('dsh-tool-subagent', () => { // Tool first: no provider yet — the tool must be absent, not broken. // Direct apply (schema bypass): also covers the waiting-note's default // toolName fallback, which validated config pre-fills. - tool.apply(ctx, { provider: 'mock' }) + await tool.apply(ctx, { provider: 'mock' }) expect(ctx.tools.schemas().some(s => s.name === 'subagent')).toBe(false) // Backend arrives (as a delayed sibling fiber would): the tool appears. await mock.mountScriptedProvider(ctx, { name: 'mock', reply: 'late but fine' }) @@ -360,7 +361,7 @@ describe('dsh-tool-subagent', () => { const ctx = await projectedContext() await mountWorkingDirectoryFixture(ctx) await ctx.plugin(SubagentRuntime) - tool.apply(ctx, { + await tool.apply(ctx, { provider: 'later-continuable', maxDepth: 'provider-managed', }) @@ -575,7 +576,6 @@ describe('dsh-tool-subagent', () => { expect(typeof unwrapped.apply).toBe('function') expect(unwrapped.Config).toBeDefined() }) - it('passes persona/toolFilter/maxDepth config through to the start request', async () => { let seen: { persona?: string; toolFilter?: unknown; maxDepth?: number } | undefined const ctx = await projectedContext() @@ -621,14 +621,14 @@ describe('dsh-tool-subagent', () => { .rejects.toThrow() }) - it('validates maxDepth when apply() is invoked directly without Schemastery', async () => { + it('validates maxDepth during initialization without Schemastery', async () => { const ctx = await projectedContext() - expect(() => { - tool.apply(ctx, { + await expect(async () => { + await tool.apply(ctx, { provider: 'unused', maxDepth: Number.NaN, }) - }).toThrow('subagent maxDepth must be a non-negative safe integer') + }).rejects.toThrow('subagent maxDepth must be a non-negative safe integer') }) it('a partial toolFilter (deny only) does not materialize an empty allow-list (deny-all trap)', async () => { @@ -806,7 +806,7 @@ describe('dsh-tool-subagent local activation', () => { return {} }, }) - tool.apply(ctx, { + await tool.apply(ctx, { provider: 'gated', toolName: 'subagent_gated', maxDepth: 3, @@ -850,7 +850,7 @@ describe('dsh-tool-subagent local activation', () => { describe('depth budget configuration', () => { /** Mount the tool over a request-capturing provider with full capabilities. */ - async function captureSetup(config: Omit = {}) { + async function captureSetup(config: Omit = {}) { const requests: SubagentStartRequest[] = [] const ctx = await projectedContext() await mountWorkingDirectoryFixture(ctx) diff --git a/packages/terminal/terminal-bash/src/index.ts b/packages/terminal/terminal-bash/src/index.ts index 131e75b553..4b05eeb668 100644 --- a/packages/terminal/terminal-bash/src/index.ts +++ b/packages/terminal/terminal-bash/src/index.ts @@ -24,13 +24,12 @@ export type { Config as TerminalLocalConfig } from './config.ts' /** Cordis plugin name. */ export const name = 'terminal-bash' -/** Required services: terminal registry, shared confinement policy, projection registry, and process substrate. */ -export const inject = ['terminals', 'sandboxPolicy', 'sessionProjections', 'subprocess', 'workingDirectory'] +/** Required services: terminal registry, shared confinement policy, process substrate, and working directory. */ +export const inject = ['terminals', 'sandboxPolicy', 'subprocess', 'workingDirectory'] interface SandboxModeFenceState { pty: Context['terminals'] sandboxPolicy: Context['sandboxPolicy'] - sessionProjections: Context['sessionProjections'] } const sandboxModeFences = new WeakMap() @@ -40,20 +39,18 @@ function ensureSandboxModeFence(ctx: Context, owner: Agent): void { if (existing !== undefined) { existing.pty = ctx.terminals existing.sandboxPolicy = ctx.sandboxPolicy - existing.sessionProjections = ctx.sessionProjections return } const state: SandboxModeFenceState = { pty: ctx.terminals, sandboxPolicy: ctx.sandboxPolicy, - sessionProjections: ctx.sessionProjections, } sandboxModeFences.set(owner, state) owner.ctx.on('internal/dispatch', (_mode, eventName, args) => { if (eventName !== 'session/event') return const [session, event] = args as [Session, SessionEvent] if (session !== owner.session || event.type !== 'sandbox/mode') return - const folded = state.sessionProjections.stateOf(session, 'sandboxMode') ?? null + const folded = state.sandboxPolicy.overrideOf(session) ?? null const currentMode = folded ?? state.sandboxPolicy.defaultMode if (event.data.mode === currentMode || !state.pty.hasOwnerActivity(owner)) return throw new Error( diff --git a/packages/terminal/terminal-bash/tests/index.spec.ts b/packages/terminal/terminal-bash/tests/index.spec.ts index a68c798321..9ed1aeeaeb 100644 --- a/packages/terminal/terminal-bash/tests/index.spec.ts +++ b/packages/terminal/terminal-bash/tests/index.spec.ts @@ -624,7 +624,7 @@ describe('terminal-bash plugin shape', () => { const loader = Object.create(Loader.prototype) as Loader const unwrapped = loader.unwrapExports(ptyLocal) as Record expect(unwrapped.name).toBe('terminal-bash') - expect(unwrapped.inject).toEqual(['terminals', 'sandboxPolicy', 'sessionProjections', 'subprocess', 'workingDirectory']) + expect(unwrapped.inject).toEqual(['terminals', 'sandboxPolicy', 'subprocess', 'workingDirectory']) expect(unwrapped.Config).toBeDefined() }) diff --git a/packages/test-support/agent-loop-testkit/tests/agent-loop-testkit.spec.ts b/packages/test-support/agent-loop-testkit/tests/agent-loop-testkit.spec.ts index 932912b4e4..7cdc154c72 100644 --- a/packages/test-support/agent-loop-testkit/tests/agent-loop-testkit.spec.ts +++ b/packages/test-support/agent-loop-testkit/tests/agent-loop-testkit.spec.ts @@ -110,7 +110,7 @@ describe('dsh-agent-loop-testkit', () => { time: 1, data: { target: 'next-turn', start: 99, inserted: [] }, }]) - expect(() => ctx.sessionProjections.stateOf(invalid, 'inbox')) + expect(() => ctx.sessionProjections.checkpoint(invalid).inbox?.val) .toThrow(/invalid persisted inbox splice/) expect(harness.claim(agent, 'next-turn', 3)).toEqual([step, turn]) expect(claimed).toEqual([ diff --git a/packages/todo/tool-todo/src/index.ts b/packages/todo/tool-todo/src/index.ts index f345045c93..5521005375 100644 --- a/packages/todo/tool-todo/src/index.ts +++ b/packages/todo/tool-todo/src/index.ts @@ -20,6 +20,7 @@ import type {} from '@deepseek-ai/dsh-session-projection' export type * from './types.ts' export const name = 'tool-todo' + export const inject = ['tools', 'sessionProjections'] /** The valid {@link TodoItem} statuses, as a runtime set for input narrowing. */ @@ -113,14 +114,15 @@ const todosProjectionSchema: ZodType = zod.union([ * `ctx.sessionProjections`. * @param ctx - registrant context carrying the tool and session-projection registries. * @param config - deployment's explicit todo policy. + * @returns when the plugin is ready. */ -export function apply(ctx: Context, config: Config): void { +export async function apply(ctx: Context, config: Config): Promise { const allowParallel = config.allowParallelInProgress // Standing-plan fold: latest whole todo/write list, cleared by the next // turn/start (turn/end keeps the finished checklist visible); null before the // first write or after a later turn begins; every other event returns the // same state reference. - ctx.sessionProjections.register<'todos', TodoItem[] | null>({ + await ctx.sessionProjections.register<'todos', TodoItem[] | null>({ key: 'todos', stateSchema: todosProjectionSchema, init: () => null, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a9b79453b..3656df5da8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6783,6 +6783,10 @@ importers: version: link:../../core/system-prompt packages/core/agent: + dependencies: + zod: + specifier: 4.4.3 + version: 4.4.3 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -9836,6 +9840,9 @@ importers: '@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:~ From 7ac8f92b700468f324d894b39e4e1d0d7dc028c6 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Mon, 12 Oct 2026 12:03:37 +0800 Subject: [PATCH 029/369] test: adapt application and benchmark fixtures to async activation --- ...09-28-session-corpus-performance.i18n.yaml | 4 +-- .../2026-09-28-session-corpus-performance.md | 2 ++ ...026-09-28-session-corpus-performance.zh.md | 2 ++ apps/cli/tests/web-agent-presets.e2e.ts | 6 ++-- apps/web/tests/agent-preset-selection.e2e.ts | 6 ++-- apps/web/tests/blank-writer-reuse.e2e.ts | 2 +- .../session-archive-active/dialog.expected.md | 2 +- apps/web/tests/session-archive-active.e2e.ts | 2 +- apps/web/tests/subagent-interrupt.e2e.ts | 4 +-- .../session-corpus/projection-list.worker.ts | 7 ++-- .../session-corpus/session-controller.ts | 32 +++++++++++++++++++ .../session-corpus/session-corpus.worker.ts | 7 ++-- .../session-open/session-open.worker.ts | 8 ++--- 13 files changed, 62 insertions(+), 22 deletions(-) create mode 100644 benchmarks/session-corpus/session-controller.ts diff --git a/.agents/notes/implemented/testing/2026-09-28-session-corpus-performance.i18n.yaml b/.agents/notes/implemented/testing/2026-09-28-session-corpus-performance.i18n.yaml index 38607ee2df..4a98c2f555 100644 --- a/.agents/notes/implemented/testing/2026-09-28-session-corpus-performance.i18n.yaml +++ b/.agents/notes/implemented/testing/2026-09-28-session-corpus-performance.i18n.yaml @@ -18,8 +18,8 @@ en: 1310a89c351e5bfe zh: e6386ae10b893c2d /agent-note-performance-baselines-for-large-session-corpora/decision/measured-endpoints: - en: b4d81575ac574f6f - zh: f4f291ab7ce83094 + en: 11937dfa4a349a19 + zh: b91a136890fe92e8 /agent-note-performance-baselines-for-large-session-corpora/calibration-evidence: en: f934c95748dcbfe0 zh: df6a68286c21510f diff --git a/.agents/notes/implemented/testing/2026-09-28-session-corpus-performance.md b/.agents/notes/implemented/testing/2026-09-28-session-corpus-performance.md index 233491ebe0..2efe66ead2 100644 --- a/.agents/notes/implemented/testing/2026-09-28-session-corpus-performance.md +++ b/.agents/notes/implemented/testing/2026-09-28-session-corpus-performance.md @@ -37,6 +37,8 @@ The projection cache is part of the list endpoint: each cold row carries cached Workers are compiled plain-Node processes, each started fresh against a seeded root. They compose the production Session store, projection registry, JSONL persistence, SQLite Session query with `openAt: first-search` on an in-memory index, JSON storage and projection cache, the Web projection set, AgentLoop, and the real `SessionController`. Transport lookups, upload registration, default model selection, and the Workspace registry are inert providers, because no measured path depends on them. +The shared corpus and projection-list controller fixture awaits controller activation and supplies the fixed attachment image policy. Its snapshots therefore include the production `imageLimits` unit; attachment I/O remains outside these workloads. Event corpora, measurement endpoints, and budgets are unchanged. + | Case | Corpus | Samples | Endpoint | |---|---|---:|---| | List | 3,000 | 3 | Host boot; first `session.list`; repeated `session.list` in the same process | diff --git a/.agents/notes/implemented/testing/2026-09-28-session-corpus-performance.zh.md b/.agents/notes/implemented/testing/2026-09-28-session-corpus-performance.zh.md index 4ed0283df7..f964d640b8 100644 --- a/.agents/notes/implemented/testing/2026-09-28-session-corpus-performance.zh.md +++ b/.agents/notes/implemented/testing/2026-09-28-session-corpus-performance.zh.md @@ -37,6 +37,8 @@ Status: implemented worker 是编译后的纯 Node 进程,每次都针对已播种根目录全新启动。它们组合生产 Session store、投影注册表、JSONL 持久化、使用 `openAt: first-search` 和内存索引的 SQLite Session query、JSON 存储和投影缓存、Web 投影集合、AgentLoop 以及真实的 `SessionController`。传输查找、上传注册、默认模型选择和 Workspace 注册表使用惰性提供者,因为没有被测路径依赖它们。 +语料与 projection-list 共用的控制器夹具等待控制器激活,并提供固定附件图片策略。因此快照包含生产 `imageLimits` 单元;附件 I/O 仍不在这些负载中。事件语料、测量端点和预算保持不变。 + | 用例 | 语料 | 样本数 | 终点 | |---|---|---:|---| | 列表 | 3,000 | 3 | Host 启动;首次 `session.list`;同一进程中重复的 `session.list` | diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts index db0d8e26e3..bf1b9ffb9e 100644 --- a/apps/cli/tests/web-agent-presets.e2e.ts +++ b/apps/cli/tests/web-agent-presets.e2e.ts @@ -1,3 +1,4 @@ +import { agentPresetProjectionDefinition } from '@deepseek-ai/dsh-agent-preset-registry' import { createRequire } from 'node:module' import { randomUUID } from 'node:crypto' import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises' @@ -730,7 +731,7 @@ describe('a switch survives the session', () => { // The header keeps the creation fact; the log carries what it runs. expect(handle.agent.session.header.agentPreset).toBe('standard') - expect(ctx.sessionProjections.stateOf(handle.agent.session, 'agentPreset')).toBe('minimal') + expect(ctx.sessionProjections.checkpoint(handle.agent.session).agentPreset?.val).toBe('minimal') } finally { await handle.dispose() } @@ -745,7 +746,8 @@ describe('a forked session', () => { meta: { agentPreset: 'minimal' }, setup: agentCtx => ctx.agentPresets.mount(agentCtx, 'minimal').then(() => undefined), }) - const inherited = ctx.sessionProjections.stateOf(parent.agent.session, 'agentPreset') ?? undefined + const projection = await ctx.sessionProjections.register(agentPresetProjectionDefinition) + const inherited = projection.stateOf(parent.agent.session) ?? undefined const child = await ctx.agents.create({ sessionId: SessionId('preset-fork-child'), seed: [], diff --git a/apps/web/tests/agent-preset-selection.e2e.ts b/apps/web/tests/agent-preset-selection.e2e.ts index 6fc12aba02..d695e52818 100644 --- a/apps/web/tests/agent-preset-selection.e2e.ts +++ b/apps/web/tests/agent-preset-selection.e2e.ts @@ -465,7 +465,7 @@ it.each([false, true])('starts Creator from Plugins with Coding Tools=%s without expect(await composer.innerText()).toBe(draft) expect(await picker.getAttribute('aria-haspopup')).toBe('menu') expect(await page.getByRole('dialog', { name: 'Add plugin', exact: true }).count()).toBe(0) - expect(scaffold.ctx.sessionProjections.stateOf(creator.session, 'agentPreset')).toBe('cordis') + expect(scaffold.ctx.sessionProjections.checkpoint(creator.session).agentPreset?.val).toBe('cordis') expect(scaffold.ctx.agentPresets.composedPreset(creator.ctx)).toBe('cordis') expect(scaffold.ctx.tools.schemas(creator).map(tool => tool.name)) .toEqual(expect.arrayContaining(['cordis_inspect_list', 'cordis_inspect_query', 'plugin_manager'])) @@ -490,9 +490,9 @@ it.each([false, true])('starts Creator from Plugins with Coding Tools=%s without const next = scaffold.ctx.agents.list().find(agent => agent.id !== creator.id)! expect(next.id).not.toBe(creator.id) expect(next.session.header.agentPreset).toBe('standard') - expect(scaffold.ctx.sessionProjections.stateOf(next.session, 'agentPreset')).toBe('standard') + expect(scaffold.ctx.sessionProjections.checkpoint(next.session).agentPreset?.val).toBe('standard') expect(scaffold.ctx.agentPresets.composedPreset(next.ctx)).toBe('standard') - expect(scaffold.ctx.sessionProjections.stateOf(creator.session, 'agentPreset')).toBe('cordis') + expect(scaffold.ctx.sessionProjections.checkpoint(creator.session).agentPreset?.val).toBe('cordis') await page.getByRole('button', { name: 'Standard mode', exact: true }).waitFor() expect(scaffold.ctx.settings.describe().find(row => row.ns === 'ui-settings')?.value).toMatchObject({ enabled: developerTools }) expect(scaffold.ctx.agentPresets.defaultId).toBe('standard') diff --git a/apps/web/tests/blank-writer-reuse.e2e.ts b/apps/web/tests/blank-writer-reuse.e2e.ts index 99b2be07e3..74cb108982 100644 --- a/apps/web/tests/blank-writer-reuse.e2e.ts +++ b/apps/web/tests/blank-writer-reuse.e2e.ts @@ -72,7 +72,7 @@ describe.each([false, true])('web e2e: selected blank writer held: %s', (held) = if (!held) { expect(selectedId).toBe(HELD) const reclaimed = scaffold.ctx.agents.get(HELD)! - expect(scaffold.ctx.sessionProjections.stateOf(reclaimed.session, 'plan')).toMatchObject({ active: true }) + expect(scaffold.ctx.sessionProjections.checkpoint(reclaimed.session).plan?.val).toMatchObject({ active: true }) await expect(scaffold.ctx.sessionPersistence.open(HELD, 'write')) .rejects.toMatchObject({ name: 'SessionAlreadyOwnedError' }) } diff --git a/apps/web/tests/expected/session-archive-active/dialog.expected.md b/apps/web/tests/expected/session-archive-active/dialog.expected.md index 2a6f4d638f..4604467eeb 100644 --- a/apps/web/tests/expected/session-archive-active/dialog.expected.md +++ b/apps/web/tests/expected/session-archive-active/dialog.expected.md @@ -3,7 +3,7 @@ - button "Close" - paragraph: “ARCHIVE_ACTIVE run the long command” still has work in progress. Archiving stops it first; you can restore the session later from the “All conversations (show archived)” filter in the sidebar, and the stopped work will not resume on its own. - list "Work that will be stopped": - - listitem: The turn in progress - listitem: "1 background job: node -e \"const fs=require('node:fs'); fs.writeFileSync('started.txt', 'started'); setInterval(() => { fs.appendFileSync('heartbeat.txt', '.') }, 200); setTimeout(() => { fs.writeFileSync('finished.txt', 'finished') }, 60000)\"" + - listitem: The turn in progress - button "Cancel" - button "Stop and archive" diff --git a/apps/web/tests/session-archive-active.e2e.ts b/apps/web/tests/session-archive-active.e2e.ts index d192d3d207..8360f937d8 100644 --- a/apps/web/tests/session-archive-active.e2e.ts +++ b/apps/web/tests/session-archive-active.e2e.ts @@ -162,7 +162,7 @@ describe.skipIf(MODE === 'record')('web e2e: archiving a running Session stops i // A foreground Bash command is a registered job from its start, so the // Host reports it beside the turn; stopping the turn kills it. await expect.poll(() => dialog.getByRole('list', { name: 'Work that will be stopped' }).locator('li').allTextContents()) - .toEqual(['The turn in progress', `1 background job: ${WAIT_COMMAND}`]) + .toEqual([`1 background job: ${WAIT_COMMAND}`, 'The turn in progress']) // Asking is not archiving: the Host set is untouched, the turn and its // Bash child keep running, and the row stays in the sidebar. expect([...scaffold.ctx.workspaceRegistry.archivedSessionIds]).toEqual([]) diff --git a/apps/web/tests/subagent-interrupt.e2e.ts b/apps/web/tests/subagent-interrupt.e2e.ts index 7a35bcfcb9..a1b1284a19 100644 --- a/apps/web/tests/subagent-interrupt.e2e.ts +++ b/apps/web/tests/subagent-interrupt.e2e.ts @@ -43,8 +43,8 @@ async function remote( } /** POST one generated Session Remote unary through the API Gateway carrier. */ -function sessionRemote(scaffold: WebScaffold, method: string, request: unknown): Promise> { - return remote(scaffold, `session/${method}`, { request }) +async function sessionRemote(scaffold: WebScaffold, method: string, request: unknown): Promise> { + return await remote(scaffold, `session/${method}`, { request }) } /** Poll a synchronous condition (hook-safe; expect.poll is test-body only). */ diff --git a/benchmarks/session-corpus/projection-list.worker.ts b/benchmarks/session-corpus/projection-list.worker.ts index 78a60b5c06..c00f9dd771 100644 --- a/benchmarks/session-corpus/projection-list.worker.ts +++ b/benchmarks/session-corpus/projection-list.worker.ts @@ -7,7 +7,7 @@ import { monitorEventLoopDelay, performance } from 'node:perf_hooks' import { scheduler, setTimeout as delay } from 'node:timers/promises' import { Context } from '@deepseek-ai/cordis' import AgentRegistry from '@deepseek-ai/dsh-agent' -import SessionController from '@deepseek-ai/dsh-api-session-controller' +import type SessionController from '@deepseek-ai/dsh-api-session-controller' import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset, type SessionHeader } from '@deepseek-ai/dsh-session' import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' @@ -20,6 +20,7 @@ import { z } from 'zod' import { generationLogPath, sessionDir, toHeaderLine } from '../../packages/session/session-persistence-jsonl/src/format.ts' import { compressZstdFrame } from '../../packages/session/session-persistence-jsonl/src/zstd.ts' import { assertBuiltBenchmarkRuntime } from '../support/built-worker.ts' +import { createBenchmarkSessionController } from './session-controller.ts' const KEY = 'syntheticProjectionList' const CWD = '/synthetic-projection-benchmark' @@ -171,7 +172,7 @@ async function mountHost(ctx: Context, root: string, workload: typeof WORKLOADS[ await ctx.plugin(SessionStore) await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjectionRegistry) - ctx.sessionProjections.register({ + await ctx.sessionProjections.register({ key: KEY, stateVersion: 1, stateSchema, init: header => makeState(Number(header.id.slice('projection-bench-'.length)), workload.baseRows), apply: state => state, @@ -200,7 +201,7 @@ async function mountHost(ctx: Context, root: string, workload: typeof WORKLOADS[ ctx.provide('fileUploads', { registerAgentResolver: () => dispose } as never) ctx.provide('agentDefaultModel', { currentSelection: () => ({ provider: 'synthetic', model: 'generated-model' }) } as never) ctx.provide('workspaceRegistry', { list: () => [], archivedSessionIds: [] } as never) - return new SessionController(ctx, { nativeOpen: false, ...(listWorkSliceMs === undefined ? {} : { listWorkSliceMs }) }, { canOpenPath: () => false }) + return createBenchmarkSessionController(ctx, { nativeOpen: false, ...(listWorkSliceMs === undefined ? {} : { listWorkSliceMs }) }) } async function memory(): Promise { diff --git a/benchmarks/session-corpus/session-controller.ts b/benchmarks/session-corpus/session-controller.ts new file mode 100644 index 0000000000..f5b3cac97e --- /dev/null +++ b/benchmarks/session-corpus/session-controller.ts @@ -0,0 +1,32 @@ +/** Session-controller initialization for synthetic corpus workers without attachment I/O. */ + +import { Service, type Context } from '@deepseek-ai/cordis' +import SessionController, { type Config } from '@deepseek-ai/dsh-api-session-controller' + +class BenchmarkSessionController extends SessionController { + initialize(): Promise { + return this[Service.init]() + } +} + +/** + * Initialize the controller against the worker's partial Host services. + * @param ctx - worker context owning service disposal. + * @param config - native-opener and list-scheduling settings. + * @returns controller ready for list, search, and fork calls. + */ +export async function createBenchmarkSessionController(ctx: Context, config: Config): Promise { + ctx.provide('attachments', { + imageLimits: { + maxImageBytes: 5 * 1024 * 1024, + maxImagesPerMessage: 20, + maxMessageImageBytes: 100 * 1024 * 1024, + maxImagePixels: 40_000_000, + maxImageDimension: 2000, + mediaTypes: ['image/png'], + }, + } as never) + const controller = new BenchmarkSessionController(ctx, config, { canOpenPath: () => false }) + await controller.initialize() + return controller +} diff --git a/benchmarks/session-corpus/session-corpus.worker.ts b/benchmarks/session-corpus/session-corpus.worker.ts index 8dabac10ca..ec56b55e44 100644 --- a/benchmarks/session-corpus/session-corpus.worker.ts +++ b/benchmarks/session-corpus/session-corpus.worker.ts @@ -8,7 +8,7 @@ import { Context } from '@deepseek-ai/cordis' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' import { agentPresetProjectionDefinition } from '@deepseek-ai/dsh-agent-preset-registry' -import SessionController from '@deepseek-ai/dsh-api-session-controller' +import type SessionController from '@deepseek-ai/dsh-api-session-controller' import { currentSessionMessageProjections } from '@deepseek-ai/dsh-session-format-catalog/message-projections' import { Session, SessionLogOffset } from '@deepseek-ai/dsh-session' import type { SessionId } from '@deepseek-ai/dsh-session' @@ -27,6 +27,7 @@ import TokenMeter from '@deepseek-ai/dsh-token-meter' import { assertBuiltBenchmarkRuntime } from '../support/built-worker.ts' import { serializeRecord } from '../../packages/storage/storage-json/src/format.ts' import { ANCHOR_COUNT } from './corpus-shape.ts' +import { createBenchmarkSessionController } from './session-controller.ts' import { anchorHeader, corpusSessionId, @@ -140,7 +141,7 @@ async function mountHost(root: string): Promise<{ readonly ctx: Context; readonl await ctx.plugin(SessionProjectionCache, { writeEveryEvents: 200, writeIntervalMs: 5_000 }) // Content search is opt-in; enabled deployments defer the in-memory index to the first search. await ctx.plugin(SqliteSessionQueryEngine, { path: ':memory:', openAt: 'first-search' }) - ctx.sessionProjections.register(agentPresetProjectionDefinition) + await ctx.sessionProjections.register(agentPresetProjectionDefinition) await ctx.plugin(SessionTitleService, { fallbackMaxWords: 5, fallbackMaxBytes: 40, maxTitleBytes: 80 }) await ctx.plugin(SessionStatsPlugin) await ctx.plugin(SessionTurnOutlinePlugin) @@ -152,7 +153,7 @@ async function mountHost(root: string): Promise<{ readonly ctx: Context; readonl ctx.provide('fileUploads', { registerAgentResolver: () => dispose } as never) ctx.provide('agentDefaultModel', { currentSelection: () => BENCH_MODEL } as never) ctx.provide('workspaceRegistry', { list: () => [], archivedSessionIds: [] } as never) - const controller = new SessionController(ctx, { nativeOpen: false }, { canOpenPath: () => false }) + const controller = await createBenchmarkSessionController(ctx, { nativeOpen: false }) return { ctx, controller } } diff --git a/benchmarks/session-open/session-open.worker.ts b/benchmarks/session-open/session-open.worker.ts index b1e2bfec1d..91a37ac8ae 100644 --- a/benchmarks/session-open/session-open.worker.ts +++ b/benchmarks/session-open/session-open.worker.ts @@ -30,7 +30,7 @@ import * as SessionTurnOutlinePlugin from '@deepseek-ai/dsh-session-turn-outline import TokenMeter from '@deepseek-ai/dsh-token-meter' // These Host-only adapters have no public Node export and are compiled into the benchmark worker. import { SessionHistoryController } from '../../packages/api/session-controller/src/history.ts' -import { installModelSelectionProjection } from '../../packages/api/session-controller/src/model-selection-projection.ts' +import { modelSelectionProjection } from '../../packages/api/session-controller/src/model-selection-projection.ts' import { assertBuiltBenchmarkRuntime } from '../support/built-worker.ts' import { SYNTHETIC_SESSION_ID } from './session-open.constants.ts' @@ -130,9 +130,9 @@ function memoryDelta( async function installProjectionSet(ctx: Context, agentLoopOwnsBoundary: boolean): Promise { // Mirrors projection owners mounted by the base and web-app bundles without timing profile boot. - if (!agentLoopOwnsBoundary) ctx.sessionProjections.register(turnBoundaryProjectionDefinition) - ctx.sessionProjections.register(agentPresetProjectionDefinition) - installModelSelectionProjection(ctx) + if (!agentLoopOwnsBoundary) await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + await ctx.sessionProjections.register(agentPresetProjectionDefinition) + await ctx.sessionProjections.register(modelSelectionProjection) await ctx.plugin(SessionTitleService, { fallbackMaxWords: 5, fallbackMaxBytes: 40, From ce5c38791ef531459916df9c6f4c48609fd8b4dc Mon Sep 17 00:00:00 2001 From: _Kerman Date: Mon, 12 Oct 2026 12:03:37 +0800 Subject: [PATCH 030/369] docs: update projection lifecycle guidance and generated catalogs --- ...ession-projection-mandatory-seam.i18n.yaml | 12 ++--- ...08-19-session-projection-mandatory-seam.md | 12 ++--- ...19-session-projection-mandatory-seam.zh.md | 12 ++--- ...-projection-registration-handles.i18n.yaml | 19 +++++++ ...session-projection-registration-handles.md | 31 +++++++++++ ...sion-projection-registration-handles.zh.md | 31 +++++++++++ AGENTS.md | 2 +- docs/architecture.i18n.yaml | 4 +- docs/architecture.md | 2 +- docs/architecture.zh.md | 2 +- docs/config-catalog.md | 30 +++++------ docs/config-catalog.zh.md | 30 +++++------ docs/defensive-patterns/remote-and-api.md | 8 +-- docs/defensive-patterns/remote-and-api.zh.md | 8 +-- docs/event-producer-consumer.md | 2 +- docs/event-producer-consumer.zh.md | 2 +- docs/persistence-catalog.i18n.yaml | 8 +-- docs/persistence-catalog.md | 4 +- docs/persistence-catalog.zh.md | 4 +- docs/persistence-schema.json | 2 +- docs/subsystems/plan.i18n.yaml | 4 +- docs/subsystems/plan.md | 2 +- docs/subsystems/plan.zh.md | 2 +- docs/subsystems/session-projection.i18n.yaml | 12 ++--- docs/subsystems/session-projection.md | 52 +++++++++++-------- docs/subsystems/session-projection.zh.md | 52 +++++++++++-------- .../guide.i18n.yaml | 16 ++++++ .../claude-code-mods-projections/guide.md | 17 ++++++ .../claude-code-mods-projections/guide.zh.md | 17 ++++++ .../subagent-activations/guide.i18n.yaml | 8 +-- .../subagent-activations/guide.md | 6 ++- .../subagent-activations/guide.zh.md | 6 ++- .../guide.i18n.yaml | 16 ++++++ .../session-projection-handles/guide.md | 21 ++++++++ .../session-projection-handles/guide.zh.md | 21 ++++++++ .../extensions/tool-cordis/src/api-catalog.ts | 24 ++++----- scripts/gen-config-catalog.spec.ts | 25 +++++++++ scripts/gen-config-catalog.ts | 2 +- scripts/gen-cordis-catalog.ts | 1 + scripts/type-equiv.manifest.json | 5 ++ 40 files changed, 386 insertions(+), 148 deletions(-) create mode 100644 .agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.i18n.yaml create mode 100644 .agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.md create mode 100644 .agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.zh.md create mode 100644 docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.i18n.yaml create mode 100644 docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.md create mode 100644 docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.zh.md create mode 100644 docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.i18n.yaml create mode 100644 docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.md create mode 100644 docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.zh.md diff --git a/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.i18n.yaml index 24f76c7cb0..f391fe518e 100644 --- a/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.i18n.yaml @@ -6,14 +6,14 @@ en: 41f30d473ff53ce2 zh: 6851c4414f5331c1 /agent-note-session-projections-as-a-required-reader-seam/problem: - en: c0c779c717cad546 - zh: 5d2916ac86bf5d84 + en: fca00eeb7a8d9abd + zh: d7a9766f5322dff9 /agent-note-session-projections-as-a-required-reader-seam/decision: - en: a1c50d2f43334a88 - zh: d7736994733454b4 + en: dd550c8f4c2aa263 + zh: 9e60b0e465697e77 /agent-note-session-projections-as-a-required-reader-seam/alternatives-considered: en: aea3da939c90d240 zh: 21e91c481cdd23b7 /agent-note-session-projections-as-a-required-reader-seam/consequences: - en: 73325513ec70fbb2 - zh: 610bf34b9f9db420 + en: ab45eb87f7c22cdb + zh: e65a97991a45164f diff --git a/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.md b/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.md index e1c6835311..5ab32c0b2c 100644 --- a/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.md +++ b/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.md @@ -6,19 +6,17 @@ English | [中文](2026-08-19-session-projection-mandatory-seam.zh.md) ## Problem -An optional projection registry lets a plugin whose host behavior reads projected state activate without that state. Unless the reader rejects the missing registry or key, the host behavior or subagent catalog fields can silently disappear. Batch-only reads also materialize every client view when a consumer needs one host value. Some contributor sites intentionally retain optional `ctx.inject` registration, so their readers need an explicit missing-state rule. +An optional projection registry lets a plugin whose host behavior reads projected state activate without that state. Unless the reader rejects the missing registry or key, the host behavior or subagent catalog fields can silently disappear. Batch-only reads also materialize every client view when a consumer needs one host value. ## Decision This decision builds on the split between host projection state and client views in [Session projection state and client views](../../archived/architecture/2026-08-19-session-projection-state-and-client-views.md). -Every host reader treats the projection registry and its required key as mandatory state. A plugin either declares `sessionProjections` as a required injection or resolves the registry and key explicitly and throws on the first dependent access. Official compositions mount the registry before those plugins. `ApiProxyService` follows the required-injection form; the lower-level `createApiProxy` factory remains tolerant for isolated tests and diagnostics. +Every host reader treats the projection registry and its required key as mandatory state. Plugins declare `sessionProjections` as a required injection and await acquisition before exposing readers. Official compositions mount the registry before those plugins. `ApiProxyService` follows the required-injection form; the lower-level `createApiProxy` factory remains tolerant for isolated tests and diagnostics. -A domain contributor may register its unit through `ctx.inject(['sessionProjections'], ...)`. Optional registration controls the child lifecycle only; it does not authorize a reader to substitute a default when the registry or key is absent. +Readers acquire state through [awaited registration and retained handles](2026-10-12-session-projection-registration-handles.md). That decision supersedes optional first-access acquisition and returning a bare disposer. It preserves the required-state rule here: a missing registry cannot silently produce default host state. Client views still contain only consumed fields; typed handle reads provide richer host state. -The registry provides `stateOf(session, key)` for one typed host state and keeps `snapshot()` for batch carriers. Client views contain only consumed fields; host readers use `stateOf` for richer state. - -`onChanged` publishes client-visible value changes only. Unit registration and removal remain effect-scoped registry lifecycle; `register()` returns the exact Cordis disposer so a composite domain owner can finish cleanup against projected state before removing its unit. Registration changes do not create a second Host event stream or client tombstone protocol. A later authoritative history or list baseline reflects the active key set. +`onChanged` publishes client-visible value changes only. Registration changes do not create a second Host event stream or client tombstone protocol. A later authoritative history or list baseline reflects the active key set. ## Alternatives considered @@ -30,6 +28,6 @@ The registry provides `stateOf(session, key)` for one typed host state and keeps ## Consequences -- Missing projection composition fails either during activation or at the first dependent host access; it never degrades to a default value. +- Missing projection composition fails during activation; it never degrades to a default value. - Host consumers avoid repeated whole-registry snapshots and log scans. - Wire payloads exclude host-only fields and per-key watermark wrappers; ordinary baselines communicate the active key set. diff --git a/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.zh.md b/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.zh.md index 5d6a5169a5..0ab43fd5c0 100644 --- a/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.zh.md @@ -6,19 +6,17 @@ Status: implemented ## 问题 -可选的投影注册表会让 host 行为读取投影状态的插件在缺少该状态时仍然激活。除非读取方拒绝缺失的注册表或 key,否则 host 行为或 subagent 目录字段可能静默消失。只有批量读取也会在消费方只需要一个 host 值时物化每个客户端 view。部分贡献位置有意保留可选的 `ctx.inject` 注册形式,因此其读取方需要明确的缺失状态规则。 +可选的投影注册表会让 host 行为读取投影状态的插件在缺少该状态时仍然激活。除非读取方拒绝缺失的注册表或 key,否则 host 行为或 subagent 目录字段可能静默消失。只有批量读取也会在消费方只需要一个 host 值时物化每个客户端 view。 ## 决策 本决策建立在[会话投影的 host 状态与客户端视图](../../archived/architecture/2026-08-19-session-projection-state-and-client-views.md)所定义的拆分之上。 -每个 host 读取方都把投影注册表及其所需 key 视为必需状态。插件要么把 `sessionProjections` 声明为必需注入,要么显式解析注册表与 key,并在第一次依赖它们的访问时抛错。正式组合在这些插件之前挂载注册表。`ApiProxyService` 采用必需注入形式;较低层的 `createApiProxy` factory 对隔离测试和诊断保持容错。 +每个 host 读取方都把投影注册表及其所需 key 视为必需状态。插件将 `sessionProjections` 声明为必需注入,并在暴露读取方前等待获取完成。正式组合在这些插件之前挂载注册表。`ApiProxyService` 采用必需注入形式;较低层的 `createApiProxy` factory 对隔离测试和诊断保持容错。 -领域贡献方可以通过 `ctx.inject(['sessionProjections'], ...)` 注册单元。可选注册只控制子功能生命周期;它并不允许读取方在注册表或 key 缺失时替换为默认值。 +读取方通过[等待注册与保留句柄](2026-10-12-session-projection-registration-handles.zh.md)获取状态。该决策取代可选首次访问获取和返回裸释放函数的方式,保留这里的必需状态规则:缺少注册表不能静默产生默认 host 状态。客户端视图仍只包含消费字段;类型化句柄读取提供更丰富的 host 状态。 -注册表提供 `stateOf(session, key)` 来读取一个类型化 host 状态,并为批量 carrier 保留 `snapshot()`。客户端 view 只包含消费方使用的字段;host 读取方通过 `stateOf` 取得更丰富的状态。 - -`onChanged` 只发布客户端可见值的变化。单元注册和移除仍是绑定 effect 的注册表生命周期;`register()` 返回 Cordis 的原始 disposer,使组合式领域 owner 可以先依据投影状态完成清理,再移除自身单元。注册变化不会创建第二条 Host 事件流或客户端 tombstone 协议。后续权威 history 或 list 基线会反映活跃 key 集。 +`onChanged` 只发布客户端可见值的变化。注册变化不会创建第二条 Host 事件流或客户端 tombstone 协议。后续权威 history 或 list 基线会反映活跃 key 集。 ## 考虑过的替代方案 @@ -30,6 +28,6 @@ Status: implemented ## 后果 -- 缺少投影组合会在激活期间或第一次依赖它的 host 访问时失败,绝不会降级为默认值。 +- 缺少投影组合会在激活期间失败,绝不会降级为默认值。 - host 消费方避免重复的全注册表快照和日志扫描。 - 协议负载排除 host 内部字段和逐 key 水位包装;普通基线会传达活跃 key 集。 diff --git a/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.i18n.yaml new file mode 100644 index 0000000000..c22458657b --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for 2026-10-12-session-projection-registration-handles.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-10-12-session-projection-registration-handles.md +/agent-note-asynchronous-projection-registration-and-retained-readers: + en: 693349255afbaec8 + zh: 13bcbf3f40b93b2b +/agent-note-asynchronous-projection-registration-and-retained-readers/problem: + en: 47ba548b869be190 + zh: c2e5038a56fe741c +/agent-note-asynchronous-projection-registration-and-retained-readers/decision: + en: 19064b5aecb26f57 + zh: 075bb87c6a5299ae +/agent-note-asynchronous-projection-registration-and-retained-readers/alternatives-considered: + en: ee5b0cd715ed1fef + zh: 6b1478dcde21916d +/agent-note-asynchronous-projection-registration-and-retained-readers/consequences: + en: 4b5e4bb446872545 + zh: 9fb2cc8ae77fb823 diff --git a/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.md b/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.md new file mode 100644 index 0000000000..ea16e10828 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.md @@ -0,0 +1,31 @@ +# Agent Note: Asynchronous projection registration and retained readers + +Status: implemented + +English | [中文](2026-10-12-session-projection-registration-handles.zh.md) + +## Problem + +Host readers coupled to registry key lookup lose access when a plugin unloads, although an Agent may still need projected state while its asynchronous cleanup drains. Repeated batch snapshots materialize unrelated client views. A synchronous registration API also makes every consumer assume that acquisition can never require asynchronous initialization. + +## Decision + +`SessionProjectionRegistry.register` returns a promise for a typed handle. Consumers await it during activation before exposing dependent operations. Function plugins retain their namespace exports and use asynchronous `apply`; services use their initialization hook. Definitions remain with their domain owner, and each reader registers the required definition instead of looking up a possibly absent key. This supersedes the optional first-access acquisition and disposer-return details in [the mandatory-reader decision](2026-08-19-session-projection-mandatory-seam.md), while retaining its mandatory-state and cropped-client-view rules. + +A handle owns one reference to a shared definition and its per-Session cells. Its disposer releases global participation exactly once; the final release removes the unit from live drives, snapshots, and checkpoints. Its reader remains usable after release, folding unseen events with the original definition. A later registration at a different state version does not replace an older handle's definition. Registration checks owner liveness after its asynchronous handoff and rejects if the owner unloaded before receiving the handle. + +AgentLoop acquires inbox and turn-boundary handles once before publishing its factory. Each Agent receives those readers during synchronous construction. Cleanup can use them after the loop fiber removes global registrations, so projection release need not wait for every Agent teardown. This preserves the existing creation/setup sequence without per-Agent registration or an extra initialization state. + +## Alternatives considered + +**Keep registry-level lookup and delay unregistering.** This couples unrelated plugin disposal order to every reader's final access. Retained readers make cleanup independent of that order. + +**Return a synchronous handle.** This preserves callers' immediate reads but commits the acquisition API to synchronous initialization. Awaited acquisition leaves that timing explicit while state reads remain synchronous after readiness. + +**Register the same units for each Agent.** The definitions already contain per-Session cells. Additional references and asynchronous Agent initialization add a creation race without providing independent data ownership. + +**Convert function plugins to classes.** Cordis already awaits promises returned by `apply`. Preserving namespace exports avoids a published entry-point and diagnostic-name break. + +## Consequences + +Consumers must migrate acquisition and retain handles. Released handles retain their definitions and weakly keyed cells for as long as the caller keeps them; they do not restore global wire participation. Registration-reader tests cover post-release folding, version replacement, unrelated-unit isolation, repeated disposal, concurrent owner disposal, and owner unload during acquisition. Agent lifecycle tests cover cleanup failures, scope-minting races, and background child shutdown. diff --git a/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.zh.md b/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.zh.md new file mode 100644 index 0000000000..28974a237b --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.zh.md @@ -0,0 +1,31 @@ +# Agent Note: 异步投影注册与保留读取句柄 + +Status: implemented + +[English](2026-10-12-session-projection-registration-handles.md) | 中文 + +## Problem + +依赖注册表键查询的 Host 读取方会在插件卸载时失去访问能力,但 Agent 可能仍需在异步清理排空期间读取投影状态。重复的批量快照会具象化无关 Client 视图。同步注册 API 也使每个消费者假定获取过程永远不需要异步初始化。 + +## Decision + +`SessionProjectionRegistry.register` 返回类型化句柄的 Promise。消费者在激活期间等待它,再暴露依赖操作。函数插件保留命名空间导出并使用异步 `apply`;服务使用初始化钩子。定义仍归领域拥有方,每个读取方注册所需定义,不再查询可能不存在的键。这取代了[必需读取方决策](2026-08-19-session-projection-mandatory-seam.zh.md)中的可选首次访问获取方式与返回释放函数的细节,同时保留必需状态和裁剪 Client 视图的规则。 + +句柄拥有共享定义及其逐 Session 单元的一次引用。其释放函数恰好一次地解除全局参与;最后一次释放会从实时推进、快照和检查点中移除单元。读取句柄在释放后仍可使用原定义折叠未读取事件。后续注册的其他状态版本不会替换旧句柄的定义。注册在异步交接后检查 owner 是否存活;如果 owner 在收到句柄前卸载,注册拒绝。 + +AgentLoop 在发布工厂前只获取一次 inbox 与轮次边界句柄。每个 Agent 在同步构造时收到这些读取句柄。loop fiber 移除全局注册后,清理过程仍可使用它们,因此投影释放无需等待每个 Agent teardown。这保留既有创建与 setup 顺序,无需逐 Agent 注册或增加初始化状态。 + +## Alternatives considered + +**保留注册表级查询并延迟注销。** 这使无关插件的释放顺序依赖每个读取方的最后一次访问。保留读取句柄使清理不再依赖该顺序。 + +**返回同步句柄。** 这保留消费者的立即读取,却将获取 API 固定为同步初始化。等待获取过程使时序显式,同时就绪后的状态读取仍同步。 + +**为每个 Agent 注册相同单元。** 定义已经包含逐 Session 单元。额外引用和异步 Agent 初始化只会引入创建竞态,不提供独立数据所有权。 + +**将函数插件改为类。** Cordis 已等待 `apply` 返回的 Promise。保留命名空间导出可避免发布入口与诊断名称的破坏性变更。 + +## Consequences + +消费者必须迁移获取方式并保留句柄。只要调用方持有已释放句柄,其定义和弱键单元就仍存在;句柄不会恢复全局 wire 参与。注册读取测试覆盖释放后折叠、版本替换、无关单元隔离、重复释放、并发 owner 释放以及获取期间 owner 卸载。Agent 生命周期测试覆盖清理失败、作用域创建竞态与后台子 Agent 停止。 diff --git a/AGENTS.md b/AGENTS.md index 724faca086..f830b6d8ca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -130,7 +130,7 @@ Real-API tests/demos read `DEEPSEEK_API_KEY`, optional `DEEPSEEK_BASE_URL`, and - Packages use `@deepseek-ai/dsh-`; vendor is [rescoped](docs/rescope.md) and `private: true`. Harness packages declare `@deepseek-ai/cordis` in `peerDependencies`/`devDependencies`. Workspace dependency sections use DSH `workspace:*`, vendor/native `workspace:~` ([rules](.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md)). - ESM everywhere (`"type": "module"`). Use package names across packages and `.ts` in local relative imports. Config subprocesses run built `lib/` under plain Node; source regressions use their declared launcher ([testing policy](docs/testing.md#test-subprocess-launch-modes)). The `dsh` CLI source launch runs through tsx's ESM-only hook (`node --import tsx/esm`); modules it reaches must stay ESM (no CJS-only exports) — Node's native TypeScript modes are unavailable across the engines range ([source-launch contract](.agents/notes/implemented/architecture/2026-07-29-dsh-source-launch-tsx-esm.md)). Raw/Web `cordis.yml` bare plugins must appear in their resolver manifest's `dependencies`; `verify-cordis-config` enforces it. -- **Registrations are effects**: every contribution goes through `ctx.effect()` / `ctx.on()`; a registry's `register()` returns the disposer. +- **Registrations are effects**: every contribution goes through `ctx.effect()` / `ctx.on()`; a registry's `register()` returns the disposer, directly or through its documented registration handle. - **Typed events use declaration merging** and merge-extensible maps. Event JSDoc needs `@mode` and payload `@param`. Public service methods document parameters and non-void returns. `SessionEventMap` members are required-on-read unless the envelope carries `ignorable: true`. Bump `SESSION_FORMAT_VERSION` only when existing discriminators cannot prevent unsafe interpretation; same-version changes require safe reader behavior ([review](.agents/notes/implemented/process/2026-10-08-session-reader-compatibility-review.md)). - **Switch on discriminant tags.** Closed unions end in `assertNever`; merge-extensible unions fall through a documented default. - **Waterfall listeners MUST call `next()`** to delegate; returning without it short-circuits the chain ([semantics](docs/cordis-primer.md#cordis-waterfall-semantics)). diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index 8206f3e6dc..5b82618b52 100644 --- a/docs/architecture.i18n.yaml +++ b/docs/architecture.i18n.yaml @@ -27,8 +27,8 @@ en: e6402b28c0ed15fa zh: 6c07be9047b42056 /deepseek-harness-architecture/session-log: - en: a2e35c43438fec6d - zh: e27bdac8e9feff32 + en: 35fcd01052084a07 + zh: e16743dac147cb6e /deepseek-harness-architecture/capability-seams: en: a5bc20c3f2a1b370 zh: 91773a281410b2f4 diff --git a/docs/architecture.md b/docs/architecture.md index 03d88148c7..b821b9fa92 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -123,7 +123,7 @@ Session consumers know only the current logical format. Header-only `stat` and ` **Model-visible means logged.** Every model request must be reconstructable from the log. New model-visible inputs require session events. Plugins changing existing message content register [pure message projections](subsystems/session.md#plugin-owned-message-projections); detached readers supply the same definitions explicitly. Tool changes are capability-independent; [Session tool history](../packages/core/session/README.md) supplies provider declarations. -**Projection seam.** `dsh-session-projection` owns `ctx.sessionProjections`: registered units fold committed events incrementally, host consumers read one typed state with `stateOf()`, and carriers batch cropped client views with `snapshot()`. A host reader either requires this service during activation or fails explicitly when the registry or required key is absent. Contributors may retain `ctx.inject(['sessionProjections'], ...)` registration without silently defaulting a missing host value. The agent loop registers shared `turnBoundary` state for its readers ([decision](../.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.md)). +**Projection seam.** `dsh-session-projection` owns `ctx.sessionProjections`: registered units fold committed events incrementally, and carriers batch cropped client views with `snapshot()`. Host readers require the registry during activation, await registration, and retain a handle for synchronous state reads. Releasing registration removes global participation; retained readers continue folding Session events. The shared `turnBoundary` definition belongs to `dsh-agent` ([registration lifecycle](../packages/session/session-projection/README.md)). [Working directories](subsystems/working-directory.md) supply user context and execution paths without changing original project identity, sandbox write roots, or existing process directories. diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 870fa1c996..403c4f3063 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -127,7 +127,7 @@ Session 消费方只了解当前逻辑格式。仅 header 的 `stat` 与 `list` **模型可见即已记录。** 每个模型请求都必须能从日志重建。新增模型可见输入需要会话事件。修改现有消息内容的插件注册[纯消息投影](subsystems/session.zh.md#plugin-owned-message-projections),独立读取器显式传入相同的处理器。 工具变更不依赖能力;[Session 工具历史](../packages/core/session/README.zh.md)提供提供方声明。 -**投影 seam。** `dsh-session-projection` 提供 `ctx.sessionProjections`:已注册单元增量折叠已提交事件,host 消费方通过 `stateOf()` 读取单个类型化状态,载体通过 `snapshot()` 批量取得裁剪后的客户端视图。host 读取方要么在激活时要求该服务,要么在注册表或必需 key 缺席时明确失败。贡献方可以保留 `ctx.inject(['sessionProjections'], ...)` 注册,但不能为缺失的 host 值静默提供默认值。agent loop 为读取方注册共享的 `turnBoundary` 状态([决策](../.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.zh.md))。 +**投影 seam。** `dsh-session-projection` 提供 `ctx.sessionProjections`:已注册单元增量折叠已提交事件,载体通过 `snapshot()` 批量取得裁剪后的客户端视图。host 读取方在激活时要求注册表,等待注册完成,并保留句柄以同步读取状态。释放注册会移除全局参与关系;已有读取方仍可继续折叠 Session 事件。共享的 `turnBoundary` 定义归 `dsh-agent` 所有([注册生命周期](../packages/session/session-projection/README.zh.md))。 [工作目录](subsystems/working-directory.zh.md) 提供用户上下文和执行路径,不改变原始项目标识、沙箱写入根目录或已有进程目录。 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 00bad2b532..eb2a6a1ea2 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -92,7 +92,7 @@ export interface Config { - `inject`: `agents` · `sessions` · `llm` · `tools` · `systemPrompt` · `sessionProjections` - `refs`: [`AgentOptions`](subsystems/core.md) · [`SessionId`](subsystems/core.md) · `Volatile` (`@deepseek-ai/cosmokit`) -- `source`: [`packages/core/agent-loop/src/index.ts:303`](../packages/core/agent-loop/src/index.ts) +- `source`: [`packages/core/agent-loop/src/index.ts:254`](../packages/core/agent-loop/src/index.ts) ```ts config-catalog /** Agent-loop plugin configuration. */ @@ -220,7 +220,7 @@ export interface Config { ## `@deepseek-ai/dsh-api-session-controller` - `inject`: `agentDefaultModel` · `agents` · `attachments` · `fileUploads` · `fs` · `llm` · `sessions` · `sessionProjections` · `sessionQuery` · `typert` · `workspaceRegistry` -- `source`: [`packages/api/session-controller/src/index.ts:80`](../packages/api/session-controller/src/index.ts) +- `source`: [`packages/api/session-controller/src/index.ts:79`](../packages/api/session-controller/src/index.ts) ```ts config-catalog /** Session Controller deployment policy. */ @@ -950,7 +950,7 @@ export interface StagehandModelConfig { ## `@deepseek-ai/dsh-experimental-claude-code-mods` -- `inject`: `workingDirectory` +- `inject`: `workingDirectory` · `sessionProjections` - `source`: [`packages/experimental/claude-code-mods/src/index.ts:53`](../packages/experimental/claude-code-mods/src/index.ts) ```ts config-catalog @@ -1458,7 +1458,7 @@ export interface HmrConfig extends ChokidarOptions { ## `@deepseek-ai/dsh-hooks-claude-code` - `inject`: `shell` · `sessionProjections` · `workingDirectory` -- `source`: [`packages/experimental/hooks-claude-code/src/index.ts:51`](../packages/experimental/hooks-claude-code/src/index.ts) +- `source`: [`packages/experimental/hooks-claude-code/src/index.ts:53`](../packages/experimental/hooks-claude-code/src/index.ts) ```ts config-catalog /** Plugin config: where the CC hook config lives + substitution roots. */ @@ -1497,7 +1497,7 @@ export interface Config { ## `@deepseek-ai/dsh-hooks-codex` - `inject`: `shell` · `sessionProjections` · `workingDirectory` -- `source`: [`packages/experimental/hooks-codex/src/index.ts:50`](../packages/experimental/hooks-codex/src/index.ts) +- `source`: [`packages/experimental/hooks-codex/src/index.ts:53`](../packages/experimental/hooks-codex/src/index.ts) ```ts config-catalog /** Plugin config: where the Codex hooks.json lives + the model name for payloads. */ @@ -2098,7 +2098,7 @@ export interface ReplayModelConfig { ## `@deepseek-ai/dsh-llm-retry` - `inject`: `agents` · `sessionProjections` -- `source`: [`packages/llm/llm-retry/src/index.ts:25`](../packages/llm/llm-retry/src/index.ts) +- `source`: [`packages/llm/llm-retry/src/index.ts:26`](../packages/llm/llm-retry/src/index.ts) ```ts config-catalog /** This policy executor has no config; providers own `retryPolicy`. */ @@ -2372,7 +2372,7 @@ export interface Config { ## `@deepseek-ai/dsh-plan-mode` - `inject`: `tools` · `systemPrompt` · `sessionProjections` -- `source`: [`packages/plan/plan-mode/src/index.ts:69`](../packages/plan/plan-mode/src/index.ts) +- `source`: [`packages/plan/plan-mode/src/index.ts:70`](../packages/plan/plan-mode/src/index.ts) ```ts config-catalog /** Deployment-owned plan guidance. */ @@ -3259,7 +3259,7 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist' ## `@deepseek-ai/dsh-subagent` -- `inject`: `workingDirectory` +- `inject`: `workingDirectory` · `sessionProjections` - `refs`: `Volatile` (`@deepseek-ai/cordis`) - `source`: [`packages/subagent/subagent/src/index.ts:184`](../packages/subagent/subagent/src/index.ts) @@ -3511,7 +3511,7 @@ export interface Config { ## `@deepseek-ai/dsh-terminal-bash` -- `inject`: `terminals` · `sandboxPolicy` · `sessionProjections` · `subprocess` · `workingDirectory` +- `inject`: `terminals` · `sandboxPolicy` · `subprocess` · `workingDirectory` - `source`: [`packages/terminal/terminal-bash/src/config.ts:10`](../packages/terminal/terminal-bash/src/config.ts) ```ts config-catalog @@ -3572,7 +3572,7 @@ export type ShellDialect = 'bash' | 'pwsh' ## `@deepseek-ai/dsh-time-context` - `inject`: `agents` · `sessionProjections` -- `source`: [`packages/context/time-context/src/index.ts:56`](../packages/context/time-context/src/index.ts) +- `source`: [`packages/context/time-context/src/index.ts:55`](../packages/context/time-context/src/index.ts) ```ts config-catalog /** Request-preparation clock formatting and append scheduling. Invalid values fail plugin load. */ @@ -3591,7 +3591,7 @@ export interface Config { ## `@deepseek-ai/dsh-tmux-context` - `inject`: `agents` · `sessionProjections` -- `source`: [`packages/context/tmux-context/src/index.ts:47`](../packages/context/tmux-context/src/index.ts) +- `source`: [`packages/context/tmux-context/src/index.ts:45`](../packages/context/tmux-context/src/index.ts) ```ts config-catalog /** Per-turn tmux-location scheduling. Invalid values fail plugin load. */ @@ -3753,7 +3753,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-goal` - `inject`: `agents` · `goals` · `tools` · `systemPrompt` · `sessionProjections` -- `source`: [`packages/goal/tool-goal/src/index.ts:32`](../packages/goal/tool-goal/src/index.ts) +- `source`: [`packages/goal/tool-goal/src/index.ts:33`](../packages/goal/tool-goal/src/index.ts) ```ts config-catalog /** Model policy and hard lower bounds for goal-state updates. */ @@ -3921,7 +3921,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-session-query` - `inject`: `tools` · `systemPrompt` · `sessionQuery` · `sessionProjections` -- `source`: [`packages/experimental/tool-session-query/src/index.ts:28`](../packages/experimental/tool-session-query/src/index.ts) +- `source`: [`packages/experimental/tool-session-query/src/index.ts:26`](../packages/experimental/tool-session-query/src/index.ts) ```ts config-catalog /** Deployment-owned search count and timeout bounds. */ @@ -3977,7 +3977,7 @@ export interface Config { - `inject`: `tools` · `subagents` · `systemPrompt` · `sessionProjections` - `refs`: [`AgentOptions`](subsystems/core.md) -- `source`: [`packages/subagent/tool-subagent/src/index.ts:48`](../packages/subagent/tool-subagent/src/index.ts) +- `source`: [`packages/subagent/tool-subagent/src/index.ts:49`](../packages/subagent/tool-subagent/src/index.ts) ```ts config-catalog /** Config: which registered provider this tool delegates to, plus child defaults. */ @@ -4054,7 +4054,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-todo` - `inject`: `tools` · `sessionProjections` -- `source`: [`packages/todo/tool-todo/src/index.ts:29`](../packages/todo/tool-todo/src/index.ts) +- `source`: [`packages/todo/tool-todo/src/index.ts:30`](../packages/todo/tool-todo/src/index.ts) ```ts config-catalog /** Model-facing todo tool configuration. */ diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 3d17bcb06a..aacdae8e58 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -94,7 +94,7 @@ export interface Config { - `inject`: `agents` · `sessions` · `llm` · `tools` · `systemPrompt` · `sessionProjections` - `refs`: [`AgentOptions`](subsystems/core.zh.md) · [`SessionId`](subsystems/core.zh.md) · `Volatile` (`@deepseek-ai/cosmokit`) -- `source`: [`packages/core/agent-loop/src/index.ts:303`](../packages/core/agent-loop/src/index.ts) +- `source`: [`packages/core/agent-loop/src/index.ts:254`](../packages/core/agent-loop/src/index.ts) ```ts config-catalog /** Agent-loop plugin configuration. */ @@ -222,7 +222,7 @@ export interface Config { ## `@deepseek-ai/dsh-api-session-controller` - `inject`: `agentDefaultModel` · `agents` · `attachments` · `fileUploads` · `fs` · `llm` · `sessions` · `sessionProjections` · `sessionQuery` · `typert` · `workspaceRegistry` -- `source`: [`packages/api/session-controller/src/index.ts:80`](../packages/api/session-controller/src/index.ts) +- `source`: [`packages/api/session-controller/src/index.ts:79`](../packages/api/session-controller/src/index.ts) ```ts config-catalog /** Session Controller deployment policy. */ @@ -952,7 +952,7 @@ export interface StagehandModelConfig { ## `@deepseek-ai/dsh-experimental-claude-code-mods` -- `inject`: `workingDirectory` +- `inject`: `workingDirectory` · `sessionProjections` - `source`: [`packages/experimental/claude-code-mods/src/index.ts:53`](../packages/experimental/claude-code-mods/src/index.ts) ```ts config-catalog @@ -1460,7 +1460,7 @@ export interface HmrConfig extends ChokidarOptions { ## `@deepseek-ai/dsh-hooks-claude-code` - `inject`: `shell` · `sessionProjections` · `workingDirectory` -- `source`: [`packages/experimental/hooks-claude-code/src/index.ts:51`](../packages/experimental/hooks-claude-code/src/index.ts) +- `source`: [`packages/experimental/hooks-claude-code/src/index.ts:53`](../packages/experimental/hooks-claude-code/src/index.ts) ```ts config-catalog /** Plugin config: where the CC hook config lives + substitution roots. */ @@ -1499,7 +1499,7 @@ export interface Config { ## `@deepseek-ai/dsh-hooks-codex` - `inject`: `shell` · `sessionProjections` · `workingDirectory` -- `source`: [`packages/experimental/hooks-codex/src/index.ts:50`](../packages/experimental/hooks-codex/src/index.ts) +- `source`: [`packages/experimental/hooks-codex/src/index.ts:53`](../packages/experimental/hooks-codex/src/index.ts) ```ts config-catalog /** Plugin config: where the Codex hooks.json lives + the model name for payloads. */ @@ -2100,7 +2100,7 @@ export interface ReplayModelConfig { ## `@deepseek-ai/dsh-llm-retry` - `inject`: `agents` · `sessionProjections` -- `source`: [`packages/llm/llm-retry/src/index.ts:25`](../packages/llm/llm-retry/src/index.ts) +- `source`: [`packages/llm/llm-retry/src/index.ts:26`](../packages/llm/llm-retry/src/index.ts) ```ts config-catalog /** This policy executor has no config; providers own `retryPolicy`. */ @@ -2374,7 +2374,7 @@ export interface Config { ## `@deepseek-ai/dsh-plan-mode` - `inject`: `tools` · `systemPrompt` · `sessionProjections` -- `source`: [`packages/plan/plan-mode/src/index.ts:69`](../packages/plan/plan-mode/src/index.ts) +- `source`: [`packages/plan/plan-mode/src/index.ts:70`](../packages/plan/plan-mode/src/index.ts) ```ts config-catalog /** Deployment-owned plan guidance. */ @@ -3261,7 +3261,7 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist' ## `@deepseek-ai/dsh-subagent` -- `inject`: `workingDirectory` +- `inject`: `workingDirectory` · `sessionProjections` - `refs`: `Volatile` (`@deepseek-ai/cordis`) - `source`: [`packages/subagent/subagent/src/index.ts:184`](../packages/subagent/subagent/src/index.ts) @@ -3513,7 +3513,7 @@ export interface Config { ## `@deepseek-ai/dsh-terminal-bash` -- `inject`: `terminals` · `sandboxPolicy` · `sessionProjections` · `subprocess` · `workingDirectory` +- `inject`: `terminals` · `sandboxPolicy` · `subprocess` · `workingDirectory` - `source`: [`packages/terminal/terminal-bash/src/config.ts:10`](../packages/terminal/terminal-bash/src/config.ts) ```ts config-catalog @@ -3574,7 +3574,7 @@ export type ShellDialect = 'bash' | 'pwsh' ## `@deepseek-ai/dsh-time-context` - `inject`: `agents` · `sessionProjections` -- `source`: [`packages/context/time-context/src/index.ts:56`](../packages/context/time-context/src/index.ts) +- `source`: [`packages/context/time-context/src/index.ts:55`](../packages/context/time-context/src/index.ts) ```ts config-catalog /** Request-preparation clock formatting and append scheduling. Invalid values fail plugin load. */ @@ -3593,7 +3593,7 @@ export interface Config { ## `@deepseek-ai/dsh-tmux-context` - `inject`: `agents` · `sessionProjections` -- `source`: [`packages/context/tmux-context/src/index.ts:47`](../packages/context/tmux-context/src/index.ts) +- `source`: [`packages/context/tmux-context/src/index.ts:45`](../packages/context/tmux-context/src/index.ts) ```ts config-catalog /** Per-turn tmux-location scheduling. Invalid values fail plugin load. */ @@ -3755,7 +3755,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-goal` - `inject`: `agents` · `goals` · `tools` · `systemPrompt` · `sessionProjections` -- `source`: [`packages/goal/tool-goal/src/index.ts:32`](../packages/goal/tool-goal/src/index.ts) +- `source`: [`packages/goal/tool-goal/src/index.ts:33`](../packages/goal/tool-goal/src/index.ts) ```ts config-catalog /** Model policy and hard lower bounds for goal-state updates. */ @@ -3923,7 +3923,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-session-query` - `inject`: `tools` · `systemPrompt` · `sessionQuery` · `sessionProjections` -- `source`: [`packages/experimental/tool-session-query/src/index.ts:28`](../packages/experimental/tool-session-query/src/index.ts) +- `source`: [`packages/experimental/tool-session-query/src/index.ts:26`](../packages/experimental/tool-session-query/src/index.ts) ```ts config-catalog /** Deployment-owned search count and timeout bounds. */ @@ -3979,7 +3979,7 @@ export interface Config { - `inject`: `tools` · `subagents` · `systemPrompt` · `sessionProjections` - `refs`: [`AgentOptions`](subsystems/core.zh.md) -- `source`: [`packages/subagent/tool-subagent/src/index.ts:48`](../packages/subagent/tool-subagent/src/index.ts) +- `source`: [`packages/subagent/tool-subagent/src/index.ts:49`](../packages/subagent/tool-subagent/src/index.ts) ```ts config-catalog /** Config: which registered provider this tool delegates to, plus child defaults. */ @@ -4056,7 +4056,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-todo` - `inject`: `tools` · `sessionProjections` -- `source`: [`packages/todo/tool-todo/src/index.ts:29`](../packages/todo/tool-todo/src/index.ts) +- `source`: [`packages/todo/tool-todo/src/index.ts:30`](../packages/todo/tool-todo/src/index.ts) ```ts config-catalog /** Model-facing todo tool configuration. */ diff --git a/docs/defensive-patterns/remote-and-api.md b/docs/defensive-patterns/remote-and-api.md index 25281e96fb..df98cdde2a 100644 --- a/docs/defensive-patterns/remote-and-api.md +++ b/docs/defensive-patterns/remote-and-api.md @@ -92,16 +92,18 @@ The owning package decides and registers its unit, and the controller only reads ```ts ignore-check // packages/workspace//src/index.ts: the rule lives with the feature it serves. -ctx.sessionProjections.register({ +export const durablePageProjectionDefinition = { key: 'durablePage', stateSchema: durablePageState, init: () => ({ durable: false }), apply: applyDurablePage, stateVersion: 1, -}) +} +await ctx.sessionProjections.register(durablePageProjectionDefinition) // packages/api/session-controller: generic orchestration reads the projected value. -const { durable } = ctx.sessionProjections.stateOf(session, 'durablePage') +const durablePage = await ctx.sessionProjections.register(durablePageProjectionDefinition) +const { durable } = durablePage.stateOf(session) ``` The wrong version decides the rule inside the shared layer: diff --git a/docs/defensive-patterns/remote-and-api.zh.md b/docs/defensive-patterns/remote-and-api.zh.md index 1a9c291d9c..79691a940a 100644 --- a/docs/defensive-patterns/remote-and-api.zh.md +++ b/docs/defensive-patterns/remote-and-api.zh.md @@ -92,16 +92,18 @@ type SessionRemote = { list(): ...; rename(id: string): ... } // hand-written ```ts ignore-check // packages/workspace//src/index.ts: the rule lives with the feature it serves. -ctx.sessionProjections.register({ +export const durablePageProjectionDefinition = { key: 'durablePage', stateSchema: durablePageState, init: () => ({ durable: false }), apply: applyDurablePage, stateVersion: 1, -}) +} +await ctx.sessionProjections.register(durablePageProjectionDefinition) // packages/api/session-controller: generic orchestration reads the projected value. -const { durable } = ctx.sessionProjections.stateOf(session, 'durablePage') +const durablePage = await ctx.sessionProjections.register(durablePageProjectionDefinition) +const { durable } = durablePage.stateOf(session) ``` 反例把这条规则的决定放进共享层: diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 0f0d3dbdcf..fd1fcb14a8 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -8,7 +8,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | Event | Mode | Declared in | Dispatchers | Listeners | | --- | --- | --- | --- | --- | -| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:249`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | +| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:200`](../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:69`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` | | `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../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:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`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), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index 16b47c76cb..ad79919624 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -10,7 +10,7 @@ | Event | Mode | Declared in | Dispatchers | Listeners | | --- | --- | --- | --- | --- | -| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:249`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | +| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:200`](../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:69`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` | | `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../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:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`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), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index e61666b542..342e85df6d 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -144,8 +144,8 @@ en: 4294f78d26ed985b zh: 4294f78d26ed985b /session-persistence-event-catalog/events/plan/plan-mode-log-only: - en: 862175bfe8692ef7 - zh: a8ab123bf674cf77 + en: 9ae5537eeae238de + zh: 6be761c7fd83fdce /session-persistence-event-catalog/events/request: en: a8df82440ebdf5f8 zh: a8df82440ebdf5f8 @@ -1347,8 +1347,8 @@ en: a74e3dfa51c987af zh: 6c3c87974d7a7260 /session-persistence-event-catalog/resolved-persistence-types/active~2: - en: a3de6a6fe9672c7a - zh: a357481e27b3b81f + en: c861888d6239b700 + zh: a9b2271d3c6b8737 /session-persistence-event-catalog/resolved-persistence-types/agentpreset: en: b8a0f8aac9708056 zh: f5f004bd205b41e1 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 3d8b0b77db..eb4ebe099b 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -700,7 +700,7 @@ Source: [`packages/interaction/permission-presets/src/index.ts:59`](../packages/ 'plan/mode': { active: boolean } ``` -Source: [`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts) +Source: [`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/src/index.ts) ### `request/*` @@ -5556,7 +5556,7 @@ Array of `unknown` (opaque). SHA-256: `20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5` -Sources: [`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts) +Sources: [`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/src/index.ts) | Property | Presence | Type | |---|---|---| diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 76af401f6b..5b1def70af 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -702,7 +702,7 @@ export type SessionEvent = { 'plan/mode': { active: boolean } ``` -来源:[`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts) +来源:[`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/src/index.ts) ### `request/*` @@ -5558,7 +5558,7 @@ SHA-256: `b1664fa89f8eab49a14e7e3168d8bc4da4aa9fd76332f6c4a053d839e5276ae7` SHA-256: `20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5` -来源:[`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts) +来源:[`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/src/index.ts) | 属性 | 存在性 | 类型 | |---|---|---| diff --git a/docs/persistence-schema.json b/docs/persistence-schema.json index 729e58948d..1da9491a5f 100644 --- a/docs/persistence-schema.json +++ b/docs/persistence-schema.json @@ -22518,7 +22518,7 @@ "digest": "20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5", "names": [], "sources": [ - "packages/plan/plan-mode/src/index.ts:52" + "packages/plan/plan-mode/src/index.ts:53" ] }, { diff --git a/docs/subsystems/plan.i18n.yaml b/docs/subsystems/plan.i18n.yaml index e5ce0f0902..a55599bd51 100644 --- a/docs/subsystems/plan.i18n.yaml +++ b/docs/subsystems/plan.i18n.yaml @@ -6,8 +6,8 @@ en: 234f6e4c49714997 zh: add95d36252b572d /plan-mode/logged-state-and-recovery: - en: dd1e8ba31e6fd535 - zh: a22dbccc60c73bc9 + en: 4c83897d7a6d8a67 + zh: 22dcc171a439ed19 /plan-mode/pending-selections-and-the-pre-step-append: en: 8d7e5b7824921ed5 zh: 910fad43593f29f4 diff --git a/docs/subsystems/plan.md b/docs/subsystems/plan.md index beba872f61..56af5f14d2 100644 --- a/docs/subsystems/plan.md +++ b/docs/subsystems/plan.md @@ -8,7 +8,7 @@ Source: [`packages/plan/plan-mode/src/index.ts`](../../packages/plan/plan-mode/s ## Logged state and recovery -`plan/mode` (`{ active: boolean }`) is a log-only, whole-value-replace [session event](session.md): durable and replayable, never in the model transcript. The optionally registered `plan` unit folds committed mode, command settlement, and the mode recorded at the latest request header. `ctx.planMode` reads that state through `stateOf()`; the first dependent access fails if the registry, `plan` key, or `turnBoundary` key is absent. Clients receive only `{ active, pending }`; resume, fork, and compaction recover both from the log. The complete event declaration is in the [persistence log event catalog](../persistence-catalog.md). +`plan/mode` (`{ active: boolean }`) is a log-only, whole-value-replace [session event](session.md): durable and replayable, never in the model transcript. The `plan` unit folds committed mode, command settlement, and the mode recorded at the latest request header. `ctx.planMode` requires the registry and awaits registration of `plan` and `turnBoundary` during activation, then reads through their retained handles. Clients receive only `{ active, pending }`; resume, fork, and compaction recover both from the log. The complete event declaration is in the [persistence log event catalog](../persistence-catalog.md). ## Pending selections and the pre-step append diff --git a/docs/subsystems/plan.zh.md b/docs/subsystems/plan.zh.md index 63463f0105..f0bb4a0d99 100644 --- a/docs/subsystems/plan.zh.md +++ b/docs/subsystems/plan.zh.md @@ -8,7 +8,7 @@ ## 已记录状态与恢复 -`plan/mode`(`{ active: boolean }`)是仅记日志、整值替换的[会话事件](session.zh.md):持久且可回放,绝不进入模型 transcript(文本记录)。可选注册的 `plan` 单元折叠已提交模式、命令结算结果和最近一次请求头记录的模式。`ctx.planMode` 通过 `stateOf()` 读取该状态;注册表、`plan` key 或 `turnBoundary` key 缺失时,第一次依赖它们的访问会失败。客户端只接收 `{ active, pending }`;恢复、fork 与压缩(compaction)都能从日志恢复两者。完整事件声明见[持久化日志事件目录](../persistence-catalog.zh.md)。 +`plan/mode`(`{ active: boolean }`)是仅记日志、整值替换的[会话事件](session.zh.md):持久且可回放,绝不进入模型 transcript(文本记录)。`plan` 单元折叠已提交模式、命令结算结果和最近一次请求头记录的模式。`ctx.planMode` 要求注册表存在,并在激活期间等待 `plan` 和 `turnBoundary` 注册完成,之后通过保留的句柄读取状态。客户端只接收 `{ active, pending }`;恢复、fork 与压缩(compaction)都能从日志恢复两者。完整事件声明见[持久化日志事件目录](../persistence-catalog.zh.md)。 ## 待生效选择与 pre-step 追加 diff --git a/docs/subsystems/session-projection.i18n.yaml b/docs/subsystems/session-projection.i18n.yaml index 8d53d7ed36..5f008d2f02 100644 --- a/docs/subsystems/session-projection.i18n.yaml +++ b/docs/subsystems/session-projection.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/subsystems/session-projection.md /session-projections: - en: a6ab5134ca1771fd - zh: 523039a9d84e3f97 + en: e9330151d3eb579a + zh: 53e47acd1d810da9 /session-projections/the-unit: en: fad897af8b8839ff zh: 3c68c5256973bc39 /session-projections/the-snapshot-and-the-change-feed: - en: a49a57c600c3cbd0 - zh: c92f00b8a9d71bad + en: 630c1041f41f89bf + zh: a3361bdead116cd7 /session-projections/the-registry-ctx-sessionprojections: - en: 50e79c1bc16abb63 - zh: 200ecbf0e4ab19d2 + en: 67fe0f49dee90e25 + zh: c23de3a2803040f0 diff --git a/docs/subsystems/session-projection.md b/docs/subsystems/session-projection.md index 8580b94c4d..5da8044825 100644 --- a/docs/subsystems/session-projection.md +++ b/docs/subsystems/session-projection.md @@ -2,10 +2,27 @@ English | [中文](session-projection.zh.md) -The session-projection seam — a [capability seam](../capability-seams.md) through which domain host plugins serve whole current values of log-derived per-session state to client carriers: the Service Definition and registry ([dsh-session-projection](../../packages/session/session-projection), `ctx.sessionProjections`), domain contributors (each registering one pure unit), and carriers ([dsh-session-controller](../../packages/api/session-controller)'s history tail page and `session/projection` push frame). It is one optional capability, not part of the agent-loop spine. The framework drives, the domain computes: the registry subscribes to `session/event` once and folds every committed event through every unit; domains hold no subscriptions and clients never fold domain events — they receive finished values. Design authority: the [session-projection RFC](../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md); drive/cache/feed contracts: the [package README](../../packages/session/session-projection/README.md). +The session-projection seam — a [capability seam](../capability-seams.md) through which domain host plugins serve whole current values of log-derived per-session state to client carriers: the Service Definition and registry ([dsh-session-projection](../../packages/session/session-projection), `ctx.sessionProjections`), domain contributors (each registering one pure unit), and carriers ([dsh-session-controller](../../packages/api/session-controller)'s history tail page and `session/projection` push frame). Host state readers require the registry during activation. The framework drives, the domain computes: the registry subscribes to `session/event` once and folds every committed event through every unit; domains hold no subscriptions and clients never fold domain events — they receive finished values. Design authority: the [session-projection RFC](../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md); drive/cache/feed contracts: the [package README](../../packages/session/session-projection/README.md). Source: [`packages/session/session-projection/src/index.ts`](../../packages/session/session-projection/src/index.ts) +A `ProjectionHandle` retains its synchronous reader after unregistration. `stateOf(session)` folds newly committed events with the original unit; it does not rejoin global snapshots, checkpoints, or notifications. Registration must still finish before the handle can be used. See the [registration lifecycle](../../packages/session/session-projection/README.md). + +```ts type-equiv +/** An effect-owned registration with a reader retained independently of registration. */ +interface ProjectionHandle { + /** + * Read the current state, including after unregistering, by folding new + * Session events with this handle's original unit. Do not mutate the live value. + * @param session - Session whose committed events are folded. + * @returns the projection state at the Session's current cursor. + */ + stateOf(session: Session): S + /** Unregister from global drives and snapshots without invalidating this reader. */ + dispose(): void | Promise +} +``` + ## The unit `SessionProjectionStateMap` is the merge-extensible table of host fold states, while `SessionProjectionMap` retains the client-visible whole values. A domain contributes one `ProjectionDefinition` per state key; a `wire` block makes that key client-visible, and rendering belongs to the slot system, never this layer: @@ -99,11 +116,11 @@ type ProjectionChangeListener = ( ) => void ``` -`snapshot(session)` is fully synchronous: a carrier reads it in the same tick as its page slice, so `asOfSeq` covers both reads at one sequence number. It returns only client views, and every value passes its unit's `viewSchema` before return. `stateOf(session, key)` reads one live host state without computing unrelated views; callers must not mutate the borrowed reference. A state-reference change computes one cached raw view, and the change feed fires only when that result changes by `Object.is`; an object-valued view must preserve its reference to suppress publication across internal-only state changes. +`snapshot(session)` is fully synchronous: a carrier reads it in the same tick as its page slice, so `asOfSeq` covers both reads at one sequence number. It returns only client views, and every value passes its unit's `viewSchema` before return. `handle.stateOf(session)` reads one live host state without computing unrelated views; callers must not mutate the borrowed reference. A state-reference change computes one cached raw view, and the change feed fires only when that result changes by `Object.is`; an object-valued view must preserve its reference to suppress publication across internal-only state changes. ## The registry: `ctx.sessionProjections` -`SessionProjectionRegistry` ([signatures](#ctxsessionprojections--sessionprojectionregistry)) owns the drive: one `session/event` subscription, eager `apply` over every registered unit, and per-session per-unit watermark cells. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Registration is an effect whose disposer rides the calling fiber: an unloaded domain plugin's key (with its cached cells) disappears from subsequent drives and snapshots, and clients read that as capability absence; a duplicate key with a different `stateVersion` throws, while same-version registrants share one unit and are counted. Domain plugins register under `ctx.inject(['sessionProjections'], …)` so headless assemblies without the registry stay unaffected. +`SessionProjectionRegistry` ([signatures](#ctxsessionprojections--sessionprojectionregistry)) owns the drive: one `session/event` subscription, eager `apply` over every registered unit, and per-session per-unit watermark cells. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Domains await `register(definition)` during activation and retain its handle. Registration belongs to the calling fiber: its last release removes the key from global drives and snapshots, while retained handles remain readable. Same-key, same-version registrants share one unit; incompatible versions reject registration. @@ -204,26 +221,27 @@ Source: [`packages/session/session-projection-cache/src/index.ts`](../../package ### `ctx.sessionProjections` — `SessionProjectionRegistry` -`ctx.sessionProjections`: the projection unit table and its drive. The service subscribes to `session/event` once; every committed event passes every registered unit's `apply` (eager drive). A changed state reference computes the next client view; the change feed is notified only when its raw result changes by `Object.is`. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Registration is an effect (disposer rides the calling fiber): an unloaded domain plugin's key disappears from snapshots and clients read it as capability absence. A host reader either declares `sessionProjections` in its plugin `inject` or fails explicitly when the registry or required key is absent. Contributors may preserve optional registration through `ctx.inject(['sessionProjections'], ...)`. Registrants sharing a key share one unit and are counted: the same tool package mounted in N agent presets registers N times, and the key survives until the last one unloads. +`ctx.sessionProjections`: the projection unit table and its drive. The service subscribes to `session/event` once; every committed event passes every registered unit's `apply` (eager drive). A changed state reference computes the next client view; the change feed is notified only when its raw result changes by `Object.is`. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Consumers declare `sessionProjections` as a required injection, await registration during activation, and retain its ready handle. Registrants sharing a key share one unit and are counted. The last release removes the unit from live drives, checkpoints, and client snapshots; retained handles still fold new events on demand using the original unit. ```ts cordis-catalog /** - * Register one domain's unit. The registration is an effect on the calling - * context's fiber: disposing the fiber (or calling the returned disposer) - * removes the key — and the unit's cached cells — from subsequent drives - * and snapshots. + * Register one domain's unit on the calling context's fiber. Fiber or handle + * disposal releases one reference; the final release removes live participation. + * Retained handles remain readable after release. + * Callers must await registration before publishing consumers; registration may perform asynchronous work. * @param definition - key, state schema, pure unit functions, and stateVersion. - * @returns the exact disposer that unregisters this unit. + * @returns a ready handle with synchronous state reads and the effect disposer. */ -register< K extends keyof SessionProjectionMap, S extends SessionProjectionStateMap[K], >( definition: Omit, 'wire'> & { wire: NonNullable['wire']> }, ): () => void +register< K extends keyof SessionProjectionMap, S extends SessionProjectionStateMap[K], >( definition: Omit, 'wire'> & { wire: NonNullable['wire']> }, ): Promise> /** * Register one host-only unit. Its state is omitted from client snapshots * and always checkpointed like every other unit. + * Callers must await registration before publishing consumers; registration may perform asynchronous work. * @param definition - key, state schema, pure unit functions, and stateVersion. - * @returns the exact disposer that unregisters this unit. + * @returns a ready handle with synchronous state reads and the effect disposer. */ -register< K extends Exclude, S extends SessionProjectionStateMap[K], >( definition: Omit, 'wire'>, ): () => void +register< K extends Exclude, S extends SessionProjectionStateMap[K], >( definition: Omit, 'wire'>, ): Promise> /** * Subscribe to the change feed. The registration is an effect on the @@ -233,16 +251,6 @@ register< K extends Exclude void -/** - * Read one unit's current host state after materializing every registered - * unit at the Session cursor. Unrelated wire views are not produced. - * The returned value is live; callers must not mutate it. - * @param session - the session whose state is read. - * @param key - the registered unit key. - * @returns current state, or `undefined` when the key is not registered. - */ -stateOf( session: Session, key: K, ): SessionProjectionStateMap[K] | undefined - /** * One consistent cut over every registered client-visible unit for one session, read from * the watermark cache (missing cells fold lazily over the in-memory log). diff --git a/docs/subsystems/session-projection.zh.md b/docs/subsystems/session-projection.zh.md index b98d348a36..c2c952b8fc 100644 --- a/docs/subsystems/session-projection.zh.md +++ b/docs/subsystems/session-projection.zh.md @@ -2,10 +2,27 @@ [English](session-projection.md) | 中文 -会话投影 seam 是一项[能力 seam](../capability-seams.zh.md):领域 host 插件经由它向客户端载体供给按会话的日志派生状态的当前全量值;三方分别是 Service Definition 与注册表([dsh-session-projection](../../packages/session/session-projection),`ctx.sessionProjections`)、领域贡献方(每个领域注册一个纯单元)与载体([dsh-session-controller](../../packages/api/session-controller) 的历史尾页与 `session/projection` 推送帧)。它是一项可选能力,不属于 agent loop(智能体循环)主干。框架负责驱动,领域负责计算:注册表只订阅一次 `session/event`,并把每个已提交事件折叠进每个单元;领域不持有任何订阅,客户端也从不折叠领域事件——它们收到的是成品值。设计权威:[session-projection RFC](../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.zh.md);驱动、缓存与变更流约定:[包 README](../../packages/session/session-projection/README.zh.md)。 +会话投影 seam 是一项[能力 seam](../capability-seams.zh.md):领域 host 插件经由它向客户端载体供给按会话的日志派生状态的当前全量值;三方分别是 Service Definition 与注册表([dsh-session-projection](../../packages/session/session-projection),`ctx.sessionProjections`)、领域贡献方(每个领域注册一个纯单元)与载体([dsh-session-controller](../../packages/api/session-controller) 的历史尾页与 `session/projection` 推送帧)。Host 状态读取方在激活期间要求注册表存在。框架负责驱动,领域负责计算:注册表只订阅一次 `session/event`,并把每个已提交事件折叠进每个单元;领域不持有任何订阅,客户端也从不折叠领域事件——它们收到的是成品值。设计权威:[session-projection RFC](../../.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.zh.md);驱动、缓存与变更流约定:[包 README](../../packages/session/session-projection/README.zh.md)。 源码:[`packages/session/session-projection/src/index.ts`](../../packages/session/session-projection/src/index.ts) +`ProjectionHandle` 在注销后仍保留同步 reader。`stateOf(session)` 用原有单元折叠新提交的事件,不会重新加入全局快照、检查点或通知。必须等待注册完成才能使用 handle。参见[注册生命周期](../../packages/session/session-projection/README.zh.md)。 + +```ts type-equiv +/** An effect-owned registration with a reader retained independently of registration. */ +interface ProjectionHandle { + /** + * Read the current state, including after unregistering, by folding new + * Session events with this handle's original unit. Do not mutate the live value. + * @param session - Session whose committed events are folded. + * @returns the projection state at the Session's current cursor. + */ + stateOf(session: Session): S + /** Unregister from global drives and snapshots without invalidating this reader. */ + dispose(): void | Promise +} +``` + ## 投影单元 `SessionProjectionStateMap` 是 host 侧折叠状态的 merge-extensible 类型表,`SessionProjectionMap` 则继续表示客户端可见的全量值。领域为每个状态 key 贡献一个 `ProjectionDefinition`;`wire` 块使该 key 对客户端可见,渲染归 slot 体系管,永远不归本层: @@ -99,11 +116,11 @@ type ProjectionChangeListener = ( ) => void ``` -`snapshot(session)` 完全同步:载体在切出页面切片的同一 tick 内读取它,因此 `asOfSeq` 使两次读取使用同一个序号。它只返回客户端视图,并在返回前通过各单元的 `viewSchema` 校验。`stateOf(session, key)` 可在不计算无关视图的情况下读取一份实时 host 状态;调用方不得修改这一借用引用。state 引用变化时,注册表计算并缓存一次原始 view;只有该结果通过 `Object.is` 判定为变化时才触发变更流,对象 view 若要在仅内部 state 变化时抑制发布就必须保留引用。 +`snapshot(session)` 完全同步:载体在切出页面切片的同一 tick 内读取它,因此 `asOfSeq` 使两次读取使用同一个序号。它只返回客户端视图,并在返回前通过各单元的 `viewSchema` 校验。`handle.stateOf(session)` 可在不计算无关视图的情况下读取一份实时 host 状态;调用方不得修改这一借用引用。state 引用变化时,注册表计算并缓存一次原始 view;只有该结果通过 `Object.is` 判定为变化时才触发变更流,对象 view 若要在仅内部 state 变化时抑制发布就必须保留引用。 ## 注册表:`ctx.sessionProjections` -`SessionProjectionRegistry`([签名](#ctxsessionprojections--sessionprojectionregistry))拥有驱动权:一份 `session/event` 订阅、对每个已注册单元即时调用 `apply`,以及每会话每单元的水位线(watermark)cell。cell 惰性构建:在事件流过之后才注册的单元,或比注册表更早的会话,都在首次触达(事件或读取)时从 `init` 出发在内存日志上折叠。注册是一个 effect,其 disposer 随调用方 fiber 走:领域插件卸载后,其 key(连同缓存的 cell)从后续驱动与快照中消失,客户端将其读作能力缺失;key 以不同 `stateVersion` 重复时直接 throw,同版本注册方则共享一个单元并被计数。领域插件在 `ctx.inject(['sessionProjections'], …)` 下注册,因此不带注册表的 headless 组装完全不受影响。 +`SessionProjectionRegistry`([签名](#ctxsessionprojections--sessionprojectionregistry))拥有驱动逻辑:一个 `session/event` 订阅、对所有已注册单元立即执行的 `apply`,以及按会话、按单元保存的水位线缓存。缓存单元惰性构建:若单元在事件产生后才注册,或会话早于注册表存在,第一次访问(事件或读取)会从 `init` 开始折叠内存日志。领域在激活期间等待 `register(definition)`,并保留返回的 handle(句柄)。注册归调用 fiber 所有:最后一次释放会从全局驱动和快照中移除该 key,已有句柄仍可读取。同 key、同版本的注册者共享一个单元;版本不兼容时注册失败。 @@ -204,26 +221,27 @@ Source: [`packages/session/session-projection-cache/src/index.ts`](../../package ### `ctx.sessionProjections` — `SessionProjectionRegistry` -`ctx.sessionProjections`: the projection unit table and its drive. The service subscribes to `session/event` once; every committed event passes every registered unit's `apply` (eager drive). A changed state reference computes the next client view; the change feed is notified only when its raw result changes by `Object.is`. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Registration is an effect (disposer rides the calling fiber): an unloaded domain plugin's key disappears from snapshots and clients read it as capability absence. A host reader either declares `sessionProjections` in its plugin `inject` or fails explicitly when the registry or required key is absent. Contributors may preserve optional registration through `ctx.inject(['sessionProjections'], ...)`. Registrants sharing a key share one unit and are counted: the same tool package mounted in N agent presets registers N times, and the key survives until the last one unloads. +`ctx.sessionProjections`: the projection unit table and its drive. The service subscribes to `session/event` once; every committed event passes every registered unit's `apply` (eager drive). A changed state reference computes the next client view; the change feed is notified only when its raw result changes by `Object.is`. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Consumers declare `sessionProjections` as a required injection, await registration during activation, and retain its ready handle. Registrants sharing a key share one unit and are counted. The last release removes the unit from live drives, checkpoints, and client snapshots; retained handles still fold new events on demand using the original unit. ```ts cordis-catalog /** - * Register one domain's unit. The registration is an effect on the calling - * context's fiber: disposing the fiber (or calling the returned disposer) - * removes the key — and the unit's cached cells — from subsequent drives - * and snapshots. + * Register one domain's unit on the calling context's fiber. Fiber or handle + * disposal releases one reference; the final release removes live participation. + * Retained handles remain readable after release. + * Callers must await registration before publishing consumers; registration may perform asynchronous work. * @param definition - key, state schema, pure unit functions, and stateVersion. - * @returns the exact disposer that unregisters this unit. + * @returns a ready handle with synchronous state reads and the effect disposer. */ -register< K extends keyof SessionProjectionMap, S extends SessionProjectionStateMap[K], >( definition: Omit, 'wire'> & { wire: NonNullable['wire']> }, ): () => void +register< K extends keyof SessionProjectionMap, S extends SessionProjectionStateMap[K], >( definition: Omit, 'wire'> & { wire: NonNullable['wire']> }, ): Promise> /** * Register one host-only unit. Its state is omitted from client snapshots * and always checkpointed like every other unit. + * Callers must await registration before publishing consumers; registration may perform asynchronous work. * @param definition - key, state schema, pure unit functions, and stateVersion. - * @returns the exact disposer that unregisters this unit. + * @returns a ready handle with synchronous state reads and the effect disposer. */ -register< K extends Exclude, S extends SessionProjectionStateMap[K], >( definition: Omit, 'wire'>, ): () => void +register< K extends Exclude, S extends SessionProjectionStateMap[K], >( definition: Omit, 'wire'>, ): Promise> /** * Subscribe to the change feed. The registration is an effect on the @@ -233,16 +251,6 @@ register< K extends Exclude void -/** - * Read one unit's current host state after materializing every registered - * unit at the Session cursor. Unrelated wire views are not produced. - * The returned value is live; callers must not mutate it. - * @param session - the session whose state is read. - * @param key - the registered unit key. - * @returns current state, or `undefined` when the key is not registered. - */ -stateOf( session: Session, key: K, ): SessionProjectionStateMap[K] | undefined - /** * One consistent cut over every registered client-visible unit for one session, read from * the watermark cache (missing cells fold lazily over the in-memory log). diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.i18n.yaml new file mode 100644 index 0000000000..6d6f45fd15 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/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-alpha.1/claude-code-mods-projections/guide.md +/: + en: 2ca1b8bc4d168c3f + zh: 2257728847687363 +/compose-session-projections-for-claude-code-mods: + en: 58c229d213b33d7b + zh: d6d539f921e59af2 +/compose-session-projections-for-claude-code-mods/change: + en: 3a76b4c79776f847 + zh: e9c7d258a2b76611 +/compose-session-projections-for-claude-code-mods/migration: + en: 6b9ecb1a635ccb44 + zh: 978dad0bdcd69d57 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.md b/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.md new file mode 100644 index 0000000000..3c97f66652 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.md @@ -0,0 +1,17 @@ +--- +kind: upgrade-guide +description: "The Claude Code mods bridge requires the session projection registry at initialization." +--- + +# Compose session projections for Claude Code mods + +English | [中文](guide.zh.md) + +## Change + +`@deepseek-ai/dsh-experimental-claude-code-mods` requires `sessionProjections` to initialize. Custom compositions that omit this service no longer start the bridge. The bridge registers the turn and context-pressure projections before serving mods. + +## Migration + +1. Add `@deepseek-ai/dsh-session-projection` to the `cordis.yml` or profile overlay that composes the bridge, unless it is already composed. +2. Start the profile and confirm the bridge and its mod plugins load successfully. diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.zh.md new file mode 100644 index 0000000000..753d5ca266 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.zh.md @@ -0,0 +1,17 @@ +--- +kind: upgrade-guide +description: "Claude Code 模组桥接在初始化时要求会话投影注册表。" +--- + +# 为 Claude Code 模组组合会话投影 + +[English](guide.md) | 中文 + +## 变更 + +`@deepseek-ai/dsh-experimental-claude-code-mods` 需要 `sessionProjections` 才能初始化。未提供此服务的自定义组合不再启动桥接。桥接在服务模组前注册轮次和上下文压力投影。 + +## 迁移 + +1. 在组合桥接的 `cordis.yml` 或配置覆盖文件中添加 `@deepseek-ai/dsh-session-projection`;已组合此服务时无需重复添加。 +2. 启动配置,确认桥接及其模组插件成功加载。 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.i18n.yaml index c1938c0f53..9407c83aff 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.i18n.yaml +++ b/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.i18n.yaml @@ -9,8 +9,8 @@ en: fcf5f06084f989d9 zh: 5e3e6a05786390dd /migrate-delegation-to-managed-subagent-activations/change: - en: 17add29d9f80674e - zh: 4d4050e99f019cca + en: e56eb4dee1cf246a + zh: f5c5c62c815c140d /migrate-delegation-to-managed-subagent-activations/migration: - en: d4269b2c58484a3d - zh: 0ae39476ecd330e6 + en: e8455cb8d6ecf66b + zh: 1b48e06d6aac7d9f diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.md b/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.md index 5a88ff2ee3..1ef7d07119 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.md +++ b/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.md @@ -11,7 +11,9 @@ English | [中文](guide.zh.md) `@deepseek-ai/dsh-tool-subagent` removes `backgroundMode`, `enableRunInBackground`, and the model argument `run_in_background`. Every delegation now returns `{ kind: 'activation', subagentId }` after accepting the child, and the runtime delivers its final answer in a parent completion notice. Foreground results and `{ kind: 'background', jobId }` results are removed; subagents are no longer collected or stopped through `job_output` or `job_kill`. -The published `@deepseek-ai/dsh-subagent-in-process-driver` package is removed. Local providers contribute creation inputs through `prepareContinuable()`; the subagent service owns execution. Spawn and Fork children can receive later messages, including after settlement when persistence and Session query are available. External providers execute once and do not accept follow-up messages. +`@deepseek-ai/dsh-subagent-in-process-driver` is removed. Local providers contribute creation inputs through `prepareContinuable()`; the subagent service owns execution. Spawn and Fork children can receive later messages, including after settlement when persistence and Session query are available. External providers execute once and do not accept follow-up messages. + +Subagent initialization requires `@deepseek-ai/dsh-session-projection`. ## Migration @@ -24,7 +26,7 @@ The published `@deepseek-ai/dsh-subagent-in-process-driver` package is removed. toolName: subagent ``` - Local tool delegation requires Session persistence. Compose it with the [subagent service and provider](../../../../packages/subagent/subagent/README.md#use-this-package). + Mount `@deepseek-ai/dsh-session-projection`. Local tool delegation requires Session persistence. Compose it with the [subagent service and provider](../../../../packages/subagent/subagent/README.md#use-this-package). 2. Remove `run_in_background` from tool calls and update result consumers to read `subagentId`. Collect the completion notice instead of foreground output or a Job result. Use `interrupt_agent` to stop the current execution; use `send_message` only for local children when those control tools are composed. diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.zh.md index aa83610496..1f2e80d7cf 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.zh.md +++ b/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.zh.md @@ -11,7 +11,9 @@ description: "受管理的子 agent activation 替代前台与 Job 后台委派 `@deepseek-ai/dsh-tool-subagent` 移除了 `backgroundMode`、`enableRunInBackground` 和模型参数 `run_in_background`。每次委派现在都会在接纳子 agent(智能体)后返回 `{ kind: 'activation', subagentId }`,运行时通过父级完成通知交付最终答案。前台结果和 `{ kind: 'background', jobId }` 结果已移除;子 agent 不再通过 `job_output` 或 `job_kill` 收集结果或停止。 -已发布的 `@deepseek-ai/dsh-subagent-in-process-driver` 包被移除。本地提供方通过 `prepareContinuable()` 提供创建输入,由子 agent 服务拥有执行过程。Spawn 和 Fork 子级可以接收后续消息;组合了持久化和 Session 查询服务时,结算后也能接收。外部提供方只执行一次,不接受后续消息。 +`@deepseek-ai/dsh-subagent-in-process-driver` 被移除。本地提供方通过 `prepareContinuable()` 提供创建输入,由子 agent 服务拥有执行过程。Spawn 和 Fork 子级可以接收后续消息;组合了持久化和 Session 查询服务时,结算后也能接收。外部提供方只执行一次,不接受后续消息。 + +子 agent 初始化需要 `@deepseek-ai/dsh-session-projection`。 ## 迁移 @@ -24,7 +26,7 @@ description: "受管理的子 agent activation 替代前台与 Job 后台委派 toolName: subagent ``` - 本地工具委派需要 Session 持久化。将其与[子 agent 服务及提供方](../../../../packages/subagent/subagent/README.zh.md#use-this-package)一起组合。 + 挂载 `@deepseek-ai/dsh-session-projection`。本地工具委派需要 Session 持久化。将其与[子 agent 服务及提供方](../../../../packages/subagent/subagent/README.zh.md#use-this-package)一起组合。 2. 从工具调用中删除 `run_in_background`,并将结果消费方改为读取 `subagentId`。收集完成通知,替代前台输出或 Job 结果。组合了相应控制工具时,使用 `interrupt_agent` 停止当前执行;`send_message` 仅用于本地子级。 diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.i18n.yaml new file mode 100644 index 0000000000..7d864c5be2 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/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-alpha.2/session-projection-handles/guide.md +/: + en: b3f54a7eadb3398a + zh: 6815b996b2cebc6f +/session-projection-registration-handles: + en: 727968e582c6eee6 + zh: e01dc83384747012 +/session-projection-registration-handles/change: + en: 06c3d4ee9031fb7e + zh: cf2b664bfdafe837 +/session-projection-registration-handles/migration: + en: 0619d4d16272fa0c + zh: c3b0bb2c9d3ecb57 diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.md b/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.md new file mode 100644 index 0000000000..f746d9170d --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.md @@ -0,0 +1,21 @@ +--- +kind: upgrade-guide +description: "Session projection registration is asynchronous and returns a retained reader instead of a disposer." +--- +# Session projection registration handles + +English | [中文](guide.zh.md) + +## Change + +`@deepseek-ai/dsh-session-projection` changes `SessionProjectionRegistry.register(definition)` from a synchronous disposer to `Promise>`. Registry-level `stateOf(session, key)` is removed. Registration errors now reject the returned promise. After the last registration is disposed, the unit leaves snapshots, checkpoints, and live notifications, but retained handles can still fold Session events on demand. + +`@deepseek-ai/dsh-subagent` now requires `sessionProjections`. `@deepseek-ai/dsh-api-session-controller` removes `installModelSelectionProjection`; consumers register `modelSelectionProjection` instead. The controller also registers `agentPreset`, whose null value means the Session has no recorded preset; that key does not establish that a preset registry is available. `turnBoundaryProjectionDefinition` is exported by `@deepseek-ai/dsh-agent`; its `agent-loop` re-export remains available. + +## Migration + +1. Declare `sessionProjections` as a required plugin injection and mount `SessionProjectionRegistry` before consumers, including `SubagentRuntime` in custom compositions. +2. In an asynchronous function-plugin `apply()` or service `[Service.init]()`, await `ctx.sessionProjections.register(definition)` and retain the returned handle before publishing dependent operations. Keep existing function-plugin namespace exports. +3. Replace `registry.stateOf(session, key)` with `handle.stateOf(session)`. Import the owning package's projection definition when a reader needs to acquire a handle itself. Replace stored unregister callbacks with `handle.dispose`; fiber disposal still releases registrations automatically. Await registration when handling initialization failures. +4. Replace `installModelSelectionProjection(ctx)` with `await ctx.sessionProjections.register(modelSelectionProjection)`, importing the definition from `@deepseek-ai/dsh-api-session-controller/src/model-selection-projection.ts`. +5. Typecheck custom plugins, then verify activation, a Session state read, and owner disposal. A released reader remains usable, while the released unit disappears from new snapshots once its final registration is gone. diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.zh.md new file mode 100644 index 0000000000..d6a19c5631 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.zh.md @@ -0,0 +1,21 @@ +--- +kind: upgrade-guide +description: "Session 投影注册改为异步,并返回保留读取能力的句柄而不是释放函数。" +--- +# Session 投影注册句柄 + +[English](guide.md) | 中文 + +## 变更 + +`@deepseek-ai/dsh-session-projection` 的 `SessionProjectionRegistry.register(definition)` 从同步释放函数改为返回 `Promise>`。注册表级 `stateOf(session, key)` 已移除,注册错误现在使返回的 Promise 拒绝。最后一次注册释放后,该投影单元不再参与快照、检查点和实时通知,但保留的句柄仍可按需折叠 Session 事件。 + +`@deepseek-ai/dsh-subagent` 现在必需注入 `sessionProjections`。`@deepseek-ai/dsh-api-session-controller` 移除了 `installModelSelectionProjection`;消费者改为注册 `modelSelectionProjection`。控制器也注册 `agentPreset`,其 null 值表示 Session 未记录预设;该键不代表预设注册表可用。`turnBoundaryProjectionDefinition` 由 `@deepseek-ai/dsh-agent` 导出;`agent-loop` 的再导出仍然可用。 + +## 迁移 + +1. 将 `sessionProjections` 声明为插件的必需注入,并在消费者之前挂载 `SessionProjectionRegistry`,包括自定义组合中的 `SubagentRuntime`。 +2. 在函数插件的异步 `apply()` 或服务的 `[Service.init]()` 中,等待 `ctx.sessionProjections.register(definition)`,在发布依赖操作前保留返回的句柄。保留既有的函数插件命名空间导出。 +3. 将 `registry.stateOf(session, key)` 替换为 `handle.stateOf(session)`。读取方需要自行取得句柄时,导入归属包的投影定义。将保存的注销回调替换为 `handle.dispose`;fiber 释放仍会自动解除注册。处理初始化错误时须等待注册完成。 +4. 将 `installModelSelectionProjection(ctx)` 替换为 `await ctx.sessionProjections.register(modelSelectionProjection)`,从 `@deepseek-ai/dsh-api-session-controller/src/model-selection-projection.ts` 导入该定义。 +5. 对自定义插件执行类型检查,再验证激活、Session 状态读取与 owner 释放。已释放的读取句柄仍可使用,而投影单元在最后一次注册解除后会从新快照中消失。 diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 7da873bea6..d2ded420f8 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -2204,19 +2204,19 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ { key: 'sessionProjections', summary: '`ctx.sessionProjections`: the projection unit table and its drive.', - description: '`ctx.sessionProjections`: the projection unit table and its drive. The service subscribes to `session/event` once; every committed event passes every registered unit\'s `apply` (eager drive). A changed state reference computes the next client view; the change feed is notified only when its raw result changes by `Object.is`. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Registration is an effect (disposer rides the calling fiber): an unloaded domain plugin\'s key disappears from snapshots and clients read it as capability absence. A host reader either declares `sessionProjections` in its plugin `inject` or fails explicitly when the registry or required key is absent. Contributors may preserve optional registration through `ctx.inject([\'sessionProjections\'], ...)`. Registrants sharing a key share one unit and are counted: the same tool package mounted in N agent presets registers N times, and the key survives until the last one unloads.', + description: '`ctx.sessionProjections`: the projection unit table and its drive. The service subscribes to `session/event` once; every committed event passes every registered unit\'s `apply` (eager drive). A changed state reference computes the next client view; the change feed is notified only when its raw result changes by `Object.is`. Cells build lazily — a unit registered after events flowed, or a session older than the registry, folds `init` over the in-memory log on first touch (event or read). Consumers declare `sessionProjections` as a required injection, await registration during activation, and retain its ready handle. Registrants sharing a key share one unit and are counted. The last release removes the unit from live drives, checkpoints, and client snapshots; retained handles still fold new events on demand using the original unit.', methods: [ { - signature: 'register< K extends keyof SessionProjectionMap, S extends SessionProjectionStateMap[K], >( definition: Omit, \'wire\'> & { wire: NonNullable[\'wire\']> }, ): () => void', - description: 'Register one domain\'s unit. The registration is an effect on the calling context\'s fiber: disposing the fiber (or calling the returned disposer) removes the key — and the unit\'s cached cells — from subsequent drives and snapshots.', + signature: 'register< K extends keyof SessionProjectionMap, S extends SessionProjectionStateMap[K], >( definition: Omit, \'wire\'> & { wire: NonNullable[\'wire\']> }, ): Promise>', + description: 'Register one domain\'s unit on the calling context\'s fiber. Fiber or handle disposal releases one reference; the final release removes live participation. Retained handles remain readable after release. Callers must await registration before publishing consumers; registration may perform asynchronous work.', parameters: [{ name: 'definition', description: 'key, state schema, pure unit functions, and stateVersion.' }], - returns: 'the exact disposer that unregisters this unit.', + returns: 'a ready handle with synchronous state reads and the effect disposer.', }, { - signature: 'register< K extends Exclude, S extends SessionProjectionStateMap[K], >( definition: Omit, \'wire\'>, ): () => void', - description: 'Register one host-only unit. Its state is omitted from client snapshots and always checkpointed like every other unit.', + signature: 'register< K extends Exclude, S extends SessionProjectionStateMap[K], >( definition: Omit, \'wire\'>, ): Promise>', + description: 'Register one host-only unit. Its state is omitted from client snapshots and always checkpointed like every other unit. Callers must await registration before publishing consumers; registration may perform asynchronous work.', parameters: [{ name: 'definition', description: 'key, state schema, pure unit functions, and stateVersion.' }], - returns: 'the exact disposer that unregisters this unit.', + returns: 'a ready handle with synchronous state reads and the effect disposer.', }, { signature: 'onChanged(listener: ProjectionChangeListener): () => void', @@ -2224,12 +2224,6 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ parameters: [{ name: 'listener', description: 'called once per client-visible unit whose raw view changed by `Object.is`, per committed event.' }], returns: 'the exact disposer that unsubscribes.', }, - { - signature: 'stateOf( session: Session, key: K, ): SessionProjectionStateMap[K] | undefined', - description: 'Read one unit\'s current host state after materializing every registered unit at the Session cursor. Unrelated wire views are not produced. The returned value is live; callers must not mutate it.', - parameters: [{ name: 'session', description: 'the session whose state is read.' }, { name: 'key', description: 'the registered unit key.' }], - returns: 'current state, or `undefined` when the key is not registered.', - }, { signature: 'snapshot( session: Session, keys?: readonly Extract[], ): ProjectionSnapshot', description: 'One consistent cut over every registered client-visible unit for one session, read from the watermark cache (missing cells fold lazily over the in-memory log). Fully synchronous — every value and `asOfSeq` reflect the same log position. Each value passes its unit\'s `viewSchema` before leaving.', @@ -6336,6 +6330,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'ProjectionDefinition', declaration: 'export interface ProjectionDefinition {\n key: K;\n stateSchema: ZodType;\n init(header: SessionHeader, inheritedEventCount: SessionLogOffset): NoInfer;\n apply(state: NoInfer, event: SessionEvent): NoInfer;\n wire?: K extends keyof SessionProjectionMap ? {\n viewSchema: ZodType;\n view(state: NoInfer): SessionProjectionMap[K];\n } : never;\n stateVersion: number;\n}', }, + { + name: 'ProjectionHandle', + declaration: 'export interface ProjectionHandle {\n stateOf(session: Session): S;\n dispose(): void | Promise;\n}', + }, { name: 'ProjectionSnapshot', declaration: 'export interface ProjectionSnapshot {\n asOfSeq: SessionSeqCursor;\n values: Partial;\n}', diff --git a/scripts/gen-config-catalog.spec.ts b/scripts/gen-config-catalog.spec.ts index 7248df7b98..4aed5f9782 100644 --- a/scripts/gen-config-catalog.spec.ts +++ b/scripts/gen-config-catalog.spec.ts @@ -97,6 +97,31 @@ export function apply(ctx: unknown, config: Config): void {} expect(new Set(provider?.schemaKeys)).toEqual(new Set(['mode', 'headless', 'endpoint'])) }) + it.each([false, true])('checks a class Config imported from a sibling module (invalid field: %s)', (invalid) => { + const { root, write } = fixture() + write('packages/test/provider/src/config.ts', ` +import Schema from '@deepseek-ai/schemastery' +export interface Config { + /** Maximum result count. */ + limit: number +} +export const Config = Schema.object({ ${invalid ? 'hidden' : 'limit'}: Schema.number() }) +`) + write('packages/test/provider/src/index.ts', ` +import { Config } from './config.ts' +export default class Provider { + static Config = Config + constructor(ctx: unknown, config: Config) {} +} +`) + if (invalid) { + expect(() => collectConfigCatalog(root)).toThrow("schema validates key 'hidden' but config type 'Config' declares no such member") + } else { + const provider = collectConfigCatalog(root).find(entry => entry.pkg === '@test/provider') + expect(provider?.schemaKeys).toEqual(['limit']) + } + }) + it('rejects type-only imports used as runtime schemas', () => { const { root, write } = fixture() write('packages/test/provider/src/index.ts', ` diff --git a/scripts/gen-config-catalog.ts b/scripts/gen-config-catalog.ts index 60a31c9fdb..df0ac078d4 100644 --- a/scripts/gen-config-catalog.ts +++ b/scripts/gen-config-catalog.ts @@ -504,7 +504,7 @@ function unwrapExpr(expr: ts.Expression): ts.Expression { * packages whose schemas an intersect composes. A key path is the top-level * key or a nested path through object/array compositions (`agents[].id`). * Handles object/union calls, chained refinements, named const schema imports - * through public workspace source paths, and `z.intersect([X.Config, …])`; errors on + * through package-relative or public workspace source paths, and `z.intersect([X.Config, …])`; errors on * anything else, so a schema the walk cannot see fails the gate instead of * silently thinning it. Nested values that are neither `object` nor `array` * compositions (primitives, unions, dynamic-key dicts) contribute no paths. diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index 89b1432bc4..0113d16160 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -805,6 +805,7 @@ export const LINK_MAP: Readonly> = { WorkflowRunInfo: 'workflow.md', WorkflowStartRequest: 'workflow.md', ProjectionDefinition: 'session-projection.md', + ProjectionHandle: 'session-projection.md', SessionProjectionMap: 'session-projection.md', SessionProjectionStateMap: 'session-projection.md', ProjectionChangeListener: 'session-projection.md', diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index 37f21e08c1..4218ec3c8c 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -2116,6 +2116,11 @@ "symbol": "PresetSpec", "source": "packages/interaction/permission-presets/src/index.ts" }, + { + "doc": "docs/subsystems/session-projection.md", + "symbol": "ProjectionHandle", + "source": "packages/session/session-projection/src/index.ts" + }, { "doc": "docs/subsystems/session-projection.md", "symbol": "ProjectionDefinition", From 99f36601205c48817eb86fa2b684d7bd4a076c31 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Mon, 12 Oct 2026 13:08:14 +0800 Subject: [PATCH 031/369] refactor(hooks-codex): simplify hook payload construction --- .../experimental/hooks-codex/src/index.ts | 64 +++++++++++-------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/packages/experimental/hooks-codex/src/index.ts b/packages/experimental/hooks-codex/src/index.ts index 6159a90dda..356e64393d 100644 --- a/packages/experimental/hooks-codex/src/index.ts +++ b/packages/experimental/hooks-codex/src/index.ts @@ -28,7 +28,7 @@ declare module '@deepseek-ai/dsh-llm' { import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm' import type { UserMessage } from '@deepseek-ai/dsh-session' -import type { PostToolDecision, PreToolDecision, ToolExecution, ToolExecutionResult } from '@deepseek-ai/dsh-tools' +import type { PostToolDecision, PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools' import { appendHookInvoked, appendHookResult, @@ -113,6 +113,10 @@ export async function apply(ctx: Context, config: Config): Promise { const turnBoundary = await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) const model = config.model ?? '' + function lastTurn(agent: Agent | undefined): number { + return agent === undefined ? 0 : turnBoundary.stateOf(agent.session).lastTurn + } + // SessionStart is the one emit-shaped (detached) point Codex has: track its // run chains so disposal aborts a still-running hook process and drains the // continuation (docs/defensive-patterns/general.md: dispose must reach quiescence). @@ -235,7 +239,10 @@ export async function apply(ctx: Context, config: Config): Promise { // PreToolUse → PreToolDecision. Codex blocks only (no allow/ask honored). ctx.on('tools/pre-execute', async (exec, next): Promise => { const turn = lastTurn(exec.agent) - const merged = await runPoint('PreToolUse', exec.name, preToolPayload(exec, model), { ...exec.agent ? { agent: exec.agent } : {}, turn, signal: exec.signal }) + const payload = toolPayload(exec, 'PreToolUse', model, turn) + const merged = await runPoint('PreToolUse', exec.name, payload, { + ...exec.agent ? { agent: exec.agent } : {}, turn, signal: exec.signal, + }) if (merged.decision === 'deny') return { kind: 'deny', reason: merged.reason ?? 'blocked by PreToolUse hook' } return next() }) @@ -243,8 +250,14 @@ export async function apply(ctx: Context, config: Config): Promise { // PostToolUse → PostToolDecision (block with feedback, or attach context). ctx.on('tools/post-execute', async (exec, result, next): Promise => { const turn = lastTurn(exec.agent) + const payload = { + ...toolPayload(exec, 'PostToolUse', model, turn), + tool_response: blocksToText(result.content), + } /* jscpd:ignore-start */ - const merged = await runPoint('PostToolUse', exec.name, postToolPayload(exec, result, model), { ...exec.agent ? { agent: exec.agent } : {}, turn, signal: exec.signal }) + const merged = await runPoint('PostToolUse', exec.name, payload, { + ...exec.agent ? { agent: exec.agent } : {}, turn, signal: exec.signal, + }) const context = contextFrom(merged) if (merged.decision === 'deny') { return { kind: 'block', feedback: [{ type: 'text', text: merged.reason ?? 'blocked by PostToolUse hook' }], ...context ? { additionalContexts: [context] } : {} } @@ -268,7 +281,13 @@ export async function apply(ctx: Context, config: Config): Promise { // avoid continuing the same turn indefinitely. It is always false here, so an // unconditionally blocking hook force-continues every step until it self-limits. ctx.on('agent/turn-stopping', async ({ agent, turn, signal }): Promise => { - const merged = await runPoint('Stop', '', { ...turnBase(agent, 'Stop', model), stop_hook_active: false, last_assistant_message: null }, { agent, turn, signal }) + const payload = { + ...base(agent, 'Stop', model), + turn_id: String(lastTurn(agent)), + stop_hook_active: false, + last_assistant_message: null, + } + const merged = await runPoint('Stop', '', payload, { agent, turn, signal }) if (merged.decision === 'deny') { // A blocking Stop hook forces continuation; a block with no reason (exit 2, // empty stderr) still forces it — fall back to a generic steering line @@ -277,27 +296,6 @@ export async function apply(ctx: Context, config: Config): Promise { agent.steer(createUserMessage({ content: [{ type: 'text', text }], source: CONTEXT_SOURCE })) } }) - - function lastTurn(agent: Agent | undefined): number { - if (!agent) return 0 - return turnBoundary.stateOf(agent.session).lastTurn - } - - function turnBase(agent: Agent | undefined, event: string, model: string): Record { - return { ...base(agent, event, model), turn_id: String(lastTurn(agent)) } - } - - function preToolPayload(exec: ToolExecution, model: string): Record { - // `tool_name` is the REAL tool name (matching the `exec.name` matcher subject); - // a hardcoded constant would disagree with what the matcher tests and make a - // config's tool matcher never fire. `tool_input` keeps Codex's `{ command }` - // shape (its shell payload), derived from the call's `command` arg when present. - return { ...turnBase(exec.agent, 'PreToolUse', model), tool_name: exec.name, tool_input: { command: commandOf(exec.arguments) }, tool_use_id: exec.callId } - } - - function postToolPayload(exec: ToolExecution, result: ToolExecutionResult, model: string): Record { - return { ...turnBase(exec.agent, 'PostToolUse', model), tool_name: exec.name, tool_input: { command: commandOf(exec.arguments) }, tool_use_id: exec.callId, tool_response: blocksToText(result.content) } - } } // --- Codex DIALECT payloads: snake_case, model on every event, turn_id on @@ -325,6 +323,22 @@ function base(agent: Agent | undefined, event: string, model: string): Record { + return { + ...base(exec.agent, event, model), + turn_id: String(turn), + tool_name: exec.name, + tool_input: { command: commandOf(exec.arguments) }, + tool_use_id: exec.callId, + } +} + /** Extract a `command` string from a tool call's parsed arguments, else ''. */ function commandOf(args: unknown): string { if (typeof args === 'object' && args !== null && 'command' in args) { From 5b401b27a5a7ed2596e6294bbd64603d4d57669d Mon Sep 17 00:00:00 2001 From: _Kerman Date: Mon, 12 Oct 2026 13:08:18 +0800 Subject: [PATCH 032/369] refactor(tool-session-query): pass projection handles to operations --- docs/config-catalog.md | 2 +- docs/config-catalog.zh.md | 2 +- .../tool-session-query/src/index.ts | 16 +- .../tool-session-query/src/operations.ts | 420 ++++++++---------- .../src/workspace-access.ts | 19 + 5 files changed, 215 insertions(+), 244 deletions(-) diff --git a/docs/config-catalog.md b/docs/config-catalog.md index eb2a6a1ea2..8460cc7395 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -3921,7 +3921,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-session-query` - `inject`: `tools` · `systemPrompt` · `sessionQuery` · `sessionProjections` -- `source`: [`packages/experimental/tool-session-query/src/index.ts:26`](../packages/experimental/tool-session-query/src/index.ts) +- `source`: [`packages/experimental/tool-session-query/src/index.ts:27`](../packages/experimental/tool-session-query/src/index.ts) ```ts config-catalog /** Deployment-owned search count and timeout bounds. */ diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index aacdae8e58..ffd0313631 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -3923,7 +3923,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-session-query` - `inject`: `tools` · `systemPrompt` · `sessionQuery` · `sessionProjections` -- `source`: [`packages/experimental/tool-session-query/src/index.ts:26`](../packages/experimental/tool-session-query/src/index.ts) +- `source`: [`packages/experimental/tool-session-query/src/index.ts:27`](../packages/experimental/tool-session-query/src/index.ts) ```ts config-catalog /** Deployment-owned search count and timeout bounds. */ diff --git a/packages/experimental/tool-session-query/src/index.ts b/packages/experimental/tool-session-query/src/index.ts index 8bbf6c3bff..2337b43402 100644 --- a/packages/experimental/tool-session-query/src/index.ts +++ b/packages/experimental/tool-session-query/src/index.ts @@ -5,11 +5,12 @@ */ import type { Context } from '@deepseek-ai/cordis' +import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent' import z from '@deepseek-ai/schemastery' import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout' import { defineTool } from '@deepseek-ai/dsh-tools' import { toolInput } from './input.ts' -import { SessionQueryOperations } from './operations.ts' +import { operations } from './operations.ts' import { presentation } from './presentation.ts' export const name = 'tool-session-query' @@ -58,8 +59,7 @@ const PROMPT_TEXT = * @returns when the tools and their projection reader are ready. */ export async function apply(ctx: Context, config: Config): Promise { - const operations = new SessionQueryOperations(ctx) - await operations.initialize() + const turnBoundary = await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) const resolved = resolveConfig(config) ctx.systemPrompt.section({ name: 'tool:session-query', @@ -73,7 +73,7 @@ export async function apply(ctx: Context, config: Config): Promise { parameters: toolInput.sessionSearchParameters, output: TEXT_OUTPUT, timeoutMs: resolved.searchTimeoutMs, - execute: (args, exec) => operations.executeSessionSearch(args, exec, resolved.maxSearchResults), + execute: (args, exec) => operations.executeSessionSearch(ctx, turnBoundary, args, exec, resolved.maxSearchResults), presentCall: presentation.presentSessionSearchCall, })) @@ -83,7 +83,7 @@ export async function apply(ctx: Context, config: Config): Promise { parameters: toolInput.eventSearchParameters, output: TEXT_OUTPUT, timeoutMs: resolved.searchTimeoutMs, - execute: (args, exec) => operations.executeEventSearch(args, exec, resolved.maxSearchResults), + execute: (args, exec) => operations.executeEventSearch(ctx, turnBoundary, args, exec, resolved.maxSearchResults), presentCall: presentation.presentEventSearchCall, })) @@ -93,7 +93,7 @@ export async function apply(ctx: Context, config: Config): Promise { parameters: toolInput.targetSessionParameter, output: TEXT_OUTPUT, isConcurrencySafe: () => true, - execute: (args, exec) => operations.executeSessionTrace(args, exec), + execute: (args, exec) => operations.executeSessionTrace(ctx, turnBoundary, args, exec), presentCall: presentation.presentSessionTraceCall, })) @@ -106,7 +106,7 @@ export async function apply(ctx: Context, config: Config): Promise { }, output: TEXT_OUTPUT, isConcurrencySafe: () => true, - execute: (args, exec) => operations.executeEventTrace(args, exec), + execute: (args, exec) => operations.executeEventTrace(ctx, turnBoundary, args, exec), presentCall: args => presentation.presentEventTargetCall('Trace event', args), })) @@ -121,7 +121,7 @@ export async function apply(ctx: Context, config: Config): Promise { }, output: TEXT_OUTPUT, isConcurrencySafe: () => true, - execute: (args, exec) => operations.executeEventRead(args, exec), + execute: (args, exec) => operations.executeEventRead(ctx, turnBoundary, args, exec), presentCall: args => presentation.presentEventTargetCall('Read event', args), })) } diff --git a/packages/experimental/tool-session-query/src/operations.ts b/packages/experimental/tool-session-query/src/operations.ts index 4c05480221..2727447537 100644 --- a/packages/experimental/tool-session-query/src/operations.ts +++ b/packages/experimental/tool-session-query/src/operations.ts @@ -4,9 +4,9 @@ * @module @deepseek-ai/dsh-tool-session-query/operations */ -import { turnBoundaryProjectionDefinition, type TurnBoundaryProjection } from '@deepseek-ai/dsh-agent' -import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection' import type { Context } from '@deepseek-ai/cordis' +import type { TurnBoundaryProjection } from '@deepseek-ai/dsh-agent' +import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection' import { HarnessError } from '@deepseek-ai/dsh-llm' import { SessionSeq } from '@deepseek-ai/dsh-session' import type { SessionId } from '@deepseek-ai/dsh-session' @@ -54,249 +54,192 @@ interface SearchCollection { readonly capped: boolean } -/** Session-history tool operations over the caller’s current turn state. */ -export class SessionQueryOperations { - private turnBoundary!: ProjectionHandle - - constructor(private readonly ctx: Context) {} - - /** - * Acquire projection readers before exposing dependent operations. - * @returns when all required registrations are ready. - */ - async initialize(): Promise { - this.turnBoundary = await this.ctx.sessionProjections.register(turnBoundaryProjectionDefinition) +async function executeSessionSearch( + ctx: Context, + turnBoundary: ProjectionHandle, + args: SessionSearchArgs, + exec: ToolRunContext, + maxResults: number, +): Promise { + const caller = workspaceAccess.callerOf(exec, turnBoundary) + const cwd = caller.header.cwd + if (cwd === undefined) { + throw new HarnessError( + 'cross-session search is unavailable because the caller session has no workspace', + 'SESSION_QUERY_TOOL_UNAUTHORIZED', + ) } - - private callerOf(exec: ToolRunContext) { - const agent = exec.agent - if (agent === undefined) { - throw new HarnessError( - 'session query tools require an agent-bound caller', - 'SESSION_QUERY_TOOL_MISSING_AGENT', - ) - } - return { - id: agent.session.id, - header: agent.session.header, - boundary: this.turnBoundary.stateOf(agent.session), - } + const query = toolInput.normalizeQuery(args.query) + const sessionFilters = toolInput.buildSessionFilters(args) + const eventFilters = toolInput.buildEventFilters({ + seqFrom: args.event_seq_from, + seqTo: args.event_seq_to, + timeFrom: args.event_time_from, + timeTo: args.event_time_to, + eventTypes: args.event_types, + surfaces: args.event_surfaces, + }) + const requestedParentIds = toolInput.materializeParentSessionIds(args.parent_session_ids) + if (requestedParentIds !== undefined || args.include_root_sessions === true) { + const authorizedParentIds = requestedParentIds === undefined + ? new Set() + : await workspaceAccess.authorizeSessionIds(ctx, caller, requestedParentIds, exec.signal) + const parentValues: Array = requestedParentIds + ?.filter(id => authorizedParentIds.has(id)) ?? [] + if (args.include_root_sessions === true) parentValues.push(null) + if (parentValues.length === 0) return presentation.formatEmptySessionSearch() + sessionFilters.push({ kind: 'parent', values: parentValues }) } + sessionFilters.push({ kind: 'cwd', values: [cwd] }) + const collected = await collectPages( + maxResults, + exec.signal, + cursor => serviceBoundary.call(ctx, exec.signal, 'session search', () => + ctx.sessionQuery.searchSessions({ + query, + sessionFilters, + eventFilters, + ...cursor === undefined ? {} : { cursor }, + }, { signal: exec.signal })), + hit => hit.header.id !== caller.id && workspaceAccess.recordAuthorized(hit, caller), + ) - /** - * Search other Sessions within the caller’s authorized workspace. - * @param args - Session and event filters. - * @param exec - Agent-bound caller and cancellation signal. - * @param maxResults - Maximum number of authorized matches to return. - * @returns Formatted authorized Session matches and truncation status. - */ - async executeSessionSearch( - args: SessionSearchArgs, - exec: ToolRunContext, - maxResults: number, - ): Promise { - const ctx = this.ctx - const caller = this.callerOf(exec) - const cwd = caller.header.cwd - if (cwd === undefined) { - throw new HarnessError( - 'cross-session search is unavailable because the caller session has no workspace', - 'SESSION_QUERY_TOOL_UNAUTHORIZED', - ) - } - const query = toolInput.normalizeQuery(args.query) - const sessionFilters = toolInput.buildSessionFilters(args) - const eventFilters = toolInput.buildEventFilters({ - seqFrom: args.event_seq_from, - seqTo: args.event_seq_to, - timeFrom: args.event_time_from, - timeTo: args.event_time_to, - eventTypes: args.event_types, - surfaces: args.event_surfaces, - }) - const requestedParentIds = toolInput.materializeParentSessionIds(args.parent_session_ids) - if (requestedParentIds !== undefined || args.include_root_sessions === true) { - const authorizedParentIds = requestedParentIds === undefined - ? new Set() - : await workspaceAccess.authorizeSessionIds(ctx, caller, requestedParentIds, exec.signal) - const parentValues: Array = requestedParentIds - ?.filter(id => authorizedParentIds.has(id)) ?? [] - if (args.include_root_sessions === true) parentValues.push(null) - if (parentValues.length === 0) return presentation.formatEmptySessionSearch() - sessionFilters.push({ kind: 'parent', values: parentValues }) - } - sessionFilters.push({ kind: 'cwd', values: [cwd] }) - const collected = await collectPages( - maxResults, - exec.signal, - cursor => serviceBoundary.call(ctx, exec.signal, 'session search', () => - ctx.sessionQuery.searchSessions({ + const parentIds = collected.items + .map(hit => hit.header.parentSession) + .filter((id): id is SessionId => id !== undefined) + const authorizedParents = await workspaceAccess.authorizeSessionIds(ctx, caller, parentIds, exec.signal) + const titles = await workspaceAccess.readTitles( + ctx, + caller, + collected.items.map(hit => hit.header.id), + exec.signal, + ) + return presentation.formatSessionSearch(collected, titles, authorizedParents) +} + +async function executeEventSearch( + ctx: Context, + turnBoundary: ProjectionHandle, + args: EventSearchArgs, + exec: ToolRunContext, + maxResults: number, +): Promise { + const caller = workspaceAccess.callerOf(exec, turnBoundary) + const sessionId = workspaceAccess.targetId(args, caller) + await workspaceAccess.authorizeTarget(ctx, caller, sessionId, exec.signal) + const query = toolInput.normalizeQuery(args.query) + const range = toolInput.sequenceRange(args.seq_from, args.seq_to) + if (sessionId === caller.id) { + const stepStartSeq = caller.boundary.lastStepStartSeq + // `null` (no step started yet) caps the range to a degenerate `to` the + // filter validation rejects — the loop never runs tools outside a step. + range.to = Math.min(range.to ?? Number.MAX_SAFE_INTEGER, (stepStartSeq ?? 0) - 1) + } + const title = await workspaceAccess.readTitle(ctx, caller, sessionId, exec.signal) + if (range.from !== undefined && range.to !== undefined && range.from > range.to) { + return presentation.formatEventSearch(sessionId, title, { items: [], capped: false }) + } + const filters = toolInput.buildEventFilters({ + seqFrom: range.from, + seqTo: range.to, + timeFrom: args.time_from, + timeTo: args.time_to, + eventTypes: args.event_types, + surfaces: args.surfaces, + }) + const collected = await collectPages( + maxResults, + exec.signal, + async (cursor): Promise => { + const page = await serviceBoundary.call(ctx, exec.signal, 'event search', () => + ctx.sessionQuery.searchEvents({ + sessionId, query, - sessionFilters, - eventFilters, + filters, ...cursor === undefined ? {} : { cursor }, - }, { signal: exec.signal })), - hit => hit.header.id !== caller.id && workspaceAccess.recordAuthorized(hit, caller), - ) + }, { signal: exec.signal })) + workspaceAccess.assertObservedTargetAuthorized(caller, sessionId, page.session) + return page + }, + () => true, + ) + return presentation.formatEventSearch(sessionId, title, collected) +} - const parentIds = collected.items - .map(hit => hit.header.parentSession) - .filter((id): id is SessionId => id !== undefined) - const authorizedParents = await workspaceAccess.authorizeSessionIds(ctx, caller, parentIds, exec.signal) - const titles = await workspaceAccess.readTitles( - ctx, - caller, - collected.items.map(hit => hit.header.id), - exec.signal, - ) - return presentation.formatSessionSearch(collected, titles, authorizedParents) - } +async function executeSessionTrace( + ctx: Context, + turnBoundary: ProjectionHandle, + args: SessionTargetArgs, + exec: ToolRunContext, +): Promise { + const caller = workspaceAccess.callerOf(exec, turnBoundary) + const sessionId = workspaceAccess.targetId(args, caller) + await workspaceAccess.authorizeTarget(ctx, caller, sessionId, exec.signal) + const trace = await serviceBoundary.call(ctx, exec.signal, 'session lineage trace', () => + ctx.sessionQuery.traceSession(sessionId, exec.signal)) + workspaceAccess.assertObservedTargetAuthorized(caller, sessionId, trace.target.header) - /** - * Search authorized Session events, excluding the caller’s current step. - * @param args - Target Session and event filters. - * @param exec - Agent-bound caller and cancellation signal. - * @param maxResults - Maximum number of authorized matches to return. - * @returns Formatted matching events and truncation status. - */ - async executeEventSearch( - args: EventSearchArgs, - exec: ToolRunContext, - maxResults: number, - ): Promise { - const ctx = this.ctx - const caller = this.callerOf(exec) - const sessionId = workspaceAccess.targetId(args, caller) - await workspaceAccess.authorizeTarget(ctx, caller, sessionId, exec.signal) - const query = toolInput.normalizeQuery(args.query) - const range = toolInput.sequenceRange(args.seq_from, args.seq_to) - if (sessionId === caller.id) { - const stepStartSeq = caller.boundary.lastStepStartSeq - // `null` (no step started yet) caps the range to a degenerate `to` the - // filter validation rejects — the loop never runs tools outside a step. - range.to = Math.min(range.to ?? Number.MAX_SAFE_INTEGER, (stepStartSeq ?? 0) - 1) + const ancestors: SessionRecord[] = [] + let ancestorBoundary = false + for (const ancestor of trace.ancestors) { + if (!workspaceAccess.recordAuthorized(ancestor, caller)) { + ancestorBoundary = true + break } - const title = await workspaceAccess.readTitle(ctx, caller, sessionId, exec.signal) - if (range.from !== undefined && range.to !== undefined && range.from > range.to) { - return presentation.formatEventSearch(sessionId, title, { items: [], capped: false }) - } - const filters = toolInput.buildEventFilters({ - seqFrom: range.from, - seqTo: range.to, - timeFrom: args.time_from, - timeTo: args.time_to, - eventTypes: args.event_types, - surfaces: args.surfaces, - }) - const collected = await collectPages( - maxResults, - exec.signal, - async (cursor): Promise => { - const page = await serviceBoundary.call(ctx, exec.signal, 'event search', () => - ctx.sessionQuery.searchEvents({ - sessionId, - query, - filters, - ...cursor === undefined ? {} : { cursor }, - }, { signal: exec.signal })) - workspaceAccess.assertObservedTargetAuthorized(caller, sessionId, page.session) - return page - }, - () => true, - ) - return presentation.formatEventSearch(sessionId, title, collected) + ancestors.push(ancestor) } + if (ancestors.length === trace.ancestors.length && !trace.complete) ancestorBoundary = true + const descendants = workspaceAccess.authorizeDescendants(trace.descendants, caller) + const visibleIds = [ + trace.target.header.id, + ...ancestors.map(record => record.header.id), + ...workspaceAccess.descendantIds(descendants), + ] + const titles = await workspaceAccess.readTitles(ctx, caller, visibleIds, exec.signal) + return presentation.formatSessionTrace(trace, ancestors, ancestorBoundary, descendants, titles) +} - /** - * Trace Session lineage within the caller’s workspace permissions. - * @param args - Optional target Session id; defaults to the caller. - * @param exec - Agent-bound caller and cancellation signal. - * @returns Formatted authorized ancestors and descendants. - */ - async executeSessionTrace( - args: SessionTargetArgs, - exec: ToolRunContext, - ): Promise { - const ctx = this.ctx - const caller = this.callerOf(exec) - const sessionId = workspaceAccess.targetId(args, caller) - await workspaceAccess.authorizeTarget(ctx, caller, sessionId, exec.signal) - const trace = await serviceBoundary.call(ctx, exec.signal, 'session lineage trace', () => - ctx.sessionQuery.traceSession(sessionId, exec.signal)) - workspaceAccess.assertObservedTargetAuthorized(caller, sessionId, trace.target.header) +async function executeEventTrace( + ctx: Context, + turnBoundary: ProjectionHandle, + args: EventTargetArgs, + exec: ToolRunContext, +): Promise { + toolInput.assertNonNegativeSafeInteger('seq', args.seq) + const seq = SessionSeq(args.seq) + const caller = workspaceAccess.callerOf(exec, turnBoundary) + const sessionId = workspaceAccess.targetId(args, caller) + await workspaceAccess.authorizeTarget(ctx, caller, sessionId, exec.signal) + const trace = await serviceBoundary.call(ctx, exec.signal, 'event trace', () => + ctx.sessionQuery.traceEvent({ sessionId, seq }, exec.signal)) + workspaceAccess.assertObservedTargetAuthorized(caller, sessionId, trace.session) + const title = await workspaceAccess.readTitle(ctx, caller, sessionId, exec.signal) + return presentation.formatEventTrace(sessionId, title, trace) +} - const ancestors: SessionRecord[] = [] - let ancestorBoundary = false - for (const ancestor of trace.ancestors) { - if (!workspaceAccess.recordAuthorized(ancestor, caller)) { - ancestorBoundary = true - break - } - ancestors.push(ancestor) - } - if (ancestors.length === trace.ancestors.length && !trace.complete) ancestorBoundary = true - const descendants = workspaceAccess.authorizeDescendants(trace.descendants, caller) - const visibleIds = [ - trace.target.header.id, - ...ancestors.map(record => record.header.id), - ...workspaceAccess.descendantIds(descendants), - ] - const titles = await workspaceAccess.readTitles(ctx, caller, visibleIds, exec.signal) - return presentation.formatSessionTrace(trace, ancestors, ancestorBoundary, descendants, titles) - } - - /** - * Trace an event after authorizing its owning Session. - * @param args - Target Session and event sequence. - * @param exec - Agent-bound caller and cancellation signal. - * @returns Formatted event lineage. - */ - async executeEventTrace( - args: EventTargetArgs, - exec: ToolRunContext, - ): Promise { - const ctx = this.ctx - toolInput.assertNonNegativeSafeInteger('seq', args.seq) - const seq = SessionSeq(args.seq) - const caller = this.callerOf(exec) - const sessionId = workspaceAccess.targetId(args, caller) - await workspaceAccess.authorizeTarget(ctx, caller, sessionId, exec.signal) - const trace = await serviceBoundary.call(ctx, exec.signal, 'event trace', () => - ctx.sessionQuery.traceEvent({ sessionId, seq }, exec.signal)) - workspaceAccess.assertObservedTargetAuthorized(caller, sessionId, trace.session) - const title = await workspaceAccess.readTitle(ctx, caller, sessionId, exec.signal) - return presentation.formatEventTrace(sessionId, title, trace) - } - - /** - * Read an event window after authorizing its owning Session. - * @param args - Target event and surrounding event counts. - * @param exec - Agent-bound caller and cancellation signal. - * @returns Formatted event window. - */ - async executeEventRead( - args: EventReadArgs, - exec: ToolRunContext, - ): Promise { - const ctx = this.ctx - toolInput.assertNonNegativeSafeInteger('seq', args.seq) - const seq = SessionSeq(args.seq) - if (args.before !== undefined) toolInput.assertNonNegativeSafeInteger('before', args.before) - if (args.after !== undefined) toolInput.assertNonNegativeSafeInteger('after', args.after) - const caller = this.callerOf(exec) - const sessionId = workspaceAccess.targetId(args, caller) - await workspaceAccess.authorizeTarget(ctx, caller, sessionId, exec.signal) - const window = await serviceBoundary.call(ctx, exec.signal, 'event read', () => - ctx.sessionQuery.readEvent({ - sessionId, - seq, - ...args.before === undefined ? {} : { before: args.before }, - ...args.after === undefined ? {} : { after: args.after }, - }, exec.signal)) - workspaceAccess.assertObservedTargetAuthorized(caller, sessionId, window.session) - const title = await workspaceAccess.readTitle(ctx, caller, sessionId, exec.signal) - return presentation.formatEventRead(sessionId, title, window) - } +async function executeEventRead( + ctx: Context, + turnBoundary: ProjectionHandle, + args: EventReadArgs, + exec: ToolRunContext, +): Promise { + toolInput.assertNonNegativeSafeInteger('seq', args.seq) + const seq = SessionSeq(args.seq) + if (args.before !== undefined) toolInput.assertNonNegativeSafeInteger('before', args.before) + if (args.after !== undefined) toolInput.assertNonNegativeSafeInteger('after', args.after) + const caller = workspaceAccess.callerOf(exec, turnBoundary) + const sessionId = workspaceAccess.targetId(args, caller) + await workspaceAccess.authorizeTarget(ctx, caller, sessionId, exec.signal) + const window = await serviceBoundary.call(ctx, exec.signal, 'event read', () => + ctx.sessionQuery.readEvent({ + sessionId, + seq, + ...args.before === undefined ? {} : { before: args.before }, + ...args.after === undefined ? {} : { after: args.after }, + }, exec.signal)) + workspaceAccess.assertObservedTargetAuthorized(caller, sessionId, window.session) + const title = await workspaceAccess.readTitle(ctx, caller, sessionId, exec.signal) + return presentation.formatEventRead(sessionId, title, window) } async function collectPages( @@ -333,3 +276,12 @@ async function collectPages( cursor = page.nextCursor } } + +/** Five model-facing session-query operation implementations. */ +export const operations = { + executeSessionSearch, + executeEventSearch, + executeSessionTrace, + executeEventTrace, + executeEventRead, +} diff --git a/packages/experimental/tool-session-query/src/workspace-access.ts b/packages/experimental/tool-session-query/src/workspace-access.ts index 4f11e64f38..836763cb3e 100644 --- a/packages/experimental/tool-session-query/src/workspace-access.ts +++ b/packages/experimental/tool-session-query/src/workspace-access.ts @@ -6,6 +6,7 @@ import type { Context } from '@deepseek-ai/cordis' import { brandString } from '@deepseek-ai/dsh-brand' +import { HarnessError } from '@deepseek-ai/dsh-llm' import { type SessionHeader, type SessionId as SessionIdValue, @@ -15,6 +16,8 @@ import type { SessionLineageNode, SessionRecord, } from '@deepseek-ai/dsh-session-query' +import type { ToolRunContext } from '@deepseek-ai/dsh-tools' +import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection' import { serviceBoundary } from './service-boundary.ts' interface Caller { @@ -50,6 +53,21 @@ interface DescendantVisit { readonly next: DescendantVisit | undefined } +function callerOf(exec: ToolRunContext, turnBoundary: ProjectionHandle): Caller { + const agent = exec.agent + if (agent === undefined) { + throw new HarnessError( + 'session query tools require an agent-bound caller', + 'SESSION_QUERY_TOOL_MISSING_AGENT', + ) + } + return { + id: agent.session.id, + header: agent.session.header, + boundary: turnBoundary.stateOf(agent.session), + } +} + function targetId(args: { readonly session_id?: string }, caller: Caller): SessionIdValue { return args.session_id === undefined ? caller.id : brandString(args.session_id) } @@ -224,6 +242,7 @@ function titleText(view: TitleView): string { /** Workspace-scoped caller authorization, title access, and lineage projection. */ export const workspaceAccess = { + callerOf, targetId, authorizeTarget, recordAuthorized, From a9bbe0c220e398cceb036c346c4b93e323f8bce6 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Mon, 12 Oct 2026 13:08:25 +0800 Subject: [PATCH 033/369] fix(session-projection): always return a promise from handle disposal --- docs/subsystems/session-projection.md | 7 +++++-- docs/subsystems/session-projection.zh.md | 7 +++++-- .../session-projection-handles/guide.i18n.yaml | 4 ++-- .../session-projection-handles/guide.md | 2 +- .../session-projection-handles/guide.zh.md | 2 +- packages/extensions/tool-cordis/src/api-catalog.ts | 2 +- packages/session/session-projection/README.i18n.yaml | 4 ++-- packages/session/session-projection/README.md | 2 +- packages/session/session-projection/README.zh.md | 2 +- packages/session/session-projection/src/index.ts | 11 ++++++++--- .../tests/registration-reader.spec.ts | 7 ++++--- 11 files changed, 31 insertions(+), 19 deletions(-) diff --git a/docs/subsystems/session-projection.md b/docs/subsystems/session-projection.md index 5da8044825..b7d8fb2bad 100644 --- a/docs/subsystems/session-projection.md +++ b/docs/subsystems/session-projection.md @@ -18,8 +18,11 @@ interface ProjectionHandle { * @returns the projection state at the Session's current cursor. */ stateOf(session: Session): S - /** Unregister from global drives and snapshots without invalidating this reader. */ - dispose(): void | Promise + /** + * Unregister from global drives and snapshots without invalidating this reader. + * @returns a promise that resolves when this registration is released. + */ + dispose(): Promise } ``` diff --git a/docs/subsystems/session-projection.zh.md b/docs/subsystems/session-projection.zh.md index c2c952b8fc..224a5dfdb6 100644 --- a/docs/subsystems/session-projection.zh.md +++ b/docs/subsystems/session-projection.zh.md @@ -18,8 +18,11 @@ interface ProjectionHandle { * @returns the projection state at the Session's current cursor. */ stateOf(session: Session): S - /** Unregister from global drives and snapshots without invalidating this reader. */ - dispose(): void | Promise + /** + * Unregister from global drives and snapshots without invalidating this reader. + * @returns a promise that resolves when this registration is released. + */ + dispose(): Promise } ``` diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.i18n.yaml index 7d864c5be2..5d03bdc8c8 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.i18n.yaml +++ b/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.i18n.yaml @@ -12,5 +12,5 @@ en: 06c3d4ee9031fb7e zh: cf2b664bfdafe837 /session-projection-registration-handles/migration: - en: 0619d4d16272fa0c - zh: c3b0bb2c9d3ecb57 + en: 50cde783ecaa35e8 + zh: 42e5574f58b049d6 diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.md b/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.md index f746d9170d..20d11a2b03 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.md +++ b/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.md @@ -16,6 +16,6 @@ English | [中文](guide.zh.md) 1. Declare `sessionProjections` as a required plugin injection and mount `SessionProjectionRegistry` before consumers, including `SubagentRuntime` in custom compositions. 2. In an asynchronous function-plugin `apply()` or service `[Service.init]()`, await `ctx.sessionProjections.register(definition)` and retain the returned handle before publishing dependent operations. Keep existing function-plugin namespace exports. -3. Replace `registry.stateOf(session, key)` with `handle.stateOf(session)`. Import the owning package's projection definition when a reader needs to acquire a handle itself. Replace stored unregister callbacks with `handle.dispose`; fiber disposal still releases registrations automatically. Await registration when handling initialization failures. +3. Replace `registry.stateOf(session, key)` with `handle.stateOf(session)`. Import the owning package's projection definition when a reader needs to acquire a handle itself. Replace stored unregister callbacks with `handle.dispose`, which always returns `Promise`, and await explicit disposal; fiber disposal still releases registrations automatically. Await registration when handling initialization failures. 4. Replace `installModelSelectionProjection(ctx)` with `await ctx.sessionProjections.register(modelSelectionProjection)`, importing the definition from `@deepseek-ai/dsh-api-session-controller/src/model-selection-projection.ts`. 5. Typecheck custom plugins, then verify activation, a Session state read, and owner disposal. A released reader remains usable, while the released unit disappears from new snapshots once its final registration is gone. diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.zh.md index d6a19c5631..fd52f33077 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.zh.md +++ b/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.zh.md @@ -16,6 +16,6 @@ description: "Session 投影注册改为异步,并返回保留读取能力的 1. 将 `sessionProjections` 声明为插件的必需注入,并在消费者之前挂载 `SessionProjectionRegistry`,包括自定义组合中的 `SubagentRuntime`。 2. 在函数插件的异步 `apply()` 或服务的 `[Service.init]()` 中,等待 `ctx.sessionProjections.register(definition)`,在发布依赖操作前保留返回的句柄。保留既有的函数插件命名空间导出。 -3. 将 `registry.stateOf(session, key)` 替换为 `handle.stateOf(session)`。读取方需要自行取得句柄时,导入归属包的投影定义。将保存的注销回调替换为 `handle.dispose`;fiber 释放仍会自动解除注册。处理初始化错误时须等待注册完成。 +3. 将 `registry.stateOf(session, key)` 替换为 `handle.stateOf(session)`。读取方需要自行取得句柄时,导入归属包的投影定义。将保存的注销回调替换为始终返回 `Promise` 的 `handle.dispose`,并等待显式释放完成;fiber 释放仍会自动解除注册。处理初始化错误时须等待注册完成。 4. 将 `installModelSelectionProjection(ctx)` 替换为 `await ctx.sessionProjections.register(modelSelectionProjection)`,从 `@deepseek-ai/dsh-api-session-controller/src/model-selection-projection.ts` 导入该定义。 5. 对自定义插件执行类型检查,再验证激活、Session 状态读取与 owner 释放。已释放的读取句柄仍可使用,而投影单元在最后一次注册解除后会从新快照中消失。 diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 4e5b3862ed..ceac4dd3a7 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -6332,7 +6332,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'ProjectionHandle', - declaration: 'export interface ProjectionHandle {\n stateOf(session: Session): S;\n dispose(): void | Promise;\n}', + declaration: 'export interface ProjectionHandle {\n stateOf(session: Session): S;\n dispose(): Promise;\n}', }, { name: 'ProjectionSnapshot', diff --git a/packages/session/session-projection/README.i18n.yaml b/packages/session/session-projection/README.i18n.yaml index cf0fd8eeeb..90321e2e39 100644 --- a/packages/session/session-projection/README.i18n.yaml +++ b/packages/session/session-projection/README.i18n.yaml @@ -24,8 +24,8 @@ en: 09758d1475fad86e zh: 473637cb34aebe72 /deepseek-ai-dsh-session-projection/use-this-package/register-and-read: - en: e10e44c46035dbdb - zh: dff312b52f4ec2ce + en: 52120bc6e18a8da8 + zh: c45ca6a83a6fcd4e /deepseek-ai-dsh-session-projection/use-this-package/persisted-checkpoints: en: 45f1c05fb7e08180 zh: f8d62c749899651a diff --git a/packages/session/session-projection/README.md b/packages/session/session-projection/README.md index 61e2677e47..9a1330ed60 100644 --- a/packages/session/session-projection/README.md +++ b/packages/session/session-projection/README.md @@ -55,7 +55,7 @@ const definition = { ### Register and read -`await register(definition)` installs the unit and returns a handle with synchronous `stateOf(session)` reads and `dispose()`. Registrants with the same key and `stateVersion` share cells; incompatible or invalid versions reject registration. Registration belongs to the calling fiber. The last unregister removes the unit from global event drives, snapshots, checkpoints, and change notifications. Existing handles remain readable: each retains its original unit and folds new Session events on demand, even if another version later registers the same key. Reading a handle does not initialize unrelated units or register it again. Carriers use `snapshot(session)` for a consistent client-view cut and `onChanged(listener)` for changes to registered units. +`await register(definition)` installs the unit and returns a handle with synchronous `stateOf(session)` reads and `dispose(): Promise`. Await `dispose()` to release the registration; repeated calls are safe. Registrants with the same key and `stateVersion` share cells; incompatible or invalid versions reject registration. Registration belongs to the calling fiber. The last unregister removes the unit from global event drives, snapshots, checkpoints, and change notifications. Existing handles remain readable: each retains its original unit and folds new Session events on demand, even if another version later registers the same key. Reading a handle does not initialize unrelated units or register it again. Carriers use `snapshot(session)` for a consistent client-view cut and `onChanged(listener)` for changes to registered units. ```text const projection = await ctx.sessionProjections.register(definition) diff --git a/packages/session/session-projection/README.zh.md b/packages/session/session-projection/README.zh.md index 2a0ac169d7..865d9e8537 100644 --- a/packages/session/session-projection/README.zh.md +++ b/packages/session/session-projection/README.zh.md @@ -55,7 +55,7 @@ const definition = { ### 注册与读取 -`await register(definition)` 安装单元,返回提供同步 `stateOf(session)` 读取和 `dispose()` 的 handle。具有相同 key 和 `stateVersion` 的注册方共享 cell;版本不兼容或非法时注册会拒绝。注册归调用方 fiber 所有。最后一个注册方注销后,单元退出全局事件驱动、快照、检查点和变更通知。已有 handle 仍可读取:它保留原有单元,按需折叠新增的 Session 事件,即使同一 key 后来注册了其他版本也不受影响。读取 handle 不会初始化无关单元,也不会重新注册。载体用 `snapshot(session)` 读取一致的客户端视图切面,用 `onChanged(listener)` 订阅已注册单元的变更。 +`await register(definition)` 安装单元,返回提供同步 `stateOf(session)` 读取和 `dispose(): Promise` 的 handle。等待 `dispose()` 完成即可释放注册;重复调用是安全的。具有相同 key 和 `stateVersion` 的注册方共享 cell;版本不兼容或非法时注册会拒绝。注册归调用方 fiber 所有。最后一个注册方注销后,单元退出全局事件驱动、快照、检查点和变更通知。已有 handle 仍可读取:它保留原有单元,按需折叠新增的 Session 事件,即使同一 key 后来注册了其他版本也不受影响。读取 handle 不会初始化无关单元,也不会重新注册。载体用 `snapshot(session)` 读取一致的客户端视图切面,用 `onChanged(listener)` 订阅已注册单元的变更。 ```text const projection = await ctx.sessionProjections.register(definition) diff --git a/packages/session/session-projection/src/index.ts b/packages/session/session-projection/src/index.ts index 9a27c2b98a..1790882394 100644 --- a/packages/session/session-projection/src/index.ts +++ b/packages/session/session-projection/src/index.ts @@ -46,8 +46,11 @@ export interface ProjectionHandle { * @returns the projection state at the Session's current cursor. */ stateOf(session: Session): S - /** Unregister from global drives and snapshots without invalidating this reader. */ - dispose(): void | Promise + /** + * Unregister from global drives and snapshots without invalidating this reader. + * @returns a promise that resolves when this registration is released. + */ + dispose(): Promise } /** @@ -303,7 +306,9 @@ export class SessionProjectionRegistry extends Service { await Promise.resolve() this.ctx.fiber.assertActive() return { - dispose, + async dispose() { + await dispose() + }, stateOf: (session) => { return this.cellFor(registration, session).state as S }, diff --git a/packages/session/session-projection/tests/registration-reader.spec.ts b/packages/session/session-projection/tests/registration-reader.spec.ts index 3ee393b85a..ab287d6110 100644 --- a/packages/session/session-projection/tests/registration-reader.spec.ts +++ b/packages/session/session-projection/tests/registration-reader.spec.ts @@ -98,10 +98,11 @@ describe('retained projection readers', () => { const retained = await ctx.sessionProjections.register(definition) const handle = await owner.ctx.sessionProjections.register(definition) if (release === 'twice') { - await handle.dispose() - await handle.dispose() + await expect(handle.dispose()).resolves.toBeUndefined() + await expect(handle.dispose()).resolves.toBeUndefined() } else { - await Promise.all([owner.dispose(), Promise.resolve(handle.dispose())]) + await Promise.all([owner.dispose(), expect(handle.dispose()).resolves.toBeUndefined()]) + await expect(handle.dispose()).resolves.toBeUndefined() } session.append('turn/start', { turn: 1 }) expect(ctx.sessionProjections.snapshot(session).values).toEqual({ 'test/reader': 1 }) From 96784dae411619183484de7db2816f6b67844aa6 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Mon, 12 Oct 2026 13:20:04 +0800 Subject: [PATCH 034/369] refactor(tool-goal): keep authority checks on the original event scan --- packages/goal/tool-goal/README.i18n.yaml | 4 +- packages/goal/tool-goal/README.md | 3 +- packages/goal/tool-goal/README.zh.md | 3 +- packages/goal/tool-goal/package.json | 3 +- packages/goal/tool-goal/src/authority.ts | 147 ++++++++---------- packages/goal/tool-goal/src/index.ts | 13 +- .../goal/tool-goal/tests/tool-goal.spec.ts | 23 ++- pnpm-lock.yaml | 3 - 8 files changed, 82 insertions(+), 117 deletions(-) diff --git a/packages/goal/tool-goal/README.i18n.yaml b/packages/goal/tool-goal/README.i18n.yaml index 1d97f69a1b..1006276ef8 100644 --- a/packages/goal/tool-goal/README.i18n.yaml +++ b/packages/goal/tool-goal/README.i18n.yaml @@ -30,8 +30,8 @@ en: 4e9fdb5a89f4baa3 zh: 50d182467ae1f3a7 /deepseek-ai-dsh-tool-goal/understand-the-implementation/design: - en: 905e5e105efdf7a8 - zh: 3704176a160aabb1 + en: 3be550d69f7167f1 + zh: 0becdbd7b7395e40 /deepseek-ai-dsh-tool-goal/understand-the-implementation/source-map: en: 4d5c648787409868 zh: b4938bfe660f18aa diff --git a/packages/goal/tool-goal/README.md b/packages/goal/tool-goal/README.md index 9ab458268d..23b457e6c8 100644 --- a/packages/goal/tool-goal/README.md +++ b/packages/goal/tool-goal/README.md @@ -68,8 +68,7 @@ This section explains how the tools enforce authority and render output; the obs ### Design -- **Authority at execution.** Every call resolves the exact live agent, its inherited `AgentRegistry` initiator, running status, and an open turn; `create`, `edit`, `pause`, and `resume` additionally require an accepted `{ kind: 'user' }` message or steering event in a runtime-root agent's current turn. A durable paused goal fails the `resume` action with `GOAL_TOOL_RESUME_PAUSED`; the user-facing command or Web control owns that transition. Durable fork lineage does not demote a resumed root; live subagent ownership does. -- **Current-turn input projection.** A host-only projection retains human-input presence and admitted goal-round identifiers, resets at each turn boundary, and reconstructs these facts from committed events when registered. +- **Authority at execution.** The plugin awaits its turn-boundary reader before registering tools. Every call resolves the exact live agent, its inherited `AgentRegistry` initiator, running status, and an open turn; `create`, `edit`, `pause`, and `resume` additionally require an accepted `{ kind: 'user' }` message or steering event in a runtime-root agent's current turn. A durable paused goal fails the `resume` action with `GOAL_TOOL_RESUME_PAUSED`; the user-facing command or Web control owns that transition. Durable fork lineage does not demote a resumed root; live subagent ownership does. - **Host attestation of human input.** `{ kind: 'user' }` is assigned by `Agent.followup()` and `steer()` when their caller omits a source, so plugins, schedulers, and other non-human producers must pass their own source rather than inheriting human authority. - **System-prompt guidance with the configured threshold.** The package registers one `tool:goal` system-prompt section whose fixed text interpolates `blockedAfterConsecutiveRounds`; the same value is the hard lower bound enforced at execution. - **Wrap-up context for terminal rounds.** A successful autonomous `complete` or `blocked` defers a closing `` or `` instruction so the model addresses the user once before the turn ends; direct-human mutations never defer this context. diff --git a/packages/goal/tool-goal/README.zh.md b/packages/goal/tool-goal/README.zh.md index 9e868061c2..e7d67715c4 100644 --- a/packages/goal/tool-goal/README.zh.md +++ b/packages/goal/tool-goal/README.zh.md @@ -68,8 +68,7 @@ kind: "package-reference" ### 设计 -- **执行时权限。** 每次调用都解析精确活跃 agent、其继承的 `AgentRegistry` initiator、running 状态与开放轮次;`create`、`edit`、`pause` 和 `resume` 还要求运行时根 agent 的当前轮次中存在已接受的 `{ kind: 'user' }` 消息或 steering 事件。持久的 paused goal 会让 `resume` 以 `GOAL_TOOL_RESUME_PAUSED` 失败;面向用户的命令或 Web 控件拥有该转换。持久 fork 谱系不会降低已恢复根 agent 的等级;活跃 subagent 所有权会降低。 -- **当前轮输入投影。** 仅供 Host 使用的投影保存是否有人类输入及已接纳的目标轮次标识,在每个轮次边界重置,并在注册时从已提交事件重建这些事实。 +- **执行时权限。** 插件等待轮次边界读取句柄就绪后才注册工具。每次调用都解析精确活跃 agent、其继承的 `AgentRegistry` initiator、running 状态与开放轮次;`create`、`edit`、`pause` 和 `resume` 还要求运行时根 agent 的当前轮次中存在已接受的 `{ kind: 'user' }` 消息或 steering 事件。持久的 paused goal 会让 `resume` 以 `GOAL_TOOL_RESUME_PAUSED` 失败;面向用户的命令或 Web 控件拥有该转换。持久 fork 谱系不会降低已恢复根 agent 的等级;活跃 subagent 所有权会降低。 - **人类输入的宿主证明。** `Agent.followup()` 与 `steer()` 会在调用方省略 source 时分配 `{ kind: 'user' }`,因此插件、调度器与其他非人类生产方必须传入自己的 source,不能继承人类权限。 - **带配置阈值的系统提示词指引。** 本包注册一个 `tool:goal` 系统提示词章节,其固定文本插入 `blockedAfterConsecutiveRounds`;同一数值就是执行时强制执行的硬下限。 - **终局 Round 的结束上下文。** 成功的自主 `complete` 或 `blocked` 会延后一条 `` 或 `` 结束指令,让模型在轮次结束前向用户做一次交代;人类直接变更绝不会延后该上下文。 diff --git a/packages/goal/tool-goal/package.json b/packages/goal/tool-goal/package.json index 65a9e48f4b..e5c0e9d8e7 100644 --- a/packages/goal/tool-goal/package.json +++ b/packages/goal/tool-goal/package.json @@ -39,8 +39,7 @@ "@deepseek-ai/dsh-session-projection": "workspace:*" }, "dependencies": { - "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "@deepseek-ai/schemastery": "workspace:~" }, "devDependencies": { "@deepseek-ai/cordis-plugin-loader": "workspace:~", diff --git a/packages/goal/tool-goal/src/authority.ts b/packages/goal/tool-goal/src/authority.ts index 6214005230..07de4e68ae 100644 --- a/packages/goal/tool-goal/src/authority.ts +++ b/packages/goal/tool-goal/src/authority.ts @@ -1,58 +1,18 @@ /** Execution-time authority checks for the model-facing goal tools. */ import type { Context } from '@deepseek-ai/cordis' -import type { Agent } from '@deepseek-ai/dsh-agent' -import { GoalId, type GoalView } from '@deepseek-ai/dsh-goal' +import type { Agent, TurnBoundaryProjection } from '@deepseek-ai/dsh-agent' +import type { GoalView } from '@deepseek-ai/dsh-goal' import { HarnessError } from '@deepseek-ai/dsh-llm' +import type { SessionEvent, SessionSeq } from '@deepseek-ai/dsh-session' import type { ToolRunContext } from '@deepseek-ai/dsh-tools' -import type { ProjectionDefinition, ProjectionHandle } from '@deepseek-ai/dsh-session-projection' -import { z as zod } from 'zod' +import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection' -const turnAuthoritySchema = zod.object({ - open: zod.boolean(), - directHuman: zod.boolean(), - goalRounds: zod.array(zod.object({ - goalId: zod.string().transform(GoalId), - revision: zod.number().int().positive(), - round: zod.number().int().positive(), - })), -}) - -/** Accepted input authority for the currently open turn, reset at both turn boundaries. */ -type TurnAuthority = zod.infer - -declare module '@deepseek-ai/dsh-session-projection/types' { - interface SessionProjectionStateMap { - /** Input sources admitted in the current turn for goal-tool authorization. */ - goalToolAuthority: TurnAuthority - } -} - -const turnAuthorityProjection = { - key: 'goalToolAuthority', - stateVersion: 1, - stateSchema: turnAuthoritySchema, - init: (): TurnAuthority => ({ open: false, directHuman: false, goalRounds: [] }), - apply: (state, event) => { - if (event.type === 'turn/start' || event.type === 'turn/end') { - return { open: event.type === 'turn/start', directHuman: false, goalRounds: [] } - } - if (!state.open || event.type !== 'user/message') return state - const source = event.data.source - if (source.kind === 'user') return state.directHuman ? state : { ...state, directHuman: true } - if (source.kind === 'goal') { - const { goalId, revision, round } = source - return { ...state, goalRounds: [...state.goalRounds, { goalId, revision, round }] } - } - // Other message producers do not grant goal-tool authority. - return state - }, -} satisfies ProjectionDefinition<'goalToolAuthority', TurnAuthority> - -/** The authenticated caller and immutable authority facts captured for one tool call. */ +/** The calling agent plus the immutable event cut and open-turn start seq used for authority checks. */ export interface GoalToolExecution { readonly agent: Agent - readonly authority: TurnAuthority + readonly events: readonly SessionEvent[] + readonly openTurnStartSeq: SessionSeq } /** Hard authority granted to one state-changing call. */ @@ -65,44 +25,56 @@ function reject(message: string, code = 'GOAL_TOOL_AUTHORITY_REQUIRED'): never { throw new HarnessError(message, code) } -/** Resolves goal-tool authority from the live driver and its committed turn. */ -export class GoalToolExecutionResolver { - private authority!: ProjectionHandle - - /** @param ctx - Context carrying the live agent registry and session projections. */ - constructor(private readonly ctx: Context) {} - - /** - * Acquire projection readers before exposing dependent operations. - * @returns when all required registrations are ready. - */ - async initialize(): Promise { - this.authority = await this.ctx.sessionProjections.register(turnAuthorityProjection) +/** Resolve the immutable event cut and open-turn boundary without copying the turn suffix. */ +function openTurnEvents( + turnBoundary: ProjectionHandle, + agent: Agent, +): Pick { + // oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred. + const events = agent.session.snapshotEvents() + const boundary = turnBoundary.stateOf(agent.session) + if (boundary.openTurnStartSeq === null) { + reject('goal tools require an open model turn', 'GOAL_TOOL_DRIVER_REQUIRED') } + return { events, openTurnStartSeq: boundary.openTurnStartSeq } +} - /** - * Authenticate the caller and capture its current turn's accepted input sources. - * @param exec - Tool execution carrying the live calling agent. - * @returns the caller and projected authority facts. - */ - resolve(exec: ToolRunContext): GoalToolExecution { - const agent = exec.agent - if (agent === undefined) { - return reject('goal tools require a calling agent', 'GOAL_TOOL_AGENT_REQUIRED') - } - if (this.ctx.agents.get(agent.id) !== agent || agent.status !== 'running' - || this.ctx.agents.currentInitiator() !== agent) { - return reject( - 'goal tools require the exact live calling agent inside its active driver', - 'GOAL_TOOL_DRIVER_REQUIRED', - ) - } - const authority = this.authority.stateOf(agent.session) - if (!authority.open) { - reject('goal tools require an open model turn', 'GOAL_TOOL_DRIVER_REQUIRED') - } - return { agent, authority } +/** + * Resolve and authenticate the calling agent and its driver boundary. + * @param ctx - Context carrying the live agent registry. + * @param exec - Tool execution metadata supplied by the registry. + * @param turnBoundary - Ready reader for the calling Session's turn boundary. + * @returns The authenticated agent, immutable event cut, and open-turn boundary. + */ +export function goalToolExecution( + ctx: Context, + exec: ToolRunContext, + turnBoundary: ProjectionHandle, +): GoalToolExecution { + const agent = exec.agent + if (agent === undefined) { + return reject('goal tools require a calling agent', 'GOAL_TOOL_AGENT_REQUIRED') } + if (ctx.agents.get(agent.id) !== agent || agent.status !== 'running' + || ctx.agents.currentInitiator() !== agent) { + return reject( + 'goal tools require the exact live calling agent inside its active driver', + 'GOAL_TOOL_DRIVER_REQUIRED', + ) + } + return { agent, ...openTurnEvents(turnBoundary, agent) } +} + +/** Whether the captured open turn contains an event accepted by `predicate`. */ +function someOpenTurnEvent( + execution: GoalToolExecution, + predicate: (event: SessionEvent) => boolean, +): boolean { + for (let seq = execution.openTurnStartSeq + 1; seq < execution.events.length; seq += 1) { + const event = execution.events[seq] + if (event !== undefined && predicate(event)) return true + } + return false } /** @@ -112,14 +84,17 @@ export class GoalToolExecutionResolver { */ function hasDirectHumanInput(ctx: Context, execution: GoalToolExecution): boolean { if (!ctx.agents.roots().includes(execution.agent)) return false - return execution.authority.directHuman + return someOpenTurnEvent(execution, event => + event.type === 'user/message' && event.data.source.kind === 'user') } /** Whether this turn is the current goal's exact admitted round. */ function isMatchingGoalRound(execution: GoalToolExecution, goal: GoalView): boolean { - return execution.authority.goalRounds.some(round => round.goalId === goal.id - && round.revision === goal.revision - && round.round === goal.roundsStarted) + return someOpenTurnEvent(execution, event => event.type === 'user/message' + && event.data.source.kind === 'goal' + && event.data.source.goalId === goal.id + && event.data.source.revision === goal.revision + && event.data.source.round === goal.roundsStarted) } /** diff --git a/packages/goal/tool-goal/src/index.ts b/packages/goal/tool-goal/src/index.ts index a364dec967..962d0b8f98 100644 --- a/packages/goal/tool-goal/src/index.ts +++ b/packages/goal/tool-goal/src/index.ts @@ -5,6 +5,7 @@ */ import type { Context } from '@deepseek-ai/cordis' +import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent' import z from '@deepseek-ai/schemastery' import { GoalId } from '@deepseek-ai/dsh-goal' import type { GoalRef, GoalView } from '@deepseek-ai/dsh-goal' @@ -20,13 +21,12 @@ import { defineTool } from '@deepseek-ai/dsh-tools' import type { GenericCallView } from '@deepseek-ai/dsh-tools' import { completionAuthority, - GoalToolExecutionResolver, + goalToolExecution, requireDirectHuman, } from './authority.ts' import { renderWrapupContext } from './wrapup.ts' export const name = 'tool-goal' - export const inject = ['agents', 'goals', 'tools', 'systemPrompt', 'sessionProjections'] /** Model policy and hard lower bounds for goal-state updates. */ @@ -186,9 +186,8 @@ function present(title: string, kind: 'read' | 'other', rawInput?: unknown): Gen /** Register the three Codex-shaped goal tools and their shared policy section. */ export async function apply(ctx: Context, config: Config): Promise { - const executionResolver = new GoalToolExecutionResolver(ctx) - await executionResolver.initialize() const resolved = resolveConfig(config) + const turnBoundary = await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) ctx.systemPrompt.section({ name: 'tool:goal', order: ctx.systemPrompt.getSectionOrder('TOOL_GOAL'), @@ -201,7 +200,7 @@ export async function apply(ctx: Context, config: Config): Promise { parameters: {}, output: GOAL_OUTPUT, execute(_args, exec) { - const execution = executionResolver.resolve(exec) + const execution = goalToolExecution(ctx, exec, turnBoundary) return Promise.resolve(goalValue(ctx.goals.get(execution.agent))) }, presentCall: () => present('Read current goal', 'read'), @@ -223,7 +222,7 @@ export async function apply(ctx: Context, config: Config): Promise { }, output: GOAL_OUTPUT, execute(args, exec) { - const execution = executionResolver.resolve(exec) + const execution = goalToolExecution(ctx, exec, turnBoundary) requireDirectHuman(ctx, execution) const goal = ctx.goals.create(execution.agent, { objective: args.objective, @@ -256,7 +255,7 @@ export async function apply(ctx: Context, config: Config): Promise { }, output: GOAL_OUTPUT, execute(args, exec) { - const execution = executionResolver.resolve(exec) + const execution = goalToolExecution(ctx, exec, turnBoundary) const ref = goalRef(args.goal_id, args.revision) const replacements = { ...hasText(args.objective) ? { objective: args.objective } : {}, diff --git a/packages/goal/tool-goal/tests/tool-goal.spec.ts b/packages/goal/tool-goal/tests/tool-goal.spec.ts index 0bb6a91a48..08d5e46589 100644 --- a/packages/goal/tool-goal/tests/tool-goal.spec.ts +++ b/packages/goal/tool-goal/tests/tool-goal.spec.ts @@ -18,7 +18,6 @@ import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime from '@deepseek-ai/dsh-tools' import type { ToolExecutionResult } from '@deepseek-ai/dsh-tools' -import * as GoalTools from '@deepseek-ai/dsh-tool-goal' import * as toolGoal from '@deepseek-ai/dsh-tool-goal' import { createInboxStub } from '@deepseek-ai/dsh-agent-loop-testkit' @@ -105,7 +104,7 @@ async function harness(config: toolGoal.Config = {}) { await ctx.plugin(AgentRegistry) await ctx.plugin(ToolRuntime) await ctx.plugin(GoalService) - const fiber = await ctx.plugin(GoalTools, config) + const fiber = await ctx.plugin(toolGoal, config) const root = stubAgent(`goal-tool-root-${Math.random()}`, undefined, ctx) await ctx.agents.register(root.agent) return { ctx, fiber, root } @@ -207,13 +206,13 @@ describe('goal tool registration and presentation', () => { await ctx.plugin(AgentRegistry) await ctx.plugin(ToolRuntime) await ctx.plugin(GoalService) - await expect(GoalTools.apply(ctx, { blockedAfterConsecutiveRounds: 1.5 })).rejects.toThrow( + await expect(toolGoal.apply(ctx, { blockedAfterConsecutiveRounds: 1.5 })).rejects.toThrow( 'blockedAfterConsecutiveRounds must be a positive safe integer', ) expect(ctx.tools.get('get_goal')).toBeUndefined() }) - it('resolves the direct-constructor default before registration', async () => { + it('resolves the direct-apply default before registration', async () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) @@ -221,7 +220,7 @@ describe('goal tool registration and presentation', () => { await ctx.plugin(AgentRegistry) await ctx.plugin(ToolRuntime) await ctx.plugin(GoalService) - await GoalTools.apply(ctx, {}) + await toolGoal.apply(ctx, {}) const section = (await ctx.systemPrompt.assemble()).sections.find(item => item.name === 'tool:goal') expect(section?.text).toContain('at least 3 consecutive goal rounds') }) @@ -347,12 +346,12 @@ describe('goal tool execution authority', () => { const { ctx, root, fiber } = await harness() await fiber.dispose() openTurn(root, { kind: 'user' }) - await ctx.plugin(GoalTools, {}) + await ctx.plugin(toolGoal, {}) const result = await execute(ctx, 'create_goal', { objective: 'accepted before tool load' }, root.agent) expect(resultGoal(result)).toMatchObject({ objective: 'accepted before tool load' }) }) - it.each([1, 2])('accepts %i direct human steering messages in a goal-sourced root turn', async (messageCount) => { + it('accepts direct human steering in a goal-sourced root turn', async () => { const { ctx, root } = await harness() const humanTurn = openTurn(root, { kind: 'user' }) const created = ctx.goals.create(root.agent, { objective: 'steer me' }) @@ -360,12 +359,10 @@ describe('goal tool execution authority', () => { openTurn(root, { kind: 'goal', goalId: created.id, revision: created.revision, round: 1, }) - for (let index = 0; index < messageCount; index++) { - root.session.append('user/message', createUserMessage({ - content: [{ type: 'text', text: 'pause now' }], - source: { kind: 'user' }, - }), { surfaceOp: 'append' }) - } + root.session.append('user/message', createUserMessage({ + content: [{ type: 'text', text: 'pause now' }], + source: { kind: 'user' }, + }), { surfaceOp: 'append' }) const paused = await execute(ctx, 'update_goal', { goal_id: created.id, revision: created.revision, action: 'pause', }, root.agent) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06d99d6378..3ffe9577da 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9843,9 +9843,6 @@ importers: '@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:~ From ea5d070e373f8d813dc392454810f8eca2b2149b Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Mon, 12 Oct 2026 13:53:20 +0800 Subject: [PATCH 035/369] test(subagent): refresh model tools in merged snapshot scenarios --- .../tool-schemas.expected.json | 45 ++++++++++++++++++- .../system-prompt.expected.md | 24 +++++++++- .../session.v4.jsonl | 6 +-- .../ptc-persistent/system-prompt.expected.md | 24 +++++++++- .../system-prompt.expected.md | 24 +++++++++- 5 files changed, 112 insertions(+), 11 deletions(-) diff --git a/snapshots/session/office-progressive-native/tool-schemas.expected.json b/snapshots/session/office-progressive-native/tool-schemas.expected.json index 79d6891abc..7269632ffd 100644 --- a/snapshots/session/office-progressive-native/tool-schemas.expected.json +++ b/snapshots/session/office-progressive-native/tool-schemas.expected.json @@ -215,6 +215,23 @@ } } }, + { + "name": "list_subagent_models", + "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "parameters": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "description": "Registered LLM provider id. Omit to list providers." + }, + "model": { + "type": "string", + "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + } + } + } + }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -294,7 +311,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", "parameters": { "type": "object", "properties": { @@ -309,6 +326,18 @@ "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." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -319,7 +348,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", "parameters": { "type": "object", "properties": { @@ -334,6 +363,18 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." + }, + "provider": { + "type": "string", + "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + }, + "model": { + "type": "string", + "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + }, + "reasoning_effort": { + "type": "string", + "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/office-progressive-ptc/system-prompt.expected.md b/snapshots/session/office-progressive-ptc/system-prompt.expected.md index 9d427282d8..e56a80173c 100644 --- a/snapshots/session/office-progressive-ptc/system-prompt.expected.md +++ b/snapshots/session/office-progressive-ptc/system-prompt.expected.md @@ -119,6 +119,13 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; + /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + list_subagent_models: { + /** Registered LLM provider id. Omit to list providers. */ + provider?: string; + /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + model?: string; + } & Record; /** Read a UTF-8 text file and return line-numbered content. */ read: { /** Path to read, resolved by the filesystem backend. */ @@ -145,7 +152,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; 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 id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -153,8 +160,14 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -162,6 +175,12 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; /** Record and update a task list to plan multi-step work and show progress; skip it for trivial single-step tasks. Add one todo per concrete step before you start. While work remains, keep the todos being worked on `in_progress`, several only when work runs in parallel. Mark each todo `completed` as soon as it is done. */ todo_write: { @@ -350,6 +369,7 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; + list_subagent_models: string; read: { /** Canonical absolute path in the filesystem execution world. */ path: string; diff --git a/snapshots/session/pi-ai-conversation-updates/session.v4.jsonl b/snapshots/session/pi-ai-conversation-updates/session.v4.jsonl index 843a75e88d..6e26da9e41 100644 --- a/snapshots/session/pi-ai-conversation-updates/session.v4.jsonl +++ b/snapshots/session/pi-ai-conversation-updates/session.v4.jsonl @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Read task.txt, call the newly available snapshot_ping tool once, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"moonshotai","model":"kimi-k3"},"tools":["bash","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"moonshotai","model":"kimi-k3"},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"moonshotai","model":"kimi-k3","contextWindow":1048576,"systemPromptUpdate":"in-history"}} {"type":"session/title","data":{"title":"Read task.txt, call the newly","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}],"source":{"kind":"model","provider":"moonshotai","model":"kimi-k3"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1790149644504,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790149644504,"index":0,"dt":[],"id":"call_task_read","name":"read","args":["{\"file_path\":\"task.txt\"}"]},{"type":"chunk","time":1790149644504,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}}},{"type":"chunk","time":1790149644504,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1790149644504,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} @@ -18,14 +18,14 @@ {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} {"type":"system/message","data":{"turn":1,"step":2,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:6}}"}},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"moonshotai","model":"kimi-k3"},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} +{"type":"request/header","data":{"header":{"config":{"provider":"moonshotai","model":"kimi-k3"},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"snapshot_ping"}],"role":"developer","id":"{{message:7}}"},"headerSeq":19},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}],"source":{"kind":"model","provider":"moonshotai","model":"kimi-k3"},"id":"{{message:8}}"},"stream":[{"type":"chunk","time":1790149644534,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790149644534,"index":0,"dt":[],"id":"call_dynamic_ping","name":"snapshot_ping","args":["{}"]},{"type":"chunk","time":1790149644534,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}}},{"type":"chunk","time":1790149644535,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}} {"type":"tool/result","data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"call_dynamic_ping"},"toolCallId":"call_dynamic_ping","content":[{"type":"text","text":"pong"}],"isError":false,"id":"{{message:9}}"}},"sourceEventSeqs":[22],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} -{"type":"request/header","data":{"header":{"config":{"provider":"moonshotai","model":"kimi-k3"},"tools":["bash","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} +{"type":"request/header","data":{"header":{"config":{"provider":"moonshotai","model":"kimi-k3"},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-removal","toolName":"snapshot_ping"}],"role":"developer","id":"{{message:10}}"}},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"moonshotai","model":"kimi-k3"},"id":"{{message:11}}"},"usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10},"stream":[{"type":"chunk","time":1790149644557,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1790149644557,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1790149644557,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1790149644557,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10}}},{"type":"chunk","time":1790149644557,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} diff --git a/snapshots/session/ptc-persistent/system-prompt.expected.md b/snapshots/session/ptc-persistent/system-prompt.expected.md index 2801645046..835372b136 100644 --- a/snapshots/session/ptc-persistent/system-prompt.expected.md +++ b/snapshots/session/ptc-persistent/system-prompt.expected.md @@ -140,6 +140,13 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; + /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + list_subagent_models: { + /** Registered LLM provider id. Omit to list providers. */ + provider?: string; + /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + model?: string; + } & Record; /** Read a UTF-8 text file and return line-numbered content. */ read: { /** Path to read, resolved by the filesystem backend. */ @@ -166,7 +173,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; 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 id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -174,8 +181,14 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -183,6 +196,12 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; /** Echo template text such as {{item}}, {{model}}, or {{ item }} without substitution. */ template_echo: { @@ -411,6 +430,7 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; + list_subagent_models: string; read: { /** Canonical absolute path in the filesystem execution world. */ path: string; diff --git a/snapshots/session/ptc-policy-change/system-prompt.expected.md b/snapshots/session/ptc-policy-change/system-prompt.expected.md index 19204006b3..8308b029e0 100644 --- a/snapshots/session/ptc-policy-change/system-prompt.expected.md +++ b/snapshots/session/ptc-policy-change/system-prompt.expected.md @@ -140,6 +140,13 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; + /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + list_subagent_models: { + /** Registered LLM provider id. Omit to list providers. */ + provider?: string; + /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + model?: string; + } & Record; /** Read a UTF-8 text file and return line-numbered content. */ read: { /** Path to read, resolved by the filesystem backend. */ @@ -166,7 +173,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; 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 id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -174,8 +181,14 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -183,6 +196,12 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; /** Record and update a task list to plan multi-step work and show progress; skip it for trivial single-step tasks. Add one todo per concrete step before you start. While work remains, keep the todos being worked on `in_progress`, several only when work runs in parallel. Mark each todo `completed` as soon as it is done. */ todo_write: { @@ -409,6 +428,7 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; + list_subagent_models: string; read: { /** Canonical absolute path in the filesystem execution world. */ path: string; From ae19c6df8ee9beecd04a21a61a23e0c85444d5cb Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Mon, 12 Oct 2026 14:02:48 +0800 Subject: [PATCH 036/369] test(web): align merged snapshots with shared subagent tools --- .../collapse-timing/next-message.expected.md | 2 +- .../web/collapse-timing/verbose.expected.md | 2 +- .../system-prompt.expected.md | 24 +++++++++++++++++-- .../system-prompt.expected.md | 24 +++++++++++++++++-- 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/snapshots/web/collapse-timing/next-message.expected.md b/snapshots/web/collapse-timing/next-message.expected.md index 5a7c69c657..8968e9991c 100644 --- a/snapshots/web/collapse-timing/next-message.expected.md +++ b/snapshots/web/collapse-timing/next-message.expected.md @@ -11,7 +11,7 @@ - button "Copy" - status: Completed - button "Completed in {{duration}}" [expanded] -- button "Tools updated 31 added" +- button "Tools updated 32 added" - button "Think Both files have been read. a.txt contains \"alpha\" and b.txt contains \"beta\". I'll now reply with DONE as instructed." - paragraph: DONE - button "Copy" diff --git a/snapshots/web/collapse-timing/verbose.expected.md b/snapshots/web/collapse-timing/verbose.expected.md index b747e28130..272ff8345d 100644 --- a/snapshots/web/collapse-timing/verbose.expected.md +++ b/snapshots/web/collapse-timing/verbose.expected.md @@ -19,7 +19,7 @@ - button "Copy" - status: Completed - button "Completed in {{duration}}" [disabled] [expanded] -- button "Tools updated 31 added" +- button "Tools updated 32 added" - button "Think Both files have been read. a.txt contains \"alpha\" and b.txt contains \"beta\". I'll now reply with DONE as instructed." - paragraph: DONE - button "Copy" diff --git a/snapshots/web/official-subagent-bundle/system-prompt.expected.md b/snapshots/web/official-subagent-bundle/system-prompt.expected.md index 9e8e56f62b..8908ece5a4 100644 --- a/snapshots/web/official-subagent-bundle/system-prompt.expected.md +++ b/snapshots/web/official-subagent-bundle/system-prompt.expected.md @@ -158,6 +158,13 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; + /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + list_subagent_models: { + /** Registered LLM provider id. Omit to list providers. */ + provider?: string; + /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + model?: string; + } & Record; /** Declare existing files as final deliverables for the user. Use it when the user needs a separate file, especially Office documents, spreadsheets, and slide decks; prefer your final response when that suffices. The user opens the current files; their contents are not copied. */ present: { /** Usually the 1-2 most important deliverables; at most 4 per call. */ @@ -281,7 +288,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; 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 id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -289,6 +296,12 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; /** 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. The runtime notifies you when it finishes. The completion notice includes its final answer. This backend does not accept follow-up messages. */ subagent_claude_code: { @@ -299,7 +312,7 @@ interface ToolArgsMap { /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -307,6 +320,12 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; /** Record and update a task list to plan multi-step work and show progress; skip it for trivial single-step tasks. Add one todo per concrete step before you start. While work remains, keep the todos being worked on `in_progress`, several only when work runs in parallel. Mark each todo `completed` as soon as it is done. */ todo_write: { @@ -509,6 +528,7 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; + list_subagent_models: string; present: { turn: number; files: { diff --git a/snapshots/web/optional-string-editor/system-prompt.expected.md b/snapshots/web/optional-string-editor/system-prompt.expected.md index f5e4347e82..00fa427842 100644 --- a/snapshots/web/optional-string-editor/system-prompt.expected.md +++ b/snapshots/web/optional-string-editor/system-prompt.expected.md @@ -158,6 +158,13 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; + /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + list_subagent_models: { + /** Registered LLM provider id. Omit to list providers. */ + provider?: string; + /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + model?: string; + } & Record; /** Declare existing files as final deliverables for the user. Use it when the user needs a separate file, especially Office documents, spreadsheets, and slide decks; prefer your final response when that suffices. The user opens the current files; their contents are not copied. */ present: { /** Usually the 1-2 most important deliverables; at most 4 per call. */ @@ -298,7 +305,7 @@ interface ToolArgsMap { /** Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file. */ view_range?: number[] | null; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; 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 id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -306,8 +313,14 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -315,6 +328,12 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; + /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + provider?: string; + /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + model?: string; + /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + reasoning_effort?: string; } & Record; /** Record and update a task list to plan multi-step work and show progress; skip it for trivial single-step tasks. Add one todo per concrete step before you start. While work remains, keep the todos being worked on `in_progress`, several only when work runs in parallel. Mark each todo `completed` as soon as it is done. */ todo_write: { @@ -517,6 +536,7 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; + list_subagent_models: string; present: { turn: number; files: { From 409ed3318384b3d3cbd9b052234373c2397acc7e Mon Sep 17 00:00:00 2001 From: _Kerman Date: Mon, 12 Oct 2026 14:04:28 +0800 Subject: [PATCH 037/369] test(tool-present): import the built plugin namespace --- packages/deliverables/tool-present/tests/built-errors.e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/deliverables/tool-present/tests/built-errors.e2e.ts b/packages/deliverables/tool-present/tests/built-errors.e2e.ts index 7595872758..2fe7201def 100644 --- a/packages/deliverables/tool-present/tests/built-errors.e2e.ts +++ b/packages/deliverables/tool-present/tests/built-errors.e2e.ts @@ -16,7 +16,7 @@ import LocalFileSystem from './packages/fs/fs-local/lib/index.js' import SystemPrompt from './packages/core/system-prompt/lib/index.js' import ToolRuntime from './packages/core/tools/lib/index.js' import { Session, SESSION_FORMAT_VERSION } from './packages/core/session/lib/index.js' -import Present from './packages/deliverables/tool-present/lib/index.js' +import * as Present from './packages/deliverables/tool-present/lib/index.js' import SessionProjectionRegistry from './packages/session/session-projection/lib/index.js' import { mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' From 41ea4eb6def8c65706e19edb4c1cfe82642ae053 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Mon, 12 Oct 2026 15:00:27 +0800 Subject: [PATCH 038/369] fix(subagent): report model discovery errors per route --- ...authorized-subagent-model-routes.i18n.yaml | 4 +- ...4-user-authorized-subagent-model-routes.md | 2 +- ...ser-authorized-subagent-model-routes.zh.md | 2 +- .../subagent/tool-subagent/README.i18n.yaml | 4 +- packages/subagent/tool-subagent/README.md | 2 +- packages/subagent/tool-subagent/README.zh.md | 2 +- .../subagent/tool-subagent/src/list-models.ts | 15 ++++- .../tests/fixtures/model-discovery.mjs | 31 ++++++++++ .../tool-subagent/tests/list-models.spec.ts | 56 ++++++++++++++++++- .../runtime.cordis.yml | 3 + .../session.v4.jsonl | 23 ++++++++ .../snapshot.yml | 7 +++ 12 files changed, 139 insertions(+), 12 deletions(-) create mode 100644 packages/subagent/tool-subagent/tests/fixtures/model-discovery.mjs create mode 100644 snapshots/session/subagent-model-discovery-errors/runtime.cordis.yml create mode 100644 snapshots/session/subagent-model-discovery-errors/session.v4.jsonl create mode 100644 snapshots/session/subagent-model-discovery-errors/snapshot.yml diff --git a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml index c5c1287e45..d9816249cd 100644 --- a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml @@ -15,8 +15,8 @@ en: e41c17226190ea8b zh: d3b1395fafe75f01 /agent-note-user-authorized-subagent-model-routes/consequences: - en: 29ca812656549a01 - zh: aacd814d7d649dbf + en: e57b48c7967a316d + zh: 085fe2e57019c01c /agent-note-user-authorized-subagent-model-routes/related-decisions: en: 17fdfabedb673935 zh: 496f4f85622d7d15 diff --git a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md index 3e2ab42e5a..e9043a8ddb 100644 --- a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md +++ b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md @@ -33,7 +33,7 @@ Model selection has no unrestricted static mode. The default-off Host setting au ## Consequences - New adapter registrations and newly advertised models do not expand user authorization. -- Adapter removal or exact-model resolution failure can make discovery unavailable without deleting the saved route decision; an allowed model remains usable when its adapter accepts it even if the advisory catalog omits it. +- Adapter removal can make provider discovery unavailable; provider model listings report exact-model resolution failures per item without discarding other results or deleting the saved route decision; an allowed model remains usable when its adapter accepts it even if the advisory catalog omits it. - The allowlist itself consumes no parent-request tokens. Only a `list_subagent_models` result enters the transcript. - The policy event is log-only and is appended while an Agent is composed, before either SDK begins its run subscription. Shipped SDK profiles do not enable this Web-owned preference, so the event changes neither SDK's expected notifications or persisted-session output; package restore tests own its durable projection instead of fabricating an SDK composition solely to emit it. - Unit coverage pins settings validation, malformed durable values, Session sampling and inheritance, shared definitions, parent-route defaults, discovery restrictions, executor denial, and the settings UI’s staged writes and revision checks. The assembled Web scenario pins the real settings document and Plugins card flow. diff --git a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md index d90bca9358..c2121c1b1c 100644 --- a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md +++ b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md @@ -33,7 +33,7 @@ Host 自有的 `subagent-model-selection` 设置 section 保存显式 `enabled` ## Consequences - 新适配器注册和新公布模型不会扩大用户授权。 -- 适配器移除或精确模型解析失败可能使发现不可用,但不会删除已存路由决定;只要适配器接受某个获准模型,即使建议性目录省略它,该模型仍可使用。 +- 适配器移除可能使提供方发现不可用;提供方模型列表逐项报告精确模型解析失败,不会丢弃其他结果或删除已存路由决定;只要适配器接受某个获准模型,即使建议性目录省略它,该模型仍可使用。 - 允许列表本身不消耗父级请求 token。只有 `list_subagent_models` 结果进入 transcript。 - 策略事件仅存在于日志,并在 Agent 组合期间、两套 SDK 开始订阅运行前追加。随附 SDK profile 不启用这项 Web 自有偏好,因此该事件不会改变任一 SDK 的预期通知或持久 Session 输出;其持久投影由包级恢复测试负责,不会为了发出该事件而虚构 SDK 组合。 - 单元覆盖固定设置校验、异常持久值、Session 取样与继承、共享定义、父级路由默认值、发现限制、执行器拒绝,以及设置 UI 的暂存写入与 revision 检查。组装 Web 场景固定真实设置文档与 Plugins 设置卡流程。 diff --git a/packages/subagent/tool-subagent/README.i18n.yaml b/packages/subagent/tool-subagent/README.i18n.yaml index 41e61ad7ac..adbba294ab 100644 --- a/packages/subagent/tool-subagent/README.i18n.yaml +++ b/packages/subagent/tool-subagent/README.i18n.yaml @@ -66,8 +66,8 @@ en: a4ca31b9cbacda6b zh: 91ac3e0784a14697 /deepseek-ai-dsh-tool-subagent/model-experience/model-selection-and-discovery/what-the-model-sees: - en: 14c12f23474ee1bf - zh: bd803212b3b0ba82 + en: 5bfd2012bcf18ccf + zh: d93155da44a977e1 /deepseek-ai-dsh-tool-subagent/model-experience/model-selection-and-discovery/token-effect: en: cfdfe3b1106c3b57 zh: 3f421f2901d6f7ab diff --git a/packages/subagent/tool-subagent/README.md b/packages/subagent/tool-subagent/README.md index e407e5f7ae..9a6a9bdeeb 100644 --- a/packages/subagent/tool-subagent/README.md +++ b/packages/subagent/tool-subagent/README.md @@ -143,7 +143,7 @@ Prefix-stable while provider instances and their configuration are unchanged. Ad #### What the model sees -The shared `list_subagent_models` definition reads the calling Session’s route policy, falling back to the current parent route when no policy is recorded. Calls reject while the optional `ctx.llm` service is unavailable. Discovery resolves only allowed routes, including exact model ids absent from advisory catalogs; unauthorized provider/model lookups fail before adapter access. Execution independently enforces the same policy. +The shared `list_subagent_models` definition reads the calling Session’s route policy, falling back to the current parent route when no policy is recorded. Calls reject while the optional `ctx.llm` service is unavailable. Discovery resolves only allowed routes, including exact model ids absent from advisory catalogs; unauthorized provider/model lookups fail before adapter access. Provider listings retain resolved models and report lookup errors beside each failed model; exact-model queries and cancellation still reject the call. Execution independently enforces the same policy. #### Token effect diff --git a/packages/subagent/tool-subagent/README.zh.md b/packages/subagent/tool-subagent/README.zh.md index ea0a06a3ba..55790c1c95 100644 --- a/packages/subagent/tool-subagent/README.zh.md +++ b/packages/subagent/tool-subagent/README.zh.md @@ -143,7 +143,7 @@ kind: "package-reference" #### 模型看到什么 -共享的 `list_subagent_models` 定义读取调用 Session 的路由策略,没有已记录策略时使用当前父级路由。可选的 `ctx.llm` 服务不可用时,调用会被拒绝。发现只解析允许的路由,包括建议性目录未列出的精确模型 ID;未授权的提供方/模型查询会在访问适配器前失败。执行过程独立强制同一策略。 +共享的 `list_subagent_models` 定义读取调用 Session 的路由策略,没有已记录策略时使用当前父级路由。可选的 `ctx.llm` 服务不可用时,调用会被拒绝。发现只解析允许的路由,包括建议性目录未列出的精确模型 ID;未授权的提供方/模型查询会在访问适配器前失败。提供方列表保留成功解析的模型,并在每个失败模型旁显示查询错误;精确模型查询失败和取消仍会使调用被拒绝。执行过程独立强制同一策略。 #### Token 影响 diff --git a/packages/subagent/tool-subagent/src/list-models.ts b/packages/subagent/tool-subagent/src/list-models.ts index c642c4a743..72a1bac263 100644 --- a/packages/subagent/tool-subagent/src/list-models.ts +++ b/packages/subagent/tool-subagent/src/list-models.ts @@ -63,8 +63,18 @@ async function listSubagentModels( } const provider = registeredProvider(llm, policy, request.provider) if (request.model === undefined) { - const models = await Promise.all(allowedRoutes.map(route => llm.resolveModelInfo(provider.id, route.model, signal))) - return models.map(model => modelLine(provider.id, model)).join('\n') + const lines = await Promise.all(allowedRoutes.map(async (route) => { + let model + try { + model = await llm.resolveModelInfo(provider.id, route.model, signal) + } catch (error) { + signal.throwIfAborted() + return `${provider.id}/${route.model} — Error: ${error instanceof Error ? error.message : String(error)}` + } + return modelLine(provider.id, model) + })) + signal.throwIfAborted() + return lines.join('\n') } if (request.model.length === 0) throw new Error('`model` must be non-empty') if (!allowedRoutes.some(route => route.model === request.model)) { @@ -80,6 +90,7 @@ async function listSubagentModels( /** * Register `list_subagent_models` in the owning composition. + * Provider listings report lookup failures per model; cancellation rejects the call. * @param ctx - Context whose tool registry owns the fixed discovery definition. * @param policyFor - Resolve the calling Session’s authorized routes. */ diff --git a/packages/subagent/tool-subagent/tests/fixtures/model-discovery.mjs b/packages/subagent/tool-subagent/tests/fixtures/model-discovery.mjs new file mode 100644 index 0000000000..668d0aba5d --- /dev/null +++ b/packages/subagent/tool-subagent/tests/fixtures/model-discovery.mjs @@ -0,0 +1,31 @@ +/** Deterministic metadata lookups for the model-discovery transcript. */ +import { LlmAdapter } from '@deepseek-ai/dsh-llm' + +class DiscoveryAdapter extends LlmAdapter { + async resolveModel(provider, model) { + if (model === 'removed') throw new Error('model was removed') + return { provider, id: model, name: 'Healthy' } + } + + async *stream() { + throw new Error('metadata-only fixture does not serve model requests') + } +} + +export const name = 'discovery-fixture' +export const inject = ['llm'] + +export function apply(ctx) { + ctx.llm.registerAdapter(['discovery-fixture'], new DiscoveryAdapter()) + ctx.on('agent/pre-step', async ({ agent, turn, step }, next) => { + if (turn === 1 && step === 1) { + agent.session.append('subagent/model-selection-policy', { + allowedModels: [ + { provider: 'discovery-fixture', model: 'removed' }, + { provider: 'discovery-fixture', model: 'healthy' }, + ], + }) + } + return next() + }) +} diff --git a/packages/subagent/tool-subagent/tests/list-models.spec.ts b/packages/subagent/tool-subagent/tests/list-models.spec.ts index 43883b48f7..e6d2fd811d 100644 --- a/packages/subagent/tool-subagent/tests/list-models.spec.ts +++ b/packages/subagent/tool-subagent/tests/list-models.spec.ts @@ -90,9 +90,9 @@ async function setupAllowedListTool() { let counter = 0 -function call(ctx: Context, args: unknown) { +function call(ctx: Context, args: unknown, signal = testToolSignal) { return ctx.tools.execute({ - signal: testToolSignal, + signal, callId: ToolCallId(`list-models-${++counter}`), name: 'list_subagent_models', arguments: args, @@ -224,6 +224,58 @@ describe('list_subagent_models', () => { expect(text(result)).toBe('alpha/fast — Fast: Focused work.') }) + it.each([false, true])('reports each failed model alongside resolved models (all failed: %s)', async (allFailed) => { + const ctx = await setupListTool([ + { provider: 'alpha', model: 'removed' }, + ...allFailed ? [] : [{ provider: 'alpha', model: 'plain' }], + { provider: 'alpha', model: 'unavailable' }, + ]) + try { + const adapter = new CatalogAdapter() + vi.spyOn(adapter, 'resolveModel').mockImplementation(async (provider, model) => { + if (model === 'removed') throw new Error('model was removed') + if (model === 'unavailable') return Promise.reject('metadata unavailable') + return { provider, id: model, name: 'Plain' } + }) + ctx.llm.registerAdapter(['alpha'], adapter) + + const result = await call(ctx, { provider: 'alpha' }) + expect(result.isError).toBe(false) + expect(text(result)).toBe([ + 'alpha/removed — Error: model was removed', + ...allFailed ? [] : ['alpha/plain — Plain'], + 'alpha/unavailable — Error: metadata unavailable', + ].join('\n')) + + const exact = await call(ctx, { provider: 'alpha', model: 'removed' }) + expect(exact.isError).toBe(true) + expect(text(exact)).toContain('model was removed') + } finally { + await ctx.fiber.dispose() + } + }) + + it.each([false, true])('does not render cancellation as a model error (adapter rejects: %s)', async (rejects) => { + const ctx = await setupListTool([{ provider: 'alpha', model: 'plain' }]) + try { + const controller = new AbortController() + const adapter = new CatalogAdapter() + vi.spyOn(adapter, 'resolveModel').mockImplementation(async (provider, model) => { + controller.abort(new Error('discovery cancelled')) + if (rejects) throw new Error('lookup interrupted') + return { provider, id: model, name: 'Plain' } + }) + ctx.llm.registerAdapter(['alpha'], adapter) + + const result = await call(ctx, { provider: 'alpha' }, controller.signal) + expect(result.isError).toBe(true) + expect(text(result)).toContain('discovery cancelled') + expect(text(result)).not.toContain('alpha/plain —') + } finally { + await ctx.fiber.dispose() + } + }) + it('inspects exact-model efforts, descriptions, and defaults', async () => { const ctx = await setupListTool() ctx.llm.registerAdapter(['alpha', 'secret'], new CatalogAdapter()) diff --git a/snapshots/session/subagent-model-discovery-errors/runtime.cordis.yml b/snapshots/session/subagent-model-discovery-errors/runtime.cordis.yml new file mode 100644 index 0000000000..3b8eed677f --- /dev/null +++ b/snapshots/session/subagent-model-discovery-errors/runtime.cordis.yml @@ -0,0 +1,3 @@ +- insert: + - id: discovery-fixture + name: '../../../packages/subagent/tool-subagent/tests/fixtures/model-discovery.mjs' diff --git a/snapshots/session/subagent-model-discovery-errors/session.v4.jsonl b/snapshots/session/subagent-model-discovery-errors/session.v4.jsonl new file mode 100644 index 0000000000..869406b413 --- /dev/null +++ b/snapshots/session/subagent-model-discovery-errors/session.v4.jsonl @@ -0,0 +1,23 @@ +{"type":"session","version":4,"id":"{{session:1}}","createdAt":1783352044766,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0} +{"type":"permission/preset","data":{"preset":"danger-full-access"}} +{"type":"sandbox/mode","data":{"mode":"danger-full-access"}} +{"type":"approval/policy","data":{"policy":"never"}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"List the models available for subagents from discovery-fixture, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}} +{"type":"turn/start","data":{"turn":1}} +{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} +{"type":"subagent/model-selection-policy","data":{"allowedModels":[{"provider":"discovery-fixture","model":"removed"},{"provider":"discovery-fixture","model":"healthy"}]}} +{"type":"step/start","data":{"turn":1,"step":1}} +{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"List the models available for subagents from discovery-fixture, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} +{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} +{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} +{"type":"session/title","data":{"title":"List the models available for","messageSeqs":[9],"source":{"kind":"fallback"}}} +{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_discovery","name":"list_subagent_models","arguments":"{\"provider\":\"discovery-fixture\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":100,"outputTokens":10},"stream":[{"type":"chunk","time":1791788384782,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"chunk","time":1791788384782,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_discovery","name":"list_subagent_models","arguments":"{\"provider\":\"discovery-fixture\"}"}}},{"type":"chunk","time":1791788384782,"chunk":{"type":"usage","usage":{"inputTokens":100,"outputTokens":10}}},{"type":"chunk","time":1791788384782,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} +{"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_discovery","name":"list_subagent_models","arguments":"{\"provider\":\"discovery-fixture\"}"}} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_discovery"},"toolCallId":"call_discovery","content":[{"type":"text","text":"discovery-fixture/removed — Error: model was removed\ndiscovery-fixture/healthy — Healthy"}],"isError":false,"id":"{{message:5}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":1}} +{"type":"step/start","data":{"turn":1,"step":2}} +{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":100,"outputTokens":10},"stream":[{"type":"chunk","time":1791788384799,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1791788384800,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1791788384800,"chunk":{"type":"usage","usage":{"inputTokens":100,"outputTokens":10}}},{"type":"chunk","time":1791788384800,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} +{"type":"step/end","data":{"turn":1,"step":2}} +{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/subagent-model-discovery-errors/snapshot.yml b/snapshots/session/subagent-model-discovery-errors/snapshot.yml new file mode 100644 index 0000000000..322ac5fd4f --- /dev/null +++ b/snapshots/session/subagent-model-discovery-errors/snapshot.yml @@ -0,0 +1,7 @@ +version: 1 +scenario: subagent-model-discovery-errors +profile: headless +composition: default +recording: authored +header: + class: default From 080b921b5b9e7af423daf2638fdddca4b0049f11 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Mon, 12 Oct 2026 15:07:11 +0800 Subject: [PATCH 039/369] docs(subagent): clarify exact-model lookup failures --- packages/subagent/tool-subagent/README.i18n.yaml | 2 +- packages/subagent/tool-subagent/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/subagent/tool-subagent/README.i18n.yaml b/packages/subagent/tool-subagent/README.i18n.yaml index adbba294ab..a27bf9f046 100644 --- a/packages/subagent/tool-subagent/README.i18n.yaml +++ b/packages/subagent/tool-subagent/README.i18n.yaml @@ -66,7 +66,7 @@ en: a4ca31b9cbacda6b zh: 91ac3e0784a14697 /deepseek-ai-dsh-tool-subagent/model-experience/model-selection-and-discovery/what-the-model-sees: - en: 5bfd2012bcf18ccf + en: b2bafbccb0e10406 zh: d93155da44a977e1 /deepseek-ai-dsh-tool-subagent/model-experience/model-selection-and-discovery/token-effect: en: cfdfe3b1106c3b57 diff --git a/packages/subagent/tool-subagent/README.md b/packages/subagent/tool-subagent/README.md index 9a6a9bdeeb..a1a2527afb 100644 --- a/packages/subagent/tool-subagent/README.md +++ b/packages/subagent/tool-subagent/README.md @@ -143,7 +143,7 @@ Prefix-stable while provider instances and their configuration are unchanged. Ad #### What the model sees -The shared `list_subagent_models` definition reads the calling Session’s route policy, falling back to the current parent route when no policy is recorded. Calls reject while the optional `ctx.llm` service is unavailable. Discovery resolves only allowed routes, including exact model ids absent from advisory catalogs; unauthorized provider/model lookups fail before adapter access. Provider listings retain resolved models and report lookup errors beside each failed model; exact-model queries and cancellation still reject the call. Execution independently enforces the same policy. +The shared `list_subagent_models` definition reads the calling Session’s route policy, falling back to the current parent route when no policy is recorded. Calls reject while the optional `ctx.llm` service is unavailable. Discovery resolves only allowed routes, including exact model ids absent from advisory catalogs; unauthorized provider/model lookups fail before adapter access. Provider listings retain resolved models and report lookup errors beside each failed model; exact-model lookup failures and cancellation still reject the call. Execution independently enforces the same policy. #### Token effect From 0948cd9809cba95464eaedc93b186ef5a41d2810 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Mon, 12 Oct 2026 15:14:22 +0800 Subject: [PATCH 040/369] test(subagent): document non-Error adapter rejection fixture --- packages/subagent/tool-subagent/tests/list-models.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/subagent/tool-subagent/tests/list-models.spec.ts b/packages/subagent/tool-subagent/tests/list-models.spec.ts index e6d2fd811d..d5b7642660 100644 --- a/packages/subagent/tool-subagent/tests/list-models.spec.ts +++ b/packages/subagent/tool-subagent/tests/list-models.spec.ts @@ -234,6 +234,7 @@ describe('list_subagent_models', () => { const adapter = new CatalogAdapter() vi.spyOn(adapter, 'resolveModel').mockImplementation(async (provider, model) => { if (model === 'removed') throw new Error('model was removed') + // oxlint-disable-next-line typescript/prefer-promise-reject-errors -- Exercise non-Error rejections from an external adapter. if (model === 'unavailable') return Promise.reject('metadata unavailable') return { provider, id: model, name: 'Plain' } }) From 7609189262a8915f0489fc08c846085d5d3ea5cf Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Mon, 12 Oct 2026 15:27:02 +0800 Subject: [PATCH 041/369] fix(subagent): intersect model discovery with authorized catalog --- ...authorized-subagent-model-routes.i18n.yaml | 8 +- ...4-user-authorized-subagent-model-routes.md | 4 +- ...ser-authorized-subagent-model-routes.zh.md | 4 +- .../subagent/tool-subagent/README.i18n.yaml | 4 +- packages/subagent/tool-subagent/README.md | 2 +- packages/subagent/tool-subagent/README.zh.md | 2 +- .../subagent/tool-subagent/src/list-models.ts | 49 +++++------ .../tests/fixtures/model-discovery.mjs | 7 ++ .../tool-subagent/tests/list-models.spec.ts | 84 +++++++++++++------ .../session.v4.jsonl | 2 +- 10 files changed, 102 insertions(+), 64 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml index d9816249cd..cd7f0e6dca 100644 --- a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml @@ -9,14 +9,14 @@ en: 3238dfb7db7dfdd8 zh: ec3bdd9a9da0a321 /agent-note-user-authorized-subagent-model-routes/decision: - en: 499721e05ad15d9c - zh: 2bcf5ade8910da8e + en: 47b020441723cc06 + zh: 8394c32b711a0b30 /agent-note-user-authorized-subagent-model-routes/alternatives-considered: en: e41c17226190ea8b zh: d3b1395fafe75f01 /agent-note-user-authorized-subagent-model-routes/consequences: - en: e57b48c7967a316d - zh: 085fe2e57019c01c + en: 1749cfbfda849692 + zh: 9925e638cd72fba6 /agent-note-user-authorized-subagent-model-routes/related-decisions: en: 17fdfabedb673935 zh: 496f4f85622d7d15 diff --git a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md index e9043a8ddb..aa0de7fad7 100644 --- a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md +++ b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md @@ -14,7 +14,7 @@ The Host-owned `subagent-model-selection` settings section stores an explicit `e A newly composed top-level Session snapshots the route list in `subagent/model-selection-policy` when the setting is enabled, before its first model request. Event presence means selection was enabled; the event does not store the global switch. Child Sessions inherit that exact list from their live parent, and resumed Sessions use the recorded event instead of current settings. Settings changes therefore affect only subsequently composed top-level Sessions. Without the event, including an explicitly empty restored Session, explicit selection remains restricted to the current parent route. -The fixed `list_subagent_models` schema does not enumerate the policy. At call time, it lists authorized registered providers and resolves their allowed exact model ids, including ids absent from advisory catalogs. Exact lookup requires authorization before resolving model metadata and all supported reasoning efforts. Without a recorded policy, discovery uses the parent Agent’s current effective route. The delegation executor independently checks any explicit provider, model, or effort selection against the same authorization before adapter preflight. A call with no selection fields retains configured or inherited routing. +The fixed `list_subagent_models` schema does not enumerate the policy. At call time, it lists authorized registered providers and intersects their advertised model catalogs with the recorded route list. Exact lookup requires authorization before resolving model metadata and all supported reasoning efforts, including for ids absent from the catalog. Without a recorded policy, discovery resolves only the parent Agent’s current effective route, even if the catalog omits it. The delegation executor independently checks any explicit provider, model, or effort selection against the same authorization before adapter preflight. A call with no selection fields retains configured or inherited routing. Model selection has no unrestricted static mode. The default-off Host setting authorizes alternative child routes; without it, the parent route and its supported efforts remain selectable. The primary spawn tool captures the setting once, while all capable delegation tools and shared discovery read the same Session policy. Fork defaults preserve the parent route, and its description warns about prefix recomputation when that route changes. @@ -33,7 +33,7 @@ Model selection has no unrestricted static mode. The default-off Host setting au ## Consequences - New adapter registrations and newly advertised models do not expand user authorization. -- Adapter removal can make provider discovery unavailable; provider model listings report exact-model resolution failures per item without discarding other results or deleting the saved route decision; an allowed model remains usable when its adapter accepts it even if the advisory catalog omits it. +- Adapter removal or catalog lookup failure can make provider discovery unavailable without deleting the saved route decision; an allowed model remains usable when its adapter accepts it even if the advisory catalog omits it. - The allowlist itself consumes no parent-request tokens. Only a `list_subagent_models` result enters the transcript. - The policy event is log-only and is appended while an Agent is composed, before either SDK begins its run subscription. Shipped SDK profiles do not enable this Web-owned preference, so the event changes neither SDK's expected notifications or persisted-session output; package restore tests own its durable projection instead of fabricating an SDK composition solely to emit it. - Unit coverage pins settings validation, malformed durable values, Session sampling and inheritance, shared definitions, parent-route defaults, discovery restrictions, executor denial, and the settings UI’s staged writes and revision checks. The assembled Web scenario pins the real settings document and Plugins card flow. diff --git a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md index c2121c1b1c..c157cec8a7 100644 --- a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md +++ b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md @@ -14,7 +14,7 @@ Host 自有的 `subagent-model-selection` 设置 section 保存显式 `enabled` 设置启用时,新组合的顶层 Session 在首个模型请求前将路由列表记录为 `subagent/model-selection-policy`。事件存在表示选择已启用;事件不保存全局开关。子 Session 从在线父级继承同一精确列表,恢复的 Session 使用已记录事件而非当前设置。因此设置变更只影响之后组合的顶层 Session。没有该事件时,包括显式为空的恢复 Session,显式选择仍仅限当前父级路由。 -固定的 `list_subagent_models` schema 不枚举策略。调用时,它列出已授权且已注册的提供方,并解析允许的精确模型 ID,包括建议性目录未列出的 ID。精确查询先要求授权,再解析模型元数据及全部支持的推理强度。没有已记录策略时,发现使用父 Agent 当前生效的路由。委派执行器会在适配器预检前独立校验显式提供方、模型或强度选择是否符合相同授权。没有选择字段的调用保留配置或继承的路由。 +固定的 `list_subagent_models` schema 不枚举策略。调用时,它列出已授权且已注册的提供方,并取其公布模型目录与已记录路由列表的交集。精确查询先要求授权,再解析模型元数据及全部支持的推理强度,也允许目录未列出的 ID。没有已记录策略时,发现只解析父 Agent 当前生效的路由,即使目录未列出它。委派执行器会在适配器预检前独立校验显式提供方、模型或强度选择是否符合相同授权。没有选择字段的调用保留配置或继承的路由。 模型选择没有无限制的静态模式。默认关闭的 Host 设置授权其他子级路由;没有该授权时,仍可选择父级路由及其支持的强度。主 spawn 工具一次性记录设置,所有支持选择的委派工具和共享发现均读取同一 Session 策略。fork 默认保留父级路由,其描述会提示改变路由时的前缀重算成本。 @@ -33,7 +33,7 @@ Host 自有的 `subagent-model-selection` 设置 section 保存显式 `enabled` ## Consequences - 新适配器注册和新公布模型不会扩大用户授权。 -- 适配器移除可能使提供方发现不可用;提供方模型列表逐项报告精确模型解析失败,不会丢弃其他结果或删除已存路由决定;只要适配器接受某个获准模型,即使建议性目录省略它,该模型仍可使用。 +- 适配器移除或目录查询失败可能使提供方发现不可用,但不会删除已存路由决定;只要适配器接受某个获准模型,即使建议性目录省略它,该模型仍可使用。 - 允许列表本身不消耗父级请求 token。只有 `list_subagent_models` 结果进入 transcript。 - 策略事件仅存在于日志,并在 Agent 组合期间、两套 SDK 开始订阅运行前追加。随附 SDK profile 不启用这项 Web 自有偏好,因此该事件不会改变任一 SDK 的预期通知或持久 Session 输出;其持久投影由包级恢复测试负责,不会为了发出该事件而虚构 SDK 组合。 - 单元覆盖固定设置校验、异常持久值、Session 取样与继承、共享定义、父级路由默认值、发现限制、执行器拒绝,以及设置 UI 的暂存写入与 revision 检查。组装 Web 场景固定真实设置文档与 Plugins 设置卡流程。 diff --git a/packages/subagent/tool-subagent/README.i18n.yaml b/packages/subagent/tool-subagent/README.i18n.yaml index a27bf9f046..e59a887fdd 100644 --- a/packages/subagent/tool-subagent/README.i18n.yaml +++ b/packages/subagent/tool-subagent/README.i18n.yaml @@ -66,8 +66,8 @@ en: a4ca31b9cbacda6b zh: 91ac3e0784a14697 /deepseek-ai-dsh-tool-subagent/model-experience/model-selection-and-discovery/what-the-model-sees: - en: b2bafbccb0e10406 - zh: d93155da44a977e1 + en: be3c5b647252ddc7 + zh: efecd46ed7dd9a95 /deepseek-ai-dsh-tool-subagent/model-experience/model-selection-and-discovery/token-effect: en: cfdfe3b1106c3b57 zh: 3f421f2901d6f7ab diff --git a/packages/subagent/tool-subagent/README.md b/packages/subagent/tool-subagent/README.md index a1a2527afb..96dcfabfe6 100644 --- a/packages/subagent/tool-subagent/README.md +++ b/packages/subagent/tool-subagent/README.md @@ -143,7 +143,7 @@ Prefix-stable while provider instances and their configuration are unchanged. Ad #### What the model sees -The shared `list_subagent_models` definition reads the calling Session’s route policy, falling back to the current parent route when no policy is recorded. Calls reject while the optional `ctx.llm` service is unavailable. Discovery resolves only allowed routes, including exact model ids absent from advisory catalogs; unauthorized provider/model lookups fail before adapter access. Provider listings retain resolved models and report lookup errors beside each failed model; exact-model lookup failures and cancellation still reject the call. Execution independently enforces the same policy. +The shared `list_subagent_models` definition reads the calling Session’s route policy, falling back to the current parent route when no policy is recorded. Calls reject while the optional `ctx.llm` service is unavailable. With a recorded policy, provider model lists intersect the advertised catalog with the Session’s authorized routes. Without one, they resolve only the current parent model, even if the catalog omits it. Exact-model queries accept authorized ids absent from the catalog; unauthorized provider/model lookups fail before adapter access. Catalog or exact-model lookup failures and cancellation reject the call. Execution independently enforces the same policy. #### Token effect diff --git a/packages/subagent/tool-subagent/README.zh.md b/packages/subagent/tool-subagent/README.zh.md index 55790c1c95..0bf8ee903f 100644 --- a/packages/subagent/tool-subagent/README.zh.md +++ b/packages/subagent/tool-subagent/README.zh.md @@ -143,7 +143,7 @@ kind: "package-reference" #### 模型看到什么 -共享的 `list_subagent_models` 定义读取调用 Session 的路由策略,没有已记录策略时使用当前父级路由。可选的 `ctx.llm` 服务不可用时,调用会被拒绝。发现只解析允许的路由,包括建议性目录未列出的精确模型 ID;未授权的提供方/模型查询会在访问适配器前失败。提供方列表保留成功解析的模型,并在每个失败模型旁显示查询错误;精确模型查询失败和取消仍会使调用被拒绝。执行过程独立强制同一策略。 +共享的 `list_subagent_models` 定义读取调用 Session 的路由策略,没有已记录策略时使用当前父级路由。可选的 `ctx.llm` 服务不可用时,调用会被拒绝。有已记录策略时,提供方模型列表取公布目录与 Session 授权路由的交集。没有策略时,只解析当前父级模型,即使目录未列出它。精确模型查询允许已授权但目录未列出的 ID;未授权的提供方/模型查询会在访问适配器前失败。目录或精确模型查询失败以及取消都会使调用被拒绝。执行过程独立强制同一策略。 #### Token 影响 diff --git a/packages/subagent/tool-subagent/src/list-models.ts b/packages/subagent/tool-subagent/src/list-models.ts index 72a1bac263..950cf09e72 100644 --- a/packages/subagent/tool-subagent/src/list-models.ts +++ b/packages/subagent/tool-subagent/src/list-models.ts @@ -38,10 +38,20 @@ function modelLine(provider: string, model: { id: string; name: string; descript /** Read the requested provider, allowed models, or exact-model efforts. */ async function listSubagentModels( ctx: Context, - policy: ModelSelectionPolicy, + policy: ModelSelectionPolicy | undefined, request: ListSubagentModelsRequest, - signal: AbortSignal, + exec: ToolRunContext, ): Promise { + const selectionEnabled = policy !== undefined + if (policy === undefined) { + if (exec.agent === undefined) throw new Error('list_subagent_models requires a calling agent') + const { provider, model } = parentAgentOptionsForDelegation(exec.agent) + if (provider === undefined || model === undefined) { + throw new Error('cannot discover child LLM routes without a parent provider and model') + } + policy = { routes: [{ provider, model }] } + } + const { signal } = exec const llm = ctx.get('llm') if (llm === undefined) { throw new Error('cannot discover child LLM routes because the `llm` service is unavailable') @@ -58,23 +68,19 @@ async function listSubagentModels( } if (request.provider.length === 0) throw new Error('`provider` must be non-empty') const allowedRoutes = policy.routes.filter(route => route.provider === request.provider) - if (allowedRoutes.length === 0) { + const [firstRoute] = allowedRoutes + if (firstRoute === undefined) { throw new Error(`LLM provider "${request.provider}" is not allowed for this Session`) } const provider = registeredProvider(llm, policy, request.provider) if (request.model === undefined) { - const lines = await Promise.all(allowedRoutes.map(async (route) => { - let model - try { - model = await llm.resolveModelInfo(provider.id, route.model, signal) - } catch (error) { - signal.throwIfAborted() - return `${provider.id}/${route.model} — Error: ${error instanceof Error ? error.message : String(error)}` - } - return modelLine(provider.id, model) - })) + const models = selectionEnabled + ? (await llm.listModels(provider.id)).filter(model => allowedRoutes.some(route => route.model === model.id)) + : [await llm.resolveModelInfo(provider.id, firstRoute.model, signal)] signal.throwIfAborted() - return lines.join('\n') + return models.length === 0 + ? `(no advertised models for ${provider.id})` + : models.map(model => modelLine(provider.id, model)).join('\n') } if (request.model.length === 0) throw new Error('`model` must be non-empty') if (!allowedRoutes.some(route => route.model === request.model)) { @@ -90,11 +96,11 @@ async function listSubagentModels( /** * Register `list_subagent_models` in the owning composition. - * Provider listings report lookup failures per model; cancellation rejects the call. + * Provider listings intersect the catalog with authorization, or resolve only the parent model when selection is disabled. * @param ctx - Context whose tool registry owns the fixed discovery definition. - * @param policyFor - Resolve the calling Session’s authorized routes. + * @param policyFor - Read the calling Session’s authorized routes; undefined restricts discovery to its parent model. */ -export function registerListSubagentModels(ctx: Context, policyFor: (exec: ToolRunContext) => ModelSelectionPolicy): void { +export function registerListSubagentModels(ctx: Context, policyFor: (exec: ToolRunContext) => ModelSelectionPolicy | undefined): void { ctx.tools.register(defineTool({ name: 'list_subagent_models', description: @@ -118,7 +124,7 @@ export function registerListSubagentModels(ctx: Context, policyFor: (exec: ToolR render: (_args, result) => [{ type: 'text', text: result }], }, execute(args, exec) { - return listSubagentModels(ctx, policyFor(exec), args, exec.signal) + return listSubagentModels(ctx, policyFor(exec), args, exec) }, })) } @@ -137,11 +143,6 @@ export function apply(ctx: Context): void { registerListSubagentModels(ctx, (exec) => { if (exec.agent === undefined) throw new Error('list_subagent_models requires a calling agent') const routes = subagentModelSelectionPolicy(ctx.sessionProjections, exec.agent.session) - if (routes !== undefined) return { routes } - const { provider, model } = parentAgentOptionsForDelegation(exec.agent) - if (provider === undefined || model === undefined) { - throw new Error('cannot discover child LLM routes without a parent provider and model') - } - return { routes: [{ provider, model }] } + return routes === undefined ? undefined : { routes } }) } diff --git a/packages/subagent/tool-subagent/tests/fixtures/model-discovery.mjs b/packages/subagent/tool-subagent/tests/fixtures/model-discovery.mjs index 668d0aba5d..871ddc0ffc 100644 --- a/packages/subagent/tool-subagent/tests/fixtures/model-discovery.mjs +++ b/packages/subagent/tool-subagent/tests/fixtures/model-discovery.mjs @@ -2,6 +2,13 @@ import { LlmAdapter } from '@deepseek-ai/dsh-llm' class DiscoveryAdapter extends LlmAdapter { + async listModels(provider) { + return [ + { provider, id: 'healthy', name: 'Healthy' }, + { provider, id: 'unauthorized', name: 'Unauthorized' }, + ] + } + async resolveModel(provider, model) { if (model === 'removed') throw new Error('model was removed') return { provider, id: model, name: 'Healthy' } diff --git a/packages/subagent/tool-subagent/tests/list-models.spec.ts b/packages/subagent/tool-subagent/tests/list-models.spec.ts index d5b7642660..00dbca7238 100644 --- a/packages/subagent/tool-subagent/tests/list-models.spec.ts +++ b/packages/subagent/tool-subagent/tests/list-models.spec.ts @@ -194,7 +194,7 @@ describe('list_subagent_models', () => { ctx.llm.registerAdapter(['alpha', 'beta'], new CatalogAdapter()) expect(text(await call(ctx, {}))).toBe('alpha — ALPHA API') - expect(text(await call(ctx, { provider: 'alpha' }))).toBe('alpha/fast — Fast: Focused work.\nalpha/unlisted — Fast: Focused work.') + expect(text(await call(ctx, { provider: 'alpha' }))).toBe('alpha/fast — Fast: Focused work.') expect(text(await call(ctx, { provider: 'alpha', model: 'unlisted' }))) .toContain('alpha/unlisted — Fast') @@ -216,62 +216,92 @@ describe('list_subagent_models', () => { expect(listModels).not.toHaveBeenCalled() }) - it('resolves an allowed model even when the advertised catalog is empty', async () => { - const ctx = await setupListTool([{ provider: 'alpha', model: 'fast' }]) - ctx.llm.registerAdapter(['alpha'], new CatalogAdapter(true)) + it('returns an empty list when the catalog contains no authorized model', async () => { + const ctx = await setupListTool([{ provider: 'alpha', model: 'unlisted' }]) + ctx.llm.registerAdapter(['alpha'], new CatalogAdapter()) const result = await call(ctx, { provider: 'alpha' }) expect(result.isError).toBe(false) - expect(text(result)).toBe('alpha/fast — Fast: Focused work.') + expect(text(result)).toBe('(no advertised models for alpha)') }) - it.each([false, true])('reports each failed model alongside resolved models (all failed: %s)', async (allFailed) => { + it('lists authorized catalog entries without resolving removed or unlisted models', async () => { const ctx = await setupListTool([ { provider: 'alpha', model: 'removed' }, - ...allFailed ? [] : [{ provider: 'alpha', model: 'plain' }], - { provider: 'alpha', model: 'unavailable' }, + { provider: 'alpha', model: 'plain' }, ]) try { const adapter = new CatalogAdapter() - vi.spyOn(adapter, 'resolveModel').mockImplementation(async (provider, model) => { - if (model === 'removed') throw new Error('model was removed') - // oxlint-disable-next-line typescript/prefer-promise-reject-errors -- Exercise non-Error rejections from an external adapter. - if (model === 'unavailable') return Promise.reject('metadata unavailable') - return { provider, id: model, name: 'Plain' } - }) + const resolveModel = vi.spyOn(adapter, 'resolveModel').mockRejectedValue(new Error('lookup unavailable')) ctx.llm.registerAdapter(['alpha'], adapter) const result = await call(ctx, { provider: 'alpha' }) expect(result.isError).toBe(false) - expect(text(result)).toBe([ - 'alpha/removed — Error: model was removed', - ...allFailed ? [] : ['alpha/plain — Plain'], - 'alpha/unavailable — Error: metadata unavailable', - ].join('\n')) + expect(text(result)).toBe('alpha/plain — Plain') + expect(resolveModel).not.toHaveBeenCalled() const exact = await call(ctx, { provider: 'alpha', model: 'removed' }) expect(exact.isError).toBe(true) - expect(text(exact)).toContain('model was removed') + expect(text(exact)).toContain('lookup unavailable') } finally { await ctx.fiber.dispose() } }) - it.each([false, true])('does not render cancellation as a model error (adapter rejects: %s)', async (rejects) => { - const ctx = await setupListTool([{ provider: 'alpha', model: 'plain' }]) + it('rejects a failed catalog lookup', async () => { + const ctx = await setupListTool() + try { + const adapter = new CatalogAdapter() + vi.spyOn(adapter, 'listModels').mockRejectedValue(new Error('catalog unavailable')) + ctx.llm.registerAdapter(['alpha'], adapter) + const result = await call(ctx, { provider: 'alpha' }) + expect(result.isError).toBe(true) + expect(text(result)).toContain('catalog unavailable') + } finally { + await ctx.fiber.dispose() + } + }) + + it('rejects cancellation after a catalog lookup', async () => { + const ctx = await setupListTool() try { const controller = new AbortController() const adapter = new CatalogAdapter() - vi.spyOn(adapter, 'resolveModel').mockImplementation(async (provider, model) => { + vi.spyOn(adapter, 'listModels').mockImplementation(async () => { controller.abort(new Error('discovery cancelled')) - if (rejects) throw new Error('lookup interrupted') - return { provider, id: model, name: 'Plain' } + return [] }) ctx.llm.registerAdapter(['alpha'], adapter) - const result = await call(ctx, { provider: 'alpha' }, controller.signal) expect(result.isError).toBe(true) expect(text(result)).toContain('discovery cancelled') - expect(text(result)).not.toContain('alpha/plain —') + } finally { + await ctx.fiber.dispose() + } + }) + + it.each([false, true])('resolves only the current parent model when selection is disabled (empty catalog: %s)', async (empty) => { + const ctx = await setup({ provider: 'mock', parentAgentOptions: { provider: 'alpha', model: 'unlisted' } }) + const adapter = new CatalogAdapter(empty) + const listModels = vi.spyOn(adapter, 'listModels') + ctx.llm.registerAdapter(['alpha'], adapter) + const result = await ctx.tools.execute({ + signal: testToolSignal, callId: ToolCallId('parent-only-discovery'), + name: 'list_subagent_models', arguments: { provider: 'alpha' }, agent: modelSelectionSetupAgent(ctx), + }) + expect(result.isError).toBe(false) + expect(text(result)).toBe('alpha/unlisted — Fast: Focused work.') + expect(listModels).not.toHaveBeenCalled() + }) + + it('requires a calling Agent when a discovery registration supplies no policy', async () => { + const ctx = new Context() + try { + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRuntime) + registerListSubagentModels(ctx, () => undefined) + const result = await call(ctx, {}) + expect(result.isError).toBe(true) + expect(text(result)).toContain('requires a calling agent') } finally { await ctx.fiber.dispose() } diff --git a/snapshots/session/subagent-model-discovery-errors/session.v4.jsonl b/snapshots/session/subagent-model-discovery-errors/session.v4.jsonl index 869406b413..1a781e4739 100644 --- a/snapshots/session/subagent-model-discovery-errors/session.v4.jsonl +++ b/snapshots/session/subagent-model-discovery-errors/session.v4.jsonl @@ -15,7 +15,7 @@ {"type":"session/title","data":{"title":"List the models available for","messageSeqs":[9],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_discovery","name":"list_subagent_models","arguments":"{\"provider\":\"discovery-fixture\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":100,"outputTokens":10},"stream":[{"type":"chunk","time":1791788384782,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"chunk","time":1791788384782,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_discovery","name":"list_subagent_models","arguments":"{\"provider\":\"discovery-fixture\"}"}}},{"type":"chunk","time":1791788384782,"chunk":{"type":"usage","usage":{"inputTokens":100,"outputTokens":10}}},{"type":"chunk","time":1791788384782,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_discovery","name":"list_subagent_models","arguments":"{\"provider\":\"discovery-fixture\"}"}} -{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_discovery"},"toolCallId":"call_discovery","content":[{"type":"text","text":"discovery-fixture/removed — Error: model was removed\ndiscovery-fixture/healthy — Healthy"}],"isError":false,"id":"{{message:5}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"} +{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_discovery"},"toolCallId":"call_discovery","content":[{"type":"text","text":"discovery-fixture/healthy — Healthy"}],"isError":false,"id":"{{message:5}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":100,"outputTokens":10},"stream":[{"type":"chunk","time":1791788384799,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1791788384800,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1791788384800,"chunk":{"type":"usage","usage":{"inputTokens":100,"outputTokens":10}}},{"type":"chunk","time":1791788384800,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} From 5705bf12f0e176b80f307dfef6dfde08590a0293 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Mon, 12 Oct 2026 15:41:37 +0800 Subject: [PATCH 042/369] refactor(agent-instructions): remove unused source type re-export --- packages/context/agent-instructions/README.i18n.yaml | 4 ++-- packages/context/agent-instructions/README.md | 2 +- packages/context/agent-instructions/README.zh.md | 2 +- packages/context/agent-instructions/src/index.ts | 1 - 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/context/agent-instructions/README.i18n.yaml b/packages/context/agent-instructions/README.i18n.yaml index 818bcda27b..9fc0cb6eae 100644 --- a/packages/context/agent-instructions/README.i18n.yaml +++ b/packages/context/agent-instructions/README.i18n.yaml @@ -30,8 +30,8 @@ en: 724df7ff6d1d9fea zh: 91fb1013eb9178ec /deepseek-ai-dsh-agent-instructions/understand-the-implementation/design-concept: - en: 566f3518b2d1d100 - zh: c7d0a22b19e7697a + en: 4708b51bf8fe86f7 + zh: 869fc4b1f78383ee /deepseek-ai-dsh-agent-instructions/understand-the-implementation/source-map: en: cb4c1f929ef5a835 zh: 3f0fcb7f568a0a5c diff --git a/packages/context/agent-instructions/README.md b/packages/context/agent-instructions/README.md index e935cedf8d..25c48ba1ac 100644 --- a/packages/context/agent-instructions/README.md +++ b/packages/context/agent-instructions/README.md @@ -85,7 +85,7 @@ This section explains the design decisions behind the plugin; the observable beh ### Design concept -The plugin is built on one principle: workspace instructions are durable conversation content, owned per agent and per session. Baseline and refresh messages are ordinary sourced `user/message` events, so they replay, compact, and resume exactly like other history, and model-visible state is always reconstructable from the session log. The plugin owns the complete `` framing and every injected message reaches the model verbatim. The package exports `AgentInstructionSource` for typed consumers of these message sources. +The plugin is built on one principle: workspace instructions are durable conversation content, owned per agent and per session. Baseline and refresh messages are ordinary sourced `user/message` events, so they replay, compact, and resume exactly like other history, and model-visible state is always reconstructable from the session log. The plugin owns the complete `` framing and every injected message reaches the model verbatim. ### Source map diff --git a/packages/context/agent-instructions/README.zh.md b/packages/context/agent-instructions/README.zh.md index cf193f2a28..a87cfae53a 100644 --- a/packages/context/agent-instructions/README.zh.md +++ b/packages/context/agent-instructions/README.zh.md @@ -85,7 +85,7 @@ export interface Config { ### 设计理念 -该插件建立在一个原则上:工作区指令是持久的对话内容,按 agent 与会话分别归属。基线消息与刷新消息都是普通的带来源 `user/message` 事件,因此与其他历史一样可回放、可压缩(compaction)、可恢复,模型可见状态总能从会话日志重建。插件拥有完整的 `` 框架,每条注入消息都原样到达模型。 本包导出 `AgentInstructionSource`,供这些消息来源的消费者进行类型检查。 +该插件建立在一个原则上:工作区指令是持久的对话内容,按 agent 与会话分别归属。基线消息与刷新消息都是普通的带来源 `user/message` 事件,因此与其他历史一样可回放、可压缩(compaction)、可恢复,模型可见状态总能从会话日志重建。插件拥有完整的 `` 框架,每条注入消息都原样到达模型。 ### 源码地图 diff --git a/packages/context/agent-instructions/src/index.ts b/packages/context/agent-instructions/src/index.ts index b32a5e6bb7..31070a9102 100644 --- a/packages/context/agent-instructions/src/index.ts +++ b/packages/context/agent-instructions/src/index.ts @@ -33,7 +33,6 @@ import { instructionCandidateGroup, instructionScopeKey, scopeForDisplayPath, ty export { Config } export { name } from './state.ts' -export type { AgentInstructionSource } from './state.ts' export { discoverBaselineInstructionFiles, loadBaselineInstructions, From 155ab69fae1e2716e09f5bcb51eb30f7d61058ca Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Mon, 12 Oct 2026 16:29:01 +0800 Subject: [PATCH 043/369] fix(subagent): preserve fork model selection restriction --- ...authorized-subagent-model-routes.i18n.yaml | 8 ++-- ...4-user-authorized-subagent-model-routes.md | 4 +- ...ser-authorized-subagent-model-routes.zh.md | 4 +- apps/cli/tests/web-agent-presets.e2e.ts | 4 +- docs/tool-catalog.i18n.yaml | 8 ++-- docs/tool-catalog.md | 4 +- docs/tool-catalog.zh.md | 4 +- packages/bundle/base/cordis.patch.yml | 2 +- .../subagent/tool-subagent/README.i18n.yaml | 20 ++++----- packages/subagent/tool-subagent/README.md | 10 ++--- packages/subagent/tool-subagent/README.zh.md | 10 ++--- packages/subagent/tool-subagent/src/index.ts | 10 +---- .../tests/model-selection.spec.ts | 26 ++++++++++-- .../tool-subagent/tests/tool-subagent.spec.ts | 2 +- scripts/gen-tool-catalog.ts | 2 +- .../sdk/bash-tool/tool-schemas.expected.json | 14 +------ .../tool-schemas.1.expected.json | 14 +------ .../tool-schemas.1.expected.json | 14 +------ .../tool-schemas.1.expected.json | 14 +------ .../tool-schemas.expected.json | 14 +------ .../tool-schemas.1.expected.json | 14 +------ .../tool-schemas.expected.json | 14 +------ .../tool-schemas.1.expected.json | 14 +------ .../tool-schemas.1.expected.json | 14 +------ .../sdk/text-turn/tool-schemas.expected.json | 14 +------ .../system-prompt.expected.md | 8 +--- .../tool-schemas.expected.json | 14 +------ .../system-prompt.expected.md | 8 +--- .../tool-schemas.expected.json | 28 +------------ .../tool-schemas.expected.json | 28 +------------ .../tool-schemas.expected.json | 14 +------ .../both-mode-turn/system-prompt.expected.md | 8 +--- .../tool-schemas.expected.json | 14 +------ .../tool-schemas.expected.json | 14 +------ .../tool-schemas.expected.json | 14 +------ .../tool-schemas.expected.json | 14 +------ .../cordis.snapshot.yml | 2 +- .../compaction-output-reserve/cordis.yml | 2 +- .../session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 28 +------------ .../cordis.snapshot.yml | 2 +- .../compaction-summary-headroom/cordis.yml | 2 +- .../session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 14 +------ .../tool-schemas.expected.json | 14 +------ .../tool-schemas.expected.json | 14 +------ .../tool-schemas.expected.json | 14 +------ .../tool-schemas.expected.json | 14 +------ .../tool-schemas.expected.json | 42 ++----------------- .../tool-schemas.expected.json | 14 +------ .../tool-schemas.expected.json | 14 +------ .../lsp-definition/tool-schemas.expected.json | 14 +------ .../system-prompt.expected.md | 8 +--- .../mcp-resources/tool-schemas.expected.json | 14 +------ .../tool-schemas.expected.json | 14 +------ .../tool-schemas.expected.json | 14 +------ .../system-prompt.expected.md | 8 +--- .../office-skills/tool-schemas.expected.json | 14 +------ .../tool-schemas.expected.json | 14 +------ .../tool-schemas.expected.json | 28 +------------ .../plugin-manager/tool-schemas.expected.json | 14 +------ .../tool-schemas.expected.json | 14 +------ .../tool-schemas.expected.json | 14 +------ .../tool-schemas.expected.json | 14 +------ .../provider-cwd/tool-schemas.expected.json | 14 +------ .../system-prompt.expected.md | 8 +--- .../system-prompt.expected.md | 8 +--- .../ptc-persistent/system-prompt.expected.md | 8 +--- .../system-prompt.expected.md | 8 +--- .../ptc-python-turn/system-prompt.expected.md | 8 +--- .../ptc-read-image/system-prompt.expected.md | 8 +--- .../ptc-turn/system-prompt.expected.md | 8 +--- .../tool-schemas.expected.json | 14 +------ .../pwsh-tool-turn/tool-schemas.expected.json | 14 +------ .../ralph-loop/tool-schemas.1.expected.json | 14 +------ .../ralph-loop/tool-schemas.2.expected.json | 14 +------ .../ralph-loop/tool-schemas.expected.json | 14 +------ .../tool-schemas.expected.json | 14 +------ .../tool-schemas.expected.json | 14 +------ .../tool-schemas.expected.json | 14 +------ .../tool-schemas.1.expected.json | 14 +------ .../tool-schemas.1.expected.json | 14 +------ .../tool-schemas.expected.json | 14 +------ .../tool-schemas.1.expected.json | 14 +------ .../tool-schemas.expected.json | 14 +------ .../text-turn/tool-schemas.expected.json | 14 +------ .../web-fetch/tool-schemas.expected.json | 14 +------ .../tool-schemas.expected.json | 14 +------ .../tool-schemas.expected.json | 14 +------ .../worktree/tool-schemas.expected.json | 14 +------ .../tool-schemas.expected.json | 14 +------ .../system-prompt.expected.md | 8 +--- .../system-prompt.expected.md | 8 +--- .../web/ptc-round/system-prompt.expected.md | 8 +--- 94 files changed, 152 insertions(+), 994 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml index cd7f0e6dca..2592a568ad 100644 --- a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml @@ -9,8 +9,8 @@ en: 3238dfb7db7dfdd8 zh: ec3bdd9a9da0a321 /agent-note-user-authorized-subagent-model-routes/decision: - en: 47b020441723cc06 - zh: 8394c32b711a0b30 + en: eba9e531eb6c450b + zh: 7c0673644e83608a /agent-note-user-authorized-subagent-model-routes/alternatives-considered: en: e41c17226190ea8b zh: d3b1395fafe75f01 @@ -18,5 +18,5 @@ en: 1749cfbfda849692 zh: 9925e638cd72fba6 /agent-note-user-authorized-subagent-model-routes/related-decisions: - en: 17fdfabedb673935 - zh: 496f4f85622d7d15 + en: 0eead87ec1172d81 + zh: 4e776a583f5a00a5 diff --git a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md index aa0de7fad7..a91f0e9e47 100644 --- a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md +++ b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md @@ -16,7 +16,7 @@ A newly composed top-level Session snapshots the route list in `subagent/model-s The fixed `list_subagent_models` schema does not enumerate the policy. At call time, it lists authorized registered providers and intersects their advertised model catalogs with the recorded route list. Exact lookup requires authorization before resolving model metadata and all supported reasoning efforts, including for ids absent from the catalog. Without a recorded policy, discovery resolves only the parent Agent’s current effective route, even if the catalog omits it. The delegation executor independently checks any explicit provider, model, or effort selection against the same authorization before adapter preflight. A call with no selection fields retains configured or inherited routing. -Model selection has no unrestricted static mode. The default-off Host setting authorizes alternative child routes; without it, the parent route and its supported efforts remain selectable. The primary spawn tool captures the setting once, while all capable delegation tools and shared discovery read the same Session policy. Fork defaults preserve the parent route, and its description warns about prefix recomputation when that route changes. +Model selection has no unrestricted static mode. The default-off Host setting authorizes alternative child routes; without it, the parent route and its supported efforts remain selectable. The primary spawn tool captures the setting once, while all capable delegation tools and shared discovery read the same Session policy. Fork tools reject model-selection arguments to preserve inherited-prefix reuse. ## Alternatives considered @@ -40,4 +40,4 @@ Model selection has no unrestricted static mode. The default-off Host setting au ## Related decisions -The route arguments, adapter preflight, discovery tool, and fork cache costs remain owned by [model-selected subagent routes](../../../../packages/subagent/tool-subagent/README.md). +The route arguments, adapter preflight, discovery tool, and fork restriction remain owned by [model-selected subagent routes](../../../../packages/subagent/tool-subagent/README.md). diff --git a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md index c157cec8a7..49a221d220 100644 --- a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md +++ b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md @@ -16,7 +16,7 @@ Host 自有的 `subagent-model-selection` 设置 section 保存显式 `enabled` 固定的 `list_subagent_models` schema 不枚举策略。调用时,它列出已授权且已注册的提供方,并取其公布模型目录与已记录路由列表的交集。精确查询先要求授权,再解析模型元数据及全部支持的推理强度,也允许目录未列出的 ID。没有已记录策略时,发现只解析父 Agent 当前生效的路由,即使目录未列出它。委派执行器会在适配器预检前独立校验显式提供方、模型或强度选择是否符合相同授权。没有选择字段的调用保留配置或继承的路由。 -模型选择没有无限制的静态模式。默认关闭的 Host 设置授权其他子级路由;没有该授权时,仍可选择父级路由及其支持的强度。主 spawn 工具一次性记录设置,所有支持选择的委派工具和共享发现均读取同一 Session 策略。fork 默认保留父级路由,其描述会提示改变路由时的前缀重算成本。 +模型选择没有无限制的静态模式。默认关闭的 Host 设置授权其他子级路由;没有该授权时,仍可选择父级路由及其支持的强度。主 spawn 工具一次性记录设置,所有支持选择的委派工具和共享发现均读取同一 Session 策略。fork 工具拒绝模型选择参数,以保留对继承前缀的复用。 ## Alternatives considered @@ -40,4 +40,4 @@ Host 自有的 `subagent-model-selection` 设置 section 保存显式 `enabled` ## Related decisions -路由参数、适配器预检、发现工具与 fork 缓存成本仍由[模型选择的 subagent 路由](../../../../packages/subagent/tool-subagent/README.zh.md)负责。 +路由参数、适配器预检、发现工具与 fork 限制仍由[模型选择的 subagent 路由](../../../../packages/subagent/tool-subagent/README.zh.md)负责。 diff --git a/apps/cli/tests/web-agent-presets.e2e.ts b/apps/cli/tests/web-agent-presets.e2e.ts index bb82f5349c..21cc43b76f 100644 --- a/apps/cli/tests/web-agent-presets.e2e.ts +++ b/apps/cli/tests/web-agent-presets.e2e.ts @@ -296,7 +296,9 @@ describe('the shipped Web composition', () => { expect(toolParameterNames(ctx, enabled.agent, 'subagent')).toEqual(expect.arrayContaining([ 'model', 'provider', 'reasoning_effort', ])) - expect(toolNames(ctx, disabled.agent)).toContain('list_subagent_models') + for (const agent of [disabled.agent, enabled.agent]) { + expect(toolParameterNames(ctx, agent, 'subagent_fork')).toEqual(['cwd', 'description', 'prompt']) + } } finally { await ctx.settings.update(SUBAGENT_MODEL_SELECTION_SETTINGS_NAMESPACE, { enabled: false }) await enabled.dispose() diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml index 692308776e..13a0309388 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: 6f5f5f987c218948 - zh: 018ff71df684009c + en: 559ba7c9e11d4919 + zh: 44c540c14cf43f95 /tool-schema-catalog/deepseek-ai-dsh-plugin-manager: en: e0143916bb634762 zh: e0143916bb634762 @@ -228,8 +228,8 @@ en: aba834ab4f70005a zh: 142f819e8da5acc7 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent/subagent: - en: c272328d36a5afaf - zh: fcf69fd0efd55ea9 + en: 0de8d49aa443ec9c + zh: 7cb7c8a55a0469b1 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control: en: ab82e130c3413f04 zh: ab82e130c3413f04 diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index 574af31642..9c94c9d1d7 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -37,7 +37,7 @@ This table connects model-visible tool names to the plugin package and service s | `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`, `ctx.workflowEngine`, `ctx.subagents`, `ctx.systemPrompt`, `a calling Agent (exec.agent parents every fresh round)` | `tool/call`, `tool/result`, `workflow and child session events during execution` | - | A fixed foreground workflow starts one fresh structured child per round; the model selects only the immutable objective and an optional round cap. | | `@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` | - | 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-tool-subagent` | `list_subagent_models`, `subagent` | `ctx.tools`, `ctx.subagents`, `ctx.systemPrompt`, `ctx.llm for model discovery and selected-route validation` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered delegation name is the load-time `toolName` config (default `subagent`); backends supporting agentOptions always expose optional route fields, and the shared discovery plugin resolves the calling Session’s allowed routes. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` uses that same policy, and without an enabled policy explicit selection and discovery are restricted to the current parent route. Each instance selects its backend through `provider` and reads model-selection settings through `modelSelectionSettings`. 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.llm for model discovery and selected-route validation` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered delegation name is the load-time `toolName` config (default `subagent`); backends supporting agentOptions without inheriting parent context always expose optional route fields, and the shared discovery plugin resolves the calling Session’s allowed routes. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` rejects model-selection arguments; without an enabled policy, spawn selection and discovery are restricted to the current parent route. Each instance selects its backend through `provider` and reads model-selection settings through `modelSelectionSettings`. 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-control` | `interrupt_agent`, `list_agents`, `send_message` | `ctx.tools`, `ctx.subagents`, `ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`, `tool/result`, `child session events through ctx.subagents` | - | The globally named discovery, interruption, and local-message tools over managed subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` and `interrupt_agent` once, plus `list_agents` from its separately loaded `/list-agents` plugin (whose catalog rows use the sessionProjections and live Agent registries). | | `@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-experimental-tool-agent-team` | `interrupt_agent`, `list_agents`, `send_message`, `spawn_teammate`, `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update`, `wait_agent` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `an exact live Team member Agent` | `tool/call`, `team/member`, `team/task`, `agent/inbox/spliced`, `user/message on inbox claim`, `tool/result` | - | All nine tools are scoped to implicit Team Leads and durable teammates. The shipped dsh-base bundle keeps the package disabled; the documented Agent Teams profile patch enables it while disabling the legacy continuable-child control names. | @@ -2079,7 +2079,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 registered delegation name is the load-time `toolName` config (default `subagent`); backends supporting agentOptions always expose optional route fields, and the shared discovery plugin resolves the calling Session’s allowed routes. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` uses that same policy, and without an enabled policy explicit selection and discovery are restricted to the current parent route. Each instance selects its backend through `provider` and reads model-selection settings through `modelSelectionSettings`. Every delegation returns a child id promptly; local children accept follow-ups, while external backends report their single execution automatically. +The registered delegation name is the load-time `toolName` config (default `subagent`); backends supporting agentOptions without inheriting parent context always expose optional route fields, and the shared discovery plugin resolves the calling Session’s allowed routes. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` rejects model-selection arguments; without an enabled policy, spawn selection and discovery are restricted to the current parent route. Each instance selects its backend through `provider` and reads model-selection settings through `modelSelectionSettings`. Every delegation returns a child id promptly; local children accept follow-ups, while external backends report their single execution automatically. diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index 8d36652493..be7b085212 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -41,7 +41,7 @@ | `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`、`ctx.workflowEngine`、`ctx.subagents`、`ctx.systemPrompt`、`a calling Agent (exec.agent parents every fresh round)` | `tool/call`、`tool/result`、`workflow and child session events during execution` | - | 固定的前台工作流会在每个 Round 启动一个全新的结构化子级;模型只能选择不可变目标和可选的 Round 上限。 | | `@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-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);支持 agentOptions 的后端始终公开可选路由字段,共享发现插件解析调用 Session 允许的路由。Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;`subagent_fork` 使用同一策略,没有启用策略时,显式选择和发现仅限当前父级路由。每个实例通过 `provider` 选择后端,通过 `modelSelectionSettings` 读取模型选择设置。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 | +| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);支持 agentOptions 且不继承父级上下文的后端始终公开可选路由字段,共享发现插件解析调用 Session 允许的路由。Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;`subagent_fork` 拒绝模型选择参数;没有启用策略时,spawn 选择和发现仅限当前父级路由。每个实例通过 `provider` 选择后端,通过 `modelSelectionSettings` 读取模型选择设置。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 | | `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`、`list_agents`、`send_message` | `ctx.tools`、`ctx.subagents`、`ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`、`tool/result`、`child session events through ctx.subagents` | - | 这些是用于发现、中断托管 subagent 和向本地子代理发送消息的全局命名工具:绑定提供方的 `tool-subagent` 实例注册不同的委派工具;本包注册一次 `send_message` 和 `interrupt_agent`,另由 `list_agents` 通过单独加载的 `/list-agents` 插件提供,其目录行使用 sessionProjections 和实时 Agent 注册表。 | | `@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-experimental-tool-agent-team` | `interrupt_agent`、`list_agents`、`send_message`、`spawn_teammate`、`team_task_create`、`team_task_get`、`team_task_list`、`team_task_update`、`wait_agent` | `ctx.tools`、`ctx.systemPrompt`、`ctx.agentTeams`、`an exact live Team member Agent` | `tool/call`、`team/member`、`team/task`、`agent/inbox/spliced`、`user/message on inbox claim`、`tool/result` | - | 这 9 个工具限定于隐式 Team Lead 与持久 teammate 作用域。随产品发布的 dsh-base bundle 默认禁用该包;文档中的 Agent Teams profile patch 会启用它,并禁用旧 continuable child 的同名控制工具。 | @@ -2085,7 +2085,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, 来源:[`packages/subagent/tool-subagent/src/index.ts`](../packages/subagent/tool-subagent/src/index.ts) -注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);支持 agentOptions 的后端始终公开可选路由字段,共享发现插件解析调用 Session 允许的路由。Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;`subagent_fork` 使用同一策略,没有启用策略时,显式选择和发现仅限当前父级路由。每个实例通过 `provider` 选择后端,通过 `modelSelectionSettings` 读取模型选择设置。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 +注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);支持 agentOptions 且不继承父级上下文的后端始终公开可选路由字段,共享发现插件解析调用 Session 允许的路由。Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;`subagent_fork` 拒绝模型选择参数;没有启用策略时,spawn 选择和发现仅限当前父级路由。每个实例通过 `provider` 选择后端,通过 `modelSelectionSettings` 读取模型选择设置。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 diff --git a/packages/bundle/base/cordis.patch.yml b/packages/bundle/base/cordis.patch.yml index 9f87051c49..f162113cae 100644 --- a/packages/bundle/base/cordis.patch.yml +++ b/packages/bundle/base/cordis.patch.yml @@ -381,7 +381,7 @@ provider: spawn toolName: subagent - # Fork inherits the parent route by default for KV Cache reuse. Parent and child + # Fork omits model-selection arguments for KV Cache reuse. Parent and child # share the messaging tools; return guidance follows the inherited history. # See packages/subagent/tool-subagent/README.md # and .agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md. diff --git a/packages/subagent/tool-subagent/README.i18n.yaml b/packages/subagent/tool-subagent/README.i18n.yaml index e59a887fdd..fa0bd6c782 100644 --- a/packages/subagent/tool-subagent/README.i18n.yaml +++ b/packages/subagent/tool-subagent/README.i18n.yaml @@ -24,14 +24,14 @@ en: 42673064ba88a211 zh: 8fa26285bd649dc9 /deepseek-ai-dsh-tool-subagent/use-this-package/selecting-a-child-llm: - en: b4ba9650c228b9b6 - zh: 027833db9aafd752 + en: f85faf2e965a541a + zh: 3c232e236199b9aa /deepseek-ai-dsh-tool-subagent/understand-the-implementation: en: 6ce5f749256a7e6c zh: a4f2adb04a70d9cf /deepseek-ai-dsh-tool-subagent/understand-the-implementation/design-concept: - en: 4847e2bcdde8e282 - zh: cc833748d954a65e + en: 5c97aae02694c27a + zh: 5507f580d3e0c43b /deepseek-ai-dsh-tool-subagent/understand-the-implementation/activation-ownership: en: ef692c4d17290920 zh: b1d0d3dab011e2cd @@ -54,14 +54,14 @@ en: e4c7376d4f29dbd5 zh: 6b7a9eaf69c9ea53 /deepseek-ai-dsh-tool-subagent/model-experience/tool-schema/what-the-model-sees: - en: c6da697a3965147c - zh: 4f4a48054bcbda88 + en: 56361a0d507bad66 + zh: 979d502dd3ac168f /deepseek-ai-dsh-tool-subagent/model-experience/tool-schema/token-effect: en: 6a1a6351a3a88424 zh: c574e9ee286518ed /deepseek-ai-dsh-tool-subagent/model-experience/tool-schema/kv-cache-effect: - en: 46f736c6c389052a - zh: a7765541030e9104 + en: f746a3c2f2f3cd18 + zh: 68de857073b229ff /deepseek-ai-dsh-tool-subagent/model-experience/model-selection-and-discovery: en: a4ca31b9cbacda6b zh: 91ac3e0784a14697 @@ -114,8 +114,8 @@ en: f5d5f97a153f7c70 zh: 3d6bc05b9eadfbc2 /deepseek-ai-dsh-tool-subagent/known-limitations-and-deferred-work: - en: 93d06d9646fe848d - zh: bc86f8bd38b4b61a + en: 897c9481685a6eab + zh: 444ee7ef4d353f60 /deepseek-ai-dsh-tool-subagent/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/subagent/tool-subagent/README.md b/packages/subagent/tool-subagent/README.md index 96dcfabfe6..8591e118e9 100644 --- a/packages/subagent/tool-subagent/README.md +++ b/packages/subagent/tool-subagent/README.md @@ -65,7 +65,7 @@ Every backend’s completion notice carries its final answer. Local Spawn and Fo ### Selecting a child LLM -Backends that support `agentOptions` always expose optional `provider`, `model`, and `reasoning_effort` fields. Load `@deepseek-ai/dsh-tool-subagent/list-models` once alongside the delegation tools for shared discovery; shipped compositions include it. Without a recorded Session policy, discovery and explicit selection permit only the parent Agent’s current effective provider/model and its supported efforts. Set `modelSelectionSettings: true` to sample the Host’s `subagent-model-selection` preference for fresh top-level Sessions. When enabled, its exact route list is recorded in the Session, inherited by child Sessions, and preserved on restore; later settings edits affect only new Sessions. A restored Session without a policy keeps the parent-route restriction. ACP, Codex, and Claude Code do not support `agentOptions` and reject selection fields. +Backends that support `agentOptions` and do not inherit parent context always expose optional `provider`, `model`, and `reasoning_effort` fields. Load `@deepseek-ai/dsh-tool-subagent/list-models` once alongside the delegation tools for shared discovery; shipped compositions include it. Without a recorded Session policy, discovery and explicit selection permit only the parent Agent’s current effective provider/model and its supported efforts. Set `modelSelectionSettings: true` to sample the Host’s `subagent-model-selection` preference for fresh top-level Sessions. When enabled, its exact route list is recorded in the Session, inherited by child Sessions, and preserved on restore; later settings edits affect only new Sessions. A restored Session without a policy keeps the parent-route restriction. ACP, Codex, and Claude Code do not support `agentOptions` and reject selection fields. A call supplies `provider` and `model` together, or supplies only an effort when configured, parent, or provider-owned defaults provide the route. Static `provider.agentRouteDefaults`, when present, form the provider/model baseline; tool configuration and model fields overlay it before route-aware effort merging and exact-route preflight. Providers without these defaults use compatible values from the parent's latest logged request, then the parent's creation options before its first request, while retaining the configured `maxTokens`. Changing the route without an explicit effort clears the inherited route-owned effort, so the selected model resolves its default. The live LLM adapter validates the effective route before child creation. Catalog membership remains advisory, so a model can use an unlisted id when its adapter accepts it. @@ -81,7 +81,7 @@ This section explains how the tool mirrors provider lifecycle and settles runs; ### Design concept -One instance is one provider plus one tool name. The plugin registers its definition in the owning composition while the provider is present and disposes it when the provider leaves. Agents inherit that shared definition, so their existing tool filters apply without per-Agent registration. Settings-backed instances capture Session authorization during Agent creation; calls read the durable policy independently of registration. Direct Agent setup supplies its unpublished Session explicitly. A numeric `maxDepth` or configured LLM selection unsupported by the provider fails the mount. Discovery has one separate composition entry shared by spawn and fork tools. No runtime invariant companion is published: policy comes directly from the Session projection and is checked at execution, with no separately retained per-Agent definition state to reconcile. +One instance is one provider plus one tool name. The plugin registers its definition in the owning composition while the provider is present and disposes it when the provider leaves. Agents inherit that shared definition, so their existing tool filters apply without per-Agent registration. Settings-backed instances capture Session authorization during Agent creation; calls read the durable policy independently of registration. Direct Agent setup supplies its unpublished Session explicitly. A numeric `maxDepth` or configured LLM selection unsupported by the provider fails the mount. Discovery has one separate composition entry. No runtime invariant companion is published: policy comes directly from the Session projection and is checked at execution, with no separately retained per-Agent definition state to reconcile. ### Activation ownership @@ -129,7 +129,7 @@ Read these pages when the package-level contract is not enough; they move from t #### What the model sees -The generated default [`subagent` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent) appears under the configured tool name while its provider exists. Backends supporting `agentOptions` always expose `provider`, `model`, and `reasoning_effort`. Context inheritance and continuation support determine task and follow-up descriptions. A tool restriction removes both the schema and its `tool:` guidance section. +The generated default [`subagent` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent) appears under the configured tool name while its provider exists. Backends supporting `agentOptions` that do not inherit parent context always expose `provider`, `model`, and `reasoning_effort`. Context inheritance and continuation support determine task and follow-up descriptions. A tool restriction removes both the schema and its `tool:` guidance section. #### Token effect @@ -137,7 +137,7 @@ Fixed schema cost per parent request, including three optional route fields on c #### KV Cache effect -Prefix-stable while provider instances and their configuration are unchanged. Adapter catalog changes do not alter the definition; a child route override may prevent a fork child from reusing the inherited parent prefix. +Prefix-stable while provider instances and their configuration are unchanged. Adapter catalog changes do not alter the definition. Fork tools omit model-selection fields to preserve reuse of the inherited parent prefix. ### Model selection and discovery @@ -209,7 +209,7 @@ Append-only; newly visible content follows the reusable request prefix and does These limits define what this tool does not return or enforce; they are current package constraints. - **This tool does not wait for results** — use the completion notice or child messages. Programmatic result waiting belongs to the subagent service. -- **Fork route changes can lose KV Cache reuse** — optional route fields follow the same Session policy as spawn; changing provider or model may require prefilling the inherited conversation again. +- **Fork tools reject model selection** — tools that inherit parent context neither expose nor accept `provider`, `model`, or `reasoning_effort`, even with Session authorization. Configured `agentOptions` remain available to the deployment. - **Non-routing child policy is fixed per instance** — another persona, tool filter, or depth cap requires another distinctly named tool. Explicit LLM selection is restricted to the Session’s authorized routes, or the current parent route without an enabled policy; unsupported backends reject selection fields. diff --git a/packages/subagent/tool-subagent/README.zh.md b/packages/subagent/tool-subagent/README.zh.md index 0bf8ee903f..5e97c8db7a 100644 --- a/packages/subagent/tool-subagent/README.zh.md +++ b/packages/subagent/tool-subagent/README.zh.md @@ -65,7 +65,7 @@ kind: "package-reference" ### 选择子级 LLM -支持 `agentOptions` 的后端始终公开可选的 `provider`、`model` 和 `reasoning_effort` 字段。在委派工具旁加载一次 `@deepseek-ai/dsh-tool-subagent/list-models` 即可共享发现功能;随附组合已包含它。没有已记录 Session 策略时,发现和显式选择只允许父 Agent 当前生效的 provider/model 及其支持的强度。设置 `modelSelectionSettings: true` 后,新顶层 Session 会读取 Host 的 `subagent-model-selection` 偏好。启用时,精确路由列表记录在 Session 中,由子 Session 继承,并在恢复时保留;之后的设置编辑只影响新 Session。没有策略的恢复 Session 仍受父级路由限制。ACP、Codex 和 Claude Code 不支持 `agentOptions`,会拒绝选择字段。 +支持 `agentOptions` 且不继承父级上下文的后端始终公开可选的 `provider`、`model` 和 `reasoning_effort` 字段。在委派工具旁加载一次 `@deepseek-ai/dsh-tool-subagent/list-models` 即可共享发现功能;随附组合已包含它。没有已记录 Session 策略时,发现和显式选择只允许父 Agent 当前生效的 provider/model 及其支持的强度。设置 `modelSelectionSettings: true` 后,新顶层 Session 会读取 Host 的 `subagent-model-selection` 偏好。启用时,精确路由列表记录在 Session 中,由子 Session 继承,并在恢复时保留;之后的设置编辑只影响新 Session。没有策略的恢复 Session 仍受父级路由限制。ACP、Codex 和 Claude Code 不支持 `agentOptions`,会拒绝选择字段。 一次调用需同时提供 `provider` 与 `model`;当配置值、父 agent 值或提供方持有的默认值能提供路由时,也可只提供推理等级。静态的 `provider.agentRouteDefaults` 在存在时构成提供方/模型基线;工具配置与模型字段会在路由相关强度合并和确切路由预检前覆盖它。没有这些默认值的提供方会使用父 agent 最新已记录请求中的兼容值,再使用父级首次请求前的创建选项,并保留配置的 `maxTokens`。更改路由但未显式提供推理等级时,会清除继承的路由自有等级,使所选模型解析自己的默认值。实时 LLM 适配器在创建子 agent 前校验有效路由。目录成员资格只提供建议,因此适配器接受时,模型可以使用未列出的 id。 @@ -81,7 +81,7 @@ kind: "package-reference" ### 设计理念 -一个实例对应一个提供方和一个工具名。插件在提供方存在时向所属组合注册定义,并在提供方离开时释放它。Agent 继承这份共享定义,因此现有工具过滤器即可生效,无需逐 Agent 注册。读取设置的实例在 Agent 创建时记录 Session 授权;调用时独立于注册读取持久策略。直接配置 Agent 时显式传入尚未发布的 Session。提供方不支持的数字 `maxDepth` 或配置的 LLM 选择会使挂载失败。发现功能由 spawn 和 fork 工具共享的独立组合项提供。本包不导出运行时不变量:策略直接来自 Session 投影,并在执行时校验,没有另行保存的逐 Agent 定义状态需要协调。 +一个实例对应一个提供方和一个工具名。插件在提供方存在时向所属组合注册定义,并在提供方离开时释放它。Agent 继承这份共享定义,因此现有工具过滤器即可生效,无需逐 Agent 注册。读取设置的实例在 Agent 创建时记录 Session 授权;调用时独立于注册读取持久策略。直接配置 Agent 时显式传入尚未发布的 Session。提供方不支持的数字 `maxDepth` 或配置的 LLM 选择会使挂载失败。发现功能由一个独立组合项提供。本包不导出运行时不变量:策略直接来自 Session 投影,并在执行时校验,没有另行保存的逐 Agent 定义状态需要协调。 ### Activation 所有权 @@ -129,7 +129,7 @@ kind: "package-reference" #### 模型看到什么 -生成的默认 [`subagent` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-subagent) 在提供方存在时以配置的工具名公开。支持 `agentOptions` 的后端始终公开 `provider`、`model` 和 `reasoning_effort`。上下文继承和继续执行能力决定任务与后续工作描述。工具限制同时移除 schema 及其 `tool:` 指导段。 +生成的默认 [`subagent` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-subagent) 在提供方存在时以配置的工具名公开。支持 `agentOptions` 且不继承父级上下文的后端始终公开 `provider`、`model` 和 `reasoning_effort`。上下文继承和继续执行能力决定任务与后续工作描述。工具限制同时移除 schema 及其 `tool:` 指导段。 #### Token 影响 @@ -137,7 +137,7 @@ kind: "package-reference" #### KV Cache 影响 -只要提供方实例及其配置不变,前缀就保持稳定。适配器目录变化不会改变定义;子级路由覆盖可能使 fork 子 agent 无法复用继承的父级前缀。 +只要提供方实例及其配置不变,前缀就保持稳定。适配器目录变化不会改变定义。fork 工具省略模型选择字段,以保留对继承的父级前缀的复用。 ### 模型选择与发现 @@ -209,7 +209,7 @@ Start independent delegations with `subagent` or `subagent_fork` together in one 这些限制说明本工具不返回或不强制执行什么;它们是当前包约束。 - **本工具不等待结果**——使用完成通知或子任务消息。程序化结果等待由 subagent 服务提供。 -- **更改 fork 路由可能失去 KV Cache 复用** — 可选路由字段遵循与 spawn 相同的 Session 策略;更改提供方或模型可能需要重新预填充继承的对话。 +- **fork 工具拒绝模型选择** — 继承父级上下文的工具既不公开也不接受 `provider`、`model` 或 `reasoning_effort`,即使 Session 已授权也不例外。部署仍可配置 `agentOptions`。 - **非路由子级策略按实例固定** — 更换角色、工具过滤器或深度上限,需要另一个不同名称的工具。显式 LLM 选择受 Session 授权路由限制,没有启用策略时只允许当前父级路由;不支持选择的后端会拒绝选择字段。 diff --git a/packages/subagent/tool-subagent/src/index.ts b/packages/subagent/tool-subagent/src/index.ts index 90f98d10aa..6cba7d3490 100644 --- a/packages/subagent/tool-subagent/src/index.ts +++ b/packages/subagent/tool-subagent/src/index.ts @@ -206,18 +206,12 @@ export function apply(ctx: Context, config: Config, session?: Session): void { let mounted: { subagentProvider: SubagentProvider; disposeTool: () => void } | undefined const mount = (subagentProvider: SubagentProvider): void => { assertSubagentProviderConfiguration(subagentProvider) - const modelSelectionEnabled = subagentProvider.capabilities.agentOptions + const modelSelectionEnabled = subagentProvider.capabilities.agentOptions && !subagentProvider.inheritsParentContext const wording = providerWording(subagentProvider.inheritsParentContext) const providerRouteDefaults = subagentProvider.agentRouteDefaults const selectionDescription = providerRouteDefaults !== undefined ? ' Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and this provider\'s route defaults. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model\'s default effort.' : ' Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model\'s default effort.' - const choiceDescription = !modelSelectionEnabled - ? '' - : selectionDescription - + (subagentProvider.inheritsParentContext - ? ' Changing the route can prevent provider-side reuse of the inherited conversation prefix.' - : '') const definition = defineTool({ name: toolName, description: wording.description @@ -225,7 +219,7 @@ export function apply(ctx: Context, config: Config, session?: Session): void { + (subagentProvider.prepareContinuable !== undefined ? ' The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.' : ' The completion notice includes its final answer. This backend does not accept follow-up messages.') - + choiceDescription, + + (modelSelectionEnabled ? selectionDescription : ''), parameters: { cwd: { type: 'string', diff --git a/packages/subagent/tool-subagent/tests/model-selection.spec.ts b/packages/subagent/tool-subagent/tests/model-selection.spec.ts index 4af0606e66..4a99144c81 100644 --- a/packages/subagent/tool-subagent/tests/model-selection.spec.ts +++ b/packages/subagent/tool-subagent/tests/model-selection.spec.ts @@ -469,11 +469,31 @@ describe('dsh-tool-subagent model selection', () => { expect(resolve).not.toHaveBeenCalled() }) - it('warns that changing a fork route can lose inherited-prefix reuse', async () => { - const ctx = await setup({ provider: 'mock', withModelSelection: true }, { inheritsParentContext: true }) + it('keeps fork model selection unavailable even with Session authorization', async () => { + const started = vi.fn() + const ctx = await setup({ provider: 'mock', withModelSelection: true }, { + inheritsParentContext: true, + onStart: started, + }) const schema = ctx.tools.schemas(modelSelectionSetupAgent(ctx)).find(entry => entry.name === 'subagent')! + const props = (schema.parameters as { properties?: Record }).properties ?? {} + expect(Object.keys(props).sort()).toEqual(['cwd', 'description', 'prompt']) expect(schema.description).toContain('inherits this conversation') - expect(schema.description).toContain('can prevent provider-side reuse of the inherited conversation prefix') + expect(schema.description).not.toContain('Child LLM selection') + + for (const selection of [ + { provider: 'alpha', model: 'fast-model' }, + { reasoning_effort: 'high' }, + ]) { + const result = await callSubagent(ctx, { description: 'fork work', prompt: 'do it', ...selection }) + expect(result.isError).toBe(true) + expect(text(result)).toContain('model selection is disabled for this tool instance') + } + expect(started).not.toHaveBeenCalled() + + const inherited = await callSubagent(ctx, { description: 'fork work', prompt: 'do it' }) + expect(inherited.isError).toBe(false) + expect(started).toHaveBeenCalledOnce() }) it('propagates an exact-route resolver failure before child creation', async () => { diff --git a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts index 569b1dcb7b..9ddecc530f 100644 --- a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts +++ b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts @@ -513,7 +513,7 @@ describe('dsh-tool-subagent', () => { const schema = ctx.tools.schemas().find(s => s.name === 'subagent')! expect(schema.description).toContain('inherits this conversation') expect(schema.description).not.toContain('does not see this conversation') - expect(schema.description).toContain('can prevent provider-side reuse of the inherited conversation prefix') + expect(schema.description).not.toContain('Child LLM selection') const props = (schema.parameters as { properties: Record }).properties expect(props['prompt']!.description).toContain('completed turns') }) diff --git a/scripts/gen-tool-catalog.ts b/scripts/gen-tool-catalog.ts index fa03619cba..81b10491e0 100644 --- a/scripts/gen-tool-catalog.ts +++ b/scripts/gen-tool-catalog.ts @@ -522,7 +522,7 @@ const TOOL_PACKAGES: ToolPackage[] = [ await ctx.plugin(SubagentModelDiscovery) }, note: - 'The registered delegation name is the load-time `toolName` config (default `subagent`); backends supporting agentOptions always expose optional route fields, and the shared discovery plugin resolves the calling Session’s allowed routes. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` uses that same policy, and without an enabled policy explicit selection and discovery are restricted to the current parent route. Each instance selects its backend through `provider` and reads model-selection settings through `modelSelectionSettings`. Every delegation returns a child id promptly; local children accept follow-ups, while external backends report their single execution automatically.', + 'The registered delegation name is the load-time `toolName` config (default `subagent`); backends supporting agentOptions without inheriting parent context always expose optional route fields, and the shared discovery plugin resolves the calling Session’s allowed routes. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` rejects model-selection arguments; without an enabled policy, spawn selection and discovery are restricted to the current parent route. Each instance selects its backend through `provider` and reads model-selection settings through `modelSelectionSettings`. Every delegation returns a child id promptly; local children accept follow-ups, while external backends report their single execution automatically.', }, { pkg: '@deepseek-ai/dsh-tool-subagent-control', diff --git a/snapshots/sdk/bash-tool/tool-schemas.expected.json b/snapshots/sdk/bash-tool/tool-schemas.expected.json index 2dd1f341df..8dc90581f4 100644 --- a/snapshots/sdk/bash-tool/tool-schemas.expected.json +++ b/snapshots/sdk/bash-tool/tool-schemas.expected.json @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +408,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/sdk/subagent-activation-limit/tool-schemas.1.expected.json b/snapshots/sdk/subagent-activation-limit/tool-schemas.1.expected.json index 2dd1f341df..8dc90581f4 100644 --- a/snapshots/sdk/subagent-activation-limit/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-activation-limit/tool-schemas.1.expected.json @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +408,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json b/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json index 2dd1f341df..8dc90581f4 100644 --- a/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +408,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json b/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json index 2dd1f341df..8dc90581f4 100644 --- a/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +408,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json b/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json index 17dff0816f..e13cfd3bb0 100644 --- a/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json +++ b/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json @@ -467,7 +467,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -482,18 +482,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json index 2dd1f341df..8dc90581f4 100644 --- a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +408,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json index 87515e6d78..5f0e685eb2 100644 --- a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json +++ b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +408,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json b/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json index 2dd1f341df..8dc90581f4 100644 --- a/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +408,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json b/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json index 2dd1f341df..8dc90581f4 100644 --- a/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +408,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/sdk/text-turn/tool-schemas.expected.json b/snapshots/sdk/text-turn/tool-schemas.expected.json index 2dd1f341df..8dc90581f4 100644 --- a/snapshots/sdk/text-turn/tool-schemas.expected.json +++ b/snapshots/sdk/text-turn/tool-schemas.expected.json @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +408,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/sdk/tool-error-details/system-prompt.expected.md b/snapshots/sdk/tool-error-details/system-prompt.expected.md index 08bc866504..4dcf8262a6 100644 --- a/snapshots/sdk/tool-error-details/system-prompt.expected.md +++ b/snapshots/sdk/tool-error-details/system-prompt.expected.md @@ -221,7 +221,7 @@ interface ToolArgsMap { /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -229,12 +229,6 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ - provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ - model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ - reasoning_effort?: string; } & Record; /** Record and update a task list to plan multi-step work and show progress; skip it for trivial single-step tasks. Add one todo per concrete step before you start. While work remains, keep the todos being worked on `in_progress`, several only when work runs in parallel. Mark each todo `completed` as soon as it is done. */ todo_write: { diff --git a/snapshots/sdk/tool-error-details/tool-schemas.expected.json b/snapshots/sdk/tool-error-details/tool-schemas.expected.json index 62d25abd75..77b5236350 100644 --- a/snapshots/sdk/tool-error-details/tool-schemas.expected.json +++ b/snapshots/sdk/tool-error-details/tool-schemas.expected.json @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +408,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/advanced-toolchain/system-prompt.expected.md b/snapshots/session/advanced-toolchain/system-prompt.expected.md index d3a212e8ff..14d8e2559d 100644 --- a/snapshots/session/advanced-toolchain/system-prompt.expected.md +++ b/snapshots/session/advanced-toolchain/system-prompt.expected.md @@ -234,7 +234,7 @@ interface ToolArgsMap { /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -242,12 +242,6 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ - provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ - model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ - reasoning_effort?: string; } & Record; /** Record and update a task list to plan multi-step work and show progress; skip it for trivial single-step tasks. Add one todo per concrete step before you start. While work remains, keep the todos being worked on `in_progress`, several only when work runs in parallel. Mark each todo `completed` as soon as it is done. */ todo_write: { diff --git a/snapshots/session/advanced-toolchain/tool-schemas.expected.json b/snapshots/session/advanced-toolchain/tool-schemas.expected.json index 46396bb306..f8cbf6b810 100644 --- a/snapshots/session/advanced-toolchain/tool-schemas.expected.json +++ b/snapshots/session/advanced-toolchain/tool-schemas.expected.json @@ -434,7 +434,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -449,18 +449,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -1188,7 +1176,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -1203,18 +1191,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/agent-instructions/tool-schemas.expected.json b/snapshots/session/agent-instructions/tool-schemas.expected.json index 472458e753..5018eac594 100644 --- a/snapshots/session/agent-instructions/tool-schemas.expected.json +++ b/snapshots/session/agent-instructions/tool-schemas.expected.json @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +408,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -1067,7 +1055,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -1082,18 +1070,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/background-confinement-failure/tool-schemas.expected.json b/snapshots/session/background-confinement-failure/tool-schemas.expected.json index 2dd1f341df..8dc90581f4 100644 --- a/snapshots/session/background-confinement-failure/tool-schemas.expected.json +++ b/snapshots/session/background-confinement-failure/tool-schemas.expected.json @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +408,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/both-mode-turn/system-prompt.expected.md b/snapshots/session/both-mode-turn/system-prompt.expected.md index 5e49146818..69bc221437 100644 --- a/snapshots/session/both-mode-turn/system-prompt.expected.md +++ b/snapshots/session/both-mode-turn/system-prompt.expected.md @@ -188,7 +188,7 @@ interface ToolArgsMap { /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -196,12 +196,6 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ - provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ - model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ - reasoning_effort?: string; } & Record; /** Record and update a task list to plan multi-step work and show progress; skip it for trivial single-step tasks. Add one todo per concrete step before you start. While work remains, keep the todos being worked on `in_progress`, several only when work runs in parallel. Mark each todo `completed` as soon as it is done. */ todo_write: { diff --git a/snapshots/session/browser-use-chrome-devtools-mcp/tool-schemas.expected.json b/snapshots/session/browser-use-chrome-devtools-mcp/tool-schemas.expected.json index a8ef352d5e..763c4962d9 100644 --- a/snapshots/session/browser-use-chrome-devtools-mcp/tool-schemas.expected.json +++ b/snapshots/session/browser-use-chrome-devtools-mcp/tool-schemas.expected.json @@ -500,7 +500,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -515,18 +515,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/browser-use-playwright-mcp/tool-schemas.expected.json b/snapshots/session/browser-use-playwright-mcp/tool-schemas.expected.json index 3d46faeef4..f8db659ff3 100644 --- a/snapshots/session/browser-use-playwright-mcp/tool-schemas.expected.json +++ b/snapshots/session/browser-use-playwright-mcp/tool-schemas.expected.json @@ -502,7 +502,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -517,18 +517,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/browser-use-stagehand-native/tool-schemas.expected.json b/snapshots/session/browser-use-stagehand-native/tool-schemas.expected.json index 33c0bdbb5a..f3d955b641 100644 --- a/snapshots/session/browser-use-stagehand-native/tool-schemas.expected.json +++ b/snapshots/session/browser-use-stagehand-native/tool-schemas.expected.json @@ -606,7 +606,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -621,18 +621,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/claude-code-mods/tool-schemas.expected.json b/snapshots/session/claude-code-mods/tool-schemas.expected.json index 2dd1f341df..8dc90581f4 100644 --- a/snapshots/session/claude-code-mods/tool-schemas.expected.json +++ b/snapshots/session/claude-code-mods/tool-schemas.expected.json @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +408,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/compaction-output-reserve/cordis.snapshot.yml b/snapshots/session/compaction-output-reserve/cordis.snapshot.yml index 15f69e1913..b9294ef273 100644 --- a/snapshots/session/compaction-output-reserve/cordis.snapshot.yml +++ b/snapshots/session/compaction-output-reserve/cordis.snapshot.yml @@ -31,7 +31,7 @@ name: DeepSeek models: - id: deepseek-v4-flash - contextWindow: 9270 + contextWindow: 9009 defaultMaxTokens: 1000 - id: compaction-working-directory name: '../../../packages/test-support/session-snapshot/tests/fixtures/compaction-working-directory.ts' diff --git a/snapshots/session/compaction-output-reserve/cordis.yml b/snapshots/session/compaction-output-reserve/cordis.yml index ea561fc74f..e970dde73c 100644 --- a/snapshots/session/compaction-output-reserve/cordis.yml +++ b/snapshots/session/compaction-output-reserve/cordis.yml @@ -7,7 +7,7 @@ config: models: - id: deepseek-v4-flash - contextWindow: 9270 + contextWindow: 9009 maxTokens: 1000 - id: compaction-basic diff --git a/snapshots/session/compaction-output-reserve/session.v4.jsonl b/snapshots/session/compaction-output-reserve/session.v4.jsonl index 590b3b6ac9..51414cb663 100644 --- a/snapshots/session/compaction-output-reserve/session.v4.jsonl +++ b/snapshots/session/compaction-output-reserve/session.v4.jsonl @@ -11,7 +11,7 @@ {"type":"user/message","data":{"content":[{"type":"text","text":"Establish a durable compaction premise before continuing. Record every part of this historical evidence: the snapshot uses keyless replay; persistence uses JSONL; the assembled headless application loads its real Cordis composition; model-visible inputs remain logged; tool calls and results remain paired and ordered; output reservation reduces the message budget before proactive compaction; compaction opens with compaction/start and closes with compaction/end; a successful auxiliary summary records compaction/summary source metadata; the replacement surface shadows only an older balanced range; the checkpoint remains smaller than the history it replaces; the newest tool result remains verbatim; the next request sees that checkpoint; the final response proves the same turn continued; deterministic snapshot evidence stays separate from the live-provider smoke; and no external API key is needed. Emit one alpha marker through bash, then finish the task after proactive compaction with the exact words PROACTIVE COMPACTION COMPLETE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"/tmp\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"/tmp\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":1000},"adapterDefaults":{"maxTokens":true},"tools":"{{tools}}"},"reason":"initial"}} -{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":9270}} +{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":9009}} {"type":"session/title","data":{"title":"Establish a durable compaction premise","messageSeqs":[9],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_compaction_marker","name":"bash","arguments":"{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":24,"outputTokens":6},"stream":[{"type":"chunk","time":1791427805327,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791427805327,"index":0,"dt":[],"id":"call_compaction_marker","name":"bash","args":["{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"]},{"type":"chunk","time":1791427805327,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_compaction_marker","name":"bash","arguments":"{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"}}},{"type":"chunk","time":1791427805327,"chunk":{"type":"usage","usage":{"inputTokens":24,"outputTokens":6}}},{"type":"chunk","time":1791427805327,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_compaction_marker","name":"bash","arguments":"{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"}} diff --git a/snapshots/session/compaction-recovery/tool-schemas.expected.json b/snapshots/session/compaction-recovery/tool-schemas.expected.json index 472458e753..5018eac594 100644 --- a/snapshots/session/compaction-recovery/tool-schemas.expected.json +++ b/snapshots/session/compaction-recovery/tool-schemas.expected.json @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +408,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -1067,7 +1055,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -1082,18 +1070,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/compaction-summary-headroom/cordis.snapshot.yml b/snapshots/session/compaction-summary-headroom/cordis.snapshot.yml index 76355f3bb4..342f2c3574 100644 --- a/snapshots/session/compaction-summary-headroom/cordis.snapshot.yml +++ b/snapshots/session/compaction-summary-headroom/cordis.snapshot.yml @@ -30,7 +30,7 @@ name: DeepSeek models: - id: deepseek-v4-flash - contextWindow: 9270 + contextWindow: 9009 defaultMaxTokens: 1000 - id: compaction-working-directory name: '../../../packages/test-support/session-snapshot/tests/fixtures/compaction-working-directory.ts' diff --git a/snapshots/session/compaction-summary-headroom/cordis.yml b/snapshots/session/compaction-summary-headroom/cordis.yml index da55c9afc7..1897460f05 100644 --- a/snapshots/session/compaction-summary-headroom/cordis.yml +++ b/snapshots/session/compaction-summary-headroom/cordis.yml @@ -7,7 +7,7 @@ config: models: - id: deepseek-v4-flash - contextWindow: 9270 + contextWindow: 9009 maxTokens: 1000 - id: compaction-basic diff --git a/snapshots/session/compaction-summary-headroom/session.v4.jsonl b/snapshots/session/compaction-summary-headroom/session.v4.jsonl index 3a4e672909..aafe00a914 100644 --- a/snapshots/session/compaction-summary-headroom/session.v4.jsonl +++ b/snapshots/session/compaction-summary-headroom/session.v4.jsonl @@ -11,7 +11,7 @@ {"type":"user/message","data":{"content":[{"type":"text","text":"Establish a durable compaction premise before continuing. Record every part of this historical evidence: the snapshot uses keyless replay; persistence uses JSONL; the assembled headless application loads its real Cordis composition; model-visible inputs remain logged; tool calls and results remain paired and ordered; output reservation reduces the message budget before proactive compaction; compaction opens with compaction/start and closes with compaction/end; a successful auxiliary summary records compaction/summary source metadata; the replacement surface shadows only an older balanced range; the checkpoint remains smaller than the history it replaces; the newest tool result remains verbatim; the next request sees that checkpoint; the final response proves the same turn continued; deterministic snapshot evidence stays separate from the live-provider smoke; and no external API key is needed. Emit one alpha marker through bash, then finish the task after proactive compaction with the exact words PROACTIVE COMPACTION COMPLETE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"/tmp\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"/tmp\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":1000},"adapterDefaults":{"maxTokens":true},"tools":"{{tools}}"},"reason":"initial"}} -{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":9270}} +{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":9009}} {"type":"session/title","data":{"title":"Establish a durable compaction premise","messageSeqs":[9],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_compaction_marker","name":"bash","arguments":"{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":24,"outputTokens":6},"stream":[{"type":"chunk","time":1791427807038,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791427807038,"index":0,"dt":[],"id":"call_compaction_marker","name":"bash","args":["{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"]},{"type":"chunk","time":1791427807038,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_compaction_marker","name":"bash","arguments":"{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"}}},{"type":"chunk","time":1791427807038,"chunk":{"type":"usage","usage":{"inputTokens":24,"outputTokens":6}}},{"type":"chunk","time":1791427807038,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_compaction_marker","name":"bash","arguments":"{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"}} diff --git a/snapshots/session/computer-use-cua-driver-mcp/tool-schemas.expected.json b/snapshots/session/computer-use-cua-driver-mcp/tool-schemas.expected.json index 9a7dc19de7..1107b15b8f 100644 --- a/snapshots/session/computer-use-cua-driver-mcp/tool-schemas.expected.json +++ b/snapshots/session/computer-use-cua-driver-mcp/tool-schemas.expected.json @@ -479,7 +479,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -494,18 +494,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/computer-use-cua-driver-native/tool-schemas.expected.json b/snapshots/session/computer-use-cua-driver-native/tool-schemas.expected.json index a95daa24a6..0bf5c52da2 100644 --- a/snapshots/session/computer-use-cua-driver-native/tool-schemas.expected.json +++ b/snapshots/session/computer-use-cua-driver-native/tool-schemas.expected.json @@ -439,7 +439,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -454,18 +454,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json b/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json index da58565d4e..e8770b87d3 100644 --- a/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json +++ b/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json @@ -434,7 +434,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -449,18 +449,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/deepseek-messages-system-prompt/tool-schemas.expected.json b/snapshots/session/deepseek-messages-system-prompt/tool-schemas.expected.json index 2dd1f341df..8dc90581f4 100644 --- a/snapshots/session/deepseek-messages-system-prompt/tool-schemas.expected.json +++ b/snapshots/session/deepseek-messages-system-prompt/tool-schemas.expected.json @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +408,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/deepseek-protocol-system-prompt/tool-schemas.expected.json b/snapshots/session/deepseek-protocol-system-prompt/tool-schemas.expected.json index 2dd1f341df..8dc90581f4 100644 --- a/snapshots/session/deepseek-protocol-system-prompt/tool-schemas.expected.json +++ b/snapshots/session/deepseek-protocol-system-prompt/tool-schemas.expected.json @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +408,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/dynamic-tool-updates/tool-schemas.expected.json b/snapshots/session/dynamic-tool-updates/tool-schemas.expected.json index 5268b0e9d8..de74610301 100644 --- a/snapshots/session/dynamic-tool-updates/tool-schemas.expected.json +++ b/snapshots/session/dynamic-tool-updates/tool-schemas.expected.json @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +408,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -1075,7 +1063,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -1090,18 +1078,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -1748,7 +1724,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -1763,18 +1739,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/foreground-confinement-timeout/tool-schemas.expected.json b/snapshots/session/foreground-confinement-timeout/tool-schemas.expected.json index 2dd1f341df..8dc90581f4 100644 --- a/snapshots/session/foreground-confinement-timeout/tool-schemas.expected.json +++ b/snapshots/session/foreground-confinement-timeout/tool-schemas.expected.json @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +408,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/fs-glob-sampling/tool-schemas.expected.json b/snapshots/session/fs-glob-sampling/tool-schemas.expected.json index 840733798a..3fb11acd59 100644 --- a/snapshots/session/fs-glob-sampling/tool-schemas.expected.json +++ b/snapshots/session/fs-glob-sampling/tool-schemas.expected.json @@ -297,7 +297,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -312,18 +312,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/lsp-definition/tool-schemas.expected.json b/snapshots/session/lsp-definition/tool-schemas.expected.json index 473b6cc111..c9da49006c 100644 --- a/snapshots/session/lsp-definition/tool-schemas.expected.json +++ b/snapshots/session/lsp-definition/tool-schemas.expected.json @@ -430,7 +430,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -445,18 +445,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/mcp-resources-ptc/system-prompt.expected.md b/snapshots/session/mcp-resources-ptc/system-prompt.expected.md index 48f67a9d80..da3f0b8ab7 100644 --- a/snapshots/session/mcp-resources-ptc/system-prompt.expected.md +++ b/snapshots/session/mcp-resources-ptc/system-prompt.expected.md @@ -217,7 +217,7 @@ interface ToolArgsMap { /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -225,12 +225,6 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ - provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ - model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ - reasoning_effort?: string; } & Record; /** Record and update a task list to plan multi-step work and show progress; skip it for trivial single-step tasks. Add one todo per concrete step before you start. While work remains, keep the todos being worked on `in_progress`, several only when work runs in parallel. Mark each todo `completed` as soon as it is done. */ todo_write: { diff --git a/snapshots/session/mcp-resources/tool-schemas.expected.json b/snapshots/session/mcp-resources/tool-schemas.expected.json index 5a7f33f534..b7e47f91c6 100644 --- a/snapshots/session/mcp-resources/tool-schemas.expected.json +++ b/snapshots/session/mcp-resources/tool-schemas.expected.json @@ -454,7 +454,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -469,18 +469,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/multimodal-spill-ends/tool-schemas.expected.json b/snapshots/session/multimodal-spill-ends/tool-schemas.expected.json index dec41c176b..8cfd4f8c9f 100644 --- a/snapshots/session/multimodal-spill-ends/tool-schemas.expected.json +++ b/snapshots/session/multimodal-spill-ends/tool-schemas.expected.json @@ -412,7 +412,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -427,18 +427,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/office-progressive-native/tool-schemas.expected.json b/snapshots/session/office-progressive-native/tool-schemas.expected.json index 7269632ffd..f43f78ab5f 100644 --- a/snapshots/session/office-progressive-native/tool-schemas.expected.json +++ b/snapshots/session/office-progressive-native/tool-schemas.expected.json @@ -348,7 +348,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -363,18 +363,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/office-progressive-ptc/system-prompt.expected.md b/snapshots/session/office-progressive-ptc/system-prompt.expected.md index e56a80173c..c5fc982e04 100644 --- a/snapshots/session/office-progressive-ptc/system-prompt.expected.md +++ b/snapshots/session/office-progressive-ptc/system-prompt.expected.md @@ -167,7 +167,7 @@ interface ToolArgsMap { /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -175,12 +175,6 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ - provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ - model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ - reasoning_effort?: string; } & Record; /** Record and update a task list to plan multi-step work and show progress; skip it for trivial single-step tasks. Add one todo per concrete step before you start. While work remains, keep the todos being worked on `in_progress`, several only when work runs in parallel. Mark each todo `completed` as soon as it is done. */ todo_write: { diff --git a/snapshots/session/office-skills/tool-schemas.expected.json b/snapshots/session/office-skills/tool-schemas.expected.json index 7269632ffd..f43f78ab5f 100644 --- a/snapshots/session/office-skills/tool-schemas.expected.json +++ b/snapshots/session/office-skills/tool-schemas.expected.json @@ -348,7 +348,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -363,18 +363,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/persistent-pwsh-tool-turn/tool-schemas.expected.json b/snapshots/session/persistent-pwsh-tool-turn/tool-schemas.expected.json index bee9fc050c..eaefa6b809 100644 --- a/snapshots/session/persistent-pwsh-tool-turn/tool-schemas.expected.json +++ b/snapshots/session/persistent-pwsh-tool-turn/tool-schemas.expected.json @@ -268,7 +268,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -283,18 +283,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/plugin-manager-mcp/tool-schemas.expected.json b/snapshots/session/plugin-manager-mcp/tool-schemas.expected.json index bc73a4b1fb..c06f67618d 100644 --- a/snapshots/session/plugin-manager-mcp/tool-schemas.expected.json +++ b/snapshots/session/plugin-manager-mcp/tool-schemas.expected.json @@ -454,7 +454,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -469,18 +469,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ @@ -1259,7 +1247,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -1274,18 +1262,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/plugin-manager/tool-schemas.expected.json b/snapshots/session/plugin-manager/tool-schemas.expected.json index 47b4c83859..6357a8beab 100644 --- a/snapshots/session/plugin-manager/tool-schemas.expected.json +++ b/snapshots/session/plugin-manager/tool-schemas.expected.json @@ -454,7 +454,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -469,18 +469,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/product-subagent-both/tool-schemas.expected.json b/snapshots/session/product-subagent-both/tool-schemas.expected.json index 779cf01085..ad221ee080 100644 --- a/snapshots/session/product-subagent-both/tool-schemas.expected.json +++ b/snapshots/session/product-subagent-both/tool-schemas.expected.json @@ -493,7 +493,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -508,18 +508,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/product-subagent-codex/tool-schemas.expected.json b/snapshots/session/product-subagent-codex/tool-schemas.expected.json index 40da027b47..7df665c825 100644 --- a/snapshots/session/product-subagent-codex/tool-schemas.expected.json +++ b/snapshots/session/product-subagent-codex/tool-schemas.expected.json @@ -443,7 +443,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -458,18 +458,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json b/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json index d202776234..50fbaad047 100644 --- a/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json +++ b/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json @@ -418,7 +418,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -433,18 +433,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/provider-cwd/tool-schemas.expected.json b/snapshots/session/provider-cwd/tool-schemas.expected.json index 2dd1f341df..8dc90581f4 100644 --- a/snapshots/session/provider-cwd/tool-schemas.expected.json +++ b/snapshots/session/provider-cwd/tool-schemas.expected.json @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +408,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/ptc-node-read-only/system-prompt.expected.md b/snapshots/session/ptc-node-read-only/system-prompt.expected.md index 835372b136..43224a84f3 100644 --- a/snapshots/session/ptc-node-read-only/system-prompt.expected.md +++ b/snapshots/session/ptc-node-read-only/system-prompt.expected.md @@ -188,7 +188,7 @@ interface ToolArgsMap { /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -196,12 +196,6 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ - provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ - model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ - reasoning_effort?: string; } & Record; /** Echo template text such as {{item}}, {{model}}, or {{ item }} without substitution. */ template_echo: { diff --git a/snapshots/session/ptc-node-workspace/system-prompt.expected.md b/snapshots/session/ptc-node-workspace/system-prompt.expected.md index 835372b136..43224a84f3 100644 --- a/snapshots/session/ptc-node-workspace/system-prompt.expected.md +++ b/snapshots/session/ptc-node-workspace/system-prompt.expected.md @@ -188,7 +188,7 @@ interface ToolArgsMap { /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -196,12 +196,6 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ - provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ - model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ - reasoning_effort?: string; } & Record; /** Echo template text such as {{item}}, {{model}}, or {{ item }} without substitution. */ template_echo: { diff --git a/snapshots/session/ptc-persistent/system-prompt.expected.md b/snapshots/session/ptc-persistent/system-prompt.expected.md index 835372b136..43224a84f3 100644 --- a/snapshots/session/ptc-persistent/system-prompt.expected.md +++ b/snapshots/session/ptc-persistent/system-prompt.expected.md @@ -188,7 +188,7 @@ interface ToolArgsMap { /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -196,12 +196,6 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ - provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ - model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ - reasoning_effort?: string; } & Record; /** Echo template text such as {{item}}, {{model}}, or {{ item }} without substitution. */ template_echo: { diff --git a/snapshots/session/ptc-policy-change/system-prompt.expected.md b/snapshots/session/ptc-policy-change/system-prompt.expected.md index 8308b029e0..07099f54b6 100644 --- a/snapshots/session/ptc-policy-change/system-prompt.expected.md +++ b/snapshots/session/ptc-policy-change/system-prompt.expected.md @@ -188,7 +188,7 @@ interface ToolArgsMap { /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -196,12 +196,6 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ - provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ - model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ - reasoning_effort?: string; } & Record; /** Record and update a task list to plan multi-step work and show progress; skip it for trivial single-step tasks. Add one todo per concrete step before you start. While work remains, keep the todos being worked on `in_progress`, several only when work runs in parallel. Mark each todo `completed` as soon as it is done. */ todo_write: { diff --git a/snapshots/session/ptc-python-turn/system-prompt.expected.md b/snapshots/session/ptc-python-turn/system-prompt.expected.md index 41a8a14056..45143cc82e 100644 --- a/snapshots/session/ptc-python-turn/system-prompt.expected.md +++ b/snapshots/session/ptc-python-turn/system-prompt.expected.md @@ -396,12 +396,6 @@ class SubagentForkArgs(TypedDict): description: str # The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. prompt: str - # LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. - provider: NotRequired[str] - # Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. - model: NotRequired[str] - # Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. - reasoning_effort: NotRequired[str] # Additional keys beyond those declared are allowed. class SubagentForkOutput(TypedDict): @@ -567,7 +561,7 @@ class Tools(Protocol): async def subagent(self, args: SubagentArgs) -> SubagentOutput: """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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.""" async def subagent_fork(self, args: SubagentForkArgs) -> SubagentForkOutput: - """Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.""" + """Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.""" async def todo_write(self, args: TodoWriteArgs) -> TodoWriteOutput: """Record and update a task list to plan multi-step work and show progress; skip it for trivial single-step tasks. Add one todo per concrete step before you start. While work remains, keep the todos being worked on `in_progress`, several only when work runs in parallel. Mark each todo `completed` as soon as it is done.""" async def update_goal(self, args: UpdateGoalArgs) -> UpdateGoalOutput1 | UpdateGoalOutput2: diff --git a/snapshots/session/ptc-read-image/system-prompt.expected.md b/snapshots/session/ptc-read-image/system-prompt.expected.md index 38a2fa9fbc..0af031b0c8 100644 --- a/snapshots/session/ptc-read-image/system-prompt.expected.md +++ b/snapshots/session/ptc-read-image/system-prompt.expected.md @@ -188,7 +188,7 @@ interface ToolArgsMap { /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -196,12 +196,6 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ - provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ - model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ - reasoning_effort?: string; } & Record; /** Record and update a task list to plan multi-step work and show progress; skip it for trivial single-step tasks. Add one todo per concrete step before you start. While work remains, keep the todos being worked on `in_progress`, several only when work runs in parallel. Mark each todo `completed` as soon as it is done. */ todo_write: { diff --git a/snapshots/session/ptc-turn/system-prompt.expected.md b/snapshots/session/ptc-turn/system-prompt.expected.md index 835372b136..43224a84f3 100644 --- a/snapshots/session/ptc-turn/system-prompt.expected.md +++ b/snapshots/session/ptc-turn/system-prompt.expected.md @@ -188,7 +188,7 @@ interface ToolArgsMap { /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -196,12 +196,6 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ - provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ - model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ - reasoning_effort?: string; } & Record; /** Echo template text such as {{item}}, {{model}}, or {{ item }} without substitution. */ template_echo: { diff --git a/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json b/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json index d33cb92a78..816b7fcb6a 100644 --- a/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json +++ b/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +408,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/pwsh-tool-turn/tool-schemas.expected.json b/snapshots/session/pwsh-tool-turn/tool-schemas.expected.json index dcf23f6b31..f9cd850b97 100644 --- a/snapshots/session/pwsh-tool-turn/tool-schemas.expected.json +++ b/snapshots/session/pwsh-tool-turn/tool-schemas.expected.json @@ -349,7 +349,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -364,18 +364,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/ralph-loop/tool-schemas.1.expected.json b/snapshots/session/ralph-loop/tool-schemas.1.expected.json index 93d4b861a1..0bb4c382a8 100644 --- a/snapshots/session/ralph-loop/tool-schemas.1.expected.json +++ b/snapshots/session/ralph-loop/tool-schemas.1.expected.json @@ -456,7 +456,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -471,18 +471,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/ralph-loop/tool-schemas.2.expected.json b/snapshots/session/ralph-loop/tool-schemas.2.expected.json index 93d4b861a1..0bb4c382a8 100644 --- a/snapshots/session/ralph-loop/tool-schemas.2.expected.json +++ b/snapshots/session/ralph-loop/tool-schemas.2.expected.json @@ -456,7 +456,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -471,18 +471,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/ralph-loop/tool-schemas.expected.json b/snapshots/session/ralph-loop/tool-schemas.expected.json index 44c18da093..f67a9cc249 100644 --- a/snapshots/session/ralph-loop/tool-schemas.expected.json +++ b/snapshots/session/ralph-loop/tool-schemas.expected.json @@ -413,7 +413,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -428,18 +428,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/session-query-spill/tool-schemas.expected.json b/snapshots/session/session-query-spill/tool-schemas.expected.json index dd5544d7d4..18b7f4a24d 100644 --- a/snapshots/session/session-query-spill/tool-schemas.expected.json +++ b/snapshots/session/session-query-spill/tool-schemas.expected.json @@ -597,7 +597,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -612,18 +612,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json b/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json index f61fc21202..233a8b87a8 100644 --- a/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json +++ b/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json @@ -418,7 +418,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -433,18 +433,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json b/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json index cd84466ebf..ac4d94f100 100644 --- a/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json +++ b/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json @@ -456,7 +456,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -471,18 +471,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/subagent-no-message-tool/tool-schemas.1.expected.json b/snapshots/session/subagent-no-message-tool/tool-schemas.1.expected.json index 960e60afd0..045312a524 100644 --- a/snapshots/session/subagent-no-message-tool/tool-schemas.1.expected.json +++ b/snapshots/session/subagent-no-message-tool/tool-schemas.1.expected.json @@ -286,7 +286,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -301,18 +301,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/subagent-structured-parent/tool-schemas.1.expected.json b/snapshots/session/subagent-structured-parent/tool-schemas.1.expected.json index 3e450f980e..9c47c0c649 100644 --- a/snapshots/session/subagent-structured-parent/tool-schemas.1.expected.json +++ b/snapshots/session/subagent-structured-parent/tool-schemas.1.expected.json @@ -408,7 +408,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -423,18 +423,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/subagent-structured-parent/tool-schemas.expected.json b/snapshots/session/subagent-structured-parent/tool-schemas.expected.json index 8ebad49ed3..2eaec5d921 100644 --- a/snapshots/session/subagent-structured-parent/tool-schemas.expected.json +++ b/snapshots/session/subagent-structured-parent/tool-schemas.expected.json @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +408,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/subagent-tool-filter/tool-schemas.1.expected.json b/snapshots/session/subagent-tool-filter/tool-schemas.1.expected.json index c38ea849b9..f9cbf2b0d8 100644 --- a/snapshots/session/subagent-tool-filter/tool-schemas.1.expected.json +++ b/snapshots/session/subagent-tool-filter/tool-schemas.1.expected.json @@ -307,7 +307,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -322,18 +322,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/system-prompt-in-history/tool-schemas.expected.json b/snapshots/session/system-prompt-in-history/tool-schemas.expected.json index 2dd1f341df..8dc90581f4 100644 --- a/snapshots/session/system-prompt-in-history/tool-schemas.expected.json +++ b/snapshots/session/system-prompt-in-history/tool-schemas.expected.json @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +408,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/text-turn/tool-schemas.expected.json b/snapshots/session/text-turn/tool-schemas.expected.json index 2dd1f341df..8dc90581f4 100644 --- a/snapshots/session/text-turn/tool-schemas.expected.json +++ b/snapshots/session/text-turn/tool-schemas.expected.json @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +408,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/web-fetch/tool-schemas.expected.json b/snapshots/session/web-fetch/tool-schemas.expected.json index 11dba4b997..40cc9548ef 100644 --- a/snapshots/session/web-fetch/tool-schemas.expected.json +++ b/snapshots/session/web-fetch/tool-schemas.expected.json @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +408,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/workflow-confinement/tool-schemas.expected.json b/snapshots/session/workflow-confinement/tool-schemas.expected.json index 2dd1f341df..8dc90581f4 100644 --- a/snapshots/session/workflow-confinement/tool-schemas.expected.json +++ b/snapshots/session/workflow-confinement/tool-schemas.expected.json @@ -393,7 +393,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +408,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/workspace-dependencies/tool-schemas.expected.json b/snapshots/session/workspace-dependencies/tool-schemas.expected.json index cca2b4d36a..9bd9bd41b5 100644 --- a/snapshots/session/workspace-dependencies/tool-schemas.expected.json +++ b/snapshots/session/workspace-dependencies/tool-schemas.expected.json @@ -356,7 +356,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +371,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/session/worktree/tool-schemas.expected.json b/snapshots/session/worktree/tool-schemas.expected.json index a18f81001c..dd66d0a51f 100644 --- a/snapshots/session/worktree/tool-schemas.expected.json +++ b/snapshots/session/worktree/tool-schemas.expected.json @@ -410,7 +410,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -425,18 +425,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/web/fresh-round-trip/tool-schemas.expected.json b/snapshots/web/fresh-round-trip/tool-schemas.expected.json index 02a74c4f52..26471f7526 100644 --- a/snapshots/web/fresh-round-trip/tool-schemas.expected.json +++ b/snapshots/web/fresh-round-trip/tool-schemas.expected.json @@ -756,7 +756,7 @@ }, { "name": "subagent_fork", - "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix.", + "description": "Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -771,18 +771,6 @@ "prompt": { "type": "string", "description": "The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new." - }, - "provider": { - "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." - }, - "model": { - "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." - }, - "reasoning_effort": { - "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." } }, "required": [ diff --git a/snapshots/web/official-subagent-bundle/system-prompt.expected.md b/snapshots/web/official-subagent-bundle/system-prompt.expected.md index 8908ece5a4..dd702b72a4 100644 --- a/snapshots/web/official-subagent-bundle/system-prompt.expected.md +++ b/snapshots/web/official-subagent-bundle/system-prompt.expected.md @@ -312,7 +312,7 @@ interface ToolArgsMap { /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -320,12 +320,6 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ - provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ - model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ - reasoning_effort?: string; } & Record; /** Record and update a task list to plan multi-step work and show progress; skip it for trivial single-step tasks. Add one todo per concrete step before you start. While work remains, keep the todos being worked on `in_progress`, several only when work runs in parallel. Mark each todo `completed` as soon as it is done. */ todo_write: { diff --git a/snapshots/web/optional-string-editor/system-prompt.expected.md b/snapshots/web/optional-string-editor/system-prompt.expected.md index 00fa427842..597ebe90cd 100644 --- a/snapshots/web/optional-string-editor/system-prompt.expected.md +++ b/snapshots/web/optional-string-editor/system-prompt.expected.md @@ -320,7 +320,7 @@ interface ToolArgsMap { /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -328,12 +328,6 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ - provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ - model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ - reasoning_effort?: string; } & Record; /** Record and update a task list to plan multi-step work and show progress; skip it for trivial single-step tasks. Add one todo per concrete step before you start. While work remains, keep the todos being worked on `in_progress`, several only when work runs in parallel. Mark each todo `completed` as soon as it is done. */ todo_write: { diff --git a/snapshots/web/ptc-round/system-prompt.expected.md b/snapshots/web/ptc-round/system-prompt.expected.md index 0c53fb848d..731d7cc7b4 100644 --- a/snapshots/web/ptc-round/system-prompt.expected.md +++ b/snapshots/web/ptc-round/system-prompt.expected.md @@ -303,7 +303,7 @@ interface ToolArgsMap { /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ reasoning_effort?: string; } & Record; - /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. Changing the route can prevent provider-side reuse of the inherited conversation prefix. */ + /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent_fork: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -311,12 +311,6 @@ interface ToolArgsMap { description: string; /** The task for the subagent. It already sees this conversation's completed turns, so build on them freely and state only what is new. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ - provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ - model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ - reasoning_effort?: string; } & Record; /** Record and update a task list to plan multi-step work and show progress; skip it for trivial single-step tasks. Add one todo per concrete step before you start. While work remains, keep the todos being worked on `in_progress`, several only when work runs in parallel. Mark each todo `completed` as soon as it is done. */ todo_write: { From 56affefb19d9f6888cd90f22295a3bb3b2801acf Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Mon, 12 Oct 2026 17:20:06 +0800 Subject: [PATCH 044/369] refactor(subagent): shorten model-selection tool descriptions --- docs/tool-catalog.i18n.yaml | 8 ++-- docs/tool-catalog.md | 14 +++---- docs/tool-catalog.zh.md | 14 +++---- packages/subagent/tool-subagent/src/index.ts | 16 +++---- .../subagent/tool-subagent/src/list-models.ts | 12 +++--- .../tool-subagent/tests/tool-subagent.spec.ts | 4 +- .../sdk/bash-tool/tool-schemas.expected.json | 14 +++---- .../tool-schemas.1.expected.json | 14 +++---- .../tool-schemas.1.expected.json | 14 +++---- .../tool-schemas.1.expected.json | 14 +++---- .../tool-schemas.expected.json | 30 ++++++------- .../tool-schemas.1.expected.json | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../tool-schemas.1.expected.json | 14 +++---- .../tool-schemas.1.expected.json | 14 +++---- .../sdk/text-turn/tool-schemas.expected.json | 14 +++---- .../system-prompt.expected.md | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../system-prompt.expected.md | 14 +++---- .../tool-schemas.expected.json | 28 ++++++------- .../tool-schemas.expected.json | 28 ++++++------- .../tool-schemas.expected.json | 14 +++---- .../both-mode-turn/system-prompt.expected.md | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../tool-schemas.expected.json | 28 ++++++------- .../tool-schemas.expected.json | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../tool-schemas.expected.json | 42 +++++++++---------- .../tool-schemas.expected.json | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../lsp-definition/tool-schemas.expected.json | 14 +++---- .../system-prompt.expected.md | 14 +++---- .../mcp-resources/tool-schemas.expected.json | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../system-prompt.expected.md | 14 +++---- .../office-skills/tool-schemas.expected.json | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../tool-schemas.expected.json | 28 ++++++------- .../plugin-manager/tool-schemas.expected.json | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../provider-cwd/tool-schemas.expected.json | 14 +++---- .../system-prompt.expected.md | 14 +++---- .../system-prompt.expected.md | 14 +++---- .../ptc-persistent/system-prompt.expected.md | 14 +++---- .../system-prompt.expected.md | 14 +++---- .../ptc-python-turn/system-prompt.expected.md | 14 +++---- .../ptc-read-image/system-prompt.expected.md | 14 +++---- .../ptc-turn/system-prompt.expected.md | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../pwsh-tool-turn/tool-schemas.expected.json | 14 +++---- .../ralph-loop/tool-schemas.1.expected.json | 14 +++---- .../ralph-loop/tool-schemas.2.expected.json | 14 +++---- .../ralph-loop/tool-schemas.expected.json | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../tool-schemas.1.expected.json | 14 +++---- .../tool-schemas.1.expected.json | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../tool-schemas.1.expected.json | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../team-targets/tool-schemas.expected.json | 6 +-- .../text-turn/tool-schemas.expected.json | 14 +++---- .../web-fetch/tool-schemas.expected.json | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../worktree/tool-schemas.expected.json | 14 +++---- .../tool-schemas.expected.json | 14 +++---- .../system-prompt.expected.md | 14 +++---- .../system-prompt.expected.md | 14 +++---- .../web/ptc-round/system-prompt.expected.md | 14 +++---- 80 files changed, 594 insertions(+), 602 deletions(-) diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml index 13a0309388..4b23c19b5c 100644 --- a/docs/tool-catalog.i18n.yaml +++ b/docs/tool-catalog.i18n.yaml @@ -225,11 +225,11 @@ en: 841abe0dd5913ea7 zh: 841abe0dd5913ea7 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent/list-subagent-models: - en: aba834ab4f70005a - zh: 142f819e8da5acc7 + en: 6872bee11fccb6c8 + zh: 230f2d443dc5d9a6 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent/subagent: - en: 0de8d49aa443ec9c - zh: 7cb7c8a55a0469b1 + en: aff4a6668b41b7d5 + zh: 98d6ddf8aa28c7f9 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control: en: ab82e130c3413f04 zh: ab82e130c3413f04 diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index 9c94c9d1d7..769ad76242 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -2017,7 +2017,7 @@ The five read-only tools hide provider cursors and authorize every result from t ### `list_subagent_models` -Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. +Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. ```json { @@ -2025,11 +2025,11 @@ Discover LLM routes for subagents without changing the current Agent. Call with "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -2039,7 +2039,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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. +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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. ```json { @@ -2059,15 +2059,15 @@ Delegate a self-contained task to a subagent (a separate agent that works in its }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index be7b085212..a91c05a5cb 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -2023,7 +2023,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, ### `list_subagent_models` -发现 subagent 可用的 LLM 路由,不更改当前 Agent。无参数调用会列出已注册提供方;提供 `provider` 时会列出其允许的模型;同时提供 `provider` 和 `model` 时会检查该精确模型及其推理强度。目录条目只提供建议:adapter 可能接受未列出的模型 id。把返回的 id 用于委派工具的 `provider`、`model` 与 `reasoning_effort` 字段。 +查询 subagent 可用的模型,不更改当前 Agent。无参数:提供方;`provider`:允许的模型;`provider` + `model`:该模型及其推理强度。将返回的 id 用于委派。 ```json { @@ -2031,11 +2031,11 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -2045,7 +2045,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, ### `subagent` -将一项自包含任务委派给 subagent(在自身上下文中工作的独立 agent),用它卸载聚焦且独立的工作,例如研究、限定范围的实现或分析,以免消耗当前对话的上下文。subagent 会返回结果,但不会返回中间步骤。此工具启动独立管理的 subagent 并立即返回其 id。运行时会在任务结束时通知你。子代理通过 `send_message` 汇报结果;你可以使用 `send_message` 在运行时引导它,或在结束后继续对话。子级 LLM 选择是可选的。省略 `provider`、`model` 和 `reasoning_effort` 会使用配置的子级默认值,并从父 Agent 继承缺失且兼容的值。先通过 `list_subagent_models` 查询路由和强度,再同时提供 `provider` 与 `model`。更改生效路由却不指定强度时,使用所选模型的默认强度。 +将一项自包含任务委派给 subagent(在自身上下文中工作的独立 agent),用它卸载聚焦且独立的工作,例如研究、限定范围的实现或分析,以免消耗当前对话的上下文。subagent 会返回结果,但不会返回中间步骤。此工具启动独立管理的 subagent 并立即返回其 id。运行时会在任务结束时通知你。子代理通过 `send_message` 汇报结果;你可以使用 `send_message` 在运行时引导它,或在结束后继续对话。先通过 `list_subagent_models` 查询选项,再同时设置 `provider` 与 `model`。省略的值使用子级默认值,缺失时使用兼容的父级值。生效路由改变时,省略 `reasoning_effort` 会使用模型的默认强度。 ```json { @@ -2065,15 +2065,15 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/packages/subagent/tool-subagent/src/index.ts b/packages/subagent/tool-subagent/src/index.ts index 6cba7d3490..19af35e38f 100644 --- a/packages/subagent/tool-subagent/src/index.ts +++ b/packages/subagent/tool-subagent/src/index.ts @@ -210,8 +210,8 @@ export function apply(ctx: Context, config: Config, session?: Session): void { const wording = providerWording(subagentProvider.inheritsParentContext) const providerRouteDefaults = subagentProvider.agentRouteDefaults const selectionDescription = providerRouteDefaults !== undefined - ? ' Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and this provider\'s route defaults. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model\'s default effort.' - : ' Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model\'s default effort.' + ? ' Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to this backend\'s route defaults. When the effective route changes, an omitted `reasoning_effort` uses the model\'s default.' + : ' Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model\'s default.' const definition = defineTool({ name: toolName, description: wording.description @@ -238,21 +238,15 @@ export function apply(ctx: Context, config: Config, session?: Session): void { ...modelSelectionEnabled ? { provider: { type: 'string' as const, - description: providerRouteDefaults !== undefined - ? 'LLM provider route for the child. Supply together with model; omit both to use configured child defaults or this provider\'s route defaults.' - : 'LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route.', + description: 'Child LLM provider id.', }, model: { type: 'string' as const, - description: providerRouteDefaults !== undefined - ? 'Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or this provider\'s route defaults.' - : 'Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route.', + description: 'Model id within that provider.', }, reasoning_effort: { type: 'string' as const, - description: providerRouteDefaults !== undefined - ? 'Adapter-owned reasoning effort for the effective child route. Omit to use a compatible configured effort or the selected model\'s default.' - : 'Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model\'s default.', + description: 'Model-specific reasoning effort id.', }, } : {}, diff --git a/packages/subagent/tool-subagent/src/list-models.ts b/packages/subagent/tool-subagent/src/list-models.ts index 950cf09e72..5c100eedf6 100644 --- a/packages/subagent/tool-subagent/src/list-models.ts +++ b/packages/subagent/tool-subagent/src/list-models.ts @@ -104,19 +104,17 @@ export function registerListSubagentModels(ctx: Context, policyFor: (exec: ToolR ctx.tools.register(defineTool({ name: 'list_subagent_models', description: - 'Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list ' - + 'registered providers, with `provider` to list its allowed models, or with `provider` and `model` ' - + 'to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may ' - + 'accept an unlisted model id. Use the returned ids with a delegation tool\'s `provider`, `model`, and ' - + '`reasoning_effort` fields.', + 'Inspect subagent models without changing the current Agent. No arguments: providers; ' + + '`provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. ' + + 'Use returned ids for delegation.', parameters: { provider: { type: 'string', - description: 'Registered LLM provider id. Omit to list providers.', + description: 'Registered LLM provider id.', }, model: { type: 'string', - description: 'Exact model id to inspect. Requires provider; omit to list that provider\'s allowed models.', + description: 'Exact model id; requires provider.', }, }, output: { diff --git a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts index 9ddecc530f..4a0a1a6a08 100644 --- a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts +++ b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts @@ -266,7 +266,7 @@ describe('dsh-tool-subagent', () => { model: 'child-model', }) expect(ctx.tools.schemas(modelSelectionSetupAgent(ctx)).find(schema => schema.name === 'subagent')?.description) - .toContain('this provider\'s route defaults') + .toContain('this backend\'s route defaults') expect(seen?.agentOptions).toEqual({ provider: 'alpha', model: 'child-model', @@ -513,7 +513,7 @@ describe('dsh-tool-subagent', () => { const schema = ctx.tools.schemas().find(s => s.name === 'subagent')! expect(schema.description).toContain('inherits this conversation') expect(schema.description).not.toContain('does not see this conversation') - expect(schema.description).not.toContain('Child LLM selection') + expect(schema.description).not.toContain('Inspect options with `list_subagent_models`') const props = (schema.parameters as { properties: Record }).properties expect(props['prompt']!.description).toContain('completed turns') }) diff --git a/snapshots/sdk/bash-tool/tool-schemas.expected.json b/snapshots/sdk/bash-tool/tool-schemas.expected.json index 8dc90581f4..185fd4ef83 100644 --- a/snapshots/sdk/bash-tool/tool-schemas.expected.json +++ b/snapshots/sdk/bash-tool/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/sdk/subagent-activation-limit/tool-schemas.1.expected.json b/snapshots/sdk/subagent-activation-limit/tool-schemas.1.expected.json index 8dc90581f4..185fd4ef83 100644 --- a/snapshots/sdk/subagent-activation-limit/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-activation-limit/tool-schemas.1.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json b/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json index 8dc90581f4..185fd4ef83 100644 --- a/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json b/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json index 8dc90581f4..185fd4ef83 100644 --- a/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json b/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json index e13cfd3bb0..ef3f413d82 100644 --- a/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json +++ b/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ @@ -393,7 +393,7 @@ }, { "name": "subagent_dsh_sdk", - "description": "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. The runtime notifies you when it finishes. The completion notice includes its final answer. This backend does not accept follow-up messages. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and this provider's route defaults. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The completion notice includes its final answer. This backend does not accept follow-up messages. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to this backend's route defaults. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -411,15 +411,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or this provider's route defaults." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or this provider's route defaults." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to use a compatible configured effort or the selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ @@ -430,7 +430,7 @@ }, { "name": "subagent_dsh_sdk_second", - "description": "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. The runtime notifies you when it finishes. The completion notice includes its final answer. This backend does not accept follow-up messages. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and this provider's route defaults. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The completion notice includes its final answer. This backend does not accept follow-up messages. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to this backend's route defaults. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -448,15 +448,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or this provider's route defaults." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or this provider's route defaults." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to use a compatible configured effort or the selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json index 8dc90581f4..185fd4ef83 100644 --- a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json index 5f0e685eb2..85e10ae4f2 100644 --- a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json +++ b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The completion notice includes its final answer. This backend does not accept follow-up messages. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and this provider's route defaults. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The completion notice includes its final answer. This backend does not accept follow-up messages. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to this backend's route defaults. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or this provider's route defaults." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or this provider's route defaults." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to use a compatible configured effort or the selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json b/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json index 8dc90581f4..185fd4ef83 100644 --- a/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json b/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json index 8dc90581f4..185fd4ef83 100644 --- a/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/sdk/text-turn/tool-schemas.expected.json b/snapshots/sdk/text-turn/tool-schemas.expected.json index 8dc90581f4..185fd4ef83 100644 --- a/snapshots/sdk/text-turn/tool-schemas.expected.json +++ b/snapshots/sdk/text-turn/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/sdk/tool-error-details/system-prompt.expected.md b/snapshots/sdk/tool-error-details/system-prompt.expected.md index 4dcf8262a6..49a6727bc7 100644 --- a/snapshots/sdk/tool-error-details/system-prompt.expected.md +++ b/snapshots/sdk/tool-error-details/system-prompt.expected.md @@ -173,11 +173,11 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ list_subagent_models: { - /** Registered LLM provider id. Omit to list providers. */ + /** Registered LLM provider id. */ provider?: string; - /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + /** Exact model id; requires provider. */ model?: string; } & Record; /** Read a UTF-8 text file and return line-numbered content. */ @@ -206,7 +206,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -214,11 +214,11 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + /** Child LLM provider id. */ provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + /** Model id within that provider. */ model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + /** Model-specific reasoning effort id. */ reasoning_effort?: string; } & Record; /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ diff --git a/snapshots/sdk/tool-error-details/tool-schemas.expected.json b/snapshots/sdk/tool-error-details/tool-schemas.expected.json index 77b5236350..628380a1d5 100644 --- a/snapshots/sdk/tool-error-details/tool-schemas.expected.json +++ b/snapshots/sdk/tool-error-details/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/advanced-toolchain/system-prompt.expected.md b/snapshots/session/advanced-toolchain/system-prompt.expected.md index 14d8e2559d..2cd6250c0f 100644 --- a/snapshots/session/advanced-toolchain/system-prompt.expected.md +++ b/snapshots/session/advanced-toolchain/system-prompt.expected.md @@ -186,11 +186,11 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ list_subagent_models: { - /** Registered LLM provider id. Omit to list providers. */ + /** Registered LLM provider id. */ provider?: string; - /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + /** Exact model id; requires provider. */ model?: string; } & Record; /** Read a UTF-8 text file and return line-numbered content. */ @@ -219,7 +219,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -227,11 +227,11 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + /** Child LLM provider id. */ provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + /** Model id within that provider. */ model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + /** Model-specific reasoning effort id. */ reasoning_effort?: string; } & Record; /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ diff --git a/snapshots/session/advanced-toolchain/tool-schemas.expected.json b/snapshots/session/advanced-toolchain/tool-schemas.expected.json index f8cbf6b810..89dd15e503 100644 --- a/snapshots/session/advanced-toolchain/tool-schemas.expected.json +++ b/snapshots/session/advanced-toolchain/tool-schemas.expected.json @@ -303,17 +303,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -397,7 +397,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -415,15 +415,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ @@ -1045,17 +1045,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -1139,7 +1139,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -1157,15 +1157,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/agent-instructions/tool-schemas.expected.json b/snapshots/session/agent-instructions/tool-schemas.expected.json index 5018eac594..ffcb7cdbcb 100644 --- a/snapshots/session/agent-instructions/tool-schemas.expected.json +++ b/snapshots/session/agent-instructions/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ @@ -924,17 +924,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -1018,7 +1018,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -1036,15 +1036,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/background-confinement-failure/tool-schemas.expected.json b/snapshots/session/background-confinement-failure/tool-schemas.expected.json index 8dc90581f4..185fd4ef83 100644 --- a/snapshots/session/background-confinement-failure/tool-schemas.expected.json +++ b/snapshots/session/background-confinement-failure/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/both-mode-turn/system-prompt.expected.md b/snapshots/session/both-mode-turn/system-prompt.expected.md index 69bc221437..da16df0b27 100644 --- a/snapshots/session/both-mode-turn/system-prompt.expected.md +++ b/snapshots/session/both-mode-turn/system-prompt.expected.md @@ -140,11 +140,11 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ list_subagent_models: { - /** Registered LLM provider id. Omit to list providers. */ + /** Registered LLM provider id. */ provider?: string; - /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + /** Exact model id; requires provider. */ model?: string; } & Record; /** Read a UTF-8 text file and return line-numbered content. */ @@ -173,7 +173,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -181,11 +181,11 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + /** Child LLM provider id. */ provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + /** Model id within that provider. */ model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + /** Model-specific reasoning effort id. */ reasoning_effort?: string; } & Record; /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ diff --git a/snapshots/session/browser-use-chrome-devtools-mcp/tool-schemas.expected.json b/snapshots/session/browser-use-chrome-devtools-mcp/tool-schemas.expected.json index 763c4962d9..f96956dd76 100644 --- a/snapshots/session/browser-use-chrome-devtools-mcp/tool-schemas.expected.json +++ b/snapshots/session/browser-use-chrome-devtools-mcp/tool-schemas.expected.json @@ -302,17 +302,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -463,7 +463,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -481,15 +481,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/browser-use-playwright-mcp/tool-schemas.expected.json b/snapshots/session/browser-use-playwright-mcp/tool-schemas.expected.json index f8db659ff3..2fc2120472 100644 --- a/snapshots/session/browser-use-playwright-mcp/tool-schemas.expected.json +++ b/snapshots/session/browser-use-playwright-mcp/tool-schemas.expected.json @@ -302,17 +302,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -465,7 +465,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -483,15 +483,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/browser-use-stagehand-native/tool-schemas.expected.json b/snapshots/session/browser-use-stagehand-native/tool-schemas.expected.json index f3d955b641..d8f286c03c 100644 --- a/snapshots/session/browser-use-stagehand-native/tool-schemas.expected.json +++ b/snapshots/session/browser-use-stagehand-native/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -569,7 +569,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -587,15 +587,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/claude-code-mods/tool-schemas.expected.json b/snapshots/session/claude-code-mods/tool-schemas.expected.json index 8dc90581f4..185fd4ef83 100644 --- a/snapshots/session/claude-code-mods/tool-schemas.expected.json +++ b/snapshots/session/claude-code-mods/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/compaction-recovery/tool-schemas.expected.json b/snapshots/session/compaction-recovery/tool-schemas.expected.json index 5018eac594..ffcb7cdbcb 100644 --- a/snapshots/session/compaction-recovery/tool-schemas.expected.json +++ b/snapshots/session/compaction-recovery/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ @@ -924,17 +924,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -1018,7 +1018,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -1036,15 +1036,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/computer-use-cua-driver-mcp/tool-schemas.expected.json b/snapshots/session/computer-use-cua-driver-mcp/tool-schemas.expected.json index 1107b15b8f..4a4cca52f0 100644 --- a/snapshots/session/computer-use-cua-driver-mcp/tool-schemas.expected.json +++ b/snapshots/session/computer-use-cua-driver-mcp/tool-schemas.expected.json @@ -302,17 +302,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -442,7 +442,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -460,15 +460,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/computer-use-cua-driver-native/tool-schemas.expected.json b/snapshots/session/computer-use-cua-driver-native/tool-schemas.expected.json index 0bf5c52da2..3957dbc2f8 100644 --- a/snapshots/session/computer-use-cua-driver-native/tool-schemas.expected.json +++ b/snapshots/session/computer-use-cua-driver-native/tool-schemas.expected.json @@ -308,17 +308,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -402,7 +402,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -420,15 +420,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json b/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json index e8770b87d3..2e5f4fc018 100644 --- a/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json +++ b/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json @@ -303,17 +303,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -397,7 +397,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -415,15 +415,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/deepseek-messages-system-prompt/tool-schemas.expected.json b/snapshots/session/deepseek-messages-system-prompt/tool-schemas.expected.json index 8dc90581f4..185fd4ef83 100644 --- a/snapshots/session/deepseek-messages-system-prompt/tool-schemas.expected.json +++ b/snapshots/session/deepseek-messages-system-prompt/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/deepseek-protocol-system-prompt/tool-schemas.expected.json b/snapshots/session/deepseek-protocol-system-prompt/tool-schemas.expected.json index 8dc90581f4..185fd4ef83 100644 --- a/snapshots/session/deepseek-protocol-system-prompt/tool-schemas.expected.json +++ b/snapshots/session/deepseek-protocol-system-prompt/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/dynamic-tool-updates/tool-schemas.expected.json b/snapshots/session/dynamic-tool-updates/tool-schemas.expected.json index de74610301..798fc8024b 100644 --- a/snapshots/session/dynamic-tool-updates/tool-schemas.expected.json +++ b/snapshots/session/dynamic-tool-updates/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ @@ -924,17 +924,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -1026,7 +1026,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -1044,15 +1044,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ @@ -1593,17 +1593,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -1687,7 +1687,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -1705,15 +1705,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/foreground-confinement-timeout/tool-schemas.expected.json b/snapshots/session/foreground-confinement-timeout/tool-schemas.expected.json index 8dc90581f4..185fd4ef83 100644 --- a/snapshots/session/foreground-confinement-timeout/tool-schemas.expected.json +++ b/snapshots/session/foreground-confinement-timeout/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/fs-glob-sampling/tool-schemas.expected.json b/snapshots/session/fs-glob-sampling/tool-schemas.expected.json index 3fb11acd59..03764bcd7e 100644 --- a/snapshots/session/fs-glob-sampling/tool-schemas.expected.json +++ b/snapshots/session/fs-glob-sampling/tool-schemas.expected.json @@ -182,17 +182,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -260,7 +260,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -278,15 +278,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/lsp-definition/tool-schemas.expected.json b/snapshots/session/lsp-definition/tool-schemas.expected.json index c9da49006c..2bc62b0f70 100644 --- a/snapshots/session/lsp-definition/tool-schemas.expected.json +++ b/snapshots/session/lsp-definition/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -393,7 +393,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -411,15 +411,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/mcp-resources-ptc/system-prompt.expected.md b/snapshots/session/mcp-resources-ptc/system-prompt.expected.md index da3f0b8ab7..b81f602015 100644 --- a/snapshots/session/mcp-resources-ptc/system-prompt.expected.md +++ b/snapshots/session/mcp-resources-ptc/system-prompt.expected.md @@ -162,11 +162,11 @@ interface ToolArgsMap { /** Continuation cursor returned by this server. */ cursor?: string; } & Record; - /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ list_subagent_models: { - /** Registered LLM provider id. Omit to list providers. */ + /** Registered LLM provider id. */ provider?: string; - /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + /** Exact model id; requires provider. */ model?: string; } & Record; /** Read a UTF-8 text file and return line-numbered content. */ @@ -202,7 +202,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -210,11 +210,11 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + /** Child LLM provider id. */ provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + /** Model id within that provider. */ model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + /** Model-specific reasoning effort id. */ reasoning_effort?: string; } & Record; /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ diff --git a/snapshots/session/mcp-resources/tool-schemas.expected.json b/snapshots/session/mcp-resources/tool-schemas.expected.json index b7e47f91c6..fa72eb6521 100644 --- a/snapshots/session/mcp-resources/tool-schemas.expected.json +++ b/snapshots/session/mcp-resources/tool-schemas.expected.json @@ -302,17 +302,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -417,7 +417,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -435,15 +435,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/multimodal-spill-ends/tool-schemas.expected.json b/snapshots/session/multimodal-spill-ends/tool-schemas.expected.json index 8cfd4f8c9f..fe1f0ce7a4 100644 --- a/snapshots/session/multimodal-spill-ends/tool-schemas.expected.json +++ b/snapshots/session/multimodal-spill-ends/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -375,7 +375,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -393,15 +393,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/office-progressive-native/tool-schemas.expected.json b/snapshots/session/office-progressive-native/tool-schemas.expected.json index f43f78ab5f..36be4284f4 100644 --- a/snapshots/session/office-progressive-native/tool-schemas.expected.json +++ b/snapshots/session/office-progressive-native/tool-schemas.expected.json @@ -217,17 +217,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -311,7 +311,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -329,15 +329,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/office-progressive-ptc/system-prompt.expected.md b/snapshots/session/office-progressive-ptc/system-prompt.expected.md index c5fc982e04..e9119ddc55 100644 --- a/snapshots/session/office-progressive-ptc/system-prompt.expected.md +++ b/snapshots/session/office-progressive-ptc/system-prompt.expected.md @@ -119,11 +119,11 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ list_subagent_models: { - /** Registered LLM provider id. Omit to list providers. */ + /** Registered LLM provider id. */ provider?: string; - /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + /** Exact model id; requires provider. */ model?: string; } & Record; /** Read a UTF-8 text file and return line-numbered content. */ @@ -152,7 +152,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -160,11 +160,11 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + /** Child LLM provider id. */ provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + /** Model id within that provider. */ model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + /** Model-specific reasoning effort id. */ reasoning_effort?: string; } & Record; /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ diff --git a/snapshots/session/office-skills/tool-schemas.expected.json b/snapshots/session/office-skills/tool-schemas.expected.json index f43f78ab5f..36be4284f4 100644 --- a/snapshots/session/office-skills/tool-schemas.expected.json +++ b/snapshots/session/office-skills/tool-schemas.expected.json @@ -217,17 +217,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -311,7 +311,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -329,15 +329,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/persistent-pwsh-tool-turn/tool-schemas.expected.json b/snapshots/session/persistent-pwsh-tool-turn/tool-schemas.expected.json index eaefa6b809..4195c5187a 100644 --- a/snapshots/session/persistent-pwsh-tool-turn/tool-schemas.expected.json +++ b/snapshots/session/persistent-pwsh-tool-turn/tool-schemas.expected.json @@ -137,17 +137,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -231,7 +231,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -249,15 +249,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/plugin-manager-mcp/tool-schemas.expected.json b/snapshots/session/plugin-manager-mcp/tool-schemas.expected.json index c06f67618d..ca83d406e0 100644 --- a/snapshots/session/plugin-manager-mcp/tool-schemas.expected.json +++ b/snapshots/session/plugin-manager-mcp/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -417,7 +417,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -435,15 +435,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ @@ -1025,17 +1025,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -1210,7 +1210,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -1228,15 +1228,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/plugin-manager/tool-schemas.expected.json b/snapshots/session/plugin-manager/tool-schemas.expected.json index 6357a8beab..62bdec5dc1 100644 --- a/snapshots/session/plugin-manager/tool-schemas.expected.json +++ b/snapshots/session/plugin-manager/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -417,7 +417,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -435,15 +435,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/product-subagent-both/tool-schemas.expected.json b/snapshots/session/product-subagent-both/tool-schemas.expected.json index ad221ee080..08ca2b1a53 100644 --- a/snapshots/session/product-subagent-both/tool-schemas.expected.json +++ b/snapshots/session/product-subagent-both/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/product-subagent-codex/tool-schemas.expected.json b/snapshots/session/product-subagent-codex/tool-schemas.expected.json index 7df665c825..49f6480eb1 100644 --- a/snapshots/session/product-subagent-codex/tool-schemas.expected.json +++ b/snapshots/session/product-subagent-codex/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json b/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json index 50fbaad047..34004323e2 100644 --- a/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json +++ b/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/provider-cwd/tool-schemas.expected.json b/snapshots/session/provider-cwd/tool-schemas.expected.json index 8dc90581f4..185fd4ef83 100644 --- a/snapshots/session/provider-cwd/tool-schemas.expected.json +++ b/snapshots/session/provider-cwd/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/ptc-node-read-only/system-prompt.expected.md b/snapshots/session/ptc-node-read-only/system-prompt.expected.md index 43224a84f3..1e964bdb4f 100644 --- a/snapshots/session/ptc-node-read-only/system-prompt.expected.md +++ b/snapshots/session/ptc-node-read-only/system-prompt.expected.md @@ -140,11 +140,11 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ list_subagent_models: { - /** Registered LLM provider id. Omit to list providers. */ + /** Registered LLM provider id. */ provider?: string; - /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + /** Exact model id; requires provider. */ model?: string; } & Record; /** Read a UTF-8 text file and return line-numbered content. */ @@ -173,7 +173,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -181,11 +181,11 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + /** Child LLM provider id. */ provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + /** Model id within that provider. */ model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + /** Model-specific reasoning effort id. */ reasoning_effort?: string; } & Record; /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ diff --git a/snapshots/session/ptc-node-workspace/system-prompt.expected.md b/snapshots/session/ptc-node-workspace/system-prompt.expected.md index 43224a84f3..1e964bdb4f 100644 --- a/snapshots/session/ptc-node-workspace/system-prompt.expected.md +++ b/snapshots/session/ptc-node-workspace/system-prompt.expected.md @@ -140,11 +140,11 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ list_subagent_models: { - /** Registered LLM provider id. Omit to list providers. */ + /** Registered LLM provider id. */ provider?: string; - /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + /** Exact model id; requires provider. */ model?: string; } & Record; /** Read a UTF-8 text file and return line-numbered content. */ @@ -173,7 +173,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -181,11 +181,11 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + /** Child LLM provider id. */ provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + /** Model id within that provider. */ model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + /** Model-specific reasoning effort id. */ reasoning_effort?: string; } & Record; /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ diff --git a/snapshots/session/ptc-persistent/system-prompt.expected.md b/snapshots/session/ptc-persistent/system-prompt.expected.md index 43224a84f3..1e964bdb4f 100644 --- a/snapshots/session/ptc-persistent/system-prompt.expected.md +++ b/snapshots/session/ptc-persistent/system-prompt.expected.md @@ -140,11 +140,11 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ list_subagent_models: { - /** Registered LLM provider id. Omit to list providers. */ + /** Registered LLM provider id. */ provider?: string; - /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + /** Exact model id; requires provider. */ model?: string; } & Record; /** Read a UTF-8 text file and return line-numbered content. */ @@ -173,7 +173,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -181,11 +181,11 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + /** Child LLM provider id. */ provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + /** Model id within that provider. */ model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + /** Model-specific reasoning effort id. */ reasoning_effort?: string; } & Record; /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ diff --git a/snapshots/session/ptc-policy-change/system-prompt.expected.md b/snapshots/session/ptc-policy-change/system-prompt.expected.md index 07099f54b6..65c682d835 100644 --- a/snapshots/session/ptc-policy-change/system-prompt.expected.md +++ b/snapshots/session/ptc-policy-change/system-prompt.expected.md @@ -140,11 +140,11 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ list_subagent_models: { - /** Registered LLM provider id. Omit to list providers. */ + /** Registered LLM provider id. */ provider?: string; - /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + /** Exact model id; requires provider. */ model?: string; } & Record; /** Read a UTF-8 text file and return line-numbered content. */ @@ -173,7 +173,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -181,11 +181,11 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + /** Child LLM provider id. */ provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + /** Model id within that provider. */ model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + /** Model-specific reasoning effort id. */ reasoning_effort?: string; } & Record; /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ diff --git a/snapshots/session/ptc-python-turn/system-prompt.expected.md b/snapshots/session/ptc-python-turn/system-prompt.expected.md index 45143cc82e..a6dfe8d41b 100644 --- a/snapshots/session/ptc-python-turn/system-prompt.expected.md +++ b/snapshots/session/ptc-python-turn/system-prompt.expected.md @@ -288,9 +288,9 @@ class ListAgentsOutput2(TypedDict): depth: NotRequired[float] class ListSubagentModelsArgs(TypedDict): - # Registered LLM provider id. Omit to list providers. + # Registered LLM provider id. provider: NotRequired[str] - # Exact model id to inspect. Requires provider; omit to list that provider's allowed models. + # Exact model id; requires provider. model: NotRequired[str] # Additional keys beyond those declared are allowed. @@ -377,11 +377,11 @@ class SubagentArgs(TypedDict): description: str # The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. prompt: str - # LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. + # Child LLM provider id. provider: NotRequired[str] - # Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. + # Model id within that provider. model: NotRequired[str] - # Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. + # Model-specific reasoning effort id. reasoning_effort: NotRequired[str] # Additional keys beyond those declared are allowed. @@ -549,7 +549,7 @@ class Tools(Protocol): async def list_agents(self, args: ListAgentsArgs) -> list[ListAgentsOutput1 | ListAgentsOutput2]: """List subagents you started, with their ids, labels, and status. running means it is working; inactive means it is not currently working. You will be notified when a subagent finishes; there is no need to keep checking its status. Use send_message to continue the conversation.""" async def list_subagent_models(self, args: ListSubagentModelsArgs) -> str: - """Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.""" + """Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.""" async def read(self, args: ReadArgs) -> ReadOutput: """Read a UTF-8 text file and return line-numbered content.""" async def read_image(self, args: ReadImageArgs) -> ReadImageOutput: @@ -559,7 +559,7 @@ class Tools(Protocol): async def skill(self, args: SkillArgs) -> SkillOutput: """Load the full instructions for a skill. Call it before acting on a task that names or clearly matches a skill in the session skill catalog.""" async def subagent(self, args: SubagentArgs) -> SubagentOutput: - """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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.""" + """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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.""" async def subagent_fork(self, args: SubagentForkArgs) -> SubagentForkOutput: """Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.""" async def todo_write(self, args: TodoWriteArgs) -> TodoWriteOutput: diff --git a/snapshots/session/ptc-read-image/system-prompt.expected.md b/snapshots/session/ptc-read-image/system-prompt.expected.md index 0af031b0c8..4d6f11b10c 100644 --- a/snapshots/session/ptc-read-image/system-prompt.expected.md +++ b/snapshots/session/ptc-read-image/system-prompt.expected.md @@ -140,11 +140,11 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ list_subagent_models: { - /** Registered LLM provider id. Omit to list providers. */ + /** Registered LLM provider id. */ provider?: string; - /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + /** Exact model id; requires provider. */ model?: string; } & Record; /** Read a UTF-8 text file and return line-numbered content. */ @@ -173,7 +173,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -181,11 +181,11 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + /** Child LLM provider id. */ provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + /** Model id within that provider. */ model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + /** Model-specific reasoning effort id. */ reasoning_effort?: string; } & Record; /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ diff --git a/snapshots/session/ptc-turn/system-prompt.expected.md b/snapshots/session/ptc-turn/system-prompt.expected.md index 43224a84f3..1e964bdb4f 100644 --- a/snapshots/session/ptc-turn/system-prompt.expected.md +++ b/snapshots/session/ptc-turn/system-prompt.expected.md @@ -140,11 +140,11 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ list_subagent_models: { - /** Registered LLM provider id. Omit to list providers. */ + /** Registered LLM provider id. */ provider?: string; - /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + /** Exact model id; requires provider. */ model?: string; } & Record; /** Read a UTF-8 text file and return line-numbered content. */ @@ -173,7 +173,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -181,11 +181,11 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + /** Child LLM provider id. */ provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + /** Model id within that provider. */ model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + /** Model-specific reasoning effort id. */ reasoning_effort?: string; } & Record; /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ diff --git a/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json b/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json index 816b7fcb6a..9127b4826a 100644 --- a/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json +++ b/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/pwsh-tool-turn/tool-schemas.expected.json b/snapshots/session/pwsh-tool-turn/tool-schemas.expected.json index f9cd850b97..c35e8cec15 100644 --- a/snapshots/session/pwsh-tool-turn/tool-schemas.expected.json +++ b/snapshots/session/pwsh-tool-turn/tool-schemas.expected.json @@ -189,17 +189,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -312,7 +312,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -330,15 +330,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/ralph-loop/tool-schemas.1.expected.json b/snapshots/session/ralph-loop/tool-schemas.1.expected.json index 0bb4c382a8..d72f9a2ae8 100644 --- a/snapshots/session/ralph-loop/tool-schemas.1.expected.json +++ b/snapshots/session/ralph-loop/tool-schemas.1.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -419,7 +419,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -437,15 +437,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/ralph-loop/tool-schemas.2.expected.json b/snapshots/session/ralph-loop/tool-schemas.2.expected.json index 0bb4c382a8..d72f9a2ae8 100644 --- a/snapshots/session/ralph-loop/tool-schemas.2.expected.json +++ b/snapshots/session/ralph-loop/tool-schemas.2.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -419,7 +419,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -437,15 +437,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/ralph-loop/tool-schemas.expected.json b/snapshots/session/ralph-loop/tool-schemas.expected.json index f67a9cc249..9fba724dcc 100644 --- a/snapshots/session/ralph-loop/tool-schemas.expected.json +++ b/snapshots/session/ralph-loop/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -376,7 +376,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -394,15 +394,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/session-query-spill/tool-schemas.expected.json b/snapshots/session/session-query-spill/tool-schemas.expected.json index 18b7f4a24d..1c778ec2c9 100644 --- a/snapshots/session/session-query-spill/tool-schemas.expected.json +++ b/snapshots/session/session-query-spill/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -560,7 +560,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -578,15 +578,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json b/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json index 233a8b87a8..48a1ac30ce 100644 --- a/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json +++ b/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json b/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json index ac4d94f100..b4fa90e046 100644 --- a/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json +++ b/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json @@ -325,17 +325,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -419,7 +419,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -437,15 +437,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/subagent-no-message-tool/tool-schemas.1.expected.json b/snapshots/session/subagent-no-message-tool/tool-schemas.1.expected.json index 045312a524..dd16436c73 100644 --- a/snapshots/session/subagent-no-message-tool/tool-schemas.1.expected.json +++ b/snapshots/session/subagent-no-message-tool/tool-schemas.1.expected.json @@ -176,17 +176,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -249,7 +249,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -267,15 +267,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/subagent-structured-parent/tool-schemas.1.expected.json b/snapshots/session/subagent-structured-parent/tool-schemas.1.expected.json index 9c47c0c649..16a256b818 100644 --- a/snapshots/session/subagent-structured-parent/tool-schemas.1.expected.json +++ b/snapshots/session/subagent-structured-parent/tool-schemas.1.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -371,7 +371,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -389,15 +389,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/subagent-structured-parent/tool-schemas.expected.json b/snapshots/session/subagent-structured-parent/tool-schemas.expected.json index 2eaec5d921..498673be8a 100644 --- a/snapshots/session/subagent-structured-parent/tool-schemas.expected.json +++ b/snapshots/session/subagent-structured-parent/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/subagent-tool-filter/tool-schemas.1.expected.json b/snapshots/session/subagent-tool-filter/tool-schemas.1.expected.json index f9cbf2b0d8..4442bcf828 100644 --- a/snapshots/session/subagent-tool-filter/tool-schemas.1.expected.json +++ b/snapshots/session/subagent-tool-filter/tool-schemas.1.expected.json @@ -176,17 +176,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -270,7 +270,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -288,15 +288,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/system-prompt-in-history/tool-schemas.expected.json b/snapshots/session/system-prompt-in-history/tool-schemas.expected.json index 8dc90581f4..185fd4ef83 100644 --- a/snapshots/session/system-prompt-in-history/tool-schemas.expected.json +++ b/snapshots/session/system-prompt-in-history/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/team-targets/tool-schemas.expected.json b/snapshots/session/team-targets/tool-schemas.expected.json index 3409e4753a..f319d5da9f 100644 --- a/snapshots/session/team-targets/tool-schemas.expected.json +++ b/snapshots/session/team-targets/tool-schemas.expected.json @@ -253,17 +253,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } diff --git a/snapshots/session/text-turn/tool-schemas.expected.json b/snapshots/session/text-turn/tool-schemas.expected.json index 8dc90581f4..185fd4ef83 100644 --- a/snapshots/session/text-turn/tool-schemas.expected.json +++ b/snapshots/session/text-turn/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/web-fetch/tool-schemas.expected.json b/snapshots/session/web-fetch/tool-schemas.expected.json index 40cc9548ef..114d460759 100644 --- a/snapshots/session/web-fetch/tool-schemas.expected.json +++ b/snapshots/session/web-fetch/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/workflow-confinement/tool-schemas.expected.json b/snapshots/session/workflow-confinement/tool-schemas.expected.json index 8dc90581f4..185fd4ef83 100644 --- a/snapshots/session/workflow-confinement/tool-schemas.expected.json +++ b/snapshots/session/workflow-confinement/tool-schemas.expected.json @@ -262,17 +262,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -374,15 +374,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/workspace-dependencies/tool-schemas.expected.json b/snapshots/session/workspace-dependencies/tool-schemas.expected.json index 9bd9bd41b5..f009027b9e 100644 --- a/snapshots/session/workspace-dependencies/tool-schemas.expected.json +++ b/snapshots/session/workspace-dependencies/tool-schemas.expected.json @@ -217,17 +217,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -319,7 +319,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -337,15 +337,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/worktree/tool-schemas.expected.json b/snapshots/session/worktree/tool-schemas.expected.json index dd66d0a51f..d95e52e13e 100644 --- a/snapshots/session/worktree/tool-schemas.expected.json +++ b/snapshots/session/worktree/tool-schemas.expected.json @@ -279,17 +279,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -373,7 +373,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -391,15 +391,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/web/fresh-round-trip/tool-schemas.expected.json b/snapshots/web/fresh-round-trip/tool-schemas.expected.json index 26471f7526..db2840f35d 100644 --- a/snapshots/web/fresh-round-trip/tool-schemas.expected.json +++ b/snapshots/web/fresh-round-trip/tool-schemas.expected.json @@ -325,17 +325,17 @@ }, { "name": "list_subagent_models", - "description": "Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", "parameters": { "type": "object", "properties": { "provider": { "type": "string", - "description": "Registered LLM provider id. Omit to list providers." + "description": "Registered LLM provider id." }, "model": { "type": "string", - "description": "Exact model id to inspect. Requires provider; omit to list that provider's allowed models." + "description": "Exact model id; requires provider." } } } @@ -719,7 +719,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { @@ -737,15 +737,15 @@ }, "provider": { "type": "string", - "description": "LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route." + "description": "Child LLM provider id." }, "model": { "type": "string", - "description": "Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route." + "description": "Model id within that provider." }, "reasoning_effort": { "type": "string", - "description": "Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default." + "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/web/official-subagent-bundle/system-prompt.expected.md b/snapshots/web/official-subagent-bundle/system-prompt.expected.md index dd702b72a4..6d483f1bb7 100644 --- a/snapshots/web/official-subagent-bundle/system-prompt.expected.md +++ b/snapshots/web/official-subagent-bundle/system-prompt.expected.md @@ -158,11 +158,11 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ list_subagent_models: { - /** Registered LLM provider id. Omit to list providers. */ + /** Registered LLM provider id. */ provider?: string; - /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + /** Exact model id; requires provider. */ model?: string; } & Record; /** Declare existing files as final deliverables for the user. Use it when the user needs a separate file, especially Office documents, spreadsheets, and slide decks; prefer your final response when that suffices. The user opens the current files; their contents are not copied. */ @@ -288,7 +288,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -296,11 +296,11 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + /** Child LLM provider id. */ provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + /** Model id within that provider. */ model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + /** Model-specific reasoning effort id. */ reasoning_effort?: string; } & Record; /** 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. The runtime notifies you when it finishes. The completion notice includes its final answer. This backend does not accept follow-up messages. */ diff --git a/snapshots/web/optional-string-editor/system-prompt.expected.md b/snapshots/web/optional-string-editor/system-prompt.expected.md index 597ebe90cd..5cb0f37436 100644 --- a/snapshots/web/optional-string-editor/system-prompt.expected.md +++ b/snapshots/web/optional-string-editor/system-prompt.expected.md @@ -158,11 +158,11 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ list_subagent_models: { - /** Registered LLM provider id. Omit to list providers. */ + /** Registered LLM provider id. */ provider?: string; - /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + /** Exact model id; requires provider. */ model?: string; } & Record; /** Declare existing files as final deliverables for the user. Use it when the user needs a separate file, especially Office documents, spreadsheets, and slide decks; prefer your final response when that suffices. The user opens the current files; their contents are not copied. */ @@ -305,7 +305,7 @@ interface ToolArgsMap { /** Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file. */ view_range?: number[] | null; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -313,11 +313,11 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + /** Child LLM provider id. */ provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + /** Model id within that provider. */ model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + /** Model-specific reasoning effort id. */ reasoning_effort?: string; } & Record; /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ diff --git a/snapshots/web/ptc-round/system-prompt.expected.md b/snapshots/web/ptc-round/system-prompt.expected.md index 731d7cc7b4..b5d6fc4177 100644 --- a/snapshots/web/ptc-round/system-prompt.expected.md +++ b/snapshots/web/ptc-round/system-prompt.expected.md @@ -158,11 +158,11 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Discover LLM routes for subagents without changing the current Agent. Call with no arguments to list registered providers, with `provider` to list its allowed models, or with `provider` and `model` to inspect that exact model and its reasoning efforts. Catalog membership is advisory: an adapter may accept an unlisted model id. Use the returned ids with a delegation tool's `provider`, `model`, and `reasoning_effort` fields. */ + /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ list_subagent_models: { - /** Registered LLM provider id. Omit to list providers. */ + /** Registered LLM provider id. */ provider?: string; - /** Exact model id to inspect. Requires provider; omit to list that provider's allowed models. */ + /** Exact model id; requires provider. */ model?: string; } & Record; /** Declare existing files as final deliverables for the user. Use it when the user needs a separate file, especially Office documents, spreadsheets, and slide decks; prefer your final response when that suffices. The user opens the current files; their contents are not copied. */ @@ -288,7 +288,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model's default effort. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -296,11 +296,11 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route. */ + /** Child LLM provider id. */ provider?: string; - /** Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route. */ + /** Model id within that provider. */ model?: string; - /** Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model's default. */ + /** Model-specific reasoning effort id. */ reasoning_effort?: string; } & Record; /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ From 764a51d5071855871ae917a4cc2f433c0d3d68da Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Mon, 12 Oct 2026 18:37:26 +0800 Subject: [PATCH 045/369] refactor(subagent): expose model selection only on settings-owning tools Route fields and list_subagent_models are now registered only by the tool-subagent instance configured with modelSelectionSettings, which keeps one fixed definition per composition for Web presets while headless and SDK profiles return to their previous schemas. Discovery registers with that instance instead of a separate list-models plugin entry, so the delegation description never references an absent tool. modelSelectionSettings on a provider that inherits the parent conversation now fails the mount. --- ...authorized-subagent-model-routes.i18n.yaml | 8 +- ...4-user-authorized-subagent-model-routes.md | 4 +- ...ser-authorized-subagent-model-routes.zh.md | 4 +- apps/cli/composition.md | 3 - .../parent.expected.jsonl | 4 +- .../tests/profiles/sdk/keyless-smoke.e2e.ts | 2 +- .../plugin-instances.expected.md | 4 - docs/config-catalog.md | 9 +- docs/config-catalog.zh.md | 9 +- docs/tool-catalog.i18n.yaml | 8 +- docs/tool-catalog.md | 18 +--- docs/tool-catalog.zh.md | 18 +--- .../guide.i18n.yaml | 16 ++++ .../fork-model-selection-settings/guide.md | 28 ++++++ .../fork-model-selection-settings/guide.zh.md | 28 ++++++ packages/bundle/base/cordis.patch.yml | 3 - packages/bundle/web-app/cordis.patch.yml | 3 - .../bundle/web-app/presets/cordis.patch.yml | 2 - packages/bundle/web-app/presets/ptc.patch.yml | 2 - .../bundle/web-app/presets/standard.patch.yml | 2 - .../subagent/tool-subagent/README.i18n.yaml | 32 +++---- packages/subagent/tool-subagent/README.md | 19 ++-- packages/subagent/tool-subagent/README.zh.md | 19 ++-- packages/subagent/tool-subagent/package.json | 5 - packages/subagent/tool-subagent/src/index.ts | 27 ++++-- .../subagent/tool-subagent/src/list-models.ts | 23 +---- .../tests/fixtures/model-discovery.mjs | 38 -------- .../subagent/tool-subagent/tests/harness.ts | 9 +- .../tool-subagent/tests/list-models.spec.ts | 30 ++++-- .../tests/model-selection-settings.spec.ts | 4 - .../tests/model-selection.spec.ts | 48 ++++++++-- .../tool-subagent/tests/tool-subagent.spec.ts | 3 - .../subagent/tool-subagent/tsdown.config.ts | 1 - scripts/check-workspace-constraints.ts | 5 +- scripts/gen-tool-catalog.ts | 6 +- .../advanced/result.json | 6 -- .../advanced/session.1.v4.jsonl | 2 +- .../advanced/session.2.v4.jsonl | 2 +- .../advanced/session.v4.jsonl | 4 +- .../dynamic-tools/tool-history.json | 3 - .../restart/requests.json | 2 - .../restart/session.1.v4.jsonl | 2 +- .../restart/session.2.v4.jsonl | 2 +- .../scheduler-recovery/session.v4.jsonl | 2 +- .../sdk/bash-tool/tool-schemas.expected.json | 31 +------ .../notifications.expected.jsonl | 6 +- .../sdk/dynamic-tool-updates/session.v4.jsonl | 6 +- snapshots/sdk/persistent-tools/cordis.yml | 4 - .../tool-schemas.1.expected.json | 31 +------ .../tool-schemas.1.expected.json | 31 +------ .../tool-schemas.1.expected.json | 31 +------ .../tool-schemas.expected.json | 59 +----------- .../tool-schemas.1.expected.json | 31 +------ .../tool-schemas.1.expected.json | 31 +------ .../tool-schemas.1.expected.json | 31 +------ .../sdk/text-turn/tool-schemas.expected.json | 31 +------ .../notifications.expected.jsonl | 4 +- .../sdk/tool-error-details/session.v4.jsonl | 4 +- .../system-prompt.expected.md | 16 +--- .../tool-schemas.expected.json | 31 +------ .../advanced-toolchain/session.v4.jsonl | 8 +- .../system-prompt.expected.md | 16 +--- .../tool-schemas.expected.json | 62 +------------ .../tool-schemas.expected.json | 62 +------------ .../tool-schemas.expected.json | 31 +------ .../both-mode-turn/system-prompt.expected.md | 16 +--- .../tool-schemas.expected.json | 31 +------ .../tool-schemas.expected.json | 31 +------ .../tool-schemas.expected.json | 31 +------ .../tool-schemas.expected.json | 31 +------ .../cordis.snapshot.yml | 4 +- .../compaction-output-reserve/cordis.yml | 2 +- .../session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 62 +------------ .../cordis.snapshot.yml | 4 +- .../compaction-summary-headroom/cordis.yml | 2 +- .../session.v4.jsonl | 2 +- .../tool-schemas.expected.json | 31 +------ .../tool-schemas.expected.json | 31 +------ .../tool-schemas.expected.json | 31 +------ .../tool-schemas.expected.json | 31 +------ .../tool-schemas.expected.json | 31 +------ .../session.v4.jsonl | 6 +- .../dynamic-tool-updates/session.v4.jsonl | 6 +- .../tool-schemas.expected.json | 93 +------------------ .../tool-schemas.expected.json | 31 +------ .../tool-schemas.expected.json | 31 +------ .../lsp-definition/tool-schemas.expected.json | 31 +------ .../system-prompt.expected.md | 16 +--- .../mcp-resources/tool-schemas.expected.json | 31 +------ .../tool-schemas.expected.json | 31 +------ .../tool-schemas.expected.json | 31 +------ .../system-prompt.expected.md | 16 +--- .../office-skills/tool-schemas.expected.json | 31 +------ .../tool-schemas.expected.json | 31 +------ .../session.v4.jsonl | 6 +- .../plugin-manager-mcp/session.v4.jsonl | 4 +- .../tool-schemas.expected.json | 62 +------------ .../plugin-manager/tool-schemas.expected.json | 31 +------ .../tool-schemas.expected.json | 31 +------ .../tool-schemas.expected.json | 31 +------ .../tool-schemas.expected.json | 31 +------ .../provider-cwd/tool-schemas.expected.json | 31 +------ .../system-prompt.expected.md | 16 +--- .../system-prompt.expected.md | 16 +--- .../ptc-persistent/system-prompt.expected.md | 16 +--- .../system-prompt.expected.md | 16 +--- .../ptc-python-turn/system-prompt.expected.md | 17 +--- .../ptc-read-image/system-prompt.expected.md | 16 +--- .../ptc-turn/system-prompt.expected.md | 16 +--- .../tool-schemas.expected.json | 31 +------ .../pwsh-tool-turn/tool-schemas.expected.json | 31 +------ .../ralph-loop/tool-schemas.1.expected.json | 31 +------ .../ralph-loop/tool-schemas.2.expected.json | 31 +------ .../ralph-loop/tool-schemas.expected.json | 31 +------ .../tool-schemas.expected.json | 31 +------ .../tool-schemas.expected.json | 31 +------ .../tool-schemas.expected.json | 31 +------ .../runtime.cordis.yml | 3 - .../session.v4.jsonl | 23 ----- .../snapshot.yml | 7 -- .../tool-schemas.1.expected.json | 31 +------ .../tool-schemas.1.expected.json | 31 +------ .../tool-schemas.expected.json | 31 +------ .../tool-schemas.1.expected.json | 31 +------ .../tool-schemas.expected.json | 31 +------ .../team-targets/tool-schemas.expected.json | 17 ---- .../text-turn/tool-schemas.expected.json | 31 +------ .../web-fetch/tool-schemas.expected.json | 31 +------ .../tool-schemas.expected.json | 31 +------ .../tool-schemas.expected.json | 31 +------ .../worktree/tool-schemas.expected.json | 31 +------ tsconfig.base.json | 1 - 133 files changed, 353 insertions(+), 2380 deletions(-) create mode 100644 docs/upgrade-guide/v0.2.1-alpha.2/fork-model-selection-settings/guide.i18n.yaml create mode 100644 docs/upgrade-guide/v0.2.1-alpha.2/fork-model-selection-settings/guide.md create mode 100644 docs/upgrade-guide/v0.2.1-alpha.2/fork-model-selection-settings/guide.zh.md delete mode 100644 packages/subagent/tool-subagent/tests/fixtures/model-discovery.mjs delete mode 100644 snapshots/session/subagent-model-discovery-errors/runtime.cordis.yml delete mode 100644 snapshots/session/subagent-model-discovery-errors/session.v4.jsonl delete mode 100644 snapshots/session/subagent-model-discovery-errors/snapshot.yml diff --git a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml index 2592a568ad..250e5e27cf 100644 --- a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml @@ -9,14 +9,14 @@ en: 3238dfb7db7dfdd8 zh: ec3bdd9a9da0a321 /agent-note-user-authorized-subagent-model-routes/decision: - en: eba9e531eb6c450b - zh: 7c0673644e83608a + en: e67644944167a865 + zh: dc2b28f630a0f6fa /agent-note-user-authorized-subagent-model-routes/alternatives-considered: en: e41c17226190ea8b zh: d3b1395fafe75f01 /agent-note-user-authorized-subagent-model-routes/consequences: - en: 1749cfbfda849692 - zh: 9925e638cd72fba6 + en: 36b76692f1605e6d + zh: a63a249c980bc654 /agent-note-user-authorized-subagent-model-routes/related-decisions: en: 0eead87ec1172d81 zh: 4e776a583f5a00a5 diff --git a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md index a91f0e9e47..357ef78e36 100644 --- a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md +++ b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md @@ -16,7 +16,7 @@ A newly composed top-level Session snapshots the route list in `subagent/model-s The fixed `list_subagent_models` schema does not enumerate the policy. At call time, it lists authorized registered providers and intersects their advertised model catalogs with the recorded route list. Exact lookup requires authorization before resolving model metadata and all supported reasoning efforts, including for ids absent from the catalog. Without a recorded policy, discovery resolves only the parent Agent’s current effective route, even if the catalog omits it. The delegation executor independently checks any explicit provider, model, or effort selection against the same authorization before adapter preflight. A call with no selection fields retains configured or inherited routing. -Model selection has no unrestricted static mode. The default-off Host setting authorizes alternative child routes; without it, the parent route and its supported efforts remain selectable. The primary spawn tool captures the setting once, while all capable delegation tools and shared discovery read the same Session policy. Fork tools reject model-selection arguments to preserve inherited-prefix reuse. +Model selection has no unrestricted static mode. The default-off Host setting authorizes alternative child routes; without it, the parent route and its supported efforts remain selectable. Only a delegation tool configured with `modelSelectionSettings` exposes the route fields and registers `list_subagent_models`; the shipped Web spawn tool is that owner, captures the setting once, and reads the Session policy at each call. Fork tools reject model-selection arguments, and the setting fails their mount, to preserve inherited-prefix reuse. ## Alternatives considered @@ -34,7 +34,7 @@ Model selection has no unrestricted static mode. The default-off Host setting au - New adapter registrations and newly advertised models do not expand user authorization. - Adapter removal or catalog lookup failure can make provider discovery unavailable without deleting the saved route decision; an allowed model remains usable when its adapter accepts it even if the advisory catalog omits it. -- The allowlist itself consumes no parent-request tokens. Only a `list_subagent_models` result enters the transcript. +- The allowlist itself consumes no parent-request tokens. Only a `list_subagent_models` result enters the transcript. Compositions with `modelSelectionSettings` carry the three route fields and the discovery schema in every Session; headless and SDK profiles carry neither. - The policy event is log-only and is appended while an Agent is composed, before either SDK begins its run subscription. Shipped SDK profiles do not enable this Web-owned preference, so the event changes neither SDK's expected notifications or persisted-session output; package restore tests own its durable projection instead of fabricating an SDK composition solely to emit it. - Unit coverage pins settings validation, malformed durable values, Session sampling and inheritance, shared definitions, parent-route defaults, discovery restrictions, executor denial, and the settings UI’s staged writes and revision checks. The assembled Web scenario pins the real settings document and Plugins card flow. diff --git a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md index 49a221d220..4fe117478a 100644 --- a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md +++ b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md @@ -16,7 +16,7 @@ Host 自有的 `subagent-model-selection` 设置 section 保存显式 `enabled` 固定的 `list_subagent_models` schema 不枚举策略。调用时,它列出已授权且已注册的提供方,并取其公布模型目录与已记录路由列表的交集。精确查询先要求授权,再解析模型元数据及全部支持的推理强度,也允许目录未列出的 ID。没有已记录策略时,发现只解析父 Agent 当前生效的路由,即使目录未列出它。委派执行器会在适配器预检前独立校验显式提供方、模型或强度选择是否符合相同授权。没有选择字段的调用保留配置或继承的路由。 -模型选择没有无限制的静态模式。默认关闭的 Host 设置授权其他子级路由;没有该授权时,仍可选择父级路由及其支持的强度。主 spawn 工具一次性记录设置,所有支持选择的委派工具和共享发现均读取同一 Session 策略。fork 工具拒绝模型选择参数,以保留对继承前缀的复用。 +模型选择没有无限制的静态模式。默认关闭的 Host 设置授权其他子级路由;没有该授权时,仍可选择父级路由及其支持的强度。只有配置了 `modelSelectionSettings` 的委派工具会公开路由字段并注册 `list_subagent_models`;随附 Web 的 spawn 工具就是该所有者,它一次性记录设置,并在每次调用时读取 Session 策略。fork 工具拒绝模型选择参数,且该设置会使其挂载失败,以保留对继承前缀的复用。 ## Alternatives considered @@ -34,7 +34,7 @@ Host 自有的 `subagent-model-selection` 设置 section 保存显式 `enabled` - 新适配器注册和新公布模型不会扩大用户授权。 - 适配器移除或目录查询失败可能使提供方发现不可用,但不会删除已存路由决定;只要适配器接受某个获准模型,即使建议性目录省略它,该模型仍可使用。 -- 允许列表本身不消耗父级请求 token。只有 `list_subagent_models` 结果进入 transcript。 +- 允许列表本身不消耗父级请求 token。只有 `list_subagent_models` 结果进入 transcript。配置了 `modelSelectionSettings` 的组合在每个 Session 中都携带三个路由字段和发现 schema;headless 与 SDK profile 两者都不携带。 - 策略事件仅存在于日志,并在 Agent 组合期间、两套 SDK 开始订阅运行前追加。随附 SDK profile 不启用这项 Web 自有偏好,因此该事件不会改变任一 SDK 的预期通知或持久 Session 输出;其持久投影由包级恢复测试负责,不会为了发出该事件而虚构 SDK 组合。 - 单元覆盖固定设置校验、异常持久值、Session 取样与继承、共享定义、父级路由默认值、发现限制、执行器拒绝,以及设置 UI 的暂存写入与 revision 检查。组装 Web 场景固定真实设置文档与 Plugins 设置卡流程。 diff --git a/apps/cli/composition.md b/apps/cli/composition.md index f2bae814f4..9888602296 100644 --- a/apps/cli/composition.md +++ b/apps/cli/composition.md @@ -144,8 +144,6 @@ flowchart LR cfg --> plugin_dsh_base_subagent_fork_in_process plugin_dsh_base_tool_subagent_control["tool-subagent-control
@deepseek-ai/dsh-tool-subagent-control"] cfg --> plugin_dsh_base_tool_subagent_control - plugin_dsh_base_tool_subagent_list_models["tool-subagent-list-models
@deepseek-ai/dsh-tool-subagent/list-models"] - cfg --> plugin_dsh_base_tool_subagent_list_models plugin_dsh_base_tool_subagent_list_agents["tool-subagent-list-agents
@deepseek-ai/dsh-tool-subagent-control/list-agents"] cfg --> plugin_dsh_base_tool_subagent_list_agents plugin_dsh_base_tool_subagent["tool-subagent
@deepseek-ai/dsh-tool-subagent"] @@ -270,7 +268,6 @@ flowchart LR | `subagent-spawn-in-process` | `@deepseek-ai/dsh-subagent-spawn-in-process` | | `subagent-fork-in-process` | `@deepseek-ai/dsh-subagent-fork-in-process` | | `tool-subagent-control` | `@deepseek-ai/dsh-tool-subagent-control` | -| `tool-subagent-list-models` | `@deepseek-ai/dsh-tool-subagent/list-models` | | `tool-subagent-list-agents` | `@deepseek-ai/dsh-tool-subagent-control/list-agents` | | `tool-subagent` | `@deepseek-ai/dsh-tool-subagent` | | `tool-subagent-fork` | `@deepseek-ai/dsh-tool-subagent` | 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 01674e2a6e..1cee326a29 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 @@ -17,8 +17,8 @@ {"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 policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}],"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 policy: ask. Operations that require approval may ask through the configured answerers; without an available answerer, the request fails closed."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"low"},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","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":"bash"},{"type":"tool-addition","toolName":"create_goal"},{"type":"tool-addition","toolName":"edit"},{"type":"tool-addition","toolName":"exit_plan_mode"},{"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":"list_subagent_models"},{"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":"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/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","reasoningEffort":"low"},"tools":["bash","create_goal","edit","exit_plan_mode","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","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":"bash"},{"type":"tool-addition","toolName":"create_goal"},{"type":"tool-addition","toolName":"edit"},{"type":"tool-addition","toolName":"exit_plan_mode"},{"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":"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"} diff --git a/apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts b/apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts index 5ee1288135..d0f44a6356 100644 --- a/apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts +++ b/apps/cli/tests/profiles/sdk/keyless-smoke.e2e.ts @@ -178,7 +178,7 @@ describe('Python SDK dsh profile keyless smoke', () => { expect(modelRequests[0]?.max_tokens).toBe(1234) expect(toolNames).toEqual(expect.arrayContaining(['read', 'write', 'edit', 'web_fetch', 'web_search'])) expect(toolNames.includes('str_replace_editor')).toBe(editorEnabled) - expect(toolNames).toContain('list_subagent_models') + expect(toolNames).not.toContain('list_subagent_models') child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 3, method: 'shutdown' })}\n`) const shutdown = await waitForLine(lines, value => value.id === 3, () => stderr) diff --git a/apps/web/tests/expected/settings-chrome/plugin-instances.expected.md b/apps/web/tests/expected/settings-chrome/plugin-instances.expected.md index 9c455c27da..239b4855e7 100644 --- a/apps/web/tests/expected/settings-chrome/plugin-instances.expected.md +++ b/apps/web/tests/expected/settings-chrome/plugin-instances.expected.md @@ -4,10 +4,6 @@ - strong: 子智能体控制工具 - text: 让 AI 向子智能体发送消息或中断其运行。 - code: tool-subagent-control - - listitem: - - button "tool-subagent/list-models, tool-subagent-list-models, 已启用": - - strong: tool-subagent/list-models - - code: tool-subagent-list-models - listitem: - button "子智能体列表工具, tool-subagent-list-agents, 已启用": - strong: 子智能体列表工具 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index a40560130b..48f63cf574 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -3977,7 +3977,7 @@ export interface Config { - `inject`: `tools` · `subagents` · `systemPrompt` · `sessionProjections` - `refs`: [`AgentOptions`](subsystems/core.md) -- `source`: [`packages/subagent/tool-subagent/src/index.ts:47`](../packages/subagent/tool-subagent/src/index.ts) +- `source`: [`packages/subagent/tool-subagent/src/index.ts:48`](../packages/subagent/tool-subagent/src/index.ts) ```ts config-catalog /** Config: which registered provider this tool delegates to, plus child defaults. */ @@ -3990,8 +3990,11 @@ export interface Config { */ toolName?: string /** - * Sample the Host `subagent-model-selection` setting for each new top-level - * Session and inherit that decision in its child Sessions. + * Expose optional child `provider`, `model`, and `reasoning_effort` fields + * plus `list_subagent_models`, and sample the Host `subagent-model-selection` + * setting for each new top-level Session; child Sessions inherit that + * decision. Requires a provider with `agentOptions` that does not inherit + * the parent conversation. At most one instance per tool scope may enable it. */ modelSelectionSettings?: boolean /** diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 10c01cbe78..b462472c26 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -3979,7 +3979,7 @@ export interface Config { - `inject`: `tools` · `subagents` · `systemPrompt` · `sessionProjections` - `refs`: [`AgentOptions`](subsystems/core.zh.md) -- `source`: [`packages/subagent/tool-subagent/src/index.ts:47`](../packages/subagent/tool-subagent/src/index.ts) +- `source`: [`packages/subagent/tool-subagent/src/index.ts:48`](../packages/subagent/tool-subagent/src/index.ts) ```ts config-catalog /** Config: which registered provider this tool delegates to, plus child defaults. */ @@ -3992,8 +3992,11 @@ export interface Config { */ toolName?: string /** - * Sample the Host `subagent-model-selection` setting for each new top-level - * Session and inherit that decision in its child Sessions. + * Expose optional child `provider`, `model`, and `reasoning_effort` fields + * plus `list_subagent_models`, and sample the Host `subagent-model-selection` + * setting for each new top-level Session; child Sessions inherit that + * decision. Requires a provider with `agentOptions` that does not inherit + * the parent conversation. At most one instance per tool scope may enable it. */ modelSelectionSettings?: boolean /** diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml index 4b23c19b5c..529e9be439 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: 559ba7c9e11d4919 - zh: 44c540c14cf43f95 + en: 551b31e8c713bbcd + zh: 861604a6d4d6db6b /tool-schema-catalog/deepseek-ai-dsh-plugin-manager: en: e0143916bb634762 zh: e0143916bb634762 @@ -228,8 +228,8 @@ en: 6872bee11fccb6c8 zh: 230f2d443dc5d9a6 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent/subagent: - en: aff4a6668b41b7d5 - zh: 98d6ddf8aa28c7f9 + en: d73236d49ab7c9f5 + zh: 1009b5c5d6d678a5 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control: en: ab82e130c3413f04 zh: ab82e130c3413f04 diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index 769ad76242..090afd911d 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -37,7 +37,7 @@ This table connects model-visible tool names to the plugin package and service s | `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`, `ctx.workflowEngine`, `ctx.subagents`, `ctx.systemPrompt`, `a calling Agent (exec.agent parents every fresh round)` | `tool/call`, `tool/result`, `workflow and child session events during execution` | - | A fixed foreground workflow starts one fresh structured child per round; the model selects only the immutable objective and an optional round cap. | | `@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` | - | 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-tool-subagent` | `list_subagent_models`, `subagent` | `ctx.tools`, `ctx.subagents`, `ctx.systemPrompt`, `ctx.llm for model discovery and selected-route validation` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered delegation name is the load-time `toolName` config (default `subagent`); backends supporting agentOptions without inheriting parent context always expose optional route fields, and the shared discovery plugin resolves the calling Session’s allowed routes. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` rejects model-selection arguments; without an enabled policy, spawn selection and discovery are restricted to the current parent route. Each instance selects its backend through `provider` and reads model-selection settings through `modelSelectionSettings`. 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.llm for model discovery and selected-route validation` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion registered by an instance with `modelSelectionSettings`. That instance always exposes optional route fields; Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions, and without an enabled policy, selection and discovery are restricted to the current parent route. `subagent_fork` exposes no route selection. Each instance selects its backend through `provider`. 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-control` | `interrupt_agent`, `list_agents`, `send_message` | `ctx.tools`, `ctx.subagents`, `ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`, `tool/result`, `child session events through ctx.subagents` | - | The globally named discovery, interruption, and local-message tools over managed subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` and `interrupt_agent` once, plus `list_agents` from its separately loaded `/list-agents` plugin (whose catalog rows use the sessionProjections and live Agent registries). | | `@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-experimental-tool-agent-team` | `interrupt_agent`, `list_agents`, `send_message`, `spawn_teammate`, `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update`, `wait_agent` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `an exact live Team member Agent` | `tool/call`, `team/member`, `team/task`, `agent/inbox/spliced`, `user/message on inbox claim`, `tool/result` | - | All nine tools are scoped to implicit Team Leads and durable teammates. The shipped dsh-base bundle keeps the package disabled; the documented Agent Teams profile patch enables it while disabling the legacy continuable-child control names. | @@ -2039,7 +2039,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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. +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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. ```json { @@ -2056,18 +2056,6 @@ Delegate a self-contained task to a subagent (a separate agent that works in its "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ @@ -2079,7 +2067,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 registered delegation name is the load-time `toolName` config (default `subagent`); backends supporting agentOptions without inheriting parent context always expose optional route fields, and the shared discovery plugin resolves the calling Session’s allowed routes. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` rejects model-selection arguments; without an enabled policy, spawn selection and discovery are restricted to the current parent route. Each instance selects its backend through `provider` and reads model-selection settings through `modelSelectionSettings`. Every delegation returns a child id promptly; local children accept follow-ups, while external backends report their single execution automatically. +The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion registered by an instance with `modelSelectionSettings`. That instance always exposes optional route fields; Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions, and without an enabled policy, selection and discovery are restricted to the current parent route. `subagent_fork` exposes no route selection. Each instance selects its backend through `provider`. Every delegation returns a child id promptly; local children accept follow-ups, while external backends report their single execution automatically. diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index a91c05a5cb..b60ade9669 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -41,7 +41,7 @@ | `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`、`ctx.workflowEngine`、`ctx.subagents`、`ctx.systemPrompt`、`a calling Agent (exec.agent parents every fresh round)` | `tool/call`、`tool/result`、`workflow and child session events during execution` | - | 固定的前台工作流会在每个 Round 启动一个全新的结构化子级;模型只能选择不可变目标和可选的 Round 上限。 | | `@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-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);支持 agentOptions 且不继承父级上下文的后端始终公开可选路由字段,共享发现插件解析调用 Session 允许的路由。Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;`subagent_fork` 拒绝模型选择参数;没有启用策略时,spawn 选择和发现仅限当前父级路由。每个实例通过 `provider` 选择后端,通过 `modelSelectionSettings` 读取模型选择设置。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 | +| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述默认 schema 关闭模型选择,而发现 schema 展示为设置了 `modelSelectionSettings` 的实例注册的固定配套工具。该实例始终公开可选路由字段;Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;没有启用策略时,选择和发现仅限当前父级路由。`subagent_fork` 不公开路由选择。每个实例通过 `provider` 选择后端。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 | | `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`、`list_agents`、`send_message` | `ctx.tools`、`ctx.subagents`、`ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`、`tool/result`、`child session events through ctx.subagents` | - | 这些是用于发现、中断托管 subagent 和向本地子代理发送消息的全局命名工具:绑定提供方的 `tool-subagent` 实例注册不同的委派工具;本包注册一次 `send_message` 和 `interrupt_agent`,另由 `list_agents` 通过单独加载的 `/list-agents` 插件提供,其目录行使用 sessionProjections 和实时 Agent 注册表。 | | `@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-experimental-tool-agent-team` | `interrupt_agent`、`list_agents`、`send_message`、`spawn_teammate`、`team_task_create`、`team_task_get`、`team_task_list`、`team_task_update`、`wait_agent` | `ctx.tools`、`ctx.systemPrompt`、`ctx.agentTeams`、`an exact live Team member Agent` | `tool/call`、`team/member`、`team/task`、`agent/inbox/spliced`、`user/message on inbox claim`、`tool/result` | - | 这 9 个工具限定于隐式 Team Lead 与持久 teammate 作用域。随产品发布的 dsh-base bundle 默认禁用该包;文档中的 Agent Teams profile patch 会启用它,并禁用旧 continuable child 的同名控制工具。 | @@ -2045,7 +2045,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, ### `subagent` -将一项自包含任务委派给 subagent(在自身上下文中工作的独立 agent),用它卸载聚焦且独立的工作,例如研究、限定范围的实现或分析,以免消耗当前对话的上下文。subagent 会返回结果,但不会返回中间步骤。此工具启动独立管理的 subagent 并立即返回其 id。运行时会在任务结束时通知你。子代理通过 `send_message` 汇报结果;你可以使用 `send_message` 在运行时引导它,或在结束后继续对话。先通过 `list_subagent_models` 查询选项,再同时设置 `provider` 与 `model`。省略的值使用子级默认值,缺失时使用兼容的父级值。生效路由改变时,省略 `reasoning_effort` 会使用模型的默认强度。 +将一项自包含任务委派给 subagent(在自身上下文中工作的独立 agent),用它卸载聚焦且独立的工作,例如研究、限定范围的实现或分析,以免消耗当前对话的上下文。subagent 会返回结果,但不会返回中间步骤。此工具启动独立管理的 subagent 并立即返回其 id。运行时会在任务结束时通知你。子代理通过 `send_message` 汇报结果;你可以使用 `send_message` 在运行时引导它,或在结束后继续对话。 ```json { @@ -2062,18 +2062,6 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ @@ -2085,7 +2073,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, 来源:[`packages/subagent/tool-subagent/src/index.ts`](../packages/subagent/tool-subagent/src/index.ts) -注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);支持 agentOptions 且不继承父级上下文的后端始终公开可选路由字段,共享发现插件解析调用 Session 允许的路由。Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;`subagent_fork` 拒绝模型选择参数;没有启用策略时,spawn 选择和发现仅限当前父级路由。每个实例通过 `provider` 选择后端,通过 `modelSelectionSettings` 读取模型选择设置。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 +注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述默认 schema 关闭模型选择,而发现 schema 展示为设置了 `modelSelectionSettings` 的实例注册的固定配套工具。该实例始终公开可选路由字段;Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;没有启用策略时,选择和发现仅限当前父级路由。`subagent_fork` 不公开路由选择。每个实例通过 `provider` 选择后端。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/fork-model-selection-settings/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.2/fork-model-selection-settings/guide.i18n.yaml new file mode 100644 index 0000000000..c3a98c82ac --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.2/fork-model-selection-settings/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-alpha.2/fork-model-selection-settings/guide.md +/: + en: 9a987818d6903fcb + zh: 8a4eefd0e7b4ad04 +/remove-modelselectionsettings-from-fork-delegation-tools: + en: f6bf4e381476a12f + zh: 0e745d803fc38fc9 +/remove-modelselectionsettings-from-fork-delegation-tools/change: + en: ccac82df3a00bf69 + zh: 1820a052adff5f6b +/remove-modelselectionsettings-from-fork-delegation-tools/migration: + en: ba409934bc81f492 + zh: fda2f67f731cd1d2 diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/fork-model-selection-settings/guide.md b/docs/upgrade-guide/v0.2.1-alpha.2/fork-model-selection-settings/guide.md new file mode 100644 index 0000000000..542cc9a183 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.2/fork-model-selection-settings/guide.md @@ -0,0 +1,28 @@ +--- +kind: upgrade-guide +description: "`modelSelectionSettings` on a fork delegation tool now fails the mount instead of exposing child route selection." +--- + +# Remove `modelSelectionSettings` from fork delegation tools + +English | [中文](guide.zh.md) + +## Change + +In v0.2.1-alpha.2, a `@deepseek-ai/dsh-tool-subagent` row whose provider inherits the parent conversation, such as the in-process `fork` provider, accepted `modelSelectionSettings: true` and exposed `provider`, `model`, `reasoning_effort`, and `list_subagent_models` in Sessions with an enabled policy. The tool now rejects that configuration when the provider is mounted, so the composition fails to load with `provider "" inherits the parent conversation and does not accept child model selection`. Fork children keep the parent route so the inherited conversation prefix stays eligible for KV Cache reuse. + +The shipped profiles are unaffected: only the Web spawn tool sets `modelSelectionSettings`. A tool row with the setting now exposes the route fields and `list_subagent_models` in every Session; without a recorded Session policy, explicit selection permits only the parent route. + +## Migration + +1. In `cordis.yml`, profile patches, and overlays, remove `modelSelectionSettings` from every `@deepseek-ai/dsh-tool-subagent` row whose `provider` names a fork provider: + + ```yaml + - name: '@deepseek-ai/dsh-tool-subagent' + config: + provider: fork + toolName: subagent_fork + ``` + +2. To let the model choose child routes, set `modelSelectionSettings: true` on one spawn-style row per tool scope instead. +3. Start the composition and confirm that it loads and that the fork tool schema lists only `cwd`, `description`, and `prompt`. diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/fork-model-selection-settings/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.2/fork-model-selection-settings/guide.zh.md new file mode 100644 index 0000000000..e400299473 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.2/fork-model-selection-settings/guide.zh.md @@ -0,0 +1,28 @@ +--- +kind: upgrade-guide +description: "fork 委派工具上的 `modelSelectionSettings` 现在会使挂载失败,而不再公开子级路由选择。" +--- + +# 从 fork 委派工具中移除 `modelSelectionSettings` + +[English](guide.md) | 中文 + +## 变更 + +在 v0.2.1-alpha.2 中,如果 `@deepseek-ai/dsh-tool-subagent` 配置行的提供方会继承父级对话(例如进程内 `fork` 提供方),它会接受 `modelSelectionSettings: true`,并在策略已启用的 Session 中公开 `provider`、`model`、`reasoning_effort` 和 `list_subagent_models`。现在该工具会在提供方挂载时拒绝这一配置,组合加载失败并报告 `provider "" inherits the parent conversation and does not accept child model selection`。fork 子级保持父级路由,使继承的对话前缀继续符合 KV Cache 复用条件。 + +随附 profile 不受影响:只有 Web 的 spawn 工具设置了 `modelSelectionSettings`。设置了该项的工具行现在会在每个 Session 中公开路由字段和 `list_subagent_models`;没有已记录 Session 策略时,显式选择只允许父级路由。 + +## 迁移 + +1. 在 `cordis.yml`、profile patch 和 overlay 中,从所有 `provider` 指向 fork 提供方的 `@deepseek-ai/dsh-tool-subagent` 配置行移除 `modelSelectionSettings`: + + ```yaml + - name: '@deepseek-ai/dsh-tool-subagent' + config: + provider: fork + toolName: subagent_fork + ``` + +2. 如需让模型选择子级路由,请改为在每个工具作用域中的一个 spawn 类配置行上设置 `modelSelectionSettings: true`。 +3. 启动组合,确认它能加载,且 fork 工具的 schema 只列出 `cwd`、`description` 和 `prompt`。 diff --git a/packages/bundle/base/cordis.patch.yml b/packages/bundle/base/cordis.patch.yml index f162113cae..de65cd8015 100644 --- a/packages/bundle/base/cordis.patch.yml +++ b/packages/bundle/base/cordis.patch.yml @@ -369,9 +369,6 @@ - id: tool-subagent-control name: '@deepseek-ai/dsh-tool-subagent-control' - - id: tool-subagent-list-models - name: '@deepseek-ai/dsh-tool-subagent/list-models' - - id: tool-subagent-list-agents name: '@deepseek-ai/dsh-tool-subagent-control/list-agents' diff --git a/packages/bundle/web-app/cordis.patch.yml b/packages/bundle/web-app/cordis.patch.yml index 18d6983aef..a1ca0b5483 100644 --- a/packages/bundle/web-app/cordis.patch.yml +++ b/packages/bundle/web-app/cordis.patch.yml @@ -552,9 +552,6 @@ - id: tool-subagent-control disabled: true -- id: tool-subagent-list-models - disabled: true - - id: tool-subagent-list-agents disabled: true diff --git a/packages/bundle/web-app/presets/cordis.patch.yml b/packages/bundle/web-app/presets/cordis.patch.yml index e324a4c837..8c8d567ae3 100644 --- a/packages/bundle/web-app/presets/cordis.patch.yml +++ b/packages/bundle/web-app/presets/cordis.patch.yml @@ -87,8 +87,6 @@ config: - id: tool-subagent-control name: '@deepseek-ai/dsh-tool-subagent-control' - - id: tool-subagent-list-models - name: '@deepseek-ai/dsh-tool-subagent/list-models' - id: tool-subagent-list-agents name: '@deepseek-ai/dsh-tool-subagent-control/list-agents' - id: tool-subagent diff --git a/packages/bundle/web-app/presets/ptc.patch.yml b/packages/bundle/web-app/presets/ptc.patch.yml index ac0cc611ef..8e1ec1fb70 100644 --- a/packages/bundle/web-app/presets/ptc.patch.yml +++ b/packages/bundle/web-app/presets/ptc.patch.yml @@ -88,8 +88,6 @@ config: - id: tool-subagent-control name: '@deepseek-ai/dsh-tool-subagent-control' - - id: tool-subagent-list-models - name: '@deepseek-ai/dsh-tool-subagent/list-models' - id: tool-subagent-list-agents name: '@deepseek-ai/dsh-tool-subagent-control/list-agents' - id: tool-subagent diff --git a/packages/bundle/web-app/presets/standard.patch.yml b/packages/bundle/web-app/presets/standard.patch.yml index 05b06df618..6a42d0a351 100644 --- a/packages/bundle/web-app/presets/standard.patch.yml +++ b/packages/bundle/web-app/presets/standard.patch.yml @@ -88,8 +88,6 @@ config: - id: tool-subagent-control name: '@deepseek-ai/dsh-tool-subagent-control' - - id: tool-subagent-list-models - name: '@deepseek-ai/dsh-tool-subagent/list-models' - id: tool-subagent-list-agents name: '@deepseek-ai/dsh-tool-subagent-control/list-agents' - id: tool-subagent diff --git a/packages/subagent/tool-subagent/README.i18n.yaml b/packages/subagent/tool-subagent/README.i18n.yaml index fa0bd6c782..a806e98777 100644 --- a/packages/subagent/tool-subagent/README.i18n.yaml +++ b/packages/subagent/tool-subagent/README.i18n.yaml @@ -18,20 +18,20 @@ en: 93f1701f5209ecb5 zh: 6310fd012400aead /deepseek-ai-dsh-tool-subagent/use-this-package/minimal-configuration: - en: d01d4425bc1875dd - zh: f3d36c8a1e5ed0fa + en: 70e715a7262f45b2 + zh: a94e81d7f1513fdf /deepseek-ai-dsh-tool-subagent/use-this-package/managed-delegation: en: 42673064ba88a211 zh: 8fa26285bd649dc9 /deepseek-ai-dsh-tool-subagent/use-this-package/selecting-a-child-llm: - en: f85faf2e965a541a - zh: 3c232e236199b9aa + en: 2bf262d3f462f5f5 + zh: c443c5366f230eee /deepseek-ai-dsh-tool-subagent/understand-the-implementation: en: 6ce5f749256a7e6c zh: a4f2adb04a70d9cf /deepseek-ai-dsh-tool-subagent/understand-the-implementation/design-concept: - en: 5c97aae02694c27a - zh: 5507f580d3e0c43b + en: 794a71280d73dbc2 + zh: acb6990a599f3f66 /deepseek-ai-dsh-tool-subagent/understand-the-implementation/activation-ownership: en: ef692c4d17290920 zh: b1d0d3dab011e2cd @@ -54,11 +54,11 @@ en: e4c7376d4f29dbd5 zh: 6b7a9eaf69c9ea53 /deepseek-ai-dsh-tool-subagent/model-experience/tool-schema/what-the-model-sees: - en: 56361a0d507bad66 - zh: 979d502dd3ac168f + en: 8266f5b60ac830db + zh: 6dceefd04897b6b0 /deepseek-ai-dsh-tool-subagent/model-experience/tool-schema/token-effect: - en: 6a1a6351a3a88424 - zh: c574e9ee286518ed + en: 86d1dafcb6c00c57 + zh: fe890e9c6146c45e /deepseek-ai-dsh-tool-subagent/model-experience/tool-schema/kv-cache-effect: en: f746a3c2f2f3cd18 zh: 68de857073b229ff @@ -66,11 +66,11 @@ en: a4ca31b9cbacda6b zh: 91ac3e0784a14697 /deepseek-ai-dsh-tool-subagent/model-experience/model-selection-and-discovery/what-the-model-sees: - en: be3c5b647252ddc7 - zh: efecd46ed7dd9a95 + en: d0df5c0c9c988408 + zh: cadaa5591dcc5784 /deepseek-ai-dsh-tool-subagent/model-experience/model-selection-and-discovery/token-effect: - en: cfdfe3b1106c3b57 - zh: 3f421f2901d6f7ab + en: cdb02d92cc19062e + zh: c466d4592828021f /deepseek-ai-dsh-tool-subagent/model-experience/model-selection-and-discovery/kv-cache-effect: en: 7dc47a34d5574fbd zh: bd7bd2a4da2f8ae2 @@ -114,8 +114,8 @@ en: f5d5f97a153f7c70 zh: 3d6bc05b9eadfbc2 /deepseek-ai-dsh-tool-subagent/known-limitations-and-deferred-work: - en: 897c9481685a6eab - zh: 444ee7ef4d353f60 + en: 8a0d48a82dff3343 + zh: 604ecd4e288ae322 /deepseek-ai-dsh-tool-subagent/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/subagent/tool-subagent/README.md b/packages/subagent/tool-subagent/README.md index 8591e118e9..6f188766d6 100644 --- a/packages/subagent/tool-subagent/README.md +++ b/packages/subagent/tool-subagent/README.md @@ -36,7 +36,6 @@ Load the subagent service, an in-process or remote backend, and this tool; then ```yaml - name: '@deepseek-ai/dsh-subagent' - name: '@deepseek-ai/dsh-subagent-spawn-in-process' -- name: '@deepseek-ai/dsh-tool-subagent/list-models' - name: '@deepseek-ai/dsh-tool-subagent' config: provider: spawn @@ -47,7 +46,7 @@ Load the subagent service, an in-process or remote backend, and this tool; then |---|---|---| | `provider` | required | Provider name on `ctx.subagents` (e.g. `spawn`, `fork`, `acp`) | | `toolName` | `subagent` | Model-facing tool name; distinct for every loaded instance | -| `modelSelectionSettings` | `false` | Sample the Host's exact-route authorization preference for each top-level Session; a standing preset observes matching Sessions, while direct Agent setup passes its Session explicitly; requires provider `agentOptions` support | +| `modelSelectionSettings` | `false` | Expose child route fields and `list_subagent_models`, and sample the Host's exact-route authorization preference for each top-level Session; a standing preset observes matching Sessions, while direct Agent setup passes its Session explicitly; requires provider `agentOptions` support without parent-context inheritance | | `agentOptions` | — | Configured child `provider`, `model`, adapter-owned `reasoningEffort`, and positive `maxTokens` defaults; requires provider `agentOptions` support and overlays any provider-owned route defaults | | `persona` | — | Per-child persona; requires the provider's `persona` capability | | `toolFilter` | — | Per-child global-tool restriction; requires the `toolFilter` capability | @@ -65,7 +64,7 @@ Every backend’s completion notice carries its final answer. Local Spawn and Fo ### Selecting a child LLM -Backends that support `agentOptions` and do not inherit parent context always expose optional `provider`, `model`, and `reasoning_effort` fields. Load `@deepseek-ai/dsh-tool-subagent/list-models` once alongside the delegation tools for shared discovery; shipped compositions include it. Without a recorded Session policy, discovery and explicit selection permit only the parent Agent’s current effective provider/model and its supported efforts. Set `modelSelectionSettings: true` to sample the Host’s `subagent-model-selection` preference for fresh top-level Sessions. When enabled, its exact route list is recorded in the Session, inherited by child Sessions, and preserved on restore; later settings edits affect only new Sessions. A restored Session without a policy keeps the parent-route restriction. ACP, Codex, and Claude Code do not support `agentOptions` and reject selection fields. +Set `modelSelectionSettings: true` to expose optional `provider`, `model`, and `reasoning_effort` fields and register `list_subagent_models` with this instance; instances without it expose neither and reject selection fields. The instance samples the Host’s `subagent-model-selection` preference for fresh top-level Sessions. When enabled, its exact route list is recorded in the Session, inherited by child Sessions, and preserved on restore; later settings edits affect only new Sessions. Without a recorded Session policy, discovery and explicit selection permit only the parent Agent’s current effective provider/model and its supported efforts. This mode requires a backend that advertises `agentOptions` and does not inherit the parent conversation; both in-process Spawn and DSH SDK qualify, while Fork, ACP, Codex, and Claude Code fail the mount. A call supplies `provider` and `model` together, or supplies only an effort when configured, parent, or provider-owned defaults provide the route. Static `provider.agentRouteDefaults`, when present, form the provider/model baseline; tool configuration and model fields overlay it before route-aware effort merging and exact-route preflight. Providers without these defaults use compatible values from the parent's latest logged request, then the parent's creation options before its first request, while retaining the configured `maxTokens`. Changing the route without an explicit effort clears the inherited route-owned effort, so the selected model resolves its default. The live LLM adapter validates the effective route before child creation. Catalog membership remains advisory, so a model can use an unlisted id when its adapter accepts it. @@ -81,7 +80,7 @@ This section explains how the tool mirrors provider lifecycle and settles runs; ### Design concept -One instance is one provider plus one tool name. The plugin registers its definition in the owning composition while the provider is present and disposes it when the provider leaves. Agents inherit that shared definition, so their existing tool filters apply without per-Agent registration. Settings-backed instances capture Session authorization during Agent creation; calls read the durable policy independently of registration. Direct Agent setup supplies its unpublished Session explicitly. A numeric `maxDepth` or configured LLM selection unsupported by the provider fails the mount. Discovery has one separate composition entry. No runtime invariant companion is published: policy comes directly from the Session projection and is checked at execution, with no separately retained per-Agent definition state to reconcile. +One instance is one provider plus one tool name. The plugin registers its definition in the owning composition while the provider is present and disposes it when the provider leaves. Agents inherit that shared definition, so their existing tool filters apply without per-Agent registration. Settings-backed instances also register `list_subagent_models` in the same composition and capture Session authorization during Agent creation; calls read the durable policy independently of registration. Direct Agent setup supplies its unpublished Session explicitly. A numeric `maxDepth` or configured LLM selection unsupported by the provider fails the mount. At most one instance in a tool scope may set `modelSelectionSettings` because `list_subagent_models` has a global name. No runtime invariant companion is published: policy comes directly from the Session projection and is checked at execution. ### Activation ownership @@ -129,11 +128,11 @@ Read these pages when the package-level contract is not enough; they move from t #### What the model sees -The generated default [`subagent` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent) appears under the configured tool name while its provider exists. Backends supporting `agentOptions` that do not inherit parent context always expose `provider`, `model`, and `reasoning_effort`. Context inheritance and continuation support determine task and follow-up descriptions. A tool restriction removes both the schema and its `tool:` guidance section. +The generated default [`subagent` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent) appears under the configured tool name while its provider exists. An instance with `modelSelectionSettings` adds `provider`, `model`, and `reasoning_effort` plus route guidance in every Session. Context inheritance and continuation support determine task and follow-up descriptions. A tool restriction removes both the schema and its `tool:` guidance section. #### Token effect -Fixed schema cost per parent request, including three optional route fields on capable backends. Each tool instance adds one schema; visible delegation tools share one short system-prompt section. +Fixed schema cost per parent request; `modelSelectionSettings` adds three optional route fields. Each tool instance adds one schema; visible delegation tools share one short system-prompt section. #### KV Cache effect @@ -143,11 +142,11 @@ Prefix-stable while provider instances and their configuration are unchanged. Ad #### What the model sees -The shared `list_subagent_models` definition reads the calling Session’s route policy, falling back to the current parent route when no policy is recorded. Calls reject while the optional `ctx.llm` service is unavailable. With a recorded policy, provider model lists intersect the advertised catalog with the Session’s authorized routes. Without one, they resolve only the current parent model, even if the catalog omits it. Exact-model queries accept authorized ids absent from the catalog; unauthorized provider/model lookups fail before adapter access. Catalog or exact-model lookup failures and cancellation reject the call. Execution independently enforces the same policy. +The `list_subagent_models` definition registered by a settings-backed instance reads the calling Session’s route policy, falling back to the current parent route when no policy is recorded. Calls reject while the optional `ctx.llm` service is unavailable. With a recorded policy, provider model lists intersect the advertised catalog with the Session’s authorized routes. Without one, they resolve only the current parent model, even if the catalog omits it. Exact-model queries accept authorized ids absent from the catalog; unauthorized provider/model lookups fail before adapter access. Catalog or exact-model lookup failures and cancellation reject the call. Execution independently enforces the same policy. #### Token effect -One fixed discovery schema is present in shipped compositions, regardless of the model-selection preference. Route metadata enters the transcript only when the model calls the tool. +One fixed discovery schema is present in compositions with `modelSelectionSettings`, regardless of the Session’s preference; the shipped Web presets include it, while headless and SDK profiles do not. Route metadata enters the transcript only when the model calls the tool. #### KV Cache effect @@ -209,8 +208,8 @@ Append-only; newly visible content follows the reusable request prefix and does These limits define what this tool does not return or enforce; they are current package constraints. - **This tool does not wait for results** — use the completion notice or child messages. Programmatic result waiting belongs to the subagent service. -- **Fork tools reject model selection** — tools that inherit parent context neither expose nor accept `provider`, `model`, or `reasoning_effort`, even with Session authorization. Configured `agentOptions` remain available to the deployment. -- **Non-routing child policy is fixed per instance** — another persona, tool filter, or depth cap requires another distinctly named tool. Explicit LLM selection is restricted to the Session’s authorized routes, or the current parent route without an enabled policy; unsupported backends reject selection fields. +- **Fork tools reject model selection** — tools that inherit parent context neither expose nor accept `provider`, `model`, or `reasoning_effort`, even with Session authorization, and `modelSelectionSettings` fails their mount. Configured `agentOptions` remain available to the deployment. +- **Non-routing child policy is fixed per instance** — another persona, tool filter, or depth cap requires another distinctly named tool. Explicit LLM selection requires `modelSelectionSettings` and is restricted to the Session’s authorized routes, or the current parent route without an enabled policy. ### Dev Note diff --git a/packages/subagent/tool-subagent/README.zh.md b/packages/subagent/tool-subagent/README.zh.md index 5e97c8db7a..7a819691ca 100644 --- a/packages/subagent/tool-subagent/README.zh.md +++ b/packages/subagent/tool-subagent/README.zh.md @@ -36,7 +36,6 @@ kind: "package-reference" ```yaml - name: '@deepseek-ai/dsh-subagent' - name: '@deepseek-ai/dsh-subagent-spawn-in-process' -- name: '@deepseek-ai/dsh-tool-subagent/list-models' - name: '@deepseek-ai/dsh-tool-subagent' config: provider: spawn @@ -47,7 +46,7 @@ kind: "package-reference" |---|---|---| | `provider` | 必填 | `ctx.subagents` 上的提供方名称(如 `spawn`、`fork`、`acp`) | | `toolName` | `subagent` | 面向模型的工具名称;每个已加载实例必须不同 | -| `modelSelectionSettings` | `false` | 为每个顶层 Session 读取宿主的精确路由授权偏好;常驻 preset 观察匹配 Session,直接 Agent setup 则显式传入其 Session;要求提供方支持 `agentOptions` | +| `modelSelectionSettings` | `false` | 公开子级路由字段和 `list_subagent_models`,并为每个顶层 Session 读取宿主的精确路由授权偏好;常驻 preset 观察匹配 Session,直接 Agent setup 则显式传入其 Session;要求提供方支持 `agentOptions` 且不继承父级上下文 | | `agentOptions` | — | 配置的子级 `provider`、`model`、适配器所有的 `reasoningEffort` 与正整数 `maxTokens` 默认值;要求提供方支持 `agentOptions`,并会覆盖提供方持有的路由默认值 | | `persona` | — | 每个子 agent 独立的 persona;要求提供方具备 `persona` 能力 | | `toolFilter` | — | 每个子 agent 独立的全局工具限制;要求提供方具备 `toolFilter` 能力 | @@ -65,7 +64,7 @@ kind: "package-reference" ### 选择子级 LLM -支持 `agentOptions` 且不继承父级上下文的后端始终公开可选的 `provider`、`model` 和 `reasoning_effort` 字段。在委派工具旁加载一次 `@deepseek-ai/dsh-tool-subagent/list-models` 即可共享发现功能;随附组合已包含它。没有已记录 Session 策略时,发现和显式选择只允许父 Agent 当前生效的 provider/model 及其支持的强度。设置 `modelSelectionSettings: true` 后,新顶层 Session 会读取 Host 的 `subagent-model-selection` 偏好。启用时,精确路由列表记录在 Session 中,由子 Session 继承,并在恢复时保留;之后的设置编辑只影响新 Session。没有策略的恢复 Session 仍受父级路由限制。ACP、Codex 和 Claude Code 不支持 `agentOptions`,会拒绝选择字段。 +设置 `modelSelectionSettings: true` 后,该实例公开可选的 `provider`、`model` 和 `reasoning_effort` 字段,并注册 `list_subagent_models`;未设置的实例两者都不公开,并拒绝选择字段。该实例为新顶层 Session 读取 Host 的 `subagent-model-selection` 偏好。启用时,精确路由列表记录在 Session 中,由子 Session 继承,并在恢复时保留;之后的设置编辑只影响新 Session。没有已记录 Session 策略时,发现和显式选择只允许父 Agent 当前生效的 provider/model 及其支持的强度。此模式要求后端公布 `agentOptions` 且不继承父级对话;进程内 Spawn 和 DSH SDK 满足条件,Fork、ACP、Codex 和 Claude Code 会使挂载失败。 一次调用需同时提供 `provider` 与 `model`;当配置值、父 agent 值或提供方持有的默认值能提供路由时,也可只提供推理等级。静态的 `provider.agentRouteDefaults` 在存在时构成提供方/模型基线;工具配置与模型字段会在路由相关强度合并和确切路由预检前覆盖它。没有这些默认值的提供方会使用父 agent 最新已记录请求中的兼容值,再使用父级首次请求前的创建选项,并保留配置的 `maxTokens`。更改路由但未显式提供推理等级时,会清除继承的路由自有等级,使所选模型解析自己的默认值。实时 LLM 适配器在创建子 agent 前校验有效路由。目录成员资格只提供建议,因此适配器接受时,模型可以使用未列出的 id。 @@ -81,7 +80,7 @@ kind: "package-reference" ### 设计理念 -一个实例对应一个提供方和一个工具名。插件在提供方存在时向所属组合注册定义,并在提供方离开时释放它。Agent 继承这份共享定义,因此现有工具过滤器即可生效,无需逐 Agent 注册。读取设置的实例在 Agent 创建时记录 Session 授权;调用时独立于注册读取持久策略。直接配置 Agent 时显式传入尚未发布的 Session。提供方不支持的数字 `maxDepth` 或配置的 LLM 选择会使挂载失败。发现功能由一个独立组合项提供。本包不导出运行时不变量:策略直接来自 Session 投影,并在执行时校验,没有另行保存的逐 Agent 定义状态需要协调。 +一个实例对应一个提供方和一个工具名。插件在提供方存在时向所属组合注册定义,并在提供方离开时释放它。Agent 继承这份共享定义,因此现有工具过滤器即可生效,无需逐 Agent 注册。读取设置的实例还会在同一组合中注册 `list_subagent_models`,并在 Agent 创建时记录 Session 授权;调用时独立于注册读取持久策略。直接配置 Agent 时显式传入尚未发布的 Session。提供方不支持的数字 `maxDepth` 或配置的 LLM 选择会使挂载失败。由于 `list_subagent_models` 使用全局名称,一个工具作用域中最多只有一个实例可以设置 `modelSelectionSettings`。本包不导出运行时不变量:策略直接来自 Session 投影,并在执行时校验。 ### Activation 所有权 @@ -129,11 +128,11 @@ kind: "package-reference" #### 模型看到什么 -生成的默认 [`subagent` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-subagent) 在提供方存在时以配置的工具名公开。支持 `agentOptions` 且不继承父级上下文的后端始终公开 `provider`、`model` 和 `reasoning_effort`。上下文继承和继续执行能力决定任务与后续工作描述。工具限制同时移除 schema 及其 `tool:` 指导段。 +生成的默认 [`subagent` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-subagent) 在提供方存在时以配置的工具名公开。设置了 `modelSelectionSettings` 的实例在每个 Session 中都会增加 `provider`、`model`、`reasoning_effort` 及路由指导。上下文继承和继续执行能力决定任务与后续工作描述。工具限制同时移除 schema 及其 `tool:` 指导段。 #### Token 影响 -每次父级请求承担固定 schema 成本,其中支持选择的后端包含三个可选路由字段。每个工具实例增加一份 schema;可见委派工具共享一个简短系统提示词段。 +每次父级请求承担固定 schema 成本;`modelSelectionSettings` 会增加三个可选路由字段。每个工具实例增加一份 schema;可见委派工具共享一个简短系统提示词段。 #### KV Cache 影响 @@ -143,11 +142,11 @@ kind: "package-reference" #### 模型看到什么 -共享的 `list_subagent_models` 定义读取调用 Session 的路由策略,没有已记录策略时使用当前父级路由。可选的 `ctx.llm` 服务不可用时,调用会被拒绝。有已记录策略时,提供方模型列表取公布目录与 Session 授权路由的交集。没有策略时,只解析当前父级模型,即使目录未列出它。精确模型查询允许已授权但目录未列出的 ID;未授权的提供方/模型查询会在访问适配器前失败。目录或精确模型查询失败以及取消都会使调用被拒绝。执行过程独立强制同一策略。 +读取设置的实例注册的 `list_subagent_models` 定义读取调用 Session 的路由策略,没有已记录策略时使用当前父级路由。可选的 `ctx.llm` 服务不可用时,调用会被拒绝。有已记录策略时,提供方模型列表取公布目录与 Session 授权路由的交集。没有策略时,只解析当前父级模型,即使目录未列出它。精确模型查询允许已授权但目录未列出的 ID;未授权的提供方/模型查询会在访问适配器前失败。目录或精确模型查询失败以及取消都会使调用被拒绝。执行过程独立强制同一策略。 #### Token 影响 -随附组合中始终存在一份固定发现 schema,不受模型选择偏好影响。只有模型调用工具时,路由元数据才进入 transcript。 +设置了 `modelSelectionSettings` 的组合中存在一份固定发现 schema,不受 Session 偏好影响;随附的 Web preset 包含它,headless 与 SDK profile 不包含。只有模型调用工具时,路由元数据才进入 transcript。 #### KV Cache 影响 @@ -209,8 +208,8 @@ Start independent delegations with `subagent` or `subagent_fork` together in one 这些限制说明本工具不返回或不强制执行什么;它们是当前包约束。 - **本工具不等待结果**——使用完成通知或子任务消息。程序化结果等待由 subagent 服务提供。 -- **fork 工具拒绝模型选择** — 继承父级上下文的工具既不公开也不接受 `provider`、`model` 或 `reasoning_effort`,即使 Session 已授权也不例外。部署仍可配置 `agentOptions`。 -- **非路由子级策略按实例固定** — 更换角色、工具过滤器或深度上限,需要另一个不同名称的工具。显式 LLM 选择受 Session 授权路由限制,没有启用策略时只允许当前父级路由;不支持选择的后端会拒绝选择字段。 +- **fork 工具拒绝模型选择** — 继承父级上下文的工具既不公开也不接受 `provider`、`model` 或 `reasoning_effort`,即使 Session 已授权也不例外;`modelSelectionSettings` 会使其挂载失败。部署仍可配置 `agentOptions`。 +- **非路由子级策略按实例固定** — 更换角色、工具过滤器或深度上限,需要另一个不同名称的工具。显式 LLM 选择需要 `modelSelectionSettings`,并受 Session 授权路由限制,没有启用策略时只允许当前父级路由。 ### 开发备注 diff --git a/packages/subagent/tool-subagent/package.json b/packages/subagent/tool-subagent/package.json index fbc8d717d5..a535e39446 100644 --- a/packages/subagent/tool-subagent/package.json +++ b/packages/subagent/tool-subagent/package.json @@ -24,10 +24,6 @@ }, "./src/*": "./src/*", "./package.json": "./package.json", - "./list-models": { - "types": "./lib/types/list-models.d.ts", - "default": "./lib/list-models.js" - }, "./locale/*.json": "./locale/*.json", "./model-selection-settings/locale/*.json": "./locale/model-selection-settings/*.json" }, @@ -35,7 +31,6 @@ "locale/*.json", "locale/model-selection-settings/*.json", "lib/index.js", - "lib/list-models.js", "lib/model-selection-settings.js", "lib/types/**/*.d.ts" ], diff --git a/packages/subagent/tool-subagent/src/index.ts b/packages/subagent/tool-subagent/src/index.ts index 19af35e38f..928e405f9f 100644 --- a/packages/subagent/tool-subagent/src/index.ts +++ b/packages/subagent/tool-subagent/src/index.ts @@ -29,6 +29,7 @@ import { requestedAgentOptions, } from './model-selection.ts' import type { DelegationModelRequest } from './model-selection.ts' +import { registerListSubagentModels } from './list-models.ts' import type {} from './model-selection-settings.ts' import { recordSubagentModelSelection, @@ -53,8 +54,11 @@ export interface Config { */ toolName?: string /** - * Sample the Host `subagent-model-selection` setting for each new top-level - * Session and inherit that decision in its child Sessions. + * Expose optional child `provider`, `model`, and `reasoning_effort` fields + * plus `list_subagent_models`, and sample the Host `subagent-model-selection` + * setting for each new top-level Session; child Sessions inherit that + * decision. Requires a provider with `agentOptions` that does not inherit + * the parent conversation. At most one instance per tool scope may enable it. */ modelSelectionSettings?: boolean /** @@ -169,7 +173,7 @@ export function apply(ctx: Context, config: Config, session?: Session): void { } const toolName = config.toolName ?? 'subagent' - const modelSelectionCapable = config.modelSelectionSettings === true + const modelSelectionEnabled = config.modelSelectionSettings === true ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition) const assertSubagentProviderConfiguration = (subagentProvider: SubagentProvider): void => { @@ -184,12 +188,17 @@ export function apply(ctx: Context, config: Config, session?: Session): void { `tool-subagent: provider "${subagentProvider.name}" does not support child agentOptions`, ) } - if (modelSelectionCapable && !subagentProvider.capabilities.agentOptions) { + if (modelSelectionEnabled && !subagentProvider.capabilities.agentOptions) { throw new Error( `tool-subagent: provider "${subagentProvider.name}" does not support child model selection`, ) } - + if (modelSelectionEnabled && subagentProvider.inheritsParentContext) { + throw new Error( + `tool-subagent: provider "${subagentProvider.name}" inherits the parent conversation and does not accept child model selection` + + ' — remove `modelSelectionSettings` from this instance', + ) + } } // Validate provider-owned config outside the optional LLM binding so an @@ -201,12 +210,18 @@ export function apply(ctx: Context, config: Config, session?: Session): void { if (initialProvider !== undefined) assertSubagentProviderConfiguration(initialProvider) const install = (runtimeCtx: Context): void => { + if (modelSelectionEnabled) { + registerListSubagentModels(runtimeCtx, (exec) => { + if (exec.agent === undefined) throw new Error('list_subagent_models requires a calling agent') + const routes = subagentModelSelectionPolicy(runtimeCtx.sessionProjections, exec.agent.session) + return routes === undefined ? undefined : { routes } + }) + } // Load order and HMR replacement can change provider availability while // this fiber remains active. let mounted: { subagentProvider: SubagentProvider; disposeTool: () => void } | undefined const mount = (subagentProvider: SubagentProvider): void => { assertSubagentProviderConfiguration(subagentProvider) - const modelSelectionEnabled = subagentProvider.capabilities.agentOptions && !subagentProvider.inheritsParentContext const wording = providerWording(subagentProvider.inheritsParentContext) const providerRouteDefaults = subagentProvider.agentRouteDefaults const selectionDescription = providerRouteDefaults !== undefined diff --git a/packages/subagent/tool-subagent/src/list-models.ts b/packages/subagent/tool-subagent/src/list-models.ts index 5c100eedf6..a25c9db8c8 100644 --- a/packages/subagent/tool-subagent/src/list-models.ts +++ b/packages/subagent/tool-subagent/src/list-models.ts @@ -6,7 +6,6 @@ import type { LlmProviderInfo } from '@deepseek-ai/dsh-llm' import { defineTool } from '@deepseek-ai/dsh-tools' import { parentAgentOptionsForDelegation } from '@deepseek-ai/dsh-subagent' import type { ToolRunContext } from '@deepseek-ai/dsh-tools' -import { subagentModelSelectionPolicy, subagentModelSelectionProjectionDefinition } from './model-selection-state.ts' import type { ModelSelectionPolicy } from './model-selection.ts' interface ListSubagentModelsRequest { @@ -95,8 +94,8 @@ async function listSubagentModels( } /** - * Register `list_subagent_models` in the owning composition. - * Provider listings intersect the catalog with authorization, or resolve only the parent model when selection is disabled. + * Register `list_subagent_models` for the delegation-tool instance that owns model selection. + * Provider listings intersect the catalog with authorization, or resolve only the parent model without a Session policy. * @param ctx - Context whose tool registry owns the fixed discovery definition. * @param policyFor - Read the calling Session’s authorized routes; undefined restricts discovery to its parent model. */ @@ -126,21 +125,3 @@ export function registerListSubagentModels(ctx: Context, policyFor: (exec: ToolR }, })) } - -/** Shared discovery plugin for all delegation tools in one composition. */ -export const name = 'tool-subagent-list-models' -/** Services used to register discovery and read durable route policy. */ -export const inject = ['tools', 'sessionProjections'] - -/** - * Register one discovery tool whose results follow the calling Session’s policy. - * @param ctx - Owning composition context. - */ -export function apply(ctx: Context): void { - ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition) - registerListSubagentModels(ctx, (exec) => { - if (exec.agent === undefined) throw new Error('list_subagent_models requires a calling agent') - const routes = subagentModelSelectionPolicy(ctx.sessionProjections, exec.agent.session) - return routes === undefined ? undefined : { routes } - }) -} diff --git a/packages/subagent/tool-subagent/tests/fixtures/model-discovery.mjs b/packages/subagent/tool-subagent/tests/fixtures/model-discovery.mjs deleted file mode 100644 index 871ddc0ffc..0000000000 --- a/packages/subagent/tool-subagent/tests/fixtures/model-discovery.mjs +++ /dev/null @@ -1,38 +0,0 @@ -/** Deterministic metadata lookups for the model-discovery transcript. */ -import { LlmAdapter } from '@deepseek-ai/dsh-llm' - -class DiscoveryAdapter extends LlmAdapter { - async listModels(provider) { - return [ - { provider, id: 'healthy', name: 'Healthy' }, - { provider, id: 'unauthorized', name: 'Unauthorized' }, - ] - } - - async resolveModel(provider, model) { - if (model === 'removed') throw new Error('model was removed') - return { provider, id: model, name: 'Healthy' } - } - - async *stream() { - throw new Error('metadata-only fixture does not serve model requests') - } -} - -export const name = 'discovery-fixture' -export const inject = ['llm'] - -export function apply(ctx) { - ctx.llm.registerAdapter(['discovery-fixture'], new DiscoveryAdapter()) - ctx.on('agent/pre-step', async ({ agent, turn, step }, next) => { - if (turn === 1 && step === 1) { - agent.session.append('subagent/model-selection-policy', { - allowedModels: [ - { provider: 'discovery-fixture', model: 'removed' }, - { provider: 'discovery-fixture', model: 'healthy' }, - ], - }) - } - return next() - }) -} diff --git a/packages/subagent/tool-subagent/tests/harness.ts b/packages/subagent/tool-subagent/tests/harness.ts index 061161673e..01279f3b13 100644 --- a/packages/subagent/tool-subagent/tests/harness.ts +++ b/packages/subagent/tool-subagent/tests/harness.ts @@ -9,7 +9,6 @@ import SubagentRuntime from '@deepseek-ai/dsh-subagent' import { SessionId } from '@deepseek-ai/dsh-session' import * as mock from './scripted-provider.ts' import * as tool from '../src/index.ts' -import * as discovery from '../src/list-models.ts' import SubagentModelSelectionConfig from '../src/model-selection-settings.ts' const contexts = new Set() @@ -35,6 +34,8 @@ let setupAgentCounter = 0 /** Test-only opt-in translated to the real Host setting and Session path. */ type SetupConfig = tool.Config & { withModelSelection?: boolean + /** Host setting sampled by a `withModelSelection` setup; defaults to enabled. */ + hostSelectionEnabled?: boolean parentAgentOptions?: AgentOptions } @@ -49,17 +50,16 @@ const TEST_ALLOWED_MODELS = [ export async function setup(toolConfig: SetupConfig, mockConfig: Partial = {}): Promise { const ctx = ownContext(new Context()) - const { withModelSelection, parentAgentOptions, ...config } = toolConfig + const { withModelSelection, hostSelectionEnabled = true, parentAgentOptions, ...config } = toolConfig if (withModelSelection === true) { await ctx.plugin(SubagentModelSelectionConfig, { - enabled: true, + enabled: hostSelectionEnabled, allowedModels: TEST_ALLOWED_MODELS, }) await mountAgentLoopTestDependencies(ctx) await ctx.plugin(AgentLoop, { agents: [] }) await mountWorkingDirectoryFixture(ctx) await ctx.plugin(SubagentRuntime) - await ctx.plugin(discovery) const provider = await mock.mountScriptedProvider(ctx, { name: 'mock', ...mockConfig }) setupProviders.set(ctx, provider) const handle = await ctx.agents.create({ @@ -82,7 +82,6 @@ export async function setup(toolConfig: SetupConfig, mockConfig: Partial { - it('is loaded through its own composition entry', async () => { + it('is omitted unless its delegation-tool instance owns model selection', async () => { const ctx = new Context() await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(LlmRuntime) @@ -114,14 +116,23 @@ describe('list_subagent_models', () => { it('requires a calling Agent and an effective parent route', async () => { const ctx = new Context() + await ctx.plugin(SubagentModelSelectionConfig) await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) - await ctx.plugin({ apply, inject, name }) + await mountWorkingDirectoryFixture(ctx) + await ctx.plugin(SubagentRuntime) + await ctx.plugin({ + name: 'direct-tool-subagent', + inject: tool.inject, + apply: (direct: Context) => { + tool.apply(direct, { provider: 'unused', modelSelectionSettings: true }, Session.create(SessionId('direct-discovery'))) + }, + }) expect(text(await call(ctx, {}))).toContain('requires a calling agent') await ctx.fiber.dispose() - const parentContext = await setup({ provider: 'mock' }) + const parentContext = await setup({ provider: 'mock', withModelSelection: true, hostSelectionEnabled: false }) const agent = modelSelectionSetupAgent(parentContext) const result = await parentContext.tools.execute({ signal: testToolSignal, callId: ToolCallId('missing-parent-route'), @@ -131,7 +142,7 @@ describe('list_subagent_models', () => { expect(text(result)).toContain('without a parent provider and model') }) - it('reads the recorded Session allowlist through the shared discovery definition', async () => { + it('reads the recorded Session allowlist through the settings-owned discovery definition', async () => { const ctx = await setup({ provider: 'mock', withModelSelection: true }) ctx.llm.registerAdapter(['alpha', 'secret'], new CatalogAdapter()) const result = await ctx.tools.execute({ @@ -279,8 +290,13 @@ describe('list_subagent_models', () => { } }) - it.each([false, true])('resolves only the current parent model when selection is disabled (empty catalog: %s)', async (empty) => { - const ctx = await setup({ provider: 'mock', parentAgentOptions: { provider: 'alpha', model: 'unlisted' } }) + it.each([false, true])('resolves only the current parent model without a Session policy (empty catalog: %s)', async (empty) => { + const ctx = await setup({ + provider: 'mock', + withModelSelection: true, + hostSelectionEnabled: false, + parentAgentOptions: { provider: 'alpha', model: 'unlisted' }, + }) const adapter = new CatalogAdapter(empty) const listModels = vi.spyOn(adapter, 'listModels') ctx.llm.registerAdapter(['alpha'], adapter) diff --git a/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts b/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts index a4dc33b0fa..ff374ed096 100644 --- a/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts +++ b/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts @@ -14,7 +14,6 @@ import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import SubagentRuntime from '@deepseek-ai/dsh-subagent' import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process' import * as tool from '../src/index.ts' -import * as discovery from '../src/list-models.ts' import SubagentModelSelectionConfig from '../src/model-selection-settings.ts' import { subagentModelSelectionPolicy, @@ -49,10 +48,8 @@ async function boot(withPreset = true): Promise { await mountWorkingDirectoryFixture(ctx) subagentConfigs.set(ctx, await liveConfig(ctx, SubagentRuntime)) await ctx.plugin(SubagentSpawn, { providerName: 'spawn' }) - await ctx.plugin(discovery) if (withPreset) { const preset = createScope(ctx, { preset: 'model-selection-test' }) - await preset.ctx.plugin(discovery) await preset.ctx.plugin(tool, { provider: 'spawn', modelSelectionSettings: true, @@ -272,7 +269,6 @@ describe('SubagentModelSelectionConfig', () => { const ctx = await boot(false) const preset = createScope(ctx, { preset: 'standard' }) const other = createScope(ctx, { preset: 'minimal' }) - await preset.ctx.plugin(discovery) await preset.ctx.plugin(tool, { provider: 'spawn', modelSelectionSettings: true, diff --git a/packages/subagent/tool-subagent/tests/model-selection.spec.ts b/packages/subagent/tool-subagent/tests/model-selection.spec.ts index 4a99144c81..69b72ae876 100644 --- a/packages/subagent/tool-subagent/tests/model-selection.spec.ts +++ b/packages/subagent/tool-subagent/tests/model-selection.spec.ts @@ -141,13 +141,37 @@ describe('dsh-tool-subagent model selection', () => { expect(text(result)).toContain('model selection is disabled for this tool instance') }) - it('keeps route fields visible but rejects other routes when selection is disabled', async () => { + it('hides and rejects route fields without modelSelectionSettings', async () => { const ctx = await setup({ provider: 'mock', parentAgentOptions: { provider: 'alpha', model: 'parent-model' } }) const schema = ctx.tools.schemas().find(entry => entry.name === 'subagent')! const props = (schema.parameters as { properties?: Record }).properties ?? {} + expect(Object.keys(props).sort()).toEqual(['cwd', 'description', 'prompt']) + expect(schema.description).not.toContain('list_subagent_models') + expect(ctx.tools.get('list_subagent_models')).toBeUndefined() + + for (const selection of [ + { provider: 'alpha', model: 'parent-model' }, + { reasoning_effort: 'low' }, + ]) { + const result = await callSubagent(ctx, { description: 'forced route', prompt: 'do it', ...selection }) + expect(result.isError).toBe(true) + expect(text(result)).toContain('child model selection is disabled for this tool instance') + } + }) + + it('keeps settings-owned route fields visible but rejects other routes without a Session policy', async () => { + const ctx = await setup({ + provider: 'mock', + withModelSelection: true, + hostSelectionEnabled: false, + parentAgentOptions: { provider: 'alpha', model: 'parent-model' }, + }) + const agent = modelSelectionSetupAgent(ctx) + const schema = ctx.tools.schemas(agent).find(entry => entry.name === 'subagent')! + const props = (schema.parameters as { properties?: Record }).properties ?? {} expect(Object.keys(props).sort()).toEqual(['cwd', 'description', 'model', 'prompt', 'provider', 'reasoning_effort']) expect(schema.description).toContain('list_subagent_models') - expect(ctx.tools.get('list_subagent_models')).toBeDefined() + expect(ctx.tools.get('list_subagent_models', agent)).toBeDefined() const result = await callSubagent(ctx, { description: 'forced route', @@ -159,10 +183,12 @@ describe('dsh-tool-subagent model selection', () => { expect(text(result)).toContain('is not allowed for this Session') }) - it('discovers only the parent route by default and permits its explicit route and effort', async () => { + it('discovers only the parent route without a Session policy and permits its explicit route and effort', async () => { const requests: SubagentStartRequest[] = [] const ctx = await setup({ provider: 'mock', + withModelSelection: true, + hostSelectionEnabled: false, parentAgentOptions: parentWithRoute().options, }, { onStart: (request) => { requests.push(request) } }) ctx.llm.registerAdapter(['alpha', 'other-provider'], new MockAdapter([], REASONING)) @@ -469,17 +495,27 @@ describe('dsh-tool-subagent model selection', () => { expect(resolve).not.toHaveBeenCalled() }) + it('rejects modelSelectionSettings on a provider that inherits the parent conversation', async () => { + await expect(setup({ provider: 'mock', withModelSelection: true }, { inheritsParentContext: true })) + .rejects.toThrow('provider "mock" inherits the parent conversation and does not accept child model selection') + }) + it('keeps fork model selection unavailable even with Session authorization', async () => { const started = vi.fn() - const ctx = await setup({ provider: 'mock', withModelSelection: true }, { + const ctx = await setup({ provider: 'mock' }, { inheritsParentContext: true, onStart: started, }) - const schema = ctx.tools.schemas(modelSelectionSetupAgent(ctx)).find(entry => entry.name === 'subagent')! + const agent = modelSelectionSetupAgent(ctx) + agent.session.append('subagent/model-selection-policy', { + allowedModels: [{ provider: 'alpha', model: 'fast-model' }], + }) + const schema = ctx.tools.schemas(agent).find(entry => entry.name === 'subagent')! const props = (schema.parameters as { properties?: Record }).properties ?? {} expect(Object.keys(props).sort()).toEqual(['cwd', 'description', 'prompt']) expect(schema.description).toContain('inherits this conversation') - expect(schema.description).not.toContain('Child LLM selection') + expect(schema.description).not.toContain('list_subagent_models') + expect(ctx.tools.get('list_subagent_models', agent)).toBeUndefined() for (const selection of [ { provider: 'alpha', model: 'fast-model' }, diff --git a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts index 4a0a1a6a08..fb39038860 100644 --- a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts +++ b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts @@ -102,10 +102,7 @@ describe('dsh-tool-subagent', () => { expect(Object.keys(props).sort()).toEqual([ 'cwd', 'description', - 'model', 'prompt', - 'provider', - 'reasoning_effort', ]) expect(schema!.description).not.toContain('job_output') }) diff --git a/packages/subagent/tool-subagent/tsdown.config.ts b/packages/subagent/tool-subagent/tsdown.config.ts index 8cdb8ec132..4a467f2c5e 100644 --- a/packages/subagent/tool-subagent/tsdown.config.ts +++ b/packages/subagent/tool-subagent/tsdown.config.ts @@ -15,5 +15,4 @@ const entry = (path: string) => ({ export default defineConfig([ entry('lib/types/index.js'), entry('lib/types/model-selection-settings.js'), - entry('lib/types/list-models.js'), ]) diff --git a/scripts/check-workspace-constraints.ts b/scripts/check-workspace-constraints.ts index 7ea4d2e0aa..1880c55d1e 100644 --- a/scripts/check-workspace-constraints.ts +++ b/scripts/check-workspace-constraints.ts @@ -195,8 +195,9 @@ const packageFileExtras: Readonly> = { '@deepseek-ai/dsh-experimental-inspector': ['lib/client.*.js', 'lib/worker.js', 'lib/devtools/**'], // Creator's composition guidance travels with the declaration package. '@deepseek-ai/dsh-agent-preset': ['skills'], - // Settings and shared discovery have independent composition entries. - '@deepseek-ai/dsh-tool-subagent': ['lib/list-models.js', 'lib/model-selection-settings.js'], + // The Web Host mounts the default-off settings owner independently of the + // delegation-tool instances. + '@deepseek-ai/dsh-tool-subagent': ['lib/model-selection-settings.js'], // The JSONL backend resolves its private verification Worker relative to // import.meta.url; it is shipped without a public package subpath. '@deepseek-ai/dsh-session-persistence-jsonl': ['lib/worker.cjs'], diff --git a/scripts/gen-tool-catalog.ts b/scripts/gen-tool-catalog.ts index 81b10491e0..4cfa955c2d 100644 --- a/scripts/gen-tool-catalog.ts +++ b/scripts/gen-tool-catalog.ts @@ -73,7 +73,7 @@ import * as PluginManagerTools from '@deepseek-ai/dsh-plugin-manager/tools' import SandboxPolicy from '@deepseek-ai/dsh-sandbox-policy' import McpResources from '@deepseek-ai/dsh-mcp-resources' import * as ToolSubagent from '@deepseek-ai/dsh-tool-subagent' -import * as SubagentModelDiscovery from '../packages/subagent/tool-subagent/src/list-models.ts' +import { registerListSubagentModels } from '../packages/subagent/tool-subagent/src/list-models.ts' import * as ToolWeb from '@deepseek-ai/dsh-tool-web' import WorkflowEngine from '@deepseek-ai/dsh-workflow' import type { WorkflowRun, WorkflowStartRequest } from '@deepseek-ai/dsh-workflow' @@ -519,10 +519,10 @@ const TOOL_PACKAGES: ToolPackage[] = [ await ctx.plugin(LlmRuntime) registerCatalogSubagentProvider(ctx, 'mock') await ctx.plugin(ToolSubagent, { provider: 'mock' }) - await ctx.plugin(SubagentModelDiscovery) + registerListSubagentModels(ctx, () => ({ routes: [{ provider: 'mock', model: 'mock' }] })) }, note: - 'The registered delegation name is the load-time `toolName` config (default `subagent`); backends supporting agentOptions without inheriting parent context always expose optional route fields, and the shared discovery plugin resolves the calling Session’s allowed routes. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` rejects model-selection arguments; without an enabled policy, spawn selection and discovery are restricted to the current parent route. Each instance selects its backend through `provider` and reads model-selection settings through `modelSelectionSettings`. Every delegation returns a child id promptly; local children accept follow-ups, while external backends report their single execution automatically.', + 'The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion registered by an instance with `modelSelectionSettings`. That instance always exposes optional route fields; Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions, and without an enabled policy, selection and discovery are restricted to the current parent route. `subagent_fork` exposes no route selection. Each instance selects its backend through `provider`. Every delegation returns a child id promptly; local children accept follow-ups, while external backends report their single execution automatically.', }, { pkg: '@deepseek-ai/dsh-tool-subagent-control', diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/result.json b/scripts/snapshots/python-sdk-single-exe/advanced/result.json index 333c0cd655..0cb043baf0 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/result.json +++ b/scripts/snapshots/python-sdk-single-exe/advanced/result.json @@ -207,7 +207,6 @@ "job_kill", "job_list", "job_output", - "list_subagent_models", "snapshot_double", "subagent", "workflow", @@ -304,7 +303,6 @@ "job_kill", "job_list", "job_output", - "list_subagent_models", "snapshot_double", "subagent", "workflow", @@ -1763,7 +1761,6 @@ "job_kill", "job_list", "job_output", - "list_subagent_models", "snapshot_double", "subagent", "workflow", @@ -1890,7 +1887,6 @@ "job_kill", "job_list", "job_output", - "list_subagent_models", "snapshot_double", "subagent", "workflow", @@ -2961,7 +2957,6 @@ "job_kill", "job_list", "job_output", - "list_subagent_models", "snapshot_double", "subagent", "workflow", @@ -3720,7 +3715,6 @@ "job_kill", "job_list", "job_output", - "list_subagent_models", "snapshot_double", "subagent", "workflow", diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.1.v4.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.1.v4.jsonl index 2c5e9c5f05..a169f4f81a 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.1.v4.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.1.v4.jsonl @@ -13,7 +13,7 @@ {"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin:serial-created-fixture"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly DIRECT_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"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 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: 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: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":"{{messageId}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","reasoningEffort":"high","maxTokens":256000},"adapterDefaults":{"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","list_subagent_models","snapshot_double","subagent","workflow","working_directory"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","reasoningEffort":"high","maxTokens":256000},"adapterDefaults":{"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","snapshot_double","subagent","workflow","working_directory"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} {"type":"session/title","data":{"title":"Reply with exactly DIRECT_CHILD_OK and","messageSeqs":[12],"source":{"kind":"fallback"}}} {"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{child-1}}","sessionFormatVersion":4,"throughSeq":16}} diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.2.v4.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.2.v4.jsonl index 5ec93b7783..88aaf9d5e2 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.2.v4.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.2.v4.jsonl @@ -13,7 +13,7 @@ {"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"text","text":"Serial agent creation completed."}],"source":{"kind":"plugin:serial-created-fixture"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Reply with exactly WORKFLOW_CHILD_OK and nothing else."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"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 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: 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: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":"{{messageId}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","reasoningEffort":"high","maxTokens":256000},"adapterDefaults":{"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","list_subagent_models","snapshot_double","subagent","workflow","working_directory"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","reasoningEffort":"high","maxTokens":256000},"adapterDefaults":{"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","snapshot_double","subagent","workflow","working_directory"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} {"type":"session/title","data":{"title":"Reply with exactly WORKFLOW_CHILD_OK and","messageSeqs":[12],"source":{"kind":"fallback"}}} {"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{child-2}}","sessionFormatVersion":4,"throughSeq":16}} diff --git a/scripts/snapshots/python-sdk-single-exe/advanced/session.v4.jsonl b/scripts/snapshots/python-sdk-single-exe/advanced/session.v4.jsonl index ed4f842bde..dcd09c277c 100644 --- a/scripts/snapshots/python-sdk-single-exe/advanced/session.v4.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/advanced/session.v4.jsonl @@ -13,12 +13,12 @@ {"type":"user/message","data":{"content":[{"type":"text","text":"Run the advanced packaged-runtime snapshot scenario."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} {"type":"user/message","data":{"id":"{{messageId}}","role":"user","content":[{"type":"image","attachment":{"attachmentId":"sha256:b1ff9c8ea3a780bad09b346c423d2d0e46815926879b18e841d928376a946640","mediaType":"image/png","width":1,"height":1,"bytes":69}}],"source":{"kind":"python-snapshot-image-offload"}},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","list_subagent_models","snapshot_double","subagent","workflow","working_directory"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","snapshot_double","subagent","workflow","working_directory"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} {"type":"session/title","data":{"title":"Run the advanced packaged-runtime snapsh","messageSeqs":[11],"source":{"kind":"fallback"}}} {"type":"assistant/attempt","data":{"turn":1,"step":1,"stream":[{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"error","failure":{"code":"IMAGE_OFFLOAD_REQUIRED","message":"authored image budget failure","offloadImages":1}}}}]}} {"type":"image/offload","data":{"targets":[{"seq":13,"imageIndexes":[0]}]}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","list_subagent_models","snapshot_double","subagent","workflow","working_directory"]},"reason":"series"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","snapshot_double","subagent","workflow","working_directory"]},"reason":"series"}} {"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{parent}}","sessionFormatVersion":4,"throughSeq":19}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-native","name":"snapshot_double","arguments":"{\"value\": 21}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-native","name":"snapshot_double","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"advanced-native","args":["{\"value\": 21}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-native","name":"snapshot_double","arguments":"{\"value\": 21}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"}]}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"advanced-native","name":"snapshot_double","arguments":"{\"value\": 21}"}} diff --git a/scripts/snapshots/python-sdk-single-exe/dynamic-tools/tool-history.json b/scripts/snapshots/python-sdk-single-exe/dynamic-tools/tool-history.json index 8637546c53..d93f0c33c1 100644 --- a/scripts/snapshots/python-sdk-single-exe/dynamic-tools/tool-history.json +++ b/scripts/snapshots/python-sdk-single-exe/dynamic-tools/tool-history.json @@ -56,7 +56,6 @@ "job_list", "job_output", "list_agents", - "list_subagent_models", "read", "read_image", "send_message", @@ -113,7 +112,6 @@ "job_list", "job_output", "list_agents", - "list_subagent_models", "read", "read_image", "send_message", @@ -185,7 +183,6 @@ "job_list", "job_output", "list_agents", - "list_subagent_models", "read", "read_image", "send_message", diff --git a/scripts/snapshots/python-sdk-single-exe/restart/requests.json b/scripts/snapshots/python-sdk-single-exe/restart/requests.json index 3c4c8b10d3..06bbf97d4f 100644 --- a/scripts/snapshots/python-sdk-single-exe/restart/requests.json +++ b/scripts/snapshots/python-sdk-single-exe/restart/requests.json @@ -23,7 +23,6 @@ "job_kill", "job_list", "job_output", - "list_subagent_models", "subagent", "workflow", "working_directory" @@ -53,7 +52,6 @@ "job_kill", "job_list", "job_output", - "list_subagent_models", "subagent", "workflow", "working_directory" diff --git a/scripts/snapshots/python-sdk-single-exe/restart/session.1.v4.jsonl b/scripts/snapshots/python-sdk-single-exe/restart/session.1.v4.jsonl index 3ce01a7a4d..59d1880f27 100644 --- a/scripts/snapshots/python-sdk-single-exe/restart/session.1.v4.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/restart/session.1.v4.jsonl @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{messageId}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Complete the first isolated Python SDK process turn."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","list_subagent_models","subagent","workflow","working_directory"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","subagent","workflow","working_directory"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} {"type":"session/title","data":{"title":"Complete the first isolated Python","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session-1}}","sessionFormatVersion":4,"throughSeq":12}} diff --git a/scripts/snapshots/python-sdk-single-exe/restart/session.2.v4.jsonl b/scripts/snapshots/python-sdk-single-exe/restart/session.2.v4.jsonl index cc12894292..be7dd18e39 100644 --- a/scripts/snapshots/python-sdk-single-exe/restart/session.2.v4.jsonl +++ b/scripts/snapshots/python-sdk-single-exe/restart/session.2.v4.jsonl @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{messageId}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Complete the second isolated Python SDK process turn."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","list_subagent_models","subagent","workflow","working_directory"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","subagent","workflow","working_directory"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} {"type":"session/title","data":{"title":"Complete the second isolated Python","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"session-log-deepseek/delivery-accepted","data":{"sessionId":"{{session-2}}","sessionFormatVersion":4,"throughSeq":12}} 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 fc8d44ed52..c679ff36b7 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 @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{messageId}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Run three todo updates; the scheduler will reject the second call."}],"source":{"kind":"user"},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{messageId}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","list_subagent_models","subagent","todo_write","workflow","working_directory"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"smoke-model","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["cordis_inspect_list","cordis_inspect_query","job_kill","job_list","job_output","subagent","todo_write","workflow","working_directory"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"smoke-model","contextWindow":1000000}} {"type":"session/title","data":{"title":"Run three todo updates; the","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"scheduler-complete","name":"todo_write","arguments":"{\"todos\": []}"},{"type":"tool-call","id":"scheduler-fail","name":"todo_write","arguments":"{\"todos\": []}"},{"type":"tool-call","id":"scheduler-unstarted","name":"todo_write","arguments":"{\"todos\": []}"}],"source":{"kind":"model","provider":"deepseek-official","model":"smoke-model","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"},{"type":"tool-call"},{"type":"tool-call"}]}},"id":"{{messageId}}"},"usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"scheduler-complete","name":"todo_write","args":[""]},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"scheduler-complete","args":["{\"todos\": []}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"scheduler-complete","name":"todo_write","arguments":"{\"todos\": []}"}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":1,"dt":[],"id":"scheduler-fail","name":"todo_write","args":[""]},{"type":"tool-call-chunks","time0":0,"index":1,"dt":[],"id":"scheduler-fail","args":["{\"todos\": []}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"scheduler-fail","name":"todo_write","arguments":"{\"todos\": []}"}}},{"type":"chunk","time":0,"chunk":{"type":"block-start","index":2,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":2,"dt":[],"id":"scheduler-unstarted","name":"todo_write","args":[""]},{"type":"tool-call-chunks","time0":0,"index":2,"dt":[],"id":"scheduler-unstarted","args":["{\"todos\": []}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":2,"block":{"type":"tool-call","id":"scheduler-unstarted","name":"todo_write","arguments":"{\"todos\": []}"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3,"totalTokens":6}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"smoke-model"},"blocks":[{"type":"tool-call"},{"type":"tool-call"},{"type":"tool-call"}]}}}]},"surfaceOp":"append"} diff --git a/snapshots/sdk/bash-tool/tool-schemas.expected.json b/snapshots/sdk/bash-tool/tool-schemas.expected.json index 185fd4ef83..2b4b1f069f 100644 --- a/snapshots/sdk/bash-tool/tool-schemas.expected.json +++ b/snapshots/sdk/bash-tool/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/sdk/dynamic-tool-updates/notifications.expected.jsonl b/snapshots/sdk/dynamic-tool-updates/notifications.expected.jsonl index b497f77dbd..bfec6f1e36 100644 --- a/snapshots/sdk/dynamic-tool-updates/notifications.expected.jsonl +++ b/snapshots/sdk/dynamic-tool-updates/notifications.expected.jsonl @@ -6,7 +6,7 @@ {"method":"session.event","params":{"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"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Read task.txt, call the newly available snapshot_ping tool once, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} {"method":"session.event","params":{"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: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} -{"method":"session.event","params":{"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":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}}}} +{"method":"session.event","params":{"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":["bash","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"Read task.txt, call the newly","messageSeqs":[8],"source":{"kind":"fallback"}}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":13,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}],"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":"call_task_read","name":"read","args":["{\"file_path\":\"task.txt\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}}},{"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"}}} @@ -14,14 +14,14 @@ {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":15,"time":0,"data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_task_read"},"toolCallId":"call_task_read","content":[{"type":"text","text":"{{cwd}}/task.txt\nfile\n\n1: Reply with the single word DONE after reading this file.\n\n(End of file - total 1 lines)\n"}],"isError":false,"id":"{{sessionId}}"},"meta":{"path":"{{cwd}}/task.txt","offset":1,"lines":[{"number":1,"text":"Reply with the single word DONE after reading this file."}],"totalLines":1}},"sourceEventSeqs":[14],"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":16,"time":0,"data":{"turn":1,"step":1}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":17,"time":0,"data":{"turn":1,"step":2}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":18,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":18,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"developer/message","seq":19,"time":0,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"snapshot_ping"}],"role":"developer","id":"{{sessionId}}"},"headerSeq":18},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":20,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"call_dynamic_ping","name":"snapshot_ping","args":["{}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":21,"time":0,"data":{"turn":1,"step":2,"callId":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/result","seq":22,"time":0,"data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"call_dynamic_ping"},"toolCallId":"call_dynamic_ping","content":[{"type":"text","text":"pong"}],"isError":false,"id":"{{sessionId}}"}},"sourceEventSeqs":[21],"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/end","seq":23,"time":0,"data":{"turn":1,"step":2}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":24,"time":0,"data":{"turn":1,"step":3}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":25,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":25,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"developer/message","seq":26,"time":0,"data":{"turn":1,"step":3,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-removal","toolName":"snapshot_ping"}],"role":"developer","id":"{{sessionId}}"}},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":27,"time":0,"data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":0,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":0,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10}}},{"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":1,"step":3}}}} diff --git a/snapshots/sdk/dynamic-tool-updates/session.v4.jsonl b/snapshots/sdk/dynamic-tool-updates/session.v4.jsonl index 8472750d16..aee79405da 100644 --- a/snapshots/sdk/dynamic-tool-updates/session.v4.jsonl +++ b/snapshots/sdk/dynamic-tool-updates/session.v4.jsonl @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Read task.txt, call the newly available snapshot_ping tool once, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Read task.txt, call the newly","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1790049391235,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790049391235,"index":0,"dt":[],"id":"call_task_read","name":"read","args":["{\"file_path\":\"task.txt\"}"]},{"type":"chunk","time":1790049391235,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}}},{"type":"chunk","time":1790049391235,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1790049391235,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} @@ -17,14 +17,14 @@ {"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_task_read"},"toolCallId":"call_task_read","content":[{"type":"text","text":"{{cwd}}/task.txt\nfile\n\n1: Reply with the single word DONE after reading this file.\n\n(End of file - total 1 lines)\n"}],"isError":false,"id":"{{message:5}}"},"meta":{"path":"{{cwd}}/task.txt","offset":1,"lines":[{"number":1,"text":"Reply with the single word DONE after reading this file."}],"totalLines":1}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"snapshot_ping"}],"role":"developer","id":"{{message:6}}"},"headerSeq":18},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"stream":[{"type":"chunk","time":1790049391255,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790049391255,"index":0,"dt":[],"id":"call_dynamic_ping","name":"snapshot_ping","args":["{}"]},{"type":"chunk","time":1790049391255,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}}},{"type":"chunk","time":1790049391255,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}} {"type":"tool/result","data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"call_dynamic_ping"},"toolCallId":"call_dynamic_ping","content":[{"type":"text","text":"pong"}],"isError":false,"id":"{{message:8}}"}},"sourceEventSeqs":[21],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-removal","toolName":"snapshot_ping"}],"role":"developer","id":"{{message:9}}"}},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10},"stream":[{"type":"chunk","time":1790049391270,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1790049391270,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1790049391270,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1790049391270,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10}}},{"type":"chunk","time":1790049391270,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} diff --git a/snapshots/sdk/persistent-tools/cordis.yml b/snapshots/sdk/persistent-tools/cordis.yml index e2ee87e8a7..6262d2fb44 100644 --- a/snapshots/sdk/persistent-tools/cordis.yml +++ b/snapshots/sdk/persistent-tools/cordis.yml @@ -84,10 +84,6 @@ name: '@deepseek-ai/dsh-tool-subagent-control' disabled: true -- id: tool-subagent-list-models - name: '@deepseek-ai/dsh-tool-subagent/list-models' - disabled: true - - id: tool-subagent-list-agents name: '@deepseek-ai/dsh-tool-subagent-control/list-agents' disabled: true diff --git a/snapshots/sdk/subagent-activation-limit/tool-schemas.1.expected.json b/snapshots/sdk/subagent-activation-limit/tool-schemas.1.expected.json index 185fd4ef83..2b4b1f069f 100644 --- a/snapshots/sdk/subagent-activation-limit/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-activation-limit/tool-schemas.1.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json b/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json index 185fd4ef83..2b4b1f069f 100644 --- a/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-continuable-inheritance/tool-schemas.1.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json b/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json index 185fd4ef83..2b4b1f069f 100644 --- a/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-continuable/tool-schemas.1.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json b/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json index ef3f413d82..ed6f50a025 100644 --- a/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json +++ b/snapshots/sdk/subagent-dsh-sdk-diagnostic/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ @@ -393,7 +364,7 @@ }, { "name": "subagent_dsh_sdk", - "description": "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. The runtime notifies you when it finishes. The completion notice includes its final answer. This backend does not accept follow-up messages. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to this backend's route defaults. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The completion notice includes its final answer. This backend does not accept follow-up messages.", "parameters": { "type": "object", "properties": { @@ -408,18 +379,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ @@ -430,7 +389,7 @@ }, { "name": "subagent_dsh_sdk_second", - "description": "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. The runtime notifies you when it finishes. The completion notice includes its final answer. This backend does not accept follow-up messages. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to this backend's route defaults. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The completion notice includes its final answer. This backend does not accept follow-up messages.", "parameters": { "type": "object", "properties": { @@ -445,18 +404,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json index 185fd4ef83..2b4b1f069f 100644 --- a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.1.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json b/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json index 185fd4ef83..2b4b1f069f 100644 --- a/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-list-agents/tool-schemas.1.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json b/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json index 185fd4ef83..2b4b1f069f 100644 --- a/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json +++ b/snapshots/sdk/subagent-send-message/tool-schemas.1.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/sdk/text-turn/tool-schemas.expected.json b/snapshots/sdk/text-turn/tool-schemas.expected.json index 185fd4ef83..2b4b1f069f 100644 --- a/snapshots/sdk/text-turn/tool-schemas.expected.json +++ b/snapshots/sdk/text-turn/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/sdk/tool-error-details/notifications.expected.jsonl b/snapshots/sdk/tool-error-details/notifications.expected.jsonl index 8d30d3d16c..e6d6e7fde2 100644 --- a/snapshots/sdk/tool-error-details/notifications.expected.jsonl +++ b/snapshots/sdk/tool-error-details/notifications.expected.jsonl @@ -6,7 +6,7 @@ {"method":"session.event","params":{"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"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"user/message","seq":8,"time":0,"data":{"content":[{"type":"text","text":"Exercise a native denial and a caught PTC inner denial, then reply ERROR_DETAILS_OK."}],"source":{"kind":"user"},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} {"method":"session.event","params":{"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: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{sessionId}}"},"surfaceOp":"append"}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":10,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["bash","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/context","seq":11,"time":0,"data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"session/title","seq":12,"time":0,"data":{"title":"Exercise a native denial and","messageSeqs":[8],"source":{"kind":"fallback"}}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":13,"time":0,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"denied-native","name":"bash","arguments":"{\"command\":\"echo AUTO_DENIED\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"denied-native","name":"bash","args":["{\"command\":\"echo AUTO_DENIED\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"denied-native","name":"bash","arguments":"{\"command\":\"echo AUTO_DENIED\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}} @@ -16,7 +16,7 @@ {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"step/start","seq":17,"time":0,"data":{"turn":1,"step":2}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"system/message","seq":18,"time":0,"data":{"turn":1,"step":2,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{sessionId}}"}},"sourceEventSeqs":[7],"surfaceOp":{"op":"replace","startSeq":7,"endSeq":7}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"request/header","seq":19,"time":0,"data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["run_code"]},"reason":"change","startsSeries":true}}}} -{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"developer/message","seq":20,"time":0,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"run_code"},{"type":"tool-removal","toolName":"bash"},{"type":"tool-removal","toolName":"create_goal"},{"type":"tool-removal","toolName":"edit"},{"type":"tool-removal","toolName":"exit_plan_mode"},{"type":"tool-removal","toolName":"get_goal"},{"type":"tool-removal","toolName":"glob"},{"type":"tool-removal","toolName":"grep"},{"type":"tool-removal","toolName":"interrupt_agent"},{"type":"tool-removal","toolName":"job_kill"},{"type":"tool-removal","toolName":"job_list"},{"type":"tool-removal","toolName":"job_output"},{"type":"tool-removal","toolName":"list_agents"},{"type":"tool-removal","toolName":"list_subagent_models"},{"type":"tool-removal","toolName":"read"},{"type":"tool-removal","toolName":"read_image"},{"type":"tool-removal","toolName":"send_message"},{"type":"tool-removal","toolName":"skill"},{"type":"tool-removal","toolName":"subagent"},{"type":"tool-removal","toolName":"subagent_fork"},{"type":"tool-removal","toolName":"todo_write"},{"type":"tool-removal","toolName":"update_goal"},{"type":"tool-removal","toolName":"web_fetch"},{"type":"tool-removal","toolName":"web_search"},{"type":"tool-removal","toolName":"workflow"},{"type":"tool-removal","toolName":"working_directory"},{"type":"tool-removal","toolName":"write"}],"role":"developer","id":"{{sessionId}}"},"headerSeq":19},"surfaceOp":"append"}}} +{"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"developer/message","seq":20,"time":0,"data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"run_code"},{"type":"tool-removal","toolName":"bash"},{"type":"tool-removal","toolName":"create_goal"},{"type":"tool-removal","toolName":"edit"},{"type":"tool-removal","toolName":"exit_plan_mode"},{"type":"tool-removal","toolName":"get_goal"},{"type":"tool-removal","toolName":"glob"},{"type":"tool-removal","toolName":"grep"},{"type":"tool-removal","toolName":"interrupt_agent"},{"type":"tool-removal","toolName":"job_kill"},{"type":"tool-removal","toolName":"job_list"},{"type":"tool-removal","toolName":"job_output"},{"type":"tool-removal","toolName":"list_agents"},{"type":"tool-removal","toolName":"read"},{"type":"tool-removal","toolName":"read_image"},{"type":"tool-removal","toolName":"send_message"},{"type":"tool-removal","toolName":"skill"},{"type":"tool-removal","toolName":"subagent"},{"type":"tool-removal","toolName":"subagent_fork"},{"type":"tool-removal","toolName":"todo_write"},{"type":"tool-removal","toolName":"update_goal"},{"type":"tool-removal","toolName":"web_fetch"},{"type":"tool-removal","toolName":"web_search"},{"type":"tool-removal","toolName":"workflow"},{"type":"tool-removal","toolName":"working_directory"},{"type":"tool-removal","toolName":"write"}],"role":"developer","id":"{{sessionId}}"},"headerSeq":19},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"assistant/message","seq":21,"time":0,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"denied-code","name":"run_code","arguments":"{\"code\":\"try { await tools.bash({command: \\\"echo AUTO_DENIED\\\"}) } catch (error) { return error.message }\",\"description\":\"Catch one denied inner call\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{sessionId}}"},"stream":[{"type":"chunk","time":0,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":0,"index":0,"dt":[],"id":"denied-code","name":"run_code","args":["{\"code\":\"try { await tools.bash({command: \\\"echo AUTO_DENIED\\\"}) } catch (error) { return error.message }\",\"description\":\"Catch one denied inner call\"}"]},{"type":"chunk","time":0,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"denied-code","name":"run_code","arguments":"{\"code\":\"try { await tools.bash({command: \\\"echo AUTO_DENIED\\\"}) } catch (error) { return error.message }\",\"description\":\"Catch one denied inner call\"}"}}},{"type":"chunk","time":0,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/call","seq":22,"time":0,"data":{"turn":1,"step":2,"callId":"denied-code","name":"run_code","arguments":"{\"code\":\"try { await tools.bash({command: \\\"echo AUTO_DENIED\\\"}) } catch (error) { return error.message }\",\"description\":\"Catch one denied inner call\"}"}}}} {"method":"session.event","params":{"sessionId":"{{sessionId}}","event":{"type":"tool/ptc-dispatch-start","seq":23,"time":0,"data":{"rootCallId":"denied-code","parentCallId":"denied-code","subCallId":"denied-code:ptc:1","name":"bash","arguments":{"command":"echo AUTO_DENIED"}}}}} diff --git a/snapshots/sdk/tool-error-details/session.v4.jsonl b/snapshots/sdk/tool-error-details/session.v4.jsonl index 806aa48ff4..871095019b 100644 --- a/snapshots/sdk/tool-error-details/session.v4.jsonl +++ b/snapshots/sdk/tool-error-details/session.v4.jsonl @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Exercise a native denial and a caught PTC inner denial, then reply ERROR_DETAILS_OK."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["bash","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Exercise a native denial and","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"denied-native","name":"bash","arguments":"{\"command\":\"echo AUTO_DENIED\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"stream":[{"type":"chunk","time":1791315723125,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791315723125,"index":0,"dt":[],"id":"denied-native","name":"bash","args":["{\"command\":\"echo AUTO_DENIED\"}"]},{"type":"chunk","time":1791315723125,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"denied-native","name":"bash","arguments":"{\"command\":\"echo AUTO_DENIED\"}"}}},{"type":"chunk","time":1791315723125,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} @@ -19,7 +19,7 @@ {"type":"step/start","data":{"turn":1,"step":2}} {"type":"system/message","data":{"turn":1,"step":2,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:6}}"}},"sourceEventSeqs":[7],"surfaceOp":{"op":"replace","startSeq":7,"endSeq":7}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["run_code"]},"reason":"change","startsSeries":true}} -{"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"run_code"},{"type":"tool-removal","toolName":"bash"},{"type":"tool-removal","toolName":"create_goal"},{"type":"tool-removal","toolName":"edit"},{"type":"tool-removal","toolName":"exit_plan_mode"},{"type":"tool-removal","toolName":"get_goal"},{"type":"tool-removal","toolName":"glob"},{"type":"tool-removal","toolName":"grep"},{"type":"tool-removal","toolName":"interrupt_agent"},{"type":"tool-removal","toolName":"job_kill"},{"type":"tool-removal","toolName":"job_list"},{"type":"tool-removal","toolName":"job_output"},{"type":"tool-removal","toolName":"list_agents"},{"type":"tool-removal","toolName":"list_subagent_models"},{"type":"tool-removal","toolName":"read"},{"type":"tool-removal","toolName":"read_image"},{"type":"tool-removal","toolName":"send_message"},{"type":"tool-removal","toolName":"skill"},{"type":"tool-removal","toolName":"subagent"},{"type":"tool-removal","toolName":"subagent_fork"},{"type":"tool-removal","toolName":"todo_write"},{"type":"tool-removal","toolName":"update_goal"},{"type":"tool-removal","toolName":"web_fetch"},{"type":"tool-removal","toolName":"web_search"},{"type":"tool-removal","toolName":"workflow"},{"type":"tool-removal","toolName":"working_directory"},{"type":"tool-removal","toolName":"write"}],"role":"developer","id":"{{message:7}}"},"headerSeq":19},"surfaceOp":"append"} +{"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"run_code"},{"type":"tool-removal","toolName":"bash"},{"type":"tool-removal","toolName":"create_goal"},{"type":"tool-removal","toolName":"edit"},{"type":"tool-removal","toolName":"exit_plan_mode"},{"type":"tool-removal","toolName":"get_goal"},{"type":"tool-removal","toolName":"glob"},{"type":"tool-removal","toolName":"grep"},{"type":"tool-removal","toolName":"interrupt_agent"},{"type":"tool-removal","toolName":"job_kill"},{"type":"tool-removal","toolName":"job_list"},{"type":"tool-removal","toolName":"job_output"},{"type":"tool-removal","toolName":"list_agents"},{"type":"tool-removal","toolName":"read"},{"type":"tool-removal","toolName":"read_image"},{"type":"tool-removal","toolName":"send_message"},{"type":"tool-removal","toolName":"skill"},{"type":"tool-removal","toolName":"subagent"},{"type":"tool-removal","toolName":"subagent_fork"},{"type":"tool-removal","toolName":"todo_write"},{"type":"tool-removal","toolName":"update_goal"},{"type":"tool-removal","toolName":"web_fetch"},{"type":"tool-removal","toolName":"web_search"},{"type":"tool-removal","toolName":"workflow"},{"type":"tool-removal","toolName":"working_directory"},{"type":"tool-removal","toolName":"write"}],"role":"developer","id":"{{message:7}}"},"headerSeq":19},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"denied-code","name":"run_code","arguments":"{\"code\":\"try { await tools.bash({command: \\\"echo AUTO_DENIED\\\"}) } catch (error) { return error.message }\",\"description\":\"Catch one denied inner call\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"stream":[{"type":"chunk","time":1791315723151,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791315723151,"index":0,"dt":[],"id":"denied-code","name":"run_code","args":["{\"code\":\"try { await tools.bash({command: \\\"echo AUTO_DENIED\\\"}) } catch (error) { return error.message }\",\"description\":\"Catch one denied inner call\"}"]},{"type":"chunk","time":1791315723151,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"denied-code","name":"run_code","arguments":"{\"code\":\"try { await tools.bash({command: \\\"echo AUTO_DENIED\\\"}) } catch (error) { return error.message }\",\"description\":\"Catch one denied inner call\"}"}}},{"type":"chunk","time":1791315723151,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"denied-code","name":"run_code","arguments":"{\"code\":\"try { await tools.bash({command: \\\"echo AUTO_DENIED\\\"}) } catch (error) { return error.message }\",\"description\":\"Catch one denied inner call\"}"}} {"type":"tool/ptc-dispatch-start","data":{"rootCallId":"denied-code","parentCallId":"denied-code","subCallId":"denied-code:ptc:1","name":"bash","arguments":{"command":"echo AUTO_DENIED"}}} diff --git a/snapshots/sdk/tool-error-details/system-prompt.expected.md b/snapshots/sdk/tool-error-details/system-prompt.expected.md index 49a6727bc7..c7bcdec744 100644 --- a/snapshots/sdk/tool-error-details/system-prompt.expected.md +++ b/snapshots/sdk/tool-error-details/system-prompt.expected.md @@ -173,13 +173,6 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ - list_subagent_models: { - /** Registered LLM provider id. */ - provider?: string; - /** Exact model id; requires provider. */ - model?: string; - } & Record; /** Read a UTF-8 text file and return line-numbered content. */ read: { /** Path to read, resolved by the filesystem backend. */ @@ -206,7 +199,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -214,12 +207,6 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** Child LLM provider id. */ - provider?: string; - /** Model id within that provider. */ - model?: string; - /** Model-specific reasoning effort id. */ - reasoning_effort?: string; } & Record; /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent_fork: { @@ -453,7 +440,6 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; - list_subagent_models: string; read: { /** Canonical absolute path in the filesystem execution world. */ path: string; diff --git a/snapshots/sdk/tool-error-details/tool-schemas.expected.json b/snapshots/sdk/tool-error-details/tool-schemas.expected.json index 628380a1d5..642da62fb7 100644 --- a/snapshots/sdk/tool-error-details/tool-schemas.expected.json +++ b/snapshots/sdk/tool-error-details/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/advanced-toolchain/session.v4.jsonl b/snapshots/session/advanced-toolchain/session.v4.jsonl index b9591d4802..c0f1a22b8d 100644 --- a/snapshots/session/advanced-toolchain/session.v4.jsonl +++ b/snapshots/session/advanced-toolchain/session.v4.jsonl @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Run this advanced flow exactly once: list runtime inspect providers; discover providers through run_code when available; delegate once to a direct spawn child; run one workflow that delegates to another spawn child; list providers again; then reply with exactly ADVANCED_HEADLESS_OK."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["bash","cordis_inspect_list","cordis_inspect_query","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["bash","cordis_inspect_list","cordis_inspect_query","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Run this advanced flow exactly","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-inspect-before","name":"cordis_inspect_list","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1791315715021,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791315715022,"index":0,"dt":[],"id":"advanced-inspect-before","name":"cordis_inspect_list","args":["{}"]},{"type":"chunk","time":1791315715022,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-inspect-before","name":"cordis_inspect_list","arguments":"{}"}}},{"type":"chunk","time":1791315715022,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1791315715022,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} @@ -19,7 +19,7 @@ {"type":"step/start","data":{"turn":1,"step":2}} {"type":"system/message","data":{"turn":1,"step":2,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:6}}"}},"sourceEventSeqs":[7],"surfaceOp":{"op":"replace","startSeq":7,"endSeq":7}} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["run_code"]},"reason":"change","startsSeries":true}} -{"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"run_code"},{"type":"tool-removal","toolName":"bash"},{"type":"tool-removal","toolName":"cordis_inspect_list"},{"type":"tool-removal","toolName":"cordis_inspect_query"},{"type":"tool-removal","toolName":"create_goal"},{"type":"tool-removal","toolName":"edit"},{"type":"tool-removal","toolName":"exit_plan_mode"},{"type":"tool-removal","toolName":"get_goal"},{"type":"tool-removal","toolName":"glob"},{"type":"tool-removal","toolName":"grep"},{"type":"tool-removal","toolName":"interrupt_agent"},{"type":"tool-removal","toolName":"job_kill"},{"type":"tool-removal","toolName":"job_list"},{"type":"tool-removal","toolName":"job_output"},{"type":"tool-removal","toolName":"list_agents"},{"type":"tool-removal","toolName":"list_subagent_models"},{"type":"tool-removal","toolName":"read"},{"type":"tool-removal","toolName":"read_image"},{"type":"tool-removal","toolName":"send_message"},{"type":"tool-removal","toolName":"skill"},{"type":"tool-removal","toolName":"subagent"},{"type":"tool-removal","toolName":"subagent_fork"},{"type":"tool-removal","toolName":"todo_write"},{"type":"tool-removal","toolName":"update_goal"},{"type":"tool-removal","toolName":"web_fetch"},{"type":"tool-removal","toolName":"web_search"},{"type":"tool-removal","toolName":"workflow"},{"type":"tool-removal","toolName":"working_directory"},{"type":"tool-removal","toolName":"write"}],"role":"developer","id":"{{message:7}}"},"headerSeq":19},"surfaceOp":"append"} +{"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"run_code"},{"type":"tool-removal","toolName":"bash"},{"type":"tool-removal","toolName":"cordis_inspect_list"},{"type":"tool-removal","toolName":"cordis_inspect_query"},{"type":"tool-removal","toolName":"create_goal"},{"type":"tool-removal","toolName":"edit"},{"type":"tool-removal","toolName":"exit_plan_mode"},{"type":"tool-removal","toolName":"get_goal"},{"type":"tool-removal","toolName":"glob"},{"type":"tool-removal","toolName":"grep"},{"type":"tool-removal","toolName":"interrupt_agent"},{"type":"tool-removal","toolName":"job_kill"},{"type":"tool-removal","toolName":"job_list"},{"type":"tool-removal","toolName":"job_output"},{"type":"tool-removal","toolName":"list_agents"},{"type":"tool-removal","toolName":"read"},{"type":"tool-removal","toolName":"read_image"},{"type":"tool-removal","toolName":"send_message"},{"type":"tool-removal","toolName":"skill"},{"type":"tool-removal","toolName":"subagent"},{"type":"tool-removal","toolName":"subagent_fork"},{"type":"tool-removal","toolName":"todo_write"},{"type":"tool-removal","toolName":"update_goal"},{"type":"tool-removal","toolName":"web_fetch"},{"type":"tool-removal","toolName":"web_search"},{"type":"tool-removal","toolName":"workflow"},{"type":"tool-removal","toolName":"working_directory"},{"type":"tool-removal","toolName":"write"}],"role":"developer","id":"{{message:7}}"},"headerSeq":19},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"const inspected = await tools.cordis_inspect_list({}); return inspected.providers.map(provider => provider.id);\",\"description\":\"Discover runtime APIs through PTC\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1791315715055,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791315715055,"index":0,"dt":[],"id":"advanced-code","name":"run_code","args":["{\"code\":\"const inspected = await tools.cordis_inspect_list({}); return inspected.providers.map(provider => provider.id);\",\"description\":\"Discover runtime APIs through PTC\"}"]},{"type":"chunk","time":1791315715055,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-code","name":"run_code","arguments":"{\"code\":\"const inspected = await tools.cordis_inspect_list({}); return inspected.providers.map(provider => provider.id);\",\"description\":\"Discover runtime APIs through PTC\"}"}}},{"type":"chunk","time":1791315715055,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1791315715055,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"advanced-code","name":"run_code","arguments":"{\"code\":\"const inspected = await tools.cordis_inspect_list({}); return inspected.providers.map(provider => provider.id);\",\"description\":\"Discover runtime APIs through PTC\"}"}} {"type":"tool/ptc-dispatch-start","data":{"rootCallId":"advanced-code","parentCallId":"advanced-code","subCallId":"advanced-code:ptc:1","name":"cordis_inspect_list","arguments":{}}} @@ -28,8 +28,8 @@ {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} {"type":"system/message","data":{"turn":1,"step":3,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:10}}"}},"sourceEventSeqs":[18],"surfaceOp":{"op":"replace","startSeq":18,"endSeq":18}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["bash","cordis_inspect_list","cordis_inspect_query","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change","startsSeries":true}} -{"type":"developer/message","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"bash"},{"type":"tool-addition","toolName":"cordis_inspect_list"},{"type":"tool-addition","toolName":"cordis_inspect_query"},{"type":"tool-addition","toolName":"create_goal"},{"type":"tool-addition","toolName":"edit"},{"type":"tool-addition","toolName":"exit_plan_mode"},{"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":"list_subagent_models"},{"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":"web_fetch"},{"type":"tool-addition","toolName":"web_search"},{"type":"tool-addition","toolName":"workflow"},{"type":"tool-addition","toolName":"working_directory"},{"type":"tool-addition","toolName":"write"},{"type":"tool-removal","toolName":"run_code"}],"role":"developer","id":"{{message:11}}"},"headerSeq":29},"surfaceOp":"append"} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":["bash","cordis_inspect_list","cordis_inspect_query","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change","startsSeries":true}} +{"type":"developer/message","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"bash"},{"type":"tool-addition","toolName":"cordis_inspect_list"},{"type":"tool-addition","toolName":"cordis_inspect_query"},{"type":"tool-addition","toolName":"create_goal"},{"type":"tool-addition","toolName":"edit"},{"type":"tool-addition","toolName":"exit_plan_mode"},{"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":"web_fetch"},{"type":"tool-addition","toolName":"web_search"},{"type":"tool-addition","toolName":"workflow"},{"type":"tool-addition","toolName":"working_directory"},{"type":"tool-addition","toolName":"write"},{"type":"tool-removal","toolName":"run_code"}],"role":"developer","id":"{{message:11}}"},"headerSeq":29},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:12}}"},"usage":{"inputTokens":3,"outputTokens":3},"stream":[{"type":"chunk","time":1791315715200,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791315715200,"index":0,"dt":[],"id":"advanced-direct-child","name":"subagent","args":["{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"]},{"type":"chunk","time":1791315715200,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}}},{"type":"chunk","time":1791315715200,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}},{"type":"chunk","time":1791315715200,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":3,"callId":"advanced-direct-child","name":"subagent","arguments":"{\"description\":\"Check direct child\",\"prompt\":\"Reply with exactly DIRECT_CHILD_OK and nothing else.\",\"run_in_background\":false}"}} {"type":"subagent/catalog","data":{"version":0,"childId":"{{session:2}}","childCreatedAt":1791315715221,"mode":"continuable","label":"Check direct child"}} diff --git a/snapshots/session/advanced-toolchain/system-prompt.expected.md b/snapshots/session/advanced-toolchain/system-prompt.expected.md index 2cd6250c0f..c97a5b8fcb 100644 --- a/snapshots/session/advanced-toolchain/system-prompt.expected.md +++ b/snapshots/session/advanced-toolchain/system-prompt.expected.md @@ -186,13 +186,6 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ - list_subagent_models: { - /** Registered LLM provider id. */ - provider?: string; - /** Exact model id; requires provider. */ - model?: string; - } & Record; /** Read a UTF-8 text file and return line-numbered content. */ read: { /** Path to read, resolved by the filesystem backend. */ @@ -219,7 +212,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -227,12 +220,6 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** Child LLM provider id. */ - provider?: string; - /** Model id within that provider. */ - model?: string; - /** Model-specific reasoning effort id. */ - reasoning_effort?: string; } & Record; /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent_fork: { @@ -468,7 +455,6 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; - list_subagent_models: string; read: { /** Canonical absolute path in the filesystem execution world. */ path: string; diff --git a/snapshots/session/advanced-toolchain/tool-schemas.expected.json b/snapshots/session/advanced-toolchain/tool-schemas.expected.json index 89dd15e503..14756944ff 100644 --- a/snapshots/session/advanced-toolchain/tool-schemas.expected.json +++ b/snapshots/session/advanced-toolchain/tool-schemas.expected.json @@ -301,23 +301,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -397,7 +380,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -412,18 +395,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ @@ -1043,23 +1014,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -1139,7 +1093,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -1154,18 +1108,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/agent-instructions/tool-schemas.expected.json b/snapshots/session/agent-instructions/tool-schemas.expected.json index ffcb7cdbcb..00e71002a7 100644 --- a/snapshots/session/agent-instructions/tool-schemas.expected.json +++ b/snapshots/session/agent-instructions/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ @@ -922,23 +893,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -1018,7 +972,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -1033,18 +987,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/background-confinement-failure/tool-schemas.expected.json b/snapshots/session/background-confinement-failure/tool-schemas.expected.json index 185fd4ef83..2b4b1f069f 100644 --- a/snapshots/session/background-confinement-failure/tool-schemas.expected.json +++ b/snapshots/session/background-confinement-failure/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/both-mode-turn/system-prompt.expected.md b/snapshots/session/both-mode-turn/system-prompt.expected.md index da16df0b27..d58e71a227 100644 --- a/snapshots/session/both-mode-turn/system-prompt.expected.md +++ b/snapshots/session/both-mode-turn/system-prompt.expected.md @@ -140,13 +140,6 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ - list_subagent_models: { - /** Registered LLM provider id. */ - provider?: string; - /** Exact model id; requires provider. */ - model?: string; - } & Record; /** Read a UTF-8 text file and return line-numbered content. */ read: { /** Path to read, resolved by the filesystem backend. */ @@ -173,7 +166,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -181,12 +174,6 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** Child LLM provider id. */ - provider?: string; - /** Model id within that provider. */ - model?: string; - /** Model-specific reasoning effort id. */ - reasoning_effort?: string; } & Record; /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent_fork: { @@ -420,7 +407,6 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; - list_subagent_models: string; read: { /** Canonical absolute path in the filesystem execution world. */ path: string; diff --git a/snapshots/session/browser-use-chrome-devtools-mcp/tool-schemas.expected.json b/snapshots/session/browser-use-chrome-devtools-mcp/tool-schemas.expected.json index f96956dd76..cd473bea70 100644 --- a/snapshots/session/browser-use-chrome-devtools-mcp/tool-schemas.expected.json +++ b/snapshots/session/browser-use-chrome-devtools-mcp/tool-schemas.expected.json @@ -300,23 +300,6 @@ ] } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "mcp__chrome-devtools-mcp__take_screenshot", "description": "Take a screenshot of the page or element.", @@ -463,7 +446,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -478,18 +461,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/browser-use-playwright-mcp/tool-schemas.expected.json b/snapshots/session/browser-use-playwright-mcp/tool-schemas.expected.json index 2fc2120472..2d24d7c17d 100644 --- a/snapshots/session/browser-use-playwright-mcp/tool-schemas.expected.json +++ b/snapshots/session/browser-use-playwright-mcp/tool-schemas.expected.json @@ -300,23 +300,6 @@ ] } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "mcp__playwright-mcp__browser_take_screenshot", "description": "Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.", @@ -465,7 +448,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -480,18 +463,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/browser-use-stagehand-native/tool-schemas.expected.json b/snapshots/session/browser-use-stagehand-native/tool-schemas.expected.json index d8f286c03c..e1acbc8206 100644 --- a/snapshots/session/browser-use-stagehand-native/tool-schemas.expected.json +++ b/snapshots/session/browser-use-stagehand-native/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -569,7 +552,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -584,18 +567,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/claude-code-mods/tool-schemas.expected.json b/snapshots/session/claude-code-mods/tool-schemas.expected.json index 185fd4ef83..2b4b1f069f 100644 --- a/snapshots/session/claude-code-mods/tool-schemas.expected.json +++ b/snapshots/session/claude-code-mods/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/compaction-output-reserve/cordis.snapshot.yml b/snapshots/session/compaction-output-reserve/cordis.snapshot.yml index b9294ef273..3dce48221f 100644 --- a/snapshots/session/compaction-output-reserve/cordis.snapshot.yml +++ b/snapshots/session/compaction-output-reserve/cordis.snapshot.yml @@ -1,5 +1,5 @@ # With 1,800 tokens of headroom, a 1,000-token output reservation lowers the -# trigger from 7,470 to 6,470, between the history and its condensed replacement. +# trigger from 6,782 to 5,782, between the history and its condensed replacement. - id: agent-instructions disabled: true @@ -31,7 +31,7 @@ name: DeepSeek models: - id: deepseek-v4-flash - contextWindow: 9009 + contextWindow: 8582 defaultMaxTokens: 1000 - id: compaction-working-directory name: '../../../packages/test-support/session-snapshot/tests/fixtures/compaction-working-directory.ts' diff --git a/snapshots/session/compaction-output-reserve/cordis.yml b/snapshots/session/compaction-output-reserve/cordis.yml index e970dde73c..a8d2088083 100644 --- a/snapshots/session/compaction-output-reserve/cordis.yml +++ b/snapshots/session/compaction-output-reserve/cordis.yml @@ -7,7 +7,7 @@ config: models: - id: deepseek-v4-flash - contextWindow: 9009 + contextWindow: 8582 maxTokens: 1000 - id: compaction-basic diff --git a/snapshots/session/compaction-output-reserve/session.v4.jsonl b/snapshots/session/compaction-output-reserve/session.v4.jsonl index 51414cb663..6b7fb48868 100644 --- a/snapshots/session/compaction-output-reserve/session.v4.jsonl +++ b/snapshots/session/compaction-output-reserve/session.v4.jsonl @@ -11,7 +11,7 @@ {"type":"user/message","data":{"content":[{"type":"text","text":"Establish a durable compaction premise before continuing. Record every part of this historical evidence: the snapshot uses keyless replay; persistence uses JSONL; the assembled headless application loads its real Cordis composition; model-visible inputs remain logged; tool calls and results remain paired and ordered; output reservation reduces the message budget before proactive compaction; compaction opens with compaction/start and closes with compaction/end; a successful auxiliary summary records compaction/summary source metadata; the replacement surface shadows only an older balanced range; the checkpoint remains smaller than the history it replaces; the newest tool result remains verbatim; the next request sees that checkpoint; the final response proves the same turn continued; deterministic snapshot evidence stays separate from the live-provider smoke; and no external API key is needed. Emit one alpha marker through bash, then finish the task after proactive compaction with the exact words PROACTIVE COMPACTION COMPLETE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"/tmp\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"/tmp\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":1000},"adapterDefaults":{"maxTokens":true},"tools":"{{tools}}"},"reason":"initial"}} -{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":9009}} +{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":8582}} {"type":"session/title","data":{"title":"Establish a durable compaction premise","messageSeqs":[9],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_compaction_marker","name":"bash","arguments":"{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":24,"outputTokens":6},"stream":[{"type":"chunk","time":1791427805327,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791427805327,"index":0,"dt":[],"id":"call_compaction_marker","name":"bash","args":["{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"]},{"type":"chunk","time":1791427805327,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_compaction_marker","name":"bash","arguments":"{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"}}},{"type":"chunk","time":1791427805327,"chunk":{"type":"usage","usage":{"inputTokens":24,"outputTokens":6}}},{"type":"chunk","time":1791427805327,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_compaction_marker","name":"bash","arguments":"{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"}} diff --git a/snapshots/session/compaction-recovery/tool-schemas.expected.json b/snapshots/session/compaction-recovery/tool-schemas.expected.json index ffcb7cdbcb..00e71002a7 100644 --- a/snapshots/session/compaction-recovery/tool-schemas.expected.json +++ b/snapshots/session/compaction-recovery/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ @@ -922,23 +893,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -1018,7 +972,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -1033,18 +987,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/compaction-summary-headroom/cordis.snapshot.yml b/snapshots/session/compaction-summary-headroom/cordis.snapshot.yml index 342f2c3574..765a3f7e8a 100644 --- a/snapshots/session/compaction-summary-headroom/cordis.snapshot.yml +++ b/snapshots/session/compaction-summary-headroom/cordis.snapshot.yml @@ -1,5 +1,5 @@ # The summary output cap inherits the 1,800-token headroom. A 1,000-token -# request output reservation lowers the proactive trigger to 6,470 tokens. +# request output reservation lowers the proactive trigger to 5,782 tokens. - id: agent-instructions disabled: true @@ -30,7 +30,7 @@ name: DeepSeek models: - id: deepseek-v4-flash - contextWindow: 9009 + contextWindow: 8582 defaultMaxTokens: 1000 - id: compaction-working-directory name: '../../../packages/test-support/session-snapshot/tests/fixtures/compaction-working-directory.ts' diff --git a/snapshots/session/compaction-summary-headroom/cordis.yml b/snapshots/session/compaction-summary-headroom/cordis.yml index 1897460f05..f65c90899e 100644 --- a/snapshots/session/compaction-summary-headroom/cordis.yml +++ b/snapshots/session/compaction-summary-headroom/cordis.yml @@ -7,7 +7,7 @@ config: models: - id: deepseek-v4-flash - contextWindow: 9009 + contextWindow: 8582 maxTokens: 1000 - id: compaction-basic diff --git a/snapshots/session/compaction-summary-headroom/session.v4.jsonl b/snapshots/session/compaction-summary-headroom/session.v4.jsonl index aafe00a914..b3ba4ad911 100644 --- a/snapshots/session/compaction-summary-headroom/session.v4.jsonl +++ b/snapshots/session/compaction-summary-headroom/session.v4.jsonl @@ -11,7 +11,7 @@ {"type":"user/message","data":{"content":[{"type":"text","text":"Establish a durable compaction premise before continuing. Record every part of this historical evidence: the snapshot uses keyless replay; persistence uses JSONL; the assembled headless application loads its real Cordis composition; model-visible inputs remain logged; tool calls and results remain paired and ordered; output reservation reduces the message budget before proactive compaction; compaction opens with compaction/start and closes with compaction/end; a successful auxiliary summary records compaction/summary source metadata; the replacement surface shadows only an older balanced range; the checkpoint remains smaller than the history it replaces; the newest tool result remains verbatim; the next request sees that checkpoint; the final response proves the same turn continued; deterministic snapshot evidence stays separate from the live-provider smoke; and no external API key is needed. Emit one alpha marker through bash, then finish the task after proactive compaction with the exact words PROACTIVE COMPACTION COMPLETE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"/tmp\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"/tmp\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":1000},"adapterDefaults":{"maxTokens":true},"tools":"{{tools}}"},"reason":"initial"}} -{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":9009}} +{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":8582}} {"type":"session/title","data":{"title":"Establish a durable compaction premise","messageSeqs":[9],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_compaction_marker","name":"bash","arguments":"{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":24,"outputTokens":6},"stream":[{"type":"chunk","time":1791427807038,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791427807038,"index":0,"dt":[],"id":"call_compaction_marker","name":"bash","args":["{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"]},{"type":"chunk","time":1791427807038,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_compaction_marker","name":"bash","arguments":"{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"}}},{"type":"chunk","time":1791427807038,"chunk":{"type":"usage","usage":{"inputTokens":24,"outputTokens":6}}},{"type":"chunk","time":1791427807038,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_compaction_marker","name":"bash","arguments":"{\"command\":\"printf 'alpha\\n'\",\"description\":\"Emit compaction premise marker\"}"}} diff --git a/snapshots/session/computer-use-cua-driver-mcp/tool-schemas.expected.json b/snapshots/session/computer-use-cua-driver-mcp/tool-schemas.expected.json index 4a4cca52f0..4b839adb31 100644 --- a/snapshots/session/computer-use-cua-driver-mcp/tool-schemas.expected.json +++ b/snapshots/session/computer-use-cua-driver-mcp/tool-schemas.expected.json @@ -300,23 +300,6 @@ ] } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "mcp__cua-driver-mcp__disconnect", "description": "Disconnect the fixture.", @@ -442,7 +425,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -457,18 +440,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/computer-use-cua-driver-native/tool-schemas.expected.json b/snapshots/session/computer-use-cua-driver-native/tool-schemas.expected.json index 3957dbc2f8..802ed2fe0d 100644 --- a/snapshots/session/computer-use-cua-driver-native/tool-schemas.expected.json +++ b/snapshots/session/computer-use-cua-driver-native/tool-schemas.expected.json @@ -306,23 +306,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -402,7 +385,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -417,18 +400,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json b/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json index 2e5f4fc018..5d880fdde7 100644 --- a/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json +++ b/snapshots/session/cordis-inspect-jsdoc/tool-schemas.expected.json @@ -301,23 +301,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -397,7 +380,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -412,18 +395,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/deepseek-messages-system-prompt/tool-schemas.expected.json b/snapshots/session/deepseek-messages-system-prompt/tool-schemas.expected.json index 185fd4ef83..2b4b1f069f 100644 --- a/snapshots/session/deepseek-messages-system-prompt/tool-schemas.expected.json +++ b/snapshots/session/deepseek-messages-system-prompt/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/deepseek-protocol-system-prompt/tool-schemas.expected.json b/snapshots/session/deepseek-protocol-system-prompt/tool-schemas.expected.json index 185fd4ef83..2b4b1f069f 100644 --- a/snapshots/session/deepseek-protocol-system-prompt/tool-schemas.expected.json +++ b/snapshots/session/deepseek-protocol-system-prompt/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/dynamic-tool-prompt-updates/session.v4.jsonl b/snapshots/session/dynamic-tool-prompt-updates/session.v4.jsonl index 53e2ccd74b..cff0e5a329 100644 --- a/snapshots/session/dynamic-tool-prompt-updates/session.v4.jsonl +++ b/snapshots/session/dynamic-tool-prompt-updates/session.v4.jsonl @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Read task.txt, call the newly available snapshot_ping tool once, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","systemPromptUpdate":"in-history"}} {"type":"session/title","data":{"title":"Read task.txt, call the newly","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1790149644504,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790149644504,"index":0,"dt":[],"id":"call_task_read","name":"read","args":["{\"file_path\":\"task.txt\"}"]},{"type":"chunk","time":1790149644504,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}}},{"type":"chunk","time":1790149644504,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1790149644504,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} @@ -18,14 +18,14 @@ {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} {"type":"system/message","data":{"turn":1,"step":2,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:6}}"}},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"snapshot_ping"}],"role":"developer","id":"{{message:7}}"},"headerSeq":19},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"stream":[{"type":"chunk","time":1790149644534,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790149644534,"index":0,"dt":[],"id":"call_dynamic_ping","name":"snapshot_ping","args":["{}"]},{"type":"chunk","time":1790149644534,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}}},{"type":"chunk","time":1790149644535,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}} {"type":"tool/result","data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"call_dynamic_ping"},"toolCallId":"call_dynamic_ping","content":[{"type":"text","text":"pong"}],"isError":false,"id":"{{message:9}}"}},"sourceEventSeqs":[22],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-removal","toolName":"snapshot_ping"}],"role":"developer","id":"{{message:10}}"}},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:11}}"},"usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10},"stream":[{"type":"chunk","time":1790149644557,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1790149644557,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1790149644557,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1790149644557,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10}}},{"type":"chunk","time":1790149644557,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} diff --git a/snapshots/session/dynamic-tool-updates/session.v4.jsonl b/snapshots/session/dynamic-tool-updates/session.v4.jsonl index 8472750d16..aee79405da 100644 --- a/snapshots/session/dynamic-tool-updates/session.v4.jsonl +++ b/snapshots/session/dynamic-tool-updates/session.v4.jsonl @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Read task.txt, call the newly available snapshot_ping tool once, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} {"type":"session/title","data":{"title":"Read task.txt, call the newly","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1790049391235,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790049391235,"index":0,"dt":[],"id":"call_task_read","name":"read","args":["{\"file_path\":\"task.txt\"}"]},{"type":"chunk","time":1790049391235,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}}},{"type":"chunk","time":1790049391235,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1790049391235,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} @@ -17,14 +17,14 @@ {"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_task_read"},"toolCallId":"call_task_read","content":[{"type":"text","text":"{{cwd}}/task.txt\nfile\n\n1: Reply with the single word DONE after reading this file.\n\n(End of file - total 1 lines)\n"}],"isError":false,"id":"{{message:5}}"},"meta":{"path":"{{cwd}}/task.txt","offset":1,"lines":[{"number":1,"text":"Reply with the single word DONE after reading this file."}],"totalLines":1}},"sourceEventSeqs":[14],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"snapshot_ping"}],"role":"developer","id":"{{message:6}}"},"headerSeq":18},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:7}}"},"stream":[{"type":"chunk","time":1790049391255,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790049391255,"index":0,"dt":[],"id":"call_dynamic_ping","name":"snapshot_ping","args":["{}"]},{"type":"chunk","time":1790049391255,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}}},{"type":"chunk","time":1790049391255,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}} {"type":"tool/result","data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"call_dynamic_ping"},"toolCallId":"call_dynamic_ping","content":[{"type":"text","text":"pong"}],"isError":false,"id":"{{message:8}}"}},"sourceEventSeqs":[21],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"high"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-removal","toolName":"snapshot_ping"}],"role":"developer","id":"{{message:9}}"}},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:10}}"},"usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10},"stream":[{"type":"chunk","time":1790049391270,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1790049391270,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1790049391270,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1790049391270,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10}}},{"type":"chunk","time":1790049391270,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} diff --git a/snapshots/session/dynamic-tool-updates/tool-schemas.expected.json b/snapshots/session/dynamic-tool-updates/tool-schemas.expected.json index 798fc8024b..87bccafb3f 100644 --- a/snapshots/session/dynamic-tool-updates/tool-schemas.expected.json +++ b/snapshots/session/dynamic-tool-updates/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ @@ -922,23 +893,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -1026,7 +980,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -1041,18 +995,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ @@ -1591,23 +1533,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -1687,7 +1612,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -1702,18 +1627,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/foreground-confinement-timeout/tool-schemas.expected.json b/snapshots/session/foreground-confinement-timeout/tool-schemas.expected.json index 185fd4ef83..2b4b1f069f 100644 --- a/snapshots/session/foreground-confinement-timeout/tool-schemas.expected.json +++ b/snapshots/session/foreground-confinement-timeout/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/fs-glob-sampling/tool-schemas.expected.json b/snapshots/session/fs-glob-sampling/tool-schemas.expected.json index 03764bcd7e..3fb3980c3b 100644 --- a/snapshots/session/fs-glob-sampling/tool-schemas.expected.json +++ b/snapshots/session/fs-glob-sampling/tool-schemas.expected.json @@ -180,23 +180,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -260,7 +243,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -275,18 +258,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/lsp-definition/tool-schemas.expected.json b/snapshots/session/lsp-definition/tool-schemas.expected.json index 2bc62b0f70..76087bb96f 100644 --- a/snapshots/session/lsp-definition/tool-schemas.expected.json +++ b/snapshots/session/lsp-definition/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "lsp", "description": "Query a language server for precise code navigation. operation is one of goToDefinition, findReferences, goToImplementation, hover. line and character are one-based UTF-16 cursor coordinates. findReferences includes the declaration.", @@ -393,7 +376,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -408,18 +391,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/mcp-resources-ptc/system-prompt.expected.md b/snapshots/session/mcp-resources-ptc/system-prompt.expected.md index b81f602015..e9b26ca33b 100644 --- a/snapshots/session/mcp-resources-ptc/system-prompt.expected.md +++ b/snapshots/session/mcp-resources-ptc/system-prompt.expected.md @@ -162,13 +162,6 @@ interface ToolArgsMap { /** Continuation cursor returned by this server. */ cursor?: string; } & Record; - /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ - list_subagent_models: { - /** Registered LLM provider id. */ - provider?: string; - /** Exact model id; requires provider. */ - model?: string; - } & Record; /** Read a UTF-8 text file and return line-numbered content. */ read: { /** Path to read, resolved by the filesystem backend. */ @@ -202,7 +195,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -210,12 +203,6 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** Child LLM provider id. */ - provider?: string; - /** Model id within that provider. */ - model?: string; - /** Model-specific reasoning effort id. */ - reasoning_effort?: string; } & Record; /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent_fork: { @@ -451,7 +438,6 @@ interface ToolOutputMap { })[]; list_mcp_resource_templates: JsonValue; list_mcp_resources: JsonValue; - list_subagent_models: string; read: { /** Canonical absolute path in the filesystem execution world. */ path: string; diff --git a/snapshots/session/mcp-resources/tool-schemas.expected.json b/snapshots/session/mcp-resources/tool-schemas.expected.json index fa72eb6521..4e5e9d557c 100644 --- a/snapshots/session/mcp-resources/tool-schemas.expected.json +++ b/snapshots/session/mcp-resources/tool-schemas.expected.json @@ -300,23 +300,6 @@ ] } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -417,7 +400,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -432,18 +415,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/multimodal-spill-ends/tool-schemas.expected.json b/snapshots/session/multimodal-spill-ends/tool-schemas.expected.json index fe1f0ce7a4..637c069b74 100644 --- a/snapshots/session/multimodal-spill-ends/tool-schemas.expected.json +++ b/snapshots/session/multimodal-spill-ends/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "mcp__fixture__inspect", "description": "Read a fixed window snapshot.", @@ -375,7 +358,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -390,18 +373,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/office-progressive-native/tool-schemas.expected.json b/snapshots/session/office-progressive-native/tool-schemas.expected.json index 36be4284f4..79d6891abc 100644 --- a/snapshots/session/office-progressive-native/tool-schemas.expected.json +++ b/snapshots/session/office-progressive-native/tool-schemas.expected.json @@ -215,23 +215,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -311,7 +294,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -326,18 +309,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/office-progressive-ptc/system-prompt.expected.md b/snapshots/session/office-progressive-ptc/system-prompt.expected.md index e9119ddc55..9d427282d8 100644 --- a/snapshots/session/office-progressive-ptc/system-prompt.expected.md +++ b/snapshots/session/office-progressive-ptc/system-prompt.expected.md @@ -119,13 +119,6 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ - list_subagent_models: { - /** Registered LLM provider id. */ - provider?: string; - /** Exact model id; requires provider. */ - model?: string; - } & Record; /** Read a UTF-8 text file and return line-numbered content. */ read: { /** Path to read, resolved by the filesystem backend. */ @@ -152,7 +145,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -160,12 +153,6 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** Child LLM provider id. */ - provider?: string; - /** Model id within that provider. */ - model?: string; - /** Model-specific reasoning effort id. */ - reasoning_effort?: string; } & Record; /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent_fork: { @@ -363,7 +350,6 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; - list_subagent_models: string; read: { /** Canonical absolute path in the filesystem execution world. */ path: string; diff --git a/snapshots/session/office-skills/tool-schemas.expected.json b/snapshots/session/office-skills/tool-schemas.expected.json index 36be4284f4..79d6891abc 100644 --- a/snapshots/session/office-skills/tool-schemas.expected.json +++ b/snapshots/session/office-skills/tool-schemas.expected.json @@ -215,23 +215,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -311,7 +294,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -326,18 +309,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/persistent-pwsh-tool-turn/tool-schemas.expected.json b/snapshots/session/persistent-pwsh-tool-turn/tool-schemas.expected.json index 4195c5187a..d1b4eef213 100644 --- a/snapshots/session/persistent-pwsh-tool-turn/tool-schemas.expected.json +++ b/snapshots/session/persistent-pwsh-tool-turn/tool-schemas.expected.json @@ -135,23 +135,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "pwsh", "description": "Run commands in a persistent PowerShell shell. State, including the current directory and exported environment variables, persists across calls for this agent.", @@ -231,7 +214,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -246,18 +229,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/pi-ai-conversation-updates/session.v4.jsonl b/snapshots/session/pi-ai-conversation-updates/session.v4.jsonl index 6e26da9e41..843a75e88d 100644 --- a/snapshots/session/pi-ai-conversation-updates/session.v4.jsonl +++ b/snapshots/session/pi-ai-conversation-updates/session.v4.jsonl @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Read task.txt, call the newly available snapshot_ping tool once, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"moonshotai","model":"kimi-k3"},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"moonshotai","model":"kimi-k3"},"tools":["bash","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"moonshotai","model":"kimi-k3","contextWindow":1048576,"systemPromptUpdate":"in-history"}} {"type":"session/title","data":{"title":"Read task.txt, call the newly","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}],"source":{"kind":"model","provider":"moonshotai","model":"kimi-k3"},"id":"{{message:4}}"},"usage":{"inputTokens":10,"outputTokens":5},"stream":[{"type":"chunk","time":1790149644504,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790149644504,"index":0,"dt":[],"id":"call_task_read","name":"read","args":["{\"file_path\":\"task.txt\"}"]},{"type":"chunk","time":1790149644504,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_task_read","name":"read","arguments":"{\"file_path\":\"task.txt\"}"}}},{"type":"chunk","time":1790149644504,"chunk":{"type":"usage","usage":{"inputTokens":10,"outputTokens":5}}},{"type":"chunk","time":1790149644504,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} @@ -18,14 +18,14 @@ {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} {"type":"system/message","data":{"turn":1,"step":2,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:6}}"}},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"moonshotai","model":"kimi-k3"},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} +{"type":"request/header","data":{"header":{"config":{"provider":"moonshotai","model":"kimi-k3"},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","read","read_image","send_message","skill","snapshot_ping","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"snapshot_ping"}],"role":"developer","id":"{{message:7}}"},"headerSeq":19},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}],"source":{"kind":"model","provider":"moonshotai","model":"kimi-k3"},"id":"{{message:8}}"},"stream":[{"type":"chunk","time":1790149644534,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1790149644534,"index":0,"dt":[],"id":"call_dynamic_ping","name":"snapshot_ping","args":["{}"]},{"type":"chunk","time":1790149644534,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}}},{"type":"chunk","time":1790149644535,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_dynamic_ping","name":"snapshot_ping","arguments":"{}"}} {"type":"tool/result","data":{"turn":1,"step":2,"message":{"role":"tool","source":{"kind":"tool","callId":"call_dynamic_ping"},"toolCallId":"call_dynamic_ping","content":[{"type":"text","text":"pong"}],"isError":false,"id":"{{message:9}}"}},"sourceEventSeqs":[22],"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":2}} {"type":"step/start","data":{"turn":1,"step":3}} -{"type":"request/header","data":{"header":{"config":{"provider":"moonshotai","model":"kimi-k3"},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} +{"type":"request/header","data":{"header":{"config":{"provider":"moonshotai","model":"kimi-k3"},"tools":["bash","create_goal","edit","exit_plan_mode","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","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change"}} {"type":"developer/message","data":{"turn":1,"step":3,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-removal","toolName":"snapshot_ping"}],"role":"developer","id":"{{message:10}}"}},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":3,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"moonshotai","model":"kimi-k3"},"id":"{{message:11}}"},"usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10},"stream":[{"type":"chunk","time":1790149644557,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"text-chunks","time0":1790149644557,"index":0,"dt":[],"texts":["DONE"]},{"type":"chunk","time":1790149644557,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1790149644557,"chunk":{"type":"usage","usage":{"inputTokens":12,"outputTokens":2,"cacheReadTokens":10}}},{"type":"chunk","time":1790149644557,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} {"type":"step/end","data":{"turn":1,"step":3}} diff --git a/snapshots/session/plugin-manager-mcp/session.v4.jsonl b/snapshots/session/plugin-manager-mcp/session.v4.jsonl index 0036cc1272..794d19b23f 100644 --- a/snapshots/session/plugin-manager-mcp/session.v4.jsonl +++ b/snapshots/session/plugin-manager-mcp/session.v4.jsonl @@ -9,7 +9,7 @@ {"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Use plugin_manager with action set_plugin, target include:demo, enabled true to activate the configured MCP entry. Then call its newly discovered mcp__demo__ping tool and report the result. Do not use other tools."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} {"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"max"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_subagent_models","plugin_manager","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"max"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","plugin_manager","read","read_image","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"initial"}} {"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash","contextWindow":1000000}} {"type":"session/title","data":{"title":"Use plugin_manager with action set_plugi","messageSeqs":[8],"source":{"kind":"fallback"}}} {"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user wants me to activate a plugin via plugin_manager, then call an MCP tool. Let me do the plugin_manager call first.\n\nNote: they say \"Do not use other tools\" other than plugin_manager and the mcp tool. But I need to first enable it. Let's call plugin_manager set_plugin.\n\nLet me also list plugins first? The instruction says use plugin_manager with action set_plugin, target include:demo, enabled true. Let's just do that."},{"type":"tool-call","id":"call_00_xLhr9wia3A5m7wuSRWwD0436","name":"plugin_manager","arguments":"{\"action\": \"set_plugin\", \"target\": \"include:demo\", \"enabled\": true}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"reasoning","signature":"75d99e21-1d47-4a33-bb88-340a1e84b749"},{"type":"tool-call"}]}},"id":"{{message:4}}"},"usage":{"inputTokens":7220,"outputTokens":174,"cacheReadTokens":0,"cacheWriteTokens":0,"totalTokens":7394},"stream":[{"type":"chunk","time":1789984418848,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1789984418848,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," wants"," me"," to"," activate"," a"," plugin"," via"," plugin","_manager",","," then"," call"," an"," M","CP"," tool","."," Let"," me"," do"," the"," plugin","_manager"," call"," first",".\n\n","Note",":"," they"," say"," \"","Do"," not"," use"," other"," tools","\""," other"," than"," plugin","_manager"," and"," the"," m","cp"," tool","."," But"," I"," need"," to"," first"," enable"," it","."," Let","'s"," call"," plugin","_manager"," set","_","plugin",".\n\n","Let"," me"," also"," list"," plugins"," first","?"," The"," instruction"," says"," use"," plugin","_manager"," with"," action"," set","_","plugin",","," target"," include",":","demo",","," enabled"," true","."," Let","'s"," just"," do"," that","."]},{"type":"chunk","time":1789984418849,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user wants me to activate a plugin via plugin_manager, then call an MCP tool. Let me do the plugin_manager call first.\n\nNote: they say \"Do not use other tools\" other than plugin_manager and the mcp tool. But I need to first enable it. Let's call plugin_manager set_plugin.\n\nLet me also list plugins first? The instruction says use plugin_manager with action set_plugin, target include:demo, enabled true. Let's just do that."}}},{"type":"chunk","time":1789984418849,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1789984418849,"index":1,"dt":[],"id":"call_00_xLhr9wia3A5m7wuSRWwD0436","name":"plugin_manager","args":[""]},{"type":"tool-call-chunks","time0":1789984418849,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_xLhr9wia3A5m7wuSRWwD0436","args":["{","\"","action","\"",": ","\"","set","_","plugin","\"",", ","\"","target","\"",": ","\"","include",":","demo","\"",", ","\"","en","abled","\"",": ","true","}"]},{"type":"chunk","time":1789984418849,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_xLhr9wia3A5m7wuSRWwD0436","name":"plugin_manager","arguments":"{\"action\": \"set_plugin\", \"target\": \"include:demo\", \"enabled\": true}"}}},{"type":"chunk","time":1789984418849,"chunk":{"type":"usage","usage":{"inputTokens":7220,"outputTokens":174,"cacheReadTokens":0,"cacheWriteTokens":0,"totalTokens":7394}}},{"type":"chunk","time":1789984418849,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"reasoning","signature":"75d99e21-1d47-4a33-bb88-340a1e84b749"},{"type":"tool-call"}]}}}]},"surfaceOp":"append"} @@ -18,7 +18,7 @@ {"type":"step/end","data":{"turn":1,"step":1}} {"type":"step/start","data":{"turn":1,"step":2}} {"type":"system/message","data":{"turn":1,"step":2,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:6}}"}},"sourceEventSeqs":[7],"surfaceOp":{"op":"replace","startSeq":7,"endSeq":7}} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"max"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_mcp_resource_templates","list_mcp_resources","list_subagent_models","mcp__demo__ping","plugin_manager","read","read_image","read_mcp_resource","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change","startsSeries":true}} +{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash","maxTokens":256000,"reasoningEffort":"max"},"adapterDefaults":{"reasoningEffort":true,"maxTokens":true},"tools":["bash","create_goal","edit","exit_plan_mode","get_goal","glob","grep","interrupt_agent","job_kill","job_list","job_output","list_agents","list_mcp_resource_templates","list_mcp_resources","mcp__demo__ping","plugin_manager","read","read_image","read_mcp_resource","send_message","skill","subagent","subagent_fork","todo_write","update_goal","web_fetch","web_search","workflow","working_directory","write"]},"reason":"change","startsSeries":true}} {"type":"developer/message","data":{"turn":1,"step":2,"message":{"source":{"kind":"tool-registry"},"content":[{"type":"tool-addition","toolName":"list_mcp_resource_templates"},{"type":"tool-addition","toolName":"list_mcp_resources"},{"type":"tool-addition","toolName":"mcp__demo__ping"},{"type":"tool-addition","toolName":"read_mcp_resource"}],"role":"developer","id":"{{message:7}}"},"headerSeq":19},"surfaceOp":"append"} {"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The plugin was enabled. Now call mcp__demo__ping.\n\nWait — the tool was already listed in my tools before enabling. Let's call it anyway."},{"type":"tool-call","id":"call_00_miuKfLCTz5Cb38gdp8kK5186","name":"mcp__demo__ping","arguments":"{}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash","replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"reasoning","signature":"b1ec1efe-4be6-4836-a10c-e5d6bfdce520"},{"type":"tool-call"}]}},"id":"{{message:8}}"},"usage":{"inputTokens":7035,"outputTokens":59,"cacheReadTokens":768,"cacheWriteTokens":0,"totalTokens":7862},"stream":[{"type":"chunk","time":1789984418998,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}},{"type":"reasoning-chunks","time0":1789984418998,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," plugin"," was"," enabled","."," Now"," call"," m","cp","__","demo","__","ping",".\n\n","Wait"," —"," the"," tool"," was"," already"," listed"," in"," my"," tools"," before"," enabling","."," Let","'s"," call"," it"," anyway","."]},{"type":"chunk","time":1789984418998,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The plugin was enabled. Now call mcp__demo__ping.\n\nWait — the tool was already listed in my tools before enabling. Let's call it anyway."}}},{"type":"chunk","time":1789984418998,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1789984418998,"index":1,"dt":[],"id":"call_00_miuKfLCTz5Cb38gdp8kK5186","name":"mcp__demo__ping","args":[""]},{"type":"tool-call-chunks","time0":1789984418998,"index":1,"dt":[],"id":"call_00_miuKfLCTz5Cb38gdp8kK5186","args":["{}"]},{"type":"chunk","time":1789984418998,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_miuKfLCTz5Cb38gdp8kK5186","name":"mcp__demo__ping","arguments":"{}"}}},{"type":"chunk","time":1789984418998,"chunk":{"type":"usage","usage":{"inputTokens":7035,"outputTokens":59,"cacheReadTokens":768,"cacheWriteTokens":0,"totalTokens":7862}}},{"type":"chunk","time":1789984418998,"chunk":{"type":"finish","reason":{"kind":"tool-calls"},"replayState":{"response":{"kind":"deepseek-messages","version":1,"model":"deepseek-v4-flash"},"blocks":[{"type":"reasoning","signature":"b1ec1efe-4be6-4836-a10c-e5d6bfdce520"},{"type":"tool-call"}]}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":1,"step":2,"callId":"call_00_miuKfLCTz5Cb38gdp8kK5186","name":"mcp__demo__ping","arguments":"{}"}} diff --git a/snapshots/session/plugin-manager-mcp/tool-schemas.expected.json b/snapshots/session/plugin-manager-mcp/tool-schemas.expected.json index ca83d406e0..f44f52640a 100644 --- a/snapshots/session/plugin-manager-mcp/tool-schemas.expected.json +++ b/snapshots/session/plugin-manager-mcp/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "plugin_manager", "description": "List plugins or bundles in the current profile, enable or disable them, install a bundle, or remove an installed bundle. Every action requires danger-full-access permission or approval for this call. Approval does not change the session permission mode. Changes affect every session in this profile. List first to obtain exact identifiers. Package installation can execute allowed build scripts. Live profiles apply changes immediately; startup profiles require restart. Incompatible DSH peer dependencies block installation and activation. Version exemptions risk crashes and data loss: warn the user and obtain explicit permission for the exact plugin and runtime versions before granting one.", @@ -417,7 +400,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -432,18 +415,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ @@ -1023,23 +994,6 @@ ] } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "mcp__demo__ping", "description": "Replies pong.", @@ -1210,7 +1164,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -1225,18 +1179,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/plugin-manager/tool-schemas.expected.json b/snapshots/session/plugin-manager/tool-schemas.expected.json index 62bdec5dc1..4596dae7a1 100644 --- a/snapshots/session/plugin-manager/tool-schemas.expected.json +++ b/snapshots/session/plugin-manager/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "plugin_manager", "description": "List plugins or bundles in the current profile, enable or disable them, install a bundle, or remove an installed bundle. Every action requires danger-full-access permission or approval for this call. Approval does not change the session permission mode. Changes affect every session in this profile. List first to obtain exact identifiers. Package installation can execute allowed build scripts. Live profiles apply changes immediately; startup profiles require restart. Incompatible DSH peer dependencies block installation and activation. Version exemptions risk crashes and data loss: warn the user and obtain explicit permission for the exact plugin and runtime versions before granting one.", @@ -417,7 +400,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -432,18 +415,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/product-subagent-both/tool-schemas.expected.json b/snapshots/session/product-subagent-both/tool-schemas.expected.json index 08ca2b1a53..2b36308f92 100644 --- a/snapshots/session/product-subagent-both/tool-schemas.expected.json +++ b/snapshots/session/product-subagent-both/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/product-subagent-codex/tool-schemas.expected.json b/snapshots/session/product-subagent-codex/tool-schemas.expected.json index 49f6480eb1..d9f3cfa9d0 100644 --- a/snapshots/session/product-subagent-codex/tool-schemas.expected.json +++ b/snapshots/session/product-subagent-codex/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json b/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json index 34004323e2..11f07197a2 100644 --- a/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json +++ b/snapshots/session/product-subagent-result-diagnostic/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/provider-cwd/tool-schemas.expected.json b/snapshots/session/provider-cwd/tool-schemas.expected.json index 185fd4ef83..2b4b1f069f 100644 --- a/snapshots/session/provider-cwd/tool-schemas.expected.json +++ b/snapshots/session/provider-cwd/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/ptc-node-read-only/system-prompt.expected.md b/snapshots/session/ptc-node-read-only/system-prompt.expected.md index 1e964bdb4f..2801645046 100644 --- a/snapshots/session/ptc-node-read-only/system-prompt.expected.md +++ b/snapshots/session/ptc-node-read-only/system-prompt.expected.md @@ -140,13 +140,6 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ - list_subagent_models: { - /** Registered LLM provider id. */ - provider?: string; - /** Exact model id; requires provider. */ - model?: string; - } & Record; /** Read a UTF-8 text file and return line-numbered content. */ read: { /** Path to read, resolved by the filesystem backend. */ @@ -173,7 +166,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -181,12 +174,6 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** Child LLM provider id. */ - provider?: string; - /** Model id within that provider. */ - model?: string; - /** Model-specific reasoning effort id. */ - reasoning_effort?: string; } & Record; /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent_fork: { @@ -424,7 +411,6 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; - list_subagent_models: string; read: { /** Canonical absolute path in the filesystem execution world. */ path: string; diff --git a/snapshots/session/ptc-node-workspace/system-prompt.expected.md b/snapshots/session/ptc-node-workspace/system-prompt.expected.md index 1e964bdb4f..2801645046 100644 --- a/snapshots/session/ptc-node-workspace/system-prompt.expected.md +++ b/snapshots/session/ptc-node-workspace/system-prompt.expected.md @@ -140,13 +140,6 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ - list_subagent_models: { - /** Registered LLM provider id. */ - provider?: string; - /** Exact model id; requires provider. */ - model?: string; - } & Record; /** Read a UTF-8 text file and return line-numbered content. */ read: { /** Path to read, resolved by the filesystem backend. */ @@ -173,7 +166,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -181,12 +174,6 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** Child LLM provider id. */ - provider?: string; - /** Model id within that provider. */ - model?: string; - /** Model-specific reasoning effort id. */ - reasoning_effort?: string; } & Record; /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent_fork: { @@ -424,7 +411,6 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; - list_subagent_models: string; read: { /** Canonical absolute path in the filesystem execution world. */ path: string; diff --git a/snapshots/session/ptc-persistent/system-prompt.expected.md b/snapshots/session/ptc-persistent/system-prompt.expected.md index 1e964bdb4f..2801645046 100644 --- a/snapshots/session/ptc-persistent/system-prompt.expected.md +++ b/snapshots/session/ptc-persistent/system-prompt.expected.md @@ -140,13 +140,6 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ - list_subagent_models: { - /** Registered LLM provider id. */ - provider?: string; - /** Exact model id; requires provider. */ - model?: string; - } & Record; /** Read a UTF-8 text file and return line-numbered content. */ read: { /** Path to read, resolved by the filesystem backend. */ @@ -173,7 +166,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -181,12 +174,6 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** Child LLM provider id. */ - provider?: string; - /** Model id within that provider. */ - model?: string; - /** Model-specific reasoning effort id. */ - reasoning_effort?: string; } & Record; /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent_fork: { @@ -424,7 +411,6 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; - list_subagent_models: string; read: { /** Canonical absolute path in the filesystem execution world. */ path: string; diff --git a/snapshots/session/ptc-policy-change/system-prompt.expected.md b/snapshots/session/ptc-policy-change/system-prompt.expected.md index 65c682d835..19204006b3 100644 --- a/snapshots/session/ptc-policy-change/system-prompt.expected.md +++ b/snapshots/session/ptc-policy-change/system-prompt.expected.md @@ -140,13 +140,6 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ - list_subagent_models: { - /** Registered LLM provider id. */ - provider?: string; - /** Exact model id; requires provider. */ - model?: string; - } & Record; /** Read a UTF-8 text file and return line-numbered content. */ read: { /** Path to read, resolved by the filesystem backend. */ @@ -173,7 +166,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -181,12 +174,6 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** Child LLM provider id. */ - provider?: string; - /** Model id within that provider. */ - model?: string; - /** Model-specific reasoning effort id. */ - reasoning_effort?: string; } & Record; /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent_fork: { @@ -422,7 +409,6 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; - list_subagent_models: string; read: { /** Canonical absolute path in the filesystem execution world. */ path: string; diff --git a/snapshots/session/ptc-python-turn/system-prompt.expected.md b/snapshots/session/ptc-python-turn/system-prompt.expected.md index a6dfe8d41b..6e5a4c7a78 100644 --- a/snapshots/session/ptc-python-turn/system-prompt.expected.md +++ b/snapshots/session/ptc-python-turn/system-prompt.expected.md @@ -287,13 +287,6 @@ class ListAgentsOutput2(TypedDict): parent: NotRequired[str] depth: NotRequired[float] -class ListSubagentModelsArgs(TypedDict): - # Registered LLM provider id. - provider: NotRequired[str] - # Exact model id; requires provider. - model: NotRequired[str] - # Additional keys beyond those declared are allowed. - class ReadArgs(TypedDict): # Path to read, resolved by the filesystem backend. file_path: str @@ -377,12 +370,6 @@ class SubagentArgs(TypedDict): description: str # The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. prompt: str - # Child LLM provider id. - provider: NotRequired[str] - # Model id within that provider. - model: NotRequired[str] - # Model-specific reasoning effort id. - reasoning_effort: NotRequired[str] # Additional keys beyond those declared are allowed. class SubagentOutput(TypedDict): @@ -548,8 +535,6 @@ class Tools(Protocol): """Read a background job: output since the previous read for stream jobs, or the result of a finished final-output job.""" async def list_agents(self, args: ListAgentsArgs) -> list[ListAgentsOutput1 | ListAgentsOutput2]: """List subagents you started, with their ids, labels, and status. running means it is working; inactive means it is not currently working. You will be notified when a subagent finishes; there is no need to keep checking its status. Use send_message to continue the conversation.""" - async def list_subagent_models(self, args: ListSubagentModelsArgs) -> str: - """Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.""" async def read(self, args: ReadArgs) -> ReadOutput: """Read a UTF-8 text file and return line-numbered content.""" async def read_image(self, args: ReadImageArgs) -> ReadImageOutput: @@ -559,7 +544,7 @@ class Tools(Protocol): async def skill(self, args: SkillArgs) -> SkillOutput: """Load the full instructions for a skill. Call it before acting on a task that names or clearly matches a skill in the session skill catalog.""" async def subagent(self, args: SubagentArgs) -> SubagentOutput: - """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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.""" + """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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.""" async def subagent_fork(self, args: SubagentForkArgs) -> SubagentForkOutput: """Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.""" async def todo_write(self, args: TodoWriteArgs) -> TodoWriteOutput: diff --git a/snapshots/session/ptc-read-image/system-prompt.expected.md b/snapshots/session/ptc-read-image/system-prompt.expected.md index 4d6f11b10c..052b90fb17 100644 --- a/snapshots/session/ptc-read-image/system-prompt.expected.md +++ b/snapshots/session/ptc-read-image/system-prompt.expected.md @@ -140,13 +140,6 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ - list_subagent_models: { - /** Registered LLM provider id. */ - provider?: string; - /** Exact model id; requires provider. */ - model?: string; - } & Record; /** Read a UTF-8 text file and return line-numbered content. */ read: { /** Path to read, resolved by the filesystem backend. */ @@ -173,7 +166,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -181,12 +174,6 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** Child LLM provider id. */ - provider?: string; - /** Model id within that provider. */ - model?: string; - /** Model-specific reasoning effort id. */ - reasoning_effort?: string; } & Record; /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent_fork: { @@ -420,7 +407,6 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; - list_subagent_models: string; read: { /** Canonical absolute path in the filesystem execution world. */ path: string; diff --git a/snapshots/session/ptc-turn/system-prompt.expected.md b/snapshots/session/ptc-turn/system-prompt.expected.md index 1e964bdb4f..2801645046 100644 --- a/snapshots/session/ptc-turn/system-prompt.expected.md +++ b/snapshots/session/ptc-turn/system-prompt.expected.md @@ -140,13 +140,6 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ - list_subagent_models: { - /** Registered LLM provider id. */ - provider?: string; - /** Exact model id; requires provider. */ - model?: string; - } & Record; /** Read a UTF-8 text file and return line-numbered content. */ read: { /** Path to read, resolved by the filesystem backend. */ @@ -173,7 +166,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; @@ -181,12 +174,6 @@ interface ToolArgsMap { description: string; /** The complete, self-contained task for the subagent. It does not share this conversation's context, so include everything it needs. */ prompt: string; - /** Child LLM provider id. */ - provider?: string; - /** Model id within that provider. */ - model?: string; - /** Model-specific reasoning effort id. */ - reasoning_effort?: string; } & Record; /** Delegate a task to a subagent that inherits this conversation: a child agent seeded with all completed turns so far (it does not see the current in-flight turn). Use this when the subtask builds on this conversation's context — a follow-up analysis, a review, a continuation — without consuming this conversation's context for the work itself. You receive its result, not its intermediate steps. This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. */ subagent_fork: { @@ -424,7 +411,6 @@ interface ToolOutputMap { parent?: string; depth?: number; })[]; - list_subagent_models: string; read: { /** Canonical absolute path in the filesystem execution world. */ path: string; diff --git a/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json b/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json index 9127b4826a..95ffc0255b 100644 --- a/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json +++ b/snapshots/session/pty-tools-sandbox-backend/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/pwsh-tool-turn/tool-schemas.expected.json b/snapshots/session/pwsh-tool-turn/tool-schemas.expected.json index c35e8cec15..d1a1729417 100644 --- a/snapshots/session/pwsh-tool-turn/tool-schemas.expected.json +++ b/snapshots/session/pwsh-tool-turn/tool-schemas.expected.json @@ -187,23 +187,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "pwsh", "description": "Execute a PowerShell command (`pwsh -Command`) and return its stdout/stderr. Each call runs in a fresh pwsh process; pass `workdir` instead of using `cd`. Paths use native Windows form (`C:\\...`); read environment variables with `$env:NAME`. Managed `$env:DSH_*` variables expose current harness environment facts. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. On Windows a force-killed command settles as `[exit code: 1]` without a signal marker — treat it as an interruption, not a command failure. Provide `description` before `command` in the arguments. Before any delete or move, verify that the resolved absolute target path is the intended one; never run it against a computed path you have not checked. Do not assign to automatic variables such as `$HOME`; variable names are case-insensitive, so `$home` is the same read-only variable. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under mode]`, a policy denial: do not retry another way. Under the Windows sandbox, read-only pwsh runs in PowerShell ConstrainedLanguage mode, while workspace-write stays in FullLanguage unless host policy says otherwise. In read-only, prefer cmdlets and core types (`[string]`, `[datetime]`, `[regex]`, `[guid]`); .NET static calls (`[System.IO.*]::`, `[math]::`), `Add-Type`, COM objects, and reflection fail with \"only core types\" errors. `-f` formatting, property access, and core cmdlets work. In both confined modes, programs cannot open named pipes, so a command that captures another program's output through piped stdio (Node.js `child_process.spawn`/`exec` with the default `stdio: 'pipe'`) fails with EPERM, while `stdio: 'inherit'` and `stdio: 'ignore'` spawns work and PowerShell's own pipelines are unaffected. That EPERM is the documented boundary: do not retry the command another way — escalate the exact command once or restructure it to avoid capturing output.", @@ -312,7 +295,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -327,18 +310,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/ralph-loop/tool-schemas.1.expected.json b/snapshots/session/ralph-loop/tool-schemas.1.expected.json index d72f9a2ae8..02ebdc4baf 100644 --- a/snapshots/session/ralph-loop/tool-schemas.1.expected.json +++ b/snapshots/session/ralph-loop/tool-schemas.1.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", @@ -419,7 +402,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -434,18 +417,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/ralph-loop/tool-schemas.2.expected.json b/snapshots/session/ralph-loop/tool-schemas.2.expected.json index d72f9a2ae8..02ebdc4baf 100644 --- a/snapshots/session/ralph-loop/tool-schemas.2.expected.json +++ b/snapshots/session/ralph-loop/tool-schemas.2.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", @@ -419,7 +402,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -434,18 +417,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/ralph-loop/tool-schemas.expected.json b/snapshots/session/ralph-loop/tool-schemas.expected.json index 9fba724dcc..deabaf4a0d 100644 --- a/snapshots/session/ralph-loop/tool-schemas.expected.json +++ b/snapshots/session/ralph-loop/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "ralph", "description": "Run a foreground fresh-agent Ralph loop toward one immutable objective. Use only when the direct human explicitly asks for Ralph or fresh-agent iteration. Each round opens a new child with no parent conversation or prior child session; the shared workspace is long-term memory, and only a bounded structured report crosses rounds. The call returns when a worker reports completion or a concrete blocker, or at the round limit. Ordinary long-running same-session work belongs to goal tools.", @@ -376,7 +359,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -391,18 +374,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/session-query-spill/tool-schemas.expected.json b/snapshots/session/session-query-spill/tool-schemas.expected.json index 1c778ec2c9..9d66381297 100644 --- a/snapshots/session/session-query-spill/tool-schemas.expected.json +++ b/snapshots/session/session-query-spill/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -560,7 +543,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -575,18 +558,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json b/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json index 48a1ac30ce..46abe32771 100644 --- a/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json +++ b/snapshots/session/subagent-acp-diagnostic/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json b/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json index b4fa90e046..b8dc7c20a8 100644 --- a/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json +++ b/snapshots/session/subagent-child-question-rejection/tool-schemas.expected.json @@ -323,23 +323,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -419,7 +402,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -434,18 +417,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/subagent-model-discovery-errors/runtime.cordis.yml b/snapshots/session/subagent-model-discovery-errors/runtime.cordis.yml deleted file mode 100644 index 3b8eed677f..0000000000 --- a/snapshots/session/subagent-model-discovery-errors/runtime.cordis.yml +++ /dev/null @@ -1,3 +0,0 @@ -- insert: - - id: discovery-fixture - name: '../../../packages/subagent/tool-subagent/tests/fixtures/model-discovery.mjs' diff --git a/snapshots/session/subagent-model-discovery-errors/session.v4.jsonl b/snapshots/session/subagent-model-discovery-errors/session.v4.jsonl deleted file mode 100644 index 1a781e4739..0000000000 --- a/snapshots/session/subagent-model-discovery-errors/session.v4.jsonl +++ /dev/null @@ -1,23 +0,0 @@ -{"type":"session","version":4,"id":"{{session:1}}","createdAt":1783352044766,"cwd":"{{cwd}}","isSeeded":false,"delegationDepth":0} -{"type":"permission/preset","data":{"preset":"danger-full-access"}} -{"type":"sandbox/mode","data":{"mode":"danger-full-access"}} -{"type":"approval/policy","data":{"policy":"never"}} -{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"inserted":[{"content":[{"type":"text","text":"List the models available for subagents from discovery-fixture, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"}]}} -{"type":"turn/start","data":{"turn":1}} -{"type":"agent/inbox/spliced","data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}} -{"type":"subagent/model-selection-policy","data":{"allowedModels":[{"provider":"discovery-fixture","model":"removed"},{"provider":"discovery-fixture","model":"healthy"}]}} -{"type":"step/start","data":{"turn":1,"step":1}} -{"type":"system/message","data":{"turn":1,"step":1,"message":{"role":"system","content":[{"type":"text","text":"{{system}}"}],"source":{"kind":"system-prompt"},"id":"{{message:2}}"}},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"List the models available for subagents from discovery-fixture, then reply DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:1}}"},"surfaceOp":"append"} -{"type":"user/message","data":{"content":[{"type":"text","text":"Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}],"source":{"kind":"runtime-context","form":"snapshot","sections":[{"name":"working-directory:current","text":"Current working directory: \"{{cwd}}\"."},{"name":"sandbox:policy","text":"Current DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations."},{"name":"approval:policy","text":"Approval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`)."}]},"role":"user","id":"{{message:3}}"},"surfaceOp":"append"} -{"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"initial"}} -{"type":"request/context","data":{"provider":"deepseek-official","model":"deepseek-v4-flash"}} -{"type":"session/title","data":{"title":"List the models available for","messageSeqs":[9],"source":{"kind":"fallback"}}} -{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_discovery","name":"list_subagent_models","arguments":"{\"provider\":\"discovery-fixture\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:4}}"},"usage":{"inputTokens":100,"outputTokens":10},"stream":[{"type":"chunk","time":1791788384782,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"chunk","time":1791788384782,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_discovery","name":"list_subagent_models","arguments":"{\"provider\":\"discovery-fixture\"}"}}},{"type":"chunk","time":1791788384782,"chunk":{"type":"usage","usage":{"inputTokens":100,"outputTokens":10}}},{"type":"chunk","time":1791788384782,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} -{"type":"tool/call","data":{"turn":1,"step":1,"callId":"call_discovery","name":"list_subagent_models","arguments":"{\"provider\":\"discovery-fixture\"}"}} -{"type":"tool/result","data":{"turn":1,"step":1,"message":{"role":"tool","source":{"kind":"tool","callId":"call_discovery"},"toolCallId":"call_discovery","content":[{"type":"text","text":"discovery-fixture/healthy — Healthy"}],"isError":false,"id":"{{message:5}}"}},"sourceEventSeqs":[15],"surfaceOp":"append"} -{"type":"step/end","data":{"turn":1,"step":1}} -{"type":"step/start","data":{"turn":1,"step":2}} -{"type":"assistant/message","data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"text","text":"DONE"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:6}}"},"usage":{"inputTokens":100,"outputTokens":10},"stream":[{"type":"chunk","time":1791788384799,"chunk":{"type":"block-start","index":0,"blockType":"text"}},{"type":"chunk","time":1791788384800,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"DONE"}}},{"type":"chunk","time":1791788384800,"chunk":{"type":"usage","usage":{"inputTokens":100,"outputTokens":10}}},{"type":"chunk","time":1791788384800,"chunk":{"type":"finish","reason":{"kind":"stop"}}}]},"surfaceOp":"append"} -{"type":"step/end","data":{"turn":1,"step":2}} -{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}}} diff --git a/snapshots/session/subagent-model-discovery-errors/snapshot.yml b/snapshots/session/subagent-model-discovery-errors/snapshot.yml deleted file mode 100644 index 322ac5fd4f..0000000000 --- a/snapshots/session/subagent-model-discovery-errors/snapshot.yml +++ /dev/null @@ -1,7 +0,0 @@ -version: 1 -scenario: subagent-model-discovery-errors -profile: headless -composition: default -recording: authored -header: - class: default diff --git a/snapshots/session/subagent-no-message-tool/tool-schemas.1.expected.json b/snapshots/session/subagent-no-message-tool/tool-schemas.1.expected.json index dd16436c73..e48983205c 100644 --- a/snapshots/session/subagent-no-message-tool/tool-schemas.1.expected.json +++ b/snapshots/session/subagent-no-message-tool/tool-schemas.1.expected.json @@ -174,23 +174,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -249,7 +232,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -264,18 +247,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/subagent-structured-parent/tool-schemas.1.expected.json b/snapshots/session/subagent-structured-parent/tool-schemas.1.expected.json index 16a256b818..d217c78441 100644 --- a/snapshots/session/subagent-structured-parent/tool-schemas.1.expected.json +++ b/snapshots/session/subagent-structured-parent/tool-schemas.1.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -371,7 +354,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -386,18 +369,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/subagent-structured-parent/tool-schemas.expected.json b/snapshots/session/subagent-structured-parent/tool-schemas.expected.json index 498673be8a..25793c1cd4 100644 --- a/snapshots/session/subagent-structured-parent/tool-schemas.expected.json +++ b/snapshots/session/subagent-structured-parent/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/subagent-tool-filter/tool-schemas.1.expected.json b/snapshots/session/subagent-tool-filter/tool-schemas.1.expected.json index 4442bcf828..45ab57c7c0 100644 --- a/snapshots/session/subagent-tool-filter/tool-schemas.1.expected.json +++ b/snapshots/session/subagent-tool-filter/tool-schemas.1.expected.json @@ -174,23 +174,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -270,7 +253,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -285,18 +268,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/system-prompt-in-history/tool-schemas.expected.json b/snapshots/session/system-prompt-in-history/tool-schemas.expected.json index 185fd4ef83..2b4b1f069f 100644 --- a/snapshots/session/system-prompt-in-history/tool-schemas.expected.json +++ b/snapshots/session/system-prompt-in-history/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/team-targets/tool-schemas.expected.json b/snapshots/session/team-targets/tool-schemas.expected.json index f319d5da9f..4926edb120 100644 --- a/snapshots/session/team-targets/tool-schemas.expected.json +++ b/snapshots/session/team-targets/tool-schemas.expected.json @@ -251,23 +251,6 @@ "properties": {} } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", diff --git a/snapshots/session/text-turn/tool-schemas.expected.json b/snapshots/session/text-turn/tool-schemas.expected.json index 185fd4ef83..2b4b1f069f 100644 --- a/snapshots/session/text-turn/tool-schemas.expected.json +++ b/snapshots/session/text-turn/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/web-fetch/tool-schemas.expected.json b/snapshots/session/web-fetch/tool-schemas.expected.json index 114d460759..5ef902c18a 100644 --- a/snapshots/session/web-fetch/tool-schemas.expected.json +++ b/snapshots/session/web-fetch/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/workflow-confinement/tool-schemas.expected.json b/snapshots/session/workflow-confinement/tool-schemas.expected.json index 185fd4ef83..2b4b1f069f 100644 --- a/snapshots/session/workflow-confinement/tool-schemas.expected.json +++ b/snapshots/session/workflow-confinement/tool-schemas.expected.json @@ -260,23 +260,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -356,7 +339,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -371,18 +354,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/workspace-dependencies/tool-schemas.expected.json b/snapshots/session/workspace-dependencies/tool-schemas.expected.json index f009027b9e..4d3db178b9 100644 --- a/snapshots/session/workspace-dependencies/tool-schemas.expected.json +++ b/snapshots/session/workspace-dependencies/tool-schemas.expected.json @@ -215,23 +215,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "load_workspace_dependencies", "description": "Get absolute paths to bundled Python and library directories, plus bundled Python distribution versions. Node.js and pnpm paths are included when the payload provides them. Python includes numpy, pandas, python-docx, python-pptx, openpyxl, Pillow, lxml, and XlsxWriter. Use these libraries for Office files unless the user or workspace instructions select another environment. When Node.js and pnpm paths are returned, run pnpm with that Node executable and pnpm script path. This does not change PATH or package-manager settings.", @@ -319,7 +302,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -334,18 +317,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/snapshots/session/worktree/tool-schemas.expected.json b/snapshots/session/worktree/tool-schemas.expected.json index d95e52e13e..6641f8f0b2 100644 --- a/snapshots/session/worktree/tool-schemas.expected.json +++ b/snapshots/session/worktree/tool-schemas.expected.json @@ -277,23 +277,6 @@ } } }, - { - "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", - "parameters": { - "type": "object", - "properties": { - "provider": { - "type": "string", - "description": "Registered LLM provider id." - }, - "model": { - "type": "string", - "description": "Exact model id; requires provider." - } - } - } - }, { "name": "read", "description": "Read a UTF-8 text file and return line-numbered content.", @@ -373,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.", "parameters": { "type": "object", "properties": { @@ -388,18 +371,6 @@ "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." - }, - "provider": { - "type": "string", - "description": "Child LLM provider id." - }, - "model": { - "type": "string", - "description": "Model id within that provider." - }, - "reasoning_effort": { - "type": "string", - "description": "Model-specific reasoning effort id." } }, "required": [ diff --git a/tsconfig.base.json b/tsconfig.base.json index dfb4faf2eb..0d5ec2c48a 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -185,7 +185,6 @@ "@deepseek-ai/dsh-tools/presentation": ["./packages/core/tools/src/presentation.ts"], "@deepseek-ai/dsh-tools/types": ["./packages/core/tools/src/types.ts"], "@deepseek-ai/dsh-tool-subagent-control/list-agents": ["./packages/subagent/tool-subagent-control/src/list-agents.ts"], - "@deepseek-ai/dsh-tool-subagent/list-models": ["./packages/subagent/tool-subagent/src/list-models.ts"], "@deepseek-ai/dsh-tool-subagent/model-selection-settings": ["./packages/subagent/tool-subagent/src/model-selection-settings.ts"], "@deepseek-ai/dsh-user-approval/types": ["./packages/interaction/user-approval/src/types.ts"], "@deepseek-ai/dsh-user-questions/types": ["./packages/interaction/user-questions/src/types.ts"], From 84c3a61abfb7724af22963dab095b6e5c82af068 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Tue, 13 Oct 2026 11:16:53 +0800 Subject: [PATCH 046/369] fix(subagent): keep fork model selection configurable --- ...authorized-subagent-model-routes.i18n.yaml | 4 +- ...4-user-authorized-subagent-model-routes.md | 2 +- ...ser-authorized-subagent-model-routes.zh.md | 2 +- docs/config-catalog.md | 4 +- docs/config-catalog.zh.md | 4 +- docs/tool-catalog.i18n.yaml | 8 ++-- docs/tool-catalog.md | 4 +- docs/tool-catalog.zh.md | 4 +- .../guide.i18n.yaml | 16 -------- .../fork-model-selection-settings/guide.md | 28 -------------- .../fork-model-selection-settings/guide.zh.md | 28 -------------- .../subagent/tool-subagent/README.i18n.yaml | 16 ++++---- packages/subagent/tool-subagent/README.md | 8 ++-- packages/subagent/tool-subagent/README.zh.md | 8 ++-- packages/subagent/tool-subagent/src/index.ts | 10 +---- .../tests/model-selection.spec.ts | 37 +++++++++++++++++-- scripts/gen-tool-catalog.ts | 2 +- 17 files changed, 68 insertions(+), 117 deletions(-) delete mode 100644 docs/upgrade-guide/v0.2.1-alpha.2/fork-model-selection-settings/guide.i18n.yaml delete mode 100644 docs/upgrade-guide/v0.2.1-alpha.2/fork-model-selection-settings/guide.md delete mode 100644 docs/upgrade-guide/v0.2.1-alpha.2/fork-model-selection-settings/guide.zh.md diff --git a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml index 250e5e27cf..4561eacc4e 100644 --- a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml @@ -9,8 +9,8 @@ en: 3238dfb7db7dfdd8 zh: ec3bdd9a9da0a321 /agent-note-user-authorized-subagent-model-routes/decision: - en: e67644944167a865 - zh: dc2b28f630a0f6fa + en: 423eefc69964717c + zh: 7f73ea1c82630314 /agent-note-user-authorized-subagent-model-routes/alternatives-considered: en: e41c17226190ea8b zh: d3b1395fafe75f01 diff --git a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md index 357ef78e36..a44c5245e1 100644 --- a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md +++ b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md @@ -16,7 +16,7 @@ A newly composed top-level Session snapshots the route list in `subagent/model-s The fixed `list_subagent_models` schema does not enumerate the policy. At call time, it lists authorized registered providers and intersects their advertised model catalogs with the recorded route list. Exact lookup requires authorization before resolving model metadata and all supported reasoning efforts, including for ids absent from the catalog. Without a recorded policy, discovery resolves only the parent Agent’s current effective route, even if the catalog omits it. The delegation executor independently checks any explicit provider, model, or effort selection against the same authorization before adapter preflight. A call with no selection fields retains configured or inherited routing. -Model selection has no unrestricted static mode. The default-off Host setting authorizes alternative child routes; without it, the parent route and its supported efforts remain selectable. Only a delegation tool configured with `modelSelectionSettings` exposes the route fields and registers `list_subagent_models`; the shipped Web spawn tool is that owner, captures the setting once, and reads the Session policy at each call. Fork tools reject model-selection arguments, and the setting fails their mount, to preserve inherited-prefix reuse. +Model selection has no unrestricted static mode. The default-off Host setting authorizes alternative child routes; without it, the parent route and its supported efforts remain selectable. Only a delegation tool configured with `modelSelectionSettings` exposes the route fields and registers `list_subagent_models`; the shipped Web spawn tool is that owner, captures the setting once, and reads the Session policy at each call. Shipped fork tools leave `modelSelectionSettings` disabled to preserve inherited-prefix reuse; custom fork compositions may enable it under the same Session authorization. ## Alternatives considered diff --git a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md index 4fe117478a..f5141f6c08 100644 --- a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md +++ b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md @@ -16,7 +16,7 @@ Host 自有的 `subagent-model-selection` 设置 section 保存显式 `enabled` 固定的 `list_subagent_models` schema 不枚举策略。调用时,它列出已授权且已注册的提供方,并取其公布模型目录与已记录路由列表的交集。精确查询先要求授权,再解析模型元数据及全部支持的推理强度,也允许目录未列出的 ID。没有已记录策略时,发现只解析父 Agent 当前生效的路由,即使目录未列出它。委派执行器会在适配器预检前独立校验显式提供方、模型或强度选择是否符合相同授权。没有选择字段的调用保留配置或继承的路由。 -模型选择没有无限制的静态模式。默认关闭的 Host 设置授权其他子级路由;没有该授权时,仍可选择父级路由及其支持的强度。只有配置了 `modelSelectionSettings` 的委派工具会公开路由字段并注册 `list_subagent_models`;随附 Web 的 spawn 工具就是该所有者,它一次性记录设置,并在每次调用时读取 Session 策略。fork 工具拒绝模型选择参数,且该设置会使其挂载失败,以保留对继承前缀的复用。 +模型选择没有无限制的静态模式。默认关闭的 Host 设置授权其他子级路由;没有该授权时,仍可选择父级路由及其支持的强度。只有配置了 `modelSelectionSettings` 的委派工具会公开路由字段并注册 `list_subagent_models`;随附 Web 的 spawn 工具就是该所有者,它一次性记录设置,并在每次调用时读取 Session 策略。随附 fork 工具保持 `modelSelectionSettings` 关闭,以保留对继承前缀的复用;自定义 fork 组合可以在相同 Session 授权下启用它。 ## Alternatives considered diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 48f63cf574..fb0e67b20e 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -3993,8 +3993,8 @@ export interface Config { * Expose optional child `provider`, `model`, and `reasoning_effort` fields * plus `list_subagent_models`, and sample the Host `subagent-model-selection` * setting for each new top-level Session; child Sessions inherit that - * decision. Requires a provider with `agentOptions` that does not inherit - * the parent conversation. At most one instance per tool scope may enable it. + * decision. Requires a provider with `agentOptions` support. + * At most one instance per tool scope may enable it. */ modelSelectionSettings?: boolean /** diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index b462472c26..69942f48e7 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -3995,8 +3995,8 @@ export interface Config { * Expose optional child `provider`, `model`, and `reasoning_effort` fields * plus `list_subagent_models`, and sample the Host `subagent-model-selection` * setting for each new top-level Session; child Sessions inherit that - * decision. Requires a provider with `agentOptions` that does not inherit - * the parent conversation. At most one instance per tool scope may enable it. + * decision. Requires a provider with `agentOptions` support. + * At most one instance per tool scope may enable it. */ modelSelectionSettings?: boolean /** diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml index 529e9be439..3ca5b75f64 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: 551b31e8c713bbcd - zh: 861604a6d4d6db6b + en: 252109967b78c79a + zh: c1f807cee3f44f94 /tool-schema-catalog/deepseek-ai-dsh-plugin-manager: en: e0143916bb634762 zh: e0143916bb634762 @@ -228,8 +228,8 @@ en: 6872bee11fccb6c8 zh: 230f2d443dc5d9a6 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent/subagent: - en: d73236d49ab7c9f5 - zh: 1009b5c5d6d678a5 + en: c184808a51ea4000 + zh: 7a936839c719dea4 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control: en: ab82e130c3413f04 zh: ab82e130c3413f04 diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index 090afd911d..cf9c3ba93a 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -37,7 +37,7 @@ This table connects model-visible tool names to the plugin package and service s | `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`, `ctx.workflowEngine`, `ctx.subagents`, `ctx.systemPrompt`, `a calling Agent (exec.agent parents every fresh round)` | `tool/call`, `tool/result`, `workflow and child session events during execution` | - | A fixed foreground workflow starts one fresh structured child per round; the model selects only the immutable objective and an optional round cap. | | `@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` | - | 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-tool-subagent` | `list_subagent_models`, `subagent` | `ctx.tools`, `ctx.subagents`, `ctx.systemPrompt`, `ctx.llm for model discovery and selected-route validation` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion registered by an instance with `modelSelectionSettings`. That instance always exposes optional route fields; Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions, and without an enabled policy, selection and discovery are restricted to the current parent route. `subagent_fork` exposes no route selection. Each instance selects its backend through `provider`. 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.llm for model discovery and selected-route validation` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion registered by an instance with `modelSelectionSettings`. That instance always exposes optional route fields; Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions, and without an enabled policy, selection and discovery are restricted to the current parent route. The shipped `subagent_fork` exposes no route selection. Each instance selects its backend through `provider`. 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-control` | `interrupt_agent`, `list_agents`, `send_message` | `ctx.tools`, `ctx.subagents`, `ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`, `tool/result`, `child session events through ctx.subagents` | - | The globally named discovery, interruption, and local-message tools over managed subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` and `interrupt_agent` once, plus `list_agents` from its separately loaded `/list-agents` plugin (whose catalog rows use the sessionProjections and live Agent registries). | | `@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-experimental-tool-agent-team` | `interrupt_agent`, `list_agents`, `send_message`, `spawn_teammate`, `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update`, `wait_agent` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `an exact live Team member Agent` | `tool/call`, `team/member`, `team/task`, `agent/inbox/spliced`, `user/message on inbox claim`, `tool/result` | - | All nine tools are scoped to implicit Team Leads and durable teammates. The shipped dsh-base bundle keeps the package disabled; the documented Agent Teams profile patch enables it while disabling the legacy continuable-child control names. | @@ -2067,7 +2067,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 registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion registered by an instance with `modelSelectionSettings`. That instance always exposes optional route fields; Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions, and without an enabled policy, selection and discovery are restricted to the current parent route. `subagent_fork` exposes no route selection. Each instance selects its backend through `provider`. Every delegation returns a child id promptly; local children accept follow-ups, while external backends report their single execution automatically. +The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion registered by an instance with `modelSelectionSettings`. That instance always exposes optional route fields; Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions, and without an enabled policy, selection and discovery are restricted to the current parent route. The shipped `subagent_fork` exposes no route selection. Each instance selects its backend through `provider`. Every delegation returns a child id promptly; local children accept follow-ups, while external backends report their single execution automatically. diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index b60ade9669..c94fd2a8fc 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -41,7 +41,7 @@ | `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`、`ctx.workflowEngine`、`ctx.subagents`、`ctx.systemPrompt`、`a calling Agent (exec.agent parents every fresh round)` | `tool/call`、`tool/result`、`workflow and child session events during execution` | - | 固定的前台工作流会在每个 Round 启动一个全新的结构化子级;模型只能选择不可变目标和可选的 Round 上限。 | | `@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-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述默认 schema 关闭模型选择,而发现 schema 展示为设置了 `modelSelectionSettings` 的实例注册的固定配套工具。该实例始终公开可选路由字段;Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;没有启用策略时,选择和发现仅限当前父级路由。`subagent_fork` 不公开路由选择。每个实例通过 `provider` 选择后端。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 | +| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述默认 schema 关闭模型选择,而发现 schema 展示为设置了 `modelSelectionSettings` 的实例注册的固定配套工具。该实例始终公开可选路由字段;Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;没有启用策略时,选择和发现仅限当前父级路由。随附的 `subagent_fork` 不公开路由选择。每个实例通过 `provider` 选择后端。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 | | `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`、`list_agents`、`send_message` | `ctx.tools`、`ctx.subagents`、`ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`、`tool/result`、`child session events through ctx.subagents` | - | 这些是用于发现、中断托管 subagent 和向本地子代理发送消息的全局命名工具:绑定提供方的 `tool-subagent` 实例注册不同的委派工具;本包注册一次 `send_message` 和 `interrupt_agent`,另由 `list_agents` 通过单独加载的 `/list-agents` 插件提供,其目录行使用 sessionProjections 和实时 Agent 注册表。 | | `@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-experimental-tool-agent-team` | `interrupt_agent`、`list_agents`、`send_message`、`spawn_teammate`、`team_task_create`、`team_task_get`、`team_task_list`、`team_task_update`、`wait_agent` | `ctx.tools`、`ctx.systemPrompt`、`ctx.agentTeams`、`an exact live Team member Agent` | `tool/call`、`team/member`、`team/task`、`agent/inbox/spliced`、`user/message on inbox claim`、`tool/result` | - | 这 9 个工具限定于隐式 Team Lead 与持久 teammate 作用域。随产品发布的 dsh-base bundle 默认禁用该包;文档中的 Agent Teams profile patch 会启用它,并禁用旧 continuable child 的同名控制工具。 | @@ -2073,7 +2073,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, 来源:[`packages/subagent/tool-subagent/src/index.ts`](../packages/subagent/tool-subagent/src/index.ts) -注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述默认 schema 关闭模型选择,而发现 schema 展示为设置了 `modelSelectionSettings` 的实例注册的固定配套工具。该实例始终公开可选路由字段;Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;没有启用策略时,选择和发现仅限当前父级路由。`subagent_fork` 不公开路由选择。每个实例通过 `provider` 选择后端。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 +注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述默认 schema 关闭模型选择,而发现 schema 展示为设置了 `modelSelectionSettings` 的实例注册的固定配套工具。该实例始终公开可选路由字段;Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;没有启用策略时,选择和发现仅限当前父级路由。随附的 `subagent_fork` 不公开路由选择。每个实例通过 `provider` 选择后端。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/fork-model-selection-settings/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.2/fork-model-selection-settings/guide.i18n.yaml deleted file mode 100644 index c3a98c82ac..0000000000 --- a/docs/upgrade-guide/v0.2.1-alpha.2/fork-model-selection-settings/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-alpha.2/fork-model-selection-settings/guide.md -/: - en: 9a987818d6903fcb - zh: 8a4eefd0e7b4ad04 -/remove-modelselectionsettings-from-fork-delegation-tools: - en: f6bf4e381476a12f - zh: 0e745d803fc38fc9 -/remove-modelselectionsettings-from-fork-delegation-tools/change: - en: ccac82df3a00bf69 - zh: 1820a052adff5f6b -/remove-modelselectionsettings-from-fork-delegation-tools/migration: - en: ba409934bc81f492 - zh: fda2f67f731cd1d2 diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/fork-model-selection-settings/guide.md b/docs/upgrade-guide/v0.2.1-alpha.2/fork-model-selection-settings/guide.md deleted file mode 100644 index 542cc9a183..0000000000 --- a/docs/upgrade-guide/v0.2.1-alpha.2/fork-model-selection-settings/guide.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -kind: upgrade-guide -description: "`modelSelectionSettings` on a fork delegation tool now fails the mount instead of exposing child route selection." ---- - -# Remove `modelSelectionSettings` from fork delegation tools - -English | [中文](guide.zh.md) - -## Change - -In v0.2.1-alpha.2, a `@deepseek-ai/dsh-tool-subagent` row whose provider inherits the parent conversation, such as the in-process `fork` provider, accepted `modelSelectionSettings: true` and exposed `provider`, `model`, `reasoning_effort`, and `list_subagent_models` in Sessions with an enabled policy. The tool now rejects that configuration when the provider is mounted, so the composition fails to load with `provider "" inherits the parent conversation and does not accept child model selection`. Fork children keep the parent route so the inherited conversation prefix stays eligible for KV Cache reuse. - -The shipped profiles are unaffected: only the Web spawn tool sets `modelSelectionSettings`. A tool row with the setting now exposes the route fields and `list_subagent_models` in every Session; without a recorded Session policy, explicit selection permits only the parent route. - -## Migration - -1. In `cordis.yml`, profile patches, and overlays, remove `modelSelectionSettings` from every `@deepseek-ai/dsh-tool-subagent` row whose `provider` names a fork provider: - - ```yaml - - name: '@deepseek-ai/dsh-tool-subagent' - config: - provider: fork - toolName: subagent_fork - ``` - -2. To let the model choose child routes, set `modelSelectionSettings: true` on one spawn-style row per tool scope instead. -3. Start the composition and confirm that it loads and that the fork tool schema lists only `cwd`, `description`, and `prompt`. diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/fork-model-selection-settings/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.2/fork-model-selection-settings/guide.zh.md deleted file mode 100644 index e400299473..0000000000 --- a/docs/upgrade-guide/v0.2.1-alpha.2/fork-model-selection-settings/guide.zh.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -kind: upgrade-guide -description: "fork 委派工具上的 `modelSelectionSettings` 现在会使挂载失败,而不再公开子级路由选择。" ---- - -# 从 fork 委派工具中移除 `modelSelectionSettings` - -[English](guide.md) | 中文 - -## 变更 - -在 v0.2.1-alpha.2 中,如果 `@deepseek-ai/dsh-tool-subagent` 配置行的提供方会继承父级对话(例如进程内 `fork` 提供方),它会接受 `modelSelectionSettings: true`,并在策略已启用的 Session 中公开 `provider`、`model`、`reasoning_effort` 和 `list_subagent_models`。现在该工具会在提供方挂载时拒绝这一配置,组合加载失败并报告 `provider "" inherits the parent conversation and does not accept child model selection`。fork 子级保持父级路由,使继承的对话前缀继续符合 KV Cache 复用条件。 - -随附 profile 不受影响:只有 Web 的 spawn 工具设置了 `modelSelectionSettings`。设置了该项的工具行现在会在每个 Session 中公开路由字段和 `list_subagent_models`;没有已记录 Session 策略时,显式选择只允许父级路由。 - -## 迁移 - -1. 在 `cordis.yml`、profile patch 和 overlay 中,从所有 `provider` 指向 fork 提供方的 `@deepseek-ai/dsh-tool-subagent` 配置行移除 `modelSelectionSettings`: - - ```yaml - - name: '@deepseek-ai/dsh-tool-subagent' - config: - provider: fork - toolName: subagent_fork - ``` - -2. 如需让模型选择子级路由,请改为在每个工具作用域中的一个 spawn 类配置行上设置 `modelSelectionSettings: true`。 -3. 启动组合,确认它能加载,且 fork 工具的 schema 只列出 `cwd`、`description` 和 `prompt`。 diff --git a/packages/subagent/tool-subagent/README.i18n.yaml b/packages/subagent/tool-subagent/README.i18n.yaml index a806e98777..f02f435211 100644 --- a/packages/subagent/tool-subagent/README.i18n.yaml +++ b/packages/subagent/tool-subagent/README.i18n.yaml @@ -18,14 +18,14 @@ en: 93f1701f5209ecb5 zh: 6310fd012400aead /deepseek-ai-dsh-tool-subagent/use-this-package/minimal-configuration: - en: 70e715a7262f45b2 - zh: a94e81d7f1513fdf + en: 5465759f3c7fd5d7 + zh: 3f0b0cba46dbbc08 /deepseek-ai-dsh-tool-subagent/use-this-package/managed-delegation: en: 42673064ba88a211 zh: 8fa26285bd649dc9 /deepseek-ai-dsh-tool-subagent/use-this-package/selecting-a-child-llm: - en: 2bf262d3f462f5f5 - zh: c443c5366f230eee + en: ff7357f058f309ba + zh: 489f05233c1785a7 /deepseek-ai-dsh-tool-subagent/understand-the-implementation: en: 6ce5f749256a7e6c zh: a4f2adb04a70d9cf @@ -60,8 +60,8 @@ en: 86d1dafcb6c00c57 zh: fe890e9c6146c45e /deepseek-ai-dsh-tool-subagent/model-experience/tool-schema/kv-cache-effect: - en: f746a3c2f2f3cd18 - zh: 68de857073b229ff + en: 5697c49cb84fb2a4 + zh: b2b8464d3cbabfa5 /deepseek-ai-dsh-tool-subagent/model-experience/model-selection-and-discovery: en: a4ca31b9cbacda6b zh: 91ac3e0784a14697 @@ -114,8 +114,8 @@ en: f5d5f97a153f7c70 zh: 3d6bc05b9eadfbc2 /deepseek-ai-dsh-tool-subagent/known-limitations-and-deferred-work: - en: 8a0d48a82dff3343 - zh: 604ecd4e288ae322 + en: cbe53030678b9dd4 + zh: 06ccb6828e5708d5 /deepseek-ai-dsh-tool-subagent/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/subagent/tool-subagent/README.md b/packages/subagent/tool-subagent/README.md index 6f188766d6..b8de1764f4 100644 --- a/packages/subagent/tool-subagent/README.md +++ b/packages/subagent/tool-subagent/README.md @@ -46,7 +46,7 @@ Load the subagent service, an in-process or remote backend, and this tool; then |---|---|---| | `provider` | required | Provider name on `ctx.subagents` (e.g. `spawn`, `fork`, `acp`) | | `toolName` | `subagent` | Model-facing tool name; distinct for every loaded instance | -| `modelSelectionSettings` | `false` | Expose child route fields and `list_subagent_models`, and sample the Host's exact-route authorization preference for each top-level Session; a standing preset observes matching Sessions, while direct Agent setup passes its Session explicitly; requires provider `agentOptions` support without parent-context inheritance | +| `modelSelectionSettings` | `false` | Expose child route fields and `list_subagent_models`, and sample the Host's exact-route authorization preference for each top-level Session; a standing preset observes matching Sessions, while direct Agent setup passes its Session explicitly; requires provider `agentOptions` support | | `agentOptions` | — | Configured child `provider`, `model`, adapter-owned `reasoningEffort`, and positive `maxTokens` defaults; requires provider `agentOptions` support and overlays any provider-owned route defaults | | `persona` | — | Per-child persona; requires the provider's `persona` capability | | `toolFilter` | — | Per-child global-tool restriction; requires the `toolFilter` capability | @@ -64,7 +64,7 @@ Every backend’s completion notice carries its final answer. Local Spawn and Fo ### Selecting a child LLM -Set `modelSelectionSettings: true` to expose optional `provider`, `model`, and `reasoning_effort` fields and register `list_subagent_models` with this instance; instances without it expose neither and reject selection fields. The instance samples the Host’s `subagent-model-selection` preference for fresh top-level Sessions. When enabled, its exact route list is recorded in the Session, inherited by child Sessions, and preserved on restore; later settings edits affect only new Sessions. Without a recorded Session policy, discovery and explicit selection permit only the parent Agent’s current effective provider/model and its supported efforts. This mode requires a backend that advertises `agentOptions` and does not inherit the parent conversation; both in-process Spawn and DSH SDK qualify, while Fork, ACP, Codex, and Claude Code fail the mount. +Set `modelSelectionSettings: true` to expose optional `provider`, `model`, and `reasoning_effort` fields and register `list_subagent_models` with this instance; instances without it expose neither and reject selection fields. The instance samples the Host’s `subagent-model-selection` preference for fresh top-level Sessions. When enabled, its exact route list is recorded in the Session, inherited by child Sessions, and preserved on restore; later settings edits affect only new Sessions. Without a recorded Session policy, discovery and explicit selection permit only the parent Agent’s current effective provider/model and its supported efforts. This mode requires a backend that advertises `agentOptions`; in-process Spawn and Fork and DSH SDK qualify, while ACP, Codex, and Claude Code fail the mount. A call supplies `provider` and `model` together, or supplies only an effort when configured, parent, or provider-owned defaults provide the route. Static `provider.agentRouteDefaults`, when present, form the provider/model baseline; tool configuration and model fields overlay it before route-aware effort merging and exact-route preflight. Providers without these defaults use compatible values from the parent's latest logged request, then the parent's creation options before its first request, while retaining the configured `maxTokens`. Changing the route without an explicit effort clears the inherited route-owned effort, so the selected model resolves its default. The live LLM adapter validates the effective route before child creation. Catalog membership remains advisory, so a model can use an unlisted id when its adapter accepts it. @@ -136,7 +136,7 @@ Fixed schema cost per parent request; `modelSelectionSettings` adds three option #### KV Cache effect -Prefix-stable while provider instances and their configuration are unchanged. Adapter catalog changes do not alter the definition. Fork tools omit model-selection fields to preserve reuse of the inherited parent prefix. +Prefix-stable while provider instances and their configuration are unchanged. Adapter catalog changes do not alter the definition. Shipped fork tools omit model-selection fields to preserve reuse of the inherited parent prefix. A custom fork tool may enable selection; changing its route can prevent prefix reuse. ### Model selection and discovery @@ -208,7 +208,7 @@ Append-only; newly visible content follows the reusable request prefix and does These limits define what this tool does not return or enforce; they are current package constraints. - **This tool does not wait for results** — use the completion notice or child messages. Programmatic result waiting belongs to the subagent service. -- **Fork tools reject model selection** — tools that inherit parent context neither expose nor accept `provider`, `model`, or `reasoning_effort`, even with Session authorization, and `modelSelectionSettings` fails their mount. Configured `agentOptions` remain available to the deployment. +- **Shipped fork tools omit model selection** — their configuration leaves `modelSelectionSettings` disabled to preserve inherited-prefix reuse. Custom fork tools may enable it under the same Session authorization and adapter validation as other delegation tools. - **Non-routing child policy is fixed per instance** — another persona, tool filter, or depth cap requires another distinctly named tool. Explicit LLM selection requires `modelSelectionSettings` and is restricted to the Session’s authorized routes, or the current parent route without an enabled policy. diff --git a/packages/subagent/tool-subagent/README.zh.md b/packages/subagent/tool-subagent/README.zh.md index 7a819691ca..575b9b4530 100644 --- a/packages/subagent/tool-subagent/README.zh.md +++ b/packages/subagent/tool-subagent/README.zh.md @@ -46,7 +46,7 @@ kind: "package-reference" |---|---|---| | `provider` | 必填 | `ctx.subagents` 上的提供方名称(如 `spawn`、`fork`、`acp`) | | `toolName` | `subagent` | 面向模型的工具名称;每个已加载实例必须不同 | -| `modelSelectionSettings` | `false` | 公开子级路由字段和 `list_subagent_models`,并为每个顶层 Session 读取宿主的精确路由授权偏好;常驻 preset 观察匹配 Session,直接 Agent setup 则显式传入其 Session;要求提供方支持 `agentOptions` 且不继承父级上下文 | +| `modelSelectionSettings` | `false` | 公开子级路由字段和 `list_subagent_models`,并为每个顶层 Session 读取宿主的精确路由授权偏好;常驻 preset 观察匹配 Session,直接 Agent setup 则显式传入其 Session;要求提供方支持 `agentOptions` | | `agentOptions` | — | 配置的子级 `provider`、`model`、适配器所有的 `reasoningEffort` 与正整数 `maxTokens` 默认值;要求提供方支持 `agentOptions`,并会覆盖提供方持有的路由默认值 | | `persona` | — | 每个子 agent 独立的 persona;要求提供方具备 `persona` 能力 | | `toolFilter` | — | 每个子 agent 独立的全局工具限制;要求提供方具备 `toolFilter` 能力 | @@ -64,7 +64,7 @@ kind: "package-reference" ### 选择子级 LLM -设置 `modelSelectionSettings: true` 后,该实例公开可选的 `provider`、`model` 和 `reasoning_effort` 字段,并注册 `list_subagent_models`;未设置的实例两者都不公开,并拒绝选择字段。该实例为新顶层 Session 读取 Host 的 `subagent-model-selection` 偏好。启用时,精确路由列表记录在 Session 中,由子 Session 继承,并在恢复时保留;之后的设置编辑只影响新 Session。没有已记录 Session 策略时,发现和显式选择只允许父 Agent 当前生效的 provider/model 及其支持的强度。此模式要求后端公布 `agentOptions` 且不继承父级对话;进程内 Spawn 和 DSH SDK 满足条件,Fork、ACP、Codex 和 Claude Code 会使挂载失败。 +设置 `modelSelectionSettings: true` 后,该实例公开可选的 `provider`、`model` 和 `reasoning_effort` 字段,并注册 `list_subagent_models`;未设置的实例两者都不公开,并拒绝选择字段。该实例为新顶层 Session 读取 Host 的 `subagent-model-selection` 偏好。启用时,精确路由列表记录在 Session 中,由子 Session 继承,并在恢复时保留;之后的设置编辑只影响新 Session。没有已记录 Session 策略时,发现和显式选择只允许父 Agent 当前生效的 provider/model 及其支持的强度。此模式要求后端公布 `agentOptions`;进程内 Spawn、Fork 和 DSH SDK 满足条件,ACP、Codex 和 Claude Code 会使挂载失败。 一次调用需同时提供 `provider` 与 `model`;当配置值、父 agent 值或提供方持有的默认值能提供路由时,也可只提供推理等级。静态的 `provider.agentRouteDefaults` 在存在时构成提供方/模型基线;工具配置与模型字段会在路由相关强度合并和确切路由预检前覆盖它。没有这些默认值的提供方会使用父 agent 最新已记录请求中的兼容值,再使用父级首次请求前的创建选项,并保留配置的 `maxTokens`。更改路由但未显式提供推理等级时,会清除继承的路由自有等级,使所选模型解析自己的默认值。实时 LLM 适配器在创建子 agent 前校验有效路由。目录成员资格只提供建议,因此适配器接受时,模型可以使用未列出的 id。 @@ -136,7 +136,7 @@ kind: "package-reference" #### KV Cache 影响 -只要提供方实例及其配置不变,前缀就保持稳定。适配器目录变化不会改变定义。fork 工具省略模型选择字段,以保留对继承的父级前缀的复用。 +只要提供方实例及其配置不变,前缀就保持稳定。适配器目录变化不会改变定义。随附 fork 工具省略模型选择字段,以保留对继承的父级前缀的复用。自定义 fork 工具可以启用选择;更改其路由可能使前缀无法复用。 ### 模型选择与发现 @@ -208,7 +208,7 @@ Start independent delegations with `subagent` or `subagent_fork` together in one 这些限制说明本工具不返回或不强制执行什么;它们是当前包约束。 - **本工具不等待结果**——使用完成通知或子任务消息。程序化结果等待由 subagent 服务提供。 -- **fork 工具拒绝模型选择** — 继承父级上下文的工具既不公开也不接受 `provider`、`model` 或 `reasoning_effort`,即使 Session 已授权也不例外;`modelSelectionSettings` 会使其挂载失败。部署仍可配置 `agentOptions`。 +- **随附 fork 工具省略模型选择**:其配置保持 `modelSelectionSettings` 关闭,以保留对继承前缀的复用。自定义 fork 工具可以启用它,并接受与其他委派工具相同的 Session 授权和适配器校验。 - **非路由子级策略按实例固定** — 更换角色、工具过滤器或深度上限,需要另一个不同名称的工具。显式 LLM 选择需要 `modelSelectionSettings`,并受 Session 授权路由限制,没有启用策略时只允许当前父级路由。 diff --git a/packages/subagent/tool-subagent/src/index.ts b/packages/subagent/tool-subagent/src/index.ts index 928e405f9f..d07e4aa883 100644 --- a/packages/subagent/tool-subagent/src/index.ts +++ b/packages/subagent/tool-subagent/src/index.ts @@ -57,8 +57,8 @@ export interface Config { * Expose optional child `provider`, `model`, and `reasoning_effort` fields * plus `list_subagent_models`, and sample the Host `subagent-model-selection` * setting for each new top-level Session; child Sessions inherit that - * decision. Requires a provider with `agentOptions` that does not inherit - * the parent conversation. At most one instance per tool scope may enable it. + * decision. Requires a provider with `agentOptions` support. + * At most one instance per tool scope may enable it. */ modelSelectionSettings?: boolean /** @@ -193,12 +193,6 @@ export function apply(ctx: Context, config: Config, session?: Session): void { `tool-subagent: provider "${subagentProvider.name}" does not support child model selection`, ) } - if (modelSelectionEnabled && subagentProvider.inheritsParentContext) { - throw new Error( - `tool-subagent: provider "${subagentProvider.name}" inherits the parent conversation and does not accept child model selection` - + ' — remove `modelSelectionSettings` from this instance', - ) - } } // Validate provider-owned config outside the optional LLM binding so an diff --git a/packages/subagent/tool-subagent/tests/model-selection.spec.ts b/packages/subagent/tool-subagent/tests/model-selection.spec.ts index 69b72ae876..0181e77b9c 100644 --- a/packages/subagent/tool-subagent/tests/model-selection.spec.ts +++ b/packages/subagent/tool-subagent/tests/model-selection.spec.ts @@ -495,12 +495,41 @@ describe('dsh-tool-subagent model selection', () => { expect(resolve).not.toHaveBeenCalled() }) - it('rejects modelSelectionSettings on a provider that inherits the parent conversation', async () => { - await expect(setup({ provider: 'mock', withModelSelection: true }, { inheritsParentContext: true })) - .rejects.toThrow('provider "mock" inherits the parent conversation and does not accept child model selection') + it('enforces Session authorization when a fork tool explicitly enables model selection', async () => { + const requests: SubagentStartRequest[] = [] + const ctx = await setup({ provider: 'mock', withModelSelection: true }, { + inheritsParentContext: true, + onStart: (request) => { requests.push(request) }, + }) + ctx.llm.registerAdapter(['alpha'], new MockAdapter([], REASONING)) + const agent = modelSelectionSetupAgent(ctx) + const schema = ctx.tools.schemas(agent).find(entry => entry.name === 'subagent')! + expect(schema.description).toContain('inherits this conversation') + expect(schema.description).toContain('list_subagent_models') + + const allowed = await callSubagent(ctx, { + description: 'fork work', prompt: 'do it', provider: 'alpha', model: 'fast-model', reasoning_effort: 'low', + }) + expect(allowed.isError, text(allowed)).toBe(false) + expect(requests[0]?.agentOptions).toEqual({ + provider: 'alpha', model: 'fast-model', reasoningEffort: ReasoningEffortId('low'), + }) + + const denied = await callSubagent(ctx, { + description: 'unauthorized route', prompt: 'do it', provider: 'alpha', model: 'denied-model', + }) + expect(denied.isError).toBe(true) + expect(text(denied)).toContain('is not allowed for this Session') + + const invalidEffort = await callSubagent(ctx, { + description: 'unsupported effort', prompt: 'do it', provider: 'alpha', model: 'fast-model', reasoning_effort: 'invalid', + }) + expect(invalidEffort.isError).toBe(true) + expect(text(invalidEffort)).toContain('invalid') + expect(requests).toHaveLength(1) }) - it('keeps fork model selection unavailable even with Session authorization', async () => { + it('keeps fork model selection unavailable without instance opt-in even with Session authorization', async () => { const started = vi.fn() const ctx = await setup({ provider: 'mock' }, { inheritsParentContext: true, diff --git a/scripts/gen-tool-catalog.ts b/scripts/gen-tool-catalog.ts index 4cfa955c2d..2d554d3542 100644 --- a/scripts/gen-tool-catalog.ts +++ b/scripts/gen-tool-catalog.ts @@ -522,7 +522,7 @@ const TOOL_PACKAGES: ToolPackage[] = [ registerListSubagentModels(ctx, () => ({ routes: [{ provider: 'mock', model: 'mock' }] })) }, note: - 'The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion registered by an instance with `modelSelectionSettings`. That instance always exposes optional route fields; Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions, and without an enabled policy, selection and discovery are restricted to the current parent route. `subagent_fork` exposes no route selection. Each instance selects its backend through `provider`. Every delegation returns a child id promptly; local children accept follow-ups, while external backends report their single execution automatically.', + 'The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion registered by an instance with `modelSelectionSettings`. That instance always exposes optional route fields; Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions, and without an enabled policy, selection and discovery are restricted to the current parent route. The shipped `subagent_fork` exposes no route selection. Each instance selects its backend through `provider`. Every delegation returns a child id promptly; local children accept follow-ups, while external backends report their single execution automatically.', }, { pkg: '@deepseek-ai/dsh-tool-subagent-control', From fc41f012a98409d2ddfcc6631e07ba2aeeb5aa1e Mon Sep 17 00:00:00 2001 From: _Kerman Date: Tue, 13 Oct 2026 11:32:36 +0800 Subject: [PATCH 047/369] docs: refresh config catalog after merging master --- docs/config-catalog.md | 2 +- docs/config-catalog.zh.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/config-catalog.md b/docs/config-catalog.md index a31297183e..188bf83918 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -3992,7 +3992,7 @@ export interface Config { - `inject`: `tools` · `subagents` · `systemPrompt` · `sessionProjections` - `refs`: [`AgentOptions`](subsystems/core.md) -- `source`: [`packages/subagent/tool-subagent/src/index.ts:49`](../packages/subagent/tool-subagent/src/index.ts) +- `source`: [`packages/subagent/tool-subagent/src/index.ts:50`](../packages/subagent/tool-subagent/src/index.ts) ```ts config-catalog /** Config: which registered provider this tool delegates to, plus child defaults. */ diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index ed71c85653..bcde75fe3d 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -3994,7 +3994,7 @@ export interface Config { - `inject`: `tools` · `subagents` · `systemPrompt` · `sessionProjections` - `refs`: [`AgentOptions`](subsystems/core.zh.md) -- `source`: [`packages/subagent/tool-subagent/src/index.ts:49`](../packages/subagent/tool-subagent/src/index.ts) +- `source`: [`packages/subagent/tool-subagent/src/index.ts:50`](../packages/subagent/tool-subagent/src/index.ts) ```ts config-catalog /** Config: which registered provider this tool delegates to, plus child defaults. */ From 8fa8e9167db1f71ab66f2ebc495bdc3399467ee9 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Tue, 13 Oct 2026 11:42:31 +0800 Subject: [PATCH 048/369] refactor(subagent): separate model discovery and session authorization --- ...authorized-subagent-model-routes.i18n.yaml | 8 +- ...4-user-authorized-subagent-model-routes.md | 6 +- ...ser-authorized-subagent-model-routes.zh.md | 6 +- docs/config-catalog.md | 12 +- docs/config-catalog.zh.md | 12 +- docs/event-producer-consumer.md | 2 +- docs/event-producer-consumer.zh.md | 2 +- docs/subsystems/subagent.md | 4 +- docs/subsystems/subagent.zh.md | 4 +- docs/tool-catalog.i18n.yaml | 8 +- docs/tool-catalog.md | 4 +- docs/tool-catalog.zh.md | 4 +- .../guide.i18n.yaml | 16 + .../guide.md | 19 + .../guide.zh.md | 19 + packages/bundle/web-app/cordis.patch.yml | 4 +- .../bundle/web-app/presets/cordis.patch.yml | 4 +- packages/bundle/web-app/presets/ptc.patch.yml | 4 +- .../bundle/web-app/presets/standard.patch.yml | 4 +- .../tests/serial-listener-review.spec.ts | 13 +- .../extensions/tool-cordis/src/api-catalog.ts | 6 +- .../fixtures/loader/scoped-tool-subagent.ts | 6 +- .../subagent/tool-subagent/README.i18n.yaml | 32 +- packages/subagent/tool-subagent/README.md | 18 +- packages/subagent/tool-subagent/README.zh.md | 18 +- packages/subagent/tool-subagent/package.json | 7 +- packages/subagent/tool-subagent/src/index.ts | 403 +++++++----------- .../subagent/tool-subagent/src/list-models.ts | 20 +- .../src/model-selection-settings.ts | 35 +- .../src/model-selection-state.ts | 17 - .../subagent/tool-subagent/tests/harness.ts | 9 +- .../tool-subagent/tests/list-models.spec.ts | 98 ++--- .../tests/model-selection-settings.spec.ts | 109 ++--- .../tests/model-selection.spec.ts | 2 +- .../subagent/tool-subagent/tsdown.config.ts | 1 + scripts/check-workspace-constraints.ts | 2 +- scripts/gen-tool-catalog.ts | 6 +- tsconfig.base.json | 1 + 38 files changed, 434 insertions(+), 511 deletions(-) create mode 100644 docs/upgrade-guide/v0.2.1-alpha.2/subagent-model-selection-composition/guide.i18n.yaml create mode 100644 docs/upgrade-guide/v0.2.1-alpha.2/subagent-model-selection-composition/guide.md create mode 100644 docs/upgrade-guide/v0.2.1-alpha.2/subagent-model-selection-composition/guide.zh.md diff --git a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml index 4561eacc4e..08799efa2b 100644 --- a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.i18n.yaml @@ -9,14 +9,14 @@ en: 3238dfb7db7dfdd8 zh: ec3bdd9a9da0a321 /agent-note-user-authorized-subagent-model-routes/decision: - en: 423eefc69964717c - zh: 7f73ea1c82630314 + en: 78824ae164ea974e + zh: 84aa7740d9790563 /agent-note-user-authorized-subagent-model-routes/alternatives-considered: en: e41c17226190ea8b zh: d3b1395fafe75f01 /agent-note-user-authorized-subagent-model-routes/consequences: - en: 36b76692f1605e6d - zh: a63a249c980bc654 + en: 77677b5828d0de29 + zh: 609109e3388933a9 /agent-note-user-authorized-subagent-model-routes/related-decisions: en: 0eead87ec1172d81 zh: 4e776a583f5a00a5 diff --git a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md index a44c5245e1..8113ddef6b 100644 --- a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md +++ b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.md @@ -12,11 +12,11 @@ Registering an LLM adapter makes its routes reachable, but does not authorize an The Host-owned `subagent-model-selection` settings section stores an explicit `enabled` switch and `allowedModels`, an array of exact `{ provider, model }` routes. Enabling requires at least one route; disabling may retain the selected routes for later reuse. The Plugins settings card reads the live adapter directory through `session/modelCatalog`, lets the user stage the switch and routes, and saves both fields in one revision-fenced settings mutation. It stores no adapter-owned display names, descriptions, or reasoning-effort metadata. A stored or staged route absent from the current directory remains visible as unavailable and removable; a provider-local catalog failure does not block other providers or erase saved authorization or an unsaved selection. A connection reset discards the draft because namespace revisions are comparable only within one Host process. -A newly composed top-level Session snapshots the route list in `subagent/model-selection-policy` when the setting is enabled, before its first model request. Event presence means selection was enabled; the event does not store the global switch. Child Sessions inherit that exact list from their live parent, and resumed Sessions use the recorded event instead of current settings. Settings changes therefore affect only subsequently composed top-level Sessions. Without the event, including an explicitly empty restored Session, explicit selection remains restricted to the current parent route. +The Host settings service snapshots the route list for each fresh top-level Session in `subagent/model-selection-policy` when the setting is enabled, before its first model request. Event presence means selection was enabled; the event does not store the global switch. Child Sessions inherit that exact list from their live parent, and resumed Sessions use the recorded event instead of current settings. Settings changes therefore affect only subsequently created top-level Sessions. Without the event, including an explicitly empty restored Session, explicit selection remains restricted to the current parent route. The fixed `list_subagent_models` schema does not enumerate the policy. At call time, it lists authorized registered providers and intersects their advertised model catalogs with the recorded route list. Exact lookup requires authorization before resolving model metadata and all supported reasoning efforts, including for ids absent from the catalog. Without a recorded policy, discovery resolves only the parent Agent’s current effective route, even if the catalog omits it. The delegation executor independently checks any explicit provider, model, or effort selection against the same authorization before adapter preflight. A call with no selection fields retains configured or inherited routing. -Model selection has no unrestricted static mode. The default-off Host setting authorizes alternative child routes; without it, the parent route and its supported efforts remain selectable. Only a delegation tool configured with `modelSelectionSettings` exposes the route fields and registers `list_subagent_models`; the shipped Web spawn tool is that owner, captures the setting once, and reads the Session policy at each call. Shipped fork tools leave `modelSelectionSettings` disabled to preserve inherited-prefix reuse; custom fork compositions may enable it under the same Session authorization. +Model selection has no unrestricted static mode. The default-off Host setting authorizes alternative child routes; without it, the parent route and its supported efforts remain selectable. Each delegation tool’s `modelSelection` option controls only its route fields. The independent `/list-models` plugin owns discovery. Web and Desktop presets mount discovery by default, enable spawn route fields, and leave fork route fields disabled to preserve inherited-prefix reuse; custom fork compositions may enable them under the same Session authorization. Tool installation does not sample or expand authorization. ## Alternatives considered @@ -34,7 +34,7 @@ Model selection has no unrestricted static mode. The default-off Host setting au - New adapter registrations and newly advertised models do not expand user authorization. - Adapter removal or catalog lookup failure can make provider discovery unavailable without deleting the saved route decision; an allowed model remains usable when its adapter accepts it even if the advisory catalog omits it. -- The allowlist itself consumes no parent-request tokens. Only a `list_subagent_models` result enters the transcript. Compositions with `modelSelectionSettings` carry the three route fields and the discovery schema in every Session; headless and SDK profiles carry neither. +- The allowlist itself consumes no parent-request tokens. Only a `list_subagent_models` result enters the transcript. The `modelSelection` option adds three route fields; `/list-models` adds a separate discovery schema. Shipped headless and SDK profiles carry neither. - The policy event is log-only and is appended while an Agent is composed, before either SDK begins its run subscription. Shipped SDK profiles do not enable this Web-owned preference, so the event changes neither SDK's expected notifications or persisted-session output; package restore tests own its durable projection instead of fabricating an SDK composition solely to emit it. - Unit coverage pins settings validation, malformed durable values, Session sampling and inheritance, shared definitions, parent-route defaults, discovery restrictions, executor denial, and the settings UI’s staged writes and revision checks. The assembled Web scenario pins the real settings document and Plugins card flow. diff --git a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md index f5141f6c08..6d33817449 100644 --- a/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md +++ b/.agents/notes/implemented/feature/2026-08-24-user-authorized-subagent-model-routes.zh.md @@ -12,11 +12,11 @@ Status: implemented Host 自有的 `subagent-model-selection` 设置 section 保存显式 `enabled` 开关与 `allowedModels`,后者是由精确 `{ provider, model }` 路由组成的数组。启用时必须至少有一条路由;关闭时可以保留已选路由,供以后重新启用。Plugins 设置卡通过 `session/modelCatalog` 读取实时适配器目录,让用户暂存开关与路由,再在一次带 revision 限制的设置 mutation 中保存两个字段。它不保存适配器自有的显示名称、描述或推理强度元数据。当前目录中缺失的已存或暂存路由仍显示为不可用并允许移除;某个提供方的目录失败不会阻塞其他提供方,也不会清除已存授权或未保存选择。连接重置会丢弃草稿,因为 namespace revision 只能在同一个 Host 进程内比较。 -设置启用时,新组合的顶层 Session 在首个模型请求前将路由列表记录为 `subagent/model-selection-policy`。事件存在表示选择已启用;事件不保存全局开关。子 Session 从在线父级继承同一精确列表,恢复的 Session 使用已记录事件而非当前设置。因此设置变更只影响之后组合的顶层 Session。没有该事件时,包括显式为空的恢复 Session,显式选择仍仅限当前父级路由。 +设置启用时,Host 设置服务为每个新顶层 Session 在首个模型请求前将路由列表记录为 `subagent/model-selection-policy`。事件存在表示选择已启用;事件不保存全局开关。子 Session 从在线父级继承同一精确列表,恢复的 Session 使用已记录事件而非当前设置。因此设置变更只影响之后创建的顶层 Session。没有该事件时,包括显式为空的恢复 Session,显式选择仍仅限当前父级路由。 固定的 `list_subagent_models` schema 不枚举策略。调用时,它列出已授权且已注册的提供方,并取其公布模型目录与已记录路由列表的交集。精确查询先要求授权,再解析模型元数据及全部支持的推理强度,也允许目录未列出的 ID。没有已记录策略时,发现只解析父 Agent 当前生效的路由,即使目录未列出它。委派执行器会在适配器预检前独立校验显式提供方、模型或强度选择是否符合相同授权。没有选择字段的调用保留配置或继承的路由。 -模型选择没有无限制的静态模式。默认关闭的 Host 设置授权其他子级路由;没有该授权时,仍可选择父级路由及其支持的强度。只有配置了 `modelSelectionSettings` 的委派工具会公开路由字段并注册 `list_subagent_models`;随附 Web 的 spawn 工具就是该所有者,它一次性记录设置,并在每次调用时读取 Session 策略。随附 fork 工具保持 `modelSelectionSettings` 关闭,以保留对继承前缀的复用;自定义 fork 组合可以在相同 Session 授权下启用它。 +模型选择没有无限制的静态模式。默认关闭的 Host 设置授权其他子级路由;没有该授权时,仍可选择父级路由及其支持的强度。各委派工具的 `modelSelection` 选项只控制自身路由字段。独立的 `/list-models` 插件负责发现。Web 与 Desktop preset 默认挂载发现工具,启用 spawn 路由字段,并为 fork 保持关闭,以保留对继承前缀的复用;自定义 fork 组合可以在相同 Session 授权下启用这些字段。工具安装不采样或扩大授权。 ## Alternatives considered @@ -34,7 +34,7 @@ Host 自有的 `subagent-model-selection` 设置 section 保存显式 `enabled` - 新适配器注册和新公布模型不会扩大用户授权。 - 适配器移除或目录查询失败可能使提供方发现不可用,但不会删除已存路由决定;只要适配器接受某个获准模型,即使建议性目录省略它,该模型仍可使用。 -- 允许列表本身不消耗父级请求 token。只有 `list_subagent_models` 结果进入 transcript。配置了 `modelSelectionSettings` 的组合在每个 Session 中都携带三个路由字段和发现 schema;headless 与 SDK profile 两者都不携带。 +- 允许列表本身不消耗父级请求 token。只有 `list_subagent_models` 结果进入 transcript。`modelSelection` 选项增加三个路由字段;`/list-models` 增加独立发现 schema。随附 headless 与 SDK profile 两者都不携带。 - 策略事件仅存在于日志,并在 Agent 组合期间、两套 SDK 开始订阅运行前追加。随附 SDK profile 不启用这项 Web 自有偏好,因此该事件不会改变任一 SDK 的预期通知或持久 Session 输出;其持久投影由包级恢复测试负责,不会为了发出该事件而虚构 SDK 组合。 - 单元覆盖固定设置校验、异常持久值、Session 取样与继承、共享定义、父级路由默认值、发现限制、执行器拒绝,以及设置 UI 的暂存写入与 revision 检查。组装 Web 场景固定真实设置文档与 Plugins 设置卡流程。 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index fb0e67b20e..76e25a9c29 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -3977,7 +3977,7 @@ export interface Config { - `inject`: `tools` · `subagents` · `systemPrompt` · `sessionProjections` - `refs`: [`AgentOptions`](subsystems/core.md) -- `source`: [`packages/subagent/tool-subagent/src/index.ts:48`](../packages/subagent/tool-subagent/src/index.ts) +- `source`: [`packages/subagent/tool-subagent/src/index.ts:42`](../packages/subagent/tool-subagent/src/index.ts) ```ts config-catalog /** Config: which registered provider this tool delegates to, plus child defaults. */ @@ -3990,13 +3990,11 @@ export interface Config { */ toolName?: string /** - * Expose optional child `provider`, `model`, and `reasoning_effort` fields - * plus `list_subagent_models`, and sample the Host `subagent-model-selection` - * setting for each new top-level Session; child Sessions inherit that - * decision. Requires a provider with `agentOptions` support. - * At most one instance per tool scope may enable it. + * Expose optional child `provider`, `model`, and `reasoning_effort` fields. + * Requires provider `agentOptions` support. Execution enforces the Session + * policy, or restricts explicit selection to the current parent route. */ - modelSelectionSettings?: boolean + modelSelection?: boolean /** * Agent options applied to every child; omitted fields use child-loop defaults. */ diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 69942f48e7..e6b40be645 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -3979,7 +3979,7 @@ export interface Config { - `inject`: `tools` · `subagents` · `systemPrompt` · `sessionProjections` - `refs`: [`AgentOptions`](subsystems/core.zh.md) -- `source`: [`packages/subagent/tool-subagent/src/index.ts:48`](../packages/subagent/tool-subagent/src/index.ts) +- `source`: [`packages/subagent/tool-subagent/src/index.ts:42`](../packages/subagent/tool-subagent/src/index.ts) ```ts config-catalog /** Config: which registered provider this tool delegates to, plus child defaults. */ @@ -3992,13 +3992,11 @@ export interface Config { */ toolName?: string /** - * Expose optional child `provider`, `model`, and `reasoning_effort` fields - * plus `list_subagent_models`, and sample the Host `subagent-model-selection` - * setting for each new top-level Session; child Sessions inherit that - * decision. Requires a provider with `agentOptions` support. - * At most one instance per tool scope may enable it. + * Expose optional child `provider`, `model`, and `reasoning_effort` fields. + * Requires provider `agentOptions` support. Execution enforces the Session + * policy, or restricts explicit selection to the current parent route. */ - modelSelectionSettings?: boolean + modelSelection?: boolean /** * Agent options applied to every child; omitted fields use child-loop defaults. */ diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index b40bcf5462..360fa5bd1f 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -73,7 +73,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `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`) | [`agent`](../packages/core/agent), `browser-use-runtime`, [`session-reference`](../packages/context/session-reference), [`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/index.ts:209`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:209`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime` | | `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:165`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` | | `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:177`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) | | `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:154`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index 9c7acd09bb..aa57743c7e 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -75,7 +75,7 @@ | `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`) | [`agent`](../packages/core/agent), `browser-use-runtime`, [`session-reference`](../packages/context/session-reference), [`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/index.ts:209`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) | +| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:209`](../packages/core/tools/src/index.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime` | | `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:165`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `browser-use-runtime`, `browser-use-stagehand-native`, `claude-code-mods`, `computer-use-cua-driver-native`, [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), `timeout-policy` | | `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:177`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `claude-code-mods`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) | | `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:154`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | `auto-review`, [`hooks-claude-code`](../packages/experimental/hooks-claude-code), [`hooks-codex`](../packages/experimental/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs), [`workspace-changes`](../packages/deliverables/workspace-changes) | diff --git a/docs/subsystems/subagent.md b/docs/subsystems/subagent.md index 5647fab452..31b95187e9 100644 --- a/docs/subsystems/subagent.md +++ b/docs/subsystems/subagent.md @@ -412,11 +412,11 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp ### `ctx.subagentModelSelection` — `SubagentModelSelectionConfig` -Singleton settings owner read when delegation tools are composed for a Session. +Host settings owner that records Session authorization before the first model request. ```ts cordis-catalog /** - * Read a detached selection preference for the next eligible Session composition. + * Read a detached selection preference for the next fresh top-level Session. * @returns the enabled state and exact allowed routes. */ current(): SubagentModelSelectionSettings diff --git a/docs/subsystems/subagent.zh.md b/docs/subsystems/subagent.zh.md index 30262036b7..8733129909 100644 --- a/docs/subsystems/subagent.zh.md +++ b/docs/subsystems/subagent.zh.md @@ -414,11 +414,11 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp ### `ctx.subagentModelSelection` — `SubagentModelSelectionConfig` -Singleton settings owner read when delegation tools are composed for a Session. +Host settings owner that records Session authorization before the first model request. ```ts cordis-catalog /** - * Read a detached selection preference for the next eligible Session composition. + * Read a detached selection preference for the next fresh top-level Session. * @returns the enabled state and exact allowed routes. */ current(): SubagentModelSelectionSettings diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml index 3ca5b75f64..86a316406d 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: 252109967b78c79a - zh: c1f807cee3f44f94 + en: 7224f159dd69abba + zh: 00b69ab3f899010a /tool-schema-catalog/deepseek-ai-dsh-plugin-manager: en: e0143916bb634762 zh: e0143916bb634762 @@ -228,8 +228,8 @@ en: 6872bee11fccb6c8 zh: 230f2d443dc5d9a6 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent/subagent: - en: c184808a51ea4000 - zh: 7a936839c719dea4 + en: 7b06bc34df756d56 + zh: bc82dab293f57b5b /tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control: en: ab82e130c3413f04 zh: ab82e130c3413f04 diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index cf9c3ba93a..7ff20028a7 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -37,7 +37,7 @@ This table connects model-visible tool names to the plugin package and service s | `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`, `ctx.workflowEngine`, `ctx.subagents`, `ctx.systemPrompt`, `a calling Agent (exec.agent parents every fresh round)` | `tool/call`, `tool/result`, `workflow and child session events during execution` | - | A fixed foreground workflow starts one fresh structured child per round; the model selects only the immutable objective and an optional round cap. | | `@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` | - | 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-tool-subagent` | `list_subagent_models`, `subagent` | `ctx.tools`, `ctx.subagents`, `ctx.systemPrompt`, `ctx.llm for model discovery and selected-route validation` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion registered by an instance with `modelSelectionSettings`. That instance always exposes optional route fields; Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions, and without an enabled policy, selection and discovery are restricted to the current parent route. The shipped `subagent_fork` exposes no route selection. Each instance selects its backend through `provider`. 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.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. The independent `/list-models` plugin registers discovery. Web and Desktop presets mount discovery, enable spawn route fields, and leave 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-control` | `interrupt_agent`, `list_agents`, `send_message` | `ctx.tools`, `ctx.subagents`, `ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`, `tool/result`, `child session events through ctx.subagents` | - | The globally named discovery, interruption, and local-message tools over managed subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` and `interrupt_agent` once, plus `list_agents` from its separately loaded `/list-agents` plugin (whose catalog rows use the sessionProjections and live Agent registries). | | `@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-experimental-tool-agent-team` | `interrupt_agent`, `list_agents`, `send_message`, `spawn_teammate`, `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update`, `wait_agent` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `an exact live Team member Agent` | `tool/call`, `team/member`, `team/task`, `agent/inbox/spliced`, `user/message on inbox claim`, `tool/result` | - | All nine tools are scoped to implicit Team Leads and durable teammates. The shipped dsh-base bundle keeps the package disabled; the documented Agent Teams profile patch enables it while disabling the legacy continuable-child control names. | @@ -2067,7 +2067,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 registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion registered by an instance with `modelSelectionSettings`. That instance always exposes optional route fields; Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions, and without an enabled policy, selection and discovery are restricted to the current parent route. The shipped `subagent_fork` exposes no route selection. Each instance selects its backend through `provider`. 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. The independent `/list-models` plugin registers discovery. Web and Desktop presets mount discovery, enable spawn route fields, and leave 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. diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index c94fd2a8fc..5231594677 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -41,7 +41,7 @@ | `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`、`ctx.workflowEngine`、`ctx.subagents`、`ctx.systemPrompt`、`a calling Agent (exec.agent parents every fresh round)` | `tool/call`、`tool/result`、`workflow and child session events during execution` | - | 固定的前台工作流会在每个 Round 启动一个全新的结构化子级;模型只能选择不可变目标和可选的 Round 上限。 | | `@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-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述默认 schema 关闭模型选择,而发现 schema 展示为设置了 `modelSelectionSettings` 的实例注册的固定配套工具。该实例始终公开可选路由字段;Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;没有启用策略时,选择和发现仅限当前父级路由。随附的 `subagent_fork` 不公开路由选择。每个实例通过 `provider` 选择后端。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 | +| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 加载时的 `toolName` 选择委派名称(默认为 `subagent`);`modelSelection` 增加可选路由字段。独立的 `/list-models` 插件注册发现工具。Web 与 Desktop preset 挂载发现工具,启用 spawn 路由字段,并为 fork 保持路由字段关闭。Host 设置服务为每个新顶层 Session 记录授权;子 Session 继承该授权。没有已记录策略时,发现和显式选择只允许当前父级路由。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 | | `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`、`list_agents`、`send_message` | `ctx.tools`、`ctx.subagents`、`ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`、`tool/result`、`child session events through ctx.subagents` | - | 这些是用于发现、中断托管 subagent 和向本地子代理发送消息的全局命名工具:绑定提供方的 `tool-subagent` 实例注册不同的委派工具;本包注册一次 `send_message` 和 `interrupt_agent`,另由 `list_agents` 通过单独加载的 `/list-agents` 插件提供,其目录行使用 sessionProjections 和实时 Agent 注册表。 | | `@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-experimental-tool-agent-team` | `interrupt_agent`、`list_agents`、`send_message`、`spawn_teammate`、`team_task_create`、`team_task_get`、`team_task_list`、`team_task_update`、`wait_agent` | `ctx.tools`、`ctx.systemPrompt`、`ctx.agentTeams`、`an exact live Team member Agent` | `tool/call`、`team/member`、`team/task`、`agent/inbox/spliced`、`user/message on inbox claim`、`tool/result` | - | 这 9 个工具限定于隐式 Team Lead 与持久 teammate 作用域。随产品发布的 dsh-base bundle 默认禁用该包;文档中的 Agent Teams profile patch 会启用它,并禁用旧 continuable child 的同名控制工具。 | @@ -2073,7 +2073,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, 来源:[`packages/subagent/tool-subagent/src/index.ts`](../packages/subagent/tool-subagent/src/index.ts) -注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述默认 schema 关闭模型选择,而发现 schema 展示为设置了 `modelSelectionSettings` 的实例注册的固定配套工具。该实例始终公开可选路由字段;Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;没有启用策略时,选择和发现仅限当前父级路由。随附的 `subagent_fork` 不公开路由选择。每个实例通过 `provider` 选择后端。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 +加载时的 `toolName` 选择委派名称(默认为 `subagent`);`modelSelection` 增加可选路由字段。独立的 `/list-models` 插件注册发现工具。Web 与 Desktop preset 挂载发现工具,启用 spawn 路由字段,并为 fork 保持路由字段关闭。Host 设置服务为每个新顶层 Session 记录授权;子 Session 继承该授权。没有已记录策略时,发现和显式选择只允许当前父级路由。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/subagent-model-selection-composition/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.2/subagent-model-selection-composition/guide.i18n.yaml new file mode 100644 index 0000000000..5cc66aa405 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.2/subagent-model-selection-composition/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-alpha.2/subagent-model-selection-composition/guide.md +/: + en: b9bbc14e49aa6bdf + zh: ce5f3d2b0d6a9ff9 +/separate-subagent-model-selection-configuration: + en: c54f686a434d8b84 + zh: f32b63140d9b2dda +/separate-subagent-model-selection-configuration/change: + en: 6773f124a0ff9f6a + zh: 99213e0ba2caff35 +/separate-subagent-model-selection-configuration/migration: + en: b83bd0256fb89c92 + zh: 5b14cf7cfa9677b3 diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/subagent-model-selection-composition/guide.md b/docs/upgrade-guide/v0.2.1-alpha.2/subagent-model-selection-composition/guide.md new file mode 100644 index 0000000000..72411e4bc9 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.2/subagent-model-selection-composition/guide.md @@ -0,0 +1,19 @@ +--- +kind: upgrade-guide +description: Custom subagent compositions must separate route fields, model discovery, and Session authorization. +--- + +# Separate subagent model selection configuration + +English | [中文](guide.zh.md) + +## Change + +`@deepseek-ai/dsh-tool-subagent` replaces `modelSelectionSettings` with `modelSelection`, which controls only optional child route fields. The tool no longer registers `list_subagent_models` or samples user settings. The Host `/model-selection-settings` service records authorization during Agent creation, including Sessions created before delegation tools are mounted. Existing Sessions keep their recorded policy; installing tools later does not resample settings. Shipped Web and Desktop presets include the independent discovery plugin and keep fork route fields disabled. Custom fork tools may enable them. + +## Migration + +1. In custom `cordis.yml`, patches, or direct plugin configuration, rename `modelSelectionSettings: true` to `modelSelection: true` on delegation tools that expose route arguments. +2. Add an independent `@deepseek-ai/dsh-tool-subagent/list-models` row to each tool scope that needs discovery. Multiple delegation tools may share it. +3. Mount `@deepseek-ai/dsh-tool-subagent/model-selection-settings` in the Host before creating Agents. Its required services are `sessions` and `sessionProjections`. Direct `apply(ctx, config)` callers no longer pass a Session argument. +4. Create a fresh Session and verify that discovery returns the authorized routes when the user preference is enabled, or only the parent Agent’s current route otherwise. Verify that an explicit unauthorized delegation fails even without first calling discovery. diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/subagent-model-selection-composition/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.2/subagent-model-selection-composition/guide.zh.md new file mode 100644 index 0000000000..f312728181 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.2/subagent-model-selection-composition/guide.zh.md @@ -0,0 +1,19 @@ +--- +kind: upgrade-guide +description: 自定义 subagent 组合必须分离路由字段、模型发现和 Session 授权。 +--- + +# 分离 subagent 模型选择配置 + +[English](guide.md) | 中文 + +## 变更 + +`@deepseek-ai/dsh-tool-subagent` 将 `modelSelectionSettings` 替换为 `modelSelection`,该选项只控制可选子级路由字段。工具不再注册 `list_subagent_models` 或采样用户设置。Host 的 `/model-selection-settings` 服务在 Agent 创建时记录授权,包括在委派工具挂载前创建的 Session。已有 Session 保留已记录策略;之后安装工具不会重新采样设置。随附 Web 与 Desktop preset 包含独立发现插件,并为 fork 保持路由字段关闭。自定义 fork 工具可以启用这些字段。 + +## 迁移 + +1. 在自定义 `cordis.yml`、patch 或直接插件配置中,将公开路由参数的委派工具的 `modelSelectionSettings: true` 改为 `modelSelection: true`。 +2. 在每个需要发现的工具作用域中,添加独立的 `@deepseek-ai/dsh-tool-subagent/list-models` 配置行。多个委派工具可以共用它。 +3. 在创建 Agent 前,将 `@deepseek-ai/dsh-tool-subagent/model-selection-settings` 挂载到 Host。它需要 `sessions` 和 `sessionProjections` 服务。直接调用 `apply(ctx, config)` 时不再传入 Session 参数。 +4. 创建一个新 Session,确认用户偏好启用时发现结果为授权路由,否则仅为父 Agent 当前路由。确认即使未先调用发现工具,显式选择未授权路由的委派仍会失败。 diff --git a/packages/bundle/web-app/cordis.patch.yml b/packages/bundle/web-app/cordis.patch.yml index a1ca0b5483..540b7650da 100644 --- a/packages/bundle/web-app/cordis.patch.yml +++ b/packages/bundle/web-app/cordis.patch.yml @@ -61,8 +61,8 @@ enabled: true appVersion: !!js process.env.DSH_CLIENT_VERSION - # Host-owned opt-in sampled when a new Web session receives its preset - # delegation tools. The Plugins page edits this settings namespace. + # Host-owned authorization sampled at Agent creation. The Plugins page + # edits this settings namespace independently of the selected preset. - id: subagent-model-selection-settings name: '@deepseek-ai/dsh-tool-subagent/model-selection-settings' diff --git a/packages/bundle/web-app/presets/cordis.patch.yml b/packages/bundle/web-app/presets/cordis.patch.yml index 8c8d567ae3..e586ec9bad 100644 --- a/packages/bundle/web-app/presets/cordis.patch.yml +++ b/packages/bundle/web-app/presets/cordis.patch.yml @@ -89,12 +89,14 @@ name: '@deepseek-ai/dsh-tool-subagent-control' - id: tool-subagent-list-agents name: '@deepseek-ai/dsh-tool-subagent-control/list-agents' + - id: tool-subagent-list-models + name: '@deepseek-ai/dsh-tool-subagent/list-models' - id: tool-subagent name: '@deepseek-ai/dsh-tool-subagent' config: provider: spawn toolName: subagent - modelSelectionSettings: true + modelSelection: true toolFilter: deny: - schedule_create diff --git a/packages/bundle/web-app/presets/ptc.patch.yml b/packages/bundle/web-app/presets/ptc.patch.yml index 8e1ec1fb70..83f3b94b17 100644 --- a/packages/bundle/web-app/presets/ptc.patch.yml +++ b/packages/bundle/web-app/presets/ptc.patch.yml @@ -90,12 +90,14 @@ name: '@deepseek-ai/dsh-tool-subagent-control' - id: tool-subagent-list-agents name: '@deepseek-ai/dsh-tool-subagent-control/list-agents' + - id: tool-subagent-list-models + name: '@deepseek-ai/dsh-tool-subagent/list-models' - id: tool-subagent name: '@deepseek-ai/dsh-tool-subagent' config: provider: spawn toolName: subagent - modelSelectionSettings: true + modelSelection: true toolFilter: deny: - schedule_create diff --git a/packages/bundle/web-app/presets/standard.patch.yml b/packages/bundle/web-app/presets/standard.patch.yml index 6a42d0a351..ef64df96e9 100644 --- a/packages/bundle/web-app/presets/standard.patch.yml +++ b/packages/bundle/web-app/presets/standard.patch.yml @@ -90,12 +90,14 @@ name: '@deepseek-ai/dsh-tool-subagent-control' - id: tool-subagent-list-agents name: '@deepseek-ai/dsh-tool-subagent-control/list-agents' + - id: tool-subagent-list-models + name: '@deepseek-ai/dsh-tool-subagent/list-models' - id: tool-subagent name: '@deepseek-ai/dsh-tool-subagent' config: provider: spawn toolName: subagent - modelSelectionSettings: true + modelSelection: true toolFilter: deny: - schedule_create diff --git a/packages/core/agent-loop/tests/serial-listener-review.spec.ts b/packages/core/agent-loop/tests/serial-listener-review.spec.ts index fcd9f0aabd..818b36520e 100644 --- a/packages/core/agent-loop/tests/serial-listener-review.spec.ts +++ b/packages/core/agent-loop/tests/serial-listener-review.spec.ts @@ -8,11 +8,7 @@ import AgentLoop from '@deepseek-ai/dsh-agent-loop' import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' import { SessionId } from '@deepseek-ai/dsh-session' -import { bindScopeParent, createScope, scopeOf } from '@deepseek-ai/dsh-scope' import LocalFileReference from '@deepseek-ai/dsh-file-reference-local' -import SubagentRuntime from '@deepseek-ai/dsh-subagent' -import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process' -import * as SubagentTool from '@deepseek-ai/dsh-tool-subagent' import Selection from '@deepseek-ai/dsh-tool-subagent/model-selection-settings' import type { Agent } from '@deepseek-ai/dsh-agent' import { createUserMessage } from '@deepseek-ai/dsh-llm' @@ -98,19 +94,12 @@ describe('serial creation listener integrations', () => { } }) - it('rolls back creation when the shared preset cannot capture model-selection policy', async () => { + it('rolls back creation when the Host settings service cannot capture model-selection policy', async () => { const ctx = await core() await ctx.plugin(Selection, { enabled: true, allowedModels: [] }) - await ctx.plugin(SubagentRuntime) - await ctx.plugin(SubagentSpawn, { providerName: 'spawn' }) - const preset = createScope(ctx, { preset: 'review' }) - await preset.ctx.plugin(SubagentTool, { provider: 'spawn', modelSelectionSettings: true }) try { await expect(ctx.agents.create({ sessionId: SessionId('review-tool'), - setup(agentCtx) { - bindScopeParent(scopeOf(agentCtx)!, scopeOf(preset.ctx)!) - }, })).rejects.toThrow('subagent model selection requires at least one allowed model') expect(ctx.agents.list()).toEqual([]) expect(ctx.sessions.list()).toEqual([]) diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 4c2409e280..87e63e1432 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -2900,12 +2900,12 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ }, { key: 'subagentModelSelection', - summary: 'Singleton settings owner read when delegation tools are composed for a Session.', - description: 'Singleton settings owner read when delegation tools are composed for a Session.', + summary: 'Host settings owner that records Session authorization before the first model request.', + description: 'Host settings owner that records Session authorization before the first model request.', methods: [ { signature: 'current(): SubagentModelSelectionSettings', - description: 'Read a detached selection preference for the next eligible Session composition.', + description: 'Read a detached selection preference for the next fresh top-level Session.', parameters: [], returns: 'the enabled state and exact allowed routes.', }, diff --git a/packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/scoped-tool-subagent.ts b/packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/scoped-tool-subagent.ts index cd30075474..71c9b7531c 100644 --- a/packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/scoped-tool-subagent.ts +++ b/packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/scoped-tool-subagent.ts @@ -3,6 +3,7 @@ import type { Context } from '@deepseek-ai/cordis' import type { Agent } from '@deepseek-ai/dsh-agent' import * as ToolSubagent from '@deepseek-ai/dsh-tool-subagent' +import * as ListModels from '@deepseek-ai/dsh-tool-subagent/list-models' import type { Config } from '@deepseek-ai/dsh-tool-subagent' export const name = 'scoped-tool-subagent' @@ -15,16 +16,17 @@ export const inject = ['agents', 'subagentModelSelection'] */ export function apply(ctx: Context, config: Config): void { const install = (agent: Agent): void => { + agent.ctx.plugin(ListModels) agent.ctx.inject(ToolSubagent.inject, (runtimeCtx) => { ToolSubagent.apply(runtimeCtx, { provider: config.provider, - modelSelectionSettings: true, + modelSelection: true, ...(config.toolName === undefined ? {} : { toolName: config.toolName }), ...(config.agentOptions === undefined ? {} : { agentOptions: config.agentOptions }), ...(config.persona === undefined ? {} : { persona: config.persona }), ...(config.toolFilter === undefined ? {} : { toolFilter: config.toolFilter }), ...(config.maxDepth === undefined ? {} : { maxDepth: config.maxDepth }), - }, agent.session) + }) }) } ctx.on('agent/created', ({ agent }) => { install(agent) }) diff --git a/packages/subagent/tool-subagent/README.i18n.yaml b/packages/subagent/tool-subagent/README.i18n.yaml index f02f435211..d5a9d02abb 100644 --- a/packages/subagent/tool-subagent/README.i18n.yaml +++ b/packages/subagent/tool-subagent/README.i18n.yaml @@ -18,20 +18,20 @@ en: 93f1701f5209ecb5 zh: 6310fd012400aead /deepseek-ai-dsh-tool-subagent/use-this-package/minimal-configuration: - en: 5465759f3c7fd5d7 - zh: 3f0b0cba46dbbc08 + en: f59fcf460bf140c9 + zh: e352bb50015b3a0f /deepseek-ai-dsh-tool-subagent/use-this-package/managed-delegation: en: 42673064ba88a211 zh: 8fa26285bd649dc9 /deepseek-ai-dsh-tool-subagent/use-this-package/selecting-a-child-llm: - en: ff7357f058f309ba - zh: 489f05233c1785a7 + en: a13c6fe5ac74be17 + zh: 3e06bb4e9662a263 /deepseek-ai-dsh-tool-subagent/understand-the-implementation: en: 6ce5f749256a7e6c zh: a4f2adb04a70d9cf /deepseek-ai-dsh-tool-subagent/understand-the-implementation/design-concept: - en: 794a71280d73dbc2 - zh: acb6990a599f3f66 + en: 5afcdbd58d405c29 + zh: ec11baf316a4e208 /deepseek-ai-dsh-tool-subagent/understand-the-implementation/activation-ownership: en: ef692c4d17290920 zh: b1d0d3dab011e2cd @@ -54,11 +54,11 @@ en: e4c7376d4f29dbd5 zh: 6b7a9eaf69c9ea53 /deepseek-ai-dsh-tool-subagent/model-experience/tool-schema/what-the-model-sees: - en: 8266f5b60ac830db - zh: 6dceefd04897b6b0 + en: 126ffa0f945dd41a + zh: 26b5e8d700c3447c /deepseek-ai-dsh-tool-subagent/model-experience/tool-schema/token-effect: - en: 86d1dafcb6c00c57 - zh: fe890e9c6146c45e + en: 5df8d3c13b5a4c54 + zh: b2fb1eb115d38ac1 /deepseek-ai-dsh-tool-subagent/model-experience/tool-schema/kv-cache-effect: en: 5697c49cb84fb2a4 zh: b2b8464d3cbabfa5 @@ -66,11 +66,11 @@ en: a4ca31b9cbacda6b zh: 91ac3e0784a14697 /deepseek-ai-dsh-tool-subagent/model-experience/model-selection-and-discovery/what-the-model-sees: - en: d0df5c0c9c988408 - zh: cadaa5591dcc5784 + en: 192670f899eb74f5 + zh: 06eac751268c3a9d /deepseek-ai-dsh-tool-subagent/model-experience/model-selection-and-discovery/token-effect: - en: cdb02d92cc19062e - zh: c466d4592828021f + en: 4fa61dc9a52259a2 + zh: bc51ac3b1a1cc6ad /deepseek-ai-dsh-tool-subagent/model-experience/model-selection-and-discovery/kv-cache-effect: en: 7dc47a34d5574fbd zh: bd7bd2a4da2f8ae2 @@ -114,8 +114,8 @@ en: f5d5f97a153f7c70 zh: 3d6bc05b9eadfbc2 /deepseek-ai-dsh-tool-subagent/known-limitations-and-deferred-work: - en: cbe53030678b9dd4 - zh: 06ccb6828e5708d5 + en: 637b5efa701c25f5 + zh: 5e00b602050780b6 /deepseek-ai-dsh-tool-subagent/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/subagent/tool-subagent/README.md b/packages/subagent/tool-subagent/README.md index b8de1764f4..5e14fb9b42 100644 --- a/packages/subagent/tool-subagent/README.md +++ b/packages/subagent/tool-subagent/README.md @@ -46,7 +46,7 @@ Load the subagent service, an in-process or remote backend, and this tool; then |---|---|---| | `provider` | required | Provider name on `ctx.subagents` (e.g. `spawn`, `fork`, `acp`) | | `toolName` | `subagent` | Model-facing tool name; distinct for every loaded instance | -| `modelSelectionSettings` | `false` | Expose child route fields and `list_subagent_models`, and sample the Host's exact-route authorization preference for each top-level Session; a standing preset observes matching Sessions, while direct Agent setup passes its Session explicitly; requires provider `agentOptions` support | +| `modelSelection` | `false` | Expose optional child route fields; requires provider `agentOptions` support | | `agentOptions` | — | Configured child `provider`, `model`, adapter-owned `reasoningEffort`, and positive `maxTokens` defaults; requires provider `agentOptions` support and overlays any provider-owned route defaults | | `persona` | — | Per-child persona; requires the provider's `persona` capability | | `toolFilter` | — | Per-child global-tool restriction; requires the `toolFilter` capability | @@ -64,7 +64,7 @@ Every backend’s completion notice carries its final answer. Local Spawn and Fo ### Selecting a child LLM -Set `modelSelectionSettings: true` to expose optional `provider`, `model`, and `reasoning_effort` fields and register `list_subagent_models` with this instance; instances without it expose neither and reject selection fields. The instance samples the Host’s `subagent-model-selection` preference for fresh top-level Sessions. When enabled, its exact route list is recorded in the Session, inherited by child Sessions, and preserved on restore; later settings edits affect only new Sessions. Without a recorded Session policy, discovery and explicit selection permit only the parent Agent’s current effective provider/model and its supported efforts. This mode requires a backend that advertises `agentOptions`; in-process Spawn and Fork and DSH SDK qualify, while ACP, Codex, and Claude Code fail the mount. +Set `modelSelection: true` to expose optional `provider`, `model`, and `reasoning_effort` fields; instances without it reject selection fields. Mount `@deepseek-ai/dsh-tool-subagent/list-models` independently to expose `list_subagent_models`. Web and Desktop presets mount discovery by default, enable route fields for spawn, and leave them disabled for fork. The Host’s `/model-selection-settings` service samples the user preference during Agent creation, independently of tool installation. When enabled, its exact route list is recorded in each fresh top-level Session, inherited by child Sessions, and preserved on restore; later settings edits affect only new Sessions. Without a recorded Session policy, discovery and explicit selection permit only the parent Agent’s current effective provider/model and its supported efforts. Route fields require a backend that advertises `agentOptions`; in-process Spawn and Fork and DSH SDK qualify, while ACP, Codex, and Claude Code fail the mount. A call supplies `provider` and `model` together, or supplies only an effort when configured, parent, or provider-owned defaults provide the route. Static `provider.agentRouteDefaults`, when present, form the provider/model baseline; tool configuration and model fields overlay it before route-aware effort merging and exact-route preflight. Providers without these defaults use compatible values from the parent's latest logged request, then the parent's creation options before its first request, while retaining the configured `maxTokens`. Changing the route without an explicit effort clears the inherited route-owned effort, so the selected model resolves its default. The live LLM adapter validates the effective route before child creation. Catalog membership remains advisory, so a model can use an unlisted id when its adapter accepts it. @@ -80,7 +80,7 @@ This section explains how the tool mirrors provider lifecycle and settles runs; ### Design concept -One instance is one provider plus one tool name. The plugin registers its definition in the owning composition while the provider is present and disposes it when the provider leaves. Agents inherit that shared definition, so their existing tool filters apply without per-Agent registration. Settings-backed instances also register `list_subagent_models` in the same composition and capture Session authorization during Agent creation; calls read the durable policy independently of registration. Direct Agent setup supplies its unpublished Session explicitly. A numeric `maxDepth` or configured LLM selection unsupported by the provider fails the mount. At most one instance in a tool scope may set `modelSelectionSettings` because `list_subagent_models` has a global name. No runtime invariant companion is published: policy comes directly from the Session projection and is checked at execution. +One instance is one provider plus one tool name. The plugin registers its definition in the owning composition while the provider is present and disposes it when the provider leaves. Agents inherit that shared definition, so their existing tool filters apply without per-Agent registration. Discovery is a separate plugin and reads the same durable Session policy as the delegation executor. A numeric `maxDepth` or configured LLM selection unsupported by the provider fails the mount. Multiple distinctly named delegation tools may expose model selection in one scope. ### Activation ownership @@ -128,11 +128,11 @@ Read these pages when the package-level contract is not enough; they move from t #### What the model sees -The generated default [`subagent` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent) appears under the configured tool name while its provider exists. An instance with `modelSelectionSettings` adds `provider`, `model`, and `reasoning_effort` plus route guidance in every Session. Context inheritance and continuation support determine task and follow-up descriptions. A tool restriction removes both the schema and its `tool:` guidance section. +The generated default [`subagent` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-subagent) appears under the configured tool name while its provider exists. An instance with `modelSelection` adds `provider`, `model`, and `reasoning_effort` plus route guidance in every Session. Context inheritance and continuation support determine task and follow-up descriptions. A tool restriction removes both the schema and its `tool:` guidance section. #### Token effect -Fixed schema cost per parent request; `modelSelectionSettings` adds three optional route fields. Each tool instance adds one schema; visible delegation tools share one short system-prompt section. +Fixed schema cost per parent request; `modelSelection` adds three optional route fields. Each tool instance adds one schema; visible delegation tools share one short system-prompt section. #### KV Cache effect @@ -142,11 +142,11 @@ Prefix-stable while provider instances and their configuration are unchanged. Ad #### What the model sees -The `list_subagent_models` definition registered by a settings-backed instance reads the calling Session’s route policy, falling back to the current parent route when no policy is recorded. Calls reject while the optional `ctx.llm` service is unavailable. With a recorded policy, provider model lists intersect the advertised catalog with the Session’s authorized routes. Without one, they resolve only the current parent model, even if the catalog omits it. Exact-model queries accept authorized ids absent from the catalog; unauthorized provider/model lookups fail before adapter access. Catalog or exact-model lookup failures and cancellation reject the call. Execution independently enforces the same policy. +The independent `list_subagent_models` definition reads the calling Session’s route policy, falling back to the current parent route when no policy is recorded. Calls reject while the optional `ctx.llm` service is unavailable. With a recorded policy, provider model lists intersect the advertised catalog with the Session’s authorized routes. Without one, they resolve only the current parent model, even if the catalog omits it. Exact-model queries accept authorized ids absent from the catalog; unauthorized provider/model lookups fail before adapter access. Catalog or exact-model lookup failures and cancellation reject the call. Execution independently enforces the same policy. #### Token effect -One fixed discovery schema is present in compositions with `modelSelectionSettings`, regardless of the Session’s preference; the shipped Web presets include it, while headless and SDK profiles do not. Route metadata enters the transcript only when the model calls the tool. +One fixed discovery schema is present wherever `/list-models` is mounted, regardless of the Session’s preference or delegation tools; the shipped Web and Desktop presets include it, while headless and SDK profiles do not. Route metadata enters the transcript only when the model calls the tool. #### KV Cache effect @@ -208,8 +208,8 @@ Append-only; newly visible content follows the reusable request prefix and does These limits define what this tool does not return or enforce; they are current package constraints. - **This tool does not wait for results** — use the completion notice or child messages. Programmatic result waiting belongs to the subagent service. -- **Shipped fork tools omit model selection** — their configuration leaves `modelSelectionSettings` disabled to preserve inherited-prefix reuse. Custom fork tools may enable it under the same Session authorization and adapter validation as other delegation tools. -- **Non-routing child policy is fixed per instance** — another persona, tool filter, or depth cap requires another distinctly named tool. Explicit LLM selection requires `modelSelectionSettings` and is restricted to the Session’s authorized routes, or the current parent route without an enabled policy. +- **Shipped fork tools omit model selection** — their configuration leaves `modelSelection` disabled to preserve inherited-prefix reuse. Custom fork tools may enable it under the same Session authorization and adapter validation as other delegation tools. +- **Non-routing child policy is fixed per instance** — another persona, tool filter, or depth cap requires another distinctly named tool. Explicit LLM selection requires `modelSelection` and is restricted to the Session’s authorized routes, or the current parent route without an enabled policy. ### Dev Note diff --git a/packages/subagent/tool-subagent/README.zh.md b/packages/subagent/tool-subagent/README.zh.md index 575b9b4530..e8d19608f2 100644 --- a/packages/subagent/tool-subagent/README.zh.md +++ b/packages/subagent/tool-subagent/README.zh.md @@ -46,7 +46,7 @@ kind: "package-reference" |---|---|---| | `provider` | 必填 | `ctx.subagents` 上的提供方名称(如 `spawn`、`fork`、`acp`) | | `toolName` | `subagent` | 面向模型的工具名称;每个已加载实例必须不同 | -| `modelSelectionSettings` | `false` | 公开子级路由字段和 `list_subagent_models`,并为每个顶层 Session 读取宿主的精确路由授权偏好;常驻 preset 观察匹配 Session,直接 Agent setup 则显式传入其 Session;要求提供方支持 `agentOptions` | +| `modelSelection` | `false` | 公开可选子级路由字段;要求提供方支持 `agentOptions` | | `agentOptions` | — | 配置的子级 `provider`、`model`、适配器所有的 `reasoningEffort` 与正整数 `maxTokens` 默认值;要求提供方支持 `agentOptions`,并会覆盖提供方持有的路由默认值 | | `persona` | — | 每个子 agent 独立的 persona;要求提供方具备 `persona` 能力 | | `toolFilter` | — | 每个子 agent 独立的全局工具限制;要求提供方具备 `toolFilter` 能力 | @@ -64,7 +64,7 @@ kind: "package-reference" ### 选择子级 LLM -设置 `modelSelectionSettings: true` 后,该实例公开可选的 `provider`、`model` 和 `reasoning_effort` 字段,并注册 `list_subagent_models`;未设置的实例两者都不公开,并拒绝选择字段。该实例为新顶层 Session 读取 Host 的 `subagent-model-selection` 偏好。启用时,精确路由列表记录在 Session 中,由子 Session 继承,并在恢复时保留;之后的设置编辑只影响新 Session。没有已记录 Session 策略时,发现和显式选择只允许父 Agent 当前生效的 provider/model 及其支持的强度。此模式要求后端公布 `agentOptions`;进程内 Spawn、Fork 和 DSH SDK 满足条件,ACP、Codex 和 Claude Code 会使挂载失败。 +设置 `modelSelection: true` 后,该实例公开可选的 `provider`、`model` 和 `reasoning_effort` 字段;未设置的实例拒绝选择字段。独立挂载 `@deepseek-ai/dsh-tool-subagent/list-models` 以公开 `list_subagent_models`。Web 与 Desktop preset 默认挂载发现工具,为 spawn 启用路由字段,为 fork 保持关闭。Host 的 `/model-selection-settings` 服务在 Agent 创建时读取用户偏好,与工具安装无关。启用时,精确路由列表记录在每个新顶层 Session 中,由子 Session 继承,并在恢复时保留;之后的设置编辑只影响新 Session。没有已记录 Session 策略时,发现和显式选择只允许父 Agent 当前生效的 provider/model 及其支持的强度。路由字段要求后端公布 `agentOptions`;进程内 Spawn、Fork 和 DSH SDK 满足条件,ACP、Codex 和 Claude Code 会使挂载失败。 一次调用需同时提供 `provider` 与 `model`;当配置值、父 agent 值或提供方持有的默认值能提供路由时,也可只提供推理等级。静态的 `provider.agentRouteDefaults` 在存在时构成提供方/模型基线;工具配置与模型字段会在路由相关强度合并和确切路由预检前覆盖它。没有这些默认值的提供方会使用父 agent 最新已记录请求中的兼容值,再使用父级首次请求前的创建选项,并保留配置的 `maxTokens`。更改路由但未显式提供推理等级时,会清除继承的路由自有等级,使所选模型解析自己的默认值。实时 LLM 适配器在创建子 agent 前校验有效路由。目录成员资格只提供建议,因此适配器接受时,模型可以使用未列出的 id。 @@ -80,7 +80,7 @@ kind: "package-reference" ### 设计理念 -一个实例对应一个提供方和一个工具名。插件在提供方存在时向所属组合注册定义,并在提供方离开时释放它。Agent 继承这份共享定义,因此现有工具过滤器即可生效,无需逐 Agent 注册。读取设置的实例还会在同一组合中注册 `list_subagent_models`,并在 Agent 创建时记录 Session 授权;调用时独立于注册读取持久策略。直接配置 Agent 时显式传入尚未发布的 Session。提供方不支持的数字 `maxDepth` 或配置的 LLM 选择会使挂载失败。由于 `list_subagent_models` 使用全局名称,一个工具作用域中最多只有一个实例可以设置 `modelSelectionSettings`。本包不导出运行时不变量:策略直接来自 Session 投影,并在执行时校验。 +一个实例对应一个提供方和一个工具名。插件在提供方存在时向所属组合注册定义,并在提供方离开时释放它。Agent 继承这份共享定义,因此现有工具过滤器即可生效,无需逐 Agent 注册。发现工具是独立插件,与委派执行器读取同一份持久 Session 策略。提供方不支持的数字 `maxDepth` 或配置的 LLM 选择会使挂载失败。同一作用域中可以有多个名称不同的委派工具公开模型选择。 ### Activation 所有权 @@ -128,11 +128,11 @@ kind: "package-reference" #### 模型看到什么 -生成的默认 [`subagent` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-subagent) 在提供方存在时以配置的工具名公开。设置了 `modelSelectionSettings` 的实例在每个 Session 中都会增加 `provider`、`model`、`reasoning_effort` 及路由指导。上下文继承和继续执行能力决定任务与后续工作描述。工具限制同时移除 schema 及其 `tool:` 指导段。 +生成的默认 [`subagent` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-subagent) 在提供方存在时以配置的工具名公开。设置了 `modelSelection` 的实例在每个 Session 中都会增加 `provider`、`model`、`reasoning_effort` 及路由指导。上下文继承和继续执行能力决定任务与后续工作描述。工具限制同时移除 schema 及其 `tool:` 指导段。 #### Token 影响 -每次父级请求承担固定 schema 成本;`modelSelectionSettings` 会增加三个可选路由字段。每个工具实例增加一份 schema;可见委派工具共享一个简短系统提示词段。 +每次父级请求承担固定 schema 成本;`modelSelection` 会增加三个可选路由字段。每个工具实例增加一份 schema;可见委派工具共享一个简短系统提示词段。 #### KV Cache 影响 @@ -142,11 +142,11 @@ kind: "package-reference" #### 模型看到什么 -读取设置的实例注册的 `list_subagent_models` 定义读取调用 Session 的路由策略,没有已记录策略时使用当前父级路由。可选的 `ctx.llm` 服务不可用时,调用会被拒绝。有已记录策略时,提供方模型列表取公布目录与 Session 授权路由的交集。没有策略时,只解析当前父级模型,即使目录未列出它。精确模型查询允许已授权但目录未列出的 ID;未授权的提供方/模型查询会在访问适配器前失败。目录或精确模型查询失败以及取消都会使调用被拒绝。执行过程独立强制同一策略。 +独立的 `list_subagent_models` 定义读取调用 Session 的路由策略,没有已记录策略时使用当前父级路由。可选的 `ctx.llm` 服务不可用时,调用会被拒绝。有已记录策略时,提供方模型列表取公布目录与 Session 授权路由的交集。没有策略时,只解析当前父级模型,即使目录未列出它。精确模型查询允许已授权但目录未列出的 ID;未授权的提供方/模型查询会在访问适配器前失败。目录或精确模型查询失败以及取消都会使调用被拒绝。执行过程独立强制同一策略。 #### Token 影响 -设置了 `modelSelectionSettings` 的组合中存在一份固定发现 schema,不受 Session 偏好影响;随附的 Web preset 包含它,headless 与 SDK profile 不包含。只有模型调用工具时,路由元数据才进入 transcript。 +挂载 `/list-models` 的组合中存在一份固定发现 schema,不受 Session 偏好或委派工具影响;随附 Web 与 Desktop preset 包含它,headless 与 SDK profile 不包含。只有模型调用工具时,路由元数据才进入 transcript。 #### KV Cache 影响 @@ -208,8 +208,8 @@ Start independent delegations with `subagent` or `subagent_fork` together in one 这些限制说明本工具不返回或不强制执行什么;它们是当前包约束。 - **本工具不等待结果**——使用完成通知或子任务消息。程序化结果等待由 subagent 服务提供。 -- **随附 fork 工具省略模型选择**:其配置保持 `modelSelectionSettings` 关闭,以保留对继承前缀的复用。自定义 fork 工具可以启用它,并接受与其他委派工具相同的 Session 授权和适配器校验。 -- **非路由子级策略按实例固定** — 更换角色、工具过滤器或深度上限,需要另一个不同名称的工具。显式 LLM 选择需要 `modelSelectionSettings`,并受 Session 授权路由限制,没有启用策略时只允许当前父级路由。 +- **随附 fork 工具省略模型选择**:其配置保持 `modelSelection` 关闭,以保留对继承前缀的复用。自定义 fork 工具可以启用它,并接受与其他委派工具相同的 Session 授权和适配器校验。 +- **非路由子级策略按实例固定** — 更换角色、工具过滤器或深度上限,需要另一个不同名称的工具。显式 LLM 选择需要 `modelSelection`,并受 Session 授权路由限制,没有启用策略时只允许当前父级路由。 ### 开发备注 diff --git a/packages/subagent/tool-subagent/package.json b/packages/subagent/tool-subagent/package.json index a535e39446..00f6896dc0 100644 --- a/packages/subagent/tool-subagent/package.json +++ b/packages/subagent/tool-subagent/package.json @@ -25,13 +25,18 @@ "./src/*": "./src/*", "./package.json": "./package.json", "./locale/*.json": "./locale/*.json", - "./model-selection-settings/locale/*.json": "./locale/model-selection-settings/*.json" + "./model-selection-settings/locale/*.json": "./locale/model-selection-settings/*.json", + "./list-models": { + "types": "./lib/types/list-models.d.ts", + "default": "./lib/list-models.js" + } }, "files": [ "locale/*.json", "locale/model-selection-settings/*.json", "lib/index.js", "lib/model-selection-settings.js", + "lib/list-models.js", "lib/types/**/*.d.ts" ], "license": "MIT", diff --git a/packages/subagent/tool-subagent/src/index.ts b/packages/subagent/tool-subagent/src/index.ts index d07e4aa883..e6185ce580 100644 --- a/packages/subagent/tool-subagent/src/index.ts +++ b/packages/subagent/tool-subagent/src/index.ts @@ -8,14 +8,11 @@ import type { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' -import { scopeChainOf, scopeOf } from '@deepseek-ai/dsh-scope' import { defineTool } from '@deepseek-ai/dsh-tools' import type { ToolDefinition } from '@deepseek-ai/dsh-tools' import type { AgentOptions } from '@deepseek-ai/dsh-agent' import { ReasoningEffortId } from '@deepseek-ai/dsh-llm' import type { ContentBlock } from '@deepseek-ai/dsh-llm' -import { SessionSeq } from '@deepseek-ai/dsh-session' -import type { Session } from '@deepseek-ai/dsh-session' import { assertSubagentMaxDepth, parentAgentOptionsForDelegation, @@ -29,10 +26,7 @@ import { requestedAgentOptions, } from './model-selection.ts' import type { DelegationModelRequest } from './model-selection.ts' -import { registerListSubagentModels } from './list-models.ts' -import type {} from './model-selection-settings.ts' import { - recordSubagentModelSelection, subagentModelSelectionProjectionDefinition, subagentModelSelectionPolicy, } from './model-selection-state.ts' @@ -54,13 +48,11 @@ export interface Config { */ toolName?: string /** - * Expose optional child `provider`, `model`, and `reasoning_effort` fields - * plus `list_subagent_models`, and sample the Host `subagent-model-selection` - * setting for each new top-level Session; child Sessions inherit that - * decision. Requires a provider with `agentOptions` support. - * At most one instance per tool scope may enable it. + * Expose optional child `provider`, `model`, and `reasoning_effort` fields. + * Requires provider `agentOptions` support. Execution enforces the Session + * policy, or restricts explicit selection to the current parent route. */ - modelSelectionSettings?: boolean + modelSelection?: boolean /** * Agent options applied to every child; omitted fields use child-loop defaults. */ @@ -97,7 +89,7 @@ export interface Config { export const Config: z = z.object({ provider: z.string().required(), toolName: z.string().default('subagent'), - modelSelectionSettings: z.boolean().default(false), + modelSelection: z.boolean().default(false), // Prevent Schemastery from materializing omitted agentOptions as `{}`. agentOptions: z.object({ provider: z.string(), @@ -161,9 +153,8 @@ function providerWording(inheritsConversation: boolean): { description: string; * Install one delegation-tool composition. * @param ctx - Context that owns the registrations. * @param config - delegation-tool configuration. - * @param session - unpublished Session supplied by a direct Agent setup; omit for a standing composition. */ -export function apply(ctx: Context, config: Config, session?: Session): void { +export function apply(ctx: Context, config: Config): void { // Direct apply() bypasses Schemastery's numeric constraints. A direct-apply // omission stays capless (the schema default only runs through the loader). if (config.maxDepth !== 'provider-managed') assertSubagentMaxDepth(config.maxDepth) @@ -173,7 +164,7 @@ export function apply(ctx: Context, config: Config, session?: Session): void { } const toolName = config.toolName ?? 'subagent' - const modelSelectionEnabled = config.modelSelectionSettings === true + const modelSelectionEnabled = config.modelSelection === true ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition) const assertSubagentProviderConfiguration = (subagentProvider: SubagentProvider): void => { @@ -203,253 +194,173 @@ export function apply(ctx: Context, config: Config, session?: Session): void { const initialProvider = ctx.subagents.getProvider(config.provider) if (initialProvider !== undefined) assertSubagentProviderConfiguration(initialProvider) - const install = (runtimeCtx: Context): void => { - if (modelSelectionEnabled) { - registerListSubagentModels(runtimeCtx, (exec) => { - if (exec.agent === undefined) throw new Error('list_subagent_models requires a calling agent') - const routes = subagentModelSelectionPolicy(runtimeCtx.sessionProjections, exec.agent.session) - return routes === undefined ? undefined : { routes } - }) - } - // Load order and HMR replacement can change provider availability while - // this fiber remains active. - let mounted: { subagentProvider: SubagentProvider; disposeTool: () => void } | undefined - const mount = (subagentProvider: SubagentProvider): void => { - assertSubagentProviderConfiguration(subagentProvider) - const wording = providerWording(subagentProvider.inheritsParentContext) - const providerRouteDefaults = subagentProvider.agentRouteDefaults - const selectionDescription = providerRouteDefaults !== undefined - ? ' Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to this backend\'s route defaults. When the effective route changes, an omitted `reasoning_effort` uses the model\'s default.' - : ' Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model\'s default.' - const definition = defineTool({ - name: toolName, - description: wording.description - + ' This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes.' - + (subagentProvider.prepareContinuable !== undefined - ? ' The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.' - : ' The completion notice includes its final answer. This backend does not accept follow-up messages.') - + (modelSelectionEnabled ? selectionDescription : ''), - parameters: { - cwd: { - type: 'string', - description: 'Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent.', + // Load order and HMR replacement can change provider availability while + // this fiber remains active. + let mounted: { subagentProvider: SubagentProvider; disposeTool: () => void } | undefined + const mount = (subagentProvider: SubagentProvider): void => { + assertSubagentProviderConfiguration(subagentProvider) + const wording = providerWording(subagentProvider.inheritsParentContext) + const providerRouteDefaults = subagentProvider.agentRouteDefaults + const selectionDescription = providerRouteDefaults !== undefined + ? ' Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to this backend\'s route defaults. When the effective route changes, an omitted `reasoning_effort` uses the model\'s default.' + : ' Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model\'s default.' + const definition = defineTool({ + name: toolName, + description: wording.description + + ' This tool starts an independently managed subagent and immediately returns its id. The runtime notifies you when it finishes.' + + (subagentProvider.prepareContinuable !== undefined + ? ' The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.' + : ' The completion notice includes its final answer. This backend does not accept follow-up messages.') + + (modelSelectionEnabled ? selectionDescription : ''), + parameters: { + cwd: { + type: 'string', + description: 'Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent.', + }, + description: { + type: 'string', + required: true, + description: 'A short (3-5 word) description of the delegated task, for display.', + }, + prompt: { + type: 'string', + required: true, + description: wording.promptDescription, + }, + ...modelSelectionEnabled ? { + provider: { + type: 'string' as const, + description: 'Child LLM provider id.', }, - description: { - type: 'string', - required: true, - description: 'A short (3-5 word) description of the delegated task, for display.', + model: { + type: 'string' as const, + description: 'Model id within that provider.', }, - prompt: { - type: 'string', - required: true, - description: wording.promptDescription, + reasoning_effort: { + type: 'string' as const, + description: 'Model-specific reasoning effort id.', }, - ...modelSelectionEnabled ? { - provider: { - type: 'string' as const, - description: 'Child LLM provider id.', - }, - model: { - type: 'string' as const, - description: 'Model id within that provider.', - }, - reasoning_effort: { - type: 'string' as const, - description: 'Model-specific reasoning effort id.', - }, - } : {}, + } : {}, - }, - output: { - schema: { - type: 'object', - additionalProperties: false, - properties: { - kind: { type: 'string', required: true, const: 'activation' }, - subagentId: { type: 'string', required: true }, - }, + }, + output: { + schema: { + type: 'object', + additionalProperties: false, + properties: { + kind: { type: 'string', required: true, const: 'activation' }, + subagentId: { type: 'string', required: true }, }, - render: (_args, value) => [{ type: 'text', text: `started subagent ${value.subagentId}` }], }, - isConcurrencySafe: () => true, - async execute(args, exec) { - const parent = exec.agent - if (!parent) { - // Non-agent callers provide no parent for delegation ownership. - throw new Error('subagent tool requires a calling agent (exec.agent was undefined)') + render: (_args, value) => [{ type: 'text', text: `started subagent ${value.subagentId}` }], + }, + isConcurrencySafe: () => true, + async execute(args, exec) { + const parent = exec.agent + if (!parent) { + // Non-agent callers provide no parent for delegation ownership. + throw new Error('subagent tool requires a calling agent (exec.agent was undefined)') + } + + const routes = subagentModelSelectionPolicy(ctx.sessionProjections, parent.session) + const modelSelectionPolicy = routes === undefined ? undefined : { routes } + const modelRequest = args as DelegationModelRequest + const parentOptions = parentAgentOptionsForDelegation(parent) + const requiresRoutePreflight = hasDelegationModelRequest(modelRequest) + || hasConfiguredLlmSelection(config.agentOptions) + const configuredChildAgentOptions = requiresRoutePreflight && providerRouteDefaults !== undefined + ? { ...providerRouteDefaults, ...config.agentOptions } + : config.agentOptions + const requestedChildAgentOptions = requestedAgentOptions( + parentOptions, + configuredChildAgentOptions, + modelRequest, + modelSelectionEnabled, + ) + assertAllowedModelSelection( + modelSelectionPolicy, + parentOptions, + requestedChildAgentOptions, + modelRequest, + ) + if (requiresRoutePreflight) { + const llm = ctx.get('llm') + if (llm === undefined) { + throw new Error('cannot resolve the selected child LLM route because the `llm` service is unavailable') } - - const routes = subagentModelSelectionPolicy(runtimeCtx.sessionProjections, parent.session) - const modelSelectionPolicy = routes === undefined ? undefined : { routes } - const modelRequest = args as DelegationModelRequest - const parentOptions = parentAgentOptionsForDelegation(parent) - const requiresRoutePreflight = hasDelegationModelRequest(modelRequest) - || hasConfiguredLlmSelection(config.agentOptions) - const configuredChildAgentOptions = requiresRoutePreflight && providerRouteDefaults !== undefined - ? { ...providerRouteDefaults, ...config.agentOptions } - : config.agentOptions - const requestedChildAgentOptions = requestedAgentOptions( - parentOptions, - configuredChildAgentOptions, - modelRequest, - modelSelectionEnabled, - ) - assertAllowedModelSelection( - modelSelectionPolicy, + await preflightChildLlmRoute( + llm, parentOptions, requestedChildAgentOptions, - modelRequest, + exec.signal, + providerRouteDefaults === undefined, ) - if (requiresRoutePreflight) { - const llm = runtimeCtx.get('llm') - if (llm === undefined) { - throw new Error('cannot resolve the selected child LLM route because the `llm` service is unavailable') - } - await preflightChildLlmRoute( - llm, - parentOptions, - requestedChildAgentOptions, - exec.signal, - providerRouteDefaults === undefined, - ) - if (runtimeCtx.subagents.getProvider(config.provider) !== subagentProvider) { - throw new Error(`subagent provider "${config.provider}" changed while resolving the child LLM route; retry the delegation`) - } + if (ctx.subagents.getProvider(config.provider) !== subagentProvider) { + throw new Error(`subagent provider "${config.provider}" changed while resolving the child LLM route; retry the delegation`) } - exec.signal.throwIfAborted() - const maxDepth = runtimeCtx.subagents.resolveMaxDepth(config.maxDepth) - const request = { - ...args.cwd === undefined ? {} : { cwd: args.cwd }, - prompt: [{ type: 'text', text: args.prompt }] as ContentBlock[], - parent, - ...requestedChildAgentOptions !== undefined ? { agentOptions: requestedChildAgentOptions } : {}, - ...config.persona !== undefined ? { persona: config.persona } : {}, - ...config.toolFilter !== undefined ? { toolFilter: config.toolFilter } : {}, - ...maxDepth !== undefined ? { maxDepth } : {}, - } - - const started = await runtimeCtx.subagents.startActivation({ - provider: config.provider, - label: args.description, - request, - signal: exec.signal, - delivery: 'parent', - }) - return { kind: 'activation' as const, subagentId: started.childId } - }, - }) - const disposeTool = runtimeCtx.effect(() => { - const unregister = runtimeCtx.tools.register(definition) - delegationTools.add(definition) - return () => { - delegationTools.delete(definition) - unregister() } - }) - // oxlint-disable-next-line typescript/no-misused-promises -- Tool and guidance disposal are synchronous. - mounted = { subagentProvider, disposeTool } - } + exec.signal.throwIfAborted() + const maxDepth = ctx.subagents.resolveMaxDepth(config.maxDepth) + const request = { + ...args.cwd === undefined ? {} : { cwd: args.cwd }, + prompt: [{ type: 'text', text: args.prompt }] as ContentBlock[], + parent, + ...requestedChildAgentOptions !== undefined ? { agentOptions: requestedChildAgentOptions } : {}, + ...config.persona !== undefined ? { persona: config.persona } : {}, + ...config.toolFilter !== undefined ? { toolFilter: config.toolFilter } : {}, + ...maxDepth !== undefined ? { maxDepth } : {}, + } - // Register listeners before checking presence so no synchronous change is missed. - runtimeCtx.on('subagent/provider-added', (subagentProvider) => { - if (subagentProvider.name === config.provider && mounted === undefined) mount(subagentProvider) - }) - runtimeCtx.on('subagent/provider-removed', (name) => { - if (name !== config.provider || mounted === undefined) return - mounted.disposeTool() - mounted = undefined - }) - const present = runtimeCtx.subagents.getProvider(config.provider) - if (present !== undefined) { - mount(present) - } else { - // A backend fiber may activate later; a misspelled provider remains visible in this log. - runtimeCtx.logger.info(`subagent provider "${config.provider}" not registered yet; the "${config.toolName ?? 'subagent'}" tool will register when it appears`) - } - runtimeCtx.systemPrompt.section({ - name: `tool:${toolName}`, - order: runtimeCtx.systemPrompt.getSectionOrder('TOOL_SUBAGENT'), - text: (context) => { - if (mounted === undefined) return '' - const visible = [...delegationTools] - .filter(tool => runtimeCtx.tools.get(tool.name, context.scope) === tool) - .map(tool => tool.name) - .sort() - if (visible[0] !== toolName) return '' - const names = visible.map(name => `\`${name}\``).join(' or ') - return `Start independent delegations with ${names} together in one assistant message and continue useful work while they run.` + const started = await ctx.subagents.startActivation({ + provider: config.provider, + label: args.description, + request, + signal: exec.signal, + delivery: 'parent', + }) + return { kind: 'activation' as const, subagentId: started.childId } }, }) - } - - if (config.modelSelectionSettings !== true) { - install(ctx) - return - } - - const settings = ctx.get('subagentModelSelection') - if (settings === undefined) { - throw new Error( - 'tool-subagent: `modelSelectionSettings` requires ' - + '@deepseek-ai/dsh-tool-subagent/model-selection-settings in the Host scope', - ) - } - const selectForSession = (target: Session): void => { - const freshSession = target.firstLiveSeq === 0 - // oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred. - && target.eventAt(SessionSeq(0))?.type !== 'session/end-seed' - let allowedModels = subagentModelSelectionPolicy(ctx.sessionProjections, target) - if (allowedModels === undefined) { - const parentId = target.header.origin === 'subagent' - ? target.header.parentSession - : undefined - if (parentId !== undefined) { - const sessions = ctx.get('sessions') - if (sessions === undefined) { - throw new Error('tool-subagent: child model-selection inheritance requires the Session registry') - } - const parent = sessions.get(parentId) - allowedModels = parent === undefined - ? undefined - : subagentModelSelectionPolicy(ctx.sessionProjections, parent) - } else if (freshSession) { - const current = settings.current() - allowedModels = current.enabled ? current.allowedModels : undefined + const disposeTool = ctx.effect(() => { + const unregister = ctx.tools.register(definition) + delegationTools.add(definition) + return () => { + delegationTools.delete(definition) + unregister() } - } - if (allowedModels !== undefined) { - recordSubagentModelSelection(ctx.sessionProjections, target, allowedModels) - } + }) + // oxlint-disable-next-line typescript/no-misused-promises -- Tool and guidance disposal are synchronous. + mounted = { subagentProvider, disposeTool } } - if (session !== undefined) { - selectForSession(session) - install(ctx) - return + // Register listeners before checking presence so no synchronous change is missed. + ctx.on('subagent/provider-added', (subagentProvider) => { + if (subagentProvider.name === config.provider && mounted === undefined) mount(subagentProvider) + }) + ctx.on('subagent/provider-removed', (name) => { + if (name !== config.provider || mounted === undefined) return + mounted.disposeTool() + mounted = undefined + }) + const present = ctx.subagents.getProvider(config.provider) + if (present !== undefined) { + mount(present) + } else { + // A backend fiber may activate later; a misspelled provider remains visible in this log. + ctx.logger.info(`subagent provider "${config.provider}" not registered yet; the "${config.toolName ?? 'subagent'}" tool will register when it appears`) } - - const compositionScope = scopeOf(ctx) - if (compositionScope === undefined) { - throw new Error('tool-subagent: standing `modelSelectionSettings` requires a scoped preset Context') - } - const agents = ctx.get('agents') - /* v8 ignore next -- shipped preset compositions always include the Agent registry. */ - if (agents === undefined) throw new Error('tool-subagent: standing `modelSelectionSettings` requires the Agent registry') - const captured = new WeakSet() - const capture = (target: Session): void => { - if (captured.has(target)) return - selectForSession(target) - captured.add(target) - } - const captureComposedSessions = (): void => { - for (const agent of agents.list()) { - if (scopeChainOf(scopeOf(agent.ctx)).includes(compositionScope)) capture(agent.session) - } - } - ctx.on('agent/created', ({ agent }) => { capture(agent.session) }) - // Blank Sessions can join a preset after creation; absence also records the - // sampled off decision for this plugin lifetime. - ctx.on('tools/change', captureComposedSessions) - captureComposedSessions() - install(ctx) + ctx.systemPrompt.section({ + name: `tool:${toolName}`, + order: ctx.systemPrompt.getSectionOrder('TOOL_SUBAGENT'), + text: (context) => { + if (mounted === undefined) return '' + const visible = [...delegationTools] + .filter(tool => ctx.tools.get(tool.name, context.scope) === tool) + .map(tool => tool.name) + .sort() + if (visible[0] !== toolName) return '' + const names = visible.map(name => `\`${name}\``).join(' or ') + return `Start independent delegations with ${names} together in one assistant message and continue useful work while they run.` + }, + }) } diff --git a/packages/subagent/tool-subagent/src/list-models.ts b/packages/subagent/tool-subagent/src/list-models.ts index a25c9db8c8..4a81c568f6 100644 --- a/packages/subagent/tool-subagent/src/list-models.ts +++ b/packages/subagent/tool-subagent/src/list-models.ts @@ -7,6 +7,10 @@ import { defineTool } from '@deepseek-ai/dsh-tools' import { parentAgentOptionsForDelegation } from '@deepseek-ai/dsh-subagent' import type { ToolRunContext } from '@deepseek-ai/dsh-tools' import type { ModelSelectionPolicy } from './model-selection.ts' +import { subagentModelSelectionPolicy, subagentModelSelectionProjectionDefinition } from './model-selection-state.ts' + +export const name = 'tool-subagent-list-models' +export const inject = ['tools', 'sessionProjections'] interface ListSubagentModelsRequest { readonly provider?: string @@ -37,13 +41,14 @@ function modelLine(provider: string, model: { id: string; name: string; descript /** Read the requested provider, allowed models, or exact-model efforts. */ async function listSubagentModels( ctx: Context, - policy: ModelSelectionPolicy | undefined, request: ListSubagentModelsRequest, exec: ToolRunContext, ): Promise { + if (exec.agent === undefined) throw new Error('list_subagent_models requires a calling agent') + const routes = subagentModelSelectionPolicy(ctx.sessionProjections, exec.agent.session) + let policy = routes === undefined ? undefined : { routes } const selectionEnabled = policy !== undefined if (policy === undefined) { - if (exec.agent === undefined) throw new Error('list_subagent_models requires a calling agent') const { provider, model } = parentAgentOptionsForDelegation(exec.agent) if (provider === undefined || model === undefined) { throw new Error('cannot discover child LLM routes without a parent provider and model') @@ -94,12 +99,11 @@ async function listSubagentModels( } /** - * Register `list_subagent_models` for the delegation-tool instance that owns model selection. - * Provider listings intersect the catalog with authorization, or resolve only the parent model without a Session policy. - * @param ctx - Context whose tool registry owns the fixed discovery definition. - * @param policyFor - Read the calling Session’s authorized routes; undefined restricts discovery to its parent model. + * Register independent discovery of the calling Session’s authorized child models. + * @param ctx - Context carrying the tool registry and Session projections. */ -export function registerListSubagentModels(ctx: Context, policyFor: (exec: ToolRunContext) => ModelSelectionPolicy | undefined): void { +export function apply(ctx: Context): void { + ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition) ctx.tools.register(defineTool({ name: 'list_subagent_models', description: @@ -121,7 +125,7 @@ export function registerListSubagentModels(ctx: Context, policyFor: (exec: ToolR render: (_args, result) => [{ type: 'text', text: result }], }, execute(args, exec) { - return listSubagentModels(ctx, policyFor(exec), args, exec) + return listSubagentModels(ctx, args, exec) }, })) } diff --git a/packages/subagent/tool-subagent/src/model-selection-settings.ts b/packages/subagent/tool-subagent/src/model-selection-settings.ts index e03d5d2b49..9e4907556b 100644 --- a/packages/subagent/tool-subagent/src/model-selection-settings.ts +++ b/packages/subagent/tool-subagent/src/model-selection-settings.ts @@ -3,6 +3,12 @@ import type { Volatile } from '@deepseek-ai/cordis' import { Context, Service } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' +import { SessionSeq } from '@deepseek-ai/dsh-session' +import type {} from '@deepseek-ai/dsh-agent' +import { + subagentModelSelectionPolicy, + subagentModelSelectionProjectionDefinition, +} from './model-selection-state.ts' import { AllowedModelRouteSchema, assertAllowedModelRoutes, @@ -11,16 +17,16 @@ import { declare module '@deepseek-ai/cordis' { interface Context { - /** User preference sampled when a new Session receives delegation tools. */ + /** User preference sampled during Agent creation. */ subagentModelSelection: SubagentModelSelectionConfig } } /** Stored user preference; the shipped composition defaults it off. */ export interface SubagentModelSelectionSettings { - /** Whether newly composed top-level Sessions receive model selection. */ + /** Whether new top-level Sessions receive model selection. */ enabled: boolean - /** Exact child LLM routes offered to newly composed top-level Sessions. */ + /** Exact child LLM routes offered to new top-level Sessions. */ allowedModels: AllowedModelRoute[] } @@ -32,8 +38,10 @@ export interface Config { allowedModels: Volatile } -/** Singleton settings owner read when delegation tools are composed for a Session. */ +/** Host settings owner that records Session authorization before the first model request. */ export class SubagentModelSelectionConfig extends Service { + static inject = ['sessionProjections', 'sessions'] + static Config = z.object({ enabled: z.boolean().default(false).volatile(), allowedModels: z.array(AllowedModelRouteSchema).default([]).volatile(), @@ -41,10 +49,27 @@ export class SubagentModelSelectionConfig extends Service { constructor(ctx: Context, private config: Config) { super(ctx, 'subagentModelSelection') + ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition) + ctx.on('agent/created', ({ agent }) => { + const target = agent.session + if (subagentModelSelectionPolicy(ctx.sessionProjections, target) !== undefined) return + let allowedModels: AllowedModelRoute[] | undefined + const parentId = target.header.origin === 'subagent' ? target.header.parentSession : undefined + if (parentId !== undefined) { + const parent = ctx.sessions.get(parentId) + if (parent !== undefined) allowedModels = subagentModelSelectionPolicy(ctx.sessionProjections, parent) + } else if (target.firstLiveSeq === 0 + // oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred. + && target.eventAt(SessionSeq(0))?.type !== 'session/end-seed') { + const current = this.current() + allowedModels = current.enabled ? current.allowedModels : undefined + } + if (allowedModels !== undefined) target.append('subagent/model-selection-policy', { allowedModels }) + }) } /** - * Read a detached selection preference for the next eligible Session composition. + * Read a detached selection preference for the next fresh top-level Session. * @returns the enabled state and exact allowed routes. */ current(): SubagentModelSelectionSettings { diff --git a/packages/subagent/tool-subagent/src/model-selection-state.ts b/packages/subagent/tool-subagent/src/model-selection-state.ts index 0854425c83..221ef5a8f6 100644 --- a/packages/subagent/tool-subagent/src/model-selection-state.ts +++ b/packages/subagent/tool-subagent/src/model-selection-state.ts @@ -62,20 +62,3 @@ export function subagentModelSelectionPolicy( ): AllowedModelRoute[] | undefined { return projections.stateOf(session, 'subagentModelSelectionPolicy')?.map(route => ({ ...route })) } - -/** - * Append the route policy once, before the Session’s first model request. - * @param projections - registry that owns the policy projection. - * @param session - session receiving route authorization. - * @param allowedModels - exact routes the Session may select explicitly. - */ -export function recordSubagentModelSelection( - projections: Pick, - session: Session, - allowedModels: readonly AllowedModelRoute[], -): void { - if (subagentModelSelectionPolicy(projections, session) !== undefined) return - session.append('subagent/model-selection-policy', { - allowedModels: allowedModels.map(route => ({ ...route })), - }) -} diff --git a/packages/subagent/tool-subagent/tests/harness.ts b/packages/subagent/tool-subagent/tests/harness.ts index 01279f3b13..e8ae24cf4a 100644 --- a/packages/subagent/tool-subagent/tests/harness.ts +++ b/packages/subagent/tool-subagent/tests/harness.ts @@ -9,6 +9,7 @@ import SubagentRuntime from '@deepseek-ai/dsh-subagent' import { SessionId } from '@deepseek-ai/dsh-session' import * as mock from './scripted-provider.ts' import * as tool from '../src/index.ts' +import * as ListModels from '../src/list-models.ts' import SubagentModelSelectionConfig from '../src/model-selection-settings.ts' const contexts = new Set() @@ -51,12 +52,12 @@ const TEST_ALLOWED_MODELS = [ export async function setup(toolConfig: SetupConfig, mockConfig: Partial = {}): Promise { const ctx = ownContext(new Context()) const { withModelSelection, hostSelectionEnabled = true, parentAgentOptions, ...config } = toolConfig + await mountAgentLoopTestDependencies(ctx) if (withModelSelection === true) { await ctx.plugin(SubagentModelSelectionConfig, { enabled: hostSelectionEnabled, allowedModels: TEST_ALLOWED_MODELS, }) - await mountAgentLoopTestDependencies(ctx) await ctx.plugin(AgentLoop, { agents: [] }) await mountWorkingDirectoryFixture(ctx) await ctx.plugin(SubagentRuntime) @@ -65,9 +66,10 @@ export async function setup(toolConfig: SetupConfig, mockConfig: Partial { + setup: async (agentCtx) => { + await agentCtx.plugin(ListModels) const fiber = agentCtx.inject(tool.inject, (runtimeCtx) => { - tool.apply(runtimeCtx, { ...config, modelSelectionSettings: true }, agent.session) + tool.apply(runtimeCtx, { ...config, modelSelection: true }) }) await fiber.await() }, @@ -75,7 +77,6 @@ export async function setup(toolConfig: SetupConfig, mockConfig: Partial() + async function setupListTool(routes = [ { provider: 'alpha', model: 'fast' }, { provider: 'alpha', model: 'plain' }, { provider: 'beta', model: 'fast' }, { provider: 'beta', model: 'plain' }, ]) { - const ctx = new Context() - await ctx.plugin(LlmRuntime) - await ctx.plugin(SystemPrompt) - await ctx.plugin(ToolRuntime) - registerListSubagentModels(ctx, () => ({ routes })) + const ctx = ownContext(new Context()) + await mountAgentLoopTestDependencies(ctx) + await ctx.plugin(AgentLoop, { agents: [] }) + await ctx.plugin(SubagentModelSelectionConfig, { enabled: true, allowedModels: routes }) + await ctx.plugin(ListModels) + const { agent } = await ctx.agents.create({ sessionId: SessionId('discovery-parent') }) + callingAgents.set(ctx, agent) return ctx } async function setupAllowedListTool() { - const ctx = new Context() - await ctx.plugin(LlmRuntime) - await ctx.plugin(SystemPrompt) - await ctx.plugin(ToolRuntime) - registerListSubagentModels(ctx, () => ({ - routes: [ - { provider: 'alpha', model: 'fast' }, - { provider: 'alpha', model: 'unlisted' }, - { provider: 'missing', model: 'hidden' }, - ], - })) - return ctx + return setupListTool([ + { provider: 'alpha', model: 'fast' }, + { provider: 'alpha', model: 'unlisted' }, + { provider: 'missing', model: 'hidden' }, + ]) } let counter = 0 @@ -98,12 +98,13 @@ function call(ctx: Context, args: unknown, signal = testToolSignal) { callId: ToolCallId(`list-models-${++counter}`), name: 'list_subagent_models', arguments: args, + ...callingAgents.has(ctx) ? { agent: callingAgents.get(ctx)! } : {}, }) } describe('list_subagent_models', () => { - it('is omitted unless its delegation-tool instance owns model selection', async () => { - const ctx = new Context() + it('is mounted independently of delegation tools', async () => { + const ctx = ownContext(new Context()) await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(LlmRuntime) await ctx.plugin(SystemPrompt) @@ -112,23 +113,19 @@ describe('list_subagent_models', () => { await ctx.plugin(SubagentRuntime) await ctx.plugin(tool, { provider: 'unused' }) expect(ctx.tools.get('list_subagent_models')).toBeUndefined() + const discovery = await ctx.plugin(ListModels) + expect(ctx.tools.get('list_subagent_models')).toBeDefined() + await discovery.dispose() + expect(ctx.tools.get('list_subagent_models')).toBeUndefined() + await ctx.fiber.dispose() }) it('requires a calling Agent and an effective parent route', async () => { - const ctx = new Context() - await ctx.plugin(SubagentModelSelectionConfig) + const ctx = ownContext(new Context()) await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) - await mountWorkingDirectoryFixture(ctx) - await ctx.plugin(SubagentRuntime) - await ctx.plugin({ - name: 'direct-tool-subagent', - inject: tool.inject, - apply: (direct: Context) => { - tool.apply(direct, { provider: 'unused', modelSelectionSettings: true }, Session.create(SessionId('direct-discovery'))) - }, - }) + await ctx.plugin(ListModels) expect(text(await call(ctx, {}))).toContain('requires a calling agent') await ctx.fiber.dispose() @@ -142,7 +139,7 @@ describe('list_subagent_models', () => { expect(text(result)).toContain('without a parent provider and model') }) - it('reads the recorded Session allowlist through the settings-owned discovery definition', async () => { + it('reads the recorded Session allowlist through the independent discovery definition', async () => { const ctx = await setup({ provider: 'mock', withModelSelection: true }) ctx.llm.registerAdapter(['alpha', 'secret'], new CatalogAdapter()) const result = await ctx.tools.execute({ @@ -153,25 +150,20 @@ describe('list_subagent_models', () => { expect(text(result)).toBe('alpha — ALPHA API') }) - it('stays registered without the optional LLM service and rejects discovery calls', async () => { - const ctx = new Context() + it('stays registered without the LLM service and rejects discovery calls', async () => { + const parent = await setupListTool() + const ctx = ownContext(new Context()) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) - await mountWorkingDirectoryFixture(ctx) - await ctx.plugin(SubagentRuntime) - registerListSubagentModels(ctx, () => ({ routes: [{ provider: 'alpha', model: 'fast' }] })) + await ctx.plugin(ListModels) + callingAgents.set(ctx, callingAgents.get(parent)!) + expect(ctx.tools.get('list_subagent_models')).toBeDefined() const result = await call(ctx, {}) expect(result.isError).toBe(true) expect(text(result)).toContain('`llm` service is unavailable') }) - it('rejects two discovery-owning instances in one tool scope', async () => { - const ctx = await setupListTool() - expect(() => { - registerListSubagentModels(ctx, () => ({ routes: [{ provider: 'alpha', model: 'fast' }] })) - }).toThrow('tool "list_subagent_models" is already registered') - }) - it('lists registered providers and follows live registration changes', async () => { const ctx = await setupListTool() const empty = await call(ctx, {}) @@ -309,20 +301,6 @@ describe('list_subagent_models', () => { expect(listModels).not.toHaveBeenCalled() }) - it('requires a calling Agent when a discovery registration supplies no policy', async () => { - const ctx = new Context() - try { - await ctx.plugin(SystemPrompt) - await ctx.plugin(ToolRuntime) - registerListSubagentModels(ctx, () => undefined) - const result = await call(ctx, {}) - expect(result.isError).toBe(true) - expect(text(result)).toContain('requires a calling agent') - } finally { - await ctx.fiber.dispose() - } - }) - it('inspects exact-model efforts, descriptions, and defaults', async () => { const ctx = await setupListTool() ctx.llm.registerAdapter(['alpha', 'secret'], new CatalogAdapter()) diff --git a/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts b/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts index ff374ed096..975abce19e 100644 --- a/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts +++ b/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts @@ -4,22 +4,22 @@ import { mountWorkingDirectoryFixture } from '../../subagent/tests/working-direc import { describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import { ToolCallId } from '@deepseek-ai/dsh-llm' -import { Session, SESSION_FORMAT_VERSION, SessionId } from '@deepseek-ai/dsh-session' +import { Session, SessionId } from '@deepseek-ai/dsh-session' import type { SessionEvent } from '@deepseek-ai/dsh-session' import { bindScopeParent, createScope, scopeOf, scopeTarget } from '@deepseek-ai/dsh-scope' import { liveConfig } from '../../../settings/settings/tests/live-config.ts' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' -import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import SubagentRuntime from '@deepseek-ai/dsh-subagent' import * as SubagentSpawn from '@deepseek-ai/dsh-subagent-spawn-in-process' import * as tool from '../src/index.ts' +import * as ListModels from '../src/list-models.ts' import SubagentModelSelectionConfig from '../src/model-selection-settings.ts' import { subagentModelSelectionPolicy, subagentModelSelectionProjectionDefinition, } from '../src/model-selection-state.ts' -import { callSubagent, text } from './harness.ts' +import { callSubagent, ownContext, text } from './harness.ts' const ALLOWED_MODELS = [{ provider: 'alpha', model: 'fast-model' }] @@ -40,19 +40,20 @@ const modelSelectionPresets = new WeakMap { - const ctx = new Context() + const ctx = ownContext(new Context()) - selectionConfigs.set(ctx, await liveConfig(ctx, SubagentModelSelectionConfig)) await mountAgentLoopTestDependencies(ctx) + selectionConfigs.set(ctx, await liveConfig(ctx, SubagentModelSelectionConfig)) await ctx.plugin(AgentLoop, { agents: [] }) await mountWorkingDirectoryFixture(ctx) subagentConfigs.set(ctx, await liveConfig(ctx, SubagentRuntime)) await ctx.plugin(SubagentSpawn, { providerName: 'spawn' }) if (withPreset) { const preset = createScope(ctx, { preset: 'model-selection-test' }) + await preset.ctx.plugin(ListModels) await preset.ctx.plugin(tool, { provider: 'spawn', - modelSelectionSettings: true, + modelSelection: true, }) modelSelectionPresets.set(ctx, preset) } @@ -78,7 +79,8 @@ async function createAgent(ctx: Context, id: string, options: { describe('SubagentModelSelectionConfig', () => { it('uses the composed default without a settings provider', async () => { - const ctx = new Context() + const ctx = ownContext(new Context()) + await mountAgentLoopTestDependencies(ctx) await ctx.plugin(SubagentModelSelectionConfig, { enabled: true, allowedModels: ALLOWED_MODELS }) expect(ctx.subagentModelSelection.current()).toEqual({ enabled: true, allowedModels: ALLOWED_MODELS }) @@ -86,7 +88,8 @@ describe('SubagentModelSelectionConfig', () => { }) it('defaults off and follows the validated user layer', async () => { - const ctx = new Context() + const ctx = ownContext(new Context()) + await mountAgentLoopTestDependencies(ctx) selectionConfigs.set(ctx, await liveConfig(ctx, SubagentModelSelectionConfig)) @@ -100,10 +103,10 @@ describe('SubagentModelSelectionConfig', () => { }) it('rejects duplicate routes, enabled empty settings, and an empty durable policy', async () => { - const ctx = new Context() + const ctx = ownContext(new Context()) + await mountAgentLoopTestDependencies(ctx) selectionConfigs.set(ctx, await liveConfig(ctx, SubagentModelSelectionConfig)) - await ctx.plugin(SessionProjectionRegistry) ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition) // A stored policy the service cannot apply fails when it is read, not when the profile row is written. @@ -171,11 +174,12 @@ describe('SubagentModelSelectionConfig', () => { sessionId: SessionId('direct-agent-setup'), setup: async (agentCtx, agent) => { prepared = agent + await agentCtx.plugin(ListModels) const fiber = agentCtx.inject(tool.inject, (runtimeCtx) => { tool.apply(runtimeCtx, { provider: 'spawn', - modelSelectionSettings: true, - }, agent.session) + modelSelection: true, + }) }) await fiber.await() }, @@ -187,7 +191,7 @@ describe('SubagentModelSelectionConfig', () => { await ctx.fiber.dispose() }) - it('captures a blank Session joining the preset without resampling existing Sessions', async () => { + it('preserves creation-time authorization when a blank Session later joins a preset', async () => { const ctx = await boot() const disabled = await createAgent(ctx, 'already-disabled') const joining = await ctx.agents.create({ sessionId: SessionId('joining-preset') }) @@ -195,23 +199,25 @@ describe('SubagentModelSelectionConfig', () => { const preset = modelSelectionPresets.get(ctx)! bindScopeParent(scopeOf(joining.agent.ctx)!, scopeOf(preset.ctx)!) ctx.emit(scopeTarget({}, scopeOf(preset.ctx)), 'tools/change') - expect(subagentModelSelectionPolicy(ctx.sessionProjections, joining.agent.session)).toEqual(ALLOWED_MODELS) + expect(subagentModelSelectionPolicy(ctx.sessionProjections, joining.agent.session)).toBeUndefined() expect(subagentModelSelectionPolicy(ctx.sessionProjections, disabled.session)).toBeUndefined() await ctx.fiber.dispose() }) - it('captures policy for existing Agents only in the mounting preset', async () => { + it('samples authorization without delegation tools and does not resample on tool installation', async () => { const ctx = await boot(false) + await selectionConfigs.get(ctx)!.update({ enabled: true, allowedModels: ALLOWED_MODELS }) const preset = createScope(ctx, { preset: 'late-tools' }) const inside = await ctx.agents.create({ sessionId: SessionId('existing-inside'), setup: (agentCtx) => { bindScopeParent(scopeOf(agentCtx)!, scopeOf(preset.ctx)!) }, }) const outside = await ctx.agents.create({ sessionId: SessionId('existing-outside') }) - await selectionConfigs.get(ctx)!.update({ enabled: true, allowedModels: ALLOWED_MODELS }) - await preset.ctx.plugin(tool, { provider: 'spawn', modelSelectionSettings: true }) + await selectionConfigs.get(ctx)!.update({ enabled: false }) + await preset.ctx.plugin(ListModels) + await preset.ctx.plugin(tool, { provider: 'spawn', modelSelection: true }) expect(subagentModelSelectionPolicy(ctx.sessionProjections, inside.agent.session)).toEqual(ALLOWED_MODELS) - expect(subagentModelSelectionPolicy(ctx.sessionProjections, outside.agent.session)).toBeUndefined() + expect(subagentModelSelectionPolicy(ctx.sessionProjections, outside.agent.session)).toEqual(ALLOWED_MODELS) await ctx.fiber.dispose() }) @@ -269,9 +275,10 @@ describe('SubagentModelSelectionConfig', () => { const ctx = await boot(false) const preset = createScope(ctx, { preset: 'standard' }) const other = createScope(ctx, { preset: 'minimal' }) + await preset.ctx.plugin(ListModels) await preset.ctx.plugin(tool, { provider: 'spawn', - modelSelectionSettings: true, + modelSelection: true, }) let enabledBinding: ReturnType | undefined @@ -387,58 +394,18 @@ describe('SubagentModelSelectionConfig', () => { await ctx.fiber.dispose() }) - it('requires both the Host setting owner and a scoped standing preset', async () => { - const withoutSettings = new Context() - await mountAgentLoopTestDependencies(withoutSettings) - await mountWorkingDirectoryFixture(withoutSettings) - await withoutSettings.plugin(SubagentRuntime) - expect(() => { - tool.apply(withoutSettings, { - provider: 'missing', - modelSelectionSettings: true, - maxDepth: 'provider-managed', - }) - }).toThrow('requires @deepseek-ai/dsh-tool-subagent/model-selection-settings') - await withoutSettings.fiber.dispose() - - const withoutAgent = await boot(false) - expect(() => { - tool.apply(withoutAgent, { - provider: 'spawn', - modelSelectionSettings: true, - }) - }).toThrow('requires a scoped preset Context') - - await withoutAgent.fiber.dispose() - }) - - it('requires the Session registry when a child inherits its parent policy', async () => { - const ctx = new Context() - try { - selectionConfigs.set(ctx, await liveConfig(ctx, SubagentModelSelectionConfig)) - await ctx.plugin(SessionProjectionRegistry) - await mountWorkingDirectoryFixture(ctx) - subagentConfigs.set(ctx, await liveConfig(ctx, SubagentRuntime)) - const childId = SessionId('child-without-session-registry') - const child = Session.create(childId, undefined, { - version: SESSION_FORMAT_VERSION, - id: childId, - createdAt: 1, - isSeeded: false, - origin: 'subagent', - parentSession: SessionId('missing-parent'), - }) - - expect(() => { - tool.apply(ctx, { - provider: 'missing', - modelSelectionSettings: true, - maxDepth: 'provider-managed', - }, child) - }).toThrow('child model-selection inheritance requires the Session registry') - } finally { - await ctx.fiber.dispose() - } + it('permits independently configured delegation tools without a settings service', async () => { + const ctx = ownContext(new Context()) + await mountAgentLoopTestDependencies(ctx) + await mountWorkingDirectoryFixture(ctx) + await ctx.plugin(SubagentRuntime) + await ctx.plugin(SubagentSpawn, { providerName: 'spawn' }) + await ctx.plugin(tool, { provider: 'spawn', modelSelection: true }) + await ctx.plugin(tool, { provider: 'spawn', toolName: 'another_spawn', modelSelection: true }) + expect(ctx.tools.get('subagent')).toBeDefined() + expect(ctx.tools.get('another_spawn')).toBeDefined() + expect(ctx.tools.get('list_subagent_models')).toBeUndefined() + await ctx.fiber.dispose() }) }) diff --git a/packages/subagent/tool-subagent/tests/model-selection.spec.ts b/packages/subagent/tool-subagent/tests/model-selection.spec.ts index 0181e77b9c..183f1f52f8 100644 --- a/packages/subagent/tool-subagent/tests/model-selection.spec.ts +++ b/packages/subagent/tool-subagent/tests/model-selection.spec.ts @@ -141,7 +141,7 @@ describe('dsh-tool-subagent model selection', () => { expect(text(result)).toContain('model selection is disabled for this tool instance') }) - it('hides and rejects route fields without modelSelectionSettings', async () => { + it('hides and rejects route fields without modelSelection', async () => { const ctx = await setup({ provider: 'mock', parentAgentOptions: { provider: 'alpha', model: 'parent-model' } }) const schema = ctx.tools.schemas().find(entry => entry.name === 'subagent')! const props = (schema.parameters as { properties?: Record }).properties ?? {} diff --git a/packages/subagent/tool-subagent/tsdown.config.ts b/packages/subagent/tool-subagent/tsdown.config.ts index 4a467f2c5e..8cdb8ec132 100644 --- a/packages/subagent/tool-subagent/tsdown.config.ts +++ b/packages/subagent/tool-subagent/tsdown.config.ts @@ -15,4 +15,5 @@ const entry = (path: string) => ({ export default defineConfig([ entry('lib/types/index.js'), entry('lib/types/model-selection-settings.js'), + entry('lib/types/list-models.js'), ]) diff --git a/scripts/check-workspace-constraints.ts b/scripts/check-workspace-constraints.ts index 1880c55d1e..ce45a292c5 100644 --- a/scripts/check-workspace-constraints.ts +++ b/scripts/check-workspace-constraints.ts @@ -197,7 +197,7 @@ const packageFileExtras: Readonly> = { '@deepseek-ai/dsh-agent-preset': ['skills'], // The Web Host mounts the default-off settings owner independently of the // delegation-tool instances. - '@deepseek-ai/dsh-tool-subagent': ['lib/model-selection-settings.js'], + '@deepseek-ai/dsh-tool-subagent': ['lib/model-selection-settings.js', 'lib/list-models.js'], // The JSONL backend resolves its private verification Worker relative to // import.meta.url; it is shipped without a public package subpath. '@deepseek-ai/dsh-session-persistence-jsonl': ['lib/worker.cjs'], diff --git a/scripts/gen-tool-catalog.ts b/scripts/gen-tool-catalog.ts index 2d554d3542..937fcb6186 100644 --- a/scripts/gen-tool-catalog.ts +++ b/scripts/gen-tool-catalog.ts @@ -73,7 +73,7 @@ import * as PluginManagerTools from '@deepseek-ai/dsh-plugin-manager/tools' import SandboxPolicy from '@deepseek-ai/dsh-sandbox-policy' import McpResources from '@deepseek-ai/dsh-mcp-resources' import * as ToolSubagent from '@deepseek-ai/dsh-tool-subagent' -import { registerListSubagentModels } from '../packages/subagent/tool-subagent/src/list-models.ts' +import * as ListSubagentModels from '../packages/subagent/tool-subagent/src/list-models.ts' import * as ToolWeb from '@deepseek-ai/dsh-tool-web' import WorkflowEngine from '@deepseek-ai/dsh-workflow' import type { WorkflowRun, WorkflowStartRequest } from '@deepseek-ai/dsh-workflow' @@ -519,10 +519,10 @@ const TOOL_PACKAGES: ToolPackage[] = [ await ctx.plugin(LlmRuntime) registerCatalogSubagentProvider(ctx, 'mock') await ctx.plugin(ToolSubagent, { provider: 'mock' }) - registerListSubagentModels(ctx, () => ({ routes: [{ provider: 'mock', model: 'mock' }] })) + await ctx.plugin(ListSubagentModels) }, note: - 'The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion registered by an instance with `modelSelectionSettings`. That instance always exposes optional route fields; Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions, and without an enabled policy, selection and discovery are restricted to the current parent route. The shipped `subagent_fork` exposes no route selection. Each instance selects its backend through `provider`. 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. The independent `/list-models` plugin registers discovery. Web and Desktop presets mount discovery, enable spawn route fields, and leave 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.', }, { pkg: '@deepseek-ai/dsh-tool-subagent-control', diff --git a/tsconfig.base.json b/tsconfig.base.json index 0d5ec2c48a..dfb4faf2eb 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -185,6 +185,7 @@ "@deepseek-ai/dsh-tools/presentation": ["./packages/core/tools/src/presentation.ts"], "@deepseek-ai/dsh-tools/types": ["./packages/core/tools/src/types.ts"], "@deepseek-ai/dsh-tool-subagent-control/list-agents": ["./packages/subagent/tool-subagent-control/src/list-agents.ts"], + "@deepseek-ai/dsh-tool-subagent/list-models": ["./packages/subagent/tool-subagent/src/list-models.ts"], "@deepseek-ai/dsh-tool-subagent/model-selection-settings": ["./packages/subagent/tool-subagent/src/model-selection-settings.ts"], "@deepseek-ai/dsh-user-approval/types": ["./packages/interaction/user-approval/src/types.ts"], "@deepseek-ai/dsh-user-questions/types": ["./packages/interaction/user-questions/src/types.ts"], From 26d038ae2b3c59797402a0daec9bee7c0e8ee8c0 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Tue, 13 Oct 2026 11:56:32 +0800 Subject: [PATCH 049/369] fix(subagent): complete discovery plugin metadata and catalog expectations --- .../expected/settings-chrome/plugin-instances.expected.md | 4 ++++ docs/tool-catalog.i18n.yaml | 8 ++++---- docs/tool-catalog.md | 4 ++-- docs/tool-catalog.zh.md | 4 ++-- .../subagent/tool-subagent/locale/list-models/en.json | 5 +++++ .../subagent/tool-subagent/locale/list-models/zh.json | 5 +++++ packages/subagent/tool-subagent/package.json | 4 +++- scripts/gen-tool-catalog.ts | 2 +- 8 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 packages/subagent/tool-subagent/locale/list-models/en.json create mode 100644 packages/subagent/tool-subagent/locale/list-models/zh.json diff --git a/apps/web/tests/expected/settings-chrome/plugin-instances.expected.md b/apps/web/tests/expected/settings-chrome/plugin-instances.expected.md index 239b4855e7..223f7ab367 100644 --- a/apps/web/tests/expected/settings-chrome/plugin-instances.expected.md +++ b/apps/web/tests/expected/settings-chrome/plugin-instances.expected.md @@ -8,6 +8,10 @@ - button "子智能体列表工具, tool-subagent-list-agents, 已启用": - strong: 子智能体列表工具 - code: tool-subagent-list-agents + - listitem: + - button "子智能体模型查询工具, tool-subagent-list-models, 已启用": + - strong: 子智能体模型查询工具 + - code: tool-subagent-list-models - listitem: - button "子智能体委派工具, tool-subagent, 已启用": - strong: 子智能体委派工具 diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml index 86a316406d..47b26056f4 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: 7224f159dd69abba - zh: 00b69ab3f899010a + en: e4b330276e8729aa + zh: 3f781cb32fd16d81 /tool-schema-catalog/deepseek-ai-dsh-plugin-manager: en: e0143916bb634762 zh: e0143916bb634762 @@ -228,8 +228,8 @@ en: 6872bee11fccb6c8 zh: 230f2d443dc5d9a6 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent/subagent: - en: 7b06bc34df756d56 - zh: bc82dab293f57b5b + en: 5bcfcf9ca4117096 + zh: 38cb646632b457b2 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent-control: en: ab82e130c3413f04 zh: ab82e130c3413f04 diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index 7ff20028a7..3b2c922a52 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -37,7 +37,7 @@ This table connects model-visible tool names to the plugin package and service s | `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`, `ctx.workflowEngine`, `ctx.subagents`, `ctx.systemPrompt`, `a calling Agent (exec.agent parents every fresh round)` | `tool/call`, `tool/result`, `workflow and child session events during execution` | - | A fixed foreground workflow starts one fresh structured child per round; the model selects only the immutable objective and an optional round cap. | | `@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` | - | 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-tool-subagent` | `list_subagent_models`, `subagent` | `ctx.tools`, `ctx.subagents`, `ctx.systemPrompt`, `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. The independent `/list-models` plugin registers discovery. Web and Desktop presets mount discovery, enable spawn route fields, and leave 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.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. The independent `/list-models` plugin registers discovery. Web and Desktop presets mount discovery, enable spawn 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-control` | `interrupt_agent`, `list_agents`, `send_message` | `ctx.tools`, `ctx.subagents`, `ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`, `tool/result`, `child session events through ctx.subagents` | - | The globally named discovery, interruption, and local-message tools over managed subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` and `interrupt_agent` once, plus `list_agents` from its separately loaded `/list-agents` plugin (whose catalog rows use the sessionProjections and live Agent registries). | | `@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-experimental-tool-agent-team` | `interrupt_agent`, `list_agents`, `send_message`, `spawn_teammate`, `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update`, `wait_agent` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `an exact live Team member Agent` | `tool/call`, `team/member`, `team/task`, `agent/inbox/spliced`, `user/message on inbox claim`, `tool/result` | - | All nine tools are scoped to implicit Team Leads and durable teammates. The shipped dsh-base bundle keeps the package disabled; the documented Agent Teams profile patch enables it while disabling the legacy continuable-child control names. | @@ -2067,7 +2067,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. The independent `/list-models` plugin registers discovery. Web and Desktop presets mount discovery, enable spawn route fields, and leave 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. The independent `/list-models` plugin registers discovery. Web and Desktop presets mount discovery, enable spawn 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. diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index 5231594677..fc60890bf1 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -41,7 +41,7 @@ | `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`、`ctx.workflowEngine`、`ctx.subagents`、`ctx.systemPrompt`、`a calling Agent (exec.agent parents every fresh round)` | `tool/call`、`tool/result`、`workflow and child session events during execution` | - | 固定的前台工作流会在每个 Round 启动一个全新的结构化子级;模型只能选择不可变目标和可选的 Round 上限。 | | `@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-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 加载时的 `toolName` 选择委派名称(默认为 `subagent`);`modelSelection` 增加可选路由字段。独立的 `/list-models` 插件注册发现工具。Web 与 Desktop preset 挂载发现工具,启用 spawn 路由字段,并为 fork 保持路由字段关闭。Host 设置服务为每个新顶层 Session 记录授权;子 Session 继承该授权。没有已记录策略时,发现和显式选择只允许当前父级路由。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 | +| `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 加载时的 `toolName` 选择委派名称(默认为 `subagent`);`modelSelection` 增加可选路由字段。独立的 `/list-models` 插件注册发现工具。Web 与 Desktop preset 挂载发现工具,启用 spawn 路由字段,并为 `subagent_fork` 保持路由字段关闭。Host 设置服务为每个新顶层 Session 记录授权;子 Session 继承该授权。没有已记录策略时,发现和显式选择只允许当前父级路由。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 | | `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`、`list_agents`、`send_message` | `ctx.tools`、`ctx.subagents`、`ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`、`tool/result`、`child session events through ctx.subagents` | - | 这些是用于发现、中断托管 subagent 和向本地子代理发送消息的全局命名工具:绑定提供方的 `tool-subagent` 实例注册不同的委派工具;本包注册一次 `send_message` 和 `interrupt_agent`,另由 `list_agents` 通过单独加载的 `/list-agents` 插件提供,其目录行使用 sessionProjections 和实时 Agent 注册表。 | | `@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-experimental-tool-agent-team` | `interrupt_agent`、`list_agents`、`send_message`、`spawn_teammate`、`team_task_create`、`team_task_get`、`team_task_list`、`team_task_update`、`wait_agent` | `ctx.tools`、`ctx.systemPrompt`、`ctx.agentTeams`、`an exact live Team member Agent` | `tool/call`、`team/member`、`team/task`、`agent/inbox/spliced`、`user/message on inbox claim`、`tool/result` | - | 这 9 个工具限定于隐式 Team Lead 与持久 teammate 作用域。随产品发布的 dsh-base bundle 默认禁用该包;文档中的 Agent Teams profile patch 会启用它,并禁用旧 continuable child 的同名控制工具。 | @@ -2073,7 +2073,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, 来源:[`packages/subagent/tool-subagent/src/index.ts`](../packages/subagent/tool-subagent/src/index.ts) -加载时的 `toolName` 选择委派名称(默认为 `subagent`);`modelSelection` 增加可选路由字段。独立的 `/list-models` 插件注册发现工具。Web 与 Desktop preset 挂载发现工具,启用 spawn 路由字段,并为 fork 保持路由字段关闭。Host 设置服务为每个新顶层 Session 记录授权;子 Session 继承该授权。没有已记录策略时,发现和显式选择只允许当前父级路由。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 +加载时的 `toolName` 选择委派名称(默认为 `subagent`);`modelSelection` 增加可选路由字段。独立的 `/list-models` 插件注册发现工具。Web 与 Desktop preset 挂载发现工具,启用 spawn 路由字段,并为 `subagent_fork` 保持路由字段关闭。Host 设置服务为每个新顶层 Session 记录授权;子 Session 继承该授权。没有已记录策略时,发现和显式选择只允许当前父级路由。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 diff --git a/packages/subagent/tool-subagent/locale/list-models/en.json b/packages/subagent/tool-subagent/locale/list-models/en.json new file mode 100644 index 0000000000..62b37b2526 --- /dev/null +++ b/packages/subagent/tool-subagent/locale/list-models/en.json @@ -0,0 +1,5 @@ +{ + "meta": { + "title": "Subagent model discovery" + } +} diff --git a/packages/subagent/tool-subagent/locale/list-models/zh.json b/packages/subagent/tool-subagent/locale/list-models/zh.json new file mode 100644 index 0000000000..c78ffcc47f --- /dev/null +++ b/packages/subagent/tool-subagent/locale/list-models/zh.json @@ -0,0 +1,5 @@ +{ + "meta": { + "title": "子智能体模型查询工具" + } +} diff --git a/packages/subagent/tool-subagent/package.json b/packages/subagent/tool-subagent/package.json index 00f6896dc0..bfd7789128 100644 --- a/packages/subagent/tool-subagent/package.json +++ b/packages/subagent/tool-subagent/package.json @@ -29,10 +29,12 @@ "./list-models": { "types": "./lib/types/list-models.d.ts", "default": "./lib/list-models.js" - } + }, + "./list-models/locale/*.json": "./locale/list-models/*.json" }, "files": [ "locale/*.json", + "locale/list-models/*.json", "locale/model-selection-settings/*.json", "lib/index.js", "lib/model-selection-settings.js", diff --git a/scripts/gen-tool-catalog.ts b/scripts/gen-tool-catalog.ts index 937fcb6186..14914a6437 100644 --- a/scripts/gen-tool-catalog.ts +++ b/scripts/gen-tool-catalog.ts @@ -522,7 +522,7 @@ const TOOL_PACKAGES: ToolPackage[] = [ await ctx.plugin(ListSubagentModels) }, note: - 'The load-time `toolName` selects the delegation name (default `subagent`); `modelSelection` adds optional route fields. The independent `/list-models` plugin registers discovery. Web and Desktop presets mount discovery, enable spawn route fields, and leave 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. The independent `/list-models` plugin registers discovery. Web and Desktop presets mount discovery, enable spawn 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.', }, { pkg: '@deepseek-ai/dsh-tool-subagent-control', From c25806f422f4491d4b8524497d046e498c0c4bea Mon Sep 17 00:00:00 2001 From: _Kerman Date: Tue, 13 Oct 2026 20:14:43 +0800 Subject: [PATCH 050/369] fix(ui-chat): settle folds with final sibling spacing --- packages/client/ui-chat/README.i18n.yaml | 4 +-- packages/client/ui-chat/README.md | 2 +- packages/client/ui-chat/README.zh.md | 2 +- .../src/client/chat/use-chat-viewport.ts | 5 ++- .../ui-chat/tests/chat-scroll.client.spec.tsx | 31 +++++++++++++++---- 5 files changed, 33 insertions(+), 11 deletions(-) diff --git a/packages/client/ui-chat/README.i18n.yaml b/packages/client/ui-chat/README.i18n.yaml index dd9033ae18..f971449ddb 100644 --- a/packages/client/ui-chat/README.i18n.yaml +++ b/packages/client/ui-chat/README.i18n.yaml @@ -36,8 +36,8 @@ en: 81d7ca7595c34d39 zh: 29eb8248c75e9ea6 /deepseek-ai-dsh-client-ui-chat/scroll-ownership: - en: 3f4e6c161b4583bb - zh: e4265bb2bce8eb1c + en: 27f0323883ced61d + zh: 33ee7115231140c4 /deepseek-ai-dsh-client-ui-chat/model-experience: en: c17b4dd43a61c569 zh: f7414309810c3a08 diff --git a/packages/client/ui-chat/README.md b/packages/client/ui-chat/README.md index f04957566b..05ca3efc62 100644 --- a/packages/client/ui-chat/README.md +++ b/packages/client/ui-chat/README.md @@ -138,7 +138,7 @@ The transcript root uses `overflow-x: visible; overflow-y: clip`: vertical overf Outer transcript following and each open capped group's following are independent. Native animation progress retains follow intent; only that scrollport's own `scrollend` settles its animation. Browsers without `scrollend` may retain the native follow target until a later bottom-aligned update or reader gesture. A reader gesture interrupts the animation, and actual movement determines whether following remains enabled. Scroll chaining can move the outer transcript, which then applies its own distance threshold. The back-to-bottom button restores only outer following. -Next-message folding combines row closure and tail following in one cancellable task, with browser scroll anchoring disabled during the fold. If the removed height exceeds the new content's height, the difference remains as bottom space rather than reducing the scroll offset to chase the unpadded content floor. This applies even when the conversation already exceeds the viewport. New content fills the retained space before following advances again; a smaller fold leaves only the remaining forward distance to scroll. Reader gestures cancel the task and active native following; resize cannot revive a cancelled task in the same fold, but new submitted input can. Off-bottom readers retain their position, and acknowledged scrolling releases only space that can be removed without clamping them. Unloading Chat cancels waiting callbacks and row transitions. Reduced-motion users receive immediate visibility and positioning changes. +Next-message folding combines row closure and tail following in one cancellable task, with browser scroll anchoring disabled during the fold. The closing frame includes final sibling spacing in its scroll position and reserved room. If the removed height exceeds the new content's height, the difference remains as bottom space rather than reducing the scroll offset to chase the unpadded content floor. This applies even when the conversation already exceeds the viewport. New content fills the retained space before following advances again; a smaller fold leaves only the remaining forward distance to scroll. Reader gestures cancel the task and active native following; resize cannot revive a cancelled task in the same fold, but new submitted input can. Off-bottom readers retain their position, and acknowledged scrolling releases only space that can be removed without clamping them. Unloading Chat cancels waiting callbacks and row transitions. Reduced-motion users receive immediate visibility and positioning changes. Changing Work details clears reserved bottom space after active folds finish and reconciles the scroll range with the selected mode. Readers already following the tail stay at the new bottom; other readers are not forced to follow. diff --git a/packages/client/ui-chat/README.zh.md b/packages/client/ui-chat/README.zh.md index 064607a1a3..3b55b0d16a 100644 --- a/packages/client/ui-chat/README.zh.md +++ b/packages/client/ui-chat/README.zh.md @@ -138,7 +138,7 @@ Chat 会在历史前插与 renderer 重新挂载时恢复语义锚点,并且 外层文本记录与每个已展开限高组的跟随状态彼此独立。原生动画的中间滚动保留跟随意图;只有该滚动容器自身的 `scrollend` 才结束其动画。不支持 `scrollend` 的浏览器可能保留原生跟随目标,直到后续贴底更新或读者手势将其结束。读者手势中断动画,是否继续跟随由实际位移决定。滚动传递可以带动外层,外层再按自身离底距离判断。回到底部按钮只恢复外层跟随。 -下次消息触发的折叠与跟尾属于同一个可取消任务,折叠期间禁用浏览器滚动锚定。如果移除的高度大于新增内容的高度,差值保留为底部空间,不通过减小滚动偏移追到未含预留空间的内容底部;对话总高度已经超过视口时也遵循此规则。后续新增内容先填充保留区域,填满后才继续跟尾;折叠高度较小时,仅滚动剩余的向前距离。读者手势取消该任务及进行中的原生跟随;同一次折叠中的尺寸变化不能恢复已取消的任务,但新提交的输入可以。离底读者保留位置,已确认的滚动只释放不会造成位置钳制的多余空间。卸载 Chat 时取消等待回调和行过渡。启用减少动态效果偏好时,可见性和定位立即变化。 +下次消息触发的折叠与跟尾属于同一个可取消任务,折叠期间禁用浏览器滚动锚定。收尾帧按最终兄弟节点间距确定滚动位置与预留空间。如果移除的高度大于新增内容的高度,差值保留为底部空间,不通过减小滚动偏移追到未含预留空间的内容底部;对话总高度已经超过视口时也遵循此规则。后续新增内容先填充保留区域,填满后才继续跟尾;折叠高度较小时,仅滚动剩余的向前距离。读者手势取消该任务及进行中的原生跟随;同一次折叠中的尺寸变化不能恢复已取消的任务,但新提交的输入可以。离底读者保留位置,已确认的滚动只释放不会造成位置钳制的多余空间。卸载 Chat 时取消等待回调和行过渡。启用减少动态效果偏好时,可见性和定位立即变化。 切换工作步骤展示档位时,当前折叠结束后清空底部预留空间,并按所选档位调整滚动范围。原本跟随底部的读者保持在新的底部;其他读者不会被强制拉回底部。 diff --git a/packages/client/ui-chat/src/client/chat/use-chat-viewport.ts b/packages/client/ui-chat/src/client/chat/use-chat-viewport.ts index ad6de91901..e33fe2b90c 100644 --- a/packages/client/ui-chat/src/client/chat/use-chat-viewport.ts +++ b/packages/client/ui-chat/src/client/chat/use-chat-viewport.ts @@ -243,7 +243,7 @@ export class ChatViewport { finish: (cancelled) => { if (this.elements !== elements) return // Commit the final sibling margins while their input-fold transition override still applies. - void elements.column.offsetHeight + const finalHeight = Math.max(elements.column.offsetHeight, elements.scroller.scrollHeight - this.spacerHeight()) delete elements.column.dataset.inputFold if (cancelled) { const metrics = scrollMetrics(elements.scroller) @@ -252,7 +252,10 @@ export class ChatViewport { this.invalidate() return } + target = Math.max(base, finalHeight - elements.scroller.clientHeight, 0) + finalRoom = target === 0 ? 0 : Math.max(0, target + elements.scroller.clientHeight - finalHeight) this.setSpacer(finalRoom) + elements.scroller.scrollTop = target const metrics = scrollMetrics(elements.scroller) const landing = { metrics, position: null, turn: this.latestTurn } this.observation = { top: metrics.top, landing } diff --git a/packages/client/ui-chat/tests/chat-scroll.client.spec.tsx b/packages/client/ui-chat/tests/chat-scroll.client.spec.tsx index b212f91940..1340e7d815 100644 --- a/packages/client/ui-chat/tests/chat-scroll.client.spec.tsx +++ b/packages/client/ui-chat/tests/chat-scroll.client.spec.tsx @@ -87,10 +87,13 @@ function mountScroll(deferCompletedTurns = true, initial: Partial) }, - startFold: () => { + startFold: (finalHeight?: number) => { const motion = callbacks.motion if (motion === undefined) throw new Error('Viewport motion callbacks are not bound') - act(() => { motion.collapse(row, () => { row.hidden = true }) }) + act(() => { motion.collapse(row, () => { + row.hidden = true + if (finalHeight !== undefined) height = finalHeight + }) }) }, finishFold: () => { const event = new Event('transitionend', { bubbles: true }) @@ -117,7 +120,7 @@ const visibleFoldCases = [ { name: 'small viewport', earlier: 200, steps: 900, body: 100, viewport: 300, top: 900, input: 30, target: 200, room: 170 }, ] as const -function mountVisibleFold(test: typeof visibleFoldCases[number], reduced: boolean) { +function mountVisibleFold(test: typeof visibleFoldCases[number], reduced: boolean, finalSiblingGrowth = 0) { let inputHeight = 0 let submission = 0 let top: number = test.top @@ -142,7 +145,7 @@ function mountVisibleFold(test: typeof visibleFoldCases[number], reduced: boolea const rowHeight = () => row.hidden ? 0 : (row.style.transition !== '' && row.style.transition !== 'none') || row.style.height === '' ? test.steps : Number.parseFloat(row.style.height) const room = () => Number.parseFloat(spacer.style.height) || 0 - const contentHeight = () => test.earlier + rowHeight() + test.body + inputHeight + const contentHeight = () => test.earlier + rowHeight() + test.body + inputHeight + (row.hidden ? finalSiblingGrowth : 0) const floor = () => Math.max(0, contentHeight() + room() - test.viewport) const scrollTo = vi.fn((options: ScrollToOptions) => { scroller.scrollTop = options.top ?? scroller.scrollTop @@ -211,6 +214,22 @@ function mountVisibleFold(test: typeof visibleFoldCases[number], reduced: boolea } describe.each([false, true])('visible input-fold matrix (reduced=%s)', (reduced) => { + it.each(['larger input', 'T/B'])('lands on final sibling spacing in the closing frame (%s)', async (name) => { + const test = visibleFoldCases.find(test => test.name === name)! + const h = mountVisibleFold(test, reduced, 4) + h.submit() + await act(async () => { await Promise.resolve() }) + for (let frame = 0; frame < 24 && !h.row.hidden; frame++) { + act(() => { vi.advanceTimersByTime(16) }) + } + expect(h.row.hidden).toBe(true) + const target = test.target + (test.room === 0 ? 4 : 0) + expect(h.scroller.scrollTop).toBe(target) + expect(h.room()).toBe(Math.max(0, test.room - 4)) + act(() => { vi.advanceTimersByTime(500) }) + expect(h.scroller.scrollTop).toBe(target) + }) + it.each(visibleFoldCases)('$name lands with only the required visible room', async (test) => { const h = mountVisibleFold(test, reduced) const positions = [h.bodyTop()] @@ -363,7 +382,7 @@ describe('Chat scroll collapse timing', () => { it('coalesces submitted inputs into one frame-owned fold', async () => { const h = mountScroll() - h.startFold() + h.startFold(920) h.update({ submissionId: 'first' }) h.update({ submissionId: 'second' }) expect(h.scrollTo).not.toHaveBeenCalled() @@ -467,7 +486,7 @@ describe('Chat scroll collapse timing', () => { it.each([false, true])('keeps a fold cancelled through resize until new input owns follow (new input=%s)', async (newInput) => { const h = mountScroll() h.scroller.scrollTop = 500 - h.startFold() + h.startFold(920) h.update({ submissionId: 'first' }) await act(async () => { await Promise.resolve() }) fireEvent.wheel(h.scroller, { deltaY: -80 }) From f50bab696ad1860aaf392c88ea5aa6a94bc097eb Mon Sep 17 00:00:00 2001 From: _Kerman Date: Tue, 13 Oct 2026 20:14:59 +0800 Subject: [PATCH 051/369] test(web): sample input folds on a controlled frame clock --- apps/web/tests/chat-scroll-contract.e2e.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/web/tests/chat-scroll-contract.e2e.ts b/apps/web/tests/chat-scroll-contract.e2e.ts index 955d251ab7..49675a473a 100644 --- a/apps/web/tests/chat-scroll-contract.e2e.ts +++ b/apps/web/tests/chat-scroll-contract.e2e.ts @@ -593,7 +593,7 @@ describe('web e2e: long Chat scroll contract', () => { const artifactRoot = fileURLToPath(new URL('../../../.artifacts/screenshots/visible-fold/', import.meta.url)) await mkdir(artifactRoot, { recursive: true }) const evidence = await mkdtemp(join(artifactRoot, `${scenario.name}-`)) - const controlledClock = scenario.name === 'content-growth-during-fold' || scenario.name === 'cancel-all-during-fold' + const controlledClock = !scenario.reduced await withScrollWorld({ failureShot: `visible-fold-${scenario.name}`, controlledClock, @@ -760,17 +760,19 @@ describe('web e2e: long Chat scroll contract', () => { try { settled = world.scaffold.whenTurnSettled(30_000) if (controlledClock) { - // Advance every animation frame so the race cannot miss the intermediate fold height. + // Sample each animation frame, including when the CI browser misses real-time frames. await page.clock.pauseAt(await page.evaluate(() => Date.now() + 1000)) try { await page.getByRole('button', { name: 'Send message', exact: true }).evaluate((button) => { if (!(button instanceof HTMLButtonElement)) throw new Error('Send control is not a button') button.click() }) - await expect.poll(async () => { - await page.clock.runFor(16) - return page.locator('[data-fold-race-applied]').count() - }, { timeout: 10_000 }).toBe(1) + if (scenario.name === 'content-growth-during-fold' || scenario.name === 'cancel-all-during-fold') { + await expect.poll(async () => { + await page.clock.runFor(16) + return page.locator('[data-fold-race-applied]').count() + }, { timeout: 10_000 }).toBe(1) + } await expect.poll(async () => { await page.clock.runFor(16) return page.locator('[data-chat-motion="collapse"]').count() From dbf7d54a18ef4965d50bed446f21c80282c5c492 Mon Sep 17 00:00:00 2001 From: Turtle Date: Tue, 13 Oct 2026 21:47:24 +0800 Subject: [PATCH 052/369] feat(client): fade a navigated source line's highlight in the document preview Opening a file link at a line now marks the landed line in both the code and plain-text renderers with a band that holds briefly and fades out over two seconds. The band lives in the scrollport, so syntax highlighting replacing the code lines keeps it. The plain renderer's persistent target background is removed. --- .../README.i18n.yaml | 4 +-- .../ui-sidebar-documentpreview/README.md | 2 +- .../ui-sidebar-documentpreview/README.zh.md | 2 +- .../src/client/TextPreview.module.css | 22 ++++++++++++-- .../src/client/TextPreview.tsx | 11 +++---- .../src/client/text/TextBody.tsx | 3 +- .../src/client/text/lines.ts | 30 ++++++++++++++++--- .../tests/text-preview.client.spec.tsx | 17 +++++++++++ 8 files changed, 74 insertions(+), 17 deletions(-) diff --git a/packages/client/ui-sidebar-documentpreview/README.i18n.yaml b/packages/client/ui-sidebar-documentpreview/README.i18n.yaml index 67ad419821..ccceabe69d 100644 --- a/packages/client/ui-sidebar-documentpreview/README.i18n.yaml +++ b/packages/client/ui-sidebar-documentpreview/README.i18n.yaml @@ -30,8 +30,8 @@ en: 372d8ce6492e6896 zh: 912f613bc9221aa3 /deepseek-ai-dsh-client-ui-sidebar-documentpreview/navigation: - en: 5cd81a7b3cdc83bd - zh: 4cdd1b7ede497925 + en: 35f843f02c858674 + zh: 44af8e323a12d4dc /deepseek-ai-dsh-client-ui-sidebar-documentpreview/model-experience: en: ca1deb99d0e44755 zh: f285fc56cee669ec diff --git a/packages/client/ui-sidebar-documentpreview/README.md b/packages/client/ui-sidebar-documentpreview/README.md index 51e23e2208..ff61b2bd5b 100644 --- a/packages/client/ui-sidebar-documentpreview/README.md +++ b/packages/client/ui-sidebar-documentpreview/README.md @@ -123,7 +123,7 @@ The page refresh shortcut reuses the selected preview’s reload operation, incl ## Navigation -`ctx.sidebarRight.openResource(address, { params: { line } })` carries a 1-based source line through the `file` parameters. In `text-pages` mode, the owner loads sequential pages until that line or EOF. Plain-text and code renderers expose source-line anchors; Markdown does not. A navigation remains pending while its selected renderer has no anchor and runs if the user switches to plain text or code. Code navigation scrolls the inner source viewport directly. Byte-mode renderers do not consume source-line navigation. Each completed navigation revision is answered once. Opening the same file without `revealIfOpened: false` focuses its existing tab and delivers a new revision. +`ctx.sidebarRight.openResource(address, { params: { line } })` carries a 1-based source line through the `file` parameters. In `text-pages` mode, the owner loads sequential pages until that line or EOF. Plain-text and code renderers expose source-line anchors; Markdown does not. A navigation remains pending while its selected renderer has no anchor and runs if the user switches to plain text or code. Code navigation scrolls the inner source viewport directly. The landed line receives a band that holds briefly and then fades out over two seconds; restoring a remounted tab's scroll position adds no band. Byte-mode renderers do not consume source-line navigation. Each completed navigation revision is answered once. Opening the same file without `revealIfOpened: false` focuses its existing tab and delivers a new revision. ## Model Experience diff --git a/packages/client/ui-sidebar-documentpreview/README.zh.md b/packages/client/ui-sidebar-documentpreview/README.zh.md index 1c5be3fdc3..586288b986 100644 --- a/packages/client/ui-sidebar-documentpreview/README.zh.md +++ b/packages/client/ui-sidebar-documentpreview/README.zh.md @@ -123,7 +123,7 @@ Office Remote 通过 Connection 的 multipart 二进制传输返回原生 `Uint8 ## 导航 -`ctx.sidebarRight.openResource(address, { params: { line } })` 通过 `file` 参数携带 1 起算的源码行号。在 `text-pages` 模式下,owner 顺序加载到该行或 EOF。纯文本与代码渲染器提供源码行锚点;Markdown 不提供。所选渲染器没有锚点时,导航保持待处理;用户切换到纯文本或代码后执行。代码导航直接滚动内部源码视口。字节模式渲染器不消费源码行导航。每个完成的导航 revision 只响应一次。不带 `revealIfOpened: false` 打开同一文件时聚焦已有 tab,并送达新 revision。 +`ctx.sidebarRight.openResource(address, { params: { line } })` 通过 `file` 参数携带 1 起算的源码行号。在 `text-pages` 模式下,owner 顺序加载到该行或 EOF。纯文本与代码渲染器提供源码行锚点;Markdown 不提供。所选渲染器没有锚点时,导航保持待处理;用户切换到纯文本或代码后执行。代码导航直接滚动内部源码视口。落定的行显示一条短暂停留后在两秒内渐隐的高亮带;重新挂载的 tab 恢复滚动位置时不显示高亮带。字节模式渲染器不消费源码行导航。每个完成的导航 revision 只响应一次。不带 `revealIfOpened: false` 打开同一文件时聚焦已有 tab,并送达新 revision。 ## 模型体验 diff --git a/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.module.css b/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.module.css index ca454e9c7d..34bed20455 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.module.css +++ b/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.module.css @@ -107,8 +107,26 @@ padding: 0 10px; } -.lineTarget { - background: var(--dsw-alias-interactive-bg-hover); +/* A navigated line's mark: a band in the scrollport over the line, so renderer + re-renders such as syntax highlighting do not drop it. It holds briefly, then + fades out and removes itself. */ +.lineMark { + position: absolute; + left: 0; + background: var(--dsw-alias-interactive-bg-hover-accent); + pointer-events: none; + animation: line-mark-fade 2s ease-in forwards; +} + +@keyframes line-mark-fade { + 0%, + 30% { + opacity: 1; + } + + 100% { + opacity: 0; + } } /* The failure with nothing read yet: the whole body, centred a little above diff --git a/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.tsx b/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.tsx index 101f2debd8..27044d5653 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.tsx +++ b/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.tsx @@ -29,7 +29,7 @@ import { binaryDocumentPath, matchingDocumentPreviews } from './document/registr import type { DocumentPreviewDefinition } from './document/registry.ts' import { unviewableBinaryPath } from './document/unviewable.ts' import { PLAIN_BODY_ID } from './text/index.ts' -import { loadedPages, lastLineLoaded, scrollToLine } from './text/lines.ts' +import { loadedPages, lastLineLoaded, markLine, scrollToLine } from './text/lines.ts' import css from './TextPreview.module.css' export { linesOf, loadedPages, lastLineLoaded, scrollToLine } from './text/lines.ts' @@ -133,8 +133,8 @@ export function TextPreview({ // Answer a navigation once: a line the pages do not reach yet loads the next // page (again, until the pages cover it or the file ends); a line they hold - // is scrolled to and marked. The store remembers the answer, so a remount - // restores the reader's place instead. + // is scrolled to and marked with a fading band. The store remembers the + // answer, so a remount restores the reader's place instead. useEffect(() => { const body = scrollportRef.current if (current === undefined || body === null || current.revision === navigation.revision) return @@ -148,8 +148,9 @@ export function TextPreview({ } return } - const landed = scrollToLine(body, line) - if (!landed && line <= loadedThrough) return + const row = scrollToLine(body, line) + if (row === undefined && line <= loadedThrough) return + if (row !== undefined) markLine(body, row, line) actions.navigated(tab.id, navigation.revision) // Recorded here as well as by the scroll event, so the store holds the // landing before any later navigation reads it. diff --git a/packages/client/ui-sidebar-documentpreview/src/client/text/TextBody.tsx b/packages/client/ui-sidebar-documentpreview/src/client/text/TextBody.tsx index 5162591967..0c26163903 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/text/TextBody.tsx +++ b/packages/client/ui-sidebar-documentpreview/src/client/text/TextBody.tsx @@ -1,6 +1,5 @@ /** Plain source display for files without a more specific document renderer. */ import type { ReactNode } from 'react' -import clsx from 'clsx' import type { DocumentPreviewProps } from '../document/contract.ts' import { linesOf } from './lines.ts' import css from '../TextPreview.module.css' @@ -20,7 +19,7 @@ export function TextBody({ content, useTabInfo }: DocumentPreviewProps): ReactNo return (
diff --git a/packages/client/ui-sidebar-documentpreview/src/client/text/lines.ts b/packages/client/ui-sidebar-documentpreview/src/client/text/lines.ts index f01743635d..2100a3c123 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/text/lines.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/text/lines.ts @@ -1,6 +1,8 @@ /** Source-line helpers shared by the plain renderer and the document scroller. */ +import clsx from 'clsx' import type { DocumentTextPage } from '../document/contract.ts' import type { TextPage } from '../store.ts' +import css from '../TextPreview.module.css' /** * Split a loaded page into its source lines. @@ -39,14 +41,34 @@ export function lastLineLoaded(pages: readonly LoadedPage[]): number { * Reveal a plain-text or highlighted source line. * @param body - scrolling document body or code-content viewport. * @param line - 1-based source line to reveal. - * @returns Whether the current renderer exposes that line. + * @returns The revealed line element, or undefined when the current renderer does not expose that line. */ -export function scrollToLine(body: HTMLElement, line: number): boolean { +export function scrollToLine(body: HTMLElement, line: number): HTMLElement | undefined { const innerCode = body.hasAttribute('data-code-block-content') const plain = body.querySelector(`[data-textpreview-line="${line}"]`) const code = innerCode ? body.querySelectorAll('pre .line').item(line - 1) : null const row = plain ?? code - if (!(row instanceof HTMLElement)) return false + if (!(row instanceof HTMLElement)) return undefined body.scrollTop = Math.max(0, row.offsetTop) - return true + return row +} + +/** + * Mark a revealed line with a fading band. The band is a child of the scrollport, + * not of the line, so a renderer that rebuilds its lines keeps the mark; it + * replaces an earlier mark and removes itself when its animation ends. + * @param body - the scrollport that {@link scrollToLine} scrolled. + * @param row - the line element it returned. + * @param line - 1-based source line, recorded on the band. + */ +export function markLine(body: HTMLElement, row: HTMLElement, line: number): void { + body.querySelector(':scope > [data-textpreview-mark]')?.remove() + const mark = body.ownerDocument.createElement('div') + mark.className = clsx(css.lineMark) + mark.setAttribute('data-textpreview-mark', String(line)) + mark.style.top = `${row.offsetTop}px` + mark.style.height = `${row.offsetHeight}px` + mark.style.width = `${body.scrollWidth}px` + mark.addEventListener('animationend', () => { mark.remove() }, { once: true }) + body.append(mark) } diff --git a/packages/client/ui-sidebar-documentpreview/tests/text-preview.client.spec.tsx b/packages/client/ui-sidebar-documentpreview/tests/text-preview.client.spec.tsx index 999b11605a..627f6a9e9b 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/text-preview.client.spec.tsx +++ b/packages/client/ui-sidebar-documentpreview/tests/text-preview.client.spec.tsx @@ -110,6 +110,10 @@ function target(container: HTMLElement): string | null { return container.querySelector('[data-textpreview-target]')?.getAttribute('data-textpreview-target') ?? null } +function marks(container: HTMLElement): HTMLElement[] { + return Array.from(container.querySelectorAll('[data-textpreview-mark]')) +} + function click(container: HTMLElement, selector: string): void { const button = container.querySelector(selector) if (button === null) throw new Error(`expected ${selector}`) @@ -525,8 +529,12 @@ describe('TextPreview — navigation and view', () => { act(() => { PendingIntersectionObserver.instances[0]!.intersect(block) }) await waitFor(() => { expect(view.container.querySelector('[data-code-preview] pre.shiki')).not.toBeNull() }) expect(scrollport(view.container)).toBe(codeScrollport) + // The band lives in the scrollport, so the highlighted swap keeps it. + expect(marks(view.container).map(mark => [mark.parentElement, mark.dataset.textpreviewMark, mark.style.top])) + .toEqual([[codeScrollport, '2', `${LINE_HEIGHT}px`]]) view.rerender() expect(codeScrollport.scrollTop).toBe(2 * LINE_HEIGHT) + expect(marks(view.container).map(mark => mark.dataset.textpreviewMark)).toEqual(['3']) expect(h.instance.getSnapshot().byTab[TAB_ID]?.revision).toBe(2) }) @@ -540,6 +548,12 @@ describe('TextPreview — navigation and view', () => { expect(h.read).toHaveBeenLastCalledWith(SESSION, PATH, 4, h.controller.signal) expect(body(view.container).scrollTop).toBe(4 * LINE_HEIGHT) expect(target(view.container)).toBe('5') + const [mark] = marks(view.container) + expect(mark?.parentElement).toBe(body(view.container)) + expect(mark?.dataset.textpreviewMark).toBe('5') + expect(mark?.style.top).toBe(`${4 * LINE_HEIGHT}px`) + fireEvent.animationEnd(mark!) + expect(marks(view.container)).toEqual([]) expect(h.instance.getSnapshot().byTab[TAB_ID]?.revision).toBe(1) expect(h.instance.getSnapshot().byTab[TAB_ID]?.scrollTop).toBe(4 * LINE_HEIGHT) }) @@ -554,6 +568,7 @@ describe('TextPreview — navigation and view', () => { const second = render() await settle() expect(body(second.container).scrollTop).toBe(300) + expect(marks(second.container)).toEqual([]) }) it('jumps again for a new navigation to the same tab', async () => { @@ -564,6 +579,7 @@ describe('TextPreview — navigation and view', () => { view.rerender() expect(body(view.container).scrollTop).toBe(1 * LINE_HEIGHT) expect(target(view.container)).toBe('2') + expect(marks(view.container).map(mark => mark.dataset.textpreviewMark)).toEqual(['2']) expect(h.instance.getSnapshot().byTab[TAB_ID]?.revision).toBe(2) }) @@ -573,6 +589,7 @@ describe('TextPreview — navigation and view', () => { await settle() expect(h.read).toHaveBeenCalledTimes(1) expect(target(view.container)).toBeNull() + expect(marks(view.container)).toEqual([]) expect(body(view.container).scrollTop).toBe(0) expect(h.instance.getSnapshot().byTab[TAB_ID]?.revision).toBe(1) view.rerender() From 7ab2ddd7a9cdd7a6a2348f2cd92f40c8aa1b68e5 Mon Sep 17 00:00:00 2001 From: Turtle Date: Tue, 13 Oct 2026 22:11:53 +0800 Subject: [PATCH 053/369] fix(client): drop the line mark when its preview reflows A resized scrollport or a wrap toggle can move the marked line, so the band is removed instead of marking another line. Document the exact timing and why reduced motion keeps the opacity fade. --- .../README.i18n.yaml | 4 +-- .../ui-sidebar-documentpreview/README.md | 2 +- .../ui-sidebar-documentpreview/README.zh.md | 2 +- .../src/client/TextPreview.module.css | 3 +- .../src/client/TextPreview.tsx | 10 ++++-- .../src/client/text/lines.ts | 28 +++++++++++++--- .../tests/text-preview.client.spec.tsx | 33 ++++++++++++++++++- 7 files changed, 70 insertions(+), 12 deletions(-) diff --git a/packages/client/ui-sidebar-documentpreview/README.i18n.yaml b/packages/client/ui-sidebar-documentpreview/README.i18n.yaml index ccceabe69d..97adbc6c21 100644 --- a/packages/client/ui-sidebar-documentpreview/README.i18n.yaml +++ b/packages/client/ui-sidebar-documentpreview/README.i18n.yaml @@ -30,8 +30,8 @@ en: 372d8ce6492e6896 zh: 912f613bc9221aa3 /deepseek-ai-dsh-client-ui-sidebar-documentpreview/navigation: - en: 35f843f02c858674 - zh: 44af8e323a12d4dc + en: 59256644b39e412d + zh: 8ab908393b018431 /deepseek-ai-dsh-client-ui-sidebar-documentpreview/model-experience: en: ca1deb99d0e44755 zh: f285fc56cee669ec diff --git a/packages/client/ui-sidebar-documentpreview/README.md b/packages/client/ui-sidebar-documentpreview/README.md index ff61b2bd5b..43ac41c129 100644 --- a/packages/client/ui-sidebar-documentpreview/README.md +++ b/packages/client/ui-sidebar-documentpreview/README.md @@ -123,7 +123,7 @@ The page refresh shortcut reuses the selected preview’s reload operation, incl ## Navigation -`ctx.sidebarRight.openResource(address, { params: { line } })` carries a 1-based source line through the `file` parameters. In `text-pages` mode, the owner loads sequential pages until that line or EOF. Plain-text and code renderers expose source-line anchors; Markdown does not. A navigation remains pending while its selected renderer has no anchor and runs if the user switches to plain text or code. Code navigation scrolls the inner source viewport directly. The landed line receives a band that holds briefly and then fades out over two seconds; restoring a remounted tab's scroll position adds no band. Byte-mode renderers do not consume source-line navigation. Each completed navigation revision is answered once. Opening the same file without `revealIfOpened: false` focuses its existing tab and delivers a new revision. +`ctx.sidebarRight.openResource(address, { params: { line } })` carries a 1-based source line through the `file` parameters. In `text-pages` mode, the owner loads sequential pages until that line or EOF. Plain-text and code renderers expose source-line anchors; Markdown does not. A navigation remains pending while its selected renderer has no anchor and runs if the user switches to plain text or code. Code navigation scrolls the inner source viewport directly. The landed line receives a band that holds for 0.6 s and then fades out over 1.4 s; toggling wrap or resizing the preview removes it early; restoring a remounted tab's scroll position adds no band. Byte-mode renderers do not consume source-line navigation. Each completed navigation revision is answered once. Opening the same file without `revealIfOpened: false` focuses its existing tab and delivers a new revision. ## Model Experience diff --git a/packages/client/ui-sidebar-documentpreview/README.zh.md b/packages/client/ui-sidebar-documentpreview/README.zh.md index 586288b986..daaf6288ab 100644 --- a/packages/client/ui-sidebar-documentpreview/README.zh.md +++ b/packages/client/ui-sidebar-documentpreview/README.zh.md @@ -123,7 +123,7 @@ Office Remote 通过 Connection 的 multipart 二进制传输返回原生 `Uint8 ## 导航 -`ctx.sidebarRight.openResource(address, { params: { line } })` 通过 `file` 参数携带 1 起算的源码行号。在 `text-pages` 模式下,owner 顺序加载到该行或 EOF。纯文本与代码渲染器提供源码行锚点;Markdown 不提供。所选渲染器没有锚点时,导航保持待处理;用户切换到纯文本或代码后执行。代码导航直接滚动内部源码视口。落定的行显示一条短暂停留后在两秒内渐隐的高亮带;重新挂载的 tab 恢复滚动位置时不显示高亮带。字节模式渲染器不消费源码行导航。每个完成的导航 revision 只响应一次。不带 `revealIfOpened: false` 打开同一文件时聚焦已有 tab,并送达新 revision。 +`ctx.sidebarRight.openResource(address, { params: { line } })` 通过 `file` 参数携带 1 起算的源码行号。在 `text-pages` 模式下,owner 顺序加载到该行或 EOF。纯文本与代码渲染器提供源码行锚点;Markdown 不提供。所选渲染器没有锚点时,导航保持待处理;用户切换到纯文本或代码后执行。代码导航直接滚动内部源码视口。落定的行显示一条高亮带,停留 0.6 秒后在 1.4 秒内渐隐;切换换行或调整预览尺寸会提前移除它;重新挂载的 tab 恢复滚动位置时不显示高亮带。字节模式渲染器不消费源码行导航。每个完成的导航 revision 只响应一次。不带 `revealIfOpened: false` 打开同一文件时聚焦已有 tab,并送达新 revision。 ## 模型体验 diff --git a/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.module.css b/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.module.css index 34bed20455..dc6a77e68f 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.module.css +++ b/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.module.css @@ -109,7 +109,8 @@ /* A navigated line's mark: a band in the scrollport over the line, so renderer re-renders such as syntax highlighting do not drop it. It holds briefly, then - fades out and removes itself. */ + fades out and removes itself. Reduced motion keeps the fade: it is an + opacity change, not movement, and its end removes the band. */ .lineMark { position: absolute; left: 0; diff --git a/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.tsx b/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.tsx index 27044d5653..ecb0f6cb13 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.tsx +++ b/packages/client/ui-sidebar-documentpreview/src/client/TextPreview.tsx @@ -29,7 +29,7 @@ import { binaryDocumentPath, matchingDocumentPreviews } from './document/registr import type { DocumentPreviewDefinition } from './document/registry.ts' import { unviewableBinaryPath } from './document/unviewable.ts' import { PLAIN_BODY_ID } from './text/index.ts' -import { loadedPages, lastLineLoaded, markLine, scrollToLine } from './text/lines.ts' +import { clearLineMark, loadedPages, lastLineLoaded, markLine, scrollToLine } from './text/lines.ts' import css from './TextPreview.module.css' export { linesOf, loadedPages, lastLineLoaded, scrollToLine } from './text/lines.ts' @@ -293,7 +293,13 @@ export function TextPreview({ aria-pressed={state.wrap} aria-label={t('wrap.aria')} data-textpreview-tool="wrap" - onClick={() => { actions.toggledWrap(tab.id) }} + onClick={() => { + actions.toggledWrap(tab.id) + // Wrapping reflows the lines under a line mark without resizing its scrollport. + const body = scrollportRef.current + /* v8 ignore next -- callback refs bind the scrollport during commit, before user input. */ + if (body !== null) clearLineMark(body) + }} > {state.wrap ? : } diff --git a/packages/client/ui-sidebar-documentpreview/src/client/text/lines.ts b/packages/client/ui-sidebar-documentpreview/src/client/text/lines.ts index 2100a3c123..5c702d6aae 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/text/lines.ts +++ b/packages/client/ui-sidebar-documentpreview/src/client/text/lines.ts @@ -55,20 +55,40 @@ export function scrollToLine(body: HTMLElement, line: number): HTMLElement | und /** * Mark a revealed line with a fading band. The band is a child of the scrollport, - * not of the line, so a renderer that rebuilds its lines keeps the mark; it - * replaces an earlier mark and removes itself when its animation ends. + * not of the line, so a renderer that rebuilds its lines keeps the mark. It + * replaces an earlier mark and removes itself when its animation ends or when + * the scrollport is resized, because a resize can reflow the lines above it. * @param body - the scrollport that {@link scrollToLine} scrolled. * @param row - the line element it returned. * @param line - 1-based source line, recorded on the band. */ export function markLine(body: HTMLElement, row: HTMLElement, line: number): void { - body.querySelector(':scope > [data-textpreview-mark]')?.remove() + clearLineMark(body) const mark = body.ownerDocument.createElement('div') mark.className = clsx(css.lineMark) mark.setAttribute('data-textpreview-mark', String(line)) mark.style.top = `${row.offsetTop}px` mark.style.height = `${row.offsetHeight}px` mark.style.width = `${body.scrollWidth}px` - mark.addEventListener('animationend', () => { mark.remove() }, { once: true }) + // The first observation reports the current size; only later ones are resizes. + let measured = false + const observer = new ResizeObserver(() => { + if (measured) dismiss() + measured = true + }) + function dismiss(): void { + observer.disconnect() + mark.remove() + } + mark.addEventListener('animationend', dismiss, { once: true }) body.append(mark) + observer.observe(body) +} + +/** + * Remove the scrollport's line mark, for layout changes that move lines without resizing it. + * @param body - the scrollport that may hold a mark. + */ +export function clearLineMark(body: HTMLElement): void { + body.querySelector(':scope > [data-textpreview-mark]')?.remove() } diff --git a/packages/client/ui-sidebar-documentpreview/tests/text-preview.client.spec.tsx b/packages/client/ui-sidebar-documentpreview/tests/text-preview.client.spec.tsx index 627f6a9e9b..1975816c27 100644 --- a/packages/client/ui-sidebar-documentpreview/tests/text-preview.client.spec.tsx +++ b/packages/client/ui-sidebar-documentpreview/tests/text-preview.client.spec.tsx @@ -9,7 +9,7 @@ * set to. Both are the browser's job; the specs assert the body's arithmetic * over them. */ -import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest' +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import { act, cleanup, fireEvent, render, waitFor } from '@testing-library/react' import { RemoteError } from '@deepseek-ai/dsh-client-test-runtime' import type { TabId } from '@deepseek-ai/dsh-client-ui-dockkit' @@ -54,6 +54,21 @@ afterAll(() => { } }) +/** Records each scrollport observer so a spec can report a resize to it. */ +class RecordingResizeObserver { + static instances: RecordingResizeObserver[] = [] + readonly targets: Element[] = [] + constructor(readonly callback: () => void) { RecordingResizeObserver.instances.push(this) } + observe(target: Element): void { this.targets.push(target) } + disconnect(): void {} + resize(): void { act(() => { this.callback() }) } +} + +beforeEach(() => { + RecordingResizeObserver.instances = [] + vi.stubGlobal('ResizeObserver', RecordingResizeObserver) +}) + afterEach(() => { cleanup() vi.unstubAllGlobals() @@ -583,6 +598,22 @@ describe('TextPreview — navigation and view', () => { expect(h.instance.getSnapshot().byTab[TAB_ID]?.revision).toBe(2) }) + it('drops the line mark when the scrollport resizes or wrapping reflows its lines', async () => { + const h = harness({ 1: page(1, ['a', 'b', 'c'], true) }) + const view = render() + await settle() + const observer = RecordingResizeObserver.instances.find(item => item.targets.includes(body(view.container))) + observer!.resize() + expect(marks(view.container).map(mark => mark.dataset.textpreviewMark)).toEqual(['3']) + observer!.resize() + expect(marks(view.container)).toEqual([]) + + view.rerender() + expect(marks(view.container)).toHaveLength(1) + fireEvent.click(view.container.querySelector('[data-textpreview-tool="wrap"]')!) + expect(marks(view.container)).toEqual([]) + }) + it('stops at the end of the file for a line past it, and answers a navigation without a line', async () => { const h = harness({ 1: page(1, ['a', 'b'], true) }) const view = render() From 25ebc2450da12c9ec2b590c37b1a424ce3e2defd Mon Sep 17 00:00:00 2001 From: Turtle Date: Wed, 14 Oct 2026 13:12:34 +0800 Subject: [PATCH 054/369] Credit review by @imccyu Co-authored-by: imccyu <276526105+imccyu@users.noreply.github.com> From e103591fd5bc2ad8af04e6bca0fc4005b3eeaa0b Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Wed, 14 Oct 2026 13:40:25 +0800 Subject: [PATCH 055/369] fix(subagent): address model selection review feedback --- docs/subsystems/subagent.md | 8 ---- docs/subsystems/subagent.zh.md | 8 ---- docs/tool-catalog.i18n.yaml | 8 ++-- docs/tool-catalog.md | 4 +- docs/tool-catalog.zh.md | 4 +- .../guide.i18n.yaml | 4 +- .../guide.md | 2 + .../guide.zh.md | 2 + .../extensions/tool-cordis/src/api-catalog.ts | 9 +--- .../subagent/tool-subagent/README.i18n.yaml | 4 +- packages/subagent/tool-subagent/README.md | 2 +- packages/subagent/tool-subagent/README.zh.md | 2 +- packages/subagent/tool-subagent/src/index.ts | 9 ++-- .../subagent/tool-subagent/src/list-models.ts | 2 +- .../src/model-selection-settings.ts | 2 +- .../src/model-selection-state.ts | 5 +- .../tool-subagent/src/model-selection.ts | 5 +- .../tests/model-selection-settings.spec.ts | 33 ++++++++----- .../tests/model-selection.spec.ts | 47 +++++++++++++++++-- scripts/gen-tool-catalog.ts | 2 +- .../tool-schemas.expected.json | 4 +- .../tool-schemas.expected.json | 4 +- .../system-prompt.expected.md | 4 +- .../system-prompt.expected.md | 4 +- .../web/ptc-round/system-prompt.expected.md | 4 +- 25 files changed, 112 insertions(+), 70 deletions(-) diff --git a/docs/subsystems/subagent.md b/docs/subsystems/subagent.md index a6910315b3..b54ce57175 100644 --- a/docs/subsystems/subagent.md +++ b/docs/subsystems/subagent.md @@ -406,14 +406,6 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp Host settings owner that records Session authorization before the first model request. -```ts cordis-catalog -/** - * Read a detached selection preference for the next fresh top-level Session. - * @returns the enabled state and exact allowed routes. - */ -current(): SubagentModelSelectionSettings -``` - Source: [`packages/subagent/tool-subagent/src/model-selection-settings.ts`](../../packages/subagent/tool-subagent/src/model-selection-settings.ts) diff --git a/docs/subsystems/subagent.zh.md b/docs/subsystems/subagent.zh.md index 8c4a34cd1a..0ca87c8503 100644 --- a/docs/subsystems/subagent.zh.md +++ b/docs/subsystems/subagent.zh.md @@ -408,14 +408,6 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp Host settings owner that records Session authorization before the first model request. -```ts cordis-catalog -/** - * Read a detached selection preference for the next fresh top-level Session. - * @returns the enabled state and exact allowed routes. - */ -current(): SubagentModelSelectionSettings -``` - Source: [`packages/subagent/tool-subagent/src/model-selection-settings.ts`](../../packages/subagent/tool-subagent/src/model-selection-settings.ts) diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml index 72364bdbef..4381738a1e 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: e4b330276e8729aa - zh: 3f781cb32fd16d81 + en: 6db10eb3e60a3015 + zh: 721026cb011ca99e /tool-schema-catalog/deepseek-ai-dsh-plugin-manager: en: e0143916bb634762 zh: e0143916bb634762 @@ -225,8 +225,8 @@ en: 841abe0dd5913ea7 zh: 841abe0dd5913ea7 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent/list-subagent-models: - en: 6872bee11fccb6c8 - zh: 230f2d443dc5d9a6 + en: 207228068bc61885 + zh: 36e9344ee1af8869 /tool-schema-catalog/deepseek-ai-dsh-tool-subagent/subagent: en: 5bcfcf9ca4117096 zh: 38cb646632b457b2 diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index 0ca8251ffd..077253129f 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -37,7 +37,7 @@ This table connects model-visible tool names to the plugin package and service s | `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`, `ctx.workflowEngine`, `ctx.subagents`, `ctx.systemPrompt`, `a calling Agent (exec.agent parents every fresh round)` | `tool/call`, `tool/result`, `workflow and child session events during execution` | - | A fixed foreground workflow starts one fresh structured child per round; the model selects only the immutable objective and an optional round cap. | | `@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` | - | 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-tool-subagent` | `list_subagent_models`, `subagent` | `ctx.tools`, `ctx.subagents`, `ctx.systemPrompt`, `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. The independent `/list-models` plugin registers discovery. Web and Desktop presets mount discovery, enable spawn 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. The independent `/list-models` plugin registers discovery. Web and Desktop presets mount discovery, enable spawn 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-control` | `interrupt_agent`, `list_agents`, `send_message` | `ctx.tools`, `ctx.subagents`, `ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`, `tool/result`, `child session events through ctx.subagents` | - | The globally named discovery, interruption, and local-message tools over managed subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` and `interrupt_agent` once, plus `list_agents` from its separately loaded `/list-agents` plugin (whose catalog rows use the sessionProjections and live Agent registries). | | `@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-experimental-tool-agent-team` | `interrupt_agent`, `list_agents`, `send_message`, `spawn_teammate`, `team_task_create`, `team_task_get`, `team_task_list`, `team_task_update`, `wait_agent` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agentTeams`, `an exact live Team member Agent` | `tool/call`, `team/member`, `team/task`, `agent/inbox/spliced`, `user/message on inbox claim`, `tool/result` | - | All nine tools are scoped to implicit Team Leads and durable teammates. The shipped dsh-base bundle keeps the package disabled; the documented Agent Teams profile patch enables it while disabling the legacy continuable-child control names. | @@ -2017,7 +2017,7 @@ The five read-only tools hide provider cursors and authorize every result from t ### `list_subagent_models` -Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. +Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Inspect options here before selecting a child provider and model; use returned ids for delegation. ```json { diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index 230ebbb0b3..cafe401972 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -41,7 +41,7 @@ | `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`、`ctx.workflowEngine`、`ctx.subagents`、`ctx.systemPrompt`、`a calling Agent (exec.agent parents every fresh round)` | `tool/call`、`tool/result`、`workflow and child session events during execution` | - | 固定的前台工作流会在每个 Round 启动一个全新的结构化子级;模型只能选择不可变目标和可选的 Round 上限。 | | `@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-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 加载时的 `toolName` 选择委派名称(默认为 `subagent`);`modelSelection` 增加可选路由字段。独立的 `/list-models` 插件注册发现工具。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` 增加可选路由字段。独立的 `/list-models` 插件注册发现工具。Web 与 Desktop preset 挂载发现工具,启用 spawn 路由字段,并为 `subagent_fork` 保持路由字段关闭。Host 设置服务为每个新顶层 Session 记录授权;子 Session 继承该授权。没有已记录策略时,发现和显式选择只允许当前父级路由。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 | | `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`、`list_agents`、`send_message` | `ctx.tools`、`ctx.subagents`、`ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`、`tool/result`、`child session events through ctx.subagents` | - | 这些是用于发现、中断托管 subagent 和向本地子代理发送消息的全局命名工具:绑定提供方的 `tool-subagent` 实例注册不同的委派工具;本包注册一次 `send_message` 和 `interrupt_agent`,另由 `list_agents` 通过单独加载的 `/list-agents` 插件提供,其目录行使用 sessionProjections 和实时 Agent 注册表。 | | `@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-experimental-tool-agent-team` | `interrupt_agent`、`list_agents`、`send_message`、`spawn_teammate`、`team_task_create`、`team_task_get`、`team_task_list`、`team_task_update`、`wait_agent` | `ctx.tools`、`ctx.systemPrompt`、`ctx.agentTeams`、`an exact live Team member Agent` | `tool/call`、`team/member`、`team/task`、`agent/inbox/spliced`、`user/message on inbox claim`、`tool/result` | - | 这 9 个工具限定于隐式 Team Lead 与持久 teammate 作用域。随产品发布的 dsh-base bundle 默认禁用该包;文档中的 Agent Teams profile patch 会启用它,并禁用旧 continuable child 的同名控制工具。 | @@ -2023,7 +2023,7 @@ lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后, ### `list_subagent_models` -查询 subagent 可用的模型,不更改当前 Agent。无参数:提供方;`provider`:允许的模型;`provider` + `model`:该模型及其推理强度。将返回的 id 用于委派。 +查询 subagent 可用的模型,不更改当前 Agent。无参数:提供方;`provider`:允许的模型;`provider` + `model`:该模型及其推理强度。选择子级提供方和模型前先在此查询选项,再将返回的 id 用于委派。 ```json { diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/subagent-model-selection-composition/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.2/subagent-model-selection-composition/guide.i18n.yaml index 5cc66aa405..565e20541f 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.2/subagent-model-selection-composition/guide.i18n.yaml +++ b/docs/upgrade-guide/v0.2.1-alpha.2/subagent-model-selection-composition/guide.i18n.yaml @@ -9,8 +9,8 @@ en: c54f686a434d8b84 zh: f32b63140d9b2dda /separate-subagent-model-selection-configuration/change: - en: 6773f124a0ff9f6a - zh: 99213e0ba2caff35 + en: 3d948fe459b20e23 + zh: cb3c924d9fde2432 /separate-subagent-model-selection-configuration/migration: en: b83bd0256fb89c92 zh: 5b14cf7cfa9677b3 diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/subagent-model-selection-composition/guide.md b/docs/upgrade-guide/v0.2.1-alpha.2/subagent-model-selection-composition/guide.md index 72411e4bc9..d76f6f28e1 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.2/subagent-model-selection-composition/guide.md +++ b/docs/upgrade-guide/v0.2.1-alpha.2/subagent-model-selection-composition/guide.md @@ -11,6 +11,8 @@ English | [中文](guide.zh.md) `@deepseek-ai/dsh-tool-subagent` replaces `modelSelectionSettings` with `modelSelection`, which controls only optional child route fields. The tool no longer registers `list_subagent_models` or samples user settings. The Host `/model-selection-settings` service records authorization during Agent creation, including Sessions created before delegation tools are mounted. Existing Sessions keep their recorded policy; installing tools later does not resample settings. Shipped Web and Desktop presets include the independent discovery plugin and keep fork route fields disabled. Custom fork tools may enable them. +A Host with model selection enabled and an empty allowed-model list rejects every fresh top-level Session, including those without delegation tools. The settings UI prevents saving that combination; configurations written directly must supply a non-empty list or disable selection. + ## Migration 1. In custom `cordis.yml`, patches, or direct plugin configuration, rename `modelSelectionSettings: true` to `modelSelection: true` on delegation tools that expose route arguments. diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/subagent-model-selection-composition/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.2/subagent-model-selection-composition/guide.zh.md index f312728181..336e18d4ee 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.2/subagent-model-selection-composition/guide.zh.md +++ b/docs/upgrade-guide/v0.2.1-alpha.2/subagent-model-selection-composition/guide.zh.md @@ -11,6 +11,8 @@ description: 自定义 subagent 组合必须分离路由字段、模型发现和 `@deepseek-ai/dsh-tool-subagent` 将 `modelSelectionSettings` 替换为 `modelSelection`,该选项只控制可选子级路由字段。工具不再注册 `list_subagent_models` 或采样用户设置。Host 的 `/model-selection-settings` 服务在 Agent 创建时记录授权,包括在委派工具挂载前创建的 Session。已有 Session 保留已记录策略;之后安装工具不会重新采样设置。随附 Web 与 Desktop preset 包含独立发现插件,并为 fork 保持路由字段关闭。自定义 fork 工具可以启用这些字段。 +Host 启用模型选择但允许模型列表为空时,会拒绝创建所有新的顶层 Session,包括未挂载委派工具的 Session。设置界面不允许保存这种组合;直接写入的配置必须提供非空列表或关闭模型选择。 + ## 迁移 1. 在自定义 `cordis.yml`、patch 或直接插件配置中,将公开路由参数的委派工具的 `modelSelectionSettings: true` 改为 `modelSelection: true`。 diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 6fb7b5084b..850553cae0 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -2958,14 +2958,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ key: 'subagentModelSelection', summary: 'Host settings owner that records Session authorization before the first model request.', description: 'Host settings owner that records Session authorization before the first model request.', - methods: [ - { - signature: 'current(): SubagentModelSelectionSettings', - description: 'Read a detached selection preference for the next fresh top-level Session.', - parameters: [], - returns: 'the enabled state and exact allowed routes.', - }, - ], + methods: [], }, { key: 'subagents', diff --git a/packages/subagent/tool-subagent/README.i18n.yaml b/packages/subagent/tool-subagent/README.i18n.yaml index 621428968a..1290c018fe 100644 --- a/packages/subagent/tool-subagent/README.i18n.yaml +++ b/packages/subagent/tool-subagent/README.i18n.yaml @@ -69,8 +69,8 @@ en: a4ca31b9cbacda6b zh: 91ac3e0784a14697 /deepseek-ai-dsh-tool-subagent/model-experience/model-selection-and-discovery/what-the-model-sees: - en: 192670f899eb74f5 - zh: 06eac751268c3a9d + en: 9cf8be0502db31dd + zh: 16eebce205f12364 /deepseek-ai-dsh-tool-subagent/model-experience/model-selection-and-discovery/token-effect: en: 4fa61dc9a52259a2 zh: bc51ac3b1a1cc6ad diff --git a/packages/subagent/tool-subagent/README.md b/packages/subagent/tool-subagent/README.md index 46be412d92..1a4f3b3677 100644 --- a/packages/subagent/tool-subagent/README.md +++ b/packages/subagent/tool-subagent/README.md @@ -152,7 +152,7 @@ Prefix-stable while provider instances and their configuration are unchanged. Ad #### What the model sees -The independent `list_subagent_models` definition reads the calling Session’s route policy, falling back to the current parent route when no policy is recorded. Calls reject while the optional `ctx.llm` service is unavailable. With a recorded policy, provider model lists intersect the advertised catalog with the Session’s authorized routes. Without one, they resolve only the current parent model, even if the catalog omits it. Exact-model queries accept authorized ids absent from the catalog; unauthorized provider/model lookups fail before adapter access. Catalog or exact-model lookup failures and cancellation reject the call. Execution independently enforces the same policy. +The independent `list_subagent_models` definition reads the calling Session’s route policy, falling back to the current parent route when no policy is recorded. Calls reject while the optional `ctx.llm` service is unavailable. With a recorded policy, provider model lists intersect the advertised catalog with the Session’s authorized routes. Without one, they resolve only the current parent model, even if the catalog omits it. The discovery tool’s own description instructs the model to query options before selecting a child route; omitting or filtering out discovery removes that guidance. Exact-model queries accept authorized ids absent from the catalog; unauthorized provider/model lookups fail before adapter access. Catalog or exact-model lookup failures and cancellation reject the call. Execution independently enforces the same policy. #### Token effect diff --git a/packages/subagent/tool-subagent/README.zh.md b/packages/subagent/tool-subagent/README.zh.md index d1b9c788e1..5a14b36b6f 100644 --- a/packages/subagent/tool-subagent/README.zh.md +++ b/packages/subagent/tool-subagent/README.zh.md @@ -152,7 +152,7 @@ kind: "package-reference" #### 模型看到什么 -独立的 `list_subagent_models` 定义读取调用 Session 的路由策略,没有已记录策略时使用当前父级路由。可选的 `ctx.llm` 服务不可用时,调用会被拒绝。有已记录策略时,提供方模型列表取公布目录与 Session 授权路由的交集。没有策略时,只解析当前父级模型,即使目录未列出它。精确模型查询允许已授权但目录未列出的 ID;未授权的提供方/模型查询会在访问适配器前失败。目录或精确模型查询失败以及取消都会使调用被拒绝。执行过程独立强制同一策略。 +独立的 `list_subagent_models` 定义读取调用 Session 的路由策略,没有已记录策略时使用当前父级路由。可选的 `ctx.llm` 服务不可用时,调用会被拒绝。有已记录策略时,提供方模型列表取公布目录与 Session 授权路由的交集。没有策略时,只解析当前父级模型,即使目录未列出它。发现工具自身的描述要求模型在选择子级路由前查询选项;未挂载或过滤掉发现工具时,这段指导也不会出现。精确模型查询允许已授权但目录未列出的 ID;未授权的提供方/模型查询会在访问适配器前失败。目录或精确模型查询失败以及取消都会使调用被拒绝。执行过程独立强制同一策略。 #### Token 影响 diff --git a/packages/subagent/tool-subagent/src/index.ts b/packages/subagent/tool-subagent/src/index.ts index b7c95a8e1a..03a5fbe514 100644 --- a/packages/subagent/tool-subagent/src/index.ts +++ b/packages/subagent/tool-subagent/src/index.ts @@ -193,8 +193,8 @@ export async function apply(ctx: Context, config: Config): Promise { const wording = providerWording(subagentProvider.inheritsParentContext) const providerRouteDefaults = subagentProvider.agentRouteDefaults const selectionDescription = providerRouteDefaults !== undefined - ? ' Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to this backend\'s route defaults. When the effective route changes, an omitted `reasoning_effort` uses the model\'s default.' - : ' Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model\'s default.' + ? ' Supply `provider` and `model` together. Omitted values use child defaults, falling back to this backend\'s route defaults. When the effective route changes, an omitted `reasoning_effort` uses the model\'s default.' + : ' Supply `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model\'s default.' const definition = defineTool({ name: toolName, description: wording.description @@ -202,7 +202,10 @@ export async function apply(ctx: Context, config: Config): Promise { + (subagentProvider.prepareContinuable !== undefined ? ' The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes.' : ' The completion notice includes its final answer. This backend does not accept follow-up messages.') - + (modelSelectionEnabled ? selectionDescription : ''), + + (modelSelectionEnabled ? selectionDescription : '') + + (modelSelectionEnabled && subagentProvider.inheritsParentContext + ? ' Changing the route can prevent provider-side reuse of the inherited conversation prefix.' + : ''), parameters: { cwd: { type: 'string', diff --git a/packages/subagent/tool-subagent/src/list-models.ts b/packages/subagent/tool-subagent/src/list-models.ts index 4a81c568f6..f220c85e32 100644 --- a/packages/subagent/tool-subagent/src/list-models.ts +++ b/packages/subagent/tool-subagent/src/list-models.ts @@ -109,7 +109,7 @@ export function apply(ctx: Context): void { description: 'Inspect subagent models without changing the current Agent. No arguments: providers; ' + '`provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. ' - + 'Use returned ids for delegation.', + + 'Inspect options here before selecting a child provider and model; use returned ids for delegation.', parameters: { provider: { type: 'string', diff --git a/packages/subagent/tool-subagent/src/model-selection-settings.ts b/packages/subagent/tool-subagent/src/model-selection-settings.ts index 9e4907556b..a7954c3850 100644 --- a/packages/subagent/tool-subagent/src/model-selection-settings.ts +++ b/packages/subagent/tool-subagent/src/model-selection-settings.ts @@ -72,7 +72,7 @@ export class SubagentModelSelectionConfig extends Service { * Read a detached selection preference for the next fresh top-level Session. * @returns the enabled state and exact allowed routes. */ - current(): SubagentModelSelectionSettings { + private current(): SubagentModelSelectionSettings { const enabled = this.config.enabled.get() const allowedModels = this.config.allowedModels.get() assertAllowedModelRoutes(allowedModels) diff --git a/packages/subagent/tool-subagent/src/model-selection-state.ts b/packages/subagent/tool-subagent/src/model-selection-state.ts index 221ef5a8f6..26516749bd 100644 --- a/packages/subagent/tool-subagent/src/model-selection-state.ts +++ b/packages/subagent/tool-subagent/src/model-selection-state.ts @@ -33,7 +33,10 @@ const modelSelectionPolicySchema: zod.ZodType = zod. model: zod.string().min(1), }).strict()).min(1).nullable() -/** Host-only projection of the durable model-selection policy. */ +/** + * Host-only projection of the durable model-selection policy. + * Each independently mounted consumer registers this shared definition; the registry reference-counts it. + */ export const subagentModelSelectionProjectionDefinition = { key: 'subagentModelSelectionPolicy', stateVersion: 1, diff --git a/packages/subagent/tool-subagent/src/model-selection.ts b/packages/subagent/tool-subagent/src/model-selection.ts index 04f4d33b5f..911901a2ce 100644 --- a/packages/subagent/tool-subagent/src/model-selection.ts +++ b/packages/subagent/tool-subagent/src/model-selection.ts @@ -152,7 +152,10 @@ export function assertAllowedModelSelection( ? provider === parentOptions.provider && model === parentOptions.model : policy.routes.some(route => route.provider === provider && route.model === model) if (allowed) return - throw new Error(`child LLM route "${provider}/${model}" is not allowed for this Session`) + throw new Error(`child LLM route "${provider}/${model}" is not allowed for this Session` + + (policy === undefined + ? '; without a recorded model-selection policy, explicit selection (including reasoning_effort) is limited to the parent route' + : '')) } /** diff --git a/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts b/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts index 0718087bb6..1b2b11390a 100644 --- a/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts +++ b/packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts @@ -78,51 +78,62 @@ async function createAgent(ctx: Context, id: string, options: { return handle.agent } +let policySessionCounter = 0 + +/** Observe settings through the policy captured by a fresh top-level Agent. */ +async function capturePolicy(ctx: Context) { + const { agent } = await ctx.agents.create({ sessionId: SessionId(`settings-policy-${++policySessionCounter}`) }) + return subagentModelSelectionPolicy(ctx.sessionProjections, agent.session) +} + describe('SubagentModelSelectionConfig', () => { it('uses the composed default without a settings provider', async () => { const ctx = ownContext(new Context()) - await mountAgentLoopTestDependencies(ctx) + await mountAgentLoopTestDependencies(ctx, { workingDirectory: true }) + await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(SubagentModelSelectionConfig, { enabled: true, allowedModels: ALLOWED_MODELS }) - expect(ctx.subagentModelSelection.current()).toEqual({ enabled: true, allowedModels: ALLOWED_MODELS }) + expect(await capturePolicy(ctx)).toEqual(ALLOWED_MODELS) await ctx.fiber.dispose() }) it('defaults off and follows the validated user layer', async () => { const ctx = ownContext(new Context()) - await mountAgentLoopTestDependencies(ctx) + await mountAgentLoopTestDependencies(ctx, { workingDirectory: true }) + await ctx.plugin(AgentLoop, { agents: [] }) selectionConfigs.set(ctx, await liveConfig(ctx, SubagentModelSelectionConfig)) - expect(ctx.subagentModelSelection.current()).toEqual({ enabled: false, allowedModels: [] }) + expect(await capturePolicy(ctx)).toBeUndefined() await selectionConfigs.get(ctx)!.update({ enabled: true, allowedModels: ALLOWED_MODELS, }) - expect(ctx.subagentModelSelection.current()).toEqual({ enabled: true, allowedModels: ALLOWED_MODELS }) + expect(await capturePolicy(ctx)).toEqual(ALLOWED_MODELS) await ctx.fiber.dispose() }) it('rejects duplicate routes, enabled empty settings, and an empty durable policy', async () => { const ctx = ownContext(new Context()) - await mountAgentLoopTestDependencies(ctx) + await mountAgentLoopTestDependencies(ctx, { workingDirectory: true }) + await ctx.plugin(AgentLoop, { agents: [] }) selectionConfigs.set(ctx, await liveConfig(ctx, SubagentModelSelectionConfig)) ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition) - // A stored policy the service cannot apply fails when it is read, not when the profile row is written. + // File-written invalid settings reject Agent creation even without delegation tools. await selectionConfigs.get(ctx)!.update({ allowedModels: [...ALLOWED_MODELS, ...ALLOWED_MODELS] }) - expect(() => ctx.subagentModelSelection.current()).toThrow('repeats route "alpha/fast-model"') + await expect(capturePolicy(ctx)).rejects.toThrow('repeats route "alpha/fast-model"') await selectionConfigs.get(ctx)!.update({ enabled: true, allowedModels: [] }) - expect(() => ctx.subagentModelSelection.current()).toThrow('enabled subagent model selection requires at least one allowed model') + await expect(capturePolicy(ctx)).rejects.toThrow('enabled subagent model selection requires at least one allowed model') await selectionConfigs.get(ctx)!.update({ enabled: false, allowedModels: ALLOWED_MODELS, }) - expect(ctx.subagentModelSelection.current()).toEqual({ enabled: false, allowedModels: ALLOWED_MODELS }) + expect(await capturePolicy(ctx)).toBeUndefined() await selectionConfigs.get(ctx)!.update({ allowedModels: [] }) - expect(ctx.subagentModelSelection.current()).toEqual({ enabled: false, allowedModels: [] }) + expect(await capturePolicy(ctx)).toBeUndefined() const invalid = Session.create(SessionId('empty-policy')) invalid.append('subagent/model-selection-policy', { allowedModels: [] }) diff --git a/packages/subagent/tool-subagent/tests/model-selection.spec.ts b/packages/subagent/tool-subagent/tests/model-selection.spec.ts index 183f1f52f8..6a437cea37 100644 --- a/packages/subagent/tool-subagent/tests/model-selection.spec.ts +++ b/packages/subagent/tool-subagent/tests/model-selection.spec.ts @@ -12,6 +12,7 @@ import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import { MockAdapter } from '../../../core/agent-loop/tests/mock-adapter.ts' import * as mock from './scripted-provider.ts' import * as tool from '../src/index.ts' +import * as ListModels from '../src/list-models.ts' import { assertAllowedModelRoutes, assertAllowedModelSelection, @@ -121,7 +122,8 @@ describe('dsh-tool-subagent model selection', () => { 'provider', 'reasoning_effort', ]) - expect(schema.description).toContain('list_subagent_models') + expect(schema.description).not.toContain('list_subagent_models') + expect(schema.description).not.toContain('Changing the route can prevent provider-side reuse') expect(ctx.tools.get('list_subagent_models', agent)).toBeDefined() expect(schema.description).not.toContain('alpha') @@ -159,6 +161,43 @@ describe('dsh-tool-subagent model selection', () => { } }) + it('shows discovery guidance only while its tool is visible, regardless of mount order', async () => { + const ctx = await setup({ provider: 'mock', modelSelection: true }) + const agent = modelSelectionSetupAgent(ctx) + const descriptions = () => ctx.tools.schemas(agent).map(schema => schema.description).join('\n') + const guidance = 'Inspect options here before selecting a child provider and model' + expect(descriptions()).not.toContain(guidance) + expect(descriptions()).not.toContain('list_subagent_models') + + const discovery = await ctx.plugin(ListModels) + expect(descriptions()).toContain(guidance) + const restore = agent.ctx.tools.restrict({ deny: ['list_subagent_models'] }) + expect(descriptions()).not.toContain(guidance) + restore() + expect(descriptions()).toContain(guidance) + await discovery.dispose() + expect(descriptions()).not.toContain(guidance) + }) + + it.each(['configured', 'provider'] as const)('explains effort-only denial on a %s route without Session authorization', async (source) => { + const defaults = { provider: 'alpha', model: 'configured-model' } + const start = vi.fn() + const ctx = await setup({ + provider: 'mock', modelSelection: true, + parentAgentOptions: { provider: 'alpha', model: 'parent-model' }, + ...source === 'configured' ? { agentOptions: defaults } : {}, + }, { + onStart: start, + ...source === 'provider' ? { agentRouteDefaults: defaults } : {}, + }) + const result = await callSubagent(ctx, { + description: 'adjust effort', prompt: 'work', reasoning_effort: 'low', + }) + expect(result.isError).toBe(true) + expect(text(result)).toContain('without a recorded model-selection policy, explicit selection (including reasoning_effort) is limited to the parent route') + expect(start).not.toHaveBeenCalled() + }) + it('keeps settings-owned route fields visible but rejects other routes without a Session policy', async () => { const ctx = await setup({ provider: 'mock', @@ -170,7 +209,8 @@ describe('dsh-tool-subagent model selection', () => { const schema = ctx.tools.schemas(agent).find(entry => entry.name === 'subagent')! const props = (schema.parameters as { properties?: Record }).properties ?? {} expect(Object.keys(props).sort()).toEqual(['cwd', 'description', 'model', 'prompt', 'provider', 'reasoning_effort']) - expect(schema.description).toContain('list_subagent_models') + expect(schema.description).not.toContain('list_subagent_models') + expect(schema.description).not.toContain('Changing the route can prevent provider-side reuse') expect(ctx.tools.get('list_subagent_models', agent)).toBeDefined() const result = await callSubagent(ctx, { @@ -505,7 +545,8 @@ describe('dsh-tool-subagent model selection', () => { const agent = modelSelectionSetupAgent(ctx) const schema = ctx.tools.schemas(agent).find(entry => entry.name === 'subagent')! expect(schema.description).toContain('inherits this conversation') - expect(schema.description).toContain('list_subagent_models') + expect(schema.description).not.toContain('list_subagent_models') + expect(schema.description).toContain('Changing the route can prevent provider-side reuse of the inherited conversation prefix.') const allowed = await callSubagent(ctx, { description: 'fork work', prompt: 'do it', provider: 'alpha', model: 'fast-model', reasoning_effort: 'low', diff --git a/scripts/gen-tool-catalog.ts b/scripts/gen-tool-catalog.ts index 3f3288a6b8..df595823fe 100644 --- a/scripts/gen-tool-catalog.ts +++ b/scripts/gen-tool-catalog.ts @@ -511,7 +511,7 @@ const TOOL_PACKAGES: ToolPackage[] = [ list_subagent_models: 'packages/subagent/tool-subagent/src/list-models.ts', subagent: 'packages/subagent/tool-subagent/src/index.ts', }, - requires: ['ctx.tools', 'ctx.subagents', 'ctx.systemPrompt', 'ctx.llm for model discovery and selected-route validation'], + requires: ['ctx.tools', 'ctx.subagents', 'ctx.systemPrompt', 'ctx.sessionProjections', 'ctx.llm for model discovery and selected-route validation'], writes: ['tool/call', 'tool/result', 'child session events through the chosen provider'], shippedNames: ['subagent', 'subagent_fork'], async mount(ctx) { diff --git a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json index 85e10ae4f2..28da314b29 100644 --- a/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json +++ b/snapshots/sdk/subagent-dsh-sdk-dynamic-route/tool-schemas.expected.json @@ -262,7 +262,7 @@ }, { "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Inspect options here before selecting a child provider and model; use returned ids for delegation.", "parameters": { "type": "object", "properties": { @@ -356,7 +356,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The completion notice includes its final answer. This backend does not accept follow-up messages. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to this backend's route defaults. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The completion notice includes its final answer. This backend does not accept follow-up messages. Supply `provider` and `model` together. Omitted values use child defaults, falling back to this backend's route defaults. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { diff --git a/snapshots/web/fresh-round-trip/tool-schemas.expected.json b/snapshots/web/fresh-round-trip/tool-schemas.expected.json index 34cc3505b3..d97f06bdb1 100644 --- a/snapshots/web/fresh-round-trip/tool-schemas.expected.json +++ b/snapshots/web/fresh-round-trip/tool-schemas.expected.json @@ -325,7 +325,7 @@ }, { "name": "list_subagent_models", - "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation.", + "description": "Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Inspect options here before selecting a child provider and model; use returned ids for delegation.", "parameters": { "type": "object", "properties": { @@ -719,7 +719,7 @@ }, { "name": "subagent", - "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", + "description": "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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Supply `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default.", "parameters": { "type": "object", "properties": { diff --git a/snapshots/web/official-subagent-bundle/system-prompt.expected.md b/snapshots/web/official-subagent-bundle/system-prompt.expected.md index bf81e7f52a..778c016948 100644 --- a/snapshots/web/official-subagent-bundle/system-prompt.expected.md +++ b/snapshots/web/official-subagent-bundle/system-prompt.expected.md @@ -158,7 +158,7 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ + /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Inspect options here before selecting a child provider and model; use returned ids for delegation. */ list_subagent_models: { /** Registered LLM provider id. */ provider?: string; @@ -288,7 +288,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Supply `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; diff --git a/snapshots/web/optional-string-editor/system-prompt.expected.md b/snapshots/web/optional-string-editor/system-prompt.expected.md index d1d3f1d372..1670257a1e 100644 --- a/snapshots/web/optional-string-editor/system-prompt.expected.md +++ b/snapshots/web/optional-string-editor/system-prompt.expected.md @@ -158,7 +158,7 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ + /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Inspect options here before selecting a child provider and model; use returned ids for delegation. */ list_subagent_models: { /** Registered LLM provider id. */ provider?: string; @@ -305,7 +305,7 @@ interface ToolArgsMap { /** Optional parameter of `view` command when `path` points to a file. If omitted or null, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file. */ view_range?: number[] | null; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Supply `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; diff --git a/snapshots/web/ptc-round/system-prompt.expected.md b/snapshots/web/ptc-round/system-prompt.expected.md index 8818cb5cce..28e71009d3 100644 --- a/snapshots/web/ptc-round/system-prompt.expected.md +++ b/snapshots/web/ptc-round/system-prompt.expected.md @@ -158,7 +158,7 @@ interface ToolArgsMap { /** children (default) lists direct children, which accept send_message in any status. descendants lists the whole tree below you with each entry's parent session id and depth; entries deeper than 1 accept only interrupt_agent. */ scope?: "children" | "descendants"; } & Record; - /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Use returned ids for delegation. */ + /** Inspect subagent models without changing the current Agent. No arguments: providers; `provider`: allowed models; `provider` + `model`: that model and its reasoning efforts. Inspect options here before selecting a child provider and model; use returned ids for delegation. */ list_subagent_models: { /** Registered LLM provider id. */ provider?: string; @@ -288,7 +288,7 @@ interface ToolArgsMap { /** The exact skill name from the available skills list. */ name: string; } & Record; - /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Inspect options with `list_subagent_models` before setting `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ + /** 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. The runtime notifies you when it finishes. The child reports results with `send_message`; use `send_message` to steer it while running or continue its conversation after it finishes. Supply `provider` and `model` together. Omitted values use child defaults, falling back to compatible parent values. When the effective route changes, an omitted `reasoning_effort` uses the model's default. */ subagent: { /** Initial child working directory. Relative paths use your current directory; omitted inherits it. Later directory changes in either agent are independent. */ cwd?: string; From c5d227001e548422a03e8f1030b80888a3ae337a Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Wed, 14 Oct 2026 13:46:35 +0800 Subject: [PATCH 056/369] test(subagent): refresh fork runtime context ordering --- snapshots/web/subagent-fork-schedule/session.1.v4.jsonl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snapshots/web/subagent-fork-schedule/session.1.v4.jsonl b/snapshots/web/subagent-fork-schedule/session.1.v4.jsonl index 080926a704..b09cd32916 100644 --- a/snapshots/web/subagent-fork-schedule/session.1.v4.jsonl +++ b/snapshots/web/subagent-fork-schedule/session.1.v4.jsonl @@ -25,7 +25,7 @@ {"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":"Call schedule_create, schedule_list, schedule_update, and schedule_delete once each, then reply CHILD_DONE."}],"source":{"kind":"user"},"role":"user","id":"{{message:6}}"},"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\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.\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\nCurrent DeepSeek Harness implementation checkout: \"{{sourceRoot}}\".\n\nCurrent DeepSeek Harness Web GUI URL: \"{{webUrl}}\"."}],"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":"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."},{"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":"harness:source","text":"Current DeepSeek Harness implementation checkout: \"{{sourceRoot}}\"."},{"name":"app:web-surface","text":"Current DeepSeek Harness Web GUI URL: \"{{webUrl}}\"."}]},"role":"user","id":"{{message:7}}"},"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\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.\n\nCurrent DeepSeek Harness implementation checkout: \"{{sourceRoot}}\".\n\nCurrent DeepSeek Harness Web GUI URL: \"{{webUrl}}\"."}],"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":"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."},{"name":"harness:source","text":"Current DeepSeek Harness implementation checkout: \"{{sourceRoot}}\"."},{"name":"app:web-surface","text":"Current DeepSeek Harness Web GUI URL: \"{{webUrl}}\"."}]},"role":"user","id":"{{message:7}}"},"surfaceOp":"append"} {"type":"request/header","data":{"header":{"config":{"provider":"deepseek-official","model":"deepseek-v4-flash"},"tools":"{{tools}}"},"reason":"resume"}} {"type":"assistant/message","data":{"turn":2,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"call_schedule_create","name":"schedule_create","arguments":"{\"title\":\"Child reminder\",\"prompt\":\"Check this\",\"after_seconds\":60}"},{"type":"tool-call","id":"call_schedule_list","name":"schedule_list","arguments":"{}"},{"type":"tool-call","id":"call_schedule_update","name":"schedule_update","arguments":"{\"id\":\"missing-reminder\",\"title\":\"Updated\"}"},{"type":"tool-call","id":"call_schedule_delete","name":"schedule_delete","arguments":"{\"id\":\"missing-reminder\"}"}],"source":{"kind":"model","provider":"deepseek-official","model":"deepseek-v4-flash"},"id":"{{message:8}}"},"stream":[{"type":"chunk","time":1791527273612,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791527273612,"index":0,"dt":[],"id":"call_schedule_create","name":"schedule_create","args":["{\"title\":\"Child reminder\",\"prompt\":\"Check this\",\"after_seconds\":60}"]},{"type":"chunk","time":1791527273612,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"call_schedule_create","name":"schedule_create","arguments":"{\"title\":\"Child reminder\",\"prompt\":\"Check this\",\"after_seconds\":60}"}}},{"type":"chunk","time":1791527273612,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791527273612,"index":1,"dt":[],"id":"call_schedule_list","name":"schedule_list","args":["{}"]},{"type":"chunk","time":1791527273612,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_schedule_list","name":"schedule_list","arguments":"{}"}}},{"type":"chunk","time":1791527273612,"chunk":{"type":"block-start","index":2,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791527273612,"index":2,"dt":[],"id":"call_schedule_update","name":"schedule_update","args":["{\"id\":\"missing-reminder\",\"title\":\"Updated\"}"]},{"type":"chunk","time":1791527273612,"chunk":{"type":"block-end","index":2,"block":{"type":"tool-call","id":"call_schedule_update","name":"schedule_update","arguments":"{\"id\":\"missing-reminder\",\"title\":\"Updated\"}"}}},{"type":"chunk","time":1791527273612,"chunk":{"type":"block-start","index":3,"blockType":"tool-call"}},{"type":"tool-call-chunks","time0":1791527273612,"index":3,"dt":[],"id":"call_schedule_delete","name":"schedule_delete","args":["{\"id\":\"missing-reminder\"}"]},{"type":"chunk","time":1791527273613,"chunk":{"type":"block-end","index":3,"block":{"type":"tool-call","id":"call_schedule_delete","name":"schedule_delete","arguments":"{\"id\":\"missing-reminder\"}"}}},{"type":"chunk","time":1791527273613,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}]},"surfaceOp":"append"} {"type":"tool/call","data":{"turn":2,"step":1,"callId":"call_schedule_create","name":"schedule_create","arguments":"{\"title\":\"Child reminder\",\"prompt\":\"Check this\",\"after_seconds\":60}"}} From c0bf2ac81dc9238257ea602e82913bd11057227a Mon Sep 17 00:00:00 2001 From: Shigma Date: Wed, 14 Oct 2026 14:28:16 +0800 Subject: [PATCH 057/369] docs(cordis): plan the staged rc.11 vendor upgrade --- vendor/README.md | 2 + vendor/cordis-rc11-upgrade-plan.md | 93 ++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 vendor/cordis-rc11-upgrade-plan.md diff --git a/vendor/README.md b/vendor/README.md index 1765056c37..654276201d 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -6,6 +6,8 @@ All vendored packages use the **`@deepseek-ai` scope** (`cordis` → `@deepseek- This file covers the manifest, the local-modification log, and the procedure for **updating** an existing vendored package. To **add a new** one, see the cookbook guide: [docs/cookbook/adding-a-vendored-package.md](../docs/cookbook/adding-a-vendored-package.md). +The proposed [Cordis rc.11 upgrade plan](cordis-rc11-upgrade-plan.md) records the package scope, PR dependencies and intended disposition of the local modifications. + ## Manifest Upstream workspace: `cordis-workspace` (local checkout: `~/repos/cordis-workspace`). diff --git a/vendor/cordis-rc11-upgrade-plan.md b/vendor/cordis-rc11-upgrade-plan.md new file mode 100644 index 0000000000..ac53067459 --- /dev/null +++ b/vendor/cordis-rc11-upgrade-plan.md @@ -0,0 +1,93 @@ +# Cordis rc.11 vendor upgrade plan + +Status: proposed + +## Problem + +The [vendor manifest](README.md) records Cordis core 4.0.0-rc.7, Loader 1.0.0-rc.5, Include 1.0.4, HMR 1.0.15 and Timer 1.1.2, with 24 local-modification entries. [Cordis v4.0.0-rc.11](https://github.com/cordiverse/cordis/releases/tag/v4.0.0-rc.11) incorporates some fixes and replaces other mechanisms. Updating every package and retiring every patch in one change would combine configuration, persistence, lifecycle and module-replacement decisions. + +DSH also has consumers that upstream Cordis does not own. Volatile configuration fields are stable references updated without restarting plugins; the settings UI reads their live values. A profile selects ordered plugin bundles and configuration overrides. [DSH HMR](../packages/boot/hmr/README.md) coordinates module replacement and profile changes through `ctx.hmr`. An entry's module namespace is its raw import result, retained before selecting `default` or `apply` so HMR can identify the module already loaded. These consumers must remain usable at every merge point. + +## Proposal + +Keep source vendoring and stage the upgrade as independently reviewable PRs, each covering a complete concern with its producers and consumers. Upgrade Core with the minimum cross-package compatibility changes, then complete Loader, Include and vendor HMR separately. Retire an existing patch when its replacement is adopted and verified; retain independently applicable patches until their behavior has a replacement. PR 0 records this plan only. It does not approve the open runtime decisions or implement the upgrade. + +### Package scope + +| Package | Target from the rc.11 tag | Treatment | +|---|---|---| +| `vendor/cordis` | 4.0.0-rc.11 | Upgrade with the Core compatibility slice. | +| `vendor/loader` | 1.0.0-rc.8 | Import required slices first; complete the package after resolution and change reporting land. | +| `vendor/include` | 1.1.1 | Separate patch composition from journal adoption. | +| `vendor/hmr` | 1.1.1 | Upgrade independently of the Include journal. | +| `vendor/timer` | 1.1.4 | Upgrade independently of Core. | +| `vendor/group` | Keep the existing pin | Its entry source matches the tag after rescoping. The Group implementation lives in Loader and still requires migration. | +| `vendor/logger-console` | Keep the existing pin | All three source files match the tag after normalizing scope, relative import extensions and comments. Core's logger changes remain part of the Core upgrade. | +| `vendor/cosmokit`, `vendor/schemastery` | Keep the existing pins | Retain the volatile extensions; their upstream upgrades are outside this plan. | + +The comparison above checks the current vendored source against the release tag; it does not assume equal version numbers imply equal code. DSH package versions, exports and build settings remain governed by the repository. A partially synchronized Loader, Include or HMR keeps its old manifest baseline and records the imported upstream slice in the local-modification log. Advance the package baseline only when the complete package has been reconciled, with remaining differences recorded explicitly. + +### PR dependencies + +The identifiers below are plan labels, not GitHub issue numbers. Preconditions are merge requirements. Independent PRs can be developed and reviewed concurrently; a dependent PR can start against the proposed changes before its predecessors merge, but must validate the actual combined result before merging. + +`Vn` below identifies item `n` in the [current local-modification log](README.md#local-modifications), not a PR. **Retire** means the upstream implementation replaces the local difference; **partial** means some behavior still needs a local adaptation; **retain** means the requirement remains; **prepare** does not retire a patch. These are proposed outcomes, confirmed only by the owning PR's tests. Track each item by its subject if later manifest edits renumber the log. + +| PR | Scope | Prerequisite PRs | Existing vendor modifications | Required evidence | +|---|---|---|---|---| +| 0 | This plan, package comparison and open decisions. | None | None retired; records the proposed disposition. | Documentation checks; no runtime changes. | +| 1 | Define access to raw config, candidate resolution and the active instance's effective config, and migrate settings, Config Editor, volatile updates and HMR readers together. Initially delegate to the old framework fields. | 0 | Prepare V15 (lazy/raw config) and V22 (volatile); retire neither yet. | Preserve existing objects, validation, settings and volatile behavior. Candidate validation does not replace the active instance's references; introduce no dependency from vendor code to DSH packages. | +| 2 | Add upstream-compatible `hmr.watch()` to both HMR providers with compatible typing, migrate the existing Include to explicit registration, and remove the equivalent Include scans. Use DSH HMR's existing coordination queue. | 0 | Adapt V9's config-path matching to explicit watches; retain its readiness and native-path requirements. | Callback ordering, disposal, explicit paths and `root: []` under the old Core; one Include refresh per change under either provider. Preserve profile watching and both module-replacement algorithms. | +| 3 | Upgrade Timer to 1.1.4, retaining DSH locale resources and build metadata. | 0 | Retain V24 (display metadata); no logged runtime patch retired. | Concurrent interval reads, throttle cancellation and void return types; check callers using the old Core. | +| 4a | Upgrade Core to rc.11 with the minimum Loader, Include and HMR compatibility slice described below. | 1 | Retire V12 (activation observer), V21 (logger disposer) and, after canonical-fiber audits, V20 (Entry identity workaround). Partial V6 (lifecycle) and V15 (config model). Retain/adapt V22 and V23 (module namespace). | Config resolution, volatile identity, service isolation, dependent-before-provider teardown, retained effect regressions and application startup. | +| 4b | Import Loader's project-relative bare-specifier resolver, runtime API detection and fallback when Node internals are absent. | 0 | Retire V19: upstream now detects the internal loader by its available APIs. | Resolve in the config project's package scope, retain subpaths, handle read-only helper creation and test Node 24's older v1 API. | +| 4c | Complete Loader's `EntryChange` / `commit(change)` reporting and remaining changes together with upstream Include's pure patch implementation and journal helper module. Adapt the old writer and in-memory trees; keep `applyEntryPatches` and `entryListSchema` exports. | 4a, 4b | Retire V18 (`disabled` expressions). Partial V11: retire the duplicate patch algorithm and insert-index fix; retain public exports. Retain/adapt V22, including the schema receiver fix, and V23. | Create, move, remove and self-update reports; no false self-disposal writes; later patches address inserted rows; cloning and config dumps agree with mounting. The old Include writer remains active. | +| 4e | Complete vendor HMR's upgrade to 1.1.1, preserving required local path, readiness and explicit-watch behavior. | 2, 3, 4a | Partial V9: adopt upstream ordering and path handling where equivalent; retain explicit readiness and realpath gaps. Retain V1 (no YAML locale imports). | Three-stage module replacement, nested entries, disabled/removed entries, explicit watches and the existing local path cases. | +| 4f | Upgrade Include to its journal implementation, retaining the explicit watch registration already adopted in PR 2. | 2, 4c | Retire V8 (old refresh implementation) and V13 (`writeTask` type). Partial V14: replace the write queue; decide unsaved-error reporting. Retain the remaining V11 exports and V15 child-expression marker. | File/runtime conflict handling, patch-owned writes, shutdown results, nested expressions and one coordinated refresh under either HMR provider. | + +Shared integration differences remain in every affected sync: V2 (manifests), V3 (TypeScript projects), V4/V10 (imports and Node-compatible modules), V5 (build overrides), V7 (API documentation), V16 (published Core source) and V17 (scope). Untouched packages retain them without edits. V6 only loses the stale-epoch guard already supplied upstream; its remaining ownership and cleanup guarantees stay. V15 replaces the old raw/effective-config and event implementation, but the Include child-expression behavior remains local. V22 is not upstreamed by this release, including its schema-bound `simplify` call. + +After PR 0, PRs 1, 2, 3 and 4b have no implementation dependency on one another. Core depends on configuration preparation; it does not depend on Timer. Loader completion and vendor HMR can proceed independently after Core. Include journal adoption depends on Loader and the complete watch migration, not on the new vendor HMR replacement algorithm; PRs 4e and 4f can merge in either order. No single GitHub stack should impose a dependency between unrelated branches. Shared files such as the vendor manifest may still require merge-conflict resolution. + +### Minimum Core compatibility slice + +PR 4a must migrate every caller of the changed Core APIs in the same PR, including callers outside vendor. This is a complete Core upgrade with local patches, paired with selected Loader changes; it is not a claim that Loader already equals 1.0.0-rc.8. + +- Adopt upstream raw `Fiber.config`, `internal/config(fiber, next)`, `internal/commit` and the new `internal/update` arguments and awaited result. Update the config accessors, provider hooks, boot observers and old Include's update listener together. The temporary commit listener writes through the old tree writer until PR 4c. +- Carry the required Loader entry/context awaiting chain, activation-failure observer and isolation changes with Core's `Impl.ctx`, `Impl.isolate`, `committed` and dependent tracking. Update direct implementation readers such as the preset registry. Include Group behavior owned by `vendor/loader/src/config/group.ts`; do not confuse it with the unchanged `vendor/group` entry package. Before retiring V20, ensure volatile updates and every other direct lifecycle-field access use the canonical `fiber.ctx.fiber`. +- Keep Include's nested-entry expressions literal until the owning child resolves them. Keep the namespace field needed by DSH HMR and preserve active volatile references through the agreed config mechanism. Update raw-config access in both HMR implementations without replacing the DSH module-replacement policy. +- Reapply the still-needed effect ownership, reentrant disposal and observer-containment guarantees to the new Core lifecycle. Remove patches already supplied by the imported Core source. Verify the combination with provider/dependent teardown before treating the compatibility slice as complete. + +Module resolution, structured tree-change reporting, journal reconciliation and the new vendor HMR algorithm are not prerequisites for these Core interfaces. Their PRs remain separate unless executable compatibility tests expose an additional dependency; amend this plan before broadening a PR silently. + +### Temporary compatibility code + +PR 4c changes tree mutation producers to `commit(change)` while the old Include remains installed. That Include can implement `commit(change)` by invoking its existing `write()` operation; it deliberately ignores the detailed change report until the journal replaces the writer. In-memory trees, including `PresetTree`, implement a no-op commit. This avoids maintaining two mutation-reporting protocols in Loader. PR 4f deletes the Include writer adapter and old queue. + +PR 4c imports `patch.ts` together with its `journal.ts` helpers and their Loader `EntryChange` dependency, even though Include does not start runtime journaling yet. The public patch export may remain an alias; it must not retain a second algorithm. Every temporary divergence has an owning introduction PR and a named removal PR. No runtime version detection or second Core event protocol is planned. + +PR 2 moves Include refresh ownership in one step: both HMR providers expose `watch()`, the existing Include registers its callback, and both providers stop their equivalent Include scans. The upstream HMR source no longer performs that scan, so importing it before migrating the Include consumer would lose config refresh. Moving the complete watch concern first avoids a temporary scan adapter and makes the later module-replacement and persistence decisions independent. + +### Decisions owned by individual PRs + +| Decision | Owning PR | Proposal and unresolved choice | +|---|---|---| +| Active effective config | 1, 4a | Store or expose the object actually supplied to the current instance. Resolving a candidate through `internal/config` must not overwrite it. Decide the publication point and invalidation rules before switching Core; a WeakMap alone does not establish those rules. | +| Persistence outcome | 4f | Prefer the journal's retry and reconciliation mechanism. Decide how explicit callers observe modifications still unsaved at shutdown. Do not require a historical failure to remain fatal after a successful retry merely because the old test expects it. | +| Nested config interpolation | 4a, 4f | Preserve child-owned expression evaluation. Keep the existing Include marker initially; any refinement that permits interpolation of Include's own fields while excluding child lists is a separate behavior change. | +| HMR replacement failure | 4e | Adopt upstream behavior for vendor HMR. Preserve DSH HMR's current source-reload rollback policy in this series; replacing that policy requires its own proposal and tests. | +| Effect cleanup guarantees | 4a | Retain guarantees not supplied by rc.11. Further upstreaming or retirement is separate work, not a prerequisite for reaching the release baseline. | + +## Acceptance criteria + +- Every implementation PR leaves supported applications usable and passes checks appropriate to its changes. A later PR may not be required to repair an earlier PR's startup, types, persistence or teardown. +- Each source-vendor update follows the [sync procedure](README.md#sync-procedure), including `pnpm run test` and `pnpm run build`, and updates its own manifest, local-modification log, affected documentation and generated references. The [pre-push workflow](../.agents/skills/dsh-pre-push-checks/SKILL.md) selects additional focused checks. Documentation freshness is not deferred to a final cleanup PR. +- Every changed behavior has an explicit decision and focused regression coverage. Tests for an intentionally superseded behavior change with its implementation and explain the new expectation. +- Once all implementation PRs are complete, Core, Loader, Include, vendor HMR and Timer use their selected rc.11-tag sources with enumerated remaining patches. Group, logger-console, Cosmokit and Schemastery keep their existing pins. All temporary writer and config adapters have been removed or reduced to the agreed permanent accessors. +- Final integration verification exercises settings and volatile updates, profile recomposition, both HMR providers, Include persistence and representative built application launches. This is an acceptance checkpoint, not an otherwise empty PR. + +## Risks + +PR 4a remains the largest necessary compatibility change: Core's service lifetime and configuration model cross repository package boundaries. The minimum slice is based on source inspection, not a completed migration experiment. Focused tests may identify additional inseparable changes. + +Intermediate Loader, Include and HMR copies intentionally combine an old baseline with documented upstream slices. Missing slice records would make a later sync unsafe. Deferred config, candidate validation, journal writes and asynchronous disposal must be tested in combination rather than inferred from independently passing unit cases. A new Include's type dependency on HMR must not force both concrete providers into the same incompatible `Context.hmr` declaration. From 7e68ab4f160609f6211c671044b2aa770ff062ed Mon Sep 17 00:00:00 2001 From: Turtle Date: Wed, 14 Oct 2026 14:43:19 +0800 Subject: [PATCH 058/369] Credit review by @Yifffan Co-authored-by: Yifffan <133629972+Yifffan@users.noreply.github.com> From 7618ccec69717a06dec0d2ecbe250b05f5a73cd1 Mon Sep 17 00:00:00 2001 From: Shigma Date: Wed, 14 Oct 2026 15:01:18 +0800 Subject: [PATCH 059/369] =?UTF-8?q?chore(cordis):=20=E5=90=8C=E6=AD=A5=20T?= =?UTF-8?q?imer=201.1.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cordis-client-runner/README.i18n.yaml | 4 +- .../extensions/cordis-client-runner/README.md | 2 +- .../cordis-client-runner/README.zh.md | 2 +- .../src/client/api-catalog.ts | 4 +- .../cordis-client-runner/src/client/timer.ts | 44 ++++---- .../tests/timer.client.spec.ts | 53 +++++++++ .../extensions/tool-cordis/src/api-catalog.ts | 4 +- scripts/cordis-timer.spec.ts | 101 ++++++++++++++++++ scripts/gen-cordis-catalog.ts | 4 +- vendor/README.md | 4 +- vendor/timer/README.md | 10 +- vendor/timer/src/index.ts | 38 ++++--- 12 files changed, 219 insertions(+), 51 deletions(-) create mode 100644 packages/extensions/cordis-client-runner/tests/timer.client.spec.ts create mode 100644 scripts/cordis-timer.spec.ts diff --git a/packages/extensions/cordis-client-runner/README.i18n.yaml b/packages/extensions/cordis-client-runner/README.i18n.yaml index f68b557073..7352fc1585 100644 --- a/packages/extensions/cordis-client-runner/README.i18n.yaml +++ b/packages/extensions/cordis-client-runner/README.i18n.yaml @@ -33,8 +33,8 @@ en: df635b29eaef982c zh: ddaa4bd98359bd30 /deepseek-ai-dsh-cordis-client-runner/understand-the-implementation/source-map: - en: cbd46fb5821cfcc7 - zh: 392e5cace2db6194 + en: 22a43e93fd123200 + zh: f8305186a748369b /deepseek-ai-dsh-cordis-client-runner/understand-the-implementation/inspecting-slot-inputs: en: 33f14a7fff7835e6 zh: 53838ac8d4a79600 diff --git a/packages/extensions/cordis-client-runner/README.md b/packages/extensions/cordis-client-runner/README.md index 07b371528a..90dfd95b97 100644 --- a/packages/extensions/cordis-client-runner/README.md +++ b/packages/extensions/cordis-client-runner/README.md @@ -64,7 +64,7 @@ The browser half is built on one principle: a dynamic package must ride the same | [`src/client/guard.ts`](src/client/guard.ts) | The whitelisting `ctx` façade for loaded browser halves | | [`src/client/inspect-registry.ts`](src/client/inspect-registry.ts) | Client Inspect Providers and the pending-query router | | [`src/client/providers.ts`](src/client/providers.ts) | First-party client Inspect Providers (slots, theme, events) | -| [`src/client/timer.ts`](src/client/timer.ts) | The client timer service dynamic packages inject | +| [`src/client/timer.ts`](src/client/timer.ts) | Client timers with ordered concurrent interval reads and void-returning throttle/debounce wrappers that become inert on disposal | ### Inspecting slot inputs diff --git a/packages/extensions/cordis-client-runner/README.zh.md b/packages/extensions/cordis-client-runner/README.zh.md index 3753a9beca..c4eea99ce7 100644 --- a/packages/extensions/cordis-client-runner/README.zh.md +++ b/packages/extensions/cordis-client-runner/README.zh.md @@ -64,7 +64,7 @@ kind: "package-reference" | [`src/client/guard.ts`](src/client/guard.ts) | 已装载浏览器半收到的白名单 `ctx` façade | | [`src/client/inspect-registry.ts`](src/client/inspect-registry.ts) | Client Inspect Provider 与待答查询路由器 | | [`src/client/providers.ts`](src/client/providers.ts) | 第一方 client Inspect Provider(slots、theme、events) | -| [`src/client/timer.ts`](src/client/timer.ts) | 动态包注入的 client 定时器服务 | +| [`src/client/timer.ts`](src/client/timer.ts) | client 定时器按顺序完成并发 interval 读取;throttle/debounce 包装函数返回 void,释放后不再执行 | ### 查询槽位输入 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 c4887547ab..e8b3248d47 100644 --- a/packages/extensions/cordis-client-runner/src/client/api-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/api-catalog.ts @@ -319,12 +319,12 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ parameters: [], }, { - signature: 'throttle void>(callback: F, delay: number, noTrailing?: boolean): F & { dispose: () => void }', + signature: 'throttle any>(callback: F, delay: number, noTrailing?: boolean): ((...args: Parameters) => void) & { dispose: () => void }', description: 'Return a throttled function whose timer is disposed with the current fiber.', parameters: [], }, { - signature: 'debounce void>(callback: F, delay: number): F & { dispose: () => void }', + signature: 'debounce any>(callback: F, delay: number): ((...args: Parameters) => void) & { dispose: () => void }', description: 'Return a debounced function whose timer is disposed with the current fiber.', parameters: [], }, diff --git a/packages/extensions/cordis-client-runner/src/client/timer.ts b/packages/extensions/cordis-client-runner/src/client/timer.ts index e2b07c8472..2c87fb978e 100644 --- a/packages/extensions/cordis-client-runner/src/client/timer.ts +++ b/packages/extensions/cordis-client-runner/src/client/timer.ts @@ -21,7 +21,7 @@ declare module '@deepseek-ai/cordis' { } } -type WithDispose = T & { dispose: () => void } +type Scheduled any> = ((...args: Parameters) => void) & { dispose: () => void } // These `any` positions mirror the Host TimerService's overload erasure: generic callback tuples and async-iterator // return/rejection values must pass through without narrowing them to one caller's invocation. @@ -104,7 +104,7 @@ export class ClientTimerService extends Service { /** * Iterate over timer ticks. * @param delay - interval in milliseconds. - * @returns async iterator of ticks. + * @returns Iterator completing concurrent reads in order, one per tick; disposal rejects all pending reads. */ interval(delay: number): AsyncIterableIterator interval(...args: any[]): any { @@ -118,33 +118,37 @@ export class ClientTimerService extends Service { } let done: { kind: 'return'; value: any } | { kind: 'throw'; reason: any } | undefined - let nextTask: PromiseWithResolvers> | undefined + const nextTasks: PromiseWithResolvers>[] = [] const dispose = this.ctx.effect(() => { const timer = globalThis.setInterval(() => { - nextTask?.resolve({ done: false, value: undefined }) + nextTasks.shift()?.resolve({ done: false, value: undefined }) }, delay) return () => { globalThis.clearInterval(timer) if (done !== undefined) return done = { kind: 'throw', reason: new Error('Context has been disposed') } - nextTask?.reject(done.reason) + for (const task of nextTasks.splice(0)) task.reject(done.reason) } }, 'ctx.interval()') return { next: () => { - if (done === undefined) return (nextTask = Promise.withResolvers()).promise + if (done === undefined) { + const task = Promise.withResolvers>() + nextTasks.push(task) + return task.promise + } if (done.kind === 'return') return Promise.resolve({ done: true, value: done.value }) return Promise.reject(done.reason) }, return: (value: any) => { if (done === undefined) done = { kind: 'return', value } - nextTask?.resolve({ done: true, value }) + for (const task of nextTasks.splice(0)) task.resolve({ done: true, value }) void dispose() return Promise.resolve({ done: true, value }) }, throw: (reason: any) => { if (done === undefined) done = { kind: 'throw', reason } - nextTask?.reject(reason) + for (const task of nextTasks.splice(0)) task.reject(reason) void dispose() return Promise.resolve({ done: true, value: undefined }) }, @@ -155,15 +159,18 @@ export class ClientTimerService extends Service { } /** Build a delayed wrapper whose pending callback belongs to the calling Fiber. */ - private schedule(label: string, trigger: (args: any[], disposed: boolean) => number | undefined, disposed = false): any { + private schedule(label: string, trigger: (args: any[]) => number | undefined): any { let timer: number | undefined + let disposed = false const dispose = this.ctx.effect(() => () => { disposed = true globalThis.clearTimeout(timer) + timer = undefined }, label) const wrapper: any = (...args: any[]): void => { + if (disposed) return globalThis.clearTimeout(timer) - timer = trigger(args, disposed) + timer = trigger(args) } wrapper.dispose = dispose return wrapper @@ -174,33 +181,32 @@ export class ClientTimerService extends Service { * @param callback - Function to throttle. * @param delay - Minimum interval between calls in milliseconds. * @param noTrailing - Whether to suppress a delayed trailing call. - * @returns Throttled function with an early disposer. + * @returns Void-returning wrapper; disposal cancels pending work and makes later calls inert. */ - throttle void>(callback: F, delay: number, noTrailing?: boolean): WithDispose { + throttle any>(callback: F, delay: number, noTrailing?: boolean): Scheduled { let lastCall = -Infinity const execute = (...args: Parameters): void => { lastCall = Date.now() callback(...args) } - return this.schedule('ctx.throttle()', (args, disposed) => { + return this.schedule('ctx.throttle()', (args) => { const remaining = delay - Date.now() + lastCall if (remaining <= 0) { execute(...args as Parameters) - } else if (!disposed) { + } else if (!noTrailing) { return globalThis.setTimeout(execute, remaining, ...args) } - }, noTrailing) + }) } /** * Return a debounced function whose timer is disposed with the calling Fiber. * @param callback - Function to debounce. * @param delay - Quiet period in milliseconds. - * @returns Debounced function with an early disposer. + * @returns Void-returning wrapper; disposal cancels pending work and makes later calls inert. */ - debounce void>(callback: F, delay: number): WithDispose { - return this.schedule('ctx.debounce()', (args, disposed) => { - if (disposed) return + debounce any>(callback: F, delay: number): Scheduled { + return this.schedule('ctx.debounce()', (args) => { return globalThis.setTimeout(callback, delay, ...args) }) } diff --git a/packages/extensions/cordis-client-runner/tests/timer.client.spec.ts b/packages/extensions/cordis-client-runner/tests/timer.client.spec.ts new file mode 100644 index 0000000000..9ad26bccc7 --- /dev/null +++ b/packages/extensions/cordis-client-runner/tests/timer.client.spec.ts @@ -0,0 +1,53 @@ +/** @vitest-environment jsdom */ +import { Context } from '@deepseek-ai/cordis' +import { afterEach, beforeEach, describe, expect, expectTypeOf, it, vi } from 'vitest' +import { ClientTimerService } from '../src/client/timer.ts' + +describe('client Timer', () => { + let ctx: Context + + beforeEach(async () => { + vi.useFakeTimers() + ctx = new Context() + await ctx.plugin(ClientTimerService) + }) + + afterEach(async () => { + try { + await ctx.fiber.dispose() + } finally { + vi.useRealTimers() + } + }) + + it('delivers concurrent reads in order and rejects the remaining reads on teardown', async () => { + const ticks = ctx.interval(20) + const outcomes: string[] = [] + for (let index = 0; index < 3; index++) { + void ticks.next().then(() => outcomes.push(`tick ${index}`), () => outcomes.push(`disposed ${index}`)) + } + await vi.advanceTimersByTimeAsync(20) + expect(outcomes).toEqual(['tick 0']) + await ctx.fiber.dispose() + expect(outcomes).toEqual(['tick 0', 'disposed 1', 'disposed 2']) + expect(vi.getTimerCount()).toBe(0) + }) + + it('keeps wrapper calls void and inert after disposal', async () => { + const work = vi.fn((value: string) => value.length) + const throttle = ctx.throttle(work, 20) + const debounce = ctx.debounce(work, 20) + expectTypeOf(throttle).returns.toBeVoid() + expectTypeOf(debounce).returns.toBeVoid() + throttle('leading') + throttle('trailing') + debounce('delayed') + throttle.dispose() + debounce.dispose() + await vi.advanceTimersByTimeAsync(40) + throttle('disposed') + debounce('disposed') + expect(work).toHaveBeenCalledExactlyOnceWith('leading') + expect(vi.getTimerCount()).toBe(0) + }) +}) diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 798e310219..402286ff4c 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -3305,12 +3305,12 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ parameters: [], }, { - signature: 'throttle void>(callback: F, delay: number, noTrailing?: boolean): F & { dispose: () => void }', + signature: 'throttle any>(callback: F, delay: number, noTrailing?: boolean): ((...args: Parameters) => void) & { dispose: () => void }', description: 'Return a throttled function whose timer is disposed with the current fiber.', parameters: [], }, { - signature: 'debounce void>(callback: F, delay: number): F & { dispose: () => void }', + signature: 'debounce any>(callback: F, delay: number): ((...args: Parameters) => void) & { dispose: () => void }', description: 'Return a debounced function whose timer is disposed with the current fiber.', parameters: [], }, diff --git a/scripts/cordis-timer.spec.ts b/scripts/cordis-timer.spec.ts new file mode 100644 index 0000000000..6f6822853f --- /dev/null +++ b/scripts/cordis-timer.spec.ts @@ -0,0 +1,101 @@ +/** Timer iterator ordering and cancellation on the vendored Cordis lifecycle. */ +import { Context } from '@deepseek-ai/cordis' +import Timer from '@deepseek-ai/cordis-plugin-timer' +import { afterEach, beforeEach, describe, expect, expectTypeOf, it, vi } from 'vitest' + +describe('Cordis Timer', () => { + let ctx: Context + + beforeEach(async () => { + vi.useFakeTimers() + ctx = new Context() + await ctx.plugin(Timer) + }) + + afterEach(async () => { + try { + await ctx.fiber.dispose() + } finally { + vi.useRealTimers() + } + }) + + it('completes concurrent interval reads in order, one per tick', async () => { + const iterator = ctx.interval(10) + const completed: number[] = [] + const reads = [0, 1, 2].map(index => iterator.next().then( + (value) => { completed.push(index); return value }, + () => undefined, + )) + for (let index = 0; index < reads.length; index++) { + await vi.advanceTimersByTimeAsync(10) + expect(completed).toEqual(Array.from({ length: index + 1 }, (_, i) => i)) + } + expect(await Promise.all(reads)).toEqual(Array(3).fill({ done: false, value: undefined })) + await iterator.return!() + expect(vi.getTimerCount()).toBe(0) + }) + + it.each(['return', 'throw', 'dispose'] as const)('settles all pending interval reads on %s', async (operation) => { + const iterator = ctx.interval(10) + const completed: PromiseSettledResult>[] = [] + for (let index = 0; index < 3; index++) { + void iterator.next().then( + value => completed.push({ status: 'fulfilled', value }), + (reason: unknown) => completed.push({ status: 'rejected', reason }), + ) + } + const error = new Error('stopped') + if (operation === 'return') await iterator.return!('finished') + else if (operation === 'throw') await iterator.throw!(error) + else await ctx.fiber.dispose() + await vi.advanceTimersByTimeAsync(0) + expect(completed).toHaveLength(3) + if (operation === 'return') { + expect(completed).toEqual(Array(3).fill({ status: 'fulfilled', value: { done: true, value: 'finished' } })) + await expect(iterator.next()).resolves.toEqual({ done: true, value: 'finished' }) + } else { + const reason = operation === 'throw' ? error : new Error('Context has been disposed') + expect(completed).toEqual(Array(3).fill({ status: 'rejected', reason })) + await expect(iterator.next()).rejects.toThrow(reason.message) + } + expect(vi.getTimerCount()).toBe(0) + }) + + it.each([false, true])('stops throttle calls and trailing work after disposal (noTrailing=%s)', async (noTrailing) => { + const callback = vi.fn((value: string) => value.length) + const throttled = ctx.throttle(callback, 10, noTrailing) + expectTypeOf(throttled).parameters.toEqualTypeOf<[string]>() + expectTypeOf(throttled).returns.toBeVoid() + throttled('first') + throttled('second') + await vi.advanceTimersByTimeAsync(10) + expect(callback.mock.calls).toEqual(noTrailing ? [['first']] : [['first'], ['second']]) + throttled('pending') + const calls = callback.mock.calls.length + throttled.dispose() + await vi.advanceTimersByTimeAsync(20) + throttled('after disposal') + expect(callback).toHaveBeenCalledTimes(calls) + expect(vi.getTimerCount()).toBe(0) + }) + + it('cancels debounce work and makes both wrappers inert after owner disposal', async () => { + const callback = vi.fn((value: string) => value.length) + const debounced = ctx.debounce(callback, 10) + const throttled = ctx.throttle(callback, 10) + expectTypeOf(debounced).parameters.toEqualTypeOf<[string]>() + expectTypeOf(debounced).returns.toBeVoid() + debounced('first') + debounced('last') + await vi.advanceTimersByTimeAsync(10) + expect(callback).toHaveBeenCalledExactlyOnceWith('last') + debounced('pending') + await ctx.fiber.dispose() + debounced('disposed') + throttled('disposed') + await vi.advanceTimersByTimeAsync(10) + expect(callback).toHaveBeenCalledOnce() + expect(vi.getTimerCount()).toBe(0) + }) +}) diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index d734035f5a..25bfc749c9 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -1007,11 +1007,11 @@ export const CORDIS_CATALOG_POLICY: CordisCatalogPolicy = { jsDoc: '/** Return an async iterator of timer ticks. */', }, { - signature: 'throttle void>(callback: F, delay: number, noTrailing?: boolean): F & { dispose: () => void }', + signature: 'throttle any>(callback: F, delay: number, noTrailing?: boolean): ((...args: Parameters) => void) & { dispose: () => void }', jsDoc: '/** Return a throttled function whose timer is disposed with the current fiber. */', }, { - signature: 'debounce void>(callback: F, delay: number): F & { dispose: () => void }', + signature: 'debounce any>(callback: F, delay: number): ((...args: Parameters) => void) & { dispose: () => void }', jsDoc: '/** Return a debounced function whose timer is disposed with the current fiber. */', }, ], diff --git a/vendor/README.md b/vendor/README.md index 654276201d..b076fb4443 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -20,7 +20,7 @@ Upstream workspace: `cordis-workspace` (local checkout: `~/repos/cordis-workspac | `loader/` | `@deepseek-ai/cordis-plugin-loader` | `@cordisjs/plugin-loader` | 1.0.0-rc.5 | https://github.com/cordiverse/cordis (`packages/loader`) | `56b3d4f725681cf4556c1a8695a709cc3b6eed74` | | `include/` | `@deepseek-ai/cordis-plugin-include` | `@cordisjs/plugin-include` | 1.0.4 | https://github.com/deepseek-harness/cordis (`packages/include`) | `abb0a307cb1d3b0947f455d590cf5ba922d4caa4` | | `group/` | `@deepseek-ai/cordis-plugin-group` | `@cordisjs/plugin-group` | 1.0.0 | https://github.com/deepseek-harness/cordis (`packages/group`) | `abb0a307cb1d3b0947f455d590cf5ba922d4caa4` | -| `timer/` | `@deepseek-ai/cordis-plugin-timer` | `@cordisjs/plugin-timer` | 1.1.2 | https://github.com/deepseek-harness/cordis (`packages/timer`) | `abb0a307cb1d3b0947f455d590cf5ba922d4caa4` | +| `timer/` | `@deepseek-ai/cordis-plugin-timer` | `@cordisjs/plugin-timer` | 1.1.4 | https://github.com/cordiverse/cordis (`packages/timer`) | `0b96d9fd517b998e44e2209560c9e5f0671ce933` | | `hmr/` | `@deepseek-ai/cordis-plugin-hmr` | `@cordisjs/plugin-hmr` | 1.0.15 | https://github.com/deepseek-harness/cordis (`packages/hmr`) | `abb0a307cb1d3b0947f455d590cf5ba922d4caa4` | | `logger-console/` | `@deepseek-ai/cordis-plugin-logger-console` | `@cordisjs/plugin-logger-console` | 1.0.0 | https://github.com/deepseek-harness/cordis (`packages/logger-console`) | `abb0a307cb1d3b0947f455d590cf5ba922d4caa4` | @@ -38,7 +38,7 @@ Keep this log exhaustive — every divergence from upstream must be listed. 4. **Vendored TypeScript source internal specifiers**: changed local relative imports/exports from upstream's specifier shape to explicit `.ts` specifiers so TypeScript rewrites emitted JS to `.js` while declarations keep explicit, NodeNext-safe `.ts` specifiers. This includes `loader/src/config/isolate.ts` using `declare module './entry.ts'`. Type-only dependencies use `import type` or inline `type` modifiers so ESM output does not retain erased interfaces. 5. **`schemastery/tsdown.config.ts` and `logger-console/tsdown.config.ts`**: ours, not upstream files — per-package build-shape overrides (dual ESM+CJS output; separate node/browser entries) for the repo-root tsdown build. They read the JS emitted under `lib/types` and then write the publish runtime entries under `lib/`. Like the regenerated tsconfigs, they are not part of the upstream sync surface. 6. **`cordis/src/fiber.ts` lifecycle hardening**: locally closes three reentrant disposal gaps. An effect's owner-list wrapper is registered before its setup body runs, so an unload begun from inside setup awaits setup and every collected cleanup; synchronous setup failure removes the wrapper and rolls back collected cleanup. Async cleanup stays owner-visible until quiescence, and Cordis's internal effect composition joins an already-running cleanup while repeated public disposer calls retain their upstream single-shot result. Effect creation is rejected while the owner is `UNLOADING` (while `PENDING` and `LOADING` remain legal), preventing cleanup-time registrations from escaping the unload snapshot. Child fibers register and receive their parent-owned disposer before `internal/plugin` publication, resolve dependency declarations added by that notification before activation, drain effects attached while pending, skip plugin execution when reentrant disposal invalidates the load epoch before its first checkpoint, and contain teardown-notification failures per observer so one callback cannot starve peers or interrupt ownership cleanup. -7. **Cordis and Loader JSDoc enrichment**: added `@param`/`@returns` tags and contract documentation (disposal semantics, waterfall veto, bail conditions, error cases) across the public plugin-author surface — `Context` (class, statics, and the `Context` interface properties incl. `root`), module re-exports, shared utility helpers, `EventsService`, `Fiber`, `RegistryService`, `ReflectService`, `Service`, `LoggerService` and their `declare module './context.ts'` overloads. Loader documentation covers `loader/src/{index,internal,config/entry,config/group,config/isolate,config/tree,config/utils}.ts`, including entry ownership, tree mutation methods, and the `!!js` discriminator. Comment-only; no code changes. Motivation: the website API-reference generator renders these docs and hard-errors on undocumented members. Retire this entry when the enrichment is upstreamed to the fork. +7. **Cordis, Loader and Timer JSDoc enrichment**: added `@param`/`@returns` tags and contract documentation (disposal semantics, waterfall veto, bail conditions, error cases) across the public plugin-author surface — `Context` (class, statics, and the `Context` interface properties incl. `root`), module re-exports, shared utility helpers, `EventsService`, `Fiber`, `RegistryService`, `ReflectService`, `Service`, `LoggerService` and their `declare module './context.ts'` overloads. Loader documentation covers `loader/src/{index,internal,config/entry,config/group,config/isolate,config/tree,config/utils}.ts`, including entry ownership, tree mutation methods, and the `!!js` discriminator. Timer retains its local class and helper summaries. Comment-only; no code changes. Motivation: the website API-reference generator renders these docs and hard-errors on undocumented members. Retire this entry when the enrichment is upstreamed to the fork. 8. **`include/src/index.ts` resilient file refresh and patch reapplication**: [#514](https://github.com/deepseek-harness/deepseek-harness/pull/514) validates a top-level entry array before caching parsed content, logs refresh failures, reapplies patches after file or Include-config edits, updates the Include config when it vetoes a restart, and uses `initial` only after `ENOENT`. These parse protections retain the running tree after an invalid file; plugin activation failures can leave a partially applied tree. Loader entry/group/tree mutations follow the pinned eager, non-transactional implementation and do not restore previous plugins or options. Covered by `packages/boot/app-boot/tests/{config-reload,user-patches}.spec.ts`. 9. **`hmr/src/index.ts` module-watcher readiness and native paths**: realpath the existing watch base, classify framework modules and attach listeners before reporting readiness, and compare config paths using both canonical and configured spellings. This preserves Node module-cache identity across Windows short-name paths and filesystem aliases. The main watcher uses `ignoreInitial: true` so startup reads do not trigger another refresh; exact profile patch watching is owned by `packages/boot/hmr/src/watch-config.ts`. Covered by `packages/boot/hmr/tests/watch-config.spec.ts`. 10. **Vendored Node-compatible TypeScript**: marked erased imports explicitly across `cordis`, `loader`, `include`, `hmr`, and `schemastery` so Node's native TypeScript transform does not request types as runtime exports. Schemastery's source uses an ESM default export and its package declares `type: module`. Conditional exports select `.mjs` for import and `.cjs` for require; without them, concurrent imports under module-hook hosts reach the CJS entry and can race its require of ESM Cosmokit (`ERR_REQUIRE_ESM_RACE_CONDITION`). diff --git a/vendor/timer/README.md b/vendor/timer/README.md index 3a1dcb1533..9735030a27 100644 --- a/vendor/timer/README.md +++ b/vendor/timer/README.md @@ -18,8 +18,7 @@ const dispose = root.timeout(() => { dispose() ``` -Timer handles are registered on the current fiber, so they are cleared -automatically when the plugin that created them is disposed. +Timer handles are registered on the current fiber, so they are cleared automatically when the plugin that created them is disposed. ## API @@ -32,5 +31,8 @@ automatically when the plugin that created them is disposed. | `ctx.throttle(callback, delay, noTrailing?)` | Return a throttled function with `.dispose()`. | | `ctx.debounce(callback, delay)` | Return a debounced function with `.dispose()`. | -`ctx.setTimeout()` and `ctx.setInterval()` are kept as deprecated aliases for -`ctx.timeout()` and `ctx.interval()`. +`ctx.setTimeout()` and `ctx.setInterval()` are kept as deprecated aliases for `ctx.timeout()` and `ctx.interval()`. + +Concurrent `next()` calls on an interval iterator complete in request order, one per tick. Returning from or throwing into the iterator settles every pending read; disposing the owning fiber rejects every pending read. Ticks without a pending read are discarded. + +Throttle and debounce wrappers return `void`, regardless of the callback return type. Calling `.dispose()` or disposing the owning fiber cancels pending work and makes later calls inert. `noTrailing` suppresses delayed throttle calls while retaining leading calls until disposal. diff --git a/vendor/timer/src/index.ts b/vendor/timer/src/index.ts index 009d14a5d4..8d133e5c7f 100644 --- a/vendor/timer/src/index.ts +++ b/vendor/timer/src/index.ts @@ -6,7 +6,7 @@ declare module '@deepseek-ai/cordis' { } } -type WithDispose = T & { dispose: () => void } +type Scheduled any> = ((...args: Parameters) => void) & { dispose: () => void } /** Disposable timer helpers mixed into Cordis contexts. */ export class TimerService extends Service { @@ -66,33 +66,37 @@ export class TimerService extends Service { }, 'ctx.interval()') } else { let done: { kind: 'return'; value: any } | { kind: 'throw'; reason: any } | undefined - let nextTask: PromiseWithResolvers> | undefined + const nextTasks: PromiseWithResolvers>[] = [] const dispose = this.ctx.effect(() => { const timer = setInterval(() => { - nextTask?.resolve({ done: false, value: undefined }) + nextTasks.shift()?.resolve({ done: false, value: undefined }) }, delay) return () => { clearInterval(timer) if (done) return done = { kind: 'throw', reason: new Error('Context has been disposed') } - nextTask?.reject(done.reason) + for (const task of nextTasks.splice(0)) task.reject(done.reason) } }, 'ctx.interval()') return { next: () => { - if (!done) return (nextTask = Promise.withResolvers()).promise + if (!done) { + const task = Promise.withResolvers>() + nextTasks.push(task) + return task.promise + } if (done.kind === 'return') return Promise.resolve({ done: true, value: done.value }) return Promise.reject(done.reason) }, return: (value) => { if (!done) done = { kind: 'return', value } - nextTask?.resolve({ done: true, value }) + for (const task of nextTasks.splice(0)) task.resolve({ done: true, value }) dispose() return Promise.resolve({ done: true, value }) }, throw: (reason) => { if (!done) done = { kind: 'throw', reason } - nextTask?.reject(reason) + for (const task of nextTasks.splice(0)) task.reject(reason) dispose() return Promise.resolve({ done: true, value: undefined }) }, @@ -103,42 +107,44 @@ export class TimerService extends Service { } } - private _schedule(label: string, trigger: (args: any[], isDisposed: boolean) => any, isDisposed = false) { + private _schedule(label: string, trigger: (args: any[]) => number | NodeJS.Timeout | undefined) { let timer: number | NodeJS.Timeout | undefined + let isDisposed = false const dispose = this.ctx.effect(() => () => { isDisposed = true clearTimeout(timer) + timer = undefined }, label) const wrapper: any = (...args: any[]) => { + if (isDisposed) return clearTimeout(timer) - timer = trigger(args, isDisposed) + timer = trigger(args) } wrapper.dispose = dispose return wrapper } /** Return a throttled function whose timer is disposed with the current fiber. */ - throttle void>(callback: F, delay: number, noTrailing?: boolean): WithDispose { + throttle any>(callback: F, delay: number, noTrailing?: boolean): Scheduled { let lastCall = -Infinity const execute = (...args: any[]) => { lastCall = Date.now() callback(...args) } - return this._schedule('ctx.throttle()', (args, isDisposed) => { + return this._schedule('ctx.throttle()', (args) => { const now = Date.now() const remaining = delay - now + lastCall if (remaining <= 0) { execute(...args) - } else if (!isDisposed) { + } else if (!noTrailing) { return setTimeout(execute, remaining, ...args) } - }, noTrailing) + }) } /** Return a debounced function whose timer is disposed with the current fiber. */ - debounce void>(callback: F, delay: number): WithDispose { - return this._schedule('ctx.debounce()', (args, isDisposed) => { - if (isDisposed) return + debounce any>(callback: F, delay: number): Scheduled { + return this._schedule('ctx.debounce()', (args) => { return setTimeout(callback, delay, ...args) }) } From c949deb1cab37f41b00c85f41fba945c7214302b Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Wed, 14 Oct 2026 15:27:15 +0800 Subject: [PATCH 060/369] refactor(util-time): publish only the span surface its consumers call The package API carried members and result types ahead of any caller. The span now publishes fromMilliseconds and decompose, the two entries dsh-time-context calls; the seconds constructor, the zero identity, the arithmetic and ordering operations, the millisecond getter, and the four fractional conversions land with the caller that needs them, recorded in the Agent Note and the README limitation. The package entry names its exports instead of re-exporting both modules, so the three result types and DurationParts, which no consumer names, are no longer part of the published surface. Every remaining new name has a named consumer: schedule imports the nine parser and bound names plus four rejection types, and canonicalizeTimeZone has seven consumer files. --- ...026-09-24-shared-time-vocabulary.i18n.yaml | 8 +- .../2026-09-24-shared-time-vocabulary.md | 4 +- .../2026-09-24-shared-time-vocabulary.zh.md | 4 +- packages/util/time/README.i18n.yaml | 12 +- packages/util/time/README.md | 11 +- packages/util/time/README.zh.md | 11 +- packages/util/time/src/duration.ts | 112 +-------------- packages/util/time/src/index.ts | 15 +- packages/util/time/tests/duration.spec.ts | 128 +++++++----------- 9 files changed, 89 insertions(+), 216 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml index c7d4d181a9..46925831d8 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.i18n.yaml @@ -9,11 +9,11 @@ en: abd0263d14861926 zh: e1736cd08e87f795 /agent-note-shared-time-vocabulary-for-wall-clock-selectors/decision: - en: db91e6074fd74bdb - zh: 1957b8b0e1243afc + en: 52b45f1ac62e65ab + zh: 7f83cd4fb8dd09a6 /agent-note-shared-time-vocabulary-for-wall-clock-selectors/alternatives-considered: en: 1737484c099fab52 zh: 303b144c52d167bd /agent-note-shared-time-vocabulary-for-wall-clock-selectors/consequences: - en: ef18e69924eb420b - zh: 27e42a923b94204a + en: 6ec74e7ac065bd5e + zh: 04d029059dea732b diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md index 5983ca85cf..607dc68b7a 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.md @@ -18,7 +18,7 @@ Schedule consumes it and keeps every domain decision. `canonicalizeTimeZone`, `d `dsh-time-context` consumes it too and keeps its stricter policy. `deriveBrowserTimeZoneContext` reads each user-rpc zone through `canonicalizeTimeZone` and refuses an alias whose canonical answer differs from the reported value, so the request boundary still requires an identity another process resolves back to the same name. Time-context owns its three refusals and their messages; the shared entry only answers the canonical name or a reason, and its `unsupported` rejection carries the platform error the library caught, so that refusal keeps its `cause` in both consumers. -`Duration` holds one span as a single millisecond count, and it is a class rather than a branded number because TypeScript's arithmetic erases a brand: adding two differently branded numbers answers an unbranded `number`, so a brand cannot stop seconds and milliseconds from being mixed, while an instance can. The unit is milliseconds because every producer and consumer in this repository already uses it (`Date.now()`, `SessionEvent.time`, the session log, the client), and because a double holds every millisecond of the whole `Date` range exactly, so no precision is spent on the unit. Instances are frozen; the count is readable only as `milliseconds` or through a fractional `toSeconds`/`toMinutes`/`toHours`/`toDays` conversion, and it deliberately overrides no coercion hook, so no reader receives a bare count whose unit it has to remember. Addition, subtraction, ordering, a negative-span test, and a whole-part `decompose()` are the whole operation set, alongside a `zero` identity and a `fromSeconds` constructor for a boundary whose unit is seconds. +`Duration` holds one span as a single millisecond count, and it is a class rather than a branded number because TypeScript's arithmetic erases a brand: adding two differently branded numbers answers an unbranded `number`, so a brand cannot stop seconds and milliseconds from being mixed, while an instance can. The unit is milliseconds because every producer and consumer in this repository already uses it (`Date.now()`, `SessionEvent.time`, the session log, the client), and because a double holds every millisecond of the whole `Date` range exactly, so no precision is spent on the unit. Instances are frozen, no coercion hook is overridden, and a span is bounded by the exactly representable millisecond range, which is what makes the split exact. The published surface is `fromMilliseconds` and a whole-part `decompose()`, the two entries the migrated consumer calls. The maintained `@js-temporal/polyfill` 0.5.1 becomes a runtime dependency of `dsh-util-time` as well as of `dsh-schedule`. Local resolution continues to use it, so the Host's IANA data still decides gaps and overlaps and no caller owns offset sampling. `util/time`'s own description and README drop the `zero-dependency` phrase as ambiguous: the group's charter forbids harness dependencies so any group may depend on util, and does not ban external packages, which is why `packages/README.md` and the `util/` line in `AGENTS.md` keep the phrase while `packages/util/README.md` and this package's own description name what it loads. @@ -44,7 +44,7 @@ The time profile now has one home. A change to the accepted instant profile or t The cost is a wider published surface and a heavier dependency graph: `dsh-util-time` gains a runtime dependency on `@js-temporal/polyfill`, so a consumer that imports only `canonicalClientTimeZone` now loads that module too. The library is also no longer a pure "validation and canonicalization only" package, which its README and package description had to state and which its Known Limitations section now records. Schedule keeps a thin mapping layer at each entry. The local-calendar and instant mappers switch over the library's closed reason unions with a `never` default, so a newly added reason fails the build there instead of silently taking a catch-all. The zone mapper instead branches only on `not_canonical_shape` — a reason the current IANA data never produces, so it is guarded by a `v8 ignore` rather than a `never` default — and folds `blank`, `padded`, `not_area_location`, and `unsupported` into one diagnostic. A reason added to `TimeZoneRejection` later would therefore compile there and silently take that generic message. -The value type ships its whole operation set before its second consumer exists. Only `fromMilliseconds` and `decompose()` have a production caller, `dsh-time-context`; `fromSeconds`, `milliseconds`, `plus`, `minus`, `compare`, `isNegative`, `zero`, and the four `to*` conversions are published without a caller. That is a deliberate choice rather than an oversight: the seconds-valued fields at `dsh-schedule` and the DeepSeek file store are the intended next consumers, and the alternative — a class that holds a span but can only split it — would have to grow the same members one call site at a time. The package README records the un-migrated boundaries as a known limitation. +A value type publishes what its consumers call, so the span exposes the constructor and the split that `dsh-time-context` uses and nothing else. A conversion, an arithmetic operation, or a `zero` identity lands with the caller that needs it; the seconds-valued fields at `dsh-schedule` and the DeepSeek file store are the spans that will ask for a second-unit conversion, and the bound and the freeze the class already carries are what make that addition a small one. The package README records the un-migrated boundaries as a known limitation. The elapsed reading keeps its split in the library and its wording in the consumer: `dsh-time-context` still decides that a backward clock reading clamps to zero and how the compact line reads, while `Duration.decompose()` answers the whole parts. That swap is byte-identical for every finite sample, measured over more than eight thousand inputs including negatives, fractions, and values past a year. That measurement is not a committed test: the consumer's own suite pins the readings it renders (`1d 1h 1m 1s`, `1m 1s`, `4m 2s`, and the clamped zero). Two samples do change: `NaN` rendered `NaNs` and `+Infinity` rendered `Infinityd NaNs`, and both now throw from the span constructor, so a reading that is not a finite number fails where it is produced instead of rendering text. A `-Infinity` sample is unchanged, because the consumer clamps it to zero before the span is built. diff --git a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md index fab2092c09..838f6b9d4b 100644 --- a/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md +++ b/.agents/notes/implemented/architecture/2026-09-24-shared-time-vocabulary.zh.md @@ -18,7 +18,7 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec `dsh-time-context` 同样消费它,并保留自己更严格的策略。`deriveBrowserTimeZoneContext` 通过 `canonicalizeTimeZone` 读取每条 user-rpc 时区,并拒绝规范答案与所报值不同的别名,因此请求边界仍然要求另一个进程能解析回同一名称。三条拒绝与它们的文案归 time-context 所有;共享入口只回答规范名称或一个原因;被拒绝的结果中只有 `unsupported` 会携带本库捕获到的平台错误,使该拒绝在两个消费方都保留其 `cause`。 -`Duration` 以单一毫秒计数保存一段时间;它做成类而不是 branded number,是因为 TypeScript 的算术会抹掉 brand:两个不同品牌的数字相加得到的是无品牌的 `number`,所以 brand 挡不住秒与毫秒的混用,而实例可以。单位选毫秒,是因为本仓库的每个生产者与消费者本来就用它(`Date.now()`、`SessionEvent.time`、Session 日志、客户端),也因为 double 能精确表示 `Date` 全范围内的每一个毫秒,这个单位不消耗任何精度。实例被冻结;计数只能通过 `milliseconds` 或按小数换算的 `toSeconds`/`toMinutes`/`toHours`/`toDays` 读取,且刻意不覆盖任何强制转换钩子,因此读取方永远不会拿到一个需要自己记单位的裸数字。加、减、排序、负值判断与整数量拆解 `decompose()` 就是全部操作集合,另有 `zero` 恒等元与面向以秒为单位边界的 `fromSeconds` 构造入口。 +`Duration` 以单一毫秒计数保存一段时间;它做成类而不是 branded number,是因为 TypeScript 的算术会抹掉 brand:两个不同品牌的数字相加得到的是无品牌的 `number`,所以 brand 挡不住秒与毫秒的混用,而实例可以。单位选毫秒,是因为本仓库的每个生产者与消费者本来就用它(`Date.now()`、`SessionEvent.time`、Session 日志、客户端),也因为 double 能精确表示 `Date` 全范围内的每一个毫秒,这个单位不消耗任何精度。实例被冻结,不覆盖任何强制转换钩子,且时间段受可精确表示的毫秒范围约束——这正是拆分精确的前提。公开面就是 `fromMilliseconds` 与整数量拆解 `decompose()`,即已迁移消费方所调用的两个入口。 受维护的 `@js-temporal/polyfill` 0.5.1 在成为 `dsh-schedule` 运行时依赖之外,也成为 `dsh-util-time` 的运行时依赖。本地解析继续使用它,因此缺口与重叠仍由宿主 IANA 数据决定,也没有调用方需要自己实现偏移采样。`util/time` 自身的描述与 README 去掉了 `zero-dependency` 这一说法,因为它容易被误读:该组的约定禁止的是 harness 依赖(以便任何组都能依赖 util),并不禁止外部包;因此 `packages/README.md` 与 `AGENTS.md` 里的 `util/` 一行保留该说法,而 `packages/util/README.md` 与本包自己的描述写明它加载了什么。 @@ -44,7 +44,7 @@ Schedule 消费它,并保留全部域内决策。`canonicalizeTimeZone`、`dec 代价是更宽的已发布接口与更重的依赖图:`dsh-util-time` 增加了对 `@js-temporal/polyfill` 的运行时依赖,因此只导入 `canonicalClientTimeZone` 的消费方也会加载该模块。本库也不再是纯"只做校验与规范化"的包——其 README 与包描述必须说明这一点,其"已知限制"一节现在也记录了它。Schedule 在每个入口保留一层薄映射。本地日历与时刻映射函数以 `never` 默认分支穷尽 switch 本库的封闭原因联合,因此新增原因会让这两个调用点构建失败,而不是静默走兜底分支。时区映射函数则只对 `not_canonical_shape` 分支——当前 IANA 数据不会产生该原因,因此它由 `v8 ignore` 守护而不是 `never` 默认分支——并把 `blank`、`padded`、`not_area_location` 与 `unsupported` 合并成一条诊断。因此日后给 `TimeZoneRejection` 新增原因时会通过编译并静默套用那条通用文案。 -该值类型在第二个消费方出现之前就交付了完整操作集合。目前只有 `fromMilliseconds` 与 `decompose()` 有生产调用方,即 `dsh-time-context`;`fromSeconds`、`milliseconds`、`plus`、`minus`、`compare`、`isNegative`、`zero` 与四个 `to*` 换算都没有调用方。这是有意的选择而不是疏漏:`dsh-schedule` 与 DeepSeek 文件存储里以秒为单位的字段是预期的下一批消费方,而另一种做法——一个能保存时间段却只能拆解它的类——会在每个调用点出现时再长出同样的成员。包 README 把这些尚未迁移的边界记为已知限制。 +值类型只发布消费方会调用的部分,因此这个时间段只暴露 `dsh-time-context` 用到的构造入口与拆解,其余不发布。某个换算、某个算术运算或 `zero` 恒等元都随第一个需要它的调用方落地;`dsh-schedule` 与 DeepSeek 文件存储里以秒为单位的字段就是将来会要求第二个单位换算的那些跨度,而类已经携带的范围约束与冻结正是让这次添加保持很小的原因。包 README 把这些尚未迁移的边界记为已知限制。 耗时读数的拆分上收到库、措辞留在一侧:`dsh-time-context` 仍然决定回退的时钟读数钳到零、以及那行紧凑文本怎么写,而 `Duration.decompose()` 回答整数量。这次替换对每个有限样本都逐字不变,用一份覆盖八千多个输入(含负数、小数与一年以上的值)的差分测得。该测量不是提交进仓库的测试:消费方自己那套测试固定了它渲染出的读数(`1d 1h 1m 1s`、`1m 1s`、`4m 2s`,以及被钳到零的那次)。有两个样本会变:`NaN` 原本渲染出 `NaNs`、`+Infinity` 原本渲染出 `Infinityd NaNs`,现在两者都由时间段构造函数抛出,于是非有限的读数在产生它的地方失败,而不是渲染出一段文本。`-Infinity` 样本不变,因为消费方在构造时间段之前已把它钳到零。 diff --git a/packages/util/time/README.i18n.yaml b/packages/util/time/README.i18n.yaml index efc4bd77f1..cad952bcf0 100644 --- a/packages/util/time/README.i18n.yaml +++ b/packages/util/time/README.i18n.yaml @@ -21,11 +21,11 @@ en: 85291d5fb4c9000b zh: d1717c3b01efb5d0 /dsh-util-time/use-this-package/entry-point: - en: 88668c3dd1bdb75b - zh: fb48d941ae433335 + en: 7ca7ddebcb6af6be + zh: 4702a29079d6b8a7 /dsh-util-time/understand-the-implementation: - en: ef1369f1e986acf5 - zh: b7a98e3d79048d50 + en: 14595019b72aa45f + zh: cfea261931019621 /dsh-util-time/further-exploration: en: 8aa95ebdfd1f323e zh: c6879eac9fa6e087 @@ -36,8 +36,8 @@ en: daf6e45a18c087b0 zh: e6714f1e65ed55ce /dsh-util-time/known-limitations-and-deferred-work: - en: 06ea0ac6f5102d88 - zh: bdc0d5d2a1faba41 + en: 854d5bf170c91bba + zh: b5544e6bd1319d90 /dsh-util-time/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: e88aeb617ffb74ea diff --git a/packages/util/time/README.md b/packages/util/time/README.md index 50976eb86f..c86f1c7462 100644 --- a/packages/util/time/README.md +++ b/packages/util/time/README.md @@ -45,11 +45,10 @@ if (local.kind === 'accepted') { resolveLocalInstant(local.time, zone ?? 'UTC') // epoch milliseconds, or undefined inside a DST gap } -Duration.fromSeconds(90).decompose() // { days: 0, hours: 0, minutes: 1, seconds: 30, milliseconds: 0 } -Duration.fromSeconds(90).milliseconds // 90000 — the unit is in the name, and no coercion answers the count +Duration.fromMilliseconds(1_500).decompose() // { days: 0, hours: 0, minutes: 0, seconds: 1, milliseconds: 500 } ``` -`canonicalClientTimeZone` answers `undefined` for a name it refuses, and the caller raises its own refusal — `session/invalid-time-zone` for the Session prompt, `subagent/invalid-time-zone` for a subagent continuation. `canonicalizeTimeZone`, the strict parsers, and `fourDigitYearEpoch` instead report why they refused, as a discriminated result whose `rejected` arm carries a stable reason, so each caller chooses its own code and diagnostic. `formatFourDigitYearInstant` and `resolveLocalInstant` take already-validated values and answer a string, or epoch milliseconds, with `undefined` for an unrepresentable epoch or a wall-clock value inside a DST gap. `Duration` is a frozen value type over one millisecond count: it validates that the count is finite and inside the exactly representable range, adds, subtracts, orders, and reports a negative span without any unit conversion, converts to seconds, minutes, hours, and days fractionally, and splits a non-negative span into whole parts. +`canonicalClientTimeZone` answers `undefined` for a name it refuses, and the caller raises its own refusal — `session/invalid-time-zone` for the Session prompt, `subagent/invalid-time-zone` for a subagent continuation. `canonicalizeTimeZone`, the strict parsers, and `fourDigitYearEpoch` instead report why they refused, as a discriminated result whose `rejected` arm carries a stable reason, so each caller chooses its own code and diagnostic. `formatFourDigitYearInstant` and `resolveLocalInstant` take already-validated values and answer a string, or epoch milliseconds, with `undefined` for an unrepresentable epoch or a wall-clock value inside a DST gap. `Duration` is a frozen value type over one millisecond count: it validates that the count is finite and inside the exactly representable range, and splits a non-negative span into whole parts. It publishes only the operations its consumers call; a member for another unit or for span arithmetic lands with the caller that needs it. ----- @@ -73,7 +72,7 @@ The strict parsers match a fixed regular expression before constructing any date | `formatFourDigitYearInstant` | An integral epoch inside the four-digit-year range; answers `undefined` outside it or when non-integral. | | `isFourDigitYearInstant` | The same range, as a predicate, so a caller that only needs the bound does not re-derive it. | | `resolveLocalInstant` | Exact calendar fields of a real date, or an already-built `Temporal.PlainDateTime`, plus an already-canonical zone. | -| `Duration` | One finite millisecond count, built from milliseconds or seconds, and never a bare count on read. | +| `Duration` | One finite millisecond count inside the exactly representable range, and its split into whole days, hours, minutes, seconds, and the remaining fraction. | @@ -105,8 +104,8 @@ None of its own. The consumer that injects a zone- or schedule-derived line into - **Alias resolution follows the runtime's ICU data** — which name an alias group canonicalizes to is the platform's answer, so two processes on different Node builds can disagree about it. - **No recurrence arithmetic** — instant validity, local-calendar parsing, zone resolution, canonical formatting, and a millisecond span only; a caller that walks candidate dates or intervals owns that search. - **`Duration` has no calendar units** — a day is exactly 24 hours and a month has no fixed length, so a caller that needs local calendar days derives them from the zone. -- **`Duration` does not convert units implicitly** — it overrides no coercion hook (`valueOf`, `toString`, `Symbol.toPrimitive`), so a reader states its unit through `milliseconds` or a `to*` conversion. -- **Seconds-unit spans still multiply** — `dsh-schedule`'s interval and delay fields and the DeepSeek file store's refresh margin are second-valued spans converted with `* 1_000`; `Duration.fromSeconds` is their intended next consumer, and none has moved yet. An epoch expressed in seconds is not a span and must not be built into one. +- **`Duration` publishes only what a consumer calls** — `fromMilliseconds` and `decompose()`; a conversion, an arithmetic operation, or a `zero` identity lands with the caller that needs it, so the published contract grows with a caller rather than ahead of one. +- **Seconds-unit spans still multiply** — `dsh-schedule`'s interval and delay fields and the DeepSeek file store's refresh margin are second-valued spans converted with `* 1_000`; none has moved onto this library, and an epoch expressed in seconds is not a span and must not be built into one. - **The `Temporal` polyfill is a runtime dependency** — a consumer that imports only `canonicalClientTimeZone` still resolves it at module load, because both entries share one module. diff --git a/packages/util/time/README.zh.md b/packages/util/time/README.zh.md index 642f921618..cecc42686f 100644 --- a/packages/util/time/README.zh.md +++ b/packages/util/time/README.zh.md @@ -45,11 +45,10 @@ if (local.kind === 'accepted') { resolveLocalInstant(local.time, zone ?? 'UTC') // epoch milliseconds, or undefined inside a DST gap } -Duration.fromSeconds(90).decompose() // { days: 0, hours: 0, minutes: 1, seconds: 30, milliseconds: 0 } -Duration.fromSeconds(90).milliseconds // 90000 — the unit is in the name, and no coercion answers the count +Duration.fromMilliseconds(1_500).decompose() // { days: 0, hours: 0, minutes: 0, seconds: 1, milliseconds: 500 } ``` -不可用的时区名称由 `canonicalClientTimeZone` 回答 `undefined`,由调用方抛出自己的拒绝——Session 提示词用 `session/invalid-time-zone`,subagent 续话用 `subagent/invalid-time-zone`。`canonicalizeTimeZone`、各严格解析器与 `fourDigitYearEpoch` 则会说明拒绝原因,以带判别值的结果回答,其 `rejected` 分支携带稳定的拒绝原因,由各调用方选择自己的错误码与诊断文案。`formatFourDigitYearInstant` 与 `resolveLocalInstant` 接收已校验的值,分别回答字符串与纪元毫秒,无法表示的纪元或落在 DST 缺口内的墙钟值回答 `undefined`。`Duration` 是一个基于单一毫秒计数的冻结值类型:它校验计数有限且落在可精确表示的范围内,加减、排序与负值判断都不做单位换算,按小数换算到秒、分、小时与天,并把非负的时间段拆成整数量。 +不可用的时区名称由 `canonicalClientTimeZone` 回答 `undefined`,由调用方抛出自己的拒绝——Session 提示词用 `session/invalid-time-zone`,subagent 续话用 `subagent/invalid-time-zone`。`canonicalizeTimeZone`、各严格解析器与 `fourDigitYearEpoch` 则会说明拒绝原因,以带判别值的结果回答,其 `rejected` 分支携带稳定的拒绝原因,由各调用方选择自己的错误码与诊断文案。`formatFourDigitYearInstant` 与 `resolveLocalInstant` 接收已校验的值,分别回答字符串与纪元毫秒,无法表示的纪元或落在 DST 缺口内的墙钟值回答 `undefined`。`Duration` 是一个基于单一毫秒计数的冻结值类型:它校验计数有限且落在可精确表示的范围内,并把非负的时间段拆成整数量。它只发布消费方会调用的部分;面向其它单位或时间段算术的成员都随第一个需要它的调用方落地。 ----- @@ -73,7 +72,7 @@ Duration.fromSeconds(90).milliseconds // 90000 — the unit is in the name, and | `formatFourDigitYearInstant` | 四位年份范围内的整数纪元毫秒;超出范围或非整数时回答 `undefined`。 | | `isFourDigitYearInstant` | 同一范围的谓词形式,只判断范围边界的调用方无需自行推导。 | | `resolveLocalInstant` | 真实日期的精确日历字段,或已构造好的 `Temporal.PlainDateTime`,外加已规范化的时区。 | -| `Duration` | 一个有限的毫秒计数,可由毫秒或秒构造,读取时从不交出裸计数。 | +| `Duration` | 一个落在可精确表示范围内的毫秒计数,以及它拆出的整日、整时、整分、整秒与不足一秒的余数。 | @@ -105,8 +104,8 @@ Duration.fromSeconds(90).milliseconds // 90000 — the unit is in the name, and - **别名解析取决于运行时的 ICU 数据**——一个别名组规范化成哪个名称由平台回答,因此两个跑在不同 Node 构建上的进程可能给出不同答案。 - **不含重复规则运算**——只做时刻有效性、本地日历解析、时区解析、规范格式化与毫秒精度的时间段;遍历候选日期或间隔的搜索由调用方自己拥有。 - **`Duration` 没有日历单位**——一天恰好是 24 小时,月没有固定长度;需要本地日历天的调用方从时区推导。 -- **`Duration` 不做隐式单位换算**——它不覆盖任何强制转换钩子(`valueOf`、`toString`、`Symbol.toPrimitive`),读取方必须通过 `milliseconds` 或某个 `to*` 换算声明单位。 -- **以秒为单位的跨度仍在自行相乘**——`dsh-schedule` 的间隔与延迟字段、DeepSeek 文件存储的刷新余量都是以秒为单位的跨度,用 `* 1_000` 换算;`Duration.fromSeconds` 是它们预期的下一个消费方,目前都还没改。以秒表示的纪元不是跨度,不得构造成跨度。 +- **`Duration` 只发布消费方会调用的部分**——`fromMilliseconds` 与 `decompose()`;某个换算、某个算术运算或 `zero` 恒等元都随第一个需要它的调用方落地,公开契约跟着调用方长,而不是长在它前面。 +- **以秒为单位的跨度仍在自行相乘**——`dsh-schedule` 的间隔与延迟字段、DeepSeek 文件存储的刷新余量都是以秒为单位的跨度,用 `* 1_000` 换算;它们都还没迁移到本库,而以秒表示的纪元不是跨度,不得构造成跨度。 - **`Temporal` polyfill 是运行时依赖**——只导入 `canonicalClientTimeZone` 的消费方在模块加载时同样会解析它,因为两个入口共用一个模块。 diff --git a/packages/util/time/src/duration.ts b/packages/util/time/src/duration.ts index 55a221e6b5..3730d0a86f 100644 --- a/packages/util/time/src/duration.ts +++ b/packages/util/time/src/duration.ts @@ -1,19 +1,16 @@ /** - * One span of time, held as a count of milliseconds. + * One span of time, held as a count of milliseconds and split into the whole + * parts a reading is written in. * * A class rather than a branded number: arithmetic on two branded numbers * answers an unbranded `number`, so a brand cannot stop seconds and - * milliseconds from being added, while an instance can. No coercion hook is - * overridden, so no reader receives a bare count whose unit it has to remember; - * a caller states the unit by reading `milliseconds` or by converting through - * one of the `to*` entries. + * milliseconds from being added, while an instance can. The published surface + * is the set the migrated consumers call; a further member lands with the caller + * that needs it. * @module @deepseek-ai/dsh-util-time */ const MS_PER_SECOND = 1_000 -const MS_PER_MINUTE = 60 * MS_PER_SECOND -const MS_PER_HOUR = 60 * MS_PER_MINUTE -const MS_PER_DAY = 24 * MS_PER_HOUR const SECONDS_PER_DAY = 86_400 const SECONDS_PER_HOUR = 3_600 const SECONDS_PER_MINUTE = 60 @@ -47,13 +44,9 @@ const MAX_SPAN_MS = Number.MAX_SAFE_INTEGER * Instances are frozen, so a span cannot drift after it is built. A span is * bounded by the exactly representable millisecond range, which is wider than * any interval this harness schedules, and it carries no calendar meaning: a - * month is not a fixed count of milliseconds, so no entry here converts to or - * from calendar units. + * month is not a fixed count of milliseconds. */ export class Duration { - /** The additive identity. */ - static readonly zero: Duration = new Duration(0) - private readonly span: number private constructor(milliseconds: number) { @@ -79,99 +72,6 @@ export class Duration { return new Duration(milliseconds) } - /** - * Build one span from a second count, for a boundary whose unit is seconds. - * @param seconds - Finite second count; a fractional count is kept as given. - * @returns The span. - * @throws TypeError when the count is not finite, or its millisecond equivalent is not finite or leaves the exactly representable range. - */ - static fromSeconds(seconds: number): Duration { - if (!Number.isFinite(seconds)) { - throw new TypeError(`duration seconds must be finite, got ${String(seconds)}`) - } - return Duration.fromMilliseconds(seconds * MS_PER_SECOND) - } - - /** The span as a millisecond count; the unit is part of the name. */ - get milliseconds(): number { - return this.span - } - - /** - * Add one span to this span. - * @param other - Span to add; both operands already share the millisecond unit. - * @returns The sum. - * @throws TypeError when the sum leaves the exactly representable millisecond range. - */ - plus(other: Duration): Duration { - return Duration.fromMilliseconds(this.span + other.span) - } - - /** - * Subtract one span from this span. - * @param other - Span to subtract. - * @returns The difference, which is negative when `other` is longer. - * @throws TypeError when the difference leaves the exactly representable millisecond range. - */ - minus(other: Duration): Duration { - return Duration.fromMilliseconds(this.span - other.span) - } - - /** - * Order two spans. - * @param other - Span to compare with. - * @returns `-1` when this span is shorter, `0` when equal, `1` when longer. - */ - compare(other: Duration): -1 | 0 | 1 { - if (this.span < other.span) return -1 - return this.span > other.span ? 1 : 0 - } - - /** - * Whether the span is a negative interval, which every sign-sensitive reader - * must decide how to handle. - * @returns Whether the millisecond count is below zero. - */ - isNegative(): boolean { - return this.span < 0 - } - - /** - * The span in seconds. - * @returns The span as a fractional seconds count: one division, so the answer is the double nearest the exact ratio. - */ - toSeconds(): number { - return this.span / MS_PER_SECOND - } - - /** - * The span in minutes. - * @returns The span as a fractional minutes count: one division, so the answer is the double nearest the exact ratio. - */ - toMinutes(): number { - return this.span / MS_PER_MINUTE - } - - /** - * The span in hours. - * @returns The span as a fractional hours count: one division, so the answer is the double nearest the exact ratio. - */ - toHours(): number { - return this.span / MS_PER_HOUR - } - - /** - * The span in days. - * - * A day here is exactly 24 hours. A wall-clock reading that crosses a - * daylight-saving transition is not a whole number of days, so a caller that - * needs local calendar days derives them from the zone instead. - * @returns The span as a fractional days count: one division, so the answer is the double nearest the exact ratio. - */ - toDays(): number { - return this.span / MS_PER_DAY - } - /** * Split the span into whole days, hours, minutes, seconds, and the remaining * fraction of a second. Days, hours, minutes, and seconds are floored; the diff --git a/packages/util/time/src/index.ts b/packages/util/time/src/index.ts index e143295709..31d50fda3f 100644 --- a/packages/util/time/src/index.ts +++ b/packages/util/time/src/index.ts @@ -15,8 +15,19 @@ import { canonicalizeTimeZone } from './instant.ts' -export * from './duration.ts' -export * from './instant.ts' +export { Duration } from './duration.ts' +export { + MAX_FOUR_DIGIT_YEAR_MS, + MIN_FOUR_DIGIT_YEAR_MS, + canonicalizeTimeZone, + formatFourDigitYearInstant, + fourDigitYearEpoch, + isFourDigitYearInstant, + parseLocalCalendar, + parseOffsetInstant, + resolveLocalInstant, +} from './instant.ts' +export type { InstantRejection, LocalCalendarRejection, LocalCalendarTime, TimeZoneRejection } from './instant.ts' /** * Validate and canonicalize one caller-supplied IANA zone at a wire boundary. diff --git a/packages/util/time/tests/duration.spec.ts b/packages/util/time/tests/duration.spec.ts index 8edd4f1ead..1430bf7805 100644 --- a/packages/util/time/tests/duration.spec.ts +++ b/packages/util/time/tests/duration.spec.ts @@ -1,45 +1,59 @@ import { describe, expect, it } from 'vitest' import { Duration } from '@deepseek-ai/dsh-util-time' +/** Rebuild a span from its own parts, so a wrong split shows up as a wrong total. */ +const partSum = (parts: { + days: number + hours: number + minutes: number + seconds: number + milliseconds: number +}): number => + parts.days * 86_400_000 + parts.hours * 3_600_000 + + parts.minutes * 60_000 + parts.seconds * 1_000 + parts.milliseconds + +/** Deterministic span source, so a failure is reproducible from the seed alone. */ +const spans = (count: number, seed: number): number[] => { + const out: number[] = [] + let state = seed + for (let i = 0; i < count; i += 1) { + state = (state * 1_103_515_245 + 12_345) % 2_147_483_648 + out.push((state / 2_147_483_648) * Number.MAX_SAFE_INTEGER) + } + return out +} + describe('Duration construction', () => { - it('holds the millisecond count it was built from, fraction included', () => { - expect(Duration.fromMilliseconds(1_500).milliseconds).toBe(1_500) - expect(Duration.fromMilliseconds(0.5).milliseconds).toBe(0.5) - expect(Duration.zero.milliseconds).toBe(0) + it('accepts a fractional millisecond count and keeps the fraction', () => { + expect(Duration.fromMilliseconds(1_500.25).decompose()) + .toEqual({ days: 0, hours: 0, minutes: 0, seconds: 1, milliseconds: 500.25 }) }) - it('converts a second count at the boundary that carries seconds', () => { - expect(Duration.fromSeconds(300).milliseconds).toBe(300_000) - expect(Duration.fromSeconds(0.25).milliseconds).toBe(250) - expect(Duration.fromSeconds(-60).milliseconds).toBe(-60_000) + it('accepts the largest exactly representable count', () => { + const parts = Duration.fromMilliseconds(Number.MAX_SAFE_INTEGER).decompose() + expect(parts).toEqual({ days: 104_249_991, hours: 8, minutes: 59, seconds: 0, milliseconds: 991 }) + expect(partSum(parts)).toBe(Number.MAX_SAFE_INTEGER) }) it('refuses a count that is not a finite number', () => { for (const value of [Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY]) { expect(() => Duration.fromMilliseconds(value)).toThrow(TypeError) - expect(() => Duration.fromSeconds(value)).toThrow(TypeError) } }) - it('refuses a finite second count whose millisecond equivalent leaves the finite range', () => { - expect(() => Duration.fromSeconds(1e306)).toThrow(TypeError) - }) - it('refuses a count above the exactly representable millisecond range', () => { expect(() => Duration.fromMilliseconds(Number.MAX_SAFE_INTEGER + 1)).toThrow(TypeError) - expect(Duration.fromMilliseconds(Number.MAX_SAFE_INTEGER).milliseconds).toBe(Number.MAX_SAFE_INTEGER) + expect(() => Duration.fromMilliseconds(-Number.MAX_SAFE_INTEGER - 2)).toThrow(TypeError) // The bound is what keeps the whole-part identity exact, so it is asserted, not assumed. - const parts = Duration.fromMilliseconds(Number.MAX_SAFE_INTEGER).decompose() - const summed = parts.days * 86_400_000 + parts.hours * 3_600_000 - + parts.minutes * 60_000 + parts.seconds * 1_000 + parts.milliseconds - expect(summed).toBe(Number.MAX_SAFE_INTEGER) + expect(partSum(Duration.fromMilliseconds(Number.MAX_SAFE_INTEGER).decompose())) + .toBe(Number.MAX_SAFE_INTEGER) }) it('is frozen, and overrides no implicit conversion', () => { const span = Duration.fromMilliseconds(1) expect(Object.isFrozen(span)).toBe(true) expect(Reflect.set(span, 'span', 2)).toBe(false) - expect(span.milliseconds).toBe(1) + expect(span.decompose().milliseconds).toBe(1) // No coercion hook is overridden, so no caller obtains the count by coercion. expect(Object.hasOwn(Duration.prototype, 'valueOf')).toBe(false) expect(Object.hasOwn(Duration.prototype, 'toString')).toBe(false) @@ -47,54 +61,6 @@ describe('Duration construction', () => { }) }) -describe('Duration arithmetic', () => { - it('adds and subtracts in the one shared unit', () => { - expect(Duration.fromSeconds(2).plus(Duration.fromMilliseconds(500)).milliseconds).toBe(2_500) - expect(Duration.fromSeconds(2).minus(Duration.fromMilliseconds(500)).milliseconds).toBe(1_500) - expect(Duration.fromMilliseconds(1).minus(Duration.fromSeconds(1)).isNegative()).toBe(true) - expect(Duration.zero.plus(Duration.zero).milliseconds).toBe(0) - }) - - it('refuses a sum or difference that leaves the exactly representable range', () => { - const max = Duration.fromMilliseconds(Number.MAX_SAFE_INTEGER) - expect(() => max.plus(Duration.fromMilliseconds(1))).toThrow(TypeError) - expect(() => Duration.zero.minus(max).minus(Duration.fromMilliseconds(1))).toThrow(TypeError) - // The bound is checked at every construction, not only at the factories. - expect(max.plus(Duration.fromMilliseconds(0)).milliseconds).toBe(Number.MAX_SAFE_INTEGER) - }) - - it('orders spans without surrendering the unit', () => { - const minute = Duration.fromSeconds(60) - const hour = Duration.fromSeconds(3_600) - expect(minute.compare(hour)).toBe(-1) - expect(hour.compare(minute)).toBe(1) - expect(minute.compare(Duration.fromMilliseconds(60_000))).toBe(0) - expect(minute.isNegative()).toBe(false) - expect(Duration.fromMilliseconds(-1).isNegative()).toBe(true) - }) -}) - -describe('Duration conversion', () => { - it('converts to larger units fractionally, by one division each', () => { - const span = Duration.fromMilliseconds(90_061_500) - expect(span.toSeconds()).toBe(90_061.5) - expect(span.toMinutes()).toBe(1_501.025) - expect(span.toHours()).toBe(25.017_083_333_333_332) - expect(span.toDays()).toBe(1.042_378_472_222_222_2) - expect(Duration.fromSeconds(span.toSeconds()).milliseconds).toBe(span.milliseconds) - // A conversion is a single division, so a round trip returns the nearest - // double rather than the original count: 1001 ms is 1.001 s, and 1.001 s is - // not exactly 1001 ms. - expect(Duration.fromMilliseconds(1_001).toSeconds()).toBe(1.001) - expect(Duration.fromSeconds(1.001).milliseconds).toBe(1_000.999_999_999_999_9) - }) - - it('covers a day as exactly twenty-four hours', () => { - expect(Duration.fromMilliseconds(86_400_000).toDays()).toBe(1) - expect(Duration.fromMilliseconds(-86_400_000).toDays()).toBe(-1) - }) -}) - describe('Duration decompose', () => { it('splits a span into floored whole parts and the fractional remainder', () => { expect(Duration.fromMilliseconds(0).decompose()) @@ -103,39 +69,37 @@ describe('Duration decompose', () => { .toEqual({ days: 0, hours: 0, minutes: 0, seconds: 0, milliseconds: 999 }) expect(Duration.fromMilliseconds(1_000).decompose()) .toEqual({ days: 0, hours: 0, minutes: 0, seconds: 1, milliseconds: 0 }) + expect(Duration.fromMilliseconds(3_661_999).decompose()) + .toEqual({ days: 0, hours: 1, minutes: 1, seconds: 1, milliseconds: 999 }) expect(Duration.fromMilliseconds(90_061_500).decompose()) .toEqual({ days: 1, hours: 1, minutes: 1, seconds: 1, milliseconds: 500 }) - expect(Duration.fromMilliseconds(86_400_000).decompose()) - .toEqual({ days: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }) expect(Duration.fromMilliseconds(90_061_500.75).decompose()) .toEqual({ days: 1, hours: 1, minutes: 1, seconds: 1, milliseconds: 500.75 }) + expect(Duration.fromMilliseconds(86_400_000).decompose()) + .toEqual({ days: 1, hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }) }) - it('answers frozen parts whose sum never exceeds the span', () => { - const parts = Duration.fromMilliseconds(3_661_999).decompose() - expect(Object.isFrozen(parts)).toBe(true) - const summed = parts.days * 86_400_000 + parts.hours * 3_600_000 - + parts.minutes * 60_000 + parts.seconds * 1_000 + parts.milliseconds - expect(summed).toBe(3_661_999) + it('answers frozen parts', () => { + expect(Object.isFrozen(Duration.fromMilliseconds(3_661_999).decompose())).toBe(true) }) it('keeps the whole-part identity across the accepted range', () => { - const sum = (parts: { days: number; hours: number; minutes: number; seconds: number; milliseconds: number }): number => - parts.days * 86_400_000 + parts.hours * 3_600_000 - + parts.minutes * 60_000 + parts.seconds * 1_000 + parts.milliseconds for (let span = 0; span <= 120_000; span += 1) { - expect(sum(Duration.fromMilliseconds(span).decompose())).toBe(span) + expect(partSum(Duration.fromMilliseconds(span).decompose())).toBe(span) } - for (let i = 0; i < 2_000; i += 1) { - const span = Math.random() * Number.MAX_SAFE_INTEGER + for (const span of spans(2_000, 20_260_924)) { const parts = Duration.fromMilliseconds(span).decompose() - expect(sum(parts)).toBe(span) + expect(partSum(parts)).toBe(span) expect(parts.milliseconds).toBeGreaterThanOrEqual(0) expect(parts.milliseconds).toBeLessThan(1_000) + expect(parts.hours).toBeLessThan(24) + expect(parts.minutes).toBeLessThan(60) + expect(parts.seconds).toBeLessThan(60) } }) it('refuses a negative span instead of inventing a sign policy', () => { expect(() => Duration.fromMilliseconds(-1).decompose()).toThrow(RangeError) + expect(() => Duration.fromMilliseconds(-0.5).decompose()).toThrow(RangeError) }) }) From f10be1f63abd2bc07eedc0d3ed5f74bf5a4ab5ca Mon Sep 17 00:00:00 2001 From: Shigma Date: Thu, 15 Oct 2026 16:47:43 +0800 Subject: [PATCH 061/369] docs(cordis): document Timer wrapper migration and local docs --- .../cordis-timer-wrappers/guide.i18n.yaml | 16 ++++++++++++++ .../cordis-timer-wrappers/guide.md | 21 +++++++++++++++++++ .../cordis-timer-wrappers/guide.zh.md | 21 +++++++++++++++++++ .../cordis-client-runner/README.i18n.yaml | 4 ++-- .../extensions/cordis-client-runner/README.md | 2 +- .../cordis-client-runner/README.zh.md | 2 +- .../tests/timer.client.spec.ts | 16 ++++++++++++++ scripts/cordis-timer.spec.ts | 2 +- vendor/README.md | 2 +- vendor/cordis-rc11-upgrade-plan.md | 2 +- 10 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 docs/upgrade-guide/v0.2.1-alpha.2/cordis-timer-wrappers/guide.i18n.yaml create mode 100644 docs/upgrade-guide/v0.2.1-alpha.2/cordis-timer-wrappers/guide.md create mode 100644 docs/upgrade-guide/v0.2.1-alpha.2/cordis-timer-wrappers/guide.zh.md diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/cordis-timer-wrappers/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.2/cordis-timer-wrappers/guide.i18n.yaml new file mode 100644 index 0000000000..05552c8917 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.2/cordis-timer-wrappers/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-alpha.2/cordis-timer-wrappers/guide.md +/: + en: a1e092dfacd2aed8 + zh: 838448e5bf0f9d93 +/timer-wrapper-return-types-and-disposal: + en: 8b8de9b0fc2e3fc3 + zh: ffa20b7ffa7540ef +/timer-wrapper-return-types-and-disposal/change: + en: 978657a23b852a1e + zh: 1402cc87410cfc7a +/timer-wrapper-return-types-and-disposal/migration: + en: c07c4d66ff9fd1ff + zh: ecebc12b08a9be47 diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/cordis-timer-wrappers/guide.md b/docs/upgrade-guide/v0.2.1-alpha.2/cordis-timer-wrappers/guide.md new file mode 100644 index 0000000000..f76b1aeba7 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.2/cordis-timer-wrappers/guide.md @@ -0,0 +1,21 @@ +--- +kind: upgrade-guide +description: "Timer wrappers return void in TypeScript and ignore calls after disposal." +--- + +# Timer wrapper return types and disposal + +English | [中文](guide.zh.md) + +## Change + +The functions returned by `ctx.throttle()` and `ctx.debounce()` now declare a `void` return type. They previously inherited the callback's return type, although they already returned `undefined` at runtime. TypeScript code that uses the wrapper's return value or assigns it to the callback's type may need updating. This applies to Host Timer and browser ClientTimerService. + +Calling `.dispose()` or disposing the owning fiber cancels pending work and makes later wrapper calls inert. A disposed throttle wrapper previously could still execute a leading call; debounce wrappers already ignored calls after disposal. + +## Migration + +1. Let TypeScript infer the wrapper type, or declare its call signature with a `void` return type while retaining `.dispose()` where needed. +2. Consume callback results inside the callback. Awaiting the wrapper does not wait for an asynchronous callback to finish. +3. Move required work before disposal, or create a new wrapper in an active plugin. +4. Run your extension's type check and verify that calls after disposal neither invoke the callback nor schedule work. diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/cordis-timer-wrappers/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.2/cordis-timer-wrappers/guide.zh.md new file mode 100644 index 0000000000..15672e374e --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.2/cordis-timer-wrappers/guide.zh.md @@ -0,0 +1,21 @@ +--- +kind: upgrade-guide +description: "Timer 包装函数的 TypeScript 返回类型为 void,释放后忽略后续调用。" +--- + +# Timer 包装函数的返回类型与资源释放 + +[English](guide.md) | 中文 + +## 变更 + +`ctx.throttle()` 和 `ctx.debounce()` 返回的函数现在声明 `void` 返回类型。此前这些函数继承回调的返回类型,但运行时已经返回 `undefined`。使用包装函数返回值或将包装函数赋给回调类型的 TypeScript 代码可能需要更新。此变更适用于 Host Timer 和浏览器 ClientTimerService。 + +调用 `.dispose()` 或释放所属 fiber 会取消待执行的工作,并使后续包装函数调用不再执行。此前已释放的 throttle 包装函数仍可能执行前沿调用;debounce 包装函数在释放后已经会忽略调用。 + +## 迁移 + +1. 让 TypeScript 推断包装函数的类型,或将其调用签名声明为返回 `void`,并按需保留 `.dispose()`。 +2. 在回调内部使用回调结果。对包装函数使用 await 不会等待异步回调完成。 +3. 将必须执行的工作移到资源释放之前,或在活动插件中创建新的包装函数。 +4. 运行扩展的类型检查,并验证释放后的调用既不会执行回调,也不会安排工作。 diff --git a/packages/extensions/cordis-client-runner/README.i18n.yaml b/packages/extensions/cordis-client-runner/README.i18n.yaml index 7352fc1585..fb20ce928e 100644 --- a/packages/extensions/cordis-client-runner/README.i18n.yaml +++ b/packages/extensions/cordis-client-runner/README.i18n.yaml @@ -33,8 +33,8 @@ en: df635b29eaef982c zh: ddaa4bd98359bd30 /deepseek-ai-dsh-cordis-client-runner/understand-the-implementation/source-map: - en: 22a43e93fd123200 - zh: f8305186a748369b + en: f69f511aa85127f2 + zh: 651ea63d81bbd648 /deepseek-ai-dsh-cordis-client-runner/understand-the-implementation/inspecting-slot-inputs: en: 33f14a7fff7835e6 zh: 53838ac8d4a79600 diff --git a/packages/extensions/cordis-client-runner/README.md b/packages/extensions/cordis-client-runner/README.md index 90dfd95b97..f1e0ea134c 100644 --- a/packages/extensions/cordis-client-runner/README.md +++ b/packages/extensions/cordis-client-runner/README.md @@ -64,7 +64,7 @@ The browser half is built on one principle: a dynamic package must ride the same | [`src/client/guard.ts`](src/client/guard.ts) | The whitelisting `ctx` façade for loaded browser halves | | [`src/client/inspect-registry.ts`](src/client/inspect-registry.ts) | Client Inspect Providers and the pending-query router | | [`src/client/providers.ts`](src/client/providers.ts) | First-party client Inspect Providers (slots, theme, events) | -| [`src/client/timer.ts`](src/client/timer.ts) | Client timers with ordered concurrent interval reads and void-returning throttle/debounce wrappers that become inert on disposal | +| [`src/client/timer.ts`](src/client/timer.ts) | Browser timer Service matching the Host Timer API | ### Inspecting slot inputs diff --git a/packages/extensions/cordis-client-runner/README.zh.md b/packages/extensions/cordis-client-runner/README.zh.md index c4eea99ce7..1440caf92e 100644 --- a/packages/extensions/cordis-client-runner/README.zh.md +++ b/packages/extensions/cordis-client-runner/README.zh.md @@ -64,7 +64,7 @@ kind: "package-reference" | [`src/client/guard.ts`](src/client/guard.ts) | 已装载浏览器半收到的白名单 `ctx` façade | | [`src/client/inspect-registry.ts`](src/client/inspect-registry.ts) | Client Inspect Provider 与待答查询路由器 | | [`src/client/providers.ts`](src/client/providers.ts) | 第一方 client Inspect Provider(slots、theme、events) | -| [`src/client/timer.ts`](src/client/timer.ts) | client 定时器按顺序完成并发 interval 读取;throttle/debounce 包装函数返回 void,释放后不再执行 | +| [`src/client/timer.ts`](src/client/timer.ts) | 与 Host Timer API 一致的浏览器定时器 Service | ### 查询槽位输入 diff --git a/packages/extensions/cordis-client-runner/tests/timer.client.spec.ts b/packages/extensions/cordis-client-runner/tests/timer.client.spec.ts index 9ad26bccc7..c672e9d9fe 100644 --- a/packages/extensions/cordis-client-runner/tests/timer.client.spec.ts +++ b/packages/extensions/cordis-client-runner/tests/timer.client.spec.ts @@ -50,4 +50,20 @@ describe('client Timer', () => { expect(work).toHaveBeenCalledExactlyOnceWith('leading') expect(vi.getTimerCount()).toBe(0) }) + + it('retains leading calls and suppresses trailing calls with noTrailing', async () => { + const work = vi.fn() + const throttle = ctx.throttle(work, 20, true) + throttle('first') + throttle('suppressed') + await vi.advanceTimersByTimeAsync(20) + expect(work).toHaveBeenCalledExactlyOnceWith('first') + throttle('next window') + expect(work.mock.calls).toEqual([['first'], ['next window']]) + throttle.dispose() + await vi.advanceTimersByTimeAsync(20) + throttle('disposed') + expect(work).toHaveBeenCalledTimes(2) + expect(vi.getTimerCount()).toBe(0) + }) }) diff --git a/scripts/cordis-timer.spec.ts b/scripts/cordis-timer.spec.ts index 6f6822853f..f2eb258352 100644 --- a/scripts/cordis-timer.spec.ts +++ b/scripts/cordis-timer.spec.ts @@ -71,7 +71,7 @@ describe('Cordis Timer', () => { throttled('second') await vi.advanceTimersByTimeAsync(10) expect(callback.mock.calls).toEqual(noTrailing ? [['first']] : [['first'], ['second']]) - throttled('pending') + throttled('before disposal') const calls = callback.mock.calls.length throttled.dispose() await vi.advanceTimersByTimeAsync(20) diff --git a/vendor/README.md b/vendor/README.md index b076fb4443..6d22829ab9 100644 --- a/vendor/README.md +++ b/vendor/README.md @@ -38,7 +38,7 @@ Keep this log exhaustive — every divergence from upstream must be listed. 4. **Vendored TypeScript source internal specifiers**: changed local relative imports/exports from upstream's specifier shape to explicit `.ts` specifiers so TypeScript rewrites emitted JS to `.js` while declarations keep explicit, NodeNext-safe `.ts` specifiers. This includes `loader/src/config/isolate.ts` using `declare module './entry.ts'`. Type-only dependencies use `import type` or inline `type` modifiers so ESM output does not retain erased interfaces. 5. **`schemastery/tsdown.config.ts` and `logger-console/tsdown.config.ts`**: ours, not upstream files — per-package build-shape overrides (dual ESM+CJS output; separate node/browser entries) for the repo-root tsdown build. They read the JS emitted under `lib/types` and then write the publish runtime entries under `lib/`. Like the regenerated tsconfigs, they are not part of the upstream sync surface. 6. **`cordis/src/fiber.ts` lifecycle hardening**: locally closes three reentrant disposal gaps. An effect's owner-list wrapper is registered before its setup body runs, so an unload begun from inside setup awaits setup and every collected cleanup; synchronous setup failure removes the wrapper and rolls back collected cleanup. Async cleanup stays owner-visible until quiescence, and Cordis's internal effect composition joins an already-running cleanup while repeated public disposer calls retain their upstream single-shot result. Effect creation is rejected while the owner is `UNLOADING` (while `PENDING` and `LOADING` remain legal), preventing cleanup-time registrations from escaping the unload snapshot. Child fibers register and receive their parent-owned disposer before `internal/plugin` publication, resolve dependency declarations added by that notification before activation, drain effects attached while pending, skip plugin execution when reentrant disposal invalidates the load epoch before its first checkpoint, and contain teardown-notification failures per observer so one callback cannot starve peers or interrupt ownership cleanup. -7. **Cordis, Loader and Timer JSDoc enrichment**: added `@param`/`@returns` tags and contract documentation (disposal semantics, waterfall veto, bail conditions, error cases) across the public plugin-author surface — `Context` (class, statics, and the `Context` interface properties incl. `root`), module re-exports, shared utility helpers, `EventsService`, `Fiber`, `RegistryService`, `ReflectService`, `Service`, `LoggerService` and their `declare module './context.ts'` overloads. Loader documentation covers `loader/src/{index,internal,config/entry,config/group,config/isolate,config/tree,config/utils}.ts`, including entry ownership, tree mutation methods, and the `!!js` discriminator. Timer retains its local class and helper summaries. Comment-only; no code changes. Motivation: the website API-reference generator renders these docs and hard-errors on undocumented members. Retire this entry when the enrichment is upstreamed to the fork. +7. **Cordis, Loader and Timer JSDoc enrichment**: added `@param`/`@returns` tags and contract documentation (disposal semantics, waterfall veto, bail conditions, error cases) across the public plugin-author surface — `Context` (class, statics, and the `Context` interface properties incl. `root`), module re-exports, shared utility helpers, `EventsService`, `Fiber`, `RegistryService`, `ReflectService`, `Service`, `LoggerService` and their `declare module './context.ts'` overloads. Loader documentation covers `loader/src/{index,internal,config/entry,config/group,config/isolate,config/tree,config/utils}.ts`, including entry ownership, tree mutation methods, and the `!!js` discriminator. Timer retains its local class and helper summaries; its README is local documentation because the upstream package has none. Comment-only; no code changes. Motivation: the website API-reference generator renders these docs and hard-errors on undocumented members. Retire this entry when the enrichment is upstreamed to the fork. 8. **`include/src/index.ts` resilient file refresh and patch reapplication**: [#514](https://github.com/deepseek-harness/deepseek-harness/pull/514) validates a top-level entry array before caching parsed content, logs refresh failures, reapplies patches after file or Include-config edits, updates the Include config when it vetoes a restart, and uses `initial` only after `ENOENT`. These parse protections retain the running tree after an invalid file; plugin activation failures can leave a partially applied tree. Loader entry/group/tree mutations follow the pinned eager, non-transactional implementation and do not restore previous plugins or options. Covered by `packages/boot/app-boot/tests/{config-reload,user-patches}.spec.ts`. 9. **`hmr/src/index.ts` module-watcher readiness and native paths**: realpath the existing watch base, classify framework modules and attach listeners before reporting readiness, and compare config paths using both canonical and configured spellings. This preserves Node module-cache identity across Windows short-name paths and filesystem aliases. The main watcher uses `ignoreInitial: true` so startup reads do not trigger another refresh; exact profile patch watching is owned by `packages/boot/hmr/src/watch-config.ts`. Covered by `packages/boot/hmr/tests/watch-config.spec.ts`. 10. **Vendored Node-compatible TypeScript**: marked erased imports explicitly across `cordis`, `loader`, `include`, `hmr`, and `schemastery` so Node's native TypeScript transform does not request types as runtime exports. Schemastery's source uses an ESM default export and its package declares `type: module`. Conditional exports select `.mjs` for import and `.cjs` for require; without them, concurrent imports under module-hook hosts reach the CJS entry and can race its require of ESM Cosmokit (`ERR_REQUIRE_ESM_RACE_CONDITION`). diff --git a/vendor/cordis-rc11-upgrade-plan.md b/vendor/cordis-rc11-upgrade-plan.md index ac53067459..5669425a68 100644 --- a/vendor/cordis-rc11-upgrade-plan.md +++ b/vendor/cordis-rc11-upgrade-plan.md @@ -4,7 +4,7 @@ Status: proposed ## Problem -The [vendor manifest](README.md) records Cordis core 4.0.0-rc.7, Loader 1.0.0-rc.5, Include 1.0.4, HMR 1.0.15 and Timer 1.1.2, with 24 local-modification entries. [Cordis v4.0.0-rc.11](https://github.com/cordiverse/cordis/releases/tag/v4.0.0-rc.11) incorporates some fixes and replaces other mechanisms. Updating every package and retiring every patch in one change would combine configuration, persistence, lifecycle and module-replacement decisions. +The upgrade starts from the [vendored](README.md) baselines Cordis core 4.0.0-rc.7, Loader 1.0.0-rc.5, Include 1.0.4, HMR 1.0.15 and Timer 1.1.2, with 24 local-modification entries. [Cordis v4.0.0-rc.11](https://github.com/cordiverse/cordis/releases/tag/v4.0.0-rc.11) incorporates some fixes and replaces other mechanisms. Updating every package and retiring every patch in one change would combine configuration, persistence, lifecycle and module-replacement decisions. DSH also has consumers that upstream Cordis does not own. Volatile configuration fields are stable references updated without restarting plugins; the settings UI reads their live values. A profile selects ordered plugin bundles and configuration overrides. [DSH HMR](../packages/boot/hmr/README.md) coordinates module replacement and profile changes through `ctx.hmr`. An entry's module namespace is its raw import result, retained before selecting `default` or `apply` so HMR can identify the module already loaded. These consumers must remain usable at every merge point. From c3efd4bff844ad479a967623518566a17af61f0e Mon Sep 17 00:00:00 2001 From: _Kerman Date: Thu, 15 Oct 2026 18:18:58 +0800 Subject: [PATCH 062/369] refactor(session-projection): keep state reads inside owning services --- ...ession-projection-mandatory-seam.i18n.yaml | 4 +- ...08-19-session-projection-mandatory-seam.md | 4 +- ...19-session-projection-mandatory-seam.zh.md | 4 +- ...-projection-registration-handles.i18n.yaml | 8 +- ...session-projection-registration-handles.md | 6 +- ...sion-projection-registration-handles.zh.md | 6 +- .../session-corpus/projection-list.worker.ts | 2 +- .../session-open/session-open.worker.ts | 10 +-- docs/architecture.i18n.yaml | 4 +- docs/architecture.md | 2 +- docs/architecture.zh.md | 2 +- docs/config-catalog.md | 46 +++++----- docs/config-catalog.zh.md | 46 +++++----- docs/event-producer-consumer.md | 2 +- docs/event-producer-consumer.zh.md | 2 +- docs/persistence-catalog.i18n.yaml | 16 ++-- docs/persistence-catalog.md | 8 +- docs/persistence-catalog.zh.md | 8 +- docs/persistence-schema.json | 4 +- docs/subsystems/core.i18n.yaml | 3 + docs/subsystems/core.md | 38 ++++++++- docs/subsystems/core.zh.md | 38 ++++++++- docs/subsystems/token-meter.i18n.yaml | 3 + docs/subsystems/token-meter.md | 45 ++++++++++ docs/subsystems/token-meter.zh.md | 45 ++++++++++ docs/tool-catalog.i18n.yaml | 4 +- docs/tool-catalog.md | 6 +- docs/tool-catalog.zh.md | 6 +- .../guide.i18n.yaml | 8 +- .../session-projection-handles/guide.md | 8 +- .../session-projection-handles/guide.zh.md | 8 +- .../api/job-controller/tests/host-harness.ts | 2 + .../job-controller/tests/kill.host.spec.ts | 2 + .../job-controller/tests/observe.host.spec.ts | 3 + .../api/session-controller/README.i18n.yaml | 4 +- packages/api/session-controller/README.md | 2 +- packages/api/session-controller/README.zh.md | 2 +- packages/api/session-controller/src/agent.ts | 18 ++-- .../tests/agent.host.spec.ts | 2 + .../tests/archived-session-gate.host.spec.ts | 3 + .../tests/commands-create-fork.host.spec.ts | 2 + .../commands-queue-attachment.host.spec.ts | 3 + .../tests/commands-upload-file.host.spec.ts | 13 ++- .../tests/controller.host.spec.ts | 4 + .../tests/projection-discovery.host.spec.ts | 2 +- .../tests/session-cold.host.spec.ts | 17 ++++ .../tests/session-fork.host.spec.ts | 2 + .../session-history-journal.host.spec.ts | 2 + .../tests/session-list-blank.host.spec.ts | 2 + .../tests/session-models.host.spec.ts | 2 + .../session-open-workspace-path.host.spec.ts | 2 + .../tests/session-presets.host.spec.ts | 7 +- .../tests/session-projections.host.spec.ts | 6 +- .../tests/session-search.host.spec.ts | 2 +- .../tests/session-skills.host.spec.ts | 2 + .../tests/subagent-catalog.host.spec.ts | 3 + .../context/agent-instructions/package.json | 1 - .../context/agent-instructions/src/index.ts | 20 ++--- .../tests/agent-instructions.spec.ts | 10 +-- .../context/agent-instructions/tsconfig.json | 3 - .../tests/service.spec.ts | 3 +- .../tests/session-reference.spec.ts | 3 +- packages/context/time-context/src/index.ts | 1 + packages/context/tmux-context/src/index.ts | 2 + .../tmux-context/tests/tmux-context.spec.ts | 2 +- packages/core/agent-loop/src/agent.ts | 10 +-- packages/core/agent-loop/src/index.ts | 35 ++++---- .../agent-loop/tests/request-freeze.spec.ts | 9 +- .../tests/resource-lifecycle.spec.ts | 2 +- packages/core/agent/README.i18n.yaml | 8 +- packages/core/agent/README.md | 4 +- packages/core/agent/README.zh.md | 4 +- packages/core/agent/src/index.ts | 24 +++++- packages/core/agent/src/types.ts | 10 +-- .../core/agent/tests/agent-initiator.spec.ts | 2 + packages/core/agent/tests/agent.spec.ts | 40 ++++++++- .../agent/tests/archive-admission.spec.ts | 4 + .../deliverables/tool-present/package.json | 1 - .../deliverables/tool-present/src/index.ts | 12 ++- .../tool-present/tests/present.spec.ts | 6 +- .../deliverables/tool-present/tsconfig.json | 3 - packages/experimental/agent-team/src/index.ts | 2 +- .../browser-use-runtime/tests/mcp.spec.ts | 1 + .../tests/resources.spec.ts | 2 + .../claude-code-mods/README.i18n.yaml | 12 +-- .../experimental/claude-code-mods/README.md | 6 +- .../claude-code-mods/README.zh.md | 6 +- .../claude-code-mods/package.json | 1 - .../claude-code-mods/src/host-ops.ts | 21 ++--- .../claude-code-mods/src/index.ts | 27 +++--- .../claude-code-mods/tests/bridge.spec.ts | 4 +- .../claude-code-mods/tests/define-mod.spec.ts | 4 + .../claude-code-mods/tests/host-ops.spec.ts | 5 +- .../claude-code-mods/tsconfig.json | 3 - .../hooks-claude-code/package.json | 1 - .../hooks-claude-code/src/index.ts | 34 +++----- .../hooks-claude-code/tests/bridge.spec.ts | 4 +- .../hooks-claude-code/tests/coverage-cases.ts | 2 +- .../experimental/hooks-codex/package.json | 1 - .../experimental/hooks-codex/src/index.ts | 83 +++++++------------ .../hooks-codex/tests/bridge.spec.ts | 4 +- .../hooks-codex/tests/coverage-cases.ts | 2 +- .../tests/provider.spec.ts | 6 +- .../tool-session-query/package.json | 1 - .../tool-session-query/src/index.ts | 25 +++--- .../tool-session-query/src/operations.ts | 17 ++-- .../src/workspace-access.ts | 5 +- .../tests/sqlite-integration.spec.ts | 5 +- .../tests/tool-session-query.spec.ts | 16 ++-- .../tool-session-query/tsconfig.json | 3 - .../tool-terminal/tests/tools.spec.ts | 2 + .../experimental/worktree/tests/harness.ts | 2 +- .../extensions/tool-cordis/src/api-catalog.ts | 26 ++++++ .../tests/command-feedback.spec.ts | 2 + .../tests/loader-composition.spec.ts | 3 + packages/fs/tool-fs/tests/tools.spec.ts | 4 +- .../tests/tools.spec.ts | 5 +- packages/goal/goal/tests/goal.spec.ts | 12 +-- packages/goal/goal/tests/projection.spec.ts | 2 +- packages/goal/tool-goal/README.i18n.yaml | 4 +- packages/goal/tool-goal/README.md | 2 +- packages/goal/tool-goal/README.zh.md | 2 +- packages/goal/tool-goal/package.json | 3 +- packages/goal/tool-goal/src/authority.ts | 16 ++-- packages/goal/tool-goal/src/index.ts | 12 ++- .../goal/tool-goal/tests/tool-goal.spec.ts | 6 +- packages/goal/tool-goal/tsconfig.json | 3 - .../permission-presets/src/index.ts | 3 +- .../tool-ask-user/tests/tool-ask-user.spec.ts | 2 + .../tests/user-questions.spec.ts | 13 ++- packages/jobs/jobs-local/tests/jobs.spec.ts | 9 ++ .../jobs/tool-jobs/tests/tool-jobs.spec.ts | 6 ++ .../llm/llm-deepseek/tests/adapter.e2e.ts | 2 + .../tests/loader-composition.spec.ts | 3 + .../llm/llm-deepseek/tests/runtime.e2e.ts | 2 + packages/llm/llm-retry/src/index.ts | 1 - .../tests/inventory.spec.ts | 3 +- .../tests/private-package-composition.spec.ts | 3 + packages/llm/token-meter/README.i18n.yaml | 8 +- packages/llm/token-meter/README.md | 4 +- packages/llm/token-meter/README.zh.md | 4 +- packages/llm/token-meter/src/index.ts | 18 +++- .../tests/token-usage-projection.spec.ts | 6 +- packages/plan/plan-mode/src/index.ts | 13 +-- .../plan/plan-mode/tests/plan-mode.spec.ts | 7 +- .../plan/plan-mode/tests/projection.spec.ts | 2 +- .../agent-preset-registry/README.i18n.yaml | 4 +- .../preset/agent-preset-registry/README.md | 2 +- .../preset/agent-preset-registry/README.zh.md | 2 +- .../preset/agent-preset-registry/src/index.ts | 17 ++-- .../tests/registry.spec.ts | 10 +-- packages/schedule/schedule/tests/harness.ts | 2 + packages/sdk/server/tests/server.spec.ts | 2 + .../session-projection/README.i18n.yaml | 8 +- packages/session/session-projection/README.md | 4 +- .../session/session-projection/README.zh.md | 4 +- packages/session/session-stats/src/index.ts | 3 +- .../tests/loader-composition.spec.ts | 3 + .../tests/provider.spec.ts | 6 +- packages/session/session-title/src/index.ts | 9 +- .../session-title/tests/persistence.spec.ts | 2 + .../session-title/tests/projection.spec.ts | 2 + .../session-title/tests/provider.spec.ts | 16 +++- .../session-title/tests/rename.spec.ts | 7 ++ .../tests/service-contracts.spec.ts | 5 ++ .../session-title/tests/session-title.spec.ts | 4 + packages/session/session-title/tsconfig.json | 1 + .../session/session-turn-outline/src/index.ts | 3 +- .../tool-bash-persistent/tests/tools.spec.ts | 3 + .../shell/tool-bash/tests/background.spec.ts | 3 + packages/shell/tool-bash/tests/tools.spec.ts | 11 ++- .../tool-pwsh-persistent/tests/tools.spec.ts | 3 + .../shell/tool-pwsh/tests/background.spec.ts | 4 + packages/shell/tool-pwsh/tests/tools.spec.ts | 9 +- .../skill/tool-skill/tests/tool-skill.spec.ts | 4 + .../tests/tool-workspace-dependencies.spec.ts | 2 + .../tests/external-activation-helpers.ts | 2 + packages/subagent/tool-subagent/src/index.ts | 3 +- .../terminal-bash/tests/index.spec.ts | 8 +- .../terminal-bash/tests/local.spec.ts | 2 +- .../terminal/terminal/tests/service.spec.ts | 2 + packages/todo/tool-todo/src/index.ts | 1 - .../todo/tool-todo/tests/projection.spec.ts | 2 +- .../tests/deepseek.spec.ts | 2 + .../tool-workflow/tests/tool-workflow.spec.ts | 3 +- .../workflow/workflow-ptc/tests/host.spec.ts | 2 +- packages/workflow/workflow-ptc/tests/setup.ts | 2 +- scripts/gen-config-catalog.ts | 2 +- scripts/gen-cordis-catalog.ts | 2 + scripts/gen-tool-catalog.ts | 9 +- scripts/type-equiv.manifest.json | 10 +++ 191 files changed, 891 insertions(+), 558 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.i18n.yaml index f391fe518e..30244c0085 100644 --- a/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.i18n.yaml @@ -9,8 +9,8 @@ en: fca00eeb7a8d9abd zh: d7a9766f5322dff9 /agent-note-session-projections-as-a-required-reader-seam/decision: - en: dd550c8f4c2aa263 - zh: 9e60b0e465697e77 + en: 8d38ed9356085c7e + zh: e15ede51415192f3 /agent-note-session-projections-as-a-required-reader-seam/alternatives-considered: en: aea3da939c90d240 zh: 21e91c481cdd23b7 diff --git a/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.md b/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.md index 5ab32c0b2c..091e8b277a 100644 --- a/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.md +++ b/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.md @@ -12,9 +12,9 @@ An optional projection registry lets a plugin whose host behavior reads projecte This decision builds on the split between host projection state and client views in [Session projection state and client views](../../archived/architecture/2026-08-19-session-projection-state-and-client-views.md). -Every host reader treats the projection registry and its required key as mandatory state. Plugins declare `sessionProjections` as a required injection and await acquisition before exposing readers. Official compositions mount the registry before those plugins. `ApiProxyService` follows the required-injection form; the lower-level `createApiProxy` factory remains tolerant for isolated tests and diagnostics. +Every host state owner treats the projection registry and its required key as mandatory state. Owners declare `sessionProjections` as a required injection and await acquisition before exposing service methods; other consumers depend on those services. Official compositions mount the registry before those plugins. `ApiProxyService` follows the required-injection form; the lower-level `createApiProxy` factory remains tolerant for isolated tests and diagnostics. -Readers acquire state through [awaited registration and retained handles](2026-10-12-session-projection-registration-handles.md). That decision supersedes optional first-access acquisition and returning a bare disposer. It preserves the required-state rule here: a missing registry cannot silently produce default host state. Client views still contain only consumed fields; typed handle reads provide richer host state. +Owners acquire state through [awaited registration and retained handles](2026-10-12-session-projection-registration-handles.md). That decision supersedes optional first-access acquisition and returning a bare disposer. It preserves the required-state rule here: a missing registry cannot silently produce default host state. Client views still contain only consumed fields; business service methods expose the host data each consumer needs. `onChanged` publishes client-visible value changes only. Registration changes do not create a second Host event stream or client tombstone protocol. A later authoritative history or list baseline reflects the active key set. diff --git a/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.zh.md b/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.zh.md index 0ab43fd5c0..c30f7bd35d 100644 --- a/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-19-session-projection-mandatory-seam.zh.md @@ -12,9 +12,9 @@ Status: implemented 本决策建立在[会话投影的 host 状态与客户端视图](../../archived/architecture/2026-08-19-session-projection-state-and-client-views.md)所定义的拆分之上。 -每个 host 读取方都把投影注册表及其所需 key 视为必需状态。插件将 `sessionProjections` 声明为必需注入,并在暴露读取方前等待获取完成。正式组合在这些插件之前挂载注册表。`ApiProxyService` 采用必需注入形式;较低层的 `createApiProxy` factory 对隔离测试和诊断保持容错。 +每个 host 状态所有者都把投影注册表及其所需 key 视为必需状态。所有者将 `sessionProjections` 声明为必需注入,并在提供服务方法前等待获取完成;其他消费方依赖这些服务。正式组合在这些插件之前挂载注册表。`ApiProxyService` 采用必需注入形式;较低层的 `createApiProxy` factory 对隔离测试和诊断保持容错。 -读取方通过[等待注册与保留句柄](2026-10-12-session-projection-registration-handles.zh.md)获取状态。该决策取代可选首次访问获取和返回裸释放函数的方式,保留这里的必需状态规则:缺少注册表不能静默产生默认 host 状态。客户端视图仍只包含消费字段;类型化句柄读取提供更丰富的 host 状态。 +所有者通过[等待注册与保留句柄](2026-10-12-session-projection-registration-handles.zh.md)获取状态。该决策取代可选首次访问获取和返回裸释放函数的方式,保留这里的必需状态规则:缺少注册表不能静默产生默认 host 状态。客户端视图仍只包含消费字段;业务服务方法提供各消费方所需的 host 数据。 `onChanged` 只发布客户端可见值的变化。注册变化不会创建第二条 Host 事件流或客户端 tombstone 协议。后续权威 history 或 list 基线会反映活跃 key 集。 diff --git a/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.i18n.yaml index c22458657b..dd6e68b389 100644 --- a/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.i18n.yaml @@ -9,11 +9,11 @@ en: 47ba548b869be190 zh: c2e5038a56fe741c /agent-note-asynchronous-projection-registration-and-retained-readers/decision: - en: 19064b5aecb26f57 - zh: 075bb87c6a5299ae + en: 9e56d4bcbdc3a291 + zh: bebaaab338a15e40 /agent-note-asynchronous-projection-registration-and-retained-readers/alternatives-considered: en: ee5b0cd715ed1fef zh: 6b1478dcde21916d /agent-note-asynchronous-projection-registration-and-retained-readers/consequences: - en: 4b5e4bb446872545 - zh: 9fb2cc8ae77fb823 + en: 6f0e04ef7924e2c6 + zh: e3730273e323440d diff --git a/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.md b/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.md index ea16e10828..90598d8f9f 100644 --- a/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.md +++ b/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.md @@ -10,11 +10,11 @@ Host readers coupled to registry key lookup lose access when a plugin unloads, a ## Decision -`SessionProjectionRegistry.register` returns a promise for a typed handle. Consumers await it during activation before exposing dependent operations. Function plugins retain their namespace exports and use asynchronous `apply`; services use their initialization hook. Definitions remain with their domain owner, and each reader registers the required definition instead of looking up a possibly absent key. This supersedes the optional first-access acquisition and disposer-return details in [the mandatory-reader decision](2026-08-19-session-projection-mandatory-seam.md), while retaining its mandatory-state and cropped-client-view rules. +`SessionProjectionRegistry.register` returns a promise for a typed handle. Domain owners await it during activation before exposing dependent operations. Function plugins retain their namespace exports and use asynchronous `apply`; services use their initialization hook. Each domain service keeps its definitions and handles internal and exposes methods returning business data. Consumers depend on that service rather than registering its definitions or receiving projection readers. This supersedes the optional first-access acquisition and disposer-return details in [the mandatory-reader decision](2026-08-19-session-projection-mandatory-seam.md), while retaining its mandatory-state and cropped-client-view rules. A handle owns one reference to a shared definition and its per-Session cells. Its disposer releases global participation exactly once; the final release removes the unit from live drives, snapshots, and checkpoints. Its reader remains usable after release, folding unseen events with the original definition. A later registration at a different state version does not replace an older handle's definition. Registration checks owner liveness after its asynchronous handoff and rejects if the owner unloaded before receiving the handle. -AgentLoop acquires inbox and turn-boundary handles once before publishing its factory. Each Agent receives those readers during synchronous construction. Cleanup can use them after the loop fiber removes global registrations, so projection release need not wait for every Agent teardown. This preserves the existing creation/setup sequence without per-Agent registration or an extra initialization state. +AgentRegistry owns turn and step state and exposes `turnBoundaryOf(session)`. AgentLoop acquires its inbox handle once before publishing its factory, reads the last turn before creating an Agent scope, and passes an inbox callback plus that turn number into synchronous Agent construction. The callback retains its handle through cleanup after the loop fiber removes global registrations. This preserves the existing creation/setup sequence without per-Agent registration or an extra initialization state. ## Alternatives considered @@ -28,4 +28,4 @@ AgentLoop acquires inbox and turn-boundary handles once before publishing its fa ## Consequences -Consumers must migrate acquisition and retain handles. Released handles retain their definitions and weakly keyed cells for as long as the caller keeps them; they do not restore global wire participation. Registration-reader tests cover post-release folding, version replacement, unrelated-unit isolation, repeated disposal, concurrent owner disposal, and owner unload during acquisition. Agent lifecycle tests cover cleanup failures, scope-minting races, and background child shutdown. +Projection owners must migrate acquisition and retain handles; other consumers use the owning services. Released handles retain their definitions and weakly keyed cells for as long as the caller keeps them; they do not restore global wire participation. Registration-reader tests cover post-release folding, version replacement, unrelated-unit isolation, repeated disposal, concurrent owner disposal, and owner unload during acquisition. Agent lifecycle tests cover cleanup failures, scope-minting races, and background child shutdown. diff --git a/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.zh.md b/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.zh.md index 28974a237b..1043a77972 100644 --- a/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.zh.md +++ b/.agents/notes/implemented/architecture/2026-10-12-session-projection-registration-handles.zh.md @@ -10,11 +10,11 @@ Status: implemented ## Decision -`SessionProjectionRegistry.register` 返回类型化句柄的 Promise。消费者在激活期间等待它,再暴露依赖操作。函数插件保留命名空间导出并使用异步 `apply`;服务使用初始化钩子。定义仍归领域拥有方,每个读取方注册所需定义,不再查询可能不存在的键。这取代了[必需读取方决策](2026-08-19-session-projection-mandatory-seam.zh.md)中的可选首次访问获取方式与返回释放函数的细节,同时保留必需状态和裁剪 Client 视图的规则。 +`SessionProjectionRegistry.register` 返回类型化句柄的 Promise。领域所有者在激活期间等待它,再提供依赖操作。函数插件保留命名空间导出并使用异步 `apply`;服务使用初始化钩子。领域服务在内部持有定义和句柄,通过方法返回业务数据。消费方依赖该服务,无需注册其定义或接收投影读取句柄。这取代了[必需读取方决策](2026-08-19-session-projection-mandatory-seam.zh.md)中的可选首次访问获取方式与返回释放函数的细节,同时保留必需状态和裁剪 Client 视图的规则。 句柄拥有共享定义及其逐 Session 单元的一次引用。其释放函数恰好一次地解除全局参与;最后一次释放会从实时推进、快照和检查点中移除单元。读取句柄在释放后仍可使用原定义折叠未读取事件。后续注册的其他状态版本不会替换旧句柄的定义。注册在异步交接后检查 owner 是否存活;如果 owner 在收到句柄前卸载,注册拒绝。 -AgentLoop 在发布工厂前只获取一次 inbox 与轮次边界句柄。每个 Agent 在同步构造时收到这些读取句柄。loop fiber 移除全局注册后,清理过程仍可使用它们,因此投影释放无需等待每个 Agent teardown。这保留既有创建与 setup 顺序,无需逐 Agent 注册或增加初始化状态。 +AgentRegistry 持有轮次和步骤状态,并提供 `turnBoundaryOf(session)`。AgentLoop 在发布工厂前只获取一次 inbox 句柄,在创建 Agent 作用域前读取最后轮次,并将 inbox 回调与该轮次编号传入同步 Agent 构造过程。loop fiber 移除全局注册后,回调仍在清理期间保留其句柄。这保留既有创建与 setup 顺序,无需逐 Agent 注册或增加初始化状态。 ## Alternatives considered @@ -28,4 +28,4 @@ AgentLoop 在发布工厂前只获取一次 inbox 与轮次边界句柄。每个 ## Consequences -消费者必须迁移获取方式并保留句柄。只要调用方持有已释放句柄,其定义和弱键单元就仍存在;句柄不会恢复全局 wire 参与。注册读取测试覆盖释放后折叠、版本替换、无关单元隔离、重复释放、并发 owner 释放以及获取期间 owner 卸载。Agent 生命周期测试覆盖清理失败、作用域创建竞态与后台子 Agent 停止。 +投影所有者必须迁移获取方式并保留句柄;其他消费方使用所属服务。只要调用方持有已释放句柄,其定义和弱键单元就仍存在;句柄不会恢复全局 wire 参与。注册读取测试覆盖释放后折叠、版本替换、无关单元隔离、重复释放、并发 owner 释放以及获取期间 owner 卸载。Agent 生命周期测试覆盖清理失败、作用域创建竞态与后台子 Agent 停止。 diff --git a/benchmarks/session-corpus/projection-list.worker.ts b/benchmarks/session-corpus/projection-list.worker.ts index 48a751aaa4..3dbe0a2d3f 100644 --- a/benchmarks/session-corpus/projection-list.worker.ts +++ b/benchmarks/session-corpus/projection-list.worker.ts @@ -171,8 +171,8 @@ async function seed(root: string, workload: typeof WORKLOADS[keyof typeof WORKLO async function mountHost(ctx: Context, root: string, workload: typeof WORKLOADS[keyof typeof WORKLOADS], observation: ViewObservation, listWorkSliceMs?: number): Promise { await ctx.plugin(SessionStore) - await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.sessionProjections.register({ key: KEY, stateVersion: 1, stateSchema, init: header => makeState(Number(header.id.slice('projection-bench-'.length)), workload.baseRows), diff --git a/benchmarks/session-open/session-open.worker.ts b/benchmarks/session-open/session-open.worker.ts index 341053de6f..00d5f17d33 100644 --- a/benchmarks/session-open/session-open.worker.ts +++ b/benchmarks/session-open/session-open.worker.ts @@ -3,7 +3,7 @@ import { performance } from 'node:perf_hooks' import { scheduler } from 'node:timers/promises' import { Context } from '@deepseek-ai/cordis' -import AgentLoop, { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent-loop' +import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' import { agentPresetProjectionDefinition } from '@deepseek-ai/dsh-agent-preset-registry' import SessionStore, { @@ -12,7 +12,7 @@ import SessionStore, { SessionLogOffset, SessionPreparation, } from '@deepseek-ai/dsh-session' -import type { AgentHandle } from '@deepseek-ai/dsh-agent' +import AgentRegistry, { type AgentHandle } from '@deepseek-ai/dsh-agent' import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import SessionQueryEngine from '@deepseek-ai/dsh-session-query' @@ -128,9 +128,8 @@ function memoryDelta( } } -async function installProjectionSet(ctx: Context, agentLoopOwnsBoundary: boolean): Promise { +async function installProjectionSet(ctx: Context): Promise { // Mirrors projection owners mounted by the base and web-app bundles without timing profile boot. - if (!agentLoopOwnsBoundary) await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) await ctx.sessionProjections.register(agentPresetProjectionDefinition) await ctx.sessionProjections.register(modelSelectionProjection) await ctx.plugin(SessionTitleService, { @@ -163,9 +162,10 @@ class SessionBenchmarkHost { if (agentScenario) await mountAgentLoopTestDependencies(ctx) else { await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionStore) } - await installProjectionSet(ctx, agentScenario) + await installProjectionSet(ctx) await ctx.plugin(JsonlSessionPersistence, { root, compression: 'zstd' }) let history: SessionHistoryController | undefined if (scenario === 'first-history') { diff --git a/docs/architecture.i18n.yaml b/docs/architecture.i18n.yaml index 5b82618b52..13b0d8b44f 100644 --- a/docs/architecture.i18n.yaml +++ b/docs/architecture.i18n.yaml @@ -27,8 +27,8 @@ en: e6402b28c0ed15fa zh: 6c07be9047b42056 /deepseek-harness-architecture/session-log: - en: 35fcd01052084a07 - zh: e16743dac147cb6e + en: f1729d805c20df36 + zh: 6b71b6039663e2e7 /deepseek-harness-architecture/capability-seams: en: a5bc20c3f2a1b370 zh: 91773a281410b2f4 diff --git a/docs/architecture.md b/docs/architecture.md index b821b9fa92..2e33a1102b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -123,7 +123,7 @@ Session consumers know only the current logical format. Header-only `stat` and ` **Model-visible means logged.** Every model request must be reconstructable from the log. New model-visible inputs require session events. Plugins changing existing message content register [pure message projections](subsystems/session.md#plugin-owned-message-projections); detached readers supply the same definitions explicitly. Tool changes are capability-independent; [Session tool history](../packages/core/session/README.md) supplies provider declarations. -**Projection seam.** `dsh-session-projection` owns `ctx.sessionProjections`: registered units fold committed events incrementally, and carriers batch cropped client views with `snapshot()`. Host readers require the registry during activation, await registration, and retain a handle for synchronous state reads. Releasing registration removes global participation; retained readers continue folding Session events. The shared `turnBoundary` definition belongs to `dsh-agent` ([registration lifecycle](../packages/session/session-projection/README.md)). +**Projection seam.** `dsh-session-projection` owns `ctx.sessionProjections`: registered units fold committed events incrementally, and carriers batch cropped client views with `snapshot()`. Domain services await registration during activation, retain private handles, and expose business methods for other plugins. Releasing registration removes global participation; retained handles continue folding Session events. `AgentRegistry` owns turn and step state and exposes `turnBoundaryOf(session)` ([registration lifecycle](../packages/session/session-projection/README.md)). [Working directories](subsystems/working-directory.md) supply user context and execution paths without changing original project identity, sandbox write roots, or existing process directories. diff --git a/docs/architecture.zh.md b/docs/architecture.zh.md index 403c4f3063..274ff87ae6 100644 --- a/docs/architecture.zh.md +++ b/docs/architecture.zh.md @@ -127,7 +127,7 @@ Session 消费方只了解当前逻辑格式。仅 header 的 `stat` 与 `list` **模型可见即已记录。** 每个模型请求都必须能从日志重建。新增模型可见输入需要会话事件。修改现有消息内容的插件注册[纯消息投影](subsystems/session.zh.md#plugin-owned-message-projections),独立读取器显式传入相同的处理器。 工具变更不依赖能力;[Session 工具历史](../packages/core/session/README.zh.md)提供提供方声明。 -**投影 seam。** `dsh-session-projection` 提供 `ctx.sessionProjections`:已注册单元增量折叠已提交事件,载体通过 `snapshot()` 批量取得裁剪后的客户端视图。host 读取方在激活时要求注册表,等待注册完成,并保留句柄以同步读取状态。释放注册会移除全局参与关系;已有读取方仍可继续折叠 Session 事件。共享的 `turnBoundary` 定义归 `dsh-agent` 所有([注册生命周期](../packages/session/session-projection/README.zh.md))。 +**投影 seam。** `dsh-session-projection` 提供 `ctx.sessionProjections`:已注册单元增量折叠已提交事件,载体通过 `snapshot()` 批量取得裁剪后的客户端视图。领域服务在激活时等待注册完成,在内部保留句柄,并向其他插件提供业务方法。释放注册会移除全局参与关系;已有句柄仍可继续折叠 Session 事件。`AgentRegistry` 持有轮次和步骤状态,并提供 `turnBoundaryOf(session)`([注册生命周期](../packages/session/session-projection/README.zh.md))。 [工作目录](subsystems/working-directory.zh.md) 提供用户上下文和执行路径,不改变原始项目标识、沙箱写入根目录或已有进程目录。 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 2b9044a6e1..7a43647d96 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -59,7 +59,7 @@ export interface Config { ## `@deepseek-ai/dsh-agent-instructions` -- `inject`: `sessionProjections` · `workingDirectory` +- `inject`: `agents` · `workingDirectory` - `source`: [`packages/context/agent-instructions/src/config.ts:18`](../packages/context/agent-instructions/src/config.ts) ```ts config-catalog @@ -92,7 +92,7 @@ export interface Config { - `inject`: `agents` · `sessions` · `llm` · `tools` · `systemPrompt` · `sessionProjections` - `refs`: [`AgentOptions`](subsystems/core.md) · [`SessionId`](subsystems/core.md) · `Volatile` (`@deepseek-ai/cosmokit`) -- `source`: [`packages/core/agent-loop/src/index.ts:254`](../packages/core/agent-loop/src/index.ts) +- `source`: [`packages/core/agent-loop/src/index.ts:250`](../packages/core/agent-loop/src/index.ts) ```ts config-catalog /** Agent-loop plugin configuration. */ @@ -137,7 +137,7 @@ export type Config = PresetDefinition ## `@deepseek-ai/dsh-agent-preset-registry` -- `inject`: `loader` · `sessionProjections` +- `inject`: `loader` · `sessionProjections` · `agents` - `refs`: `Volatile` (`@deepseek-ai/cordis`) - `source`: [`packages/preset/agent-preset-registry/src/preset.ts:13`](../packages/preset/agent-preset-registry/src/preset.ts) @@ -950,7 +950,7 @@ export interface StagehandModelConfig { ## `@deepseek-ai/dsh-experimental-claude-code-mods` -- `inject`: `workingDirectory` · `sessionProjections` +- `inject`: `workingDirectory` · `agents` - `source`: [`packages/experimental/claude-code-mods/src/index.ts:53`](../packages/experimental/claude-code-mods/src/index.ts) ```ts config-catalog @@ -1459,8 +1459,8 @@ export interface HmrConfig extends ChokidarOptions { ## `@deepseek-ai/dsh-hooks-claude-code` -- `inject`: `shell` · `sessionProjections` · `workingDirectory` -- `source`: [`packages/experimental/hooks-claude-code/src/index.ts:53`](../packages/experimental/hooks-claude-code/src/index.ts) +- `inject`: `shell` · `agents` · `workingDirectory` +- `source`: [`packages/experimental/hooks-claude-code/src/index.ts:50`](../packages/experimental/hooks-claude-code/src/index.ts) ```ts config-catalog /** Plugin config: where the CC hook config lives + substitution roots. */ @@ -1498,8 +1498,8 @@ export interface Config { ## `@deepseek-ai/dsh-hooks-codex` -- `inject`: `shell` · `sessionProjections` · `workingDirectory` -- `source`: [`packages/experimental/hooks-codex/src/index.ts:53`](../packages/experimental/hooks-codex/src/index.ts) +- `inject`: `shell` · `agents` · `workingDirectory` +- `source`: [`packages/experimental/hooks-codex/src/index.ts:49`](../packages/experimental/hooks-codex/src/index.ts) ```ts config-catalog /** Plugin config: where the Codex hooks.json lives + the model name for payloads. */ @@ -2113,7 +2113,7 @@ export interface ReplayModelConfig { ## `@deepseek-ai/dsh-llm-retry` - `inject`: `agents` · `sessionProjections` -- `source`: [`packages/llm/llm-retry/src/index.ts:26`](../packages/llm/llm-retry/src/index.ts) +- `source`: [`packages/llm/llm-retry/src/index.ts:25`](../packages/llm/llm-retry/src/index.ts) ```ts config-catalog /** This policy executor has no config; providers own `retryPolicy`. */ @@ -2386,8 +2386,8 @@ export interface Config { ## `@deepseek-ai/dsh-plan-mode` -- `inject`: `tools` · `systemPrompt` · `sessionProjections` -- `source`: [`packages/plan/plan-mode/src/index.ts:70`](../packages/plan/plan-mode/src/index.ts) +- `inject`: `tools` · `systemPrompt` · `sessionProjections` · `agents` +- `source`: [`packages/plan/plan-mode/src/index.ts:69`](../packages/plan/plan-mode/src/index.ts) ```ts config-catalog /** Deployment-owned plan guidance. */ @@ -2945,7 +2945,7 @@ export enum SessionTelemetryMode { ## `@deepseek-ai/dsh-session-title` -- `inject`: `sessions` · `sessionProjections` +- `inject`: `sessions` · `sessionProjections` · `agents` - `source`: [`packages/session/session-title/src/index.ts:56`](../packages/session/session-title/src/index.ts) ```ts config-catalog @@ -3593,7 +3593,7 @@ export type ShellDialect = 'bash' | 'pwsh' ## `@deepseek-ai/dsh-time-context` - `inject`: `agents` · `sessionProjections` -- `source`: [`packages/context/time-context/src/index.ts:55`](../packages/context/time-context/src/index.ts) +- `source`: [`packages/context/time-context/src/index.ts:56`](../packages/context/time-context/src/index.ts) ```ts config-catalog /** Request-preparation clock formatting and append scheduling. Invalid values fail plugin load. */ @@ -3612,7 +3612,7 @@ export interface Config { ## `@deepseek-ai/dsh-tmux-context` - `inject`: `agents` · `sessionProjections` -- `source`: [`packages/context/tmux-context/src/index.ts:45`](../packages/context/tmux-context/src/index.ts) +- `source`: [`packages/context/tmux-context/src/index.ts:47`](../packages/context/tmux-context/src/index.ts) ```ts config-catalog /** Per-turn tmux-location scheduling. Invalid values fail plugin load. */ @@ -3781,8 +3781,8 @@ export interface Config { ## `@deepseek-ai/dsh-tool-goal` -- `inject`: `agents` · `goals` · `tools` · `systemPrompt` · `sessionProjections` -- `source`: [`packages/goal/tool-goal/src/index.ts:33`](../packages/goal/tool-goal/src/index.ts) +- `inject`: `agents` · `goals` · `tools` · `systemPrompt` +- `source`: [`packages/goal/tool-goal/src/index.ts:32`](../packages/goal/tool-goal/src/index.ts) ```ts config-catalog /** Model policy and hard lower bounds for goal-state updates. */ @@ -3856,8 +3856,8 @@ export interface Config { ## `@deepseek-ai/dsh-tool-present` -- `inject`: `tools` · `fs` · `sessionProjections` · `workingDirectory` -- `source`: [`packages/deliverables/tool-present/src/index.ts:16`](../packages/deliverables/tool-present/src/index.ts) +- `inject`: `tools` · `fs` · `agents` · `workingDirectory` +- `source`: [`packages/deliverables/tool-present/src/index.ts:15`](../packages/deliverables/tool-present/src/index.ts) ```ts config-catalog /** Per-call delivery limit. */ @@ -3949,8 +3949,8 @@ export interface Config { ## `@deepseek-ai/dsh-tool-session-query` -- `inject`: `tools` · `systemPrompt` · `sessionQuery` · `sessionProjections` -- `source`: [`packages/experimental/tool-session-query/src/index.ts:27`](../packages/experimental/tool-session-query/src/index.ts) +- `inject`: `tools` · `systemPrompt` · `sessionQuery` · `agents` +- `source`: [`packages/experimental/tool-session-query/src/index.ts:28`](../packages/experimental/tool-session-query/src/index.ts) ```ts config-catalog /** Deployment-owned search count and timeout bounds. */ @@ -4006,7 +4006,7 @@ export interface Config { - `inject`: `tools` · `subagents` · `systemPrompt` · `sessionProjections` - `refs`: [`AgentOptions`](subsystems/core.md) -- `source`: [`packages/subagent/tool-subagent/src/index.ts:51`](../packages/subagent/tool-subagent/src/index.ts) +- `source`: [`packages/subagent/tool-subagent/src/index.ts:50`](../packages/subagent/tool-subagent/src/index.ts) ```ts config-catalog /** Config: which registered provider this tool delegates to, plus child defaults. */ @@ -4075,7 +4075,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-todo` - `inject`: `tools` · `sessionProjections` -- `source`: [`packages/todo/tool-todo/src/index.ts:30`](../packages/todo/tool-todo/src/index.ts) +- `source`: [`packages/todo/tool-todo/src/index.ts:29`](../packages/todo/tool-todo/src/index.ts) ```ts config-catalog /** Model-facing todo tool configuration. */ @@ -4519,7 +4519,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co | `package` | `inject` | `source` | | --- | --- | --- | | `@deepseek-ai/dsh-acp-app` | `cmdlineArgs` | [`packages/bundle/acp-app/src/index.ts`](../packages/bundle/acp-app/src/index.ts) | -| `@deepseek-ai/dsh-agent` | — | [`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts) | +| `@deepseek-ai/dsh-agent` | `sessionProjections` | [`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts) | | `@deepseek-ai/dsh-api-account-controller` | `deepseekAccount` · `agents` | [`packages/api/account-controller/src/index.ts`](../packages/api/account-controller/src/index.ts) | | `@deepseek-ai/dsh-api-remote-access` | — | [`packages/api/remote-access/src/index.ts`](../packages/api/remote-access/src/index.ts) | | `@deepseek-ai/dsh-api-remotes` | `typertGateway` | [`packages/api/remotes/src/index.ts`](../packages/api/remotes/src/index.ts) | diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 59e65918f3..2e40740617 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -61,7 +61,7 @@ export interface Config { ## `@deepseek-ai/dsh-agent-instructions` -- `inject`: `sessionProjections` · `workingDirectory` +- `inject`: `agents` · `workingDirectory` - `source`: [`packages/context/agent-instructions/src/config.ts:18`](../packages/context/agent-instructions/src/config.ts) ```ts config-catalog @@ -94,7 +94,7 @@ export interface Config { - `inject`: `agents` · `sessions` · `llm` · `tools` · `systemPrompt` · `sessionProjections` - `refs`: [`AgentOptions`](subsystems/core.zh.md) · [`SessionId`](subsystems/core.zh.md) · `Volatile` (`@deepseek-ai/cosmokit`) -- `source`: [`packages/core/agent-loop/src/index.ts:254`](../packages/core/agent-loop/src/index.ts) +- `source`: [`packages/core/agent-loop/src/index.ts:250`](../packages/core/agent-loop/src/index.ts) ```ts config-catalog /** Agent-loop plugin configuration. */ @@ -139,7 +139,7 @@ export type Config = PresetDefinition ## `@deepseek-ai/dsh-agent-preset-registry` -- `inject`: `loader` · `sessionProjections` +- `inject`: `loader` · `sessionProjections` · `agents` - `refs`: `Volatile` (`@deepseek-ai/cordis`) - `source`: [`packages/preset/agent-preset-registry/src/preset.ts:13`](../packages/preset/agent-preset-registry/src/preset.ts) @@ -952,7 +952,7 @@ export interface StagehandModelConfig { ## `@deepseek-ai/dsh-experimental-claude-code-mods` -- `inject`: `workingDirectory` · `sessionProjections` +- `inject`: `workingDirectory` · `agents` - `source`: [`packages/experimental/claude-code-mods/src/index.ts:53`](../packages/experimental/claude-code-mods/src/index.ts) ```ts config-catalog @@ -1461,8 +1461,8 @@ export interface HmrConfig extends ChokidarOptions { ## `@deepseek-ai/dsh-hooks-claude-code` -- `inject`: `shell` · `sessionProjections` · `workingDirectory` -- `source`: [`packages/experimental/hooks-claude-code/src/index.ts:53`](../packages/experimental/hooks-claude-code/src/index.ts) +- `inject`: `shell` · `agents` · `workingDirectory` +- `source`: [`packages/experimental/hooks-claude-code/src/index.ts:50`](../packages/experimental/hooks-claude-code/src/index.ts) ```ts config-catalog /** Plugin config: where the CC hook config lives + substitution roots. */ @@ -1500,8 +1500,8 @@ export interface Config { ## `@deepseek-ai/dsh-hooks-codex` -- `inject`: `shell` · `sessionProjections` · `workingDirectory` -- `source`: [`packages/experimental/hooks-codex/src/index.ts:53`](../packages/experimental/hooks-codex/src/index.ts) +- `inject`: `shell` · `agents` · `workingDirectory` +- `source`: [`packages/experimental/hooks-codex/src/index.ts:49`](../packages/experimental/hooks-codex/src/index.ts) ```ts config-catalog /** Plugin config: where the Codex hooks.json lives + the model name for payloads. */ @@ -2115,7 +2115,7 @@ export interface ReplayModelConfig { ## `@deepseek-ai/dsh-llm-retry` - `inject`: `agents` · `sessionProjections` -- `source`: [`packages/llm/llm-retry/src/index.ts:26`](../packages/llm/llm-retry/src/index.ts) +- `source`: [`packages/llm/llm-retry/src/index.ts:25`](../packages/llm/llm-retry/src/index.ts) ```ts config-catalog /** This policy executor has no config; providers own `retryPolicy`. */ @@ -2388,8 +2388,8 @@ export interface Config { ## `@deepseek-ai/dsh-plan-mode` -- `inject`: `tools` · `systemPrompt` · `sessionProjections` -- `source`: [`packages/plan/plan-mode/src/index.ts:70`](../packages/plan/plan-mode/src/index.ts) +- `inject`: `tools` · `systemPrompt` · `sessionProjections` · `agents` +- `source`: [`packages/plan/plan-mode/src/index.ts:69`](../packages/plan/plan-mode/src/index.ts) ```ts config-catalog /** Deployment-owned plan guidance. */ @@ -2947,7 +2947,7 @@ export enum SessionTelemetryMode { ## `@deepseek-ai/dsh-session-title` -- `inject`: `sessions` · `sessionProjections` +- `inject`: `sessions` · `sessionProjections` · `agents` - `source`: [`packages/session/session-title/src/index.ts:56`](../packages/session/session-title/src/index.ts) ```ts config-catalog @@ -3595,7 +3595,7 @@ export type ShellDialect = 'bash' | 'pwsh' ## `@deepseek-ai/dsh-time-context` - `inject`: `agents` · `sessionProjections` -- `source`: [`packages/context/time-context/src/index.ts:55`](../packages/context/time-context/src/index.ts) +- `source`: [`packages/context/time-context/src/index.ts:56`](../packages/context/time-context/src/index.ts) ```ts config-catalog /** Request-preparation clock formatting and append scheduling. Invalid values fail plugin load. */ @@ -3614,7 +3614,7 @@ export interface Config { ## `@deepseek-ai/dsh-tmux-context` - `inject`: `agents` · `sessionProjections` -- `source`: [`packages/context/tmux-context/src/index.ts:45`](../packages/context/tmux-context/src/index.ts) +- `source`: [`packages/context/tmux-context/src/index.ts:47`](../packages/context/tmux-context/src/index.ts) ```ts config-catalog /** Per-turn tmux-location scheduling. Invalid values fail plugin load. */ @@ -3783,8 +3783,8 @@ export interface Config { ## `@deepseek-ai/dsh-tool-goal` -- `inject`: `agents` · `goals` · `tools` · `systemPrompt` · `sessionProjections` -- `source`: [`packages/goal/tool-goal/src/index.ts:33`](../packages/goal/tool-goal/src/index.ts) +- `inject`: `agents` · `goals` · `tools` · `systemPrompt` +- `source`: [`packages/goal/tool-goal/src/index.ts:32`](../packages/goal/tool-goal/src/index.ts) ```ts config-catalog /** Model policy and hard lower bounds for goal-state updates. */ @@ -3858,8 +3858,8 @@ export interface Config { ## `@deepseek-ai/dsh-tool-present` -- `inject`: `tools` · `fs` · `sessionProjections` · `workingDirectory` -- `source`: [`packages/deliverables/tool-present/src/index.ts:16`](../packages/deliverables/tool-present/src/index.ts) +- `inject`: `tools` · `fs` · `agents` · `workingDirectory` +- `source`: [`packages/deliverables/tool-present/src/index.ts:15`](../packages/deliverables/tool-present/src/index.ts) ```ts config-catalog /** Per-call delivery limit. */ @@ -3951,8 +3951,8 @@ export interface Config { ## `@deepseek-ai/dsh-tool-session-query` -- `inject`: `tools` · `systemPrompt` · `sessionQuery` · `sessionProjections` -- `source`: [`packages/experimental/tool-session-query/src/index.ts:27`](../packages/experimental/tool-session-query/src/index.ts) +- `inject`: `tools` · `systemPrompt` · `sessionQuery` · `agents` +- `source`: [`packages/experimental/tool-session-query/src/index.ts:28`](../packages/experimental/tool-session-query/src/index.ts) ```ts config-catalog /** Deployment-owned search count and timeout bounds. */ @@ -4008,7 +4008,7 @@ export interface Config { - `inject`: `tools` · `subagents` · `systemPrompt` · `sessionProjections` - `refs`: [`AgentOptions`](subsystems/core.zh.md) -- `source`: [`packages/subagent/tool-subagent/src/index.ts:51`](../packages/subagent/tool-subagent/src/index.ts) +- `source`: [`packages/subagent/tool-subagent/src/index.ts:50`](../packages/subagent/tool-subagent/src/index.ts) ```ts config-catalog /** Config: which registered provider this tool delegates to, plus child defaults. */ @@ -4077,7 +4077,7 @@ export interface Config { ## `@deepseek-ai/dsh-tool-todo` - `inject`: `tools` · `sessionProjections` -- `source`: [`packages/todo/tool-todo/src/index.ts:30`](../packages/todo/tool-todo/src/index.ts) +- `source`: [`packages/todo/tool-todo/src/index.ts:29`](../packages/todo/tool-todo/src/index.ts) ```ts config-catalog /** Model-facing todo tool configuration. */ @@ -4521,7 +4521,7 @@ export interface Config { | `package` | `inject` | `source` | | --- | --- | --- | | `@deepseek-ai/dsh-acp-app` | `cmdlineArgs` | [`packages/bundle/acp-app/src/index.ts`](../packages/bundle/acp-app/src/index.ts) | -| `@deepseek-ai/dsh-agent` | — | [`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts) | +| `@deepseek-ai/dsh-agent` | `sessionProjections` | [`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts) | | `@deepseek-ai/dsh-api-account-controller` | `deepseekAccount` · `agents` | [`packages/api/account-controller/src/index.ts`](../packages/api/account-controller/src/index.ts) | | `@deepseek-ai/dsh-api-remote-access` | — | [`packages/api/remote-access/src/index.ts`](../packages/api/remote-access/src/index.ts) | | `@deepseek-ai/dsh-api-remotes` | `typertGateway` | [`packages/api/remotes/src/index.ts`](../packages/api/remotes/src/index.ts) | diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 33f30e06ce..183b62184c 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -8,7 +8,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac | Event | Mode | Declared in | Dispatchers | Listeners | | --- | --- | --- | --- | --- | -| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:200`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | +| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:196`](../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:69`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` | | `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../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:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`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), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index 7953fffda4..9015f77ea3 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -10,7 +10,7 @@ | Event | Mode | Declared in | Dispatchers | Listeners | | --- | --- | --- | --- | --- | -| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:200`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - | +| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:196`](../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:69`](../packages/preset/agent-preset-registry/src/types.ts) | [`agent-preset-registry`](../packages/preset/agent-preset-registry) (`emit`) | `remotes` | | `agent/assistant-stream` | `emit` | [`packages/core/agent/src/runtime-types.ts:363`](../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:261`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`serial`) | `agent-team`, `browser-use-runtime`, `claude-code-mods`, [`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), `session-controller`, `tool-agent-team`, [`tool-subagent`](../packages/subagent/tool-subagent) | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index bffa5a533d..2062734ebd 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: bc3be962bc8738b1 - zh: e538aa8ff144468f + en: 269a193ddd48917f + zh: 63ef1beb457dbdd9 /session-persistence-event-catalog/events/agent-preset: en: 49891ca9be5ac123 zh: 49891ca9be5ac123 @@ -153,8 +153,8 @@ en: 4294f78d26ed985b zh: 4294f78d26ed985b /session-persistence-event-catalog/events/plan/plan-mode-log-only: - en: 9ae5537eeae238de - zh: 6be761c7fd83fdce + en: 862175bfe8692ef7 + zh: a8ab123bf674cf77 /session-persistence-event-catalog/events/request: en: a8df82440ebdf5f8 zh: a8df82440ebdf5f8 @@ -1395,8 +1395,8 @@ en: a74e3dfa51c987af zh: 6c3c87974d7a7260 /session-persistence-event-catalog/resolved-persistence-types/active~2: - en: c861888d6239b700 - zh: a9b2271d3c6b8737 + en: a3de6a6fe9672c7a + zh: a357481e27b3b81f /session-persistence-event-catalog/resolved-persistence-types/agentpreset: en: 1afbe8196f161601 zh: a320a36a8928817c @@ -1521,8 +1521,8 @@ en: 8a9199b1784c70f5 zh: 76c98bc74686fda0 /session-persistence-event-catalog/resolved-persistence-types/inserted-outcome-removedcount-start: - en: e77d5d524dd8b4ea - zh: 4dac28dda78a510f + en: fb9bed95931d510b + zh: dcaccd4254b4475c /session-persistence-event-catalog/resolved-persistence-types/interrupted-message-step-stream: en: ec10cd8e9c202a81 zh: e08f859bcb13f0d4 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index a0ff256594..f8363dc80e 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -185,7 +185,7 @@ Sources: [`packages/core/session/src/types.ts:432`](../packages/core/session/src } ``` -Source: [`packages/core/agent/src/types.ts:96`](../packages/core/agent/src/types.ts) +Source: [`packages/core/agent/src/types.ts:88`](../packages/core/agent/src/types.ts) ### `agent-preset/*` @@ -735,7 +735,7 @@ Source: [`packages/interaction/permission-presets/src/index.ts:59`](../packages/ 'plan/mode': { active: boolean } ``` -Source: [`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/src/index.ts) +Source: [`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts) ### `request/*` @@ -5761,7 +5761,7 @@ Array of `unknown` (opaque). SHA-256: `20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5` -Sources: [`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/src/index.ts) +Sources: [`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts) | Property | Presence | Type | |---|---|---| @@ -6369,7 +6369,7 @@ Sources: [`packages/core/session/src/types.ts:428`](../packages/core/session/src SHA-256: `fa106c5b33b657e8ef16e8cbf39f3b7d44e5cbcdf4f9303d9db6410f28951e42` -Sources: [`packages/core/agent/src/types.ts:96`](../packages/core/agent/src/types.ts) +Sources: [`packages/core/agent/src/types.ts:88`](../packages/core/agent/src/types.ts) | Property | Presence | Type | |---|---|---| diff --git a/docs/persistence-catalog.zh.md b/docs/persistence-catalog.zh.md index 8999e2592c..8344b8cd46 100644 --- a/docs/persistence-catalog.zh.md +++ b/docs/persistence-catalog.zh.md @@ -187,7 +187,7 @@ export type SessionEvent = { } ``` -来源:[`packages/core/agent/src/types.ts:96`](../packages/core/agent/src/types.ts) +来源:[`packages/core/agent/src/types.ts:88`](../packages/core/agent/src/types.ts) ### `agent-preset/*` @@ -737,7 +737,7 @@ export type SessionEvent = { 'plan/mode': { active: boolean } ``` -来源:[`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/src/index.ts) +来源:[`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts) ### `request/*` @@ -5763,7 +5763,7 @@ SHA-256: `b1664fa89f8eab49a14e7e3168d8bc4da4aa9fd76332f6c4a053d839e5276ae7` SHA-256: `20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5` -来源:[`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/src/index.ts) +来源:[`packages/plan/plan-mode/src/index.ts:52`](../packages/plan/plan-mode/src/index.ts) | 属性 | 存在性 | 类型 | |---|---|---| @@ -6371,7 +6371,7 @@ SHA-256: `17d1afb770d9941936130996da00dc86782cfef731d8d6526162c301256a4ac3` SHA-256: `fa106c5b33b657e8ef16e8cbf39f3b7d44e5cbcdf4f9303d9db6410f28951e42` -来源:[`packages/core/agent/src/types.ts:96`](../packages/core/agent/src/types.ts) +来源:[`packages/core/agent/src/types.ts:88`](../packages/core/agent/src/types.ts) | 属性 | 存在性 | 类型 | |---|---|---| diff --git a/docs/persistence-schema.json b/docs/persistence-schema.json index 7f6f2f787f..3291b0db74 100644 --- a/docs/persistence-schema.json +++ b/docs/persistence-schema.json @@ -22903,7 +22903,7 @@ "digest": "20a57544bbf204dacd1ae6beb2ab29444365c3c869f2afdf489dcdcbc3d9c8c5", "names": [], "sources": [ - "packages/plan/plan-mode/src/index.ts:53" + "packages/plan/plan-mode/src/index.ts:52" ] }, { @@ -26047,7 +26047,7 @@ "digest": "fa106c5b33b657e8ef16e8cbf39f3b7d44e5cbcdf4f9303d9db6410f28951e42", "names": [], "sources": [ - "packages/core/agent/src/types.ts:96" + "packages/core/agent/src/types.ts:88" ] }, { diff --git a/docs/subsystems/core.i18n.yaml b/docs/subsystems/core.i18n.yaml index 94bed76054..4033bc6756 100644 --- a/docs/subsystems/core.i18n.yaml +++ b/docs/subsystems/core.i18n.yaml @@ -35,3 +35,6 @@ /core/repo-wide-type-patterns/branded-ids: en: 10f8ac0f97e7c54a zh: 75cd2311aabfe464 +/core/turnboundaryprojection: + en: 3892ad06e90b25c1 + zh: ee6f592230ad89df diff --git a/docs/subsystems/core.md b/docs/subsystems/core.md index 42f6d2e24a..9ae060186b 100644 --- a/docs/subsystems/core.md +++ b/docs/subsystems/core.md @@ -409,6 +409,26 @@ type Branded = string & { readonly [BRAND]: B } The two core IDs are `ToolCallId` (correlates a tool call with its result; dsh-llm) and `SessionId` (the shared live agent and durable session identity; dsh-session). Capability packages brand their own ids too, such as `JobId` in [jobs.md](jobs.md). +## `TurnBoundaryProjection` + +`ctx.agents.turnBoundaryOf(session)` reads recorded turn and step positions without requiring a live Agent. The registry owns this state; consumers do not register its projection definition. + +Source: [`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts) + +```ts type-equiv +/** Turn and step positions recorded in a Session. */ +interface TurnBoundaryProjection { + /** Seq of the open turn's `turn/start`, or null between turns. */ + readonly openTurnStartSeq: OptionalSessionSeq + /** Seq of the latest `step/start` event, or null before the first step. */ + readonly lastStepStartSeq: OptionalSessionSeq + /** The latest step boundary (`step/start` or `step/end`) and its seq, or null before the first step boundary. */ + readonly lastStepBoundary: { readonly kind: 'start' | 'end'; readonly seq: SessionSeq } | null + /** Turn number of the latest `turn/start`; 0 before the first turn. */ + readonly lastTurn: number +} +``` + @@ -489,6 +509,13 @@ Source: [`packages/core/agent-loop/src/index.ts`](../../packages/core/agent-loop Registry of YAML-declared presets and the revisions live Agents retain. ```ts cordis-catalog +/** + * Read the preset recorded for a Session, including selections no longer in the roster. + * @param session - Session whose committed preset selection is read. + * @returns the recorded preset, or null when none was selected. + */ +presetForSession(session: Session): string | null + /** Register and eagerly load a definition; activation failure remains visible in the roster. * @param definition Parsed configuration supplied by the declaring plugin. * @returns Definition disposer after activation or its diagnostic settles; the declaring plugin owns it. @@ -578,7 +605,7 @@ async acquireScope(id?: string): Promise<{ key: ScopeKey } & AsyncDisposable> compositionInventory(): Promise ``` -Types: [ScopeKey](scope.md) +Types: [ScopeKey](scope.md) · [Session](session.md) Source: [`packages/preset/agent-preset-registry/src/index.ts`](../../packages/preset/agent-preset-registry/src/index.ts) @@ -591,6 +618,13 @@ Agent service (`ctx.agents`): tracks live agents and carries the initiating Agen Initiator methods provide same-process causal attribution only. Ambient presence is neither liveness proof nor authorization; subjects and owners remain explicit, as does identity at worker, process, persistence, and wire boundaries. Returned Promise boundaries drain during teardown, except a nested lineage that starts an owning-fiber unload is excluded from its own drain. ```ts cordis-catalog +/** + * Read the turn and step positions recorded in a Session. + * @param session - Session whose committed events are read. + * @returns the current turn and step positions; do not mutate the returned value. + */ +turnBoundaryOf(session: Session): TurnBoundaryProjection + /** * Read the Agent that initiated the inherited asynchronous driver chain. * Use this optional form for logging, tracing, metrics, or host attribution @@ -757,6 +791,8 @@ list(): Agent[] roots(): Agent[] ``` +Types: [Session](session.md) + Source: [`packages/core/agent/src/index.ts`](../../packages/core/agent/src/index.ts) diff --git a/docs/subsystems/core.zh.md b/docs/subsystems/core.zh.md index 9714c7c62f..fcd3d50f36 100644 --- a/docs/subsystems/core.zh.md +++ b/docs/subsystems/core.zh.md @@ -421,6 +421,26 @@ type Branded = string & { readonly [BRAND]: B } 两个核心 ID 是 `ToolCallId`(关联工具调用及其结果;dsh-llm)和 `SessionId`(活跃 agent 与持久会话共享的标识;dsh-session)。能力包也会品牌化各自的 id,例如 [jobs.md](jobs.zh.md) 中的 `JobId`。 +## `TurnBoundaryProjection` + +`ctx.agents.turnBoundaryOf(session)` 读取已记录的轮次和步骤位置,无需活跃 Agent。注册表持有此状态;消费方不注册其投影定义。 + +来源: [`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts) + +```ts type-equiv +/** Turn and step positions recorded in a Session. */ +interface TurnBoundaryProjection { + /** Seq of the open turn's `turn/start`, or null between turns. */ + readonly openTurnStartSeq: OptionalSessionSeq + /** Seq of the latest `step/start` event, or null before the first step. */ + readonly lastStepStartSeq: OptionalSessionSeq + /** The latest step boundary (`step/start` or `step/end`) and its seq, or null before the first step boundary. */ + readonly lastStepBoundary: { readonly kind: 'start' | 'end'; readonly seq: SessionSeq } | null + /** Turn number of the latest `turn/start`; 0 before the first turn. */ + readonly lastTurn: number +} +``` + @@ -501,6 +521,13 @@ Source: [`packages/core/agent-loop/src/index.ts`](../../packages/core/agent-loop Registry of YAML-declared presets and the revisions live Agents retain. ```ts cordis-catalog +/** + * Read the preset recorded for a Session, including selections no longer in the roster. + * @param session - Session whose committed preset selection is read. + * @returns the recorded preset, or null when none was selected. + */ +presetForSession(session: Session): string | null + /** Register and eagerly load a definition; activation failure remains visible in the roster. * @param definition Parsed configuration supplied by the declaring plugin. * @returns Definition disposer after activation or its diagnostic settles; the declaring plugin owns it. @@ -590,7 +617,7 @@ async acquireScope(id?: string): Promise<{ key: ScopeKey } & AsyncDisposable> compositionInventory(): Promise ``` -Types: [ScopeKey](scope.zh.md) +Types: [ScopeKey](scope.zh.md) · [Session](session.zh.md) Source: [`packages/preset/agent-preset-registry/src/index.ts`](../../packages/preset/agent-preset-registry/src/index.ts) @@ -603,6 +630,13 @@ Agent service (`ctx.agents`): tracks live agents and carries the initiating Agen Initiator methods provide same-process causal attribution only. Ambient presence is neither liveness proof nor authorization; subjects and owners remain explicit, as does identity at worker, process, persistence, and wire boundaries. Returned Promise boundaries drain during teardown, except a nested lineage that starts an owning-fiber unload is excluded from its own drain. ```ts cordis-catalog +/** + * Read the turn and step positions recorded in a Session. + * @param session - Session whose committed events are read. + * @returns the current turn and step positions; do not mutate the returned value. + */ +turnBoundaryOf(session: Session): TurnBoundaryProjection + /** * Read the Agent that initiated the inherited asynchronous driver chain. * Use this optional form for logging, tracing, metrics, or host attribution @@ -769,6 +803,8 @@ list(): Agent[] roots(): Agent[] ``` +Types: [Session](session.zh.md) + Source: [`packages/core/agent/src/index.ts`](../../packages/core/agent/src/index.ts) diff --git a/docs/subsystems/token-meter.i18n.yaml b/docs/subsystems/token-meter.i18n.yaml index 1e666632b5..eff44c4e64 100644 --- a/docs/subsystems/token-meter.i18n.yaml +++ b/docs/subsystems/token-meter.i18n.yaml @@ -11,3 +11,6 @@ /token-meter/tokensurfacenode: en: e445d12b10f651d0 zh: e93be5fb9da1da15 +/token-meter/contextpressureprojection: + en: 78c8308e9f54fc38 + zh: f76658e7a2739807 diff --git a/docs/subsystems/token-meter.md b/docs/subsystems/token-meter.md index 8accb9cf9f..9476165e45 100644 --- a/docs/subsystems/token-meter.md +++ b/docs/subsystems/token-meter.md @@ -53,6 +53,44 @@ interface TokenSurfaceNode { Surface order is authoritative; replacement nodes can have higher durable seqs than later positional nodes. The snapshot is immutable and does not grow when the underlying replay fold advances. +## `ContextPressureProjection` + +`ctx.tokenMeter.contextPressure(session)` synchronously reads the latest recorded context pressure and capacity. These display values do not price a new request; use `measure()` for request pricing. + +Source: [`packages/llm/token-meter/src/projection.ts`](../../packages/llm/token-meter/src/projection.ts) + +```ts type-equiv +/** + * Approximate context occupancy for a status display. + * + * The fields, when present, are deliberately NOT one atomic request + * observation: each is a last-wins record of a different moment. Switching + * models can therefore pair a fresh capacity with the previous route's + * pressure until the next request reports usage. This is an intentional trade + * — the value is a user-facing reference, not a billing or gating input. See + * the token-meter README for the full rationale. + */ +interface ContextPressureProjection { + /** + * Provider-reported prompt size of the most recent request: uncached input + * plus cache reads and writes. Response output is excluded, so this does not + * grow as the current turn streams. Absent until a provider reports usage. + */ + pressureTokens?: number + /** + * What the NEXT request's prompt would cost: {@link pressureTokens} plus the + * heuristic repricing of everything the surface gained or lost since that + * sample. Only the delta is estimated, so the figure stays anchored to the + * provider while still reacting the moment a compaction shadows a span — + * which `pressureTokens` alone cannot do, since compaction reports no usage + * of its own. Absent until a provider reports usage. + */ + projectedTokens?: number + /** Newest recorded route capacity; absent when no adapter advertised one. */ + contextWindow?: number +} +``` + @@ -68,6 +106,13 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp Replay owner for one service-wide estimator and isolated per-session folds. ```ts cordis-catalog +/** + * Read the latest logged context occupancy and its projected surface adjustment. + * @param session - Session whose committed usage and request capacity are read. + * @returns context pressure; fields remain absent until their source events are recorded. + */ +contextPressure(session: Session): ContextPressureProjection + /** * Measure current request pressure and surface through the durable tail. * diff --git a/docs/subsystems/token-meter.zh.md b/docs/subsystems/token-meter.zh.md index 6c149ec3f2..07893eacce 100644 --- a/docs/subsystems/token-meter.zh.md +++ b/docs/subsystems/token-meter.zh.md @@ -53,6 +53,44 @@ interface TokenSurfaceNode { 表层顺序具有权威性;替换节点的持久 seq 可能高于位置排在其后的节点。该快照不可变,不会随底层回放折叠推进而增长。 +## `ContextPressureProjection` + +`ctx.tokenMeter.contextPressure(session)` 同步读取最新记录的上下文压力与容量。这些显示值不为新请求计价;请求计价使用 `measure()`。 + +来源: [`packages/llm/token-meter/src/projection.ts`](../../packages/llm/token-meter/src/projection.ts) + +```ts type-equiv +/** + * Approximate context occupancy for a status display. + * + * The fields, when present, are deliberately NOT one atomic request + * observation: each is a last-wins record of a different moment. Switching + * models can therefore pair a fresh capacity with the previous route's + * pressure until the next request reports usage. This is an intentional trade + * — the value is a user-facing reference, not a billing or gating input. See + * the token-meter README for the full rationale. + */ +interface ContextPressureProjection { + /** + * Provider-reported prompt size of the most recent request: uncached input + * plus cache reads and writes. Response output is excluded, so this does not + * grow as the current turn streams. Absent until a provider reports usage. + */ + pressureTokens?: number + /** + * What the NEXT request's prompt would cost: {@link pressureTokens} plus the + * heuristic repricing of everything the surface gained or lost since that + * sample. Only the delta is estimated, so the figure stays anchored to the + * provider while still reacting the moment a compaction shadows a span — + * which `pressureTokens` alone cannot do, since compaction reports no usage + * of its own. Absent until a provider reports usage. + */ + projectedTokens?: number + /** Newest recorded route capacity; absent when no adapter advertised one. */ + contextWindow?: number +} +``` + @@ -68,6 +106,13 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp Replay owner for one service-wide estimator and isolated per-session folds. ```ts cordis-catalog +/** + * Read the latest logged context occupancy and its projected surface adjustment. + * @param session - Session whose committed usage and request capacity are read. + * @returns context pressure; fields remain absent until their source events are recorded. + */ +contextPressure(session: Session): ContextPressureProjection + /** * Measure current request pressure and surface through the durable tail. * diff --git a/docs/tool-catalog.i18n.yaml b/docs/tool-catalog.i18n.yaml index af0cd38b46..0dd8e8ce38 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: e18a3e3c916afb23 - zh: 4284b50a5e9bd12f + en: fb7e632265a9426c + zh: c2586fe58cb934d5 /tool-schema-catalog/deepseek-ai-dsh-plugin-manager: en: e0143916bb634762 zh: e0143916bb634762 diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index f96fe0802d..85df254590 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -20,9 +20,9 @@ This table connects model-visible tool names to the plugin package and service s | `@deepseek-ai/dsh-experimental-browser-use-stagehand-native` | `stagehand_act`, `stagehand_extract`, `stagehand_navigate`, `stagehand_observe`, `stagehand_screenshot`, `stagehand_tabs` | `ctx.browserUse`, `ctx.agents`, `ctx.tools`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | - | | `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`, `ctx.userQuestions` | `tool/call`, `tool/result after an answer or timeout`, `late user/message` | - | ask_user_question keeps the original blocking behavior by default; set `mode: timed` to opt into a foreground timeout and pending result while the question remains answerable. In timed mode, `timeout: -1` keeps that call blocking indefinitely. | | `@deepseek-ai/dsh-tools` | `run_code` | `ctx.workingDirectory (Agent executions)`, `ctx.tools`, `ctx.ptcRuntime (execution time)`, `ctx.systemPrompt` | `tool/call`, `one tool/ptc-dispatch-start + tool/ptc-dispatch pair per bridged sub-call`, `tool/result` | - | Owned by the tool registry as a reserved transport outside filterable capability layers under `mode: ptc` (see the PTC mode Agent Note). Under `ptc` it is the registry's only wire contribution; the other visible capabilities are declared in a generated SDK section in the loaded runtime's language, and a program calls them through bindings scheduled under the native concurrency contract (submission-ordered starts and policy; concurrency-safe bodies overlap up to `maxParallelSubCalls`) that re-enter the complete guarded tool pipeline and link each nested execution to this outer result. | -| `@deepseek-ai/dsh-plan-mode` | `exit_plan_mode` | `ctx.tools`, `ctx.systemPrompt`, `ctx.userQuestions (execution time, opportunistic)` | `tool/call`, `plan/mode inactive on an approved review`, `tool/result` | - | exit_plan_mode stays in the model-facing schema while planning is inactive so transitions add no tool-catalog churn on top of the plan-policy change. Its execute path rejects calls outside plan mode; in plan mode it presents the plan over the user-questions seam (approve / keep planning with feedback), and approval logs plan mode inactive at the step boundary. | +| `@deepseek-ai/dsh-plan-mode` | `exit_plan_mode` | `ctx.tools`, `ctx.systemPrompt`, `ctx.agents`, `ctx.userQuestions (execution time, opportunistic)` | `tool/call`, `plan/mode inactive on an approved review`, `tool/result` | - | exit_plan_mode stays in the model-facing schema while planning is inactive so transitions add no tool-catalog churn on top of the plan-policy change. Its execute path rejects calls outside plan mode; in plan mode it presents the plan over the user-questions seam (approve / keep planning with feedback), and approval logs plan mode inactive at the step boundary. | | `@deepseek-ai/dsh-tool-bash` | `bash` | `ctx.workingDirectory`, `ctx.tools`, `ctx.shell`, `ctx.systemPrompt`, `ctx.shellEnv`, `ctx.jobs for run_in_background and the job-backed foreground path` | `tool/call`, `tool/result` | - | The bash tool is the model-facing consumer of the bash executor seam. With a job registry composed every call registers with the generic `ctx.jobs` runtime as it starts, collected/stopped through the `job_*` tools from `@deepseek-ai/dsh-tool-jobs`; without one, or with `enableRunInBackground: false`, the tool registers a foreground-only schema without the `run_in_background` parameter. | -| `@deepseek-ai/dsh-tool-present` | `present` | `ctx.workingDirectory`, `ctx.tools`, `ctx.fs`, `ctx.sessionProjections` | `tool/call`, `deliverables/presented after a successful final result`, `tool/result` | - | Deliveries belong to the calling Session; Web ui-deliverables supplies source-file opening and cards. | +| `@deepseek-ai/dsh-tool-present` | `present` | `ctx.workingDirectory`, `ctx.tools`, `ctx.fs`, `ctx.agents` | `tool/call`, `deliverables/presented after a successful final result`, `tool/result` | - | Deliveries belong to the calling Session; Web ui-deliverables supplies source-file opening and cards. | | `@deepseek-ai/dsh-tool-pwsh` | `pwsh` | `ctx.workingDirectory`, `ctx.tools`, `ctx.shell`, `ctx.systemPrompt`, `ctx.shellEnv`, `ctx.jobs for run_in_background and the job-backed foreground path` | `tool/call`, `tool/result` | - | The pwsh tool is the PowerShell-dialect consumer of the bash executor seam for Windows compositions (a PowerShell executor such as `@deepseek-ai/dsh-pwsh-local` backs `ctx.shell`); it mirrors the bash tool call-for-call minus sandbox controls — `run_in_background` runs register with the generic `ctx.jobs` runtime and are collected/stopped through the `job_*` tools, and the managed `DSH_*` environment comes from `@deepseek-ai/dsh-shell-env`. Each call runs in a fresh process (no persistent PTY session), with native `C:\...` paths and `$env:NAME` variables. | | `@deepseek-ai/dsh-tool-cordis` | `cordis_inspect_list`, `cordis_inspect_query` | `ctx.tools`, `ctx.cordisInspect` | `tool/call`, `tool/result` | - | Creator mode provides two read-only runtime inspection tools. The Cordis host runner supplies the inspection registry; Client queries require a connected page. Author persistent changes as bundles and install them with plugin_manager. | | `@deepseek-ai/dsh-tool-bash-persistent` | `bash` | `ctx.workingDirectory`, `ctx.tools`, `ctx.terminals`, `an owning Agent at execution time` | `tool/call`, `PTY shell state`, `tool/result` | - | One owner-isolated persistent bash tool; deployment composition supplies the PTY backend and may override the model-facing environment description. | @@ -36,7 +36,7 @@ This table connects model-visible tool names to the plugin package and service s | `@deepseek-ai/dsh-tool-lsp` | `lsp` | `ctx.workingDirectory`, `ctx.tools`, `ctx.lsp`, `ctx.systemPrompt` | `tool/call`, `tool/result` | - | The lsp tool keeps provider selection and language-server subprocesses behind ctx.lsp, so its model-visible schema stays stable across providers. Requires a registered provider (e.g. `@deepseek-ai/dsh-lsp-stdio`) at runtime; without one, a query returns the structured `LSP_UNAVAILABLE` error rather than changing the schema. | | `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`, `ctx.workflowEngine`, `ctx.subagents`, `ctx.systemPrompt`, `a calling Agent (exec.agent parents every fresh round)` | `tool/call`, `tool/result`, `workflow and child session events during execution` | - | A fixed foreground workflow starts one fresh structured child per round; the model selects only the immutable objective and an optional round cap. | | `@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` | - | 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-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-tool-subagent` | `list_subagent_models`, `subagent` | `ctx.tools`, `ctx.subagents`, `ctx.systemPrompt`, `ctx.llm for model discovery and selected-route validation` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The registered delegation name is the load-time `toolName` config (default `subagent`); the default schema above has model selection off, while the discovery schema is shown as the fixed companion available in an enabled Session. Web presets sample the Plugins preference for each new top-level Session and preserve that decision for its child Sessions; `subagent_fork` remains fixed-route. Each instance selects its backend through `provider` and reads model-selection settings through `modelSelectionSettings`. 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-control` | `interrupt_agent`, `list_agents`, `send_message` | `ctx.tools`, `ctx.subagents`, `ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`, `tool/result`, `child session events through ctx.subagents` | - | The globally named discovery, interruption, and local-message tools over managed subagents: provider-bound `tool-subagent` instances register distinct delegation tools, while this package registers `send_message` and `interrupt_agent` once, plus `list_agents` from its separately loaded `/list-agents` plugin (whose catalog rows use the sessionProjections and live Agent registries). | | `@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()`. | diff --git a/docs/tool-catalog.zh.md b/docs/tool-catalog.zh.md index f7ed4a8c08..b4f833c305 100644 --- a/docs/tool-catalog.zh.md +++ b/docs/tool-catalog.zh.md @@ -24,9 +24,9 @@ | `@deepseek-ai/dsh-experimental-browser-use-stagehand-native` | `stagehand_act`、`stagehand_extract`、`stagehand_navigate`、`stagehand_observe`、`stagehand_screenshot`、`stagehand_tabs` | `ctx.browserUse`、`ctx.agents`、`ctx.tools`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | - | | `@deepseek-ai/dsh-tool-ask-user` | `ask_user_question` | `ctx.tools`、`ctx.userQuestions` | `tool/call`、`tool/result after an answer or timeout`、`late user/message` | - | ask_user_question 默认保持原有阻塞行为;设置 `mode: timed` 后才启用前台超时与 pending 结果,同时问题仍可回答;timed 模式内 `timeout: -1` 让本次调用无限期阻塞。 | | `@deepseek-ai/dsh-tools` | `run_code` | `ctx.workingDirectory (Agent executions)`、`ctx.tools`、`ctx.ptcRuntime (execution time)`、`ctx.systemPrompt` | `tool/call`、`one tool/ptc-dispatch-start + tool/ptc-dispatch pair per bridged sub-call`、`tool/result` | - | 在 `mode: ptc` 下,它由工具注册表所有,作为可过滤能力层之外的保留传输机制(参见 PTC mode Agent Note)。在 `ptc` 下,它是注册表对协议格式(wire format)的唯一贡献;其他可见能力在使用已加载运行时语言生成的 SDK 章节中声明。程序通过 binding 调用这些能力,调用按照原生并发约定调度:启动顺序和策略遵循提交顺序,并发安全的函数体最多重叠执行 `maxParallelSubCalls` 个。调用会重新进入完整且受守卫保护的工具流水线,并将每个嵌套执行关联到此外层结果。 | -| `@deepseek-ai/dsh-plan-mode` | `exit_plan_mode` | `ctx.tools`、`ctx.systemPrompt`、`ctx.userQuestions (execution time, opportunistic)` | `tool/call`、`plan/mode inactive on an approved review`、`tool/result` | - | 规划未激活时,exit_plan_mode 仍保留在面向模型的 schema 中,这样状态转换不会在规划策略变更之外额外造成工具目录变动。其执行路径会拒绝规划模式之外的调用;在规划模式下,它通过用户交互 seam 提交计划(批准/根据反馈继续规划),批准后会在步骤边界记录规划模式已停用。 | +| `@deepseek-ai/dsh-plan-mode` | `exit_plan_mode` | `ctx.tools`、`ctx.systemPrompt`、`ctx.agents`、`ctx.userQuestions (execution time, opportunistic)` | `tool/call`、`plan/mode inactive on an approved review`、`tool/result` | - | 规划未激活时,exit_plan_mode 仍保留在面向模型的 schema 中,这样状态转换不会在规划策略变更之外额外造成工具目录变动。其执行路径会拒绝规划模式之外的调用;在规划模式下,它通过用户交互 seam 提交计划(批准/根据反馈继续规划),批准后会在步骤边界记录规划模式已停用。 | | `@deepseek-ai/dsh-tool-bash` | `bash` | `ctx.workingDirectory`、`ctx.tools`、`ctx.shell`、`ctx.systemPrompt`、`ctx.shellEnv`、`ctx.jobs for run_in_background and the job-backed foreground path` | `tool/call`、`tool/result` | - | bash 工具是 bash 执行器 seam 面向模型的消费方。组合中有 job 注册表时,每次调用一启动就注册到通用 `ctx.jobs` 运行时,并通过 `job_*` 工具(来自 `@deepseek-ai/dsh-tool-jobs`)收集/停止;没有注册表或 `enableRunInBackground: false` 时,工具注册不带 `run_in_background` 参数的纯前台 schema。 | -| `@deepseek-ai/dsh-tool-present` | `present` | `ctx.workingDirectory`、`ctx.tools`, `ctx.fs`, `ctx.sessionProjections` | `tool/call`, `deliverables/presented 在成功的最终结果之后`, `tool/result` | - | 交付归调用方 Session 所有;Web ui-deliverables 提供源文件打开与卡片。 | +| `@deepseek-ai/dsh-tool-present` | `present` | `ctx.workingDirectory`、`ctx.tools`, `ctx.fs`, `ctx.agents` | `tool/call`, `deliverables/presented 在成功的最终结果之后`, `tool/result` | - | 交付归调用方 Session 所有;Web ui-deliverables 提供源文件打开与卡片。 | | `@deepseek-ai/dsh-tool-pwsh` | `pwsh` | `ctx.workingDirectory`、`ctx.tools`、`ctx.shell`、`ctx.systemPrompt`、`ctx.shellEnv`、`ctx.jobs for run_in_background and the job-backed foreground path` | `tool/call`、`tool/result` | - | pwsh 工具是 Windows 组合中 bash 执行器 seam 的 PowerShell 方言消费方(由 `@deepseek-ai/dsh-pwsh-local` 等 PowerShell 执行器为 `ctx.shell` 提供后端);除沙箱接口外,它逐项对应 bash 工具调用。使用 `run_in_background` 的运行会注册到通用 `ctx.jobs` 运行时,并通过 `job_*` 工具收集/停止;托管的 `DSH_*` 环境来自 `@deepseek-ai/dsh-shell-env`。每次调用都在新进程中运行,不使用持久 PTY 会话。路径采用原生 `C:\...` 形式,变量采用 `$env:NAME`。 | | `@deepseek-ai/dsh-tool-cordis` | `cordis_inspect_list`, `cordis_inspect_query` | `ctx.tools`, `ctx.cordisInspect` | `tool/call`, `tool/result` | - | 创造模式提供两个只读运行时检查工具。Cordis host runner 提供检查注册表;Client 查询需要已连接页面。持久化变更编写为组合包,再通过 plugin_manager 安装。 | | `@deepseek-ai/dsh-tool-bash-persistent` | `bash` | `ctx.workingDirectory`、`ctx.tools`、`ctx.terminals`、`an owning Agent at execution time` | `tool/call`、`PTY shell state`、`tool/result` | - | 一个按所有者隔离的持久 bash 工具;部署组合提供 PTY 后端,并可覆盖面向模型的环境描述。 | @@ -40,7 +40,7 @@ | `@deepseek-ai/dsh-tool-lsp` | `lsp` | `ctx.workingDirectory`、`ctx.tools`、`ctx.lsp`、`ctx.systemPrompt` | `tool/call`、`tool/result` | - | lsp 工具将提供方选择和语言服务器子进程置于 ctx.lsp 之后,因此其模型可见 schema 在更换提供方时保持稳定。运行时要求已注册提供方,例如 `@deepseek-ai/dsh-lsp-stdio`;如果没有提供方,查询会返回结构化 `LSP_UNAVAILABLE` 错误,而不会改变 schema。 | | `@deepseek-ai/dsh-tool-ralph` | `ralph` | `ctx.tools`、`ctx.workflowEngine`、`ctx.subagents`、`ctx.systemPrompt`、`a calling Agent (exec.agent parents every fresh round)` | `tool/call`、`tool/result`、`workflow and child session events during execution` | - | 固定的前台工作流会在每个 Round 启动一个全新的结构化子级;模型只能选择不可变目标和可选的 Round 上限。 | | `@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-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` | - | 这 5 个只读工具会隐藏提供方游标,并根据不可变的调用 agent 会话为每个结果授权。该包需要选择启用;需要强制截止时间或限制行内输出的组合还会挂载通用超时或 spill 策略。 | | `@deepseek-ai/dsh-tool-subagent` | `list_subagent_models`、`subagent` | `ctx.tools`、`ctx.subagents`、`ctx.systemPrompt`、`用于模型发现和所选路由校验的 ctx.llm` | `tool/call`、`tool/result`、`child session events through the chosen provider` | `subagent`、`subagent_fork` | 注册的委派工具名称取决于加载时 `toolName` 配置(默认为 `subagent`);上述默认 schema 关闭模型选择,而发现 schema 则展示为已启用 Session 中可用的固定配套工具。Web preset 会在每个新顶层 Session 创建时读取插件页偏好,并为其子 Session 保留该决定;`subagent_fork` 始终使用固定路由。每个实例通过 `provider` 选择后端,通过 `modelSelectionSettings` 读取模型选择设置。每次委派都会立即返回子代理 id;本地子代理接受后续消息,外部后端自动汇报单次执行结果。 | | `@deepseek-ai/dsh-tool-subagent-control` | `interrupt_agent`、`list_agents`、`send_message` | `ctx.tools`、`ctx.subagents`、`ctx.agents and ctx.sessionProjections (list_agents only)` | `tool/call`、`tool/result`、`child session events through ctx.subagents` | - | 这些是用于发现、中断托管 subagent 和向本地子代理发送消息的全局命名工具:绑定提供方的 `tool-subagent` 实例注册不同的委派工具;本包注册一次 `send_message` 和 `interrupt_agent`,另由 `list_agents` 通过单独加载的 `/list-agents` 插件提供,其目录行使用 sessionProjections 和实时 Agent 注册表。 | | `@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()`。 | diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.i18n.yaml index 5d03bdc8c8..9af48f5ce7 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.i18n.yaml +++ b/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.i18n.yaml @@ -9,8 +9,8 @@ en: 727968e582c6eee6 zh: e01dc83384747012 /session-projection-registration-handles/change: - en: 06c3d4ee9031fb7e - zh: cf2b664bfdafe837 + en: 154904370da3336a + zh: 8e508300cf097a27 /session-projection-registration-handles/migration: - en: 50cde783ecaa35e8 - zh: 42e5574f58b049d6 + en: 2d27fb8cc2f18a79 + zh: 6d8461cd25c3a608 diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.md b/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.md index 20d11a2b03..4014d86b93 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.md +++ b/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.md @@ -10,12 +10,12 @@ English | [中文](guide.zh.md) `@deepseek-ai/dsh-session-projection` changes `SessionProjectionRegistry.register(definition)` from a synchronous disposer to `Promise>`. Registry-level `stateOf(session, key)` is removed. Registration errors now reject the returned promise. After the last registration is disposed, the unit leaves snapshots, checkpoints, and live notifications, but retained handles can still fold Session events on demand. -`@deepseek-ai/dsh-subagent` now requires `sessionProjections`. `@deepseek-ai/dsh-api-session-controller` removes `installModelSelectionProjection`; consumers register `modelSelectionProjection` instead. The controller also registers `agentPreset`, whose null value means the Session has no recorded preset; that key does not establish that a preset registry is available. `turnBoundaryProjectionDefinition` is exported by `@deepseek-ai/dsh-agent`; its `agent-loop` re-export remains available. +`@deepseek-ai/dsh-agent` and `@deepseek-ai/dsh-subagent` require `sessionProjections`. AgentRegistry owns turn and step state; consumers use `ctx.agents.turnBoundaryOf(session)` instead of importing `turnBoundaryProjectionDefinition`. Preset and context-pressure reads use `ctx.agentPresets.presetForSession(session)` and `ctx.tokenMeter.contextPressure(session)`. The API controller and mods bridge no longer register those definitions independently; their optional-service behavior applies when the owning service is absent. `@deepseek-ai/dsh-api-session-controller` also removes `installModelSelectionProjection`. ## Migration -1. Declare `sessionProjections` as a required plugin injection and mount `SessionProjectionRegistry` before consumers, including `SubagentRuntime` in custom compositions. -2. In an asynchronous function-plugin `apply()` or service `[Service.init]()`, await `ctx.sessionProjections.register(definition)` and retain the returned handle before publishing dependent operations. Keep existing function-plugin namespace exports. -3. Replace `registry.stateOf(session, key)` with `handle.stateOf(session)`. Import the owning package's projection definition when a reader needs to acquire a handle itself. Replace stored unregister callbacks with `handle.dispose`, which always returns `Promise`, and await explicit disposal; fiber disposal still releases registrations automatically. Await registration when handling initialization failures. +1. Mount `SessionProjectionRegistry` before `AgentRegistry`, `SubagentRuntime`, and other projection owners in custom compositions. Consumers of domain data inject the owning service. +2. For a plugin's own definitions, await `ctx.sessionProjections.register(definition)` in asynchronous `apply()` or `[Service.init]()` and retain the handle privately before publishing dependent operations. Keep existing function-plugin namespace exports. +3. Replace `registry.stateOf(session, key)` with a business service method for another domain, or `handle.stateOf(session)` inside the owner. Replace stored unregister callbacks with `handle.dispose`, which always returns `Promise`, and await explicit disposal; fiber disposal still releases registrations automatically. Await registration when handling initialization failures. 4. Replace `installModelSelectionProjection(ctx)` with `await ctx.sessionProjections.register(modelSelectionProjection)`, importing the definition from `@deepseek-ai/dsh-api-session-controller/src/model-selection-projection.ts`. 5. Typecheck custom plugins, then verify activation, a Session state read, and owner disposal. A released reader remains usable, while the released unit disappears from new snapshots once its final registration is gone. diff --git a/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.zh.md index fd52f33077..b48ff66064 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.zh.md +++ b/docs/upgrade-guide/v0.2.1-alpha.2/session-projection-handles/guide.zh.md @@ -10,12 +10,12 @@ description: "Session 投影注册改为异步,并返回保留读取能力的 `@deepseek-ai/dsh-session-projection` 的 `SessionProjectionRegistry.register(definition)` 从同步释放函数改为返回 `Promise>`。注册表级 `stateOf(session, key)` 已移除,注册错误现在使返回的 Promise 拒绝。最后一次注册释放后,该投影单元不再参与快照、检查点和实时通知,但保留的句柄仍可按需折叠 Session 事件。 -`@deepseek-ai/dsh-subagent` 现在必需注入 `sessionProjections`。`@deepseek-ai/dsh-api-session-controller` 移除了 `installModelSelectionProjection`;消费者改为注册 `modelSelectionProjection`。控制器也注册 `agentPreset`,其 null 值表示 Session 未记录预设;该键不代表预设注册表可用。`turnBoundaryProjectionDefinition` 由 `@deepseek-ai/dsh-agent` 导出;`agent-loop` 的再导出仍然可用。 +`@deepseek-ai/dsh-agent` 与 `@deepseek-ai/dsh-subagent` 依赖 `sessionProjections`。AgentRegistry 持有轮次和步骤状态;消费方使用 `ctx.agents.turnBoundaryOf(session)`,不再导入 `turnBoundaryProjectionDefinition`。预设与上下文压力分别通过 `ctx.agentPresets.presetForSession(session)` 和 `ctx.tokenMeter.contextPressure(session)` 读取。API 控制器与 mods 桥接不再独立注册这些定义;所属服务缺席时采用各自的可选服务行为。`@deepseek-ai/dsh-api-session-controller` 同时移除了 `installModelSelectionProjection`。 ## 迁移 -1. 将 `sessionProjections` 声明为插件的必需注入,并在消费者之前挂载 `SessionProjectionRegistry`,包括自定义组合中的 `SubagentRuntime`。 -2. 在函数插件的异步 `apply()` 或服务的 `[Service.init]()` 中,等待 `ctx.sessionProjections.register(definition)`,在发布依赖操作前保留返回的句柄。保留既有的函数插件命名空间导出。 -3. 将 `registry.stateOf(session, key)` 替换为 `handle.stateOf(session)`。读取方需要自行取得句柄时,导入归属包的投影定义。将保存的注销回调替换为始终返回 `Promise` 的 `handle.dispose`,并等待显式释放完成;fiber 释放仍会自动解除注册。处理初始化错误时须等待注册完成。 +1. 在自定义组合中,先挂载 `SessionProjectionRegistry`,再挂载 `AgentRegistry`、`SubagentRuntime` 及其他投影所有者。领域数据的消费方注入所属服务。 +2. 对插件自己的定义,在异步 `apply()` 或 `[Service.init]()` 中等待 `ctx.sessionProjections.register(definition)`,并在提供依赖操作前于内部保存句柄。保留现有函数插件命名空间导出。 +3. 将 `registry.stateOf(session, key)` 替换为其他领域的业务服务方法,或所有者内部的 `handle.stateOf(session)`。将保存的取消注册回调替换为始终返回 `Promise` 的 `handle.dispose`,并等待显式释放完成;fiber 释放仍会自动释放注册。处理初始化失败时须等待注册。 4. 将 `installModelSelectionProjection(ctx)` 替换为 `await ctx.sessionProjections.register(modelSelectionProjection)`,从 `@deepseek-ai/dsh-api-session-controller/src/model-selection-projection.ts` 导入该定义。 5. 对自定义插件执行类型检查,再验证激活、Session 状态读取与 owner 释放。已释放的读取句柄仍可使用,而投影单元在最后一次注册解除后会从新快照中消失。 diff --git a/packages/api/job-controller/tests/host-harness.ts b/packages/api/job-controller/tests/host-harness.ts index 85609166b0..4a8fca3bfe 100644 --- a/packages/api/job-controller/tests/host-harness.ts +++ b/packages/api/job-controller/tests/host-harness.ts @@ -1,5 +1,6 @@ import { Context } from '@deepseek-ai/cordis' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import { unsupportedInbox } from '@deepseek-ai/dsh-agent-loop-testkit' import type { Agent } from '@deepseek-ai/dsh-agent' import type { JobAppendOptions, JobHandle, JobOutcome } from '@deepseek-ai/dsh-jobs' @@ -9,6 +10,7 @@ import { Session, SessionId } from '@deepseek-ai/dsh-session' /** A registry with a tiny ring so eviction is cheap to reach. */ export async function harness(): Promise { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry, { retainBytes: 64, settledRetainBytes: 64 }) ctx.jobs.attachController('job-controller-test') diff --git a/packages/api/job-controller/tests/kill.host.spec.ts b/packages/api/job-controller/tests/kill.host.spec.ts index 2a14afac2d..bfaf0c2141 100644 --- a/packages/api/job-controller/tests/kill.host.spec.ts +++ b/packages/api/job-controller/tests/kill.host.spec.ts @@ -1,5 +1,6 @@ import { Context } from '@deepseek-ai/cordis' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import { unsupportedInbox } from '@deepseek-ai/dsh-agent-loop-testkit' import type { Agent } from '@deepseek-ai/dsh-agent' import type { JobId, JobOutcome } from '@deepseek-ai/dsh-jobs' @@ -45,6 +46,7 @@ async function harness(): Promise<{ }> { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) ctx.jobs.attachController('kill-test') diff --git a/packages/api/job-controller/tests/observe.host.spec.ts b/packages/api/job-controller/tests/observe.host.spec.ts index 583862eeac..92c680ed07 100644 --- a/packages/api/job-controller/tests/observe.host.spec.ts +++ b/packages/api/job-controller/tests/observe.host.spec.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import { unsupportedInbox } from '@deepseek-ai/dsh-agent-loop-testkit' import type { Agent } from '@deepseek-ai/dsh-agent' import { JobId } from '@deepseek-ai/dsh-jobs' @@ -215,6 +216,7 @@ describe('JobController', () => { async function controllerHarness() { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await ctx.plugin(TypertRegistry) @@ -247,6 +249,7 @@ describe('JobController', () => { it('waits for the job registry instead of serving without one', async () => { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(TypertRegistry) ctx.plugin(JobController, {}) diff --git a/packages/api/session-controller/README.i18n.yaml b/packages/api/session-controller/README.i18n.yaml index 87382e312d..ff8ee89c2f 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: ffe8f507e1ac31c7 - zh: 3cd25c71b42a85c1 + en: 8fcfa14545bd9d1c + zh: c10c2bba792699ef /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 66a7186d60..fbc0aeaa3b 100644 --- a/packages/api/session-controller/README.md +++ b/packages/api/session-controller/README.md @@ -27,7 +27,7 @@ Desktop analytics follows the [product collection policy](../../client/product-a ## Use this package -The controller registers the `agentPreset` projection independently of preset execution support. Its value is the recorded preset id or null; key presence does not indicate that `agentPresets` is composed. +The controller reads a Session's recorded preset through the optional `agentPresets.presetForSession(session)` service method. A null result means no preset was recorded; an absent service means preset identity is unavailable to the controller. History pages and follow opening snapshots carry one `{ type: 'event', event: SessionWireEvent }` record per durable Session event. The Client retains each accepted record as one durable `SessionEventLikeEntry`; Assistant token boundaries remain inside the compact stream on `assistant/message` or `assistant/attempt`. Tool arguments, result content, failures, and `tool/result.data.meta` pass through unchanged; the controller does not resolve a Tool definition, run a presenter, or attach UI data. diff --git a/packages/api/session-controller/README.zh.md b/packages/api/session-controller/README.zh.md index a4406446ae..d63735689f 100644 --- a/packages/api/session-controller/README.zh.md +++ b/packages/api/session-controller/README.zh.md @@ -27,7 +27,7 @@ kind: "package-reference" ## 使用本包 -控制器独立于预设执行能力注册 `agentPreset` 投影。其值为已记录的预设 id 或 null;键存在不代表组合中包含 `agentPresets`。 +控制器通过可选服务方法 `agentPresets.presetForSession(session)` 读取 Session 已记录的预设。null 结果表示未记录预设;服务缺席表示控制器无法获知预设身份。 历史页与 follow opening 快照为每个持久 Session 事件携带一条 `{ type: 'event', event: SessionWireEvent }` record。Client 把每条已接受 record 保留为一个持久 `SessionEventLikeEntry`;Assistant token 边界保留在 `assistant/message` 或 `assistant/attempt` 的紧凑流内。工具参数、结果内容、失败信息和 `tool/result.data.meta` 原样通过;控制器不解析工具定义、不运行展示转换器,也不附加 UI 数据。 diff --git a/packages/api/session-controller/src/agent.ts b/packages/api/session-controller/src/agent.ts index 697cfb91de..6c967c5c10 100644 --- a/packages/api/session-controller/src/agent.ts +++ b/packages/api/session-controller/src/agent.ts @@ -3,12 +3,12 @@ import { randomUUID } from 'node:crypto' import { mkdir } from 'node:fs/promises' import type { Context } from '@deepseek-ai/cordis' -import { installModelSelection, turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent' +import { installModelSelection } from '@deepseek-ai/dsh-agent' import type { - Agent, AgentOptions, AgentSetup, ModelSelection as AgentModelSelection, ModelSelectionRef, TurnBoundaryProjection, + Agent, AgentOptions, AgentSetup, ModelSelection as AgentModelSelection, ModelSelectionRef, } from '@deepseek-ai/dsh-agent' import type {} from '@deepseek-ai/dsh-agent-default-model' -import { agentPresetProjectionDefinition } from '@deepseek-ai/dsh-agent-preset-registry' +import type {} from '@deepseek-ai/dsh-agent-preset-registry' import { brandString } from '@deepseek-ai/dsh-brand' import { ReasoningEffortId } from '@deepseek-ai/dsh-llm' import type { Session, SessionId } from '@deepseek-ai/dsh-session' @@ -171,8 +171,6 @@ export async function inspectApiSession( /** Owns every operation that may create, resume, or configure a Web Agent. */ export class ApiSessionAgentController { private modelSelection!: ProjectionHandle - private agentPreset!: ProjectionHandle - private turnBoundary!: ProjectionHandle private readonly resumes = new Map>() private readonly claims = new Map>() private readonly selections = new WeakMap() @@ -188,8 +186,6 @@ export class ApiSessionAgentController { async initialize(): Promise { const ctx = this.ctx this.modelSelection = await ctx.sessionProjections.register(modelSelectionProjection) - this.agentPreset = await ctx.sessionProjections.register(agentPresetProjectionDefinition) - this.turnBoundary = await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) ctx.typert.lookups.configure('agent', async (sessionId: SessionId) => { const found = await this.resolveAgent(sessionId) if ('error' in found) throw found.error @@ -373,10 +369,10 @@ export class ApiSessionAgentController { /** * Read the current Agent preset from the Session projection. * @param session - live Session whose projection state is available. - * @returns the current preset, or undefined when the Session has no recorded preset. + * @returns the current preset, or undefined without a preset service or recorded selection. */ presetForSession(session: Session): string | undefined { - return this.agentPreset.stateOf(session) ?? undefined + return this.ctx.get('agentPresets')?.presetForSession(session) ?? undefined } /** @@ -554,7 +550,7 @@ export class ApiSessionAgentController { /** Whether no turn has started, by the predicate the preset registry's `select` enforces. */ private isBlank(session: Session): boolean { - const boundary = this.turnBoundary.stateOf(session) + const boundary = this.ctx.agents.turnBoundaryOf(session) return boundary.openTurnStartSeq === null && boundary.lastTurn === 0 } @@ -601,7 +597,7 @@ export class ApiSessionAgentController { /** * Read the current Agent preset from an all-projections observation. * @param observation - exact Session observation carrying its projection snapshot. - * @returns the current preset, or undefined when the Session has no recorded preset. + * @returns the current preset, or undefined when the capability is absent. */ presetForObservation(observation: SessionObservation): string | undefined { if (observation.projections === undefined) { diff --git a/packages/api/session-controller/tests/agent.host.spec.ts b/packages/api/session-controller/tests/agent.host.spec.ts index 030db3eae1..9dba50297c 100644 --- a/packages/api/session-controller/tests/agent.host.spec.ts +++ b/packages/api/session-controller/tests/agent.host.spec.ts @@ -3,6 +3,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { Context } from '@deepseek-ai/cordis' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Agent } from '@deepseek-ai/dsh-agent' import { agentPresetProjectionDefinition } from '@deepseek-ai/dsh-agent-preset-registry' import SessionStore, { SESSION_FORMAT_VERSION, SessionLogOffset, SessionId } from '@deepseek-ai/dsh-session' @@ -34,6 +35,7 @@ async function harness(): Promise<{ ctx: Context; agents: ApiSessionAgentControl roots.push(ctx) await ctx.plugin(TypertRegistry) await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) installSessionReadTestServices(ctx) await ctx.sessionProjections.register(agentPresetProjectionDefinition) diff --git a/packages/api/session-controller/tests/archived-session-gate.host.spec.ts b/packages/api/session-controller/tests/archived-session-gate.host.spec.ts index 79bad41b7c..d7ab911e57 100644 --- a/packages/api/session-controller/tests/archived-session-gate.host.spec.ts +++ b/packages/api/session-controller/tests/archived-session-gate.host.spec.ts @@ -1,5 +1,6 @@ import { Context } from '@deepseek-ai/cordis' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Agent } from '@deepseek-ai/dsh-agent' import { unsupportedInbox } from '@deepseek-ai/dsh-agent-loop-testkit' import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' @@ -16,6 +17,7 @@ interface Harness { async function harness(): Promise { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const archived: SessionId[] = [] ctx.provide('workspaceRegistry', { get archivedSessionIds() { return archived } } as never) @@ -107,6 +109,7 @@ describe('Archived-session gate: lifetime', () => { it('stops rejecting once the plugin fiber is disposed', async () => { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const archived: SessionId[] = [] ctx.provide('workspaceRegistry', { get archivedSessionIds() { return archived } } as never) diff --git a/packages/api/session-controller/tests/commands-create-fork.host.spec.ts b/packages/api/session-controller/tests/commands-create-fork.host.spec.ts index f43ce3f116..eb2093ea18 100644 --- a/packages/api/session-controller/tests/commands-create-fork.host.spec.ts +++ b/packages/api/session-controller/tests/commands-create-fork.host.spec.ts @@ -1,5 +1,6 @@ import { Context } from '@deepseek-ai/cordis' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Agent, AgentHandle, CreateAgentOptions } from '@deepseek-ai/dsh-agent' import type {} from '@deepseek-ai/dsh-agent-preset-registry' import { createUserMessage } from '@deepseek-ai/dsh-llm' @@ -47,6 +48,7 @@ async function baseContext(): Promise { const ctx = new Context() await ctx.plugin(TypertRegistry) await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) installSessionReadTestServices(ctx) ctx.provide('agentDefaultModel', { diff --git a/packages/api/session-controller/tests/commands-queue-attachment.host.spec.ts b/packages/api/session-controller/tests/commands-queue-attachment.host.spec.ts index ff5f980250..246d512896 100644 --- a/packages/api/session-controller/tests/commands-queue-attachment.host.spec.ts +++ b/packages/api/session-controller/tests/commands-queue-attachment.host.spec.ts @@ -1,6 +1,7 @@ import { RemoteError } from '@deepseek-ai/dsh-typert-protocol' import { Context } from '@deepseek-ai/cordis' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Agent, Inbox, ModelSelectionRef } from '@deepseek-ai/dsh-agent' import { AttachmentError, AttachmentId } from '@deepseek-ai/dsh-attachment' import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment' @@ -36,6 +37,7 @@ async function commandHarness( }> { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) installSessionReadTestServices(ctx) await ctx.sessionProjections.register(subagentIdentityProjectionDefinition) @@ -126,6 +128,7 @@ describe('Session queue commands', () => { it('preserves the cold Agent resolver rejection', async () => { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const error = new RemoteError('session/agent-busy', 'owned by a child', { reason: 'subagent-owned' }) const controller = new SessionCommandController(ctx, { diff --git a/packages/api/session-controller/tests/commands-upload-file.host.spec.ts b/packages/api/session-controller/tests/commands-upload-file.host.spec.ts index 4c7bc2ed5a..aa7422918e 100644 --- a/packages/api/session-controller/tests/commands-upload-file.host.spec.ts +++ b/packages/api/session-controller/tests/commands-upload-file.host.spec.ts @@ -1,5 +1,6 @@ import { Context } from '@deepseek-ai/cordis' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import { createInboxStub } from '@deepseek-ai/dsh-agent-loop-testkit' import type { Agent, ModelSelectionRef } from '@deepseek-ai/dsh-agent' import AttachmentStore, { AttachmentId } from '@deepseek-ai/dsh-attachment' @@ -35,6 +36,7 @@ async function uploadHarness(origin?: 'subagent'): Promise<{ const ctx = new Context() onTestFinished(() => ctx.fiber.dispose()) await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(CommandRuntime) const session = ctx.sessions.create(SESSION, { @@ -321,13 +323,10 @@ describe('Session file uploads', () => { new AbortController().signal, ) await first.controller.prompt(promptRequest([{ type: 'file', receiptId: observed.receiptId }]), USER_SOURCE) - first.ctx.emit('session/event', first.agent.session, { - type: 'user/message', - data: createUserMessage({ - content: [{ type: 'text', text: 'extension event' }], - source: { kind: 'user', rpcId: 1 } as never, - }), - } as never) + first.agent.session.append('user/message', createUserMessage({ + content: [{ type: 'text', text: 'extension event' }], + source: { kind: 'user', rpcId: 1 } as never, + }), { surfaceOp: 'append' }) expect(first.uploads.resolve(first.agent, observed.receiptId)).toEqual(observed.file) first.agent.session.append('user/message', createUserMessage({ content: [{ type: 'text', text: 'observed' }], diff --git a/packages/api/session-controller/tests/controller.host.spec.ts b/packages/api/session-controller/tests/controller.host.spec.ts index 45d3a523ec..cbee7aa031 100644 --- a/packages/api/session-controller/tests/controller.host.spec.ts +++ b/packages/api/session-controller/tests/controller.host.spec.ts @@ -1,5 +1,6 @@ import { Context } from '@deepseek-ai/cordis' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Agent } from '@deepseek-ai/dsh-agent' import { createUserMessage } from '@deepseek-ai/dsh-llm' import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session' @@ -23,6 +24,7 @@ describe('SessionController facade', () => { it('owns Host service methods and publishes Agent lifecycle projections', async () => { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const sessionId = SessionId('controller-session') const header: SessionHeader = { @@ -144,6 +146,7 @@ describe('SessionController facade', () => { async (outcome) => { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const sessionId = SessionId(`background-${outcome}`) const header: SessionHeader = { @@ -200,6 +203,7 @@ describe('SessionController facade', () => { it('waits for an admitted background promotion during teardown', async () => { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const sessionId = SessionId('background-disposal') const header: SessionHeader = { diff --git a/packages/api/session-controller/tests/projection-discovery.host.spec.ts b/packages/api/session-controller/tests/projection-discovery.host.spec.ts index 56dc88fd5d..150b82d415 100644 --- a/packages/api/session-controller/tests/projection-discovery.host.spec.ts +++ b/packages/api/session-controller/tests/projection-discovery.host.spec.ts @@ -47,8 +47,8 @@ async function harness(withPersistence = true) { const ctx = new Context() contexts.push(ctx) await ctx.plugin(SessionStore) - await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.sessionProjections.register(subagentCatalogProjectionDefinition) await ctx.sessionProjections.register(titleProjectionDefinition) if (withPersistence) { 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 dd1a68f610..1544d49c4c 100644 --- a/packages/api/session-controller/tests/session-cold.host.spec.ts +++ b/packages/api/session-controller/tests/session-cold.host.spec.ts @@ -9,6 +9,7 @@ import { describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import SessionStore from '@deepseek-ai/dsh-session' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import { SessionHistoryController } from '@deepseek-ai/dsh-api-session-controller/src/history.ts' import { subagentIdentityProjectionDefinition } from '@deepseek-ai/dsh-subagent/src/projection.ts' import TypertRegistry from '@deepseek-ai/dsh-typert-registry' @@ -81,6 +82,7 @@ describe('sessions.list cold merge', () => { const ctx = new Context() try { await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const historical = header('format-historical', 100) const current = header('format-current', 200) @@ -246,6 +248,7 @@ describe('attached updatedAt tracks human prompts', () => { it('ignores pickup and non-prompt work after the latest human message', async () => { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) await new Promise(resolve => setTimeout(resolve, 0)) @@ -390,6 +393,7 @@ describe('Remote Agent and Session lookup policy', () => { it('keeps queue-item-not-found for a cold session when no persistence backend is composed', async () => { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), @@ -410,6 +414,7 @@ describe('Remote Agent and Session lookup policy', () => { const ctx = new Context() await ctx.plugin(TypertRegistry) await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const sessionId = sid('session-remote-cold') const meta = header(sessionId, 1000) @@ -450,6 +455,7 @@ describe('Remote Agent and Session lookup policy', () => { const ctx = new Context() await ctx.plugin(TypertRegistry) await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const coldId = sid('session-remote-cold-child') const coldMeta = header(coldId, 1000, { @@ -494,6 +500,7 @@ describe('Remote Agent and Session lookup policy', () => { const ctx = new Context() await ctx.plugin(TypertRegistry) await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const sessionId = sid('session-remote-resumed-child') const meta = header(sessionId, 1000) @@ -529,6 +536,7 @@ describe('subagent ownership fence', () => { it('reads a cold child without an Agent and rejects generic resume or adoption', async () => { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const sessionId = sid('session-child') const meta = header('session-child', 1000, { @@ -607,6 +615,7 @@ describe('subagent ownership fence', () => { it('no longer treats a descriptor-only cold child without origin as subagent-owned', async () => { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const sessionId = sid('session-legacy-child') const meta = header('session-legacy-child', 1000, { @@ -644,6 +653,7 @@ describe('subagent ownership fence', () => { it('rejects origin-marked and runtime-owned live children from generic controls', async () => { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const parentSession = ctx.sessions.create(sid('session-parent'), { meta: { cwd: '/proj' } }) const parent = { id: parentSession.id, session: parentSession, status: 'idle', ctx } as Agent @@ -703,6 +713,7 @@ describe('subagent ownership fence', () => { it('does not classify an ordinary fork from an inherited ancestor descriptor', async () => { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const session = ctx.sessions.create(sid('session-ordinary-fork'), { seed: [{ @@ -733,6 +744,7 @@ describe('subagent ownership fence', () => { it('canonicalizes a supplied browser zone on the exact prompt and rejects invalid names', async () => { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const session = ctx.sessions.create(sid('session-browser-zone'), { meta: { cwd: '/proj' } }) const followup = vi.fn() @@ -804,6 +816,7 @@ describe('degenerate composition (no persistence, no factory)', () => { it('lists no cold rows and reports an absent point source as not found', async () => { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const remote = await createSessionTestRemote(ctx, { defaultModelSelection: () => ({ provider: 'p', model: 'm' }), cwd: '/tmp' }) @@ -825,6 +838,7 @@ describe('degenerate composition (no persistence, no factory)', () => { it('maps a missing direct persistence read to session-not-found', async () => { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const inspect = vi.fn() const stat = vi.fn(() => Promise.resolve(undefined)) @@ -851,6 +865,7 @@ describe('sessions.prompt synchronous rejection', () => { it('rejects content without non-whitespace text or an attachment before delivery or Session events', async () => { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const session = ctx.sessions.create(sid('session-empty-prompt')) const followup = vi.fn() @@ -947,6 +962,7 @@ describe('sessions.prompt synchronous rejection', () => { it('maps a synchronous send throw (disposed/invalid input) to agent-busy with the reason attached', async () => { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const session = ctx.sessions.create(sid('session-throwing')) // A live structural stub whose delivery verbs throw synchronously, the @@ -980,6 +996,7 @@ describe('sessions.prompt synchronous rejection', () => { it('classifies a raced cold-resume ID collision as agent-busy', async () => { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const sessionId = sid('race-resume') const meta: SessionHeader = header('race-resume', 1000) diff --git a/packages/api/session-controller/tests/session-fork.host.spec.ts b/packages/api/session-controller/tests/session-fork.host.spec.ts index 87fbd2456e..417112f78e 100644 --- a/packages/api/session-controller/tests/session-fork.host.spec.ts +++ b/packages/api/session-controller/tests/session-fork.host.spec.ts @@ -3,6 +3,7 @@ import { describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import AgentRegistry, { agentEvents } from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Agent, AgentHandle, CreateAgentOptions } from '@deepseek-ai/dsh-agent' import { mountAgentLoopTestDependencies, mountAgentLoopTestHarness } from '@deepseek-ai/dsh-agent-loop-testkit' import { BasicCompactionEngine } from '@deepseek-ai/dsh-compaction-basic' @@ -32,6 +33,7 @@ async function composed(workspaces: readonly Workspace[] = []): Promise const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SystemPrompt, { personaPrefix: '' }) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) installSessionReadTestServices(ctx) ctx.provide('workspaceRegistry', { list: () => workspaces, archivedSessionIds: [] } as never) diff --git a/packages/api/session-controller/tests/session-history-journal.host.spec.ts b/packages/api/session-controller/tests/session-history-journal.host.spec.ts index bf0c163914..f1954ed935 100644 --- a/packages/api/session-controller/tests/session-history-journal.host.spec.ts +++ b/packages/api/session-controller/tests/session-history-journal.host.spec.ts @@ -3,6 +3,7 @@ import { describe, expect, it, onTestFinished, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import AgentRegistry, { type Agent, type AssistantStreamFrame } from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import SessionStore from '@deepseek-ai/dsh-session' import { LlmAttemptId, ToolCallId, createMessage, createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm' import type { MessageSource } from '@deepseek-ai/dsh-llm' @@ -50,6 +51,7 @@ function appendExtension(session: Session, type: string, data: unknown): Session async function harness(): Promise<{ ctx: Context }> { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) installSessionReadTestServices(ctx) return { ctx } diff --git a/packages/api/session-controller/tests/session-list-blank.host.spec.ts b/packages/api/session-controller/tests/session-list-blank.host.spec.ts index 7ea81ea4bc..f433d20c1b 100644 --- a/packages/api/session-controller/tests/session-list-blank.host.spec.ts +++ b/packages/api/session-controller/tests/session-list-blank.host.spec.ts @@ -10,6 +10,7 @@ import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Agent } from '@deepseek-ai/dsh-agent' import SessionStore from '@deepseek-ai/dsh-session' import type { Session } from '@deepseek-ai/dsh-session' @@ -22,6 +23,7 @@ import { createSessionTestRemote, type TestSessionRemote } from './test-remote.t async function harness(): Promise<{ ctx: Context; remote: TestSessionRemote; attach: (session: Session) => Promise }> { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) return { ctx, diff --git a/packages/api/session-controller/tests/session-models.host.spec.ts b/packages/api/session-controller/tests/session-models.host.spec.ts index 6af6983f70..c5619d59aa 100644 --- a/packages/api/session-controller/tests/session-models.host.spec.ts +++ b/packages/api/session-controller/tests/session-models.host.spec.ts @@ -8,6 +8,7 @@ import { describe, expect, it, vi, onTestFinished } from 'vitest' import { Context } from '@deepseek-ai/cordis' import AgentRegistry, { agentEvents } from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Agent } from '@deepseek-ai/dsh-agent' import AttachmentStore from '@deepseek-ai/dsh-attachment' import LlmRuntime, { LlmAdapter, ReasoningEffortId } from '@deepseek-ai/dsh-llm' @@ -96,6 +97,7 @@ async function harness(logged?: { await ctx.plugin(SessionStore) await ctx.plugin(SystemPrompt, { personaPrefix: '' }) await ctx.plugin(LlmRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) ctx.llm.registerAdapter(['deepseek-official'], new CatalogAdapter('DeepSeek', [ { provider: 'deepseek-official', id: 'deepseek-chat', name: 'DeepSeek Chat' }, diff --git a/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts b/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts index a2ab39254a..157d6202bb 100644 --- a/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts +++ b/packages/api/session-controller/tests/session-open-workspace-path.host.spec.ts @@ -2,6 +2,7 @@ import { resolve } from 'node:path' import * as nativeCommand from '@deepseek-ai/dsh-native-command' import { Context } from '@deepseek-ai/cordis' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import SessionStore from '@deepseek-ai/dsh-session' import FsLocal from '@deepseek-ai/dsh-fs-local' import { onTestFinished } from 'vitest' @@ -14,6 +15,7 @@ import { async function context(): Promise { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(FsLocal) onTestFinished(() => ctx.fiber.dispose()) diff --git a/packages/api/session-controller/tests/session-presets.host.spec.ts b/packages/api/session-controller/tests/session-presets.host.spec.ts index 459ea0253a..2308ef77c0 100644 --- a/packages/api/session-controller/tests/session-presets.host.spec.ts +++ b/packages/api/session-controller/tests/session-presets.host.spec.ts @@ -5,8 +5,8 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { Context } from '@deepseek-ai/cordis' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Agent, AgentFactory } from '@deepseek-ai/dsh-agent' -import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent-loop' import { agentPresetProjectionDefinition } from '@deepseek-ai/dsh-agent-preset-registry' import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionSeq } from '@deepseek-ai/dsh-session' import type { Session, SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session' @@ -64,7 +64,9 @@ async function harness(options: HarnessOptions = {}) { const ctx = new Context() contexts.push(ctx) await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) + const preset = await ctx.sessionProjections.register(agentPresetProjectionDefinition) const presets = options.presets const selections: { sessionId: SessionId; agentPreset: string }[] = [] const resumed: SessionId[] = [] @@ -87,6 +89,7 @@ async function harness(options: HarnessOptions = {}) { } ctx.provide('agentPresets', { defaultId: presets[0], + presetForSession: (session: Session) => preset.stateOf(session), resolve: (id?: string) => Promise.resolve(id === undefined ? undefined : options.resolving?.get(id)) .then(() => ({ id: known(id) })), mount: (_ctx: Context, id?: string) => Promise.resolve().then(() => ({ id: usable(id) })), @@ -165,8 +168,6 @@ async function harness(options: HarnessOptions = {}) { defaultModelSelection: () => ({ provider: 'test', model: 'test-model' }), cwd, }) - await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) - const preset = await ctx.sessionProjections.register(agentPresetProjectionDefinition) const presetOf = (id: SessionId): string | null | undefined => { const session = ctx.sessions.get(id) return session === undefined ? undefined : preset.stateOf(session) diff --git a/packages/api/session-controller/tests/session-projections.host.spec.ts b/packages/api/session-controller/tests/session-projections.host.spec.ts index 0b565dfd33..b8848c2084 100644 --- a/packages/api/session-controller/tests/session-projections.host.spec.ts +++ b/packages/api/session-controller/tests/session-projections.host.spec.ts @@ -166,8 +166,8 @@ describe('session.history projections block', () => { it('keeps the v0 numeric seed cut on the wire while logical headers expose only lineage', async () => { const ctx = new Context() await ctx.plugin(SessionStore) - await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) const parent = ctx.sessions.create(SessionId('wire-seed-parent'), { meta: { cwd: '/workspace' } }) parent.append('turn/start', { turn: 1 }) parent.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) @@ -578,8 +578,8 @@ describe('session.list projections column', () => { await ctx.plugin(StorageJson, { root }) await ctx.plugin(StorageDomain, { backend: 'json' }) await ctx.plugin(SessionStore) - await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.sessionProjections.register(privatePromptUnit()) await ctx.plugin(SessionProjectionCache, { writeEveryEvents: 100, writeIntervalMs: 60_000 }) const gateway = await remote(ctx) @@ -633,8 +633,8 @@ describe('session.list projections column', () => { await ctx.plugin(StorageJson, { root }) await ctx.plugin(StorageDomain, { backend: 'json' }) await ctx.plugin(SessionStore) - await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.sessionProjections.register(titleProjectionDefinition) await ctx.plugin(SessionProjectionCache, { writeEveryEvents: 100, writeIntervalMs: 60_000 }) return ctx diff --git a/packages/api/session-controller/tests/session-search.host.spec.ts b/packages/api/session-controller/tests/session-search.host.spec.ts index b96b6e9463..9b14011fbe 100644 --- a/packages/api/session-controller/tests/session-search.host.spec.ts +++ b/packages/api/session-controller/tests/session-search.host.spec.ts @@ -57,8 +57,8 @@ function hit(id: string, index = 0): SessionSearchHit { async function baseContext(): Promise { const ctx = new Context() await ctx.plugin(SessionStore) - await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) return ctx } diff --git a/packages/api/session-controller/tests/session-skills.host.spec.ts b/packages/api/session-controller/tests/session-skills.host.spec.ts index 939ef7a269..5181f37c7b 100644 --- a/packages/api/session-controller/tests/session-skills.host.spec.ts +++ b/packages/api/session-controller/tests/session-skills.host.spec.ts @@ -1,5 +1,6 @@ import { Context } from '@deepseek-ai/cordis' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import { provideWorkingDirectoryFixture } from '@deepseek-ai/dsh-agent-loop-testkit' import type { Agent } from '@deepseek-ai/dsh-agent' import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session' @@ -41,6 +42,7 @@ function observation( async function context(): Promise { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) provideWorkingDirectoryFixture(ctx, '/catalog/default') return ctx diff --git a/packages/api/session-controller/tests/subagent-catalog.host.spec.ts b/packages/api/session-controller/tests/subagent-catalog.host.spec.ts index 7d7d5b384f..9534b8b66d 100644 --- a/packages/api/session-controller/tests/subagent-catalog.host.spec.ts +++ b/packages/api/session-controller/tests/subagent-catalog.host.spec.ts @@ -1,6 +1,7 @@ import { Context } from '@deepseek-ai/cordis' import { provideWorkingDirectoryFixture } from '@deepseek-ai/dsh-agent-loop-testkit' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Agent } from '@deepseek-ai/dsh-agent' import SessionStore, { SESSION_FORMAT_VERSION, SessionSeq, SessionId } from '@deepseek-ai/dsh-session' import type { SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session' @@ -49,6 +50,7 @@ async function bench() { const ctx = new Context() contexts.push(ctx) await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) ctx.provide('sessionPersistence', testSessionPersistence(ctx, { list: () => Promise.resolve([header]), @@ -193,6 +195,7 @@ describe('SessionController subagent catalog', () => { const ctx = new Context() contexts.push(ctx) await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) installSessionReadTestServices(ctx) const controller = await createSessionTestController(ctx, defaults) diff --git a/packages/context/agent-instructions/package.json b/packages/context/agent-instructions/package.json index cd365762ac..6e692037ae 100644 --- a/packages/context/agent-instructions/package.json +++ b/packages/context/agent-instructions/package.json @@ -35,7 +35,6 @@ "@deepseek-ai/dsh-home-paths": "workspace:*", "@deepseek-ai/dsh-llm": "workspace:*", "@deepseek-ai/dsh-session": "workspace:*", - "@deepseek-ai/dsh-session-projection": "workspace:*", "@deepseek-ai/dsh-tools": "workspace:*", "@deepseek-ai/dsh-working-directory": "workspace:*" }, diff --git a/packages/context/agent-instructions/src/index.ts b/packages/context/agent-instructions/src/index.ts index 31070a9102..ec752e0715 100644 --- a/packages/context/agent-instructions/src/index.ts +++ b/packages/context/agent-instructions/src/index.ts @@ -9,14 +9,12 @@ * @module @deepseek-ai/dsh-agent-instructions */ -import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent' import type {} from '@deepseek-ai/dsh-working-directory' import type { Context } from '@deepseek-ai/cordis' import { isDeepStrictEqual } from 'node:util' import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent' import { createUserMessage } from '@deepseek-ai/dsh-llm' import type { Session, UserMessage } from '@deepseek-ai/dsh-session' -import type {} from '@deepseek-ai/dsh-session-projection' import type { ToolExecution, ToolExecutionResult, ToolExecutionToken } from '@deepseek-ai/dsh-tools' import { Config, resolveConfig, workspaceBaselineIdentity, type ResolvedConfig } from './config.ts' import { findProjectRoot, loadBaselineInstructionSet } from './files.ts' @@ -24,6 +22,7 @@ import { trimmedInstructionDigest } from './digest.ts' import { applyInstructionVersionUpdates, baselineInstructionState, + name, reconcileInstructionContext, agentInstructionsMessage, type InstructionVersionCache, @@ -31,8 +30,9 @@ import { } from './state.ts' import { instructionCandidateGroup, instructionScopeKey, scopeForDisplayPath, type AgentInstructionChange } from './render.ts' -export { Config } -export { name } from './state.ts' +export { Config, name } +/** Services required by workspace instruction projection. */ +export const inject = ['agents', 'workingDirectory'] export { discoverBaselineInstructionFiles, loadBaselineInstructions, @@ -44,8 +44,6 @@ export type { export { renderAgentInstructions } from './render.ts' export type { RenderedAgentInstructions, TruncatedInstruction } from './render.ts' -export const inject = ['sessionProjections', 'workingDirectory'] - function visibleBaselineSource( agent: Agent, authorityMessages: readonly UserMessage[], @@ -84,13 +82,7 @@ function filePathFromExecution(exec: ToolExecution): string | undefined { return filePath.length > 0 ? filePath : undefined } -/** Register the agent-instructions plugin after its projections are ready. - * @param ctx - plugin context. - * @param config - workspace instruction policy. - * @returns when the plugin is ready. - */ -export async function apply(ctx: Context, config: Config): Promise { - const turnBoundary = await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) +export function apply(ctx: Context, config: Config): void { const resolved: ResolvedConfig = resolveConfig(config) const instructionVersions: InstructionVersionCache = new WeakMap() const baselinePreparations = new WeakMap { } const stepIsOpen = (session: Session): boolean => { - const boundary = turnBoundary.stateOf(session) + const boundary = ctx.agents.turnBoundaryOf(session) return boundary.openTurnStartSeq !== null && boundary.lastStepBoundary?.kind === 'start' && boundary.lastStepBoundary.seq > boundary.openTurnStartSeq diff --git a/packages/context/agent-instructions/tests/agent-instructions.spec.ts b/packages/context/agent-instructions/tests/agent-instructions.spec.ts index a4df1f6d21..692bc3e388 100644 --- a/packages/context/agent-instructions/tests/agent-instructions.spec.ts +++ b/packages/context/agent-instructions/tests/agent-instructions.spec.ts @@ -12,7 +12,7 @@ import type { ContextFormed } from '@deepseek-ai/dsh-llm' import SessionStore, { SessionId, SessionSeq, type SessionEvent, type SurfaceIntent, type UserMessage } from '@deepseek-ai/dsh-session' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import AgentRegistry, { agentEvents, type Agent } from '@deepseek-ai/dsh-agent' -import AgentLoop, { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent-loop' +import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { FileSystem, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs' import type { FsDirEntry, @@ -239,8 +239,8 @@ class BlockingReadFileSystem extends RecordingFileSystem { async function mountAgentInstructionsPlugin(ctx: Context, config: AgentInstructionsConfig): Promise>> { if (ctx.get('workingDirectory') === undefined) provideWorkingDirectoryFixture(ctx) - if (ctx.get('sessionProjections') === undefined) await ctx.plugin(SessionProjectionRegistry) - await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + if (ctx.get('sessionProjections') === undefined) if (ctx.get('sessionProjections') === undefined) await ctx.plugin(SessionProjectionRegistry) + if (ctx.get('agents') === undefined) await ctx.plugin(AgentRegistry) return ctx.plugin(AgentInstructions, config) } @@ -1248,8 +1248,8 @@ describe('workspace context request injection', () => { } }) - it('requires projections without making the optional filesystem a static dependency', () => { - expect(AgentInstructions.inject).toEqual(['sessionProjections', 'workingDirectory']) + it('requires agents without making the optional filesystem a static dependency', () => { + expect(AgentInstructions.inject).toEqual(['agents', 'workingDirectory']) }) it('does not inject baseline context when no filesystem provider is present', async () => { diff --git a/packages/context/agent-instructions/tsconfig.json b/packages/context/agent-instructions/tsconfig.json index 56237a330b..f8940375c5 100644 --- a/packages/context/agent-instructions/tsconfig.json +++ b/packages/context/agent-instructions/tsconfig.json @@ -23,9 +23,6 @@ { "path": "../../core/session" }, - { - "path": "../../session/session-projection" - }, { "path": "../../core/tools" }, diff --git a/packages/context/file-reference-local/tests/service.spec.ts b/packages/context/file-reference-local/tests/service.spec.ts index 19ec85c3f6..88cef08184 100644 --- a/packages/context/file-reference-local/tests/service.spec.ts +++ b/packages/context/file-reference-local/tests/service.spec.ts @@ -26,9 +26,9 @@ async function harness(): Promise { await ctx.plugin(SessionStore) await ctx.plugin(SystemPrompt, { personaPrefix: '' }) await ctx.plugin(ToolRegistry) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalFileSystem) - await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(WorkingDirectory) return ctx } @@ -64,6 +64,7 @@ describe('LocalFileReferenceService', () => { it('defers guidance until optional prompt and tool services become available', async () => { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) ctx.provide('workingDirectory', { ensure: vi.fn() }) await ctx.plugin(LocalFileReferenceService) diff --git a/packages/context/session-reference/tests/session-reference.spec.ts b/packages/context/session-reference/tests/session-reference.spec.ts index efc8447e90..b896bd5217 100644 --- a/packages/context/session-reference/tests/session-reference.spec.ts +++ b/packages/context/session-reference/tests/session-reference.spec.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { agentEvents, installModelSelection, type Agent, type ModelSelectionRef } from '@deepseek-ai/dsh-agent' +import AgentRegistry, { agentEvents, installModelSelection, type Agent, type ModelSelectionRef } from '@deepseek-ai/dsh-agent' import { CompactionId, compactCheckpointSource } from '@deepseek-ai/dsh-compaction' import LlmRuntime, { createDeveloperMessage, createMessage, createSystemMessage, createToolResultMessage, createUserMessage, LlmError, ToolCallId } from '@deepseek-ai/dsh-llm' import type { ContextFormed } from '@deepseek-ai/dsh-llm' @@ -50,6 +50,7 @@ async function harness(config: Config = {}): Promise { // The live registry and the title unit it hosts: discovery labels an // attached session from its projection cut, never from its log. await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) // Shipped base values: this suite only needs the unit the service registers. await ctx.plugin(SessionTitleService, { fallbackMaxWords: 5, fallbackMaxBytes: 40, maxTitleBytes: 80 }) await ctx.plugin(TestSessionQueryEngine) diff --git a/packages/context/time-context/src/index.ts b/packages/context/time-context/src/index.ts index d1049f4203..e38ff615fa 100644 --- a/packages/context/time-context/src/index.ts +++ b/packages/context/time-context/src/index.ts @@ -49,6 +49,7 @@ const timeContextStateSchema = zod.object({ /** Folded time-context readings. */ type TimeContextProjection = zod.infer +/** The agent registry that owns pre-step processing. */ export const inject = ['agents', 'sessionProjections'] /** Request-preparation clock formatting and append scheduling. Invalid values fail plugin load. */ diff --git a/packages/context/tmux-context/src/index.ts b/packages/context/tmux-context/src/index.ts index fec71d8d69..fea575008d 100644 --- a/packages/context/tmux-context/src/index.ts +++ b/packages/context/tmux-context/src/index.ts @@ -26,6 +26,7 @@ import type {} from '@deepseek-ai/dsh-session-projection' import type { ShellExecutor, ShellRunResult } from '@deepseek-ai/dsh-shell' import { createUserMessage } from '@deepseek-ai/dsh-llm' import type { ContextFormed } from '@deepseek-ai/dsh-llm' + declare module '@deepseek-ai/dsh-llm' { interface MessageSourceMap { /** Location attribution; readers preserve the content without this producer. @@ -39,6 +40,7 @@ declare module '@deepseek-ai/dsh-llm' { /** Cordis plugin name used by loader diagnostics. */ export const name = 'tmux-context' +/** The agent registry that owns pre-step processing. */ export const inject = ['agents', 'sessionProjections'] /** Per-turn tmux-location scheduling. Invalid values fail plugin load. */ diff --git a/packages/context/tmux-context/tests/tmux-context.spec.ts b/packages/context/tmux-context/tests/tmux-context.spec.ts index ae15f20a46..33a7aabeba 100644 --- a/packages/context/tmux-context/tests/tmux-context.spec.ts +++ b/packages/context/tmux-context/tests/tmux-context.spec.ts @@ -93,8 +93,8 @@ async function mount( withBash = false, ): Promise<{ ctx: Context; bash: FakeBash | undefined }> { const ctx = new Context() - await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) let bash: FakeBash | undefined if (withBash) { await ctx.plugin(FakeBash) diff --git a/packages/core/agent-loop/src/agent.ts b/packages/core/agent-loop/src/agent.ts index 65797b988f..20154c10fc 100644 --- a/packages/core/agent-loop/src/agent.ts +++ b/packages/core/agent-loop/src/agent.ts @@ -13,7 +13,6 @@ import type { CancelOptions, InboxTarget, InboxState, - TurnBoundaryProjection, PreStepDecision, RequestErrorAction, } from '@deepseek-ai/dsh-agent' @@ -33,7 +32,6 @@ import type { EpochHeader, RequestContext, Session, SessionId, SessionSeq, TurnE import { canonicalHeader, headerEquals, ToolCallRecovery } from '@deepseek-ai/dsh-session' import { joinContextSections, renderContextSections, renderPrompt } from '@deepseek-ai/dsh-system-prompt' import type { PromptAssembly } from '@deepseek-ai/dsh-system-prompt' -import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection' import type { Context } from '@deepseek-ai/cordis' import { ReactLoopInbox } from './inbox.ts' import { RuntimeContextProjection } from './runtime-context.ts' @@ -126,15 +124,15 @@ export class ReactLoopAgent implements Agent { public readonly id: SessionId, public readonly options: AgentOptions, public readonly session: Session, - inbox: ProjectionHandle, - boundary: ProjectionHandle, + readInbox: () => InboxState, + lastTurn: number, ) { this.requestSurfaceGeneration = session.surface.contentGeneration this.dispatch = agentEvents(loopCtx, this) this.scope = createScope(loopCtx, this) this.ctx = this.scope.ctx - this.inbox = new ReactLoopInbox(() => inbox.stateOf(session), session, this.dispatch) - this.phase = { kind: 'idle', lastTurn: boundary.stateOf(session).lastTurn } + this.inbox = new ReactLoopInbox(readInbox, session, this.dispatch) + this.phase = { kind: 'idle', lastTurn } this.runtimeContext = new RuntimeContextProjection(this.ctx, session) this.systemPrompt = new SystemPromptProjection(session) } diff --git a/packages/core/agent-loop/src/index.ts b/packages/core/agent-loop/src/index.ts index 60d4e2e8c5..54476ac913 100644 --- a/packages/core/agent-loop/src/index.ts +++ b/packages/core/agent-loop/src/index.ts @@ -10,18 +10,16 @@ import { Context, FiberState, Service } from '@deepseek-ai/cordis' import { randomUUID } from 'node:crypto' import z from '@deepseek-ai/schemastery' import { brandString } from '@deepseek-ai/dsh-brand' -import { - turnBoundaryProjectionDefinition, - type Agent, - type AgentFactory, - type AgentHandle, - type AgentOptions, - type AgentSetup, - type CreateAgentOptions, - type ResumeAgentOptions, - type SessionStartSource, - type InboxState, - type TurnBoundaryProjection, +import type { + Agent, + AgentFactory, + AgentHandle, + AgentOptions, + AgentSetup, + CreateAgentOptions, + ResumeAgentOptions, + SessionStartSource, + InboxState, } from '@deepseek-ai/dsh-agent' import { errorChain, ReasoningEffortId } from '@deepseek-ai/dsh-llm' import { interruptedTurnClosers, SessionLogOffset, SessionPreparation } from '@deepseek-ai/dsh-session' @@ -36,8 +34,6 @@ import { inboxProjectionDefinition } from './inbox.ts' import { DEFAULT_MAX_PARALLEL_TOOL_CALLS } from './constants.ts' import type {} from './runtime-context.ts' -export { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent' - /** Fiber states that cannot own or serve a new lifecycle. */ const INACTIVE_STATES: ReadonlySet = new Set([ FiberState.UNLOADING, @@ -313,7 +309,6 @@ export class AgentLoop extends Service implements AgentFactory { /** Plain holder prevents Cordis from re-tracing the factory's dependency context through a caller shadow. */ private readonly runtime: { ctx: Context } private inbox!: ProjectionHandle - private turnBoundary!: ProjectionHandle constructor(ctx: Context, config: Config) { super(ctx, 'agentLoop') @@ -323,8 +318,6 @@ export class AgentLoop extends Service implements AgentFactory { maxParallelToolCalls: config.maxParallelToolCalls, } validateConfiguredAgents(this.config.agents) - // Register only after every config validation above has passed, so a - // rejected constructor leaves no projection unit behind. this.ownership = new FactoryOwnership(ctx.fiber) this.runtime = { ctx } } @@ -332,9 +325,8 @@ export class AgentLoop extends Service implements AgentFactory { protected async [Service.init](): Promise { const ctx = this.runtime.ctx const ownership = this.ownership - // Agents retain readers while fiber disposal removes global projection registrations. + // The inbox remains readable until agent teardown finishes. ctx.effect(() => () => ownership.dispose(), 'agentLoop.transactions()') - this.turnBoundary = await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) this.inbox = await ctx.sessionProjections.register(inboxProjectionDefinition) ctx.effect(() => ctx.agents.setFactory(this), 'agentLoop.setFactory()') ctx.systemPrompt.variable('provider', context => context.agent?.options.provider) @@ -467,6 +459,7 @@ export class AgentLoop extends Service implements AgentFactory { : new Error(`agent "${id}" creation aborted`, { cause: callerSignal.reason }) } const loopCtx = this.runtime.ctx + const lastTurn = loopCtx.agents.turnBoundaryOf(session).lastTurn // Deactivation fuses three owners, each with its own reason: the caller's // cancellation signal, the owner fiber's unload, and factory teardown. @@ -550,10 +543,10 @@ export class AgentLoop extends Service implements AgentFactory { } } const untrack = this.ownership.track(dispose) - const { inbox, turnBoundary } = this + const inbox = this.inbox try { unfollowOwner = ownerCtx.effect(function* () { - const created = machine = new ReactLoopAgent(loopCtx, id, options, session, inbox, turnBoundary) + const created = machine = new ReactLoopAgent(loopCtx, id, options, session, () => inbox.stateOf(session), lastTurn) machineReady.resolve() // Adopt the scope on its original fiber so it cannot unload before teardown. // A loop already unloading during minting owns the provisional scope itself. diff --git a/packages/core/agent-loop/tests/request-freeze.spec.ts b/packages/core/agent-loop/tests/request-freeze.spec.ts index ccb4c89770..b0b4c22ff8 100644 --- a/packages/core/agent-loop/tests/request-freeze.spec.ts +++ b/packages/core/agent-loop/tests/request-freeze.spec.ts @@ -3,7 +3,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import AgentLoop from '@deepseek-ai/dsh-agent-loop' -import { turnBoundaryProjectionDefinition, type Agent } from '@deepseek-ai/dsh-agent' +import type { Agent } from '@deepseek-ai/dsh-agent' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' import { createAssistantMessage, createUserMessage, isAgentLoopRequest } from '@deepseek-ai/dsh-llm' import type { GenerateOptions, ToolSchema } from '@deepseek-ai/dsh-llm' @@ -74,9 +74,9 @@ describe('loop-owned request freezing', () => { expect(Object.isFrozen(userEvent.data.content)).toBe(false) expect(Object.isFrozen(assistantEvent.data.message)).toBe(false) ctx.effect(() => ctx.sessions.enter(session)) + const inbox = await loopCtx.sessionProjections.register(inboxProjectionDefinition) const agent = new ReactLoopAgent(loopCtx, id, { provider: 'mock', model: 'mock' }, session, - await loopCtx.sessionProjections.register(inboxProjectionDefinition), - await loopCtx.sessionProjections.register(turnBoundaryProjectionDefinition)) + () => inbox.stateOf(session), ctx.agents.turnBoundaryOf(session).lastTurn) cleanups.push(async () => { agent.cancel({ kind: 'disposed' }) await agent.whenIdle() @@ -126,8 +126,7 @@ describe('loop-owned request freezing', () => { expect(freeze.mock.calls.filter(([value]) => value === userEvent.data)).toHaveLength(1) expect(freeze.mock.calls.filter(([value]) => value === replacement.data)).toHaveLength(1) const resumed = new ReactLoopAgent(loopCtx, id, { provider: 'mock', model: 'mock' }, session, - await loopCtx.sessionProjections.register(inboxProjectionDefinition), - await loopCtx.sessionProjections.register(turnBoundaryProjectionDefinition)) + () => inbox.stateOf(session), ctx.agents.turnBoundaryOf(session).lastTurn) cleanups.push(async () => { resumed.cancel({ kind: 'disposed' }) await resumed.whenIdle() diff --git a/packages/core/agent-loop/tests/resource-lifecycle.spec.ts b/packages/core/agent-loop/tests/resource-lifecycle.spec.ts index 21fcee429f..eececf3f92 100644 --- a/packages/core/agent-loop/tests/resource-lifecycle.spec.ts +++ b/packages/core/agent-loop/tests/resource-lifecycle.spec.ts @@ -71,7 +71,7 @@ it.each([0, 1, 2])('retains Agent resources until all work exits with %i failed expect(scopeDisposed).toBe(true) expect(abortHandled).toBe(1) expect(projections.checkpoint(agent.session).inbox).toBeUndefined() - expect(projections.checkpoint(agent.session).turnBoundary).toBeUndefined() + expect(projections.checkpoint(agent.session).turnBoundary).toBeDefined() expect(ctx.agents.get(agent.id)).toBeUndefined() expect(ctx.sessions.get(agent.session.id)).toBeUndefined() }) diff --git a/packages/core/agent/README.i18n.yaml b/packages/core/agent/README.i18n.yaml index befdc09642..8956e55244 100644 --- a/packages/core/agent/README.i18n.yaml +++ b/packages/core/agent/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-agent/use-this-package: - en: b48322540e229083 - zh: 05aec0854397f4c3 + en: 197d9f5c8671d103 + zh: c622822905725d5f /deepseek-ai-dsh-agent/use-this-package/create-or-resume-an-agent: en: e305441a5a4a9b44 zh: 3e997b12ce0ce04c @@ -39,8 +39,8 @@ en: 348b9ecd0890c585 zh: d0f73d28d0d2c6f1 /deepseek-ai-dsh-agent/understand-the-implementation/durable-inbox: - en: f6802c7f94f623b3 - zh: cc3885db80765970 + en: 8f3fe9303468d735 + zh: 52162010ae1ed29e /deepseek-ai-dsh-agent/understand-the-implementation/source-map: en: 7e09b94c79dc97ca zh: 3f9c5455c4dc563a diff --git a/packages/core/agent/README.md b/packages/core/agent/README.md index 9da1f06f64..5678895970 100644 --- a/packages/core/agent/README.md +++ b/packages/core/agent/README.md @@ -25,7 +25,7 @@ Use `dsh-agent` to create or resume live agents, send follow-up or steering inpu ## Use this package -Mount `dsh-agent` wherever live agents exist: it provides `ctx.agents` and the `Agent` handle that plugins, UI, hooks, and orchestrators work against. The service is inert until a driver registers a factory — the shipped driver is `dsh-agent-loop`, so the smallest useful composition loads both. +Mount `dsh-agent` with `dsh-session-projection`: it provides `ctx.agents`, the `Agent` handle, and `turnBoundaryOf(session)` for recorded turn and step positions. Creating or resuming an Agent also requires a driver to register a factory; the shipped driver is `dsh-agent-loop`. ### Create or resume an agent @@ -86,7 +86,7 @@ The package is built on one separation: the public `Agent` surface and registry ### Durable inbox -`Agent.inbox` exposes only the structural `Inbox` interface and the projection vocabulary stays in this package. dsh-agent-loop owns the package-internal `ReactLoopInbox` and the standard `inbox` projection; constructing its concrete inbox ensures that the projection registry owns one registration for the durable `agent/inbox/spliced` fold. The registry remains the sole owner of the live `{ 'next-turn', 'next-step' }` state. Reconstruction rejects unsafe or out-of-range splice coordinates and duplicate `MessageId` values across both pending lists and reports the offending event seq. +`Agent.inbox` exposes only the structural `Inbox` interface and the projection vocabulary stays in this package. dsh-agent-loop owns the package-internal `ReactLoopInbox` and the standard `inbox` projection; the loop service acquires one registration for the durable `agent/inbox/spliced` fold and gives each inbox a state-reading callback. The registry remains the sole owner of the live `{ 'next-turn', 'next-step' }` state. Reconstruction rejects unsafe or out-of-range splice coordinates and duplicate `MessageId` values across both pending lists and reports the offending event seq. `Inbox` exposes pending `nextTurn` and `nextStep` messages and mutates them through `append`, `prepend`, `replace`, `remove`, `clear`, and `splice`. Ordinary removals and `clear()` are durable cancellations. At a step boundary, the loop's internal implementation claims pending input through pure deletion splices. Live notifications are deliberately per-message and minimal: `agent/inbox/inserted { message }`, `agent/inbox/claimed { message, turn }`, and `agent/inbox/discarded { message }`. diff --git a/packages/core/agent/README.zh.md b/packages/core/agent/README.zh.md index 0166f96a80..cd394d7657 100644 --- a/packages/core/agent/README.zh.md +++ b/packages/core/agent/README.zh.md @@ -25,7 +25,7 @@ kind: "package-reference" ## 使用本包 -在存在实时 agent 的任何地方挂载 `dsh-agent`:它提供 `ctx.agents` 以及插件、UI、钩子和编排器所面向编程的 `Agent` 句柄。在没有驱动器注册工厂之前,该服务保持惰性——随附驱动器是 `dsh-agent-loop`,因此最小的可用组合需要同时加载两者。 +将 `dsh-agent` 与 `dsh-session-projection` 一起挂载:它提供 `ctx.agents`、`Agent` 句柄,以及读取已记录轮次和步骤位置的 `turnBoundaryOf(session)`。创建或恢复 Agent 还需要驱动器注册工厂;随附驱动器是 `dsh-agent-loop`。 ### 创建或恢复 agent @@ -86,7 +86,7 @@ await handle.agent.whenIdle() ### 持久 inbox -`Agent.inbox` 只暴露结构型 `Inbox` 接口,投影词汇仍位于本包。dsh-agent-loop 持有包内部的 `ReactLoopInbox` 与标准 `inbox` 投影;构造具体 inbox 时会确保投影注册表为持久 `agent/inbox/spliced` fold 持有一份注册。注册表继续作为实时 `{ 'next-turn', 'next-step' }` 状态的唯一所有者。重建过程会拒绝不安全或越界的 splice 坐标,以及跨两份待处理列表重复的 `MessageId`,并报告出错事件的 seq。 +`Agent.inbox` 只暴露结构型 `Inbox` 接口,投影词汇仍位于本包。dsh-agent-loop 持有包内部的 `ReactLoopInbox` 与标准 `inbox` 投影;loop 服务为持久 `agent/inbox/spliced` fold 获取一份注册,并向每个 inbox 提供状态读取回调。注册表继续作为实时 `{ 'next-turn', 'next-step' }` 状态的唯一所有者。重建过程会拒绝不安全或越界的 splice 坐标,以及跨两份待处理列表重复的 `MessageId`,并报告出错事件的 seq。 `Inbox` 暴露待处理的 `nextTurn` 与 `nextStep` 消息,并通过 `append`、`prepend`、`replace`、`remove`、`clear` 与 `splice` 变更它们。普通删除和 `clear()` 都是持久取消。在步骤边界,循环的内部实现会通过纯删除 splice 领取待处理输入。实时通知刻意采用逐消息的最小载荷:`agent/inbox/inserted { message }`、`agent/inbox/claimed { message, turn }` 与 `agent/inbox/discarded { message }`。 diff --git a/packages/core/agent/src/index.ts b/packages/core/agent/src/index.ts index 59b0cc5eba..270aadbef9 100644 --- a/packages/core/agent/src/index.ts +++ b/packages/core/agent/src/index.ts @@ -11,14 +11,16 @@ import { AsyncLocalStorage } from 'node:async_hooks' import { isPromise } from 'node:util/types' import { scopeTarget } from '@deepseek-ai/dsh-scope' import type { Scoped } from '@deepseek-ai/dsh-scope' -import type { SessionEvent, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session' +import type { Session, SessionEvent, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session' +import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection' import { installTurnArchiveAdmission } from './archive-admission.ts' -import type { Agent } from './types.ts' +import type { Agent, TurnBoundaryProjection } from './types.ts' import type { AgentOptions, SessionStartSource } from './runtime-types.ts' +import { turnBoundaryProjectionDefinition } from './projection.ts' export * from './runtime-types.ts' export * from './types.ts' -export { turnBoundaryProjectionDefinition } from './projection.ts' +export type * from './projection.ts' export * from './consumed-work.ts' export * from './model-selection.ts' export { agentCarrier, agentEvents, assembleContextFor, emitAgentEvent } from './dispatch.ts' @@ -243,6 +245,9 @@ interface FactorySlot { * nested lineage that starts an owning-fiber unload is excluded from its own drain. */ export class AgentRegistry extends Service { + static inject = ['sessionProjections'] + + private turnBoundaryProjection!: ProjectionHandle private store = new Map() private factory: FactorySlot | undefined private readonly initiators = new AsyncLocalStorage() @@ -284,6 +289,19 @@ export class AgentRegistry extends Service { installTurnArchiveAdmission(ctx, sessionId => this.get(sessionId)) } + protected async [Service.init](): Promise { + this.turnBoundaryProjection = await this.ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + } + + /** + * Read the turn and step positions recorded in a Session. + * @param session - Session whose committed events are read. + * @returns the current turn and step positions; do not mutate the returned value. + */ + turnBoundaryOf(session: Session): TurnBoundaryProjection { + return this.turnBoundaryProjection.stateOf(session) + } + /** * Read the Agent that initiated the inherited asynchronous driver chain. * Use this optional form for logging, tracing, metrics, or host attribution diff --git a/packages/core/agent/src/types.ts b/packages/core/agent/src/types.ts index 5cbc81df38..2045e9ca11 100644 --- a/packages/core/agent/src/types.ts +++ b/packages/core/agent/src/types.ts @@ -66,15 +66,7 @@ declare module '@deepseek-ai/dsh-session-projection/types' { } } -/** - * Turn and step boundaries folded from one agent session log. - * - * Reader contract: the key is registered by `dsh-agent-loop` and absent - * otherwise. Without agent-loop no turn events exist, so readers treat an - * absent key as "no open turn / no boundaries" — capability absence, not a - * corrupt state. A reader whose behavior has no safe fallback for that - * absence (the step-open decision, for example) may fail loud instead. - */ +/** Turn and step positions recorded in a Session. */ export interface TurnBoundaryProjection { /** Seq of the open turn's `turn/start`, or null between turns. */ readonly openTurnStartSeq: OptionalSessionSeq diff --git a/packages/core/agent/tests/agent-initiator.spec.ts b/packages/core/agent/tests/agent-initiator.spec.ts index c86e0edfcf..7b7a5a13bb 100644 --- a/packages/core/agent/tests/agent-initiator.spec.ts +++ b/packages/core/agent/tests/agent-initiator.spec.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' import { runInNewContext } from 'node:vm' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Agent } from '@deepseek-ai/dsh-agent' import { SessionId } from '@deepseek-ai/dsh-session' @@ -15,6 +16,7 @@ async function harness(): Promise<{ dispose: () => Promise }> { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) const fiber = await ctx.plugin(AgentRegistry) return { ctx, diff --git a/packages/core/agent/tests/agent.spec.ts b/packages/core/agent/tests/agent.spec.ts index 21d90151cb..39516873f0 100644 --- a/packages/core/agent/tests/agent.spec.ts +++ b/packages/core/agent/tests/agent.spec.ts @@ -1,7 +1,8 @@ -import { describe, expect, expectTypeOf, it } from 'vitest' +import { describe, expect, expectTypeOf, it, onTestFinished } from 'vitest' import { Context, Service, symbols } from '@deepseek-ai/cordis' import { Session, SessionId } from '@deepseek-ai/dsh-session' import AgentRegistry, { agentEvents } from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import TypertRegistry from '@deepseek-ai/dsh-typert-registry' import type { @@ -39,8 +40,33 @@ function stubAgent(rawId: string, overrides: Partial = {}): Agent { } describe('AgentRegistry', () => { + it('waits for its projection owner and reads turn positions without a loop', async () => { + const ctx = new Context() + onTestFinished(() => ctx.fiber.dispose()) + const fiber = ctx.plugin(AgentRegistry) + expect(ctx.get('agents')).toBeUndefined() + await ctx.plugin(SessionProjectionRegistry) + await fiber + const session = Session.create(SessionId('turn-positions')) + expect(ctx.agents.turnBoundaryOf(session)).toEqual({ + openTurnStartSeq: null, lastStepStartSeq: null, lastStepBoundary: null, lastTurn: 0, + }) + const turn = session.append('turn/start', { turn: 3 }) + const step = session.append('step/start', { turn: 3, step: 1 }) + expect(ctx.agents.turnBoundaryOf(session)).toEqual({ + openTurnStartSeq: turn.seq, lastStepStartSeq: step.seq, + lastStepBoundary: { kind: 'start', seq: step.seq }, lastTurn: 3, + }) + session.append('turn/end', { turn: 3, reason: { kind: 'completed' } }) + expect(ctx.agents.turnBoundaryOf(session)).toMatchObject({ openTurnStartSeq: null, lastTurn: 3 }) + expect(ctx.sessionProjections.checkpoint(session).turnBoundary).toBeDefined() + await fiber.dispose() + expect(ctx.sessionProjections.checkpoint(session).turnBoundary).toBeUndefined() + }) + it('contributes Agent lookup and scoped Context providers while Typert is live', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) const agentFiber = ctx.plugin(AgentRegistry) await agentFiber await ctx.plugin(TypertRegistry) @@ -69,6 +95,7 @@ describe('AgentRegistry', () => { it('registers exact entries, emits lifecycle events, and unregisters on owner disposal', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const lifecycle: string[] = [] ctx.on('agent/created', ({ agent }) => void lifecycle.push(`created:${agent.id}`)) @@ -88,6 +115,7 @@ describe('AgentRegistry', () => { it('rejects an agent whose registry and session identities differ', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const agent = stubAgent('agent-id', { session: Session.create(SessionId('session-id')) }) @@ -98,6 +126,7 @@ describe('AgentRegistry', () => { it('tracks runtime creator ownership separately from registry order', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const root = stubAgent('root') const child = stubAgent('child') @@ -119,6 +148,7 @@ describe('AgentRegistry', () => { it('rolls an entry back and pairs a partially delivered creation when a listener throws', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const lifecycle: string[] = [] ctx.on('agent/created', ({ agent }) => void lifecycle.push(`created:${agent.id}`)) @@ -132,6 +162,7 @@ describe('AgentRegistry', () => { it('rolls back asynchronous creation rejection and contains every disposal-listener failure', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const warnings: string[] = [] const heard: string[] = [] @@ -155,6 +186,7 @@ describe('AgentRegistry', () => { it('awaits each creation listener before the next listener and registration completion', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const entered = Promise.withResolvers() const release = Promise.withResolvers() @@ -185,6 +217,7 @@ describe('AgentRegistry', () => { it('separates entry from announcement and stale/idempotent detach cannot remove a replacement', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const lifecycle: string[] = [] ctx.on('agent/created', ({ agent }) => void lifecycle.push(`created:${agent.id}`)) @@ -209,6 +242,7 @@ describe('AgentRegistry', () => { it('defers detach requested by a creation listener until that dispatch unwinds', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const order: string[] = [] const agent = stubAgent('reentrant') @@ -304,6 +338,7 @@ describe('AgentRegistry factory seam', () => { it('requires a factory and delegates through the calling context', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await expect(ctx.agents.create({ sessionId: SessionId('s') })).rejects.toThrow(/no agent factory/) const { factory, calls } = stubFactory() @@ -323,6 +358,7 @@ describe('AgentRegistry factory seam', () => { it('keeps the runtime parent in options separately from the caller context', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const { factory, calls } = stubFactory() ctx.agents.setFactory(factory) @@ -337,6 +373,7 @@ describe('AgentRegistry factory seam', () => { it('rejects a second factory and clears the slot with its owner (HMR)', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const owner = await ctx.plugin(Object.assign((inner: Context) => { inner.agents.setFactory(stubFactory().factory) @@ -349,6 +386,7 @@ describe('AgentRegistry factory seam', () => { it('canonicalizes an already traced Service before tracing it for the caller', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const states = new WeakMap() class TracedFactory extends Service implements AgentFactory { diff --git a/packages/core/agent/tests/archive-admission.spec.ts b/packages/core/agent/tests/archive-admission.spec.ts index 0f84de4fe0..8813d4e66f 100644 --- a/packages/core/agent/tests/archive-admission.spec.ts +++ b/packages/core/agent/tests/archive-admission.spec.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' import { Session, SessionId } from '@deepseek-ai/dsh-session' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Agent, AgentCancelCause, AgentStatus, CancelOptions } from '@deepseek-ai/dsh-agent' import type { SessionActivity } from '@deepseek-ai/dsh-workspace' @@ -46,6 +47,7 @@ function stop(ctx: Context, sessionId: SessionId): Promise { describe('Turn archive admission', () => { it('reports the turn family only while the session\'s Agent is running, ahead of later providers', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const agent = await liveAgent(ctx, 'busy', 'running') await liveAgent(ctx, 'quiet', 'idle') @@ -61,6 +63,7 @@ describe('Turn archive admission', () => { it('cancels a running turn with the user cause and without keeping the inbox, and leaves the rest alone', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const running = await liveAgent(ctx, 'stop-me', 'running') const idle = await liveAgent(ctx, 'already-idle', 'idle') @@ -74,6 +77,7 @@ describe('Turn archive admission', () => { it('stops answering once the registry fiber is disposed', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) const fiber = await ctx.plugin(AgentRegistry) const agent = await liveAgent(ctx, 'watched', 'running') expect(await ask(ctx, agent.id)).toEqual([{ kind: 'turn' }]) diff --git a/packages/deliverables/tool-present/package.json b/packages/deliverables/tool-present/package.json index 6876a3ac26..317eba55d7 100644 --- a/packages/deliverables/tool-present/package.json +++ b/packages/deliverables/tool-present/package.json @@ -42,7 +42,6 @@ "@deepseek-ai/dsh-fs": "workspace:*", "@deepseek-ai/dsh-llm": "workspace:*", "@deepseek-ai/dsh-session": "workspace:*", - "@deepseek-ai/dsh-session-projection": "workspace:*", "@deepseek-ai/dsh-tools": "workspace:*", "@deepseek-ai/dsh-working-directory": "workspace:*" }, diff --git a/packages/deliverables/tool-present/src/index.ts b/packages/deliverables/tool-present/src/index.ts index afc6ab82a8..41edef2e12 100644 --- a/packages/deliverables/tool-present/src/index.ts +++ b/packages/deliverables/tool-present/src/index.ts @@ -1,15 +1,14 @@ /** Scoped tool that declares filesystem deliveries in their owning Session. */ -import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent' import type {} from '@deepseek-ai/dsh-working-directory' import type { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import { FsError } from '@deepseek-ai/dsh-fs' import { defineTool, type ToolExecution } from '@deepseek-ai/dsh-tools' import type {} from '@deepseek-ai/dsh-agent' -import type {} from '@deepseek-ai/dsh-session-projection' import type { Session } from '@deepseek-ai/dsh-session' import type { PresentedFile } from './types.ts' +/** Stable Loader identity. */ export const name = 'tool-present' /** Per-call delivery limit. */ @@ -23,16 +22,15 @@ export const Config: z = z.object({ maxFiles: z.number().default(8), }) -export const inject = ['tools', 'fs', 'sessionProjections', 'workingDirectory'] +/** Services used by the scoped delivery tool. */ +export const inject = ['tools', 'fs', 'agents', 'workingDirectory'] /** * Register present with durable file references in its tool result. * @param ctx - agent-scoped services. * @param config - maximum files per call. - * @returns when the plugin is ready. */ -export async function apply(ctx: Context, config: Config): Promise { - const turnBoundary = await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) +export function apply(ctx: Context, config: Config): void { if (!Number.isSafeInteger(config.maxFiles) || config.maxFiles < 1) { throw new Error('present requires a positive integer maxFiles') } @@ -77,7 +75,7 @@ export async function apply(ctx: Context, config: Config): Promise { }, async execute(args, exec) { if (exec.agent === undefined) throw new Error('present requires an agent Session') - const boundary = turnBoundary.stateOf(exec.agent.session) + const boundary = ctx.agents.turnBoundaryOf(exec.agent.session) if (boundary.openTurnStartSeq === null) throw new Error('present requires an open turn') if (args.files.length === 0 || args.files.length > config.maxFiles) throw new Error(`present accepts 1 to ${config.maxFiles} files`) const cwd = await ctx.workingDirectory.ensure(exec.agent, exec.signal) diff --git a/packages/deliverables/tool-present/tests/present.spec.ts b/packages/deliverables/tool-present/tests/present.spec.ts index 94fae8366f..10d1221db4 100644 --- a/packages/deliverables/tool-present/tests/present.spec.ts +++ b/packages/deliverables/tool-present/tests/present.spec.ts @@ -11,7 +11,6 @@ import { createScope, type Scope } from '@deepseek-ai/dsh-scope' import { ToolCallId } from '@deepseek-ai/dsh-llm' import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' -import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent-loop' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { defineTool } from '@deepseek-ai/dsh-tools' import type { PresentedFile } from '../src/types.ts' @@ -59,10 +58,9 @@ async function setup() { cleanups.push(() => ctx.fiber.dispose()) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalFileSystem, { cwd: root }) - await ctx.plugin(SessionProjectionRegistry) - await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) const fiber = ctx.plugin(Present, { maxFiles: 2 }) await fiber const owner = await agent(ctx, root) @@ -154,7 +152,7 @@ it('validates deployment limits before registering the tool', async () => { cleanups.push(() => ctx.fiber.dispose()) await ctx.plugin(SessionProjectionRegistry) for (const config of [{ maxFiles: 0 }, { maxFiles: 1.5 }, { maxFiles: Number.POSITIVE_INFINITY }]) { - await expect(Present.apply(ctx, config)).rejects.toThrow('positive integer maxFiles') + expect(() => { Present.apply(ctx, config) }).toThrow('positive integer maxFiles') } }) diff --git a/packages/deliverables/tool-present/tsconfig.json b/packages/deliverables/tool-present/tsconfig.json index 0e428995b8..24ab42a2f7 100644 --- a/packages/deliverables/tool-present/tsconfig.json +++ b/packages/deliverables/tool-present/tsconfig.json @@ -26,9 +26,6 @@ { "path": "../../core/session" }, - { - "path": "../../session/session-projection" - }, { "path": "../../core/tools" }, diff --git a/packages/experimental/agent-team/src/index.ts b/packages/experimental/agent-team/src/index.ts index fd1ea84a29..a37e4e22b3 100644 --- a/packages/experimental/agent-team/src/index.ts +++ b/packages/experimental/agent-team/src/index.ts @@ -9,8 +9,8 @@ import { steerHostSubagentPrompt } from '@deepseek-ai/dsh-subagent/internal' import { TeamActivity } from './activity.ts' import { errorMessage, TeamError } from './error.ts' import { TeamJournal } from './journal.ts' -import { teamProjectionDefinition } from './projection.ts' import { TeamRuntimeLifecycle } from './lifecycle.ts' +import { teamProjectionDefinition } from './projection.ts' import { resolveActiveMember, TeamRoster } from './roster.ts' import type { TeamMembership } from './roster.ts' import { TeamTaskBoard } from './task-board.ts' diff --git a/packages/experimental/browser-use-runtime/tests/mcp.spec.ts b/packages/experimental/browser-use-runtime/tests/mcp.spec.ts index d9c4465447..fdb9a6a749 100644 --- a/packages/experimental/browser-use-runtime/tests/mcp.spec.ts +++ b/packages/experimental/browser-use-runtime/tests/mcp.spec.ts @@ -148,6 +148,7 @@ it('waits for SystemPrompt and ToolRuntime before reserving the provider', async const ctx = new Context() contexts.push(ctx) await ctx.plugin(BrowserUse) + await ctx.plugin(Projections) await ctx.plugin(Agents) provideWorkingDirectoryFixture(ctx) const tools = ctx.plugin(Tools) diff --git a/packages/experimental/browser-use-runtime/tests/resources.spec.ts b/packages/experimental/browser-use-runtime/tests/resources.spec.ts index f838191d44..7fadb86163 100644 --- a/packages/experimental/browser-use-runtime/tests/resources.spec.ts +++ b/packages/experimental/browser-use-runtime/tests/resources.spec.ts @@ -1,5 +1,6 @@ import { Context } from '@deepseek-ai/cordis' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Agent } from '@deepseek-ai/dsh-agent' import { unsupportedInbox } from '@deepseek-ai/dsh-agent-loop-testkit' import { Session, SessionId } from '@deepseek-ai/dsh-session' @@ -11,6 +12,7 @@ const contexts: Context[] = [] async function fixture() { const ctx = new Context() contexts.push(ctx) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) async function owner(id: string) { const fiber = ctx.plugin(() => {}) diff --git a/packages/experimental/claude-code-mods/README.i18n.yaml b/packages/experimental/claude-code-mods/README.i18n.yaml index 68c4c306d6..f208b59a1b 100644 --- a/packages/experimental/claude-code-mods/README.i18n.yaml +++ b/packages/experimental/claude-code-mods/README.i18n.yaml @@ -9,8 +9,8 @@ en: bf54b493565f1624 zh: 6f28a518f7b0dea8 /deepseek-ai-dsh-experimental-claude-code-mods/summary: - en: e21a08cf7ad99a77 - zh: df7634372a42d9b2 + en: 2b8cee860a1ada68 + zh: 185cf00e144270dc /deepseek-ai-dsh-experimental-claude-code-mods/table-of-contents: en: d152484eb41ac6b4 zh: 09388d293f9be9cb @@ -24,8 +24,8 @@ en: 2b04be6b58e61a74 zh: f4854198d8999d11 /deepseek-ai-dsh-experimental-claude-code-mods/use-this-package/which-members-your-mod-can-call: - en: bf4f9f971aeb1d17 - zh: 83804163ca9039d5 + en: 563ff4127a48a194 + zh: 2acf06dfcfcc6457 /deepseek-ai-dsh-experimental-claude-code-mods/use-this-package/test-a-mod: en: 5848fccf79f41e0a zh: 62bb20c4d7e4d8c2 @@ -78,8 +78,8 @@ en: ca31263f47f654d9 zh: 17d55e6641705627 /deepseek-ai-dsh-experimental-claude-code-mods/known-limitations-and-deferred-work: - en: 99aaa73c8ef9c609 - zh: 46fab480bd2b1706 + en: 42cdaecb2a072efe + zh: 09419c1d94f3d95a /deepseek-ai-dsh-experimental-claude-code-mods/known-limitations-and-deferred-work/dev-note: en: 4d1e26f4cc9c3c1c zh: d80209b56c56b11c diff --git a/packages/experimental/claude-code-mods/README.md b/packages/experimental/claude-code-mods/README.md index 52dc778067..af51499ead 100644 --- a/packages/experimental/claude-code-mods/README.md +++ b/packages/experimental/claude-code-mods/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -Run [Claude Code mods](https://code.claude.com/docs/en/plugins/mods/overview) inside agent runs: wrap a mod's `register(on, options)` with `defineMod`, mount it as a plugin after this bridge, and its hooks guard tool calls, rewrite prompts, add commands and tools, read session facts, and draw a band above the prompt through the same `$`, `e`, `next` chain. The bridge requires `dsh-working-directory` and `dsh-session-projection`, and each `$` call rides a composed harness service. It is an alpha interface-compatibility demonstration: an unserved event is reported at load, an unserved `$` member fails naming the gap, and [the compatibility page](../../../docs/subsystems/claude-code-mods.md) lists every difference. +Run [Claude Code mods](https://code.claude.com/docs/en/plugins/mods/overview) inside agent runs: wrap a mod's `register(on, options)` with `defineMod`, mount it as a plugin after this bridge, and its hooks guard tool calls, rewrite prompts, add commands and tools, read session facts, and draw a band above the prompt through the same `$`, `e`, `next` chain. The bridge requires `dsh-working-directory` and `dsh-agent`, and each `$` call rides a composed harness service. It is an alpha interface-compatibility demonstration: an unserved event is reported at load, an unserved `$` member fails naming the gap, and [the compatibility page](../../../docs/subsystems/claude-code-mods.md) lists every difference. ## Table of Contents @@ -87,7 +87,7 @@ The band is one instance per session, drawn by the first mod in load order whose | `$.command` | `register`, `run`, `list` | `ctx.commands`, scoped to the agent whose event is running | | `$.tool` | `register`, `call`, `list` | `ctx.tools`; a registered tool is named `mcp____`; contexts a tool defers to the next request are injected into the session | | `$.prompt` | `submit` | `agent.followup()` as a `user`-sourced message, framed as a message from the mod unless `asUser`; the `prompt.submit` it raises carries `origin: { kind: 'plugin', name }` | -| `$.session` | `id`, `cwd`, `root`, `model`, `turns`, `messages`, `usage`, `version` | the agent's Session and the `turnBoundary` and `contextPressure` projections; `cwd` reports the committed current directory through `ctx.workingDirectory`; `root` keeps the original project | +| `$.session` | `id`, `cwd`, `root`, `model`, `turns`, `messages`, `usage`, `version` | the agent's Session, `agents.turnBoundaryOf()`, and optional `tokenMeter.contextPressure()`; `cwd` reports the committed current directory through `ctx.workingDirectory`; `root` keeps the original project | | `$.state` | `get`, `set` | memory held for the session, addressed by the `{ plugin, key }` a mod names; a read during `ui.render` subscribes the band | | `$.store` | `get`, `set`, `delete`, `keys` | the `claude_code_mods` storage domain, one JSON object per plugin, 4 MiB | | `$.clock` | `now`, `sleep`, `after`, `every` | timers owned by the session whose event scheduled them; `sleep` rejects when the event is cancelled | @@ -206,7 +206,7 @@ These limits describe where a Claude Code mod behaves differently through this b - **One process, many sessions** — a mod's module-level variables and `$.env.set` writes are shared by every session and plugin in the process, where Claude Code runs one session per process; keep per-session values in `$.state`. `session.start` and `session.end` fire for root agents only. - **No sandbox, no static analysis, no hot reload** — the hooks module runs in-process with Node's globals and the process's full authority (its environment, network, filesystem, and tools); the `$`-only access rule, `claude plugin validate`, type generation, `--plugin-dir` watching, and the in-session mod authoring flow are not implemented. Mount only mods you would run as a plugin. Mounting a mod again re-runs `register` on the same evaluated module, so module-level variables keep their values. - **`turn.complete` text** — the `{ text }` a hook returns reaches the host log, not a line under the answer; `durationMs` counts from the turn's `turn/start`. -- **`$.session.usage`** — `window` is `0` and `percent` absent until the route's context window and a provider usage report are known through the token meter; `rateLimits` is always empty. `$.fs.stat` reports `mtimeMs: 0`. +- **`$.session.usage`** — `window` is `0` and `percent` absent until the route's context window and a provider usage report are known through the token meter; without the token-meter service, token counts are absent and `window` is `0`. `rateLimits` is always empty. `$.fs.stat` reports `mtimeMs: 0`. - **`plugin.json` and `hooks.json`** — not read; `defineMod` carries the identity, and a plugin's settings hooks need `@deepseek-ai/dsh-hooks-claude-code`. diff --git a/packages/experimental/claude-code-mods/README.zh.md b/packages/experimental/claude-code-mods/README.zh.md index 6b11df8418..9b460694d8 100644 --- a/packages/experimental/claude-code-mods/README.zh.md +++ b/packages/experimental/claude-code-mods/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -在智能体运行中执行 [Claude Code 模组](https://code.claude.com/docs/en/plugins/mods/overview):用 `defineMod` 包装模组的 `register(on, options)`,在本桥接之后作为插件挂载,其钩子即可通过同一套 `$`、`e`、`next` 链守卫工具调用、改写提示词、添加命令与工具、读取会话事实,并在提示框上方绘制一条横幅。桥接需要 `dsh-working-directory` 和 `dsh-session-projection`,每次 `$` 调用都落在一个已组合的 harness 服务上。它是 alpha 阶段的接口兼容性演示:未服务的事件在加载时报告,未服务的 `$` 成员以指明缺口的消息失败,[兼容性页面](../../../docs/subsystems/claude-code-mods.zh.md) 列出了全部差异。 +在智能体运行中执行 [Claude Code 模组](https://code.claude.com/docs/en/plugins/mods/overview):用 `defineMod` 包装模组的 `register(on, options)`,在本桥接之后作为插件挂载,其钩子即可通过同一套 `$`、`e`、`next` 链守卫工具调用、改写提示词、添加命令与工具、读取会话事实,并在提示框上方绘制一条横幅。桥接需要 `dsh-working-directory` 和 `dsh-agent`,每次 `$` 调用都落在一个已组合的 harness 服务上。它是 alpha 阶段的接口兼容性演示:未服务的事件在加载时报告,未服务的 `$` 成员以指明缺口的消息失败,[兼容性页面](../../../docs/subsystems/claude-code-mods.zh.md) 列出了全部差异。 ## 目录 @@ -87,7 +87,7 @@ Blast Radius 的 DSH 包装使用当前运行的可执行文件提供 Node 计 | `$.command` | `register`、`run`、`list` | `ctx.commands`,限定在事件所属的智能体 | | `$.tool` | `register`、`call`、`list` | `ctx.tools`;注册的工具名为 `mcp____`;工具推迟到下一请求的上下文会注入会话 | | `$.prompt` | `submit` | `agent.followup()`,作为 `user` 来源的消息,除非 `asUser` 否则套上"来自模组"的框架;它触发的 `prompt.submit` 带 `origin: { kind: 'plugin', name }` | -| `$.session` | `id`、`cwd`、`root`、`model`、`turns`、`messages`、`usage`、`version` | 智能体的 Session 及 `turnBoundary`、`contextPressure` 投影;`cwd` 通过 `ctx.workingDirectory` 报告已提交的当前目录;`root` 保留原始项目 | +| `$.session` | `id`、`cwd`、`root`、`model`、`turns`、`messages`、`usage`、`version` | 智能体的 Session、`agents.turnBoundaryOf()` 和可选的 `tokenMeter.contextPressure()`;`cwd` 通过 `ctx.workingDirectory` 报告已提交的当前目录;`root` 保留原始项目 | | `$.state` | `get`、`set` | 为会话持有的内存,以模组命名的 `{ plugin, key }` 寻址;`ui.render` 期间的读取会让横幅订阅 | | `$.store` | `get`、`set`、`delete`、`keys` | `claude_code_mods` 存储域,每插件一个 JSON 对象,4 MiB | | `$.clock` | `now`、`sleep`、`after`、`every` | 由安排它们的事件所属会话拥有的定时器;事件取消时 `sleep` 拒绝 | @@ -206,7 +206,7 @@ Message from the "" mod: - **一个进程,多个会话** — 模组的模块级变量与 `$.env.set` 写入由进程内所有会话与插件共享,而 Claude Code 每进程一个会话;按会话的值请放在 `$.state`。`session.start` 与 `session.end` 只为根智能体触发。 - **无沙箱、无静态分析、无热重载** — 钩子模块以 Node 全局对象在进程内运行,拥有本进程的全部权限(环境变量、网络、文件系统与工具);仅 `$` 访问规则、`claude plugin validate`、类型生成、`--plugin-dir` 监视与会话内模组编写流程均未实现。只挂载你愿意作为插件运行的模组。再次挂载模组会在同一已求值模块上重新运行 `register`,模块级变量保留其值。 - **`turn.complete` 文本** — 钩子返回的 `{ text }` 到达宿主日志,而非答案下方的一行;`durationMs` 从该轮的 `turn/start` 起算。 -- **`$.session.usage`** — 在 token 计量器得知路由的上下文窗口与提供商用量报告之前,`window` 为 `0` 且无 `percent`;`rateLimits` 始终为空。`$.fs.stat` 报告 `mtimeMs: 0`。 +- **`$.session.usage`** — 在 token 计量器得知路由的上下文窗口与提供商用量报告之前,`window` 为 `0` 且无 `percent`;未挂载 token-meter 服务时,token 数缺席且 `window` 为 `0`。`rateLimits` 始终为空。`$.fs.stat` 报告 `mtimeMs: 0`。 - **`plugin.json` 与 `hooks.json`** — 不读取;`defineMod` 承载身份,插件的设置钩子需要 `@deepseek-ai/dsh-hooks-claude-code`。 diff --git a/packages/experimental/claude-code-mods/package.json b/packages/experimental/claude-code-mods/package.json index 13ea2530f6..e0b8f0de7b 100644 --- a/packages/experimental/claude-code-mods/package.json +++ b/packages/experimental/claude-code-mods/package.json @@ -62,7 +62,6 @@ "@deepseek-ai/dsh-llm": "workspace:*", "@deepseek-ai/dsh-scope": "workspace:*", "@deepseek-ai/dsh-session": "workspace:*", - "@deepseek-ai/dsh-session-projection": "workspace:*", "@deepseek-ai/dsh-storage-domain": "workspace:*", "@deepseek-ai/dsh-subprocess": "workspace:*", "@deepseek-ai/dsh-token-meter": "workspace:*", diff --git a/packages/experimental/claude-code-mods/src/host-ops.ts b/packages/experimental/claude-code-mods/src/host-ops.ts index 7b61a18a4c..84f04d6138 100644 --- a/packages/experimental/claude-code-mods/src/host-ops.ts +++ b/packages/experimental/claude-code-mods/src/host-ops.ts @@ -9,8 +9,7 @@ */ import type { Context } from '@deepseek-ai/cordis' -import type {} from '@deepseek-ai/dsh-session-projection' -import { turnBoundaryProjectionDefinition, type Agent } from '@deepseek-ai/dsh-agent' +import type { Agent } from '@deepseek-ai/dsh-agent' // Type-only: activate the `ctx.get('subprocess')` Context declaration. import type {} from '@deepseek-ai/dsh-subprocess' import type { CommandDefinition } from '@deepseek-ai/dsh-commands' @@ -19,7 +18,7 @@ import { createUserMessage, ToolCallId } from '@deepseek-ai/dsh-llm' import type { ContentBlock, Message } from '@deepseek-ai/dsh-llm' import { scopeOf } from '@deepseek-ai/dsh-scope' import type { WorkingDirectoryService } from '@deepseek-ai/dsh-working-directory' -import { contextPressureProjectionDefinition } from '@deepseek-ai/dsh-token-meter' +import type {} from '@deepseek-ai/dsh-token-meter' import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain' import type { Domain } from '@deepseek-ai/dsh-storage-domain' import { assertObjectJsonSchema } from '@deepseek-ai/dsh-tools' @@ -158,14 +157,12 @@ function sizeOf(info: { size?: number } | undefined): number { } /** - * Build the op table with the required session projection registry and composed harness services. + * Build the op table over the composed harness services. * @param options - the context, aliases, limits, and registration tracking. - * @returns engine behaviors by event name, after both session projections are registered. + * @returns engine behaviors by event name. */ -export async function createHostOps(options: HostOpsOptions): Promise> { +export function createHostOps(options: HostOpsOptions): OpTable { const { ctx, aliases } = options - const turnBoundary = await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) - const contextPressure = await ctx.sessionProjections.register(contextPressureProjectionDefinition) let storeDomain: Promise> | undefined function store(): Promise> { @@ -405,14 +402,14 @@ export async function createHostOps(options: HostOpsOptions): Promise { const agent = requireAgent(context, 'session.turns') - return turnBoundary.stateOf(agent.session).lastTurn + return ctx.agents.turnBoundaryOf(agent.session).lastTurn }, 'session.messages': (_input, context) => sessionMessages(requireAgent(context, 'session.messages').session.deriveMessages(), aliases), 'session.usage': (_input, context): SessionUsage => { const agent = requireAgent(context, 'session.usage') - const pressure = contextPressure.stateOf(agent.session) - const tokens = pressure.pressureTokens - const window = pressure.contextWindow ?? 0 + const pressure = ctx.get('tokenMeter')?.contextPressure(agent.session) + const tokens = pressure?.pressureTokens + const window = pressure?.contextWindow ?? 0 return { startedAt: agent.session.header.createdAt, context: { diff --git a/packages/experimental/claude-code-mods/src/index.ts b/packages/experimental/claude-code-mods/src/index.ts index f715a74d7f..b86713bff7 100644 --- a/packages/experimental/claude-code-mods/src/index.ts +++ b/packages/experimental/claude-code-mods/src/index.ts @@ -10,7 +10,7 @@ * @module @deepseek-ai/dsh-experimental-claude-code-mods */ -import { Service, type Context } from '@deepseek-ai/cordis' +import type { Context } from '@deepseek-ai/cordis' import type {} from '@deepseek-ai/dsh-working-directory' import z from '@deepseek-ai/schemastery' import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent' @@ -208,25 +208,22 @@ export class ClaudeCodeMods extends TypertRemoteService { bandRows: z.number().default(10), }) - // Session initialization requires its directory owner and projection registry; other services are read on demand. - static inject = ['workingDirectory', 'sessionProjections'] + // Session initialization requires its directory and turn-boundary reader; other services are read on demand. + static inject = ['workingDirectory', 'agents'] - private engine!: ModsEngine + private readonly engine: ModsEngine private readonly registrations = new Map void>>() - private surfaces!: SurfaceTable + private readonly surfaces: SurfaceTable - constructor(ctx: Context, private readonly config: Config) { + constructor(ctx: Context, config: Config) { super(ctx, 'claudeCodeMods', { namespace: 'claudeCodeMods' }) - for (const key of ['hookTimeoutMs', 'catchTimeoutMs', 'processTimeoutMs', 'bandColumns', 'bandRows'] as const) { - const value = config[key] - if (value !== undefined) assertPositive(key, value) - } - } - - protected async [Service.init](): Promise { - const { ctx, config } = this // The schema supplies every default; a direct construction passes the fields it needs. const { hookTimeoutMs = 10_000, catchTimeoutMs = 1_000, processTimeoutMs = 30_000, bandColumns = 120, bandRows = 10 } = config + assertPositive('hookTimeoutMs', hookTimeoutMs) + assertPositive('catchTimeoutMs', catchTimeoutMs) + assertPositive('processTimeoutMs', processTimeoutMs) + assertPositive('bandColumns', bandColumns) + assertPositive('bandRows', bandRows) const aliases = createToolNameAliases(config.toolAliases) const callOrigins = new Map() const modCommands = new Set() @@ -262,7 +259,7 @@ export class ClaudeCodeMods extends TypertRemoteService { }, 0).unref() } const modSubmissions = new Map() - const ops = await createHostOps({ + const ops = createHostOps({ ctx, aliases, processTimeoutMs, registrations: this.registrations, callOrigins, modCommands, modTools, redraw, submitted: (messageId, mod) => { modSubmissions.set(messageId, mod) }, }) diff --git a/packages/experimental/claude-code-mods/tests/bridge.spec.ts b/packages/experimental/claude-code-mods/tests/bridge.spec.ts index 7243febfc6..e5d0d19775 100644 --- a/packages/experimental/claude-code-mods/tests/bridge.spec.ts +++ b/packages/experimental/claude-code-mods/tests/bridge.spec.ts @@ -535,7 +535,7 @@ describe('the mods API over harness services', () => { expect(out.session).toEqual({ id: 'a1', cwd: h.workspace, root: h.workspace, model: 'mock', turns: 1, version: MODS_API_VERSION, - usage: { startedAt: agent.session.header.createdAt, context: { tokens: 10, window: 0 }, rateLimits: [] }, + usage: { startedAt: agent.session.header.createdAt, context: { window: 0 }, rateLimits: [] }, messages: [ { role: 'user', text: 'hello', toolUses: [] }, { role: 'user', text: `Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: ${JSON.stringify(h.workspace)}.`, toolUses: [] }, @@ -951,7 +951,7 @@ describe('loading diagnostics and configuration', () => { }) it('is a Service plugin the Loader keeps by its default export', () => { - expect(ClaudeCodeMods.inject).toEqual(['workingDirectory', 'sessionProjections']) + expect(ClaudeCodeMods.inject).toEqual(['workingDirectory', 'agents']) const loader = Object.create(Loader.prototype) as Loader const unwrapped: unknown = loader.unwrapExports({ default: ClaudeCodeMods }) expect(unwrapped).toBe(ClaudeCodeMods) diff --git a/packages/experimental/claude-code-mods/tests/define-mod.spec.ts b/packages/experimental/claude-code-mods/tests/define-mod.spec.ts index 987c2db123..af4b86f1d0 100644 --- a/packages/experimental/claude-code-mods/tests/define-mod.spec.ts +++ b/packages/experimental/claude-code-mods/tests/define-mod.spec.ts @@ -1,5 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { Context, type Fiber } from '@deepseek-ai/cordis' +import AgentRegistry from '@deepseek-ai/dsh-agent' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import { provideWorkingDirectoryFixture } from '@deepseek-ai/dsh-agent-loop-testkit' import ClaudeCodeMods, { defineMod } from '../src/index.ts' @@ -36,6 +37,7 @@ describe('defineMod', () => { const bridge = ctx.plugin(ClaudeCodeMods, {}) expect(ctx.get('claudeCodeMods')).toBeUndefined() await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await bridge const fiber = await ctx.plugin(plugin, { history: 3 }) await fiber.await() @@ -57,6 +59,7 @@ describe('defineMod', () => { fibers.push(ctx.fiber) provideWorkingDirectoryFixture(ctx) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(ClaudeCodeMods, {}) const fiber = ctx.plugin(slow, {}) await vi.waitFor(() => { expect(release).toBeDefined() }) @@ -75,6 +78,7 @@ describe('defineMod', () => { fibers.push(ctx.fiber) provideWorkingDirectoryFixture(ctx) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(ClaudeCodeMods, {}) const fiber = await ctx.plugin(bare, {}) await fiber.await() diff --git a/packages/experimental/claude-code-mods/tests/host-ops.spec.ts b/packages/experimental/claude-code-mods/tests/host-ops.spec.ts index 750a68adf9..2a86de5e61 100644 --- a/packages/experimental/claude-code-mods/tests/host-ops.spec.ts +++ b/packages/experimental/claude-code-mods/tests/host-ops.spec.ts @@ -4,7 +4,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import type { Agent } from '@deepseek-ai/dsh-agent' +import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent' import type { Message } from '@deepseek-ai/dsh-llm' import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' @@ -38,8 +38,9 @@ const mod: LoadedMod = { name: 'unit-mod', version: undefined, root: '/mods/unit async function setup(ctx = new Context()) { contexts.push(ctx) if (ctx.get('sessionProjections') === undefined) await ctx.plugin(SessionProjectionRegistry) + if (ctx.get('agents') === undefined) await ctx.plugin(AgentRegistry) const registrations = new Map void>>() - const ops = await createHostOps({ + const ops = createHostOps({ ctx, aliases: createToolNameAliases(), processTimeoutMs: 5_000, diff --git a/packages/experimental/claude-code-mods/tsconfig.json b/packages/experimental/claude-code-mods/tsconfig.json index a4aad54eac..2b285b1ded 100644 --- a/packages/experimental/claude-code-mods/tsconfig.json +++ b/packages/experimental/claude-code-mods/tsconfig.json @@ -44,9 +44,6 @@ { "path": "../../llm/token-meter" }, - { - "path": "../../session/session-projection" - }, { "path": "../../storage/storage-domain" }, diff --git a/packages/experimental/hooks-claude-code/package.json b/packages/experimental/hooks-claude-code/package.json index 99015f344d..07c4fd4b5c 100644 --- a/packages/experimental/hooks-claude-code/package.json +++ b/packages/experimental/hooks-claude-code/package.json @@ -37,7 +37,6 @@ "@deepseek-ai/dsh-subagent": "workspace:*", "@deepseek-ai/dsh-tools": "workspace:*", "@deepseek-ai/cordis": "workspace:~", - "@deepseek-ai/dsh-session-projection": "workspace:*", "@deepseek-ai/dsh-working-directory": "workspace:*" }, "devDependencies": { diff --git a/packages/experimental/hooks-claude-code/src/index.ts b/packages/experimental/hooks-claude-code/src/index.ts index 1c79429b80..5e2b59d3d8 100644 --- a/packages/experimental/hooks-claude-code/src/index.ts +++ b/packages/experimental/hooks-claude-code/src/index.ts @@ -8,12 +8,10 @@ * @module @deepseek-ai/dsh-hooks-claude-code */ -import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent' import type {} from '@deepseek-ai/dsh-working-directory' import { readFileSync } from 'node:fs' import type { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' -import type {} from '@deepseek-ai/dsh-session-projection' import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent' import { createUserMessage } from '@deepseek-ai/dsh-llm' import type { ContextFormed } from '@deepseek-ai/dsh-llm' @@ -44,11 +42,10 @@ import type { SubagentRunId } from '@deepseek-ai/dsh-subagent' import { parseClaudeCodeConfig, type ClaudeCodeHookConfig } from './config.ts' export const name = 'hooks-claude-code' - -export const inject = ['shell', 'sessionProjections', 'workingDirectory'] - -// `shell` runs hooks and `sessionProjections` supplies turn numbers; the rest +// `shell` runs hooks and `agents` supplies turn numbers; the rest // are read opportunistically via ctx.get so a deployment can omit them. +export const inject = ['shell', 'agents', 'workingDirectory'] + /** Plugin config: where the CC hook config lives + substitution roots. */ export interface Config { /** @@ -101,12 +98,7 @@ function assertPositiveInteger(name: string, value: number): void { } } -/** Register the hooks-claude-code plugin after its projections are ready. - * @param ctx - plugin context. - * @param config - hook configuration path and execution limits. - * @returns when the plugin is ready. - */ -export async function apply(ctx: Context, config: Config): Promise { +export function apply(ctx: Context, config: Config): void { // Validate before config parsing so a bad value cannot be hidden by its early return. const stderrSummaryMaxChars = config.stderrSummaryMaxChars ?? DEFAULT_STDERR_SUMMARY_MAX_CHARS assertPositiveInteger('stderrSummaryMaxChars', stderrSummaryMaxChars) @@ -128,8 +120,6 @@ export async function apply(ctx: Context, config: Config): Promise { return } - const turnBoundary = await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) - // Emit-shaped points run detached, so track their chains; disposal aborts // active hooks and drains continuations before resolving. const detached = createDetachedRuns() @@ -251,7 +241,7 @@ export async function apply(ctx: Context, config: Config): Promise { // --- PreToolUse → PreToolDecision. Matcher subject is the tool name. --- ctx.on('tools/pre-execute', async (exec, next): Promise => { - const turn = lastTurn(exec.agent) + const turn = lastTurn(ctx, exec.agent) const merged = await runPoint('PreToolUse', exec.name, preToolPayload(exec), { ...exec.agent ? { agent: exec.agent } : {}, turn, signal: exec.signal }) if (merged.decision === 'deny') return { kind: 'deny', reason: merged.reason ?? 'blocked by PreToolUse hook' } if (merged.decision === 'ask') return { kind: 'ask', ...merged.reason !== undefined ? { reason: merged.reason } : {} } @@ -260,7 +250,7 @@ export async function apply(ctx: Context, config: Config): Promise { // --- PostToolUse → PostToolDecision. Matcher subject is the tool name. --- ctx.on('tools/post-execute', async (exec, result, next): Promise => { - const turn = lastTurn(exec.agent) + const turn = lastTurn(ctx, exec.agent) const merged = await runPoint('PostToolUse', exec.name, postToolPayload(exec, result), { ...exec.agent ? { agent: exec.agent } : {}, turn, signal: exec.signal }) const context = contextFrom(merged) if (merged.decision === 'deny') { @@ -310,12 +300,6 @@ export async function apply(ctx: Context, config: Config): Promise { detached.track(runPoint('SubagentStop', SUBAGENT_TYPE, subagentPayload('SubagentStop', info, child), { ...child ? { agent: child, directory: 'committed' } : {}, signal: detached.signal }) .catch((error: unknown) => { ctx.logger.warn(`hooks-claude-code: SubagentStop hook failed: ${String(error)}`) })) }) - - function lastTurn(agent: Agent | undefined): number { - if (!agent) return 0 - const boundary = turnBoundary.stateOf(agent.session) - return boundary.lastTurn - } } /** @@ -329,6 +313,12 @@ const SUBAGENT_TYPE = 'general-purpose' // --- Per-event stdin payloads (the CC DIALECT shape). Field names match CC's // hook input schema; this is the part a bridge owns. --- +/** The last open turn number in the agent's log, or 0 without an agent. */ +function lastTurn(ctx: Context, agent: Agent | undefined): number { + if (!agent) return 0 + return ctx.agents.turnBoundaryOf(agent.session).lastTurn +} + /** Flatten content blocks to the text a hook payload carries (the common case). */ function blocksToText(content: ContentBlock[]): string { return content.filter((b): b is Extract => b.type === 'text').map(b => b.text).join('') diff --git a/packages/experimental/hooks-claude-code/tests/bridge.spec.ts b/packages/experimental/hooks-claude-code/tests/bridge.spec.ts index f6b012bc28..3096a8459f 100644 --- a/packages/experimental/hooks-claude-code/tests/bridge.spec.ts +++ b/packages/experimental/hooks-claude-code/tests/bridge.spec.ts @@ -467,12 +467,12 @@ describe('hooks-claude-code bridge — load resilience', () => { // "cannot get property … without inject". Guard the shape directly. expect('default' in HooksClaude).toBe(false) expect(HooksClaude.name).toBe('hooks-claude-code') - expect(HooksClaude.inject).toEqual(['shell', 'sessionProjections', 'workingDirectory']) + expect(HooksClaude.inject).toEqual(['shell', 'agents', 'workingDirectory']) const loader = Object.create(Loader.prototype) as Loader const unwrapped = loader.unwrapExports(HooksClaude) as Record expect(unwrapped).toBe(HooksClaude) expect(unwrapped.name).toBe('hooks-claude-code') - expect(unwrapped.inject).toEqual(['shell', 'sessionProjections', 'workingDirectory']) + expect(unwrapped.inject).toEqual(['shell', 'agents', 'workingDirectory']) expect(typeof unwrapped.apply).toBe('function') }) }) diff --git a/packages/experimental/hooks-claude-code/tests/coverage-cases.ts b/packages/experimental/hooks-claude-code/tests/coverage-cases.ts index bea498dae6..82ea12294f 100644 --- a/packages/experimental/hooks-claude-code/tests/coverage-cases.ts +++ b/packages/experimental/hooks-claude-code/tests/coverage-cases.ts @@ -381,7 +381,7 @@ export function defineCoverageCases(group: CoverageGroup): void { // Direct apply with only configPath — bypasses schemastery's defaults, so // the bridge must run on the raw minimal config (the per-hook timeout is // the protocol lib's reference default, not a config knob). - await HooksClaude.apply(ctx, { configPath: join(d, 'hooks.json') }) + HooksClaude.apply(ctx, { configPath: join(d, 'hooks.json') }) ctx.llm.registerAdapter(['mock'], adapter) const agent = await ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })) diff --git a/packages/experimental/hooks-codex/package.json b/packages/experimental/hooks-codex/package.json index 61ddc09cbd..d40fd4d55c 100644 --- a/packages/experimental/hooks-codex/package.json +++ b/packages/experimental/hooks-codex/package.json @@ -36,7 +36,6 @@ "@deepseek-ai/dsh-session": "workspace:*", "@deepseek-ai/dsh-tools": "workspace:*", "@deepseek-ai/cordis": "workspace:~", - "@deepseek-ai/dsh-session-projection": "workspace:*", "@deepseek-ai/dsh-working-directory": "workspace:*" }, "devDependencies": { diff --git a/packages/experimental/hooks-codex/src/index.ts b/packages/experimental/hooks-codex/src/index.ts index 356e64393d..2d9b3c3daf 100644 --- a/packages/experimental/hooks-codex/src/index.ts +++ b/packages/experimental/hooks-codex/src/index.ts @@ -11,12 +11,10 @@ // Each dialect bridge keeps its complete dependency list visible at the entry // point; a cross-package facade for imports alone would add indirection. /* jscpd:ignore-start */ -import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent' import type {} from '@deepseek-ai/dsh-working-directory' import { readFileSync } from 'node:fs' import type { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' -import type {} from '@deepseek-ai/dsh-session-projection' import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent' import { createUserMessage } from '@deepseek-ai/dsh-llm' import type { ContextFormed } from '@deepseek-ai/dsh-llm' @@ -28,7 +26,7 @@ declare module '@deepseek-ai/dsh-llm' { import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm' import type { UserMessage } from '@deepseek-ai/dsh-session' -import type { PostToolDecision, PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools' +import type { PostToolDecision, PreToolDecision, ToolExecution, ToolExecutionResult } from '@deepseek-ai/dsh-tools' import { appendHookInvoked, appendHookResult, @@ -42,12 +40,10 @@ import { type MergedHookOutcome, } from '@deepseek-ai/dsh-hook-protocol' import { parseCodexConfig, type CodexHookConfig } from './config.ts' +/* jscpd:ignore-end */ export const name = 'hooks-codex' - -export const inject = ['shell', 'sessionProjections', 'workingDirectory'] - -/* jscpd:ignore-end */ +export const inject = ['shell', 'agents', 'workingDirectory'] /** Plugin config: where the Codex hooks.json lives + the model name for payloads. */ export interface Config { @@ -87,12 +83,7 @@ function assertPositiveInteger(name: string, value: number): void { } } -/** Register the hooks-codex plugin after its projections are ready. - * @param ctx - plugin context. - * @param config - hook configuration path and execution limits. - * @returns when the plugin is ready. - */ -export async function apply(ctx: Context, config: Config): Promise { +export function apply(ctx: Context, config: Config): void { // Validate before config parsing so a bad value cannot be hidden by its early return. const stderrSummaryMaxChars = config.stderrSummaryMaxChars ?? DEFAULT_STDERR_SUMMARY_MAX_CHARS assertPositiveInteger('stderrSummaryMaxChars', stderrSummaryMaxChars) @@ -110,13 +101,8 @@ export async function apply(ctx: Context, config: Config): Promise { return } - const turnBoundary = await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) const model = config.model ?? '' - function lastTurn(agent: Agent | undefined): number { - return agent === undefined ? 0 : turnBoundary.stateOf(agent.session).lastTurn - } - // SessionStart is the one emit-shaped (detached) point Codex has: track its // run chains so disposal aborts a still-running hook process and drains the // continuation (docs/defensive-patterns/general.md: dispose must reach quiescence). @@ -208,6 +194,7 @@ export async function apply(ctx: Context, config: Config): Promise { .catch((error: unknown) => { ctx.logger.warn(`hooks-codex: SessionStart hook failed: ${String(error)}`) }) detached.track(run) await run + /* jscpd:ignore-end */ }) // UserPromptSubmit → PreStepDecision. Codex supports reject, not rewrite or ask. @@ -238,26 +225,18 @@ export async function apply(ctx: Context, config: Config): Promise { // PreToolUse → PreToolDecision. Codex blocks only (no allow/ask honored). ctx.on('tools/pre-execute', async (exec, next): Promise => { - const turn = lastTurn(exec.agent) - const payload = toolPayload(exec, 'PreToolUse', model, turn) - const merged = await runPoint('PreToolUse', exec.name, payload, { - ...exec.agent ? { agent: exec.agent } : {}, turn, signal: exec.signal, - }) + const turn = lastTurn(ctx, exec.agent) + const merged = await runPoint('PreToolUse', exec.name, preToolPayload(ctx, exec, model), { ...exec.agent ? { agent: exec.agent } : {}, turn, signal: exec.signal }) + /* jscpd:ignore-end */ if (merged.decision === 'deny') return { kind: 'deny', reason: merged.reason ?? 'blocked by PreToolUse hook' } return next() }) // PostToolUse → PostToolDecision (block with feedback, or attach context). ctx.on('tools/post-execute', async (exec, result, next): Promise => { - const turn = lastTurn(exec.agent) - const payload = { - ...toolPayload(exec, 'PostToolUse', model, turn), - tool_response: blocksToText(result.content), - } + const turn = lastTurn(ctx, exec.agent) /* jscpd:ignore-start */ - const merged = await runPoint('PostToolUse', exec.name, payload, { - ...exec.agent ? { agent: exec.agent } : {}, turn, signal: exec.signal, - }) + const merged = await runPoint('PostToolUse', exec.name, postToolPayload(ctx, exec, result, model), { ...exec.agent ? { agent: exec.agent } : {}, turn, signal: exec.signal }) const context = contextFrom(merged) if (merged.decision === 'deny') { return { kind: 'block', feedback: [{ type: 'text', text: merged.reason ?? 'blocked by PostToolUse hook' }], ...context ? { additionalContexts: [context] } : {} } @@ -281,13 +260,8 @@ export async function apply(ctx: Context, config: Config): Promise { // avoid continuing the same turn indefinitely. It is always false here, so an // unconditionally blocking hook force-continues every step until it self-limits. ctx.on('agent/turn-stopping', async ({ agent, turn, signal }): Promise => { - const payload = { - ...base(agent, 'Stop', model), - turn_id: String(lastTurn(agent)), - stop_hook_active: false, - last_assistant_message: null, - } - const merged = await runPoint('Stop', '', payload, { agent, turn, signal }) + const merged = await runPoint('Stop', '', { ...turnBase(ctx, agent, 'Stop', model), stop_hook_active: false, last_assistant_message: null }, { agent, turn, signal }) + /* jscpd:ignore-end */ if (merged.decision === 'deny') { // A blocking Stop hook forces continuation; a block with no reason (exit 2, // empty stderr) still forces it — fall back to a generic steering line @@ -304,6 +278,10 @@ export async function apply(ctx: Context, config: Config): Promise { // These small payload helpers intentionally remain next to the dialect shape; // sharing them would pull bridge-only agent/LLM dependencies into hook-protocol. /* jscpd:ignore-start */ +function lastTurn(ctx: Context, agent: Agent | undefined): number { + if (!agent) return 0 + return ctx.agents.turnBoundaryOf(agent.session).lastTurn +} function blocksToText(content: ContentBlock[]): string { return content.filter((b): b is Extract => b.type === 'text').map(b => b.text).join('') @@ -323,20 +301,9 @@ function base(agent: Agent | undefined, event: string, model: string): Record { - return { - ...base(exec.agent, event, model), - turn_id: String(turn), - tool_name: exec.name, - tool_input: { command: commandOf(exec.arguments) }, - tool_use_id: exec.callId, - } +/** Base + turn_id, for the turn-scoped events (PreToolUse/PostToolUse/UserPromptSubmit/Stop). */ +function turnBase(ctx: Context, agent: Agent | undefined, event: string, model: string): Record { + return { ...base(agent, event, model), turn_id: String(lastTurn(ctx, agent)) } } /** Extract a `command` string from a tool call's parsed arguments, else ''. */ @@ -347,3 +314,15 @@ function commandOf(args: unknown): string { } return '' } + +function preToolPayload(ctx: Context, exec: ToolExecution, model: string): Record { + // `tool_name` is the REAL tool name (matching the `exec.name` matcher subject); + // a hardcoded constant would disagree with what the matcher tests and make a + // config's tool matcher never fire. `tool_input` keeps Codex's `{ command }` + // shape (its shell payload), derived from the call's `command` arg when present. + return { ...turnBase(ctx, exec.agent, 'PreToolUse', model), tool_name: exec.name, tool_input: { command: commandOf(exec.arguments) }, tool_use_id: exec.callId } +} + +function postToolPayload(ctx: Context, exec: ToolExecution, result: ToolExecutionResult, model: string): Record { + return { ...turnBase(ctx, exec.agent, 'PostToolUse', model), tool_name: exec.name, tool_input: { command: commandOf(exec.arguments) }, tool_use_id: exec.callId, tool_response: blocksToText(result.content) } +} diff --git a/packages/experimental/hooks-codex/tests/bridge.spec.ts b/packages/experimental/hooks-codex/tests/bridge.spec.ts index 242eb91819..8089e0c092 100644 --- a/packages/experimental/hooks-codex/tests/bridge.spec.ts +++ b/packages/experimental/hooks-codex/tests/bridge.spec.ts @@ -287,12 +287,12 @@ describe('hooks-codex bridge', () => { it('has the namespace-plugin export shape (no stray default) so the Loader keeps name/inject/apply', () => { expect('default' in HooksCodex).toBe(false) expect(HooksCodex.name).toBe('hooks-codex') - expect(HooksCodex.inject).toEqual(['shell', 'sessionProjections', 'workingDirectory']) + expect(HooksCodex.inject).toEqual(['shell', 'agents', 'workingDirectory']) const loader = Object.create(Loader.prototype) as Loader const unwrapped = loader.unwrapExports(HooksCodex) as Record expect(unwrapped).toBe(HooksCodex) expect(unwrapped.name).toBe('hooks-codex') - expect(unwrapped.inject).toEqual(['shell', 'sessionProjections', 'workingDirectory']) + expect(unwrapped.inject).toEqual(['shell', 'agents', 'workingDirectory']) expect(typeof unwrapped.apply).toBe('function') }) }) diff --git a/packages/experimental/hooks-codex/tests/coverage-cases.ts b/packages/experimental/hooks-codex/tests/coverage-cases.ts index 2ab643a50d..f3458daeab 100644 --- a/packages/experimental/hooks-codex/tests/coverage-cases.ts +++ b/packages/experimental/hooks-codex/tests/coverage-cases.ts @@ -326,7 +326,7 @@ export function defineCoverageCases(groups: CoverageGroup | readonly CoverageGro await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000 }) ctx.logger.warn = warn as never // Direct apply (schema bypass) → the `model ?? ''` fallback is exercised. - await HooksCodex.apply(ctx, { configPath: join(d, 'hooks.json') }) + HooksCodex.apply(ctx, { configPath: join(d, 'hooks.json') }) ctx.llm.registerAdapter(['mock'], adapter) const agent = await ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) agent.followup(createUserMessage({ content: [{ type: 'text', text: 'go' }], source: { kind: 'user' } })); await waitForIdle(ctx, agent) diff --git a/packages/experimental/session-title-all-prompts-llm/tests/provider.spec.ts b/packages/experimental/session-title-all-prompts-llm/tests/provider.spec.ts index 282662b1a0..648fd59039 100644 --- a/packages/experimental/session-title-all-prompts-llm/tests/provider.spec.ts +++ b/packages/experimental/session-title-all-prompts-llm/tests/provider.spec.ts @@ -1,10 +1,10 @@ +import AgentRegistry from '@deepseek-ai/dsh-agent' import { Context } from '@deepseek-ai/cordis' import { describe, expect, it, onTestFinished } from 'vitest' import LlmRuntime, { createUserMessage, LlmAdapter, ReasoningEffortId, ToolCallId } from '@deepseek-ai/dsh-llm' import type { GenerateOptions, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm' import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' -import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent-loop' import SessionTitleService, { SessionTitleProviderId, type SessionTitleUserMessage, @@ -92,7 +92,7 @@ async function harness( await ctx.plugin(LlmRuntime) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionTitleService, TITLE_CONFIG) const adapter = new RecordingAdapter(script, reasoningEfforts) ctx.llm.registerAdapter(['title-route'], adapter) @@ -215,7 +215,7 @@ describe('all-messages LLM title provider', () => { await ctx.plugin(LlmRuntime) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + await ctx.plugin(AgentRegistry) // A one-byte fallback cap cannot derive a title from the multibyte message, // so the service supplies no currentTitle to the provider. await ctx.plugin(SessionTitleService, { fallbackMaxWords: 5, fallbackMaxBytes: 1, maxTitleBytes: 80 }) diff --git a/packages/experimental/tool-session-query/package.json b/packages/experimental/tool-session-query/package.json index 32f3903015..51d9143100 100644 --- a/packages/experimental/tool-session-query/package.json +++ b/packages/experimental/tool-session-query/package.json @@ -31,7 +31,6 @@ "@deepseek-ai/dsh-agent": "workspace:*", "@deepseek-ai/dsh-llm": "workspace:*", "@deepseek-ai/dsh-session": "workspace:*", - "@deepseek-ai/dsh-session-projection": "workspace:*", "@deepseek-ai/dsh-session-query": "workspace:*", "@deepseek-ai/dsh-system-prompt": "workspace:*", "@deepseek-ai/dsh-timeout": "workspace:*", diff --git a/packages/experimental/tool-session-query/src/index.ts b/packages/experimental/tool-session-query/src/index.ts index 2337b43402..1e1d2f38d2 100644 --- a/packages/experimental/tool-session-query/src/index.ts +++ b/packages/experimental/tool-session-query/src/index.ts @@ -5,7 +5,6 @@ */ import type { Context } from '@deepseek-ai/cordis' -import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent' import z from '@deepseek-ai/schemastery' import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout' import { defineTool } from '@deepseek-ai/dsh-tools' @@ -13,9 +12,11 @@ import { toolInput } from './input.ts' import { operations } from './operations.ts' import { presentation } from './presentation.ts' +/** Cordis plugin name used by Loader diagnostics. */ export const name = 'tool-session-query' -export const inject = ['tools', 'systemPrompt', 'sessionQuery', 'sessionProjections'] +/** Capability services required by the model-facing consumer. */ +export const inject = ['tools', 'systemPrompt', 'sessionQuery', 'agents'] /** Default maximum number of authorized search hits returned by one call. */ export const DEFAULT_MAX_SEARCH_RESULTS = 100 @@ -52,14 +53,8 @@ const PROMPT_TEXT = + 'events in one session. Search results are cursor-free and workspace-scoped. Follow a useful hit with ' + 'session_trace, session_event_trace, or session_event_read when you need lineage, relationships, or exact data.' -/** - * Register all five tools and their shared model guidance. - * @param ctx - owning plugin context. - * @param config - search limits. - * @returns when the tools and their projection reader are ready. - */ -export async function apply(ctx: Context, config: Config): Promise { - const turnBoundary = await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) +/** Register all five tools and their shared model guidance. */ +export function apply(ctx: Context, config: Config): void { const resolved = resolveConfig(config) ctx.systemPrompt.section({ name: 'tool:session-query', @@ -73,7 +68,7 @@ export async function apply(ctx: Context, config: Config): Promise { parameters: toolInput.sessionSearchParameters, output: TEXT_OUTPUT, timeoutMs: resolved.searchTimeoutMs, - execute: (args, exec) => operations.executeSessionSearch(ctx, turnBoundary, args, exec, resolved.maxSearchResults), + execute: (args, exec) => operations.executeSessionSearch(ctx, args, exec, resolved.maxSearchResults), presentCall: presentation.presentSessionSearchCall, })) @@ -83,7 +78,7 @@ export async function apply(ctx: Context, config: Config): Promise { parameters: toolInput.eventSearchParameters, output: TEXT_OUTPUT, timeoutMs: resolved.searchTimeoutMs, - execute: (args, exec) => operations.executeEventSearch(ctx, turnBoundary, args, exec, resolved.maxSearchResults), + execute: (args, exec) => operations.executeEventSearch(ctx, args, exec, resolved.maxSearchResults), presentCall: presentation.presentEventSearchCall, })) @@ -93,7 +88,7 @@ export async function apply(ctx: Context, config: Config): Promise { parameters: toolInput.targetSessionParameter, output: TEXT_OUTPUT, isConcurrencySafe: () => true, - execute: (args, exec) => operations.executeSessionTrace(ctx, turnBoundary, args, exec), + execute: (args, exec) => operations.executeSessionTrace(ctx, args, exec), presentCall: presentation.presentSessionTraceCall, })) @@ -106,7 +101,7 @@ export async function apply(ctx: Context, config: Config): Promise { }, output: TEXT_OUTPUT, isConcurrencySafe: () => true, - execute: (args, exec) => operations.executeEventTrace(ctx, turnBoundary, args, exec), + execute: (args, exec) => operations.executeEventTrace(ctx, args, exec), presentCall: args => presentation.presentEventTargetCall('Trace event', args), })) @@ -121,7 +116,7 @@ export async function apply(ctx: Context, config: Config): Promise { }, output: TEXT_OUTPUT, isConcurrencySafe: () => true, - execute: (args, exec) => operations.executeEventRead(ctx, turnBoundary, args, exec), + execute: (args, exec) => operations.executeEventRead(ctx, args, exec), presentCall: args => presentation.presentEventTargetCall('Read event', args), })) } diff --git a/packages/experimental/tool-session-query/src/operations.ts b/packages/experimental/tool-session-query/src/operations.ts index 2727447537..1d10d0a34b 100644 --- a/packages/experimental/tool-session-query/src/operations.ts +++ b/packages/experimental/tool-session-query/src/operations.ts @@ -5,8 +5,6 @@ */ import type { Context } from '@deepseek-ai/cordis' -import type { TurnBoundaryProjection } from '@deepseek-ai/dsh-agent' -import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection' import { HarnessError } from '@deepseek-ai/dsh-llm' import { SessionSeq } from '@deepseek-ai/dsh-session' import type { SessionId } from '@deepseek-ai/dsh-session' @@ -56,12 +54,11 @@ interface SearchCollection { async function executeSessionSearch( ctx: Context, - turnBoundary: ProjectionHandle, args: SessionSearchArgs, exec: ToolRunContext, maxResults: number, ): Promise { - const caller = workspaceAccess.callerOf(exec, turnBoundary) + const caller = workspaceAccess.callerOf(exec, ctx) const cwd = caller.header.cwd if (cwd === undefined) { throw new HarnessError( @@ -119,12 +116,11 @@ async function executeSessionSearch( async function executeEventSearch( ctx: Context, - turnBoundary: ProjectionHandle, args: EventSearchArgs, exec: ToolRunContext, maxResults: number, ): Promise { - const caller = workspaceAccess.callerOf(exec, turnBoundary) + const caller = workspaceAccess.callerOf(exec, ctx) const sessionId = workspaceAccess.targetId(args, caller) await workspaceAccess.authorizeTarget(ctx, caller, sessionId, exec.signal) const query = toolInput.normalizeQuery(args.query) @@ -168,11 +164,10 @@ async function executeEventSearch( async function executeSessionTrace( ctx: Context, - turnBoundary: ProjectionHandle, args: SessionTargetArgs, exec: ToolRunContext, ): Promise { - const caller = workspaceAccess.callerOf(exec, turnBoundary) + const caller = workspaceAccess.callerOf(exec, ctx) const sessionId = workspaceAccess.targetId(args, caller) await workspaceAccess.authorizeTarget(ctx, caller, sessionId, exec.signal) const trace = await serviceBoundary.call(ctx, exec.signal, 'session lineage trace', () => @@ -201,13 +196,12 @@ async function executeSessionTrace( async function executeEventTrace( ctx: Context, - turnBoundary: ProjectionHandle, args: EventTargetArgs, exec: ToolRunContext, ): Promise { toolInput.assertNonNegativeSafeInteger('seq', args.seq) const seq = SessionSeq(args.seq) - const caller = workspaceAccess.callerOf(exec, turnBoundary) + const caller = workspaceAccess.callerOf(exec, ctx) const sessionId = workspaceAccess.targetId(args, caller) await workspaceAccess.authorizeTarget(ctx, caller, sessionId, exec.signal) const trace = await serviceBoundary.call(ctx, exec.signal, 'event trace', () => @@ -219,7 +213,6 @@ async function executeEventTrace( async function executeEventRead( ctx: Context, - turnBoundary: ProjectionHandle, args: EventReadArgs, exec: ToolRunContext, ): Promise { @@ -227,7 +220,7 @@ async function executeEventRead( const seq = SessionSeq(args.seq) if (args.before !== undefined) toolInput.assertNonNegativeSafeInteger('before', args.before) if (args.after !== undefined) toolInput.assertNonNegativeSafeInteger('after', args.after) - const caller = workspaceAccess.callerOf(exec, turnBoundary) + const caller = workspaceAccess.callerOf(exec, ctx) const sessionId = workspaceAccess.targetId(args, caller) await workspaceAccess.authorizeTarget(ctx, caller, sessionId, exec.signal) const window = await serviceBoundary.call(ctx, exec.signal, 'event read', () => diff --git a/packages/experimental/tool-session-query/src/workspace-access.ts b/packages/experimental/tool-session-query/src/workspace-access.ts index 836763cb3e..60bf265129 100644 --- a/packages/experimental/tool-session-query/src/workspace-access.ts +++ b/packages/experimental/tool-session-query/src/workspace-access.ts @@ -17,7 +17,6 @@ import type { SessionRecord, } from '@deepseek-ai/dsh-session-query' import type { ToolRunContext } from '@deepseek-ai/dsh-tools' -import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection' import { serviceBoundary } from './service-boundary.ts' interface Caller { @@ -53,7 +52,7 @@ interface DescendantVisit { readonly next: DescendantVisit | undefined } -function callerOf(exec: ToolRunContext, turnBoundary: ProjectionHandle): Caller { +function callerOf(exec: ToolRunContext, ctx: Context): Caller { const agent = exec.agent if (agent === undefined) { throw new HarnessError( @@ -64,7 +63,7 @@ function callerOf(exec: ToolRunContext, turnBoundary: ProjectionHandle { - await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + await ctx.plugin(AgentRegistry) } describe('tool-session-query with the real SQLite provider', () => { diff --git a/packages/experimental/tool-session-query/tests/tool-session-query.spec.ts b/packages/experimental/tool-session-query/tests/tool-session-query.spec.ts index c5dd7fddfc..6dcc87cbb1 100644 --- a/packages/experimental/tool-session-query/tests/tool-session-query.spec.ts +++ b/packages/experimental/tool-session-query/tests/tool-session-query.spec.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { Context, type Fiber } from '@deepseek-ai/cordis' -import type { Agent } from '@deepseek-ai/dsh-agent' +import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent' import { createUserMessage, ToolCallId, HarnessError , createMessage } from '@deepseek-ai/dsh-llm' import type { ContextFormed } from '@deepseek-ai/dsh-llm' import { MAX_TIMER_DELAY_MS, TimeoutReason } from '@deepseek-ai/dsh-timeout' @@ -15,7 +15,6 @@ import SessionStore, { type SessionId as SessionIdValue, } from '@deepseek-ai/dsh-session' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' -import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent-loop' import SessionQueryEngine, { SessionQueryError, SessionSearchCursor, @@ -209,7 +208,7 @@ interface Mounted { } async function registerTurnBoundary(ctx: Context): Promise { - await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + await ctx.plugin(AgentRegistry) } async function mount( @@ -330,14 +329,14 @@ describe('registration and schemas', () => { it('fails invalid direct config before registering anything', async () => { const mounted = await mount() for (const maxSearchResults of [0, 1.5, Number.NaN]) { - await expect(ToolSessionQuery.apply(mounted.ctx, { maxSearchResults })) - .rejects.toThrow('maxSearchResults') + expect(() => { ToolSessionQuery.apply(mounted.ctx, { maxSearchResults }) }) + .toThrow('maxSearchResults') } for (const searchTimeoutMs of [0, 1.5, Number.POSITIVE_INFINITY, MAX_TIMER_DELAY_MS + 1]) { - await expect(ToolSessionQuery.apply(mounted.ctx, { searchTimeoutMs })) - .rejects.toThrow(`no greater than ${MAX_TIMER_DELAY_MS}`) + expect(() => { ToolSessionQuery.apply(mounted.ctx, { searchTimeoutMs }) }) + .toThrow(`no greater than ${MAX_TIMER_DELAY_MS}`) } - await expect(ToolSessionQuery.apply(new Context(), {})).rejects.toThrow() + expect(() => { ToolSessionQuery.apply(new Context(), {}) }).toThrow() }) it('expresses the complete Node timer range in the Loader config schema', () => { @@ -1661,6 +1660,7 @@ describe('search paging, prior-history bounds, titles, and cancellation', () => await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) await ctx.plugin(FakeQuery) + await ctx.plugin(AgentRegistry) await ctx.plugin(ToolSessionQuery) const session = createSession(ctx, 'no-boundary-caller', '/work') const result = await ctx.tools.execute({ diff --git a/packages/experimental/tool-session-query/tsconfig.json b/packages/experimental/tool-session-query/tsconfig.json index c3636a6deb..71a98f903b 100644 --- a/packages/experimental/tool-session-query/tsconfig.json +++ b/packages/experimental/tool-session-query/tsconfig.json @@ -35,9 +35,6 @@ { "path": "../../util/timeout" }, - { - "path": "../../session/session-projection" - }, { "path": "../../core/agent" } diff --git a/packages/experimental/tool-terminal/tests/tools.spec.ts b/packages/experimental/tool-terminal/tests/tools.spec.ts index fcc0a10405..3efbaf7ca3 100644 --- a/packages/experimental/tool-terminal/tests/tools.spec.ts +++ b/packages/experimental/tool-terminal/tests/tools.spec.ts @@ -4,6 +4,7 @@ import { Context } from '@deepseek-ai/cordis' import { ToolCallId } from '@deepseek-ai/dsh-llm' import { Session, SessionId } from '@deepseek-ai/dsh-session' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Agent } from '@deepseek-ai/dsh-agent' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { renderToolsSdk } from '@deepseek-ai/dsh-tools' @@ -114,6 +115,7 @@ async function setupBase(jobs: boolean) { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(TerminalSessionService) const stub = stubBackend() diff --git a/packages/experimental/worktree/tests/harness.ts b/packages/experimental/worktree/tests/harness.ts index c38be4d57c..369c13fa2f 100644 --- a/packages/experimental/worktree/tests/harness.ts +++ b/packages/experimental/worktree/tests/harness.ts @@ -71,8 +71,8 @@ export async function repository(parent = tmpdir()): Promise { export async function harness(root: string, config: Config = {}, mode: SandboxMode = 'danger-full-access'): Promise { const ctx = new Context() try { - await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(SystemPrompt) await ctx.plugin(LocalFileSystem, { cwd: root }) await ctx.plugin(LocalSubprocessRuntime) diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 1167405bb7..aa98e5b079 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -135,6 +135,12 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ summary: 'Registry of YAML-declared presets and the revisions live Agents retain.', description: 'Registry of YAML-declared presets and the revisions live Agents retain.', methods: [ + { + signature: 'presetForSession(session: Session): string | null', + description: 'Read the preset recorded for a Session, including selections no longer in the roster.', + parameters: [{ name: 'session', description: 'Session whose committed preset selection is read.' }], + returns: 'the recorded preset, or null when none was selected.', + }, { signature: 'async register(definition: PresetDefinition): Promise<() => Promise>', description: 'Register and eagerly load a definition; activation failure remains visible in the roster.', @@ -226,6 +232,12 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ summary: 'Agent service (`ctx.agents`): tracks live agents and carries the initiating Agent through one process-local asynchronous driver chain.', description: 'Agent service (`ctx.agents`): tracks live agents and carries the initiating Agent through one process-local asynchronous driver chain. Agent *creation* is provided by whichever plugin implements the AgentFactory (`@deepseek-ai/dsh-agent-loop`), registered via setFactory.\n\nInitiator methods provide same-process causal attribution only. Ambient presence is neither liveness proof nor authorization; subjects and owners remain explicit, as does identity at worker, process, persistence, and wire boundaries. Returned Promise boundaries drain during teardown, except a nested lineage that starts an owning-fiber unload is excluded from its own drain.', methods: [ + { + signature: 'turnBoundaryOf(session: Session): TurnBoundaryProjection', + description: 'Read the turn and step positions recorded in a Session.', + parameters: [{ name: 'session', description: 'Session whose committed events are read.' }], + returns: 'the current turn and step positions; do not mutate the returned value.', + }, { signature: 'currentInitiator(): Agent | undefined', description: 'Read the Agent that initiated the inherited asynchronous driver chain. Use this optional form for logging, tracing, metrics, or host attribution that also supports agentless calls. When a parent creates a child, setup reports the causal parent while the setup callback\'s Agent parameter identifies the child.', @@ -3315,6 +3327,12 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ summary: 'Replay owner for one service-wide estimator and isolated per-session folds.', description: 'Replay owner for one service-wide estimator and isolated per-session folds.', methods: [ + { + signature: 'contextPressure(session: Session): ContextPressureProjection', + description: 'Read the latest logged context occupancy and its projected surface adjustment.', + parameters: [{ name: 'session', description: 'Session whose committed usage and request capacity are read.' }], + returns: 'context pressure; fields remain absent until their source events are recorded.', + }, { signature: 'measure(session: Session, requestHeader?: EpochHeader): TokenMeasurement', description: 'Measure current request pressure and surface through the durable tail.\n\nThe effective envelope\'s routed provider/model selects the request-image pricing every node is priced under: a route whose adapter declares image pricing charges each retained image its visual tokens plus its model-visible text, while other routes keep the fixed heuristic. Provider usage is reused only when the latest successful call\'s canonical request envelope matches `requestHeader` and its total is no lower than that call\'s full route-priced anchor; otherwise the complete envelope and surface are repriced. The anchor includes all surface nodes immediately before the assistant message, including inputs admitted after step/start.\n\n`requestHeader` replaces the latest logged envelope for pressure and node pricing; the node set always describes the current session surface. Every call clones those positional nodes, so measurement is O(surface).', @@ -5098,6 +5116,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'ContentBlockType', declaration: 'export type ContentBlockType = keyof ContentBlockMap;', }, + { + name: 'ContextPressureProjection', + declaration: 'export interface ContextPressureProjection {\n pressureTokens?: number;\n projectedTokens?: number;\n contextWindow?: number;\n}', + }, { name: 'ContinuableCreateRequest', declaration: 'export interface ContinuableCreateRequest {\n readonly cwd: string;\n readonly sessionId: SessionId;\n readonly parent: Agent;\n readonly signal: AbortSignal;\n}', @@ -8222,6 +8244,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'TranslationSpec', declaration: 'export type TranslationSpec = AnonymousTranslationSpec | PaidTranslationSpec;', }, + { + name: 'TurnBoundaryProjection', + declaration: 'export interface TurnBoundaryProjection {\n readonly openTurnStartSeq: OptionalSessionSeq;\n readonly lastStepStartSeq: OptionalSessionSeq;\n readonly lastStepBoundary: {\n readonly kind: \'start\' | \'end\';\n readonly seq: SessionSeq;\n } | null;\n readonly lastTurn: number;\n}', + }, { name: 'TurnCompleteInput', declaration: 'export interface TurnCompleteInput {\n turnId: string;\n answer: string;\n durationMs: number;\n isAborted: boolean;\n reason: \'answer\' | \'aborted\' | \'error\';\n agentId?: string;\n usage?: TurnUsage;\n}', diff --git a/packages/feedback/command-feedback/tests/command-feedback.spec.ts b/packages/feedback/command-feedback/tests/command-feedback.spec.ts index 4e5401c09e..f218d8ca9b 100644 --- a/packages/feedback/command-feedback/tests/command-feedback.spec.ts +++ b/packages/feedback/command-feedback/tests/command-feedback.spec.ts @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent' import CommandRuntime from '@deepseek-ai/dsh-commands' import SessionStore, { foldSurface, Session, SessionId } from '@deepseek-ai/dsh-session' @@ -53,6 +54,7 @@ function stubAgent(ctx: Context, id: string): { agent: Agent; session: Session } async function harness(): Promise { const ctx = new Context() await ctx.plugin(CommandRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(SessionStore) const plugin = await ctx.plugin(commandFeedback) diff --git a/packages/feedback/command-feedback/tests/loader-composition.spec.ts b/packages/feedback/command-feedback/tests/loader-composition.spec.ts index 273657e579..4508e8fa39 100644 --- a/packages/feedback/command-feedback/tests/loader-composition.spec.ts +++ b/packages/feedback/command-feedback/tests/loader-composition.spec.ts @@ -7,6 +7,7 @@ import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' import Include from '@deepseek-ai/cordis-plugin-include' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent' import CommandRuntime from '@deepseek-ai/dsh-commands' import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' @@ -57,6 +58,7 @@ describe('/feedback real Loader composition through cordis.yml', () => { const configPath = join(root, 'cordis.yml') await writeFile(configPath, [ "- name: '@deepseek-ai/dsh-agent'", + "- name: '@deepseek-ai/dsh-session-projection'", "- name: '@deepseek-ai/dsh-session'", "- name: '@deepseek-ai/dsh-commands'", "- name: '@deepseek-ai/dsh-command-feedback'", @@ -69,6 +71,7 @@ describe('/feedback real Loader composition through cordis.yml', () => { context.loader.builtins.include = Include const modules = new Map([ ['@deepseek-ai/dsh-agent', AgentRegistry], + ['@deepseek-ai/dsh-session-projection', SessionProjectionRegistry], ['@deepseek-ai/dsh-session', SessionStore], ['@deepseek-ai/dsh-commands', CommandRuntime], ['@deepseek-ai/dsh-command-feedback', CommandFeedback], diff --git a/packages/fs/tool-fs/tests/tools.spec.ts b/packages/fs/tool-fs/tests/tools.spec.ts index d0cbd0ef03..1e84bc9b48 100644 --- a/packages/fs/tool-fs/tests/tools.spec.ts +++ b/packages/fs/tool-fs/tests/tools.spec.ts @@ -1,3 +1,4 @@ +import AgentRegistry from '@deepseek-ai/dsh-agent' /** * Consumer API tests over a fake provider and the real policy collaborator: schemas, * validation, formatting, typed errors, intent dispatch, and observation-driven authorization. @@ -8,7 +9,6 @@ import { describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import { PtcRuntime } from '@deepseek-ai/dsh-ptc-runtime' import { createScope, type Scope } from '@deepseek-ai/dsh-scope' -import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent-loop' import { ToolCallId } from '@deepseek-ai/dsh-llm' import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { type ToolResult } from '@deepseek-ai/dsh-tools' @@ -863,7 +863,7 @@ describe('sandbox escalation API (write/edit)', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) await ctx.plugin(SessionProjectionRegistry) - await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + await ctx.plugin(AgentRegistry) await ctx.plugin(SandboxPolicyService, { mode: 'workspace-write' }) await ctx.plugin(SandboxingFakeFs) await ctx.plugin(FsPolicy) diff --git a/packages/fs/tool-str-replace-editor/tests/tools.spec.ts b/packages/fs/tool-str-replace-editor/tests/tools.spec.ts index 74b8ce5dd6..710939691f 100644 --- a/packages/fs/tool-str-replace-editor/tests/tools.spec.ts +++ b/packages/fs/tool-str-replace-editor/tests/tools.spec.ts @@ -76,13 +76,11 @@ async function setup( contexts.push(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) if (options.sandboxMode === undefined) { await ctx.plugin(LocalFileSystem, { cwd: root }) } else { - // SandboxPolicy declares the registry as a required injection; mount it - // before the policy activates. - await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(SandboxPolicy, { mode: options.sandboxMode, workspaceRoot: root }) await ctx.plugin(SandboxedFileSystem, { cwd: root }) } @@ -616,6 +614,7 @@ describe('tool-str-replace-editor', () => { contexts.push(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalFileSystem, { cwd: root }) Object.defineProperty(ctx.fs, 'sandboxMode', { value: 'read-only' }) diff --git a/packages/goal/goal/tests/goal.spec.ts b/packages/goal/goal/tests/goal.spec.ts index 490a4c814d..8948fe916b 100644 --- a/packages/goal/goal/tests/goal.spec.ts +++ b/packages/goal/goal/tests/goal.spec.ts @@ -172,8 +172,8 @@ describe('GoalService creation and replay', () => { it('also resolves the default when constructed directly without Cordis config normalization', async () => { const ctx = new Context() - await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) const stub = stubAgent('goal-direct-construction') await ctx.agents.register(stub.agent) class DirectGoalService extends GoalService { @@ -188,8 +188,8 @@ describe('GoalService creation and replay', () => { it('declares no resource readers on goals/get, leaving its Session resource to the Agent lookup', async () => { const ctx = new Context() - await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) const goals = new GoalService(ctx) const marker = remoteMethods(goals).find(method => method.method === 'get') expect(marker).toMatchObject({ method: 'get' }) @@ -198,8 +198,8 @@ describe('GoalService creation and replay', () => { it('rejects invalid direct configuration', async () => { const ctx = new Context() - await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await expect(ctx.plugin(GoalService, { defaultMaxGoalRounds: -1 })).rejects.toThrow(expect.objectContaining({ code: 'GOAL_INVALID_MAX_ROUNDS', })) @@ -212,8 +212,8 @@ describe('GoalService creation and replay', () => { appendRound(first.session, created, 2) const ctx = new Context() - await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(GoalService) const resumed = stubAgent('seeded-goal', first.session.snapshotEvents()) await ctx.agents.register(resumed.agent) @@ -282,8 +282,8 @@ describe('GoalService creation and replay', () => { it('removes the service and projection with the providing fiber', async () => { const ctx = new Context() - await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) const fiber = await ctx.plugin(GoalService) const first = ctx.goals const stub = stubAgent('goal-hmr') @@ -510,8 +510,8 @@ describe('GoalService mutations', () => { it('does not delegate goal persistence to agent injection', async () => { const ctx = new Context() - await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(GoalService) const stub = stubAgent('goal-independent-injection') stub.agent.inject = () => { throw new Error('injection must not be called') } diff --git a/packages/goal/goal/tests/projection.spec.ts b/packages/goal/goal/tests/projection.spec.ts index 64071e4038..f71559bb8e 100644 --- a/packages/goal/goal/tests/projection.spec.ts +++ b/packages/goal/goal/tests/projection.spec.ts @@ -60,8 +60,8 @@ async function liveAgent(ctx: Context, session: Session): Promise { async function harness(withGoal: boolean): Promise { const ctx = new Context() await ctx.plugin(SessionStore) - await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) if (withGoal) await ctx.plugin(GoalService) const session = ctx.sessions.create() const agent = await liveAgent(ctx, session) diff --git a/packages/goal/tool-goal/README.i18n.yaml b/packages/goal/tool-goal/README.i18n.yaml index 1006276ef8..ee11ff3200 100644 --- a/packages/goal/tool-goal/README.i18n.yaml +++ b/packages/goal/tool-goal/README.i18n.yaml @@ -30,8 +30,8 @@ en: 4e9fdb5a89f4baa3 zh: 50d182467ae1f3a7 /deepseek-ai-dsh-tool-goal/understand-the-implementation/design: - en: 3be550d69f7167f1 - zh: 0becdbd7b7395e40 + en: 1c51463a2abd9514 + zh: b02fbbd125d96bf6 /deepseek-ai-dsh-tool-goal/understand-the-implementation/source-map: en: 4d5c648787409868 zh: b4938bfe660f18aa diff --git a/packages/goal/tool-goal/README.md b/packages/goal/tool-goal/README.md index 23b457e6c8..adb65e4f48 100644 --- a/packages/goal/tool-goal/README.md +++ b/packages/goal/tool-goal/README.md @@ -68,7 +68,7 @@ This section explains how the tools enforce authority and render output; the obs ### Design -- **Authority at execution.** The plugin awaits its turn-boundary reader before registering tools. Every call resolves the exact live agent, its inherited `AgentRegistry` initiator, running status, and an open turn; `create`, `edit`, `pause`, and `resume` additionally require an accepted `{ kind: 'user' }` message or steering event in a runtime-root agent's current turn. A durable paused goal fails the `resume` action with `GOAL_TOOL_RESUME_PAUSED`; the user-facing command or Web control owns that transition. Durable fork lineage does not demote a resumed root; live subagent ownership does. +- **Authority at execution.** The plugin requires `agents` and reads turn state through `turnBoundaryOf(session)`. Every call resolves the exact live agent, its inherited `AgentRegistry` initiator, running status, and an open turn; `create`, `edit`, `pause`, and `resume` additionally require an accepted `{ kind: 'user' }` message or steering event in a runtime-root agent's current turn. A durable paused goal fails the `resume` action with `GOAL_TOOL_RESUME_PAUSED`; the user-facing command or Web control owns that transition. Durable fork lineage does not demote a resumed root; live subagent ownership does. - **Host attestation of human input.** `{ kind: 'user' }` is assigned by `Agent.followup()` and `steer()` when their caller omits a source, so plugins, schedulers, and other non-human producers must pass their own source rather than inheriting human authority. - **System-prompt guidance with the configured threshold.** The package registers one `tool:goal` system-prompt section whose fixed text interpolates `blockedAfterConsecutiveRounds`; the same value is the hard lower bound enforced at execution. - **Wrap-up context for terminal rounds.** A successful autonomous `complete` or `blocked` defers a closing `` or `` instruction so the model addresses the user once before the turn ends; direct-human mutations never defer this context. diff --git a/packages/goal/tool-goal/README.zh.md b/packages/goal/tool-goal/README.zh.md index e7d67715c4..6f3da635bd 100644 --- a/packages/goal/tool-goal/README.zh.md +++ b/packages/goal/tool-goal/README.zh.md @@ -68,7 +68,7 @@ kind: "package-reference" ### 设计 -- **执行时权限。** 插件等待轮次边界读取句柄就绪后才注册工具。每次调用都解析精确活跃 agent、其继承的 `AgentRegistry` initiator、running 状态与开放轮次;`create`、`edit`、`pause` 和 `resume` 还要求运行时根 agent 的当前轮次中存在已接受的 `{ kind: 'user' }` 消息或 steering 事件。持久的 paused goal 会让 `resume` 以 `GOAL_TOOL_RESUME_PAUSED` 失败;面向用户的命令或 Web 控件拥有该转换。持久 fork 谱系不会降低已恢复根 agent 的等级;活跃 subagent 所有权会降低。 +- **执行时权限。** 插件依赖 `agents`,并通过 `turnBoundaryOf(session)` 读取轮次状态。每次调用都解析精确活跃 agent、其继承的 `AgentRegistry` initiator、running 状态与开放轮次;`create`、`edit`、`pause` 和 `resume` 还要求运行时根 agent 的当前轮次中存在已接受的 `{ kind: 'user' }` 消息或 steering 事件。持久的 paused goal 会让 `resume` 以 `GOAL_TOOL_RESUME_PAUSED` 失败;面向用户的命令或 Web 控件拥有该转换。持久 fork 谱系不会降低已恢复根 agent 的等级;活跃 subagent 所有权会降低。 - **人类输入的宿主证明。** `Agent.followup()` 与 `steer()` 会在调用方省略 source 时分配 `{ kind: 'user' }`,因此插件、调度器与其他非人类生产方必须传入自己的 source,不能继承人类权限。 - **带配置阈值的系统提示词指引。** 本包注册一个 `tool:goal` 系统提示词章节,其固定文本插入 `blockedAfterConsecutiveRounds`;同一数值就是执行时强制执行的硬下限。 - **终局 Round 的结束上下文。** 成功的自主 `complete` 或 `blocked` 会延后一条 `` 或 `` 结束指令,让模型在轮次结束前向用户做一次交代;人类直接变更绝不会延后该上下文。 diff --git a/packages/goal/tool-goal/package.json b/packages/goal/tool-goal/package.json index e5c0e9d8e7..ce2d48e51d 100644 --- a/packages/goal/tool-goal/package.json +++ b/packages/goal/tool-goal/package.json @@ -35,8 +35,7 @@ "@deepseek-ai/dsh-session": "workspace:*", "@deepseek-ai/dsh-system-prompt": "workspace:*", "@deepseek-ai/dsh-tools": "workspace:*", - "@deepseek-ai/cordis": "workspace:~", - "@deepseek-ai/dsh-session-projection": "workspace:*" + "@deepseek-ai/cordis": "workspace:~" }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~" diff --git a/packages/goal/tool-goal/src/authority.ts b/packages/goal/tool-goal/src/authority.ts index 07de4e68ae..77568e3746 100644 --- a/packages/goal/tool-goal/src/authority.ts +++ b/packages/goal/tool-goal/src/authority.ts @@ -1,12 +1,11 @@ /** Execution-time authority checks for the model-facing goal tools. */ import type { Context } from '@deepseek-ai/cordis' -import type { Agent, TurnBoundaryProjection } from '@deepseek-ai/dsh-agent' +import type { Agent } from '@deepseek-ai/dsh-agent' import type { GoalView } from '@deepseek-ai/dsh-goal' import { HarnessError } from '@deepseek-ai/dsh-llm' import type { SessionEvent, SessionSeq } from '@deepseek-ai/dsh-session' import type { ToolRunContext } from '@deepseek-ai/dsh-tools' -import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection' /** The calling agent plus the immutable event cut and open-turn start seq used for authority checks. */ export interface GoalToolExecution { @@ -27,12 +26,12 @@ function reject(message: string, code = 'GOAL_TOOL_AUTHORITY_REQUIRED'): never { /** Resolve the immutable event cut and open-turn boundary without copying the turn suffix. */ function openTurnEvents( - turnBoundary: ProjectionHandle, + ctx: Context, agent: Agent, ): Pick { // oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred. const events = agent.session.snapshotEvents() - const boundary = turnBoundary.stateOf(agent.session) + const boundary = ctx.agents.turnBoundaryOf(agent.session) if (boundary.openTurnStartSeq === null) { reject('goal tools require an open model turn', 'GOAL_TOOL_DRIVER_REQUIRED') } @@ -43,14 +42,9 @@ function openTurnEvents( * Resolve and authenticate the calling agent and its driver boundary. * @param ctx - Context carrying the live agent registry. * @param exec - Tool execution metadata supplied by the registry. - * @param turnBoundary - Ready reader for the calling Session's turn boundary. * @returns The authenticated agent, immutable event cut, and open-turn boundary. */ -export function goalToolExecution( - ctx: Context, - exec: ToolRunContext, - turnBoundary: ProjectionHandle, -): GoalToolExecution { +export function goalToolExecution(ctx: Context, exec: ToolRunContext): GoalToolExecution { const agent = exec.agent if (agent === undefined) { return reject('goal tools require a calling agent', 'GOAL_TOOL_AGENT_REQUIRED') @@ -62,7 +56,7 @@ export function goalToolExecution( 'GOAL_TOOL_DRIVER_REQUIRED', ) } - return { agent, ...openTurnEvents(turnBoundary, agent) } + return { agent, ...openTurnEvents(ctx, agent) } } /** Whether the captured open turn contains an event accepted by `predicate`. */ diff --git a/packages/goal/tool-goal/src/index.ts b/packages/goal/tool-goal/src/index.ts index 962d0b8f98..482d614160 100644 --- a/packages/goal/tool-goal/src/index.ts +++ b/packages/goal/tool-goal/src/index.ts @@ -5,7 +5,6 @@ */ import type { Context } from '@deepseek-ai/cordis' -import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent' import z from '@deepseek-ai/schemastery' import { GoalId } from '@deepseek-ai/dsh-goal' import type { GoalRef, GoalView } from '@deepseek-ai/dsh-goal' @@ -27,7 +26,7 @@ import { import { renderWrapupContext } from './wrapup.ts' export const name = 'tool-goal' -export const inject = ['agents', 'goals', 'tools', 'systemPrompt', 'sessionProjections'] +export const inject = ['agents', 'goals', 'tools', 'systemPrompt'] /** Model policy and hard lower bounds for goal-state updates. */ export interface Config { @@ -185,9 +184,8 @@ function present(title: string, kind: 'read' | 'other', rawInput?: unknown): Gen } /** Register the three Codex-shaped goal tools and their shared policy section. */ -export async function apply(ctx: Context, config: Config): Promise { +export function apply(ctx: Context, config: Config): void { const resolved = resolveConfig(config) - const turnBoundary = await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) ctx.systemPrompt.section({ name: 'tool:goal', order: ctx.systemPrompt.getSectionOrder('TOOL_GOAL'), @@ -200,7 +198,7 @@ export async function apply(ctx: Context, config: Config): Promise { parameters: {}, output: GOAL_OUTPUT, execute(_args, exec) { - const execution = goalToolExecution(ctx, exec, turnBoundary) + const execution = goalToolExecution(ctx, exec) return Promise.resolve(goalValue(ctx.goals.get(execution.agent))) }, presentCall: () => present('Read current goal', 'read'), @@ -222,7 +220,7 @@ export async function apply(ctx: Context, config: Config): Promise { }, output: GOAL_OUTPUT, execute(args, exec) { - const execution = goalToolExecution(ctx, exec, turnBoundary) + const execution = goalToolExecution(ctx, exec) requireDirectHuman(ctx, execution) const goal = ctx.goals.create(execution.agent, { objective: args.objective, @@ -255,7 +253,7 @@ export async function apply(ctx: Context, config: Config): Promise { }, output: GOAL_OUTPUT, execute(args, exec) { - const execution = goalToolExecution(ctx, exec, turnBoundary) + const execution = goalToolExecution(ctx, exec) const ref = goalRef(args.goal_id, args.revision) const replacements = { ...hasText(args.objective) ? { objective: args.objective } : {}, diff --git a/packages/goal/tool-goal/tests/tool-goal.spec.ts b/packages/goal/tool-goal/tests/tool-goal.spec.ts index 08d5e46589..233ed1526f 100644 --- a/packages/goal/tool-goal/tests/tool-goal.spec.ts +++ b/packages/goal/tool-goal/tests/tool-goal.spec.ts @@ -193,7 +193,7 @@ describe('goal tool registration and presentation', () => { it('has the Loader-safe namespace export shape', () => { expect('default' in toolGoal).toBe(false) expect(toolGoal.name).toBe('tool-goal') - expect(toolGoal.inject).toEqual(['agents', 'goals', 'tools', 'systemPrompt', 'sessionProjections']) + expect(toolGoal.inject).toEqual(['agents', 'goals', 'tools', 'systemPrompt']) const loader = Object.create(Loader.prototype) as Loader expect(loader.unwrapExports(toolGoal)).toBe(toolGoal) }) @@ -206,7 +206,7 @@ describe('goal tool registration and presentation', () => { await ctx.plugin(AgentRegistry) await ctx.plugin(ToolRuntime) await ctx.plugin(GoalService) - await expect(toolGoal.apply(ctx, { blockedAfterConsecutiveRounds: 1.5 })).rejects.toThrow( + expect(() => { toolGoal.apply(ctx, { blockedAfterConsecutiveRounds: 1.5 }) }).toThrow( 'blockedAfterConsecutiveRounds must be a positive safe integer', ) expect(ctx.tools.get('get_goal')).toBeUndefined() @@ -220,7 +220,7 @@ describe('goal tool registration and presentation', () => { await ctx.plugin(AgentRegistry) await ctx.plugin(ToolRuntime) await ctx.plugin(GoalService) - await toolGoal.apply(ctx, {}) + toolGoal.apply(ctx, {}) const section = (await ctx.systemPrompt.assemble()).sections.find(item => item.name === 'tool:goal') expect(section?.text).toContain('at least 3 consecutive goal rounds') }) diff --git a/packages/goal/tool-goal/tsconfig.json b/packages/goal/tool-goal/tsconfig.json index fc97dd09aa..d5c34d4e29 100644 --- a/packages/goal/tool-goal/tsconfig.json +++ b/packages/goal/tool-goal/tsconfig.json @@ -34,9 +34,6 @@ }, { "path": "../goal" - }, - { - "path": "../../session/session-projection" } ] } diff --git a/packages/interaction/permission-presets/src/index.ts b/packages/interaction/permission-presets/src/index.ts index a3df0f8d74..e9cb9a38c3 100644 --- a/packages/interaction/permission-presets/src/index.ts +++ b/packages/interaction/permission-presets/src/index.ts @@ -331,8 +331,7 @@ export class PermissionPresetService extends TypertRemoteService { } private permissionState(session: Session): PermissionProjectionState { - const state = this.projection.stateOf(session) - return state + return this.projection.stateOf(session) } /** diff --git a/packages/interaction/tool-ask-user/tests/tool-ask-user.spec.ts b/packages/interaction/tool-ask-user/tests/tool-ask-user.spec.ts index 5ba9c7f544..7218538e69 100644 --- a/packages/interaction/tool-ask-user/tests/tool-ask-user.spec.ts +++ b/packages/interaction/tool-ask-user/tests/tool-ask-user.spec.ts @@ -2,6 +2,7 @@ import { describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import { createToolResultMessage, ToolCallId, type ToolSchema } from '@deepseek-ai/dsh-llm' import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import { Session, SessionId, SessionSeq, SESSION_FORMAT_VERSION, type SessionEvent } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime from '@deepseek-ai/dsh-tools' @@ -69,6 +70,7 @@ interface OptionSchemaShape { async function setup(config: toolAskUser.Config = { mode: 'timed', timeout: -1 }) { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) diff --git a/packages/interaction/user-questions/tests/user-questions.spec.ts b/packages/interaction/user-questions/tests/user-questions.spec.ts index 0cbb2a5031..1b03fa746a 100644 --- a/packages/interaction/user-questions/tests/user-questions.spec.ts +++ b/packages/interaction/user-questions/tests/user-questions.spec.ts @@ -198,6 +198,7 @@ describe('UserQuestionService', () => { it('rejects a live runtime-owned agent before reaching the provider', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(UserQuestionService) const p = { ask: vi.fn(async () => ({ answers: [] })) } @@ -220,6 +221,7 @@ describe('UserQuestionService', () => { it('reaches the provider for a lineage-bearing session resumed as a runtime root', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(UserQuestionService) const p = provider('yes') @@ -250,6 +252,7 @@ describe('UserQuestionService', () => { it('rejects a stale agent object that reuses a live id', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(UserQuestionService) const p = { ask: vi.fn(async () => ({ answers: [] })) } @@ -389,8 +392,8 @@ function liveAgent(id: string): LiveAgent { async function timedContext(): Promise { const ctx = new Context() - await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(UserQuestionService) return ctx } @@ -834,10 +837,12 @@ describe('late replies', () => { it('answers nothing when the composition carries no Session projections', async () => { const ctx = new Context() - await ctx.plugin(AgentRegistry) - await ctx.plugin(UserQuestionService) const agent = liveAgent('late-no-projections') - ctx.agents.enter(agent, undefined) + ctx.provide('agents', { + get: (id: SessionId) => id === agent.id ? agent : undefined, + roots: () => [agent], + } as never) + await ctx.plugin(UserQuestionService) askInLog(agent, timedCallId) continueInLog(agent, timedCallId) diff --git a/packages/jobs/jobs-local/tests/jobs.spec.ts b/packages/jobs/jobs-local/tests/jobs.spec.ts index 538c956140..1457e8413b 100644 --- a/packages/jobs/jobs-local/tests/jobs.spec.ts +++ b/packages/jobs/jobs-local/tests/jobs.spec.ts @@ -5,6 +5,7 @@ import { Session, SessionId } from '@deepseek-ai/dsh-session' import { ToolCallId } from '@deepseek-ai/dsh-llm/brand' import { createUserMessage } from '@deepseek-ai/dsh-llm' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Agent } from '@deepseek-ai/dsh-agent' import { bindScopeParent, createScope, scopeOf } from '@deepseek-ai/dsh-scope' import type { ScopeKey } from '@deepseek-ai/dsh-scope' @@ -117,6 +118,7 @@ function producer(overrides: ProducerOverrides = {}) { async function harness(config: JobsConfig = {}) { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry, config) ctx.jobs.attachController('test-controller') @@ -183,6 +185,7 @@ describe('LocalJobRegistry.start', () => { it('refuses an owner whose own composition attaches no controller', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) // Two standing preset mounts over one registry; only the first loads the @@ -204,6 +207,7 @@ describe('LocalJobRegistry.start', () => { it('lets a controller attached without a scope serve every owner', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) // The host-plane composition's own controls: no scope, so the global layer @@ -911,6 +915,7 @@ describe('LocalJobRegistry owner cleanup', () => { it('registers owner cleanup on the agent scope rather than the jobs fiber', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const tasksFiber = await ctx.plugin(LocalJobRegistry) ctx.jobs.attachController('test-controller') @@ -977,6 +982,7 @@ describe('LocalJobRegistry owner cleanup', () => { describe('LocalJobRegistry disposal', () => { it('cancels live jobs, awaits settlement, and announces the teardown to outside subscribers', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const fiber = await ctx.plugin(LocalJobRegistry) await ctx.plugin(Object.assign((inner: Context) => { @@ -1007,6 +1013,7 @@ describe('LocalJobRegistry disposal', () => { it('force-fails a throwing cancel so service disposal does not await producer done', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const fiber = await ctx.plugin(LocalJobRegistry) ctx.jobs.attachController('test-controller') @@ -1044,6 +1051,7 @@ describe('LocalJobRegistry disposal', () => { it('detaches owner effects from still-live agent scopes when the service unloads', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const tasksFiber = await ctx.plugin(LocalJobRegistry) ctx.jobs.attachController('test-controller') @@ -1069,6 +1077,7 @@ describe('LocalJobRegistry disposal', () => { it('drops a scoped layer when its registrations dispose', async () => { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) const standing = createScope(ctx, {}) diff --git a/packages/jobs/tool-jobs/tests/tool-jobs.spec.ts b/packages/jobs/tool-jobs/tests/tool-jobs.spec.ts index 0e69eca22c..d2e9daa981 100644 --- a/packages/jobs/tool-jobs/tests/tool-jobs.spec.ts +++ b/packages/jobs/tool-jobs/tests/tool-jobs.spec.ts @@ -4,6 +4,7 @@ import { ToolCallId } from '@deepseek-ai/dsh-llm' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime from '@deepseek-ai/dsh-tools' import AgentRegistry, { emitAgentEvent } from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Agent } from '@deepseek-ai/dsh-agent' import { createUserMessage } from '@deepseek-ai/dsh-llm' import { SessionId } from '@deepseek-ai/dsh-session' @@ -45,6 +46,7 @@ async function setup(config: ToolJobs.Config = {}, registry: RegistryConfig = {} const ctx = new Context() await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) const agentsFiber = await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry, registry) const toolsFiber = await ctx.plugin(ToolJobs, config) @@ -182,6 +184,7 @@ describe('tool-jobs setup', () => { const ctx = new Context() await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await expect(ctx.plugin(ToolJobs, { waitTimeoutMs: 100, maxWaitTimeoutMs: 50 })) @@ -201,6 +204,7 @@ describe('tool-jobs setup', () => { const ctx = new Context() await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) try { @@ -299,6 +303,7 @@ describe('tool-jobs setup', () => { const ctx = new Context() await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) ToolJobs.apply(ctx, {}) @@ -704,6 +709,7 @@ describe('completion notices across scoped mounts', () => { const ctx = new Context() await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) diff --git a/packages/llm/llm-deepseek/tests/adapter.e2e.ts b/packages/llm/llm-deepseek/tests/adapter.e2e.ts index 698065e66e..6952c493e2 100644 --- a/packages/llm/llm-deepseek/tests/adapter.e2e.ts +++ b/packages/llm/llm-deepseek/tests/adapter.e2e.ts @@ -13,6 +13,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { Context, LoggerLevel } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import LocalAttachments from '@deepseek-ai/dsh-attachment-local' import DeepSeekLlmApiExtensionRegistry from '@deepseek-ai/dsh-deepseek-llm-api-extensions' import LlmRuntime, { BlockAssembler, createAssistantMessage, createSystemMessage, createToolResultMessage, ReasoningEffortId, ToolCallId } from '@deepseek-ai/dsh-llm' @@ -148,6 +149,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('DeepSeek Messages real API', () const unversionedModule = pathToFileURL(join(pluginRoot, 'plugin.mjs')).href const ctx = await boot() await ctx.plugin(Loader) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(SessionStore) await ctx.plugin(DeepSeekLlmApiExtensionRegistry) diff --git a/packages/llm/llm-deepseek/tests/loader-composition.spec.ts b/packages/llm/llm-deepseek/tests/loader-composition.spec.ts index 9ad719a67d..5c401272fa 100644 --- a/packages/llm/llm-deepseek/tests/loader-composition.spec.ts +++ b/packages/llm/llm-deepseek/tests/loader-composition.spec.ts @@ -10,6 +10,7 @@ import Loader from '@deepseek-ai/cordis-plugin-loader' import Include from '@deepseek-ai/cordis-plugin-include' import LlmRuntime from '@deepseek-ai/dsh-llm' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' import { credentialRef } from '@deepseek-ai/dsh-credentials' import LocalCredentialProvider from '@deepseek-ai/dsh-credentials-local' @@ -60,6 +61,7 @@ async function loadComposition( " name: '@deepseek-ai/dsh-session'", '- id: agents', " name: '@deepseek-ai/dsh-agent'", + "- name: '@deepseek-ai/dsh-session-projection'", '- id: deepseek-llm-api-extensions', " name: '@deepseek-ai/dsh-deepseek-llm-api-extensions'", '- id: session-log-deepseek', @@ -94,6 +96,7 @@ async function loadComposition( ['@deepseek-ai/dsh-llm', LlmRuntime], ['@deepseek-ai/dsh-session', SessionStore], ['@deepseek-ai/dsh-agent', AgentRegistry], + ['@deepseek-ai/dsh-session-projection', SessionProjectionRegistry], ['@deepseek-ai/dsh-deepseek-llm-api-extensions', DeepSeekLlmApiExtensionRegistry], ['@deepseek-ai/dsh-session-log-deepseek', SessionLogDeepSeek], ['@deepseek-ai/dsh-plugin-package-inventory-deepseek', DeepSeekPluginPackageInventory], diff --git a/packages/llm/llm-deepseek/tests/runtime.e2e.ts b/packages/llm/llm-deepseek/tests/runtime.e2e.ts index 4738fe9b6a..6321512705 100644 --- a/packages/llm/llm-deepseek/tests/runtime.e2e.ts +++ b/packages/llm/llm-deepseek/tests/runtime.e2e.ts @@ -8,6 +8,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import LlmRuntime, { createToolResultMessage, createUserMessage, ToolCallId, ReasoningEffortId, createMessage, createSystemMessage } from '@deepseek-ai/dsh-llm' import type { Message, ToolSchema } from '@deepseek-ai/dsh-llm' import AttachmentStore, { AttachmentId, ImageVariantId } from '@deepseek-ai/dsh-attachment' @@ -228,6 +229,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-deepseek e2e (real API)', () const ctx = new Context() contexts.push(ctx) await ctx.plugin(Loader) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LlmRuntime) await ctx.plugin(SessionStore) diff --git a/packages/llm/llm-retry/src/index.ts b/packages/llm/llm-retry/src/index.ts index 60cca5ba2f..016d19f400 100644 --- a/packages/llm/llm-retry/src/index.ts +++ b/packages/llm/llm-retry/src/index.ts @@ -19,7 +19,6 @@ export type { LlmRetryEventData, LlmRetryStartedEventData } from './types.ts' export { RetryId } from './brand.ts' export const name = 'llm-retry' - export const inject = ['agents', 'sessionProjections'] /** This policy executor has no config; providers own `retryPolicy`. */ diff --git a/packages/llm/plugin-package-inventory-deepseek/tests/inventory.spec.ts b/packages/llm/plugin-package-inventory-deepseek/tests/inventory.spec.ts index 28793ba945..17b4a558f8 100644 --- a/packages/llm/plugin-package-inventory-deepseek/tests/inventory.spec.ts +++ b/packages/llm/plugin-package-inventory-deepseek/tests/inventory.spec.ts @@ -48,8 +48,8 @@ async function harness( await ctx.plugin(Loader) if (packageService) await ctx.plugin(PluginPackages) ctx.loader.builtins.include = Include - await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(AgentPresets, { default: 'fixture' }) await ctx.plugin(DeepSeekLlmApiExtensionRegistry) const inventory = enabled === undefined @@ -260,6 +260,7 @@ describe('DeepSeek plugin package inventory', () => { const ctx = new Context() contexts.push(ctx) await ctx.plugin(Loader) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(DeepSeekLlmApiExtensionRegistry) await ctx.plugin(PluginInventory) diff --git a/packages/llm/plugin-package-inventory-deepseek/tests/private-package-composition.spec.ts b/packages/llm/plugin-package-inventory-deepseek/tests/private-package-composition.spec.ts index ce1c4c96cb..3b8607402c 100644 --- a/packages/llm/plugin-package-inventory-deepseek/tests/private-package-composition.spec.ts +++ b/packages/llm/plugin-package-inventory-deepseek/tests/private-package-composition.spec.ts @@ -8,6 +8,7 @@ import { Context, FiberState } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' import Include from '@deepseek-ai/cordis-plugin-include' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import DeepSeekLlmApiExtensionRegistry from '@deepseek-ai/dsh-deepseek-llm-api-extensions' import * as PluginInventory from '../src/index.ts' @@ -41,6 +42,7 @@ it.each([true, false])('reports an unversioned package by name alongside a versi const configPath = join(root, 'cordis.yml') await writeFile(configPath, [ '- name: cordis:agents', + '- name: cordis:projections', '- name: cordis:deepseek-extensions', '- name: cordis:plugin-inventory', `- name: ${JSON.stringify(privateUrl)}`, @@ -54,6 +56,7 @@ it.each([true, false])('reports an unversioned package by name alongside a versi await ctx.plugin(Loader) ctx.loader.builtins.include = Include ctx.loader.builtins.agents = AgentRegistry + ctx.loader.builtins.projections = SessionProjectionRegistry ctx.loader.builtins['deepseek-extensions'] = DeepSeekLlmApiExtensionRegistry ctx.loader.builtins['plugin-inventory'] = PluginInventory await ctx.loader.create({ name: 'cordis:include', config: { path: pathToFileURL(configPath).href } }) diff --git a/packages/llm/token-meter/README.i18n.yaml b/packages/llm/token-meter/README.i18n.yaml index 17a68fbe02..ba794c93da 100644 --- a/packages/llm/token-meter/README.i18n.yaml +++ b/packages/llm/token-meter/README.i18n.yaml @@ -9,8 +9,8 @@ en: e51cc1114d193318 zh: f45cbb692bc8de69 /deepseek-ai-dsh-token-meter/summary: - en: 106e3eda9a85bdd1 - zh: 051bc764bf7a1912 + en: 7f5142b836b0338a + zh: 2735005241e2aae2 /deepseek-ai-dsh-token-meter/table-of-contents: en: d152484eb41ac6b4 zh: 09388d293f9be9cb @@ -24,8 +24,8 @@ en: 58de3b364ab5af6b zh: c5440af43d321e56 /deepseek-ai-dsh-token-meter/use-this-package/session-projections: - en: 6720ae9c7a0602b2 - zh: 699a0b9927300aa3 + en: afa4b026b55ec1ba + zh: 9805b2924b6a786b /deepseek-ai-dsh-token-meter/use-this-package/composition: en: 1a0b7f49e01cba08 zh: 666213a1024767c5 diff --git a/packages/llm/token-meter/README.md b/packages/llm/token-meter/README.md index 51eee2f2ae..214970a2f1 100644 --- a/packages/llm/token-meter/README.md +++ b/packages/llm/token-meter/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -Use `ctx.tokenMeter` to estimate a session's current request and context pressure or price one message. Measurements replay the durable session log, remain deterministic, and make no model calls, so compaction, occupancy displays, and telemetry can share one result. When session projections are available, consumers can read `tokenUsage`, `contextPressure`, and `contextBreakdown`; text and routes without image pricing use an approximate fixed heuristic, declared visual-token pricing applies when available, and files are priced as model-visible handle text. Provider-reported usage is reused only for an identical request envelope; the package adds no model-visible content and makes no loop decisions. +Use `ctx.tokenMeter` to estimate a session's current request and context pressure or price one message. Measurements replay the durable session log, remain deterministic, and make no model calls, so compaction, occupancy displays, and telemetry can share one result. The service requires session projections; consumers can read `tokenUsage`, `contextPressure`, and `contextBreakdown`; text and routes without image pricing use an approximate fixed heuristic, declared visual-token pricing applies when available, and files are priced as model-visible handle text. Provider-reported usage is reused only for an identical request envelope; the package adds no model-visible content and makes no loop decisions. ## Table of Contents @@ -46,7 +46,7 @@ The measurement anchor includes the priced surface immediately before the succes ### Session projections -When the composition provides `ctx.sessionProjections`, token-meter registers three projection units. `tokenUsage` carries the complete durable log's `uncachedInputTokens`, `outputTokens`, `cacheReadTokens`, and `cacheWriteTokens`. A final assistant-message sample replaces streaming usage from the same attempt; `llm/retry-started` ends that replacement scope, so a retry in the same step contributes another billed attempt. `contextPressure` carries optional `pressureTokens` (the newest provider-reported prompt size), optional `projectedTokens` (what the next request's prompt would cost), and optional `contextWindow` from the newest `request/context` record. `contextBreakdown` carries heuristic `systemTokens`, `toolsTokens`, and `messageTokens` — the context's composition, not its provider-billed size. Unloading the plugin removes all three keys. +Token-meter registers three projection units through its required `ctx.sessionProjections` service. Host consumers call `ctx.tokenMeter.contextPressure(session)` to read the current pressure fields synchronously; the registration handle stays private. `tokenUsage` carries the complete durable log's `uncachedInputTokens`, `outputTokens`, `cacheReadTokens`, and `cacheWriteTokens`. A final assistant-message sample replaces streaming usage from the same attempt; `llm/retry-started` ends that replacement scope, so a retry in the same step contributes another billed attempt. `contextPressure` carries optional `pressureTokens` (the newest provider-reported prompt size), optional `projectedTokens` (what the next request's prompt would cost), and optional `contextWindow` from the newest `request/context` record. `contextBreakdown` carries heuristic `systemTokens`, `toolsTokens`, and `messageTokens` — the context's composition, not its provider-billed size. Unloading the plugin removes all three keys. Image offload reprices existing node identities while preserving prior usage anchors. The fixed reference heuristic excludes `offloaded` metadata, so an offload decision does not change `contextBreakdown` or the scalar heuristic total; route-aware measurement replaces the selected visual prices with placeholder text. diff --git a/packages/llm/token-meter/README.zh.md b/packages/llm/token-meter/README.zh.md index 0dc929f492..8f8d4f8a7f 100644 --- a/packages/llm/token-meter/README.zh.md +++ b/packages/llm/token-meter/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -使用 `ctx.tokenMeter` 估算会话当前的请求与上下文压力,或为单条消息计价。测量会回放持久会话日志,结果确定且不进行模型调用,因此压缩、占用显示与遥测可以共享同一结果。会话投影可用时,消费方可以读取 `tokenUsage`、`contextPressure` 与 `contextBreakdown`;文本和没有图片定价的路由采用近似的固定启发式规则,存在声明时应用视觉 token 定价,文件则按模型可见的句柄文本计价。只有请求 envelope 完全相同时才复用提供方报告的用量;本包不添加模型可见内容,也不在 loop 中做决策。 +使用 `ctx.tokenMeter` 估算会话当前的请求与上下文压力,或为单条消息计价。测量会回放持久会话日志,结果确定且不进行模型调用,因此压缩、占用显示与遥测可以共享同一结果。该服务依赖会话投影;消费方可以读取 `tokenUsage`、`contextPressure` 与 `contextBreakdown`;文本和没有图片定价的路由采用近似的固定启发式规则,存在声明时应用视觉 token 定价,文件则按模型可见的句柄文本计价。只有请求 envelope 完全相同时才复用提供方报告的用量;本包不添加模型可见内容,也不在 loop 中做决策。 ## 目录 @@ -46,7 +46,7 @@ const price = ctx.tokenMeter.estimateMessage(message) ### 会话投影 -当组合提供 `ctx.sessionProjections` 时,token-meter 注册三个投影单元。`tokenUsage` 携带完整持久日志中的 `uncachedInputTokens`、`outputTokens`、`cacheReadTokens` 与 `cacheWriteTokens`。最终 assistant 消息样本会替换同一次尝试的流式用量;`llm/retry-started` 会结束该替换范围,因此同一步骤中的重试会贡献另一次计费用量。`contextPressure` 携带可选 `pressureTokens`(提供方报告的最新提示词规模)、可选 `projectedTokens`(下一个请求的提示词将花费多少)与来自最新一条 `request/context` 记录的可选 `contextWindow`。`contextBreakdown` 携带启发式 `systemTokens`、`toolsTokens` 与 `messageTokens`——上下文的构成,而非提供方计费规模。卸载插件会移除全部三个键。 +token-meter 通过必需的 `ctx.sessionProjections` 服务注册三个投影单元。Host 消费方调用 `ctx.tokenMeter.contextPressure(session)` 同步读取当前压力字段;注册句柄保留在内部。`tokenUsage` 携带完整持久日志中的 `uncachedInputTokens`、`outputTokens`、`cacheReadTokens` 与 `cacheWriteTokens`。最终 assistant 消息样本会替换同一次尝试的流式用量;`llm/retry-started` 会结束该替换范围,因此同一步骤中的重试会贡献另一次计费用量。`contextPressure` 携带可选 `pressureTokens`(提供方报告的最新提示词规模)、可选 `projectedTokens`(下一个请求的提示词将花费多少)与来自最新一条 `request/context` 记录的可选 `contextWindow`。`contextBreakdown` 携带启发式 `systemTokens`、`toolsTokens` 与 `messageTokens`——上下文的构成,而非提供方计费规模。卸载插件会移除全部三个键。 图片省略重新计算现有节点的价格,同时保留此前的用量锚点。固定引用启发式规则不计入 `offloaded` 元数据,因此一次省略决定不改变 `contextBreakdown` 或标量启发式总量,按路由的测量则把所选图片的视觉价格换成占位文本价格。 diff --git a/packages/llm/token-meter/src/index.ts b/packages/llm/token-meter/src/index.ts index 7c1ac24432..367a0cab76 100644 --- a/packages/llm/token-meter/src/index.ts +++ b/packages/llm/token-meter/src/index.ts @@ -23,9 +23,10 @@ import { SessionLogOffset, SessionSeq, } from '@deepseek-ai/dsh-session' -// Type-only: activates the `ctx.sessionProjections` Context declaration. -import type {} from '@deepseek-ai/dsh-session-projection' +import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection' +import type { SessionProjectionStateMap } from '@deepseek-ai/dsh-session-projection/types' import type { + ContextPressureProjection, TokenMeasurement, TokenMeasurementBaseline, TokenMeterConfig, @@ -38,7 +39,6 @@ import type { MeterSurfaceNode } from './surface-fold.ts' import { priceSurface } from './route-pricing.ts' export type * from './types.ts' -export { contextPressureProjectionDefinition } from './usage-projection.ts' // Module-edge re-export: forces the emitted index.d.ts to import the // projection-unit modules, so their SessionProjectionStateMap augmentations load // in aggregate programs that only import the package root. @@ -107,6 +107,7 @@ export class TokenMeter extends Service { static inject = ['sessionProjections'] private readonly states = new WeakMap() + private contextPressureProjection!: ProjectionHandle constructor(ctx: Context, config: TokenMeterConfig = {}) { super(ctx, 'tokenMeter') @@ -116,7 +117,7 @@ export class TokenMeter extends Service { protected async [Service.init](): Promise { const ctx = this.ctx await ctx.sessionProjections.register(tokenUsageProjectionDefinition) - await ctx.sessionProjections.register(contextPressureProjectionDefinition) + this.contextPressureProjection = await ctx.sessionProjections.register(contextPressureProjectionDefinition) await ctx.sessionProjections.register(contextBreakdownProjectionDefinition) // Readers catch up independently, while eager observation bounds ordinary @@ -126,6 +127,15 @@ export class TokenMeter extends Service { }) } + /** + * Read the latest logged context occupancy and its projected surface adjustment. + * @param session - Session whose committed usage and request capacity are read. + * @returns context pressure; fields remain absent until their source events are recorded. + */ + contextPressure(session: Session): ContextPressureProjection { + return contextPressureProjectionDefinition.wire.view(this.contextPressureProjection.stateOf(session)) + } + /** * Measure current request pressure and surface through the durable tail. * diff --git a/packages/llm/token-meter/tests/token-usage-projection.spec.ts b/packages/llm/token-meter/tests/token-usage-projection.spec.ts index b34e6e31ff..fee8d43ba5 100644 --- a/packages/llm/token-meter/tests/token-usage-projection.spec.ts +++ b/packages/llm/token-meter/tests/token-usage-projection.spec.ts @@ -361,14 +361,14 @@ function appendAssistant( describe('contextPressure session projection', () => { it('serves no pressure or capacity for an empty log', async () => { const { ctx, session } = await harness() - expect(pressure(ctx, session)).toEqual({}) + expect(ctx.tokenMeter.contextPressure(session)).toEqual({}) }) it('does not synthesize zero pressure before a provider usage sample', async () => { const { ctx, session } = await harness() startStep(session, 1, 1) recordContext(session, 'small', 64_000) - expect(pressure(ctx, session)).toEqual({ contextWindow: 64_000 }) + expect(ctx.tokenMeter.contextPressure(session)).toEqual({ contextWindow: 64_000 }) }) it('sums prompt-side buckets and excludes response output', async () => { @@ -382,7 +382,7 @@ describe('contextPressure session projection', () => { }, 1, 1) // Output is deliberately absent: occupancy describes the prompt that was // sent, so it holds still while the response streams. - expect(pressure(ctx, session).pressureTokens).toBe(125) + expect(ctx.tokenMeter.contextPressure(session)).toEqual({ pressureTokens: 125, projectedTokens: 125 }) }) it('replaces pressure with the newest request rather than accumulating', async () => { diff --git a/packages/plan/plan-mode/src/index.ts b/packages/plan/plan-mode/src/index.ts index 6f05c60d1d..603810713a 100644 --- a/packages/plan/plan-mode/src/index.ts +++ b/packages/plan/plan-mode/src/index.ts @@ -25,7 +25,6 @@ import { Context, Service } from '@deepseek-ai/cordis' import { brandString } from '@deepseek-ai/dsh-brand' import { z as zod } from 'zod' import type { ZodType } from 'zod' -import { turnBoundaryProjectionDefinition, type TurnBoundaryProjection } from '@deepseek-ai/dsh-agent' import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent' import { createUserMessage } from '@deepseek-ai/dsh-llm' import type { ContextFormed } from '@deepseek-ai/dsh-llm' @@ -174,12 +173,11 @@ export const planProjectionDefinition = { * Client carriers expose the projection's cropped `{ active, pending }` view. */ export class PlanModeController extends Service { - static inject = ['tools', 'systemPrompt', 'sessionProjections'] + static inject = ['tools', 'systemPrompt', 'sessionProjections', 'agents'] /** Validated deployment-owned guidance. */ private readonly section: string private projection!: ProjectionHandle - private turnBoundary!: ProjectionHandle /** * Latest selection per session awaiting the next accepted in-turn pre-step. @@ -196,7 +194,6 @@ export class PlanModeController extends Service { protected async [Service.init](): Promise { const ctx = this.ctx this.projection = await ctx.sessionProjections.register(planProjectionDefinition) - this.turnBoundary = await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) let disposed = false // Pre-step is outside Session.append publication, so it can append the // log-only mode event inside an open turn without re-entering the session. @@ -232,7 +229,6 @@ export class PlanModeController extends Service { }, }) - // The command child activates only when a command registry is composed. ctx.inject(['commands'], (commandCtx) => { commandCtx.commands.register({ @@ -385,18 +381,15 @@ export class PlanModeController extends Service { } private hasOpenTurn(session: Session): boolean { - const state = this.turnBoundary.stateOf(session) - return state.openTurnStartSeq !== null + return this.ctx.agents.turnBoundaryOf(session).openTurnStartSeq !== null } private loggedActiveAtLastHeader(session: Session): boolean | undefined { return this.planState(session).activeAtLastHeader ?? undefined } - /** Read the required plan projection state or fail at the first service access. */ private planState(session: Session): PlanUnitState { - const state = this.projection.stateOf(session) - return state + return this.projection.stateOf(session) } /** diff --git a/packages/plan/plan-mode/tests/plan-mode.spec.ts b/packages/plan/plan-mode/tests/plan-mode.spec.ts index 2ba26b9a09..9bc5917ea8 100644 --- a/packages/plan/plan-mode/tests/plan-mode.spec.ts +++ b/packages/plan/plan-mode/tests/plan-mode.spec.ts @@ -13,7 +13,6 @@ import UserQuestionService, { import CommandRuntime from '@deepseek-ai/dsh-commands' import { PtcRuntime, type PtcRunRequest, type PtcRunResult } from '@deepseek-ai/dsh-ptc-runtime' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' -import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent-loop' import PlanModeController, { EXIT_PLAN_MODE, planProjectionDefinition, resolveConfig } from '../src/index.ts' import type { PlanModeConfig } from '../src/index.ts' import type { PlanUnitState } from '../src/types.ts' @@ -89,7 +88,7 @@ function foldPlanMode(events: readonly SessionEvent[], end = events.length): boo async function mountProjectionSeam(ctx: Context): Promise { await ctx.plugin(SessionProjectionRegistry) - await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + await ctx.plugin(AgentRegistry) } async function setup(config: PlanModeConfig = PLAN_CONFIG): Promise { @@ -770,7 +769,6 @@ describe('/plan', () => { describe('exit_plan_mode', () => { async function setupWithReview(answer?: { selected: string[]; custom?: string }) { const ctx = await setup() - await ctx.plugin(AgentRegistry) await ctx.plugin(UserQuestionService) const asked: AskUserQuestionRequest[] = [] if (answer !== undefined) { @@ -850,7 +848,6 @@ describe('exit_plan_mode', () => { it('rejects review from a runtime-owned agent with consumer-neutral guidance', async () => { const ctx = await setup() - await ctx.plugin(AgentRegistry) await ctx.plugin(UserQuestionService) const ask = vi.fn(async () => ({ answers: [{ id: 'plan-review', selected: ['Approve'] }] })) registerQuestionAnswerer(ctx, { ask }) @@ -907,7 +904,6 @@ describe('exit_plan_mode', () => { provideWorkingDirectoryFixture(ctx) await mountProjectionSeam(ctx) await ctx.plugin(PlanModeController, PLAN_CONFIG) - await ctx.plugin(AgentRegistry) await ctx.plugin(UserQuestionService) const asked: AskUserQuestionRequest[] = [] registerQuestionAnswerer(ctx, { @@ -1086,7 +1082,6 @@ describe('exit_plan_mode', () => { await ctx.plugin(ToolRuntime) await mountProjectionSeam(ctx) const fiber = await ctx.plugin(PlanModeController, PLAN_CONFIG) - await ctx.plugin(AgentRegistry) await ctx.plugin(UserQuestionService) let answer!: (value: { answers: { id: string; selected: string[] }[] }) => void registerQuestionAnswerer(ctx, { diff --git a/packages/plan/plan-mode/tests/projection.spec.ts b/packages/plan/plan-mode/tests/projection.spec.ts index 99c2b538ea..8809293a5d 100644 --- a/packages/plan/plan-mode/tests/projection.spec.ts +++ b/packages/plan/plan-mode/tests/projection.spec.ts @@ -25,8 +25,8 @@ async function harness(withPlanMode: boolean): Promise { await ctx.plugin(SystemPrompt, { personaPrefix: '' }) await ctx.plugin(ToolRuntime) await ctx.plugin(UserQuestionService) - await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) if (withPlanMode) await ctx.plugin(PlanModeController, { section: 'plan policy' }) const session = ctx.sessions.create() await ctx.agents.register({ id: session.id, session, status: 'idle', ctx } as Agent) diff --git a/packages/preset/agent-preset-registry/README.i18n.yaml b/packages/preset/agent-preset-registry/README.i18n.yaml index ae11417a45..09ae85b7e6 100644 --- a/packages/preset/agent-preset-registry/README.i18n.yaml +++ b/packages/preset/agent-preset-registry/README.i18n.yaml @@ -21,8 +21,8 @@ en: cd28d9b4f46c46a8 zh: 4921d853f98469fe /deepseek-ai-dsh-agent-preset-registry/understand-the-implementation: - en: 7df811c1347b9a94 - zh: 141a7feb5ca597fe + en: 32c8a5c63022e275 + zh: 1d53e28b0d7375da /deepseek-ai-dsh-agent-preset-registry/further-exploration: en: bbae8d79844bc481 zh: 4302107fd1f460e6 diff --git a/packages/preset/agent-preset-registry/README.md b/packages/preset/agent-preset-registry/README.md index 02530d05d3..bc4b722daf 100644 --- a/packages/preset/agent-preset-registry/README.md +++ b/packages/preset/agent-preset-registry/README.md @@ -57,7 +57,7 @@ Each declaration eagerly creates a registry-owned scope and an in-memory Loader A preset service provider and its consumers must share one `cordis:group` isolation realm. Entry-local isolation does not propagate through the Agent registration scope: ordinary Context lookup outside the group, including `agent.ctx`, still selects the Host realm. `agentPresets.serviceFor()` explicitly inspects the retained composition; it does not change ordinary service lookup. -Activation auditing checks imports, missing services and globally leaked services. Import failures, activation failures and leaks reject the mount. A row waiting for a Host service stays mounted, and every read and binding re-audits it after the Host Loader tree settles, so startup order does not decide the outcome. Failure prevents new bindings to that definition. Session logs retain the preset ID and blank-session selections; recovery after restart uses the current definition of that ID and rejects a missing definition. `select` leaves a blank Session that records the requested preset and already runs its current revision unchanged, without another selection record or `tools/change`. +Activation auditing checks imports, missing services and globally leaked services. Import failures, activation failures and leaks reject the mount. A row waiting for a Host service stays mounted, and every read and binding re-audits it after the Host Loader tree settles, so startup order does not decide the outcome. Failure prevents new bindings to that definition. `presetForSession(session)` synchronously reads the latest recorded preset ID, or null when none is recorded. Session logs retain the preset ID and blank-session selections; recovery after restart uses the current definition of that ID and rejects a missing definition. `select` leaves a blank Session that records the requested preset and already runs its current revision unchanged, without another selection record or `tools/change`. `inspectCompositions()` reads all revisions retained by this registry; passing an Agent context selects its exact revision, or returns an empty list when it has no binding here. Each `AgentPresetInspection` contains a preset ID, detached active-module references with their resolution bases, and leaked-service names. Inspectors query the runtime service rather than a module-local mount table; the results contain no Loader trees or fibers. [composition-inventory.ts](src/composition-inventory.ts) defines these records. diff --git a/packages/preset/agent-preset-registry/README.zh.md b/packages/preset/agent-preset-registry/README.zh.md index 334a72fd96..5d9a105c42 100644 --- a/packages/preset/agent-preset-registry/README.zh.md +++ b/packages/preset/agent-preset-registry/README.zh.md @@ -57,7 +57,7 @@ Web 内置定义来自 `dsh-web-app` bundle。定义使用普通插件行;注 preset 的服务提供方及其消费方必须共享同一个 `cordis:group` 隔离 realm。条目局部隔离不会沿 Agent 注册 scope 传播:组外的普通 Context 查找(包括 `agent.ctx`)仍选择 Host realm。`agentPresets.serviceFor()` 显式检查保留的组合,不改变普通服务查找。 -激活审计检查导入失败、缺失服务和向全局泄漏的服务。导入失败、激活失败和泄漏会拒绝挂载。等待 Host 服务的行保持挂载,每次读取和绑定都在 Host Loader 树结算后重新审计,因此启动顺序不决定结果。失败只禁用该定义的新绑定。会话日志保存 preset ID 和空白会话的切换记录;重启恢复使用该 ID 的当前定义,缺失时拒绝恢复。若空白会话已记录所请求的 preset 且正运行其当前版本,`select` 不做任何改动,也不追加切换记录或触发 `tools/change`。 +激活审计检查导入失败、缺失服务和向全局泄漏的服务。导入失败、激活失败和泄漏会拒绝挂载。等待 Host 服务的行保持挂载,每次读取和绑定都在 Host Loader 树结算后重新审计,因此启动顺序不决定结果。失败只禁用该定义的新绑定。`presetForSession(session)` 同步读取最新记录的预设 ID,未记录时返回 null。会话日志保存 preset ID 和空白会话的切换记录;重启恢复使用该 ID 的当前定义,缺失时拒绝恢复。若空白会话已记录所请求的 preset 且正运行其当前版本,`select` 不做任何改动,也不追加切换记录或触发 `tools/change`。 `inspectCompositions()` 读取本注册表仍保留的全部代际;传入 Agent 上下文时只选择它绑定的精确代际,在此没有绑定时返回空列表。每个 `AgentPresetInspection` 包含 preset ID、纯数据形式的活动模块引用及解析基址,以及泄漏的服务名。检查器查询运行中的服务,而不是模块级挂载表;结果不包含 Loader 树或 fiber。这些记录定义在 [composition-inventory.ts](src/composition-inventory.ts) 中。 diff --git a/packages/preset/agent-preset-registry/src/index.ts b/packages/preset/agent-preset-registry/src/index.ts index 6e79d2ded7..4b4bb467d0 100644 --- a/packages/preset/agent-preset-registry/src/index.ts +++ b/packages/preset/agent-preset-registry/src/index.ts @@ -1,5 +1,4 @@ /** Declarative Agent capability sets, activation and session binding. */ -import { turnBoundaryProjectionDefinition, type TurnBoundaryProjection } from '@deepseek-ai/dsh-agent' import type { ProjectionHandle } from '@deepseek-ai/dsh-session-projection' import { Context, Service } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' @@ -8,6 +7,7 @@ import { bindScopeParent, createScope, scopeOf, scopeParentOf, type Scope, type import { entryListSchema } from '@deepseek-ai/cordis-plugin-include' import { dump } from 'js-yaml' import type { Agent } from '@deepseek-ai/dsh-agent' +import type { Session } from '@deepseek-ai/dsh-session' // Type-only: the optional `settings` service this registry keeps off the generated pages. import type {} from '@deepseek-ai/dsh-settings' import type {} from '@deepseek-ai/dsh-tools' @@ -52,12 +52,11 @@ interface Binding { /** Registry of YAML-declared presets and the revisions live Agents retain. */ export class AgentPresetRegistry extends TypertRemoteService { - static inject = ['loader', 'sessionProjections'] + static inject = ['loader', 'sessionProjections', 'agents'] static Config = z.object({ default: z.string().required(), selectedDefault: z.string().volatile(), }) - private turnBoundary!: ProjectionHandle private agentPreset!: ProjectionHandle private readonly owner: Context private readonly definitions = new Map() @@ -73,7 +72,6 @@ export class AgentPresetRegistry extends TypertRemoteService { protected async [Service.init](): Promise { const ctx = this.owner this.agentPreset = await ctx.sessionProjections.register(agentPresetProjectionDefinition) - this.turnBoundary = await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) ctx.inject(['settings'], (child) => { child.effect(() => child.settings.configure({ auto: false }, ctx.fiber)) }) ctx.on('session/event', (session, event) => { if (event.type === 'agent-preset/selected') ctx.emit('agent-preset/selected', session.id, event.data.agentPreset) @@ -83,6 +81,15 @@ export class AgentPresetRegistry extends TypertRemoteService { /** Default preset for a subsequently created session. */ get defaultId(): string { return this.config.selectedDefault.get() ?? this.config.default } + /** + * Read the preset recorded for a Session, including selections no longer in the roster. + * @param session - Session whose committed preset selection is read. + * @returns the recorded preset, or null when none was selected. + */ + presetForSession(session: Session): string | null { + return this.agentPreset.stateOf(session) + } + /** Register and eagerly load a definition; activation failure remains visible in the roster. * @param definition Parsed configuration supplied by the declaring plugin. * @returns Definition disposer after activation or its diagnostic settles; the declaring plugin owns it. @@ -357,7 +364,7 @@ export class AgentPresetRegistry extends TypertRemoteService { @Remote('select') async select(agent: Agent, agentPreset: string): Promise { const turn = (this.switches.get(agent.id) ?? Promise.resolve()).then(async () => { - const boundary = this.turnBoundary.stateOf(agent.session) + const boundary = this.owner.agents.turnBoundaryOf(agent.session) if (boundary.openTurnStartSeq !== null || boundary.lastTurn > 0) { throw new RemoteError('agent-preset/locked', 'This session has already started', { sessionId: agent.id, agentPreset }) } diff --git a/packages/preset/agent-preset-registry/tests/registry.spec.ts b/packages/preset/agent-preset-registry/tests/registry.spec.ts index 9616f78769..2c9c4486f3 100644 --- a/packages/preset/agent-preset-registry/tests/registry.spec.ts +++ b/packages/preset/agent-preset-registry/tests/registry.spec.ts @@ -1,12 +1,12 @@ import { afterEach, describe, expect, it } from 'vitest' import { createScope } from '@deepseek-ai/dsh-scope' -import { assembleContextFor } from '@deepseek-ai/dsh-agent' +import AgentRegistry, { assembleContextFor } from '@deepseek-ai/dsh-agent' import { entryListProblem } from '../src/index.ts' import { currentKey, harness, declare, contribution, agentOn, liveRegistries, plugin } from './harness.ts' import { omitsGeneratedPage } from '../../../settings/settings/tests/live-config.ts' import Loader from '@deepseek-ai/cordis-plugin-loader' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' -import AgentPresets, { agentPresetProjectionDefinition } from '../src/index.ts' +import AgentPresets from '../src/index.ts' import type { Context } from '@deepseek-ai/cordis' const contexts: Context[] = [] @@ -246,6 +246,7 @@ it('ignores a retired modeSelectionEnabled field in the user patch', async () => it('keeps its own instance off the generated Settings pages', () => omitsGeneratedPage(async (ctx) => { await ctx.plugin(Loader) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) return ctx.plugin(AgentPresets, { default: 'standard' }) })) @@ -317,12 +318,11 @@ it('logs the default preset a blank Session runs once it is selected without a r const ctx = await setup() await declare(ctx, contribution('standard')) const agent = await agentOn(ctx, 'unrecorded') - const preset = await ctx.sessionProjections.register(agentPresetProjectionDefinition) - expect(preset.stateOf(agent.session)).toBeNull() + expect(ctx.agentPresets.presetForSession(agent.session)).toBeNull() expect(await ctx.agentPresets.select(agent, 'standard')).toBe('standard') - expect(preset.stateOf(agent.session)).toBe('standard') + expect(ctx.agentPresets.presetForSession(agent.session)).toBe('standard') }) it('recomposes and logs a repeated selection once the preset definition was replaced', async () => { diff --git a/packages/schedule/schedule/tests/harness.ts b/packages/schedule/schedule/tests/harness.ts index bf152ad187..768125a479 100644 --- a/packages/schedule/schedule/tests/harness.ts +++ b/packages/schedule/schedule/tests/harness.ts @@ -1,6 +1,7 @@ /** Host Schedule tests use real domain storage and an explicit Session resolver. */ import { Context, Service } from '@deepseek-ai/cordis' import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import { unsupportedInbox } from '@deepseek-ai/dsh-agent-loop-testkit' import type { UserMessage } from '@deepseek-ai/dsh-llm' import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' @@ -72,6 +73,7 @@ export async function harness(options: { const ctx = new Context() options.onContext?.(ctx) await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(SystemPrompt, {}) await ctx.plugin(ToolRuntime) diff --git a/packages/sdk/server/tests/server.spec.ts b/packages/sdk/server/tests/server.spec.ts index 2553bb43a2..f5d4e785cd 100644 --- a/packages/sdk/server/tests/server.spec.ts +++ b/packages/sdk/server/tests/server.spec.ts @@ -12,6 +12,7 @@ import { Context } from '@deepseek-ai/cordis' import { scopeTarget } from '@deepseek-ai/dsh-scope' import { randomUUID } from 'node:crypto' import AgentRegistry, { type Agent, type AgentHandle } from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit' import { MockAdapter, textResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' @@ -456,6 +457,7 @@ describe('HarnessSdkJsonRpcServer', () => { it('forwards whole-agent status without attributing a turn outcome', async () => { const ctx = new Context() await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const transport = new FakeTransport() const server = new HarnessSdkJsonRpcServer(ctx, transport) diff --git a/packages/session/session-projection/README.i18n.yaml b/packages/session/session-projection/README.i18n.yaml index 90321e2e39..96becd6d49 100644 --- a/packages/session/session-projection/README.i18n.yaml +++ b/packages/session/session-projection/README.i18n.yaml @@ -18,14 +18,14 @@ en: 3a15581f0b043e79 zh: 302fc8afe8fb45fa /deepseek-ai-dsh-session-projection/use-this-package/when-to-choose-it: - en: 7ea79ed69e0ea085 - zh: b7791be4c547495c + en: e16fd6f4fcace77c + zh: 4488f7f0b4751ea4 /deepseek-ai-dsh-session-projection/use-this-package/define-a-projection-unit: en: 09758d1475fad86e zh: 473637cb34aebe72 /deepseek-ai-dsh-session-projection/use-this-package/register-and-read: - en: 52120bc6e18a8da8 - zh: c45ca6a83a6fcd4e + en: 43af8fda90742877 + zh: 4d03fef88cb0eb54 /deepseek-ai-dsh-session-projection/use-this-package/persisted-checkpoints: en: 45f1c05fb7e08180 zh: f8d62c749899651a diff --git a/packages/session/session-projection/README.md b/packages/session/session-projection/README.md index 9a1330ed60..4ddaed7c6b 100644 --- a/packages/session/session-projection/README.md +++ b/packages/session/session-projection/README.md @@ -29,7 +29,7 @@ Mount `dsh-session-projection` wherever client carriers need current values of l ### When to choose it -Choose it when a domain keeps state that clients should see without re-deriving it — a todo list, a goal snapshot, conversation statistics. The registry drives units eagerly over committed events, so any registered unit's value is current by construction. A unit without a `wire` block keeps its state host-only. Host readers declare `sessionProjections` in their plugin dependencies and await registration before exposing synchronous reads. +Choose it when a domain keeps state that clients should see without re-deriving it — a todo list, a goal snapshot, conversation statistics. The registry drives units eagerly over committed events, so any registered unit's value is current by construction. A unit without a `wire` block keeps its state host-only. Domain owners declare `sessionProjections` in their plugin dependencies and await registration before exposing synchronous service methods. Other plugins read those methods through the owning service. ### Define a projection unit @@ -63,7 +63,7 @@ const state = projection.stateOf(session) const { asOfSeq, values } = ctx.sessionProjections.snapshot(session) ``` -A domain that requires projected state declares `sessionProjections` as a Cordis service dependency; optional contributors may register under `ctx.inject(['sessionProjections'], …)`. Carriers use `ctx.get('sessionProjections')` and omit their block or frames when the registry is absent. +A domain that owns projected state declares `sessionProjections` as a Cordis service dependency and keeps its handle private. Consumers inject that domain service and use its methods, such as `ctx.agents.turnBoundaryOf(session)`, without importing definitions or retaining projection readers. Optional contributors may register under `ctx.inject(['sessionProjections'], …)`. Carriers use `ctx.get('sessionProjections')` and omit their block or frames when the registry is absent. ### Persisted checkpoints diff --git a/packages/session/session-projection/README.zh.md b/packages/session/session-projection/README.zh.md index 865d9e8537..de05300b76 100644 --- a/packages/session/session-projection/README.zh.md +++ b/packages/session/session-projection/README.zh.md @@ -29,7 +29,7 @@ kind: "package-reference" ### 何时选择 -当领域保存客户端应看到、但不应自行重新派生的状态——todo 清单、goal 快照、对话统计——时选择本包。注册表在已提交事件上主动驱动单元,因此任何已注册单元的值按构造即为当前值。不带 `wire` 块的单元将状态保留在 host。host 读取方在插件依赖中声明 `sessionProjections`,并等待注册完成后才提供同步读取。 +当领域保存客户端应看到、但不应自行重新派生的状态——todo 清单、goal 快照、对话统计——时选择本包。注册表在已提交事件上主动驱动单元,因此任何已注册单元的值按构造即为当前值。不带 `wire` 块的单元将状态保留在 host。领域所有者在插件依赖中声明 `sessionProjections`,并等待注册完成后才提供同步服务方法。其他插件通过所属服务调用这些方法。 ### 定义投影单元 @@ -63,7 +63,7 @@ const state = projection.stateOf(session) const { asOfSeq, values } = ctx.sessionProjections.snapshot(session) ``` -必须使用投影状态的领域把 `sessionProjections` 声明为 Cordis 服务依赖;可选贡献方可以在 `ctx.inject(['sessionProjections'], …)` 下注册。载体使用 `ctx.get('sessionProjections')`,注册表缺席时省略自己的块或帧。 +持有投影状态的领域将 `sessionProjections` 声明为 Cordis 服务依赖,并在内部保留句柄。消费方注入该领域服务并使用其方法,例如 `ctx.agents.turnBoundaryOf(session)`,无需导入定义或保留投影读取句柄。可选贡献方可以在 `ctx.inject(['sessionProjections'], …)` 下注册。载体使用 `ctx.get('sessionProjections')`,注册表缺席时省略自己的块或帧。 ### 持久检查点 diff --git a/packages/session/session-stats/src/index.ts b/packages/session/session-stats/src/index.ts index 6439070f01..570a21daaa 100644 --- a/packages/session/session-stats/src/index.ts +++ b/packages/session/session-stats/src/index.ts @@ -14,8 +14,9 @@ import { sessionStatsProjectionDefinition } from './projection.ts' export type * from './types.ts' +/** Cordis plugin name. */ export const name = 'session-stats' - +/** The projection registry is the plugin's whole purpose; without it the fiber stays pending. */ export const inject = ['sessionProjections'] /** diff --git a/packages/session/session-title-first-prompt-llm/tests/loader-composition.spec.ts b/packages/session/session-title-first-prompt-llm/tests/loader-composition.spec.ts index 410e4c2ecf..5dce4f54d5 100644 --- a/packages/session/session-title-first-prompt-llm/tests/loader-composition.spec.ts +++ b/packages/session/session-title-first-prompt-llm/tests/loader-composition.spec.ts @@ -12,6 +12,7 @@ import * as LlmPiAi from '@deepseek-ai/dsh-llm-pi-ai' import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' import type { Session, SessionSeq } from '@deepseek-ai/dsh-session' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' +import AgentRegistry from '@deepseek-ai/dsh-agent' import SessionTitleService from '@deepseek-ai/dsh-session-title' import * as providerPlugin from '@deepseek-ai/dsh-session-title-first-prompt-llm' import { anthropicTextEvents, closeMockServers, mockServer } from '../../../llm/llm-pi-ai/tests/mock-server.ts' @@ -56,6 +57,7 @@ async function loadComposition( "- name: '@deepseek-ai/dsh-llm'", "- name: '@deepseek-ai/dsh-session'", "- name: '@deepseek-ai/dsh-session-projection'", + "- name: '@deepseek-ai/dsh-agent'", "- name: '@deepseek-ai/dsh-session-title'", ' config:', ' fallbackMaxWords: 5', @@ -78,6 +80,7 @@ async function loadComposition( ['@deepseek-ai/dsh-llm', LlmRuntime], ['@deepseek-ai/dsh-session', SessionStore], ['@deepseek-ai/dsh-session-projection', SessionProjectionRegistry], + ['@deepseek-ai/dsh-agent', AgentRegistry], ['@deepseek-ai/dsh-session-title', SessionTitleService], ['@deepseek-ai/dsh-session-title-first-prompt-llm', providerPlugin], ...entryModules, diff --git a/packages/session/session-title-first-prompt-llm/tests/provider.spec.ts b/packages/session/session-title-first-prompt-llm/tests/provider.spec.ts index 92565e1cc9..d4d8fa6615 100644 --- a/packages/session/session-title-first-prompt-llm/tests/provider.spec.ts +++ b/packages/session/session-title-first-prompt-llm/tests/provider.spec.ts @@ -1,10 +1,10 @@ +import AgentRegistry from '@deepseek-ai/dsh-agent' import { Context } from '@deepseek-ai/cordis' import { describe, expect, it, onTestFinished } from 'vitest' import LlmRuntime, { createUserMessage, LlmAdapter, ReasoningEffortId, ToolCallId } from '@deepseek-ai/dsh-llm' import type { GenerateOptions, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm' import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' -import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent-loop' import SessionTitleService, { type SessionTitleProvider } from '@deepseek-ai/dsh-session-title' import * as providerPlugin from '@deepseek-ai/dsh-session-title-first-prompt-llm' @@ -76,7 +76,7 @@ async function harness( await ctx.plugin(LlmRuntime) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionTitleService, TITLE_CONFIG) const adapter = new RecordingAdapter(script, reasoningEfforts) ctx.llm.registerAdapter(['title-route'], adapter) @@ -249,7 +249,7 @@ describe('first-prompt LLM title provider', () => { await ctx.plugin(LlmRuntime) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionTitleService, TITLE_CONFIG) // Delegate to the real registration while capturing it, to reach the guard // the service never produces. diff --git a/packages/session/session-title/src/index.ts b/packages/session/session-title/src/index.ts index 3d5dc3d7e0..8b888b80d4 100644 --- a/packages/session/session-title/src/index.ts +++ b/packages/session/session-title/src/index.ts @@ -3,7 +3,6 @@ * @module @deepseek-ai/dsh-session-title */ -import { turnBoundaryProjectionDefinition, type TurnBoundaryProjection } from '@deepseek-ai/dsh-agent' import { Context, FiberState, Service, type Fiber } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import { z as zod } from 'zod' @@ -19,6 +18,7 @@ import type { import { SessionSeq } from '@deepseek-ai/dsh-session' import type {} from '@deepseek-ai/dsh-session-projection' import type { ProjectionDefinition, ProjectionHandle } from '@deepseek-ai/dsh-session-projection' +import type {} from '@deepseek-ai/dsh-agent' export type { SessionTitleEventData, SessionTitleModelIdentity, @@ -299,14 +299,13 @@ export function foldSessionTitle(events: readonly SessionEvent[]): SessionTitleS /** Log-backed title fold plus asynchronous fallback generation. */ export class SessionTitleService extends Service { - static inject = ['sessions', 'sessionProjections'] + static inject = ['sessions', 'sessionProjections', 'agents'] static Config: z = z.object({ fallbackMaxWords: z.number().step(1).min(1).required(), fallbackMaxBytes: z.number().step(1).min(1).required(), maxTitleBytes: z.number().step(1).min(1).required(), }) - private turnBoundary!: ProjectionHandle private titleInput!: ProjectionHandle private readonly config: ResolvedConfig private readonly ownerFiber: Fiber @@ -334,8 +333,6 @@ export class SessionTitleService extends Service { protected async [Service.init](): Promise { const ctx = this.ctx - this.turnBoundary = await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) - ctx.effect(() => async () => { this.lifetime.abort(new Error('session-title service disposed')) if (this.registration !== undefined) this.registration.closing = true @@ -556,7 +553,7 @@ export class SessionTitleService extends Service { const state = session === undefined ? undefined : this.work.get(session) const pending = state?.pending if (session === undefined || state === undefined || pending === undefined) return - const boundary = this.turnBoundary.stateOf(session).lastStepBoundary + const boundary = this.ctx.agents.turnBoundaryOf(session).lastStepBoundary const route = session.requestHeader()?.config if (boundary?.kind !== 'start' || route?.provider !== options.provider diff --git a/packages/session/session-title/tests/persistence.spec.ts b/packages/session/session-title/tests/persistence.spec.ts index dd85dd1376..4edfdd5579 100644 --- a/packages/session/session-title/tests/persistence.spec.ts +++ b/packages/session/session-title/tests/persistence.spec.ts @@ -5,6 +5,7 @@ import { mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' +import AgentRegistry from '@deepseek-ai/dsh-agent' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import * as JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl' import SessionTitleService, { foldSessionTitle } from '@deepseek-ai/dsh-session-title' @@ -68,6 +69,7 @@ describe('session title persistence round trips', () => { const writer = new Context() await writer.plugin(SessionStore) await writer.plugin(SessionProjectionRegistry) + await writer.plugin(AgentRegistry) await writer.plugin(JsonlSessionPersistence, { root, compression: 'none' }) await writer.plugin(SessionTitleService, CONFIG) await appendPersistedTitle(writer, id) diff --git a/packages/session/session-title/tests/projection.spec.ts b/packages/session/session-title/tests/projection.spec.ts index 784df3c69d..aa39739120 100644 --- a/packages/session/session-title/tests/projection.spec.ts +++ b/packages/session/session-title/tests/projection.spec.ts @@ -3,6 +3,7 @@ import { Context } from '@deepseek-ai/cordis' import { createUserMessage } from '@deepseek-ai/dsh-llm' import SessionStore, { SessionId, SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session' import type { Session, SessionSeq as SessionSeqType } from '@deepseek-ai/dsh-session' +import AgentRegistry from '@deepseek-ai/dsh-agent' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import SessionTitleService from '@deepseek-ai/dsh-session-title' @@ -12,6 +13,7 @@ async function harness(withTitleService: boolean): Promise<{ ctx: Context; sessi const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) if (withTitleService) await ctx.plugin(SessionTitleService, CONFIG) return { ctx, session: ctx.sessions.create(SessionId('titled')) } } diff --git a/packages/session/session-title/tests/provider.spec.ts b/packages/session/session-title/tests/provider.spec.ts index f08002e145..f7e7830048 100644 --- a/packages/session/session-title/tests/provider.spec.ts +++ b/packages/session/session-title/tests/provider.spec.ts @@ -1,10 +1,10 @@ +import AgentRegistry from '@deepseek-ai/dsh-agent' import { Context } from '@deepseek-ai/cordis' import { describe, expect, it, onTestFinished, vi } from 'vitest' import LlmRuntime, { createUserMessage, markAgentLoopRequest } from '@deepseek-ai/dsh-llm' import { deepFreeze } from '@deepseek-ai/dsh-util-values' import SessionStore, { SessionId, SessionSeq } from '@deepseek-ai/dsh-session' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' -import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent-loop' import SessionTitleService, { SessionTitleProviderId, type SessionTitleProvider, @@ -56,6 +56,7 @@ describe('SessionTitleService Provider lifecycle', () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionTitleService, CONFIG) const parent = ctx.sessions.create(SessionId('title-parent')) parent.append('turn/start', { @@ -121,6 +122,7 @@ describe('SessionTitleService Provider lifecycle', () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionTitleService, CONFIG) const requests: SessionTitleProviderRequest[] = [] const provider: SessionTitleProvider = { @@ -185,6 +187,7 @@ describe('SessionTitleService Provider lifecycle', () => { onTestFinished(() => ctx.fiber.dispose()) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionTitleService, { fallbackMaxWords: 5, fallbackMaxBytes: 1, maxTitleBytes: 24 }) const currentTitles: (SessionTitleSnapshot | undefined)[] = [] ctx.sessionTitle.register({ @@ -214,6 +217,7 @@ describe('SessionTitleService Provider lifecycle', () => { onTestFinished(() => ctx.fiber.dispose()) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionTitleService, CONFIG) const currentTitles: (SessionTitleSnapshot | undefined)[] = [] ctx.sessionTitle.register({ @@ -255,6 +259,7 @@ describe('SessionTitleService Provider lifecycle', () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionTitleService, CONFIG) const session = ctx.sessions.create(SessionId('chunked-provider-input')) session.append('turn/start', { turn: 1 }) @@ -284,6 +289,7 @@ describe('SessionTitleService Provider lifecycle', () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionTitleService, CONFIG) const requests: SessionTitleProviderRequest[] = [] ctx.sessionTitle.register({ @@ -320,6 +326,7 @@ describe('SessionTitleService Provider lifecycle', () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionTitleService, CONFIG) const pending = deferred() let observedSignal: AbortSignal | undefined @@ -372,6 +379,7 @@ describe('SessionTitleService Provider lifecycle', () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionTitleService, CONFIG) const pending = deferred() const dispose = ctx.sessionTitle.register({ @@ -407,6 +415,7 @@ describe('SessionTitleService Provider lifecycle', () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionTitleService, CONFIG) const firstResult = deferred() const requests: SessionTitleProviderRequest[] = [] @@ -452,7 +461,7 @@ describe('SessionTitleService Provider lifecycle', () => { await ctx.plugin(LlmRuntime) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionTitleService, CONFIG) const requests: SessionTitleProviderRequest[] = [] ctx.sessionTitle.register({ @@ -508,7 +517,7 @@ describe('SessionTitleService Provider lifecycle', () => { await ctx.plugin(LlmRuntime) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) - await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionTitleService, CONFIG) const generate = vi.fn(async (request: SessionTitleProviderRequest): Promise => ({ title: 'Unexpected title', @@ -541,6 +550,7 @@ describe('SessionTitleService Provider lifecycle', () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionTitleService, CONFIG) const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => undefined) const provider: SessionTitleProvider = { diff --git a/packages/session/session-title/tests/rename.spec.ts b/packages/session/session-title/tests/rename.spec.ts index 9654c18edc..184c03df88 100644 --- a/packages/session/session-title/tests/rename.spec.ts +++ b/packages/session/session-title/tests/rename.spec.ts @@ -5,6 +5,7 @@ import { Context } from '@deepseek-ai/cordis' import { describe, expect, it, vi } from 'vitest' import { createUserMessage } from '@deepseek-ai/dsh-llm' import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session' +import AgentRegistry from '@deepseek-ai/dsh-agent' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import SessionTitleService, { SessionTitleProviderId, @@ -34,6 +35,7 @@ describe('SessionTitleService.rename', () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionTitleService, CONFIG) const session = ctx.sessions.create(SessionId('rename-accept')) session.append('turn/start', { turn: 1 }) @@ -60,6 +62,7 @@ describe('SessionTitleService.rename', () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionTitleService, CONFIG) const session = ctx.sessions.create(SessionId('rename-reject')) expect(() => ctx.sessionTitle.rename(session, '  ')).toThrow(/visible characters/) @@ -72,6 +75,7 @@ describe('SessionTitleService.rename', () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionTitleService, CONFIG) const generate = vi.fn(async (request: SessionTitleProviderRequest) => ({ title: 'Provider title', @@ -110,6 +114,7 @@ describe('SessionTitleService.rename', () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionTitleService, CONFIG) const session = ctx.sessions.create(SessionId('rename-unpin-fallback')) session.append('turn/start', { turn: 1 }) @@ -132,6 +137,7 @@ describe('SessionTitleService.rename', () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionTitleService, CONFIG) // The provider parks on a test-held deferred so rename lands while its // generation is ACTIVE (not merely scheduled). @@ -172,6 +178,7 @@ describe('SessionTitleService.rename', () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) // A 3-byte fallback cap cannot hold the 4-byte emoji prompt: the // re-derived fallback is empty, so the pinned title survives the refresh. await ctx.plugin(SessionTitleService, { ...CONFIG, fallbackMaxBytes: 3 }) diff --git a/packages/session/session-title/tests/service-contracts.spec.ts b/packages/session/session-title/tests/service-contracts.spec.ts index c6e0dc93c3..644b6074f9 100644 --- a/packages/session/session-title/tests/service-contracts.spec.ts +++ b/packages/session/session-title/tests/service-contracts.spec.ts @@ -2,6 +2,7 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm' import { Context, type Fiber } from '@deepseek-ai/cordis' import { describe, expect, it, vi } from 'vitest' import SessionStore, { Session, SessionId, SessionSeq } from '@deepseek-ai/dsh-session' +import AgentRegistry from '@deepseek-ai/dsh-agent' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import SessionTitleService, { SessionTitleProviderId, @@ -31,6 +32,7 @@ async function setup(config: Config = CONFIG): Promise { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionTitleService, config) return ctx } @@ -244,6 +246,7 @@ describe('SessionTitleService configuration and refresh boundaries', () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) const lifecycle: { fiber?: Fiber; session?: Session; inactiveRefresh?: Promise } = {} ctx.on('internal/plugin', (subject) => { if (subject !== lifecycle.fiber || subject.uid !== null || lifecycle.session === undefined) return @@ -277,6 +280,7 @@ describe('SessionTitleService configuration and refresh boundaries', () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) const fiber = await ctx.plugin(SessionTitleService, CONFIG) const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => undefined) const session = startSession(ctx, 'service-unload-started-fallback') @@ -293,6 +297,7 @@ describe('SessionTitleService configuration and refresh boundaries', () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) const fiber = await ctx.plugin(SessionTitleService, CONFIG) const result = deferred() const requests: SessionTitleProviderRequest[] = [] diff --git a/packages/session/session-title/tests/session-title.spec.ts b/packages/session/session-title/tests/session-title.spec.ts index 1349c52d4b..43d439bb06 100644 --- a/packages/session/session-title/tests/session-title.spec.ts +++ b/packages/session/session-title/tests/session-title.spec.ts @@ -3,6 +3,7 @@ import type { ContextFormed } from '@deepseek-ai/dsh-llm' import { Context } from '@deepseek-ai/cordis' import { describe, expect, it } from 'vitest' import SessionStore, { Session, SessionId, SessionSeq } from '@deepseek-ai/dsh-session' +import AgentRegistry from '@deepseek-ai/dsh-agent' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import SessionTitleService, { SessionTitleProviderId, @@ -48,6 +49,7 @@ describe('SessionTitleService', () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionTitleService, CONFIG) const session = ctx.sessions.create(SessionId('fresh')) session.append('turn/start', { @@ -85,6 +87,7 @@ describe('SessionTitleService', () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionTitleService, CONFIG) const session = ctx.sessions.create(SessionId('prefixed-title')) session.append('user/message', createUserMessage({ @@ -113,6 +116,7 @@ describe('SessionTitleService', () => { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionTitleService, CONFIG) const session = ctx.sessions.create(SessionId('eligibility')) session.append('turn/start', { diff --git a/packages/session/session-title/tsconfig.json b/packages/session/session-title/tsconfig.json index e056eda2bd..5755ac55e2 100644 --- a/packages/session/session-title/tsconfig.json +++ b/packages/session/session-title/tsconfig.json @@ -31,6 +31,7 @@ }, { "path": "../../core/agent" + } ] } diff --git a/packages/session/session-turn-outline/src/index.ts b/packages/session/session-turn-outline/src/index.ts index ab2c5f667e..a5dd6f2198 100644 --- a/packages/session/session-turn-outline/src/index.ts +++ b/packages/session/session-turn-outline/src/index.ts @@ -14,8 +14,9 @@ import { turnOutlineProjectionDefinition } from './projection.ts' export type * from './types.ts' +/** Cordis plugin name. */ export const name = 'session-turn-outline' - +/** The projection registry is the plugin's whole purpose; without it the fiber stays pending. */ export const inject = ['sessionProjections'] /** diff --git a/packages/shell/tool-bash-persistent/tests/tools.spec.ts b/packages/shell/tool-bash-persistent/tests/tools.spec.ts index d3507373c7..a094cd0d13 100644 --- a/packages/shell/tool-bash-persistent/tests/tools.spec.ts +++ b/packages/shell/tool-bash-persistent/tests/tools.spec.ts @@ -4,6 +4,7 @@ import { Context } from '@deepseek-ai/cordis' import { ToolCallId } from '@deepseek-ai/dsh-llm' import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Agent } from '@deepseek-ai/dsh-agent' import TerminalSessionService from '@deepseek-ai/dsh-terminal' import type { @@ -307,6 +308,7 @@ async function setup( contexts.push(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(TerminalSessionService) const stub = stubBackend(initialMode) @@ -715,6 +717,7 @@ describe('tool-bash-persistent', () => { contexts.push(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(TerminalSessionService) const spawnStarted = Promise.withResolvers() diff --git a/packages/shell/tool-bash/tests/background.spec.ts b/packages/shell/tool-bash/tests/background.spec.ts index a4c6cd41de..c2e5db5201 100644 --- a/packages/shell/tool-bash/tests/background.spec.ts +++ b/packages/shell/tool-bash/tests/background.spec.ts @@ -9,6 +9,7 @@ import { SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session/types' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { TOOL_ABORTED } from '@deepseek-ai/dsh-tools' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Agent } from '@deepseek-ai/dsh-agent' import { provideWorkingDirectoryFixture, unsupportedInbox } from '@deepseek-ai/dsh-agent-loop-testkit' import LocalJobRegistry from '@deepseek-ai/dsh-jobs-local' @@ -37,6 +38,7 @@ async function setup() { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry, { pumpPollMs: 25 }) await ctx.plugin(ToolTasks) @@ -424,6 +426,7 @@ describe('foreground commands as jobs', () => { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry, { pumpPollMs: 25 }) await ctx.plugin(ToolTasks) diff --git a/packages/shell/tool-bash/tests/tools.spec.ts b/packages/shell/tool-bash/tests/tools.spec.ts index 96fd9aa7b8..9a95c328f9 100644 --- a/packages/shell/tool-bash/tests/tools.spec.ts +++ b/packages/shell/tool-bash/tests/tools.spec.ts @@ -11,7 +11,6 @@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { TOOL_ABORTED, TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools' import AgentRegistry from '@deepseek-ai/dsh-agent' import type { Agent } from '@deepseek-ai/dsh-agent' -import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent-loop' import { SessionId } from '@deepseek-ai/dsh-session' import LocalJobRegistry from '@deepseek-ai/dsh-jobs-local' import * as ToolJobs from '@deepseek-ai/dsh-tool-jobs' @@ -51,6 +50,7 @@ async function setup() { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalSubprocessRuntime) ;(ctx.subprocess as LocalSubprocessRuntime).internals = { spillDir } @@ -66,6 +66,7 @@ async function setupWithJobs() { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await ctx.plugin(ToolJobs) @@ -214,11 +215,10 @@ async function setupSandboxed(withApproval = false) { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await ctx.plugin(ToolJobs) - await ctx.plugin(SessionProjectionRegistry) - await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) await ctx.plugin(SandboxPolicyService, {}) await ctx.plugin(RecordingSandboxExecutor) if (withApproval) await ctx.plugin(ApprovalService) @@ -507,6 +507,7 @@ describe('bash tool', () => { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await ctx.plugin(LocalSubprocessRuntime) @@ -526,6 +527,7 @@ describe('the background surface follows the job registry', () => { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalSubprocessRuntime) ;(ctx.subprocess as LocalSubprocessRuntime).internals = { spillDir } @@ -637,6 +639,7 @@ describe('background execution through the job runtime', () => { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await ctx.plugin(ToolJobs) @@ -667,6 +670,7 @@ describe('background execution through the job runtime', () => { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await ctx.plugin(CountingStartExecutor) @@ -1337,6 +1341,7 @@ describe('the model-facing bash tool builds its request from named args only (no provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await ctx.plugin(ToolJobs) diff --git a/packages/shell/tool-pwsh-persistent/tests/tools.spec.ts b/packages/shell/tool-pwsh-persistent/tests/tools.spec.ts index b2b6d96137..a3c434abb6 100644 --- a/packages/shell/tool-pwsh-persistent/tests/tools.spec.ts +++ b/packages/shell/tool-pwsh-persistent/tests/tools.spec.ts @@ -4,6 +4,7 @@ import { Context } from '@deepseek-ai/cordis' import { ToolCallId } from '@deepseek-ai/dsh-llm' import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Agent } from '@deepseek-ai/dsh-agent' import TerminalSessionService from '@deepseek-ai/dsh-terminal' import type { @@ -321,6 +322,7 @@ async function setup( contexts.push(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(TerminalSessionService) const stub = stubBackend(initialMode) @@ -792,6 +794,7 @@ describe('tool-pwsh-persistent', () => { contexts.push(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(TerminalSessionService) const spawnStarted = Promise.withResolvers() diff --git a/packages/shell/tool-pwsh/tests/background.spec.ts b/packages/shell/tool-pwsh/tests/background.spec.ts index 59b99b965c..bb28376873 100644 --- a/packages/shell/tool-pwsh/tests/background.spec.ts +++ b/packages/shell/tool-pwsh/tests/background.spec.ts @@ -6,6 +6,7 @@ import { SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session/types' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { TOOL_ABORTED } from '@deepseek-ai/dsh-tools' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Agent } from '@deepseek-ai/dsh-agent' import { provideWorkingDirectoryFixture, unsupportedInbox } from '@deepseek-ai/dsh-agent-loop-testkit' import LocalJobRegistry from '@deepseek-ai/dsh-jobs-local' @@ -86,6 +87,7 @@ async function setup() { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry, { pumpPollMs: 5 }) await ctx.plugin(ToolTasks) @@ -456,6 +458,7 @@ describe('foreground commands as jobs (pwsh)', () => { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await ctx.plugin(BashEnvPlugin) @@ -494,6 +497,7 @@ describe('foreground commands as jobs (pwsh)', () => { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await ctx.plugin(ToolTasks) diff --git a/packages/shell/tool-pwsh/tests/tools.spec.ts b/packages/shell/tool-pwsh/tests/tools.spec.ts index 9b2d5eb040..abe3bf1bf3 100644 --- a/packages/shell/tool-pwsh/tests/tools.spec.ts +++ b/packages/shell/tool-pwsh/tests/tools.spec.ts @@ -29,7 +29,6 @@ import type { ApprovalOutcome } from '@deepseek-ai/dsh-user-approval' import { ShellExecutor } from '@deepseek-ai/dsh-shell' import type { ShellExecRequest, ShellExecSpec, ShellExecution, ShellProcess, ShellRunResult } from '@deepseek-ai/dsh-shell' import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' -import { turnBoundaryProjectionDefinition } from '@deepseek-ai/dsh-agent-loop' import SandboxPolicyService from '@deepseek-ai/dsh-sandbox-policy' import { escalationHintMarker, sandboxDenialMarker } from '@deepseek-ai/dsh-sandbox' import * as ToolPwsh from '@deepseek-ai/dsh-tool-pwsh' @@ -152,6 +151,7 @@ async function setup(toolConfig: Partial = {}, dshHome?: string provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(BashEnvPlugin, dshHome === undefined ? {} : { dshHome }) await ctx.plugin(FakeBash) @@ -166,6 +166,7 @@ async function setupWithJobs(toolConfig: Partial = {}, dshHome? provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await ctx.plugin(ToolJobs) @@ -227,15 +228,14 @@ async function setupSandboxed(withApproval = false) { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await ctx.plugin(ToolJobs) await ctx.plugin(BashEnvPlugin) - await ctx.plugin(SessionProjectionRegistry) // The loop's turnBoundary unit (the open-turn fold) is not mounted in this // bench — the loop itself is not composed. Register its open-turn fold so // the approval service's turn-enclosure gate reads the seeded log shape. - await ctx.sessionProjections.register(turnBoundaryProjectionDefinition) await ctx.plugin(SandboxPolicyService, {}) await ctx.plugin(ConfiningFakeBash) if (withApproval) await ctx.plugin(ApprovalService) @@ -618,6 +618,7 @@ describe('per-call sandbox policy resolution', () => { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(BashEnvPlugin) await ctx.plugin(ConfiningFakeBash) @@ -914,6 +915,7 @@ describe('background execution through the job runtime', () => { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await ctx.plugin(BashEnvPlugin) @@ -933,6 +935,7 @@ describe('the background surface follows the job registry', () => { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(BashEnvPlugin) await ctx.plugin(FakeBash) diff --git a/packages/skill/tool-skill/tests/tool-skill.spec.ts b/packages/skill/tool-skill/tests/tool-skill.spec.ts index e3362cda0a..fc4cc6bd89 100644 --- a/packages/skill/tool-skill/tests/tool-skill.spec.ts +++ b/packages/skill/tool-skill/tests/tool-skill.spec.ts @@ -13,6 +13,7 @@ import { import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { defineContentToolFixture } from '@deepseek-ai/dsh-tools' import AgentRegistry, { agentEvents, type Agent, type PreStepDecision } from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import SkillRegistry from '@deepseek-ai/dsh-skill' import * as SkillFileSystem from '@deepseek-ai/dsh-skill-filesystem' import * as toolSkill from '@deepseek-ai/dsh-tool-skill' @@ -56,6 +57,7 @@ async function setup(home: string, config: toolSkill.Config = {}): Promise { provideWorkingDirectoryFixture(ctx) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const home = await tempDir('tool-schema') await ctx.plugin(SkillRegistry) @@ -794,6 +797,7 @@ describe('dsh-tool-skill', () => { provideWorkingDirectoryFixture(ctx, home) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(SkillRegistry) await ctx.plugin(SkillFileSystem, { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), watch: false }) diff --git a/packages/skill/tool-workspace-dependencies/tests/tool-workspace-dependencies.spec.ts b/packages/skill/tool-workspace-dependencies/tests/tool-workspace-dependencies.spec.ts index c9418f1da1..ffff21a7b9 100644 --- a/packages/skill/tool-workspace-dependencies/tests/tool-workspace-dependencies.spec.ts +++ b/packages/skill/tool-workspace-dependencies/tests/tool-workspace-dependencies.spec.ts @@ -9,6 +9,7 @@ import Loader from '@deepseek-ai/cordis-plugin-loader' import Include from '@deepseek-ai/cordis-plugin-include' import z from '@deepseek-ai/schemastery' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime from '@deepseek-ai/dsh-tools' import { ToolCallId } from '@deepseek-ai/dsh-llm' @@ -302,6 +303,7 @@ it('rejects a payload whose Node.js or pnpm version is malformed', async () => { async function tool(config: workspaceDependencies.Config) { const ctx = new Context() try { + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) diff --git a/packages/subagent/subagent/tests/external-activation-helpers.ts b/packages/subagent/subagent/tests/external-activation-helpers.ts index 389ce804d6..8ac5f26057 100644 --- a/packages/subagent/subagent/tests/external-activation-helpers.ts +++ b/packages/subagent/subagent/tests/external-activation-helpers.ts @@ -4,6 +4,7 @@ import { randomUUID } from 'node:crypto' import { onTestFinished } from 'vitest' import type { Context } from '@deepseek-ai/cordis' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Agent } from '@deepseek-ai/dsh-agent' import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' import { createInboxStub } from '@deepseek-ai/dsh-agent-loop-testkit' @@ -18,6 +19,7 @@ function parentStore(ctx: Context): Promise> { onTestFinished(() => ctx.fiber.dispose()) const ready = (async () => { if (ctx.get('sessions') === undefined) await ctx.plugin(SessionStore) + if (ctx.get('sessionProjections') === undefined) await ctx.plugin(SessionProjectionRegistry) if (ctx.get('agents') === undefined) await ctx.plugin(AgentRegistry) return new WeakMap() })() diff --git a/packages/subagent/tool-subagent/src/index.ts b/packages/subagent/tool-subagent/src/index.ts index 123a99b98e..9ca378d63d 100644 --- a/packages/subagent/tool-subagent/src/index.ts +++ b/packages/subagent/tool-subagent/src/index.ts @@ -35,12 +35,11 @@ import { SubagentPresetSelection, type PresetSelectionConfig, type PresetSelecti import type {} from './model-selection-settings.ts' import { recordSubagentModelSelection, - subagentModelSelectionPolicy, subagentModelSelectionProjectionDefinition, + subagentModelSelectionPolicy, } from './model-selection-state.ts' export const name = 'tool-subagent' - export const inject = ['tools', 'subagents', 'systemPrompt', 'sessionProjections'] // Definition identity keeps shared guidance scoped to the actual visible tools, diff --git a/packages/terminal/terminal-bash/tests/index.spec.ts b/packages/terminal/terminal-bash/tests/index.spec.ts index 9ed1aeeaeb..f3d991dbbb 100644 --- a/packages/terminal/terminal-bash/tests/index.spec.ts +++ b/packages/terminal/terminal-bash/tests/index.spec.ts @@ -631,9 +631,9 @@ describe('terminal-bash plugin shape', () => { it('validates config and registers the configured backend', async () => { const ctx = new Context() provideWorkingDirectoryFixture(ctx) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(TerminalSessionService) - await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(SandboxPolicyService, { mode: 'danger-full-access', workspaceRoot: '/tmp' }) await ctx.plugin(StubSubprocessRuntime) const fiber = await ctx.plugin(ptyLocal, config()) @@ -646,10 +646,10 @@ describe('terminal-bash plugin shape', () => { const ctx = new Context() provideWorkingDirectoryFixture(ctx) await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(TerminalSessionService) await ctx.plugin(EmptySandbox) - await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(SandboxPolicyService, { mode: 'danger-full-access', workspaceRoot: '/tmp' }) await ctx.plugin(StubSubprocessRuntime) await ctx.plugin(ptyLocal, config()) @@ -665,10 +665,10 @@ describe('terminal-bash plugin shape', () => { const ctx = new Context() provideWorkingDirectoryFixture(ctx) await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(TerminalSessionService) await ctx.plugin(RecordingSandbox) - await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(SandboxPolicyService, { mode: 'danger-full-access', workspaceRoot: '/tmp' }) await ctx.plugin(StubSubprocessRuntime) @@ -716,10 +716,10 @@ describe('terminal-bash plugin shape', () => { const ctx = new Context() provideWorkingDirectoryFixture(ctx) await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(TerminalSessionService) await ctx.plugin(RecordingSandbox) - await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(SandboxPolicyService, { mode: 'danger-full-access', workspaceRoot: '/tmp' }) await ctx.plugin(StubSubprocessRuntime) diff --git a/packages/terminal/terminal-bash/tests/local.spec.ts b/packages/terminal/terminal-bash/tests/local.spec.ts index e5a53123d0..c12f6181ed 100644 --- a/packages/terminal/terminal-bash/tests/local.spec.ts +++ b/packages/terminal/terminal-bash/tests/local.spec.ts @@ -61,10 +61,10 @@ async function harness( const ctx = new Context() provideWorkingDirectoryFixture(ctx) contexts.push(ctx) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(TerminalSessionService) await ctx.plugin(PassthroughSandbox) - await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(SandboxPolicyService, { mode, workspaceRoot: root }) await ctx.plugin(LocalSubprocessRuntime) const fiber = await ctx.plugin(ptyLocal, { diff --git a/packages/terminal/terminal/tests/service.spec.ts b/packages/terminal/terminal/tests/service.spec.ts index 11da341945..bfd3d58ce3 100644 --- a/packages/terminal/terminal/tests/service.spec.ts +++ b/packages/terminal/terminal/tests/service.spec.ts @@ -2,6 +2,7 @@ import { describe, expect, expectTypeOf, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' import { Session, SessionId } from '@deepseek-ai/dsh-session' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Agent } from '@deepseek-ai/dsh-agent' import TerminalSessionService, { TerminalBackendCleanupError, TerminalError, TerminalSessionId } from '@deepseek-ai/dsh-terminal' import type { @@ -120,6 +121,7 @@ function backend(type = 'stub') { async function harness() { const ctx = new Context() + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) const fiber = await ctx.plugin(TerminalSessionService) ptyServiceDisposers.set(ctx, async () => { await fiber.dispose() }) diff --git a/packages/todo/tool-todo/src/index.ts b/packages/todo/tool-todo/src/index.ts index 5521005375..7fe073137c 100644 --- a/packages/todo/tool-todo/src/index.ts +++ b/packages/todo/tool-todo/src/index.ts @@ -20,7 +20,6 @@ import type {} from '@deepseek-ai/dsh-session-projection' export type * from './types.ts' export const name = 'tool-todo' - export const inject = ['tools', 'sessionProjections'] /** The valid {@link TodoItem} statuses, as a runtime set for input narrowing. */ diff --git a/packages/todo/tool-todo/tests/projection.spec.ts b/packages/todo/tool-todo/tests/projection.spec.ts index 20975df8c0..1c86347250 100644 --- a/packages/todo/tool-todo/tests/projection.spec.ts +++ b/packages/todo/tool-todo/tests/projection.spec.ts @@ -33,8 +33,8 @@ async function harness(withTodoTool: boolean): Promise { await ctx.plugin(SystemPrompt, { personaPrefix: '' }) await ctx.plugin(ToolRuntime) await ctx.plugin(UserQuestionService) - await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) if (withTodoTool) await ctx.plugin(ToolTodo, { allowParallelInProgress: true }) const session = ctx.sessions.create() await ctx.agents.register({ id: session.id, session, status: 'idle', ctx } as Agent) diff --git a/packages/web/web-search-deepseek/tests/deepseek.spec.ts b/packages/web/web-search-deepseek/tests/deepseek.spec.ts index 09406f8a68..f03fb6b28d 100644 --- a/packages/web/web-search-deepseek/tests/deepseek.spec.ts +++ b/packages/web/web-search-deepseek/tests/deepseek.spec.ts @@ -5,6 +5,7 @@ import { join } from 'node:path' import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' import AgentRegistry from '@deepseek-ai/dsh-agent' +import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection' import type { Agent } from '@deepseek-ai/dsh-agent' import { credentialRef } from '@deepseek-ai/dsh-credentials' import LocalCredentialProvider from '@deepseek-ai/dsh-credentials-local' @@ -704,6 +705,7 @@ describe('web-search-deepseek account route selection', () => { const ctx = new Context() try { await ctx.plugin(SessionStore) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(WebRuntime, { searchProvider: DEEPSEEK_PROVIDER_ID }) ctx.provide('deepseekAccount', { diff --git a/packages/workflow/tool-workflow/tests/tool-workflow.spec.ts b/packages/workflow/tool-workflow/tests/tool-workflow.spec.ts index 7375e52194..229c1e6eaf 100644 --- a/packages/workflow/tool-workflow/tests/tool-workflow.spec.ts +++ b/packages/workflow/tool-workflow/tests/tool-workflow.spec.ts @@ -449,6 +449,7 @@ describe('dsh-tool-workflow', () => { onTestFinished(async () => { await ctx.fiber.dispose() }) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await ctx.plugin(ToolTasks) @@ -659,10 +660,10 @@ describe('dsh-tool-workflow', () => { onTestFinished(async () => { await ctx.fiber.dispose() }) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRuntime) + await ctx.plugin(SessionProjectionRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(LocalJobRegistry) await ctx.plugin(ToolTasks) - await ctx.plugin(SessionProjectionRegistry) provideWorkingDirectoryFixture(ctx) await ctx.plugin(SubagentRuntime) ctx.subagents.registerProvider({ diff --git a/packages/workflow/workflow-ptc/tests/host.spec.ts b/packages/workflow/workflow-ptc/tests/host.spec.ts index 4a866175a5..5254a25ceb 100644 --- a/packages/workflow/workflow-ptc/tests/host.spec.ts +++ b/packages/workflow/workflow-ptc/tests/host.spec.ts @@ -34,8 +34,8 @@ async function setup(execute?: (bindings: HostBindings, spec: PtcRunSpec) => Pro const ctx = new Context() onTestFinished(async () => { await ctx.fiber.dispose() }) await ctx.plugin(SessionStore) - await ctx.plugin(AgentRegistry) await ctx.plugin(SessionProjections) + await ctx.plugin(AgentRegistry) await ctx.plugin(SandboxPolicy, { mode: 'read-only' }) provideWorkingDirectoryFixture(ctx) await ctx.plugin(SubagentRuntime) diff --git a/packages/workflow/workflow-ptc/tests/setup.ts b/packages/workflow/workflow-ptc/tests/setup.ts index 0a63dad5f8..3feb65d171 100644 --- a/packages/workflow/workflow-ptc/tests/setup.ts +++ b/packages/workflow/workflow-ptc/tests/setup.ts @@ -37,8 +37,8 @@ export async function mountWorkflowRuntime( options: { cwd?: string; mode?: SandboxMode; runtimeConfig?: NodeRuntimeConfig } = {}, ): Promise { if (!ctx.get('sessions')) await ctx.plugin(SessionStore) - if (!ctx.get('agents')) await ctx.plugin(AgentRegistry) if (!ctx.get('sessionProjections')) await ctx.plugin(SessionProjections) + if (!ctx.get('agents')) await ctx.plugin(AgentRegistry) if (!ctx.get('fs')) await ctx.plugin(FileSystem) if (!ctx.get('subprocess')) await ctx.plugin(Subprocess) if (!ctx.get('sandbox')) await ctx.plugin(Sandbox) diff --git a/scripts/gen-config-catalog.ts b/scripts/gen-config-catalog.ts index df0ac078d4..60a31c9fdb 100644 --- a/scripts/gen-config-catalog.ts +++ b/scripts/gen-config-catalog.ts @@ -504,7 +504,7 @@ function unwrapExpr(expr: ts.Expression): ts.Expression { * packages whose schemas an intersect composes. A key path is the top-level * key or a nested path through object/array compositions (`agents[].id`). * Handles object/union calls, chained refinements, named const schema imports - * through package-relative or public workspace source paths, and `z.intersect([X.Config, …])`; errors on + * through public workspace source paths, and `z.intersect([X.Config, …])`; errors on * anything else, so a schema the walk cannot see fails the gate instead of * silently thinning it. Nested values that are neither `object` nor `array` * compositions (primitives, unions, dynamic-key dicts) contribute no paths. diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index b0c6123981..508e2994d4 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -329,6 +329,7 @@ export const LINK_MAP: Readonly> = { AllowedModelRoute: 'subagent.md', SubagentModelSelectionSettings: 'subagent.md', AgentOptions: 'core.md', + TurnBoundaryProjection: 'core.md', AgentStatus: 'core.md', AssistantStreamFrame: 'core.md', ContentBlock: 'llm-streaming.md', @@ -710,6 +711,7 @@ export const LINK_MAP: Readonly> = { ModDefinition: 'claude-code-mods.md', SurfaceSnapshot: 'claude-code-mods.md', TokenMeasurement: 'token-meter.md', + ContextPressureProjection: 'token-meter.md', PtcDispatchLog: 'tools.md', PostToolDecision: 'tools.md', PreToolDecision: 'tools.md', diff --git a/scripts/gen-tool-catalog.ts b/scripts/gen-tool-catalog.ts index 5dfcabf115..668ef3cf94 100644 --- a/scripts/gen-tool-catalog.ts +++ b/scripts/gen-tool-catalog.ts @@ -258,9 +258,10 @@ const TOOL_PACKAGES: ToolPackage[] = [ pkg: '@deepseek-ai/dsh-plan-mode', dir: 'plan-mode', source: 'packages/plan/plan-mode/src/index.ts', - requires: ['ctx.tools', 'ctx.systemPrompt', 'ctx.userQuestions (execution time, opportunistic)'], + requires: ['ctx.tools', 'ctx.systemPrompt', 'ctx.agents', 'ctx.userQuestions (execution time, opportunistic)'], writes: ['tool/call', 'plan/mode inactive on an approved review', 'tool/result'], async mount(ctx) { + await ctx.plugin(AgentRegistry) await ctx.plugin(PlanModeController, { section: 'Tool catalog schema harvest.' }) }, note: @@ -288,9 +289,10 @@ const TOOL_PACKAGES: ToolPackage[] = [ pkg: '@deepseek-ai/dsh-tool-present', dir: 'tool-present', source: 'packages/deliverables/tool-present/src/index.ts', - requires: ['ctx.workingDirectory', 'ctx.tools', 'ctx.fs', 'ctx.sessionProjections'], + requires: ['ctx.workingDirectory', 'ctx.tools', 'ctx.fs', 'ctx.agents'], writes: ['tool/call', 'deliverables/presented after a successful final result', 'tool/result'], async mount(ctx) { + await ctx.plugin(AgentRegistry) await ctx.plugin(ToolPresent) }, note: 'Deliveries belong to the calling Session; Web ui-deliverables supplies source-file opening and cards.', @@ -494,10 +496,11 @@ const TOOL_PACKAGES: ToolPackage[] = [ pkg: '@deepseek-ai/dsh-tool-session-query', dir: 'tool-session-query', source: 'packages/experimental/tool-session-query/src/index.ts', - requires: ['ctx.tools', 'ctx.systemPrompt', 'ctx.sessionQuery', 'a calling Agent for workspace authority'], + requires: ['ctx.tools', 'ctx.systemPrompt', 'ctx.sessionQuery', 'ctx.agents', 'a calling Agent for workspace authority'], writes: ['tool/call', 'tool/result'], async mount(ctx) { await ctx.plugin(SessionStore) + await ctx.plugin(AgentRegistry) await ctx.plugin(SqliteSessionQueryEngine, { path: ':memory:' }) await ctx.plugin(ToolSessionQuery) }, diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index 0a5a8431a4..6f4a10e801 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -2481,6 +2481,16 @@ "doc": "docs/subsystems/boot.md", "symbol": "PendingModule", "source": "packages/boot/hmr/src/index.ts" + }, + { + "doc": "docs/subsystems/core.md", + "symbol": "TurnBoundaryProjection", + "source": "packages/core/agent/src/types.ts" + }, + { + "doc": "docs/subsystems/token-meter.md", + "symbol": "ContextPressureProjection", + "source": "packages/llm/token-meter/src/projection.ts" } ] } From bad994ee010e7299efba826a603d630e1dbc480c Mon Sep 17 00:00:00 2001 From: _Kerman Date: Thu, 15 Oct 2026 18:29:20 +0800 Subject: [PATCH 063/369] docs(upgrade): consolidate projection migration in the current release guide --- .../guide.i18n.yaml | 16 ---------------- .../claude-code-mods-projections/guide.md | 17 ----------------- .../claude-code-mods-projections/guide.zh.md | 17 ----------------- .../subagent-activations/guide.i18n.yaml | 8 ++++---- .../subagent-activations/guide.md | 6 ++---- .../subagent-activations/guide.zh.md | 6 ++---- .../session-projection-handles/guide.i18n.yaml | 8 ++++---- .../session-projection-handles/guide.md | 4 ++-- .../session-projection-handles/guide.zh.md | 4 ++-- 9 files changed, 16 insertions(+), 70 deletions(-) delete mode 100644 docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.i18n.yaml delete mode 100644 docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.md delete mode 100644 docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.zh.md diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.i18n.yaml deleted file mode 100644 index 6d6f45fd15..0000000000 --- a/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/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-alpha.1/claude-code-mods-projections/guide.md -/: - en: 2ca1b8bc4d168c3f - zh: 2257728847687363 -/compose-session-projections-for-claude-code-mods: - en: 58c229d213b33d7b - zh: d6d539f921e59af2 -/compose-session-projections-for-claude-code-mods/change: - en: 3a76b4c79776f847 - zh: e9c7d258a2b76611 -/compose-session-projections-for-claude-code-mods/migration: - en: 6b9ecb1a635ccb44 - zh: 978dad0bdcd69d57 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.md b/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.md deleted file mode 100644 index 3c97f66652..0000000000 --- a/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -kind: upgrade-guide -description: "The Claude Code mods bridge requires the session projection registry at initialization." ---- - -# Compose session projections for Claude Code mods - -English | [中文](guide.zh.md) - -## Change - -`@deepseek-ai/dsh-experimental-claude-code-mods` requires `sessionProjections` to initialize. Custom compositions that omit this service no longer start the bridge. The bridge registers the turn and context-pressure projections before serving mods. - -## Migration - -1. Add `@deepseek-ai/dsh-session-projection` to the `cordis.yml` or profile overlay that composes the bridge, unless it is already composed. -2. Start the profile and confirm the bridge and its mod plugins load successfully. diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.zh.md deleted file mode 100644 index 753d5ca266..0000000000 --- a/docs/upgrade-guide/v0.2.1-alpha.1/claude-code-mods-projections/guide.zh.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -kind: upgrade-guide -description: "Claude Code 模组桥接在初始化时要求会话投影注册表。" ---- - -# 为 Claude Code 模组组合会话投影 - -[English](guide.md) | 中文 - -## 变更 - -`@deepseek-ai/dsh-experimental-claude-code-mods` 需要 `sessionProjections` 才能初始化。未提供此服务的自定义组合不再启动桥接。桥接在服务模组前注册轮次和上下文压力投影。 - -## 迁移 - -1. 在组合桥接的 `cordis.yml` 或配置覆盖文件中添加 `@deepseek-ai/dsh-session-projection`;已组合此服务时无需重复添加。 -2. 启动配置,确认桥接及其模组插件成功加载。 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.i18n.yaml index 9407c83aff..c1938c0f53 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.i18n.yaml +++ b/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.i18n.yaml @@ -9,8 +9,8 @@ en: fcf5f06084f989d9 zh: 5e3e6a05786390dd /migrate-delegation-to-managed-subagent-activations/change: - en: e56eb4dee1cf246a - zh: f5c5c62c815c140d + en: 17add29d9f80674e + zh: 4d4050e99f019cca /migrate-delegation-to-managed-subagent-activations/migration: - en: e8455cb8d6ecf66b - zh: 1b48e06d6aac7d9f + en: d4269b2c58484a3d + zh: 0ae39476ecd330e6 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.md b/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.md index 1ef7d07119..5a88ff2ee3 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.md +++ b/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.md @@ -11,9 +11,7 @@ English | [中文](guide.zh.md) `@deepseek-ai/dsh-tool-subagent` removes `backgroundMode`, `enableRunInBackground`, and the model argument `run_in_background`. Every delegation now returns `{ kind: 'activation', subagentId }` after accepting the child, and the runtime delivers its final answer in a parent completion notice. Foreground results and `{ kind: 'background', jobId }` results are removed; subagents are no longer collected or stopped through `job_output` or `job_kill`. -`@deepseek-ai/dsh-subagent-in-process-driver` is removed. Local providers contribute creation inputs through `prepareContinuable()`; the subagent service owns execution. Spawn and Fork children can receive later messages, including after settlement when persistence and Session query are available. External providers execute once and do not accept follow-up messages. - -Subagent initialization requires `@deepseek-ai/dsh-session-projection`. +The published `@deepseek-ai/dsh-subagent-in-process-driver` package is removed. Local providers contribute creation inputs through `prepareContinuable()`; the subagent service owns execution. Spawn and Fork children can receive later messages, including after settlement when persistence and Session query are available. External providers execute once and do not accept follow-up messages. ## Migration @@ -26,7 +24,7 @@ Subagent initialization requires `@deepseek-ai/dsh-session-projection`. toolName: subagent ``` - Mount `@deepseek-ai/dsh-session-projection`. Local tool delegation requires Session persistence. Compose it with the [subagent service and provider](../../../../packages/subagent/subagent/README.md#use-this-package). + Local tool delegation requires Session persistence. Compose it with the [subagent service and provider](../../../../packages/subagent/subagent/README.md#use-this-package). 2. Remove `run_in_background` from tool calls and update result consumers to read `subagentId`. Collect the completion notice instead of foreground output or a Job result. Use `interrupt_agent` to stop the current execution; use `send_message` only for local children when those control tools are composed. diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.zh.md index 1f2e80d7cf..aa83610496 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.zh.md +++ b/docs/upgrade-guide/v0.2.1-alpha.1/subagent-activations/guide.zh.md @@ -11,9 +11,7 @@ description: "受管理的子 agent activation 替代前台与 Job 后台委派 `@deepseek-ai/dsh-tool-subagent` 移除了 `backgroundMode`、`enableRunInBackground` 和模型参数 `run_in_background`。每次委派现在都会在接纳子 agent(智能体)后返回 `{ kind: 'activation', subagentId }`,运行时通过父级完成通知交付最终答案。前台结果和 `{ kind: 'background', jobId }` 结果已移除;子 agent 不再通过 `job_output` 或 `job_kill` 收集结果或停止。 -`@deepseek-ai/dsh-subagent-in-process-driver` 被移除。本地提供方通过 `prepareContinuable()` 提供创建输入,由子 agent 服务拥有执行过程。Spawn 和 Fork 子级可以接收后续消息;组合了持久化和 Session 查询服务时,结算后也能接收。外部提供方只执行一次,不接受后续消息。 - -子 agent 初始化需要 `@deepseek-ai/dsh-session-projection`。 +已发布的 `@deepseek-ai/dsh-subagent-in-process-driver` 包被移除。本地提供方通过 `prepareContinuable()` 提供创建输入,由子 agent 服务拥有执行过程。Spawn 和 Fork 子级可以接收后续消息;组合了持久化和 Session 查询服务时,结算后也能接收。外部提供方只执行一次,不接受后续消息。 ## 迁移 @@ -26,7 +24,7 @@ description: "受管理的子 agent activation 替代前台与 Job 后台委派 toolName: subagent ``` - 挂载 `@deepseek-ai/dsh-session-projection`。本地工具委派需要 Session 持久化。将其与[子 agent 服务及提供方](../../../../packages/subagent/subagent/README.zh.md#use-this-package)一起组合。 + 本地工具委派需要 Session 持久化。将其与[子 agent 服务及提供方](../../../../packages/subagent/subagent/README.zh.md#use-this-package)一起组合。 2. 从工具调用中删除 `run_in_background`,并将结果消费方改为读取 `subagentId`。收集完成通知,替代前台输出或 Job 结果。组合了相应控制工具时,使用 `interrupt_agent` 停止当前执行;`send_message` 仅用于本地子级。 diff --git a/docs/upgrade-guide/v0.2.1-alpha.3/session-projection-handles/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.3/session-projection-handles/guide.i18n.yaml index 8e72ee90bf..16a7f3f11c 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.3/session-projection-handles/guide.i18n.yaml +++ b/docs/upgrade-guide/v0.2.1-alpha.3/session-projection-handles/guide.i18n.yaml @@ -9,8 +9,8 @@ en: 5cf8cb0051860df5 zh: 2578c6c3e77bc1c7 /session-projection-registration-handles/change: - en: 154904370da3336a - zh: 8e508300cf097a27 + en: d488f71dcb5fcafb + zh: 0133ba0a568993ad /session-projection-registration-handles/migration: - en: 2d27fb8cc2f18a79 - zh: 6d8461cd25c3a608 + en: f268d8ba1d131b0f + zh: 29c3fe01d8b1cd0e diff --git a/docs/upgrade-guide/v0.2.1-alpha.3/session-projection-handles/guide.md b/docs/upgrade-guide/v0.2.1-alpha.3/session-projection-handles/guide.md index 4014d86b93..25f76616c2 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.3/session-projection-handles/guide.md +++ b/docs/upgrade-guide/v0.2.1-alpha.3/session-projection-handles/guide.md @@ -10,11 +10,11 @@ English | [中文](guide.zh.md) `@deepseek-ai/dsh-session-projection` changes `SessionProjectionRegistry.register(definition)` from a synchronous disposer to `Promise>`. Registry-level `stateOf(session, key)` is removed. Registration errors now reject the returned promise. After the last registration is disposed, the unit leaves snapshots, checkpoints, and live notifications, but retained handles can still fold Session events on demand. -`@deepseek-ai/dsh-agent` and `@deepseek-ai/dsh-subagent` require `sessionProjections`. AgentRegistry owns turn and step state; consumers use `ctx.agents.turnBoundaryOf(session)` instead of importing `turnBoundaryProjectionDefinition`. Preset and context-pressure reads use `ctx.agentPresets.presetForSession(session)` and `ctx.tokenMeter.contextPressure(session)`. The API controller and mods bridge no longer register those definitions independently; their optional-service behavior applies when the owning service is absent. `@deepseek-ai/dsh-api-session-controller` also removes `installModelSelectionProjection`. +`@deepseek-ai/dsh-agent` and `@deepseek-ai/dsh-subagent` require `sessionProjections`. AgentRegistry owns turn and step state; consumers use `ctx.agents.turnBoundaryOf(session)` instead of importing `turnBoundaryProjectionDefinition`. Turn-state consumers, including the Claude Code mods bridge, require `agents`. Preset and context-pressure reads use `ctx.agentPresets.presetForSession(session)` and `ctx.tokenMeter.contextPressure(session)`. The API controller and mods bridge no longer register those definitions independently; their optional-service behavior applies when the owning service is absent. `@deepseek-ai/dsh-api-session-controller` also removes `installModelSelectionProjection`. ## Migration -1. Mount `SessionProjectionRegistry` before `AgentRegistry`, `SubagentRuntime`, and other projection owners in custom compositions. Consumers of domain data inject the owning service. +1. Mount `SessionProjectionRegistry` before `AgentRegistry`, `SubagentRuntime`, and other projection owners in custom compositions. Include `@deepseek-ai/dsh-agent` wherever a plugin requires `agents`. Consumers of domain data inject the owning service. 2. For a plugin's own definitions, await `ctx.sessionProjections.register(definition)` in asynchronous `apply()` or `[Service.init]()` and retain the handle privately before publishing dependent operations. Keep existing function-plugin namespace exports. 3. Replace `registry.stateOf(session, key)` with a business service method for another domain, or `handle.stateOf(session)` inside the owner. Replace stored unregister callbacks with `handle.dispose`, which always returns `Promise`, and await explicit disposal; fiber disposal still releases registrations automatically. Await registration when handling initialization failures. 4. Replace `installModelSelectionProjection(ctx)` with `await ctx.sessionProjections.register(modelSelectionProjection)`, importing the definition from `@deepseek-ai/dsh-api-session-controller/src/model-selection-projection.ts`. diff --git a/docs/upgrade-guide/v0.2.1-alpha.3/session-projection-handles/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.3/session-projection-handles/guide.zh.md index b48ff66064..7c917af0e4 100644 --- a/docs/upgrade-guide/v0.2.1-alpha.3/session-projection-handles/guide.zh.md +++ b/docs/upgrade-guide/v0.2.1-alpha.3/session-projection-handles/guide.zh.md @@ -10,11 +10,11 @@ description: "Session 投影注册改为异步,并返回保留读取能力的 `@deepseek-ai/dsh-session-projection` 的 `SessionProjectionRegistry.register(definition)` 从同步释放函数改为返回 `Promise>`。注册表级 `stateOf(session, key)` 已移除,注册错误现在使返回的 Promise 拒绝。最后一次注册释放后,该投影单元不再参与快照、检查点和实时通知,但保留的句柄仍可按需折叠 Session 事件。 -`@deepseek-ai/dsh-agent` 与 `@deepseek-ai/dsh-subagent` 依赖 `sessionProjections`。AgentRegistry 持有轮次和步骤状态;消费方使用 `ctx.agents.turnBoundaryOf(session)`,不再导入 `turnBoundaryProjectionDefinition`。预设与上下文压力分别通过 `ctx.agentPresets.presetForSession(session)` 和 `ctx.tokenMeter.contextPressure(session)` 读取。API 控制器与 mods 桥接不再独立注册这些定义;所属服务缺席时采用各自的可选服务行为。`@deepseek-ai/dsh-api-session-controller` 同时移除了 `installModelSelectionProjection`。 +`@deepseek-ai/dsh-agent` 与 `@deepseek-ai/dsh-subagent` 依赖 `sessionProjections`。AgentRegistry 持有轮次和步骤状态;消费方使用 `ctx.agents.turnBoundaryOf(session)`,不再导入 `turnBoundaryProjectionDefinition`。包括 Claude Code mods 桥接在内的轮次状态消费方依赖 `agents`。预设与上下文压力分别通过 `ctx.agentPresets.presetForSession(session)` 和 `ctx.tokenMeter.contextPressure(session)` 读取。API 控制器与 mods 桥接不再独立注册这些定义;所属服务缺席时采用各自的可选服务行为。`@deepseek-ai/dsh-api-session-controller` 同时移除了 `installModelSelectionProjection`。 ## 迁移 -1. 在自定义组合中,先挂载 `SessionProjectionRegistry`,再挂载 `AgentRegistry`、`SubagentRuntime` 及其他投影所有者。领域数据的消费方注入所属服务。 +1. 在自定义组合中,先挂载 `SessionProjectionRegistry`,再挂载 `AgentRegistry`、`SubagentRuntime` 及其他投影所有者。组合中的插件依赖 `agents` 时,加入 `@deepseek-ai/dsh-agent`。领域数据的消费方注入所属服务。 2. 对插件自己的定义,在异步 `apply()` 或 `[Service.init]()` 中等待 `ctx.sessionProjections.register(definition)`,并在提供依赖操作前于内部保存句柄。保留现有函数插件命名空间导出。 3. 将 `registry.stateOf(session, key)` 替换为其他领域的业务服务方法,或所有者内部的 `handle.stateOf(session)`。将保存的取消注册回调替换为始终返回 `Promise` 的 `handle.dispose`,并等待显式释放完成;fiber 释放仍会自动释放注册。处理初始化失败时须等待注册。 4. 将 `installModelSelectionProjection(ctx)` 替换为 `await ctx.sessionProjections.register(modelSelectionProjection)`,从 `@deepseek-ai/dsh-api-session-controller/src/model-selection-projection.ts` 导入该定义。 From aedf8e9ee8cbcf6036e30c8f5775856ebb658453 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Thu, 15 Oct 2026 19:02:35 +0800 Subject: [PATCH 064/369] refactor(tmux-context): name the projection registration handle --- packages/context/tmux-context/src/index.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/context/tmux-context/src/index.ts b/packages/context/tmux-context/src/index.ts index fea575008d..8a5f9d7ae3 100644 --- a/packages/context/tmux-context/src/index.ts +++ b/packages/context/tmux-context/src/index.ts @@ -210,12 +210,6 @@ const tmuxContextStateSchema = zod.object({ type TmuxContextState = zod.infer -/** - * Register a prepended pre-step listener for the lifetime of `ctx`. - * @param ctx - plugin context; the listener is disposed with it. - * @param config - durable refresh scheduling configuration. - * @throws when the refresh interval is invalid. - */ declare module '@deepseek-ai/dsh-session-projection/types' { interface SessionProjectionStateMap { /** The stable state block of this plugin's latest durable injection, or null. */ @@ -223,15 +217,18 @@ declare module '@deepseek-ai/dsh-session-projection/types' { } } -/** Register the tmux-context plugin after its projections are ready. - * @param ctx - plugin context. - * @returns when the plugin is ready. +/** + * Register a prepended pre-step listener for the lifetime of `ctx`. + * @param ctx - plugin context; the listener is disposed with it. + * @param config - durable refresh scheduling configuration. + * @returns when the listener is registered. + * @throws when the refresh interval is invalid. */ export async function apply(ctx: Context, config: Config): Promise { const refreshIntervalMs = config.refreshIntervalMs validateRefreshInterval(refreshIntervalMs) - const projection = await ctx.sessionProjections.register({ + const tmuxContextProjectionHandle = await ctx.sessionProjections.register({ key: 'tmuxContext', stateVersion: 1, stateSchema: tmuxContextStateSchema, @@ -255,7 +252,7 @@ export async function apply(ctx: Context, config: Config): Promise { if (decision.kind === 'reject' || signal.aborted || step !== 1) return decision const bash = ctx.get('shell') if (bash === undefined) return decision - const previous = projection.stateOf(agent.session) + const previous = tmuxContextProjectionHandle.stateOf(agent.session) if (refreshIntervalMs !== undefined && refreshIntervalMs > 0 && previous !== null) { const now = Date.now() if (now >= previous.time && now - previous.time < refreshIntervalMs) return decision From d62915e72a85e60a9471445373782e051e709346 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Thu, 15 Oct 2026 19:02:49 +0800 Subject: [PATCH 065/369] chore(terminal-bash): remove unused projection peer dependency --- packages/terminal/terminal-bash/package.json | 2 +- packages/terminal/terminal-bash/src/index.ts | 1 - pnpm-lock.yaml | 6 +++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/terminal/terminal-bash/package.json b/packages/terminal/terminal-bash/package.json index bec256dbd5..7fa52c7a48 100644 --- a/packages/terminal/terminal-bash/package.json +++ b/packages/terminal/terminal-bash/package.json @@ -36,7 +36,6 @@ "@deepseek-ai/dsh-session": "workspace:*", "@deepseek-ai/dsh-subprocess": "workspace:*", "@deepseek-ai/cordis": "workspace:~", - "@deepseek-ai/dsh-session-projection": "workspace:*", "@deepseek-ai/dsh-working-directory": "workspace:*" }, "dependencies": { @@ -52,6 +51,7 @@ "@deepseek-ai/dsh-sandbox": "workspace:*", "@deepseek-ai/dsh-sandbox-policy": "workspace:*", "@deepseek-ai/dsh-session": "workspace:*", + "@deepseek-ai/dsh-session-projection": "workspace:*", "@deepseek-ai/dsh-subprocess": "workspace:*", "@deepseek-ai/dsh-subprocess-local": "workspace:*", "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/terminal/terminal-bash/src/index.ts b/packages/terminal/terminal-bash/src/index.ts index 4b05eeb668..5359948eb6 100644 --- a/packages/terminal/terminal-bash/src/index.ts +++ b/packages/terminal/terminal-bash/src/index.ts @@ -13,7 +13,6 @@ import type { TerminalBackend, TerminalBackendSpawnSpec, TerminalSendOperation } import type { SubprocessTerminalHandle, SubprocessTerminalSpawnSpec } from '@deepseek-ai/dsh-subprocess' import type { SandboxExecutionPolicy } from '@deepseek-ai/dsh-sandbox' import type {} from '@deepseek-ai/dsh-sandbox-policy' -import type {} from '@deepseek-ai/dsh-session-projection' import { ENCODING_PREAMBLE } from '@deepseek-ai/dsh-pwsh-local' import { type Config, type ResolvedConfig, resolveConfig, type ShellDialect, validateConfig } from './config.ts' import { LocalPtySession } from './session.ts' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ac35f64c1..e3b75b9858 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14453,9 +14453,6 @@ importers: '@deepseek-ai/dsh-pwsh-local': specifier: workspace:* version: link:../../shell/pwsh-local - '@deepseek-ai/dsh-session-projection': - specifier: workspace:* - version: link:../../session/session-projection '@deepseek-ai/schemastery': specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery @@ -14481,6 +14478,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-subprocess': specifier: workspace:* version: link:../../subprocess/subprocess From 0570efc15731edd2fb3d6d0270ee542e6979b203 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Thu, 15 Oct 2026 19:34:21 +0800 Subject: [PATCH 066/369] test(session-title): mount agent registry in provider e2e --- .../session-title-first-prompt-llm/tests/provider.e2e.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/session/session-title-first-prompt-llm/tests/provider.e2e.ts b/packages/session/session-title-first-prompt-llm/tests/provider.e2e.ts index b76e25a67a..ae57203690 100644 --- a/packages/session/session-title-first-prompt-llm/tests/provider.e2e.ts +++ b/packages/session/session-title-first-prompt-llm/tests/provider.e2e.ts @@ -1,3 +1,4 @@ +import AgentRegistry from '@deepseek-ai/dsh-agent' import { createUserMessage } from '@deepseek-ai/dsh-llm' import { afterEach, describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' @@ -22,6 +23,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('first-prompt title provider with await ctx.plugin(LlmDeepSeek, { thinking: 'disabled' }) await ctx.plugin(SessionStore) await ctx.plugin(SessionProjectionRegistry) + await ctx.plugin(AgentRegistry) await ctx.plugin(SessionTitleService, { fallbackMaxWords: 5, fallbackMaxBytes: 40, From d7a1aa839a2540f5001543650df4de3d393e70e6 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Thu, 15 Oct 2026 19:34:27 +0800 Subject: [PATCH 067/369] docs: refresh module graph after projection dependency cleanup --- docs/module-graph.md | 24 ++++++++---------------- docs/module-graph.zh.md | 24 ++++++++---------------- 2 files changed, 16 insertions(+), 32 deletions(-) diff --git a/docs/module-graph.md b/docs/module-graph.md index 00bc4da738..ba46785157 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -797,7 +797,6 @@ flowchart TD pkg_terminal_bash --> pkg_sandbox pkg_terminal_bash --> pkg_sandbox_policy pkg_terminal_bash --> pkg_session - pkg_terminal_bash --> pkg_session_projection pkg_terminal_bash --> pkg_subprocess pkg_terminal_bash --> pkg_terminal pkg_terminal_bash --> pkg_working_directory @@ -814,7 +813,6 @@ flowchart TD pkg_tool_goal --> pkg_goal pkg_tool_goal --> pkg_llm pkg_tool_goal --> pkg_session - pkg_tool_goal --> pkg_session_projection pkg_tool_goal --> pkg_system_prompt pkg_tool_goal --> pkg_tools pkg_tool_fs --> pkg_attachment @@ -874,7 +872,6 @@ flowchart TD pkg_agent_instructions --> pkg_home_paths pkg_agent_instructions --> pkg_llm pkg_agent_instructions --> pkg_session - pkg_agent_instructions --> pkg_session_projection pkg_agent_instructions --> pkg_tools pkg_agent_instructions --> pkg_working_directory pkg_file_reference_local --> pkg_agent @@ -886,7 +883,6 @@ flowchart TD pkg_tool_present --> pkg_fs pkg_tool_present --> pkg_llm pkg_tool_present --> pkg_session - pkg_tool_present --> pkg_session_projection pkg_tool_present --> pkg_tools pkg_tool_present --> pkg_working_directory pkg_workspace_changes --> pkg_agent @@ -931,7 +927,6 @@ flowchart TD pkg_hooks_codex --> pkg_hook_protocol pkg_hooks_codex --> pkg_llm pkg_hooks_codex --> pkg_session - pkg_hooks_codex --> pkg_session_projection pkg_hooks_codex --> pkg_tools pkg_hooks_codex --> pkg_working_directory pkg_session_title_all_prompts_llm --> pkg_llm @@ -1207,7 +1202,6 @@ flowchart TD pkg_experimental_claude_code_mods --> pkg_llm pkg_experimental_claude_code_mods --> pkg_scope pkg_experimental_claude_code_mods --> pkg_session - pkg_experimental_claude_code_mods --> pkg_session_projection pkg_experimental_claude_code_mods --> pkg_storage_domain pkg_experimental_claude_code_mods --> pkg_subprocess pkg_experimental_claude_code_mods --> pkg_token_meter @@ -1218,7 +1212,6 @@ flowchart TD pkg_tool_session_query --> pkg_agent pkg_tool_session_query --> pkg_llm pkg_tool_session_query --> pkg_session - pkg_tool_session_query --> pkg_session_projection pkg_tool_session_query --> pkg_session_query pkg_tool_session_query --> pkg_system_prompt pkg_tool_session_query --> pkg_timeout @@ -1336,7 +1329,6 @@ flowchart TD pkg_hooks_claude_code --> pkg_hook_protocol pkg_hooks_claude_code --> pkg_llm pkg_hooks_claude_code --> pkg_session - pkg_hooks_claude_code --> pkg_session_projection pkg_hooks_claude_code --> pkg_subagent pkg_hooks_claude_code --> pkg_tools pkg_hooks_claude_code --> pkg_working_directory @@ -1663,10 +1655,10 @@ flowchart TD | [`session-title-llm`](../packages/session/session-title-llm) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`timeout`](../packages/util/timeout) | | [`bash-sandbox`](../packages/shell/bash-sandbox) | `shell` | [`bash-local`](../packages/shell/bash-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell) | | [`pwsh-sandbox`](../packages/shell/pwsh-sandbox) | `shell` | [`pwsh-local`](../packages/shell/pwsh-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell) | -| [`terminal-bash`](../packages/terminal/terminal-bash) | `terminal` | [`agent`](../packages/core/agent), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subprocess`](../packages/subprocess/subprocess), [`terminal`](../packages/terminal/terminal), [`working-directory`](../packages/session/working-directory) | +| [`terminal-bash`](../packages/terminal/terminal-bash) | `terminal` | [`agent`](../packages/core/agent), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess), [`terminal`](../packages/terminal/terminal), [`working-directory`](../packages/session/working-directory) | | [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`agent-tool-presentation`](../packages/core/agent-tool-presentation) | `core` | [`tools`](../packages/core/tools) | -| [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) | | [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`output-retention`](../packages/util/output-retention), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) | @@ -1677,9 +1669,9 @@ flowchart TD | [`plan-mode`](../packages/plan/plan-mode) | `plan` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions) | | [`command-compact`](../packages/compaction/command-compact) | `compaction` | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction) | | [`compaction-image-offload`](../packages/compaction/compaction-image-offload) | `compaction` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | -| [`agent-instructions`](../packages/context/agent-instructions) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`agent-instructions`](../packages/context/agent-instructions) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`file-reference-local`](../packages/context/file-reference-local) | `context` | [`agent`](../packages/core/agent), [`file-reference`](../packages/context/file-reference), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | -| [`tool-present`](../packages/deliverables/tool-present) | `deliverables` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`tool-present`](../packages/deliverables/tool-present) | `deliverables` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`workspace-changes`](../packages/deliverables/workspace-changes) | `deliverables` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools) | | [`cua`](../packages/experimental/cua) | `experimental` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`computer-use`](../packages/computer-use/computer-use), [`llm`](../packages/llm/llm), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions) | | [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | @@ -1688,7 +1680,7 @@ flowchart TD | [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | `experimental` | [`computer-use`](../packages/computer-use/computer-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`experimental-cua-macos`](../packages/experimental/cua-macos) | `experimental` | [`agent`](../packages/core/agent), [`computer-use`](../packages/computer-use/computer-use), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions) | | [`experimental-tool-worktree`](../packages/experimental/tool-worktree) | `experimental` | [`experimental-worktree`](../packages/experimental/worktree), [`tools`](../packages/core/tools) | -| [`hooks-codex`](../packages/experimental/hooks-codex) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`hooks-codex`](../packages/experimental/hooks-codex) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`session-title-all-prompts-llm`](../packages/experimental/session-title-all-prompts-llm) | `experimental` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) | | [`tool-terminal`](../packages/experimental/tool-terminal) | `experimental` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`system-prompt`](../packages/core/system-prompt), [`terminal`](../packages/terminal/terminal), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) | `extensions` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) | @@ -1734,8 +1726,8 @@ flowchart TD | [`plugin-manager`](../packages/boot/plugin-manager) | `boot` | [`brand`](../packages/util/brand), [`hmr`](../packages/boot/hmr), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) | | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | | [`experimental-browser-use-runtime`](../packages/experimental/browser-use-runtime) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`mcp-client`](../packages/mcp/mcp-client), [`scope`](../packages/core/scope), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | -| [`experimental-claude-code-mods`](../packages/experimental/claude-code-mods) | `experimental` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain), [`subprocess`](../packages/subprocess/subprocess), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-questions`](../packages/interaction/user-questions), [`working-directory`](../packages/session/working-directory) | -| [`tool-session-query`](../packages/experimental/tool-session-query) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | +| [`experimental-claude-code-mods`](../packages/experimental/claude-code-mods) | `experimental` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`storage-domain`](../packages/storage/storage-domain), [`subprocess`](../packages/subprocess/subprocess), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-questions`](../packages/interaction/user-questions), [`working-directory`](../packages/session/working-directory) | +| [`tool-session-query`](../packages/experimental/tool-session-query) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | | [`webhook-github`](../packages/experimental/webhook-github) | `experimental` | [`credentials`](../packages/credentials/credentials), [`host-webserver`](../packages/host/webserver), [`session`](../packages/core/session), [`webhook`](../packages/experimental/webhook) | | [`goal-round-driver`](../packages/goal/goal-round-driver) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | | [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | @@ -1751,7 +1743,7 @@ flowchart TD | [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | | [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`spill`](../packages/spill/spill), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) | | [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent) | -| [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`tool-ralph`](../packages/experimental/tool-ralph) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | | [`workflow-ptc`](../packages/workflow/workflow-ptc) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow), [`working-directory`](../packages/session/working-directory) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index c452bebf03..4ba3fcd5f2 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -799,7 +799,6 @@ flowchart TD pkg_terminal_bash --> pkg_sandbox pkg_terminal_bash --> pkg_sandbox_policy pkg_terminal_bash --> pkg_session - pkg_terminal_bash --> pkg_session_projection pkg_terminal_bash --> pkg_subprocess pkg_terminal_bash --> pkg_terminal pkg_terminal_bash --> pkg_working_directory @@ -816,7 +815,6 @@ flowchart TD pkg_tool_goal --> pkg_goal pkg_tool_goal --> pkg_llm pkg_tool_goal --> pkg_session - pkg_tool_goal --> pkg_session_projection pkg_tool_goal --> pkg_system_prompt pkg_tool_goal --> pkg_tools pkg_tool_fs --> pkg_attachment @@ -876,7 +874,6 @@ flowchart TD pkg_agent_instructions --> pkg_home_paths pkg_agent_instructions --> pkg_llm pkg_agent_instructions --> pkg_session - pkg_agent_instructions --> pkg_session_projection pkg_agent_instructions --> pkg_tools pkg_agent_instructions --> pkg_working_directory pkg_file_reference_local --> pkg_agent @@ -888,7 +885,6 @@ flowchart TD pkg_tool_present --> pkg_fs pkg_tool_present --> pkg_llm pkg_tool_present --> pkg_session - pkg_tool_present --> pkg_session_projection pkg_tool_present --> pkg_tools pkg_tool_present --> pkg_working_directory pkg_workspace_changes --> pkg_agent @@ -933,7 +929,6 @@ flowchart TD pkg_hooks_codex --> pkg_hook_protocol pkg_hooks_codex --> pkg_llm pkg_hooks_codex --> pkg_session - pkg_hooks_codex --> pkg_session_projection pkg_hooks_codex --> pkg_tools pkg_hooks_codex --> pkg_working_directory pkg_session_title_all_prompts_llm --> pkg_llm @@ -1209,7 +1204,6 @@ flowchart TD pkg_experimental_claude_code_mods --> pkg_llm pkg_experimental_claude_code_mods --> pkg_scope pkg_experimental_claude_code_mods --> pkg_session - pkg_experimental_claude_code_mods --> pkg_session_projection pkg_experimental_claude_code_mods --> pkg_storage_domain pkg_experimental_claude_code_mods --> pkg_subprocess pkg_experimental_claude_code_mods --> pkg_token_meter @@ -1220,7 +1214,6 @@ flowchart TD pkg_tool_session_query --> pkg_agent pkg_tool_session_query --> pkg_llm pkg_tool_session_query --> pkg_session - pkg_tool_session_query --> pkg_session_projection pkg_tool_session_query --> pkg_session_query pkg_tool_session_query --> pkg_system_prompt pkg_tool_session_query --> pkg_timeout @@ -1338,7 +1331,6 @@ flowchart TD pkg_hooks_claude_code --> pkg_hook_protocol pkg_hooks_claude_code --> pkg_llm pkg_hooks_claude_code --> pkg_session - pkg_hooks_claude_code --> pkg_session_projection pkg_hooks_claude_code --> pkg_subagent pkg_hooks_claude_code --> pkg_tools pkg_hooks_claude_code --> pkg_working_directory @@ -1665,10 +1657,10 @@ flowchart TD | [`session-title-llm`](../packages/session/session-title-llm) | `session` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`timeout`](../packages/util/timeout) | | [`bash-sandbox`](../packages/shell/bash-sandbox) | `shell` | [`bash-local`](../packages/shell/bash-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell) | | [`pwsh-sandbox`](../packages/shell/pwsh-sandbox) | `shell` | [`pwsh-local`](../packages/shell/pwsh-local), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`shell`](../packages/shell/shell) | -| [`terminal-bash`](../packages/terminal/terminal-bash) | `terminal` | [`agent`](../packages/core/agent), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subprocess`](../packages/subprocess/subprocess), [`terminal`](../packages/terminal/terminal), [`working-directory`](../packages/session/working-directory) | +| [`terminal-bash`](../packages/terminal/terminal-bash) | `terminal` | [`agent`](../packages/core/agent), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess), [`terminal`](../packages/terminal/terminal), [`working-directory`](../packages/session/working-directory) | | [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`agent-tool-presentation`](../packages/core/agent-tool-presentation) | `core` | [`tools`](../packages/core/tools) | -| [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`tool-goal`](../packages/goal/tool-goal) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`attachment`](../packages/attachment/attachment), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`working-directory`](../packages/session/working-directory) | | [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) | `fs` | [`fs`](../packages/fs/fs), [`output-retention`](../packages/util/output-retention), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`tools`](../packages/core/tools) | @@ -1679,9 +1671,9 @@ flowchart TD | [`plan-mode`](../packages/plan/plan-mode) | `plan` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions) | | [`command-compact`](../packages/compaction/command-compact) | `compaction` | [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction) | | [`compaction-image-offload`](../packages/compaction/compaction-image-offload) | `compaction` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | -| [`agent-instructions`](../packages/context/agent-instructions) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`agent-instructions`](../packages/context/agent-instructions) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`file-reference-local`](../packages/context/file-reference-local) | `context` | [`agent`](../packages/core/agent), [`file-reference`](../packages/context/file-reference), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | -| [`tool-present`](../packages/deliverables/tool-present) | `deliverables` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`tool-present`](../packages/deliverables/tool-present) | `deliverables` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`workspace-changes`](../packages/deliverables/workspace-changes) | `deliverables` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`session`](../packages/core/session), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools) | | [`cua`](../packages/experimental/cua) | `experimental` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`computer-use`](../packages/computer-use/computer-use), [`llm`](../packages/llm/llm), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions) | | [`experimental-browser-use-chrome-devtools-mcp`](../packages/experimental/browser-use-chrome-devtools-mcp) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | @@ -1690,7 +1682,7 @@ flowchart TD | [`experimental-computer-use-cua-driver-native`](../packages/experimental/computer-use-cua-driver-native) | `experimental` | [`computer-use`](../packages/computer-use/computer-use), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`experimental-cua-macos`](../packages/experimental/cua-macos) | `experimental` | [`agent`](../packages/core/agent), [`computer-use`](../packages/computer-use/computer-use), [`subprocess`](../packages/subprocess/subprocess), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-questions`](../packages/interaction/user-questions) | | [`experimental-tool-worktree`](../packages/experimental/tool-worktree) | `experimental` | [`experimental-worktree`](../packages/experimental/worktree), [`tools`](../packages/core/tools) | -| [`hooks-codex`](../packages/experimental/hooks-codex) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`hooks-codex`](../packages/experimental/hooks-codex) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`session-title-all-prompts-llm`](../packages/experimental/session-title-all-prompts-llm) | `experimental` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`session-title-llm`](../packages/session/session-title-llm) | | [`tool-terminal`](../packages/experimental/tool-terminal) | `experimental` | [`agent`](../packages/core/agent), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`system-prompt`](../packages/core/system-prompt), [`terminal`](../packages/terminal/terminal), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) | `extensions` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol) | @@ -1736,8 +1728,8 @@ flowchart TD | [`plugin-manager`](../packages/boot/plugin-manager) | `boot` | [`brand`](../packages/util/brand), [`hmr`](../packages/boot/hmr), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`subprocess`](../packages/subprocess/subprocess), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval) | | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | | [`experimental-browser-use-runtime`](../packages/experimental/browser-use-runtime) | `experimental` | [`agent`](../packages/core/agent), [`browser-use`](../packages/browser-use/browser-use), [`mcp-client`](../packages/mcp/mcp-client), [`scope`](../packages/core/scope), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | -| [`experimental-claude-code-mods`](../packages/experimental/claude-code-mods) | `experimental` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain), [`subprocess`](../packages/subprocess/subprocess), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-questions`](../packages/interaction/user-questions), [`working-directory`](../packages/session/working-directory) | -| [`tool-session-query`](../packages/experimental/tool-session-query) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | +| [`experimental-claude-code-mods`](../packages/experimental/claude-code-mods) | `experimental` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`storage-domain`](../packages/storage/storage-domain), [`subprocess`](../packages/subprocess/subprocess), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-questions`](../packages/interaction/user-questions), [`working-directory`](../packages/session/working-directory) | +| [`tool-session-query`](../packages/experimental/tool-session-query) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) | | [`webhook-github`](../packages/experimental/webhook-github) | `experimental` | [`credentials`](../packages/credentials/credentials), [`host-webserver`](../packages/host/webserver), [`session`](../packages/core/session), [`webhook`](../packages/experimental/webhook) | | [`goal-round-driver`](../packages/goal/goal-round-driver) | `goal` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | | [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | @@ -1753,7 +1745,7 @@ flowchart TD | [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | | [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compaction`](../packages/compaction/compaction), [`llm`](../packages/llm/llm), [`output-retention`](../packages/util/output-retention), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`session-title`](../packages/session/session-title), [`spill`](../packages/spill/spill), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`typert-protocol`](../packages/typert/protocol) | | [`experimental-agent-team`](../packages/experimental/agent-team) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent) | -| [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | +| [`hooks-claude-code`](../packages/experimental/hooks-claude-code) | `experimental` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/experimental/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`working-directory`](../packages/session/working-directory) | | [`tool-ralph`](../packages/experimental/tool-ralph) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | | [`workflow-ptc`](../packages/workflow/workflow-ptc) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow), [`working-directory`](../packages/session/working-directory) | From 72a607dfee4988953b927e240d5fc197c999eedf Mon Sep 17 00:00:00 2001 From: Turtle Date: Thu, 15 Oct 2026 19:52:23 +0800 Subject: [PATCH 068/369] fix(ui-settings-models): open model picker on the configured models The fetched-model picker now starts with the configured models checked and Update list adds newly checked candidates and removes unchecked configured ones. Configured models the provider does not list stay. Select all and Deselect all change only visible candidates so a hidden configured model is not removed unseen. Refs #6290 --- .../models-settings/model-picker.expected.md | 8 +- apps/web/tests/models-settings.e2e.ts | 15 +-- docs/user/guide/providers.i18n.yaml | 4 +- docs/user/guide/providers.md | 2 +- docs/user/guide/providers.zh.md | 2 +- .../ui-settings-models/README.i18n.yaml | 4 +- packages/client/ui-settings-models/README.md | 2 +- .../client/ui-settings-models/README.zh.md | 2 +- .../src/client/ModelListEditor.tsx | 95 +++++++++++-------- .../ui-settings-models/src/client/locales.ts | 12 +-- .../tests/provider-form.client.spec.tsx | 42 ++++++-- 11 files changed, 116 insertions(+), 72 deletions(-) diff --git a/apps/web/tests/expected/models-settings/model-picker.expected.md b/apps/web/tests/expected/models-settings/model-picker.expected.md index 01fd8e7166..a46d19c495 100644 --- a/apps/web/tests/expected/models-settings/model-picker.expected.md +++ b/apps/web/tests/expected/models-settings/model-picker.expected.md @@ -1,7 +1,7 @@ -- dialog "选择要添加的模型": - - heading "选择要添加的模型" [level=2] +- dialog "选择模型": + - heading "选择模型" [level=2] - button "关闭" - - paragraph: 以下是模型提供商的可用模型,勾选要添加的模型。 + - paragraph: 以下是模型提供商的可用模型。已勾选的模型会保留在列表中,取消勾选已配置的模型即可移除。 - searchbox "搜索模型" - button "取消全选" - list: @@ -15,4 +15,4 @@ - checkbox "MiniMax-M3" [checked] - text: MiniMax-M3 - button "取消" - - button "添加所选" + - button "更新列表" diff --git a/apps/web/tests/models-settings.e2e.ts b/apps/web/tests/models-settings.e2e.ts index 0abc01dcd1..b110eeaf8f 100644 --- a/apps/web/tests/models-settings.e2e.ts +++ b/apps/web/tests/models-settings.e2e.ts @@ -241,14 +241,14 @@ describe('web e2e: Models settings page configures a dormant provider', () => { expect(tripwire.pageErrors).toEqual([]) }, 60_000) - it('filters the discovered model catalog and clears hidden selections', async () => { + it('filters the discovered model catalog and deselects only visible candidates', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-models-picker')) const settingsDialog = page.getByRole('dialog', { name: '设置' }) await settingsDialog.getByRole('button', { name: '编辑 minimax-cn' }).click() await settingsDialog.getByText('自定义设置').click() await settingsDialog.getByRole('button', { name: '获取可用模型' }).click() - const picker = page.getByRole('dialog', { name: '选择要添加的模型' }) + const picker = page.getByRole('dialog', { name: '选择模型' }) await picker.waitFor({ timeout: 10_000 }) const boxes = picker.getByRole('checkbox') const count = await boxes.count() @@ -268,12 +268,13 @@ describe('web e2e: Models settings page configures a dormant provider', () => { await search.fill('') await expect.poll(async () => boxes.count()).toBe(count) const restored = await boxes.evaluateAll(nodes => nodes.map(node => (node as HTMLInputElement).checked)) - expect(restored).toEqual(Array.from({ length: count }, () => false)) + expect(restored.filter(checked => !checked)).toHaveLength(1) + expect(await picker.getByRole('checkbox', { name: 'MiniMax-M2.7-highspeed', exact: true }).isChecked()).toBe(false) await picker.getByRole('button', { name: '全选' }).waitFor() await picker.getByRole('button', { name: '全选' }).click() const snapshot = await captureStableAria( page, - '[role="dialog"][aria-label="选择要添加的模型"]', + '[role="dialog"][aria-label="选择模型"]', scaffold.workspaceCwd, ) await compareOrRefreshGolden(MODEL_PICKER_EXPECTED, snapshot, MODE) @@ -514,11 +515,11 @@ describe('web e2e: Models settings page configures a dormant provider', () => { await dialog.getByRole('button', { name: '删除模型 1' }).click() await dialog.getByRole('button', { name: '获取可用模型' }).click() - const picker = page.getByRole('dialog', { name: '选择要添加的模型' }) + const picker = page.getByRole('dialog', { name: '选择模型' }) await picker.getByRole('button', { name: '取消全选' }).click() await picker.getByRole('searchbox', { name: '搜索模型' }).fill('gpt-6-astra') await picker.getByRole('checkbox', { name: 'gpt-6-astra', exact: true }).check() - await picker.getByRole('button', { name: '添加所选' }).click() + await picker.getByRole('button', { name: '更新列表' }).click() await dialog.getByRole('button', { name: '模型选项 1' }).click() expect(await image.isChecked()).toBe(true) await dialog.getByRole('button', { name: '保存', exact: true }).click() @@ -527,7 +528,7 @@ describe('web e2e: Models settings page configures a dormant provider', () => { } finally { await scaffold.ctx.settings.mutate('llm-pi-ai', [{ op: 'unset', path: ['providers', 'openai'] }]) await edit.waitFor({ state: 'detached' }) - await page.getByRole('dialog', { name: '选择要添加的模型' }).waitFor({ state: 'detached' }) + await page.getByRole('dialog', { name: '选择模型' }).waitFor({ state: 'detached' }) } expect(tripwire.pageErrors).toEqual([]) }, 60_000) diff --git a/docs/user/guide/providers.i18n.yaml b/docs/user/guide/providers.i18n.yaml index e96b8abe57..6f7e665a4f 100644 --- a/docs/user/guide/providers.i18n.yaml +++ b/docs/user/guide/providers.i18n.yaml @@ -15,8 +15,8 @@ en: 17867305a3e1b52a zh: 876fedf1f6f563e4 /configure-models/add-a-custom-model-api/discover-models: - en: 97318c5a115199ff - zh: 0c455e4232cf0265 + en: ec10ba53a00eddc6 + zh: 857373a4e0fefee4 /configure-models/select-a-model: en: 6a94264c99535caf zh: f97bb0dc6a40c0d3 diff --git a/docs/user/guide/providers.md b/docs/user/guide/providers.md index 4a57c7b7bc..c1118e469f 100644 --- a/docs/user/guide/providers.md +++ b/docs/user/guide/providers.md @@ -32,7 +32,7 @@ The Provider ID is permanent because requests, saved sessions, model defaults, a ### Discover models -Under **Model catalog**, choose **Fetch available models** to ask the endpoint which models it serves. The request uses the base URL, protocol, and key currently in the form, or a saved provider's stored key, and the reply opens a searchable picker: search, tick the models you want, and choose **Add selected**. Nothing is stored until you save or create the provider. +Under **Model catalog**, choose **Fetch available models** to ask the endpoint which models it serves. The request uses the base URL, protocol, and key currently in the form, or a saved provider's stored key, and the reply opens a searchable picker: models already in the list start ticked, so search, tick the models to add, untick the ones to remove, and choose **Update list**. Configured models the endpoint does not list stay. Nothing is stored until you save or create the provider. Discovery reads the listing formats common gateways publish, but not every endpoint answers in one of them, so treat it as a convenience rather than a guarantee: when it fails or lists nothing, add the model ids by hand and they work just the same. A built-in provider is always answered from the installed catalog, even when its base URL points at a gateway, so fetch through a custom provider to see what the gateway really serves. diff --git a/docs/user/guide/providers.zh.md b/docs/user/guide/providers.zh.md index 9189f66815..ebf6147681 100644 --- a/docs/user/guide/providers.zh.md +++ b/docs/user/guide/providers.zh.md @@ -32,7 +32,7 @@ Provider ID 是永久的,因为请求、已保存会话、模型默认值和 ### 探测模型 -在**模型目录**中选择**获取可用模型**,即可询问端点它提供哪些模型。请求使用表单当前的 API 地址、协议和密钥,已保存的提供商则用已存储的密钥;响应会打开一个可搜索的选择框,搜索、勾选想要的模型,再点**添加所选**。保存或创建提供商之前不会存储任何内容。 +在**模型目录**中选择**获取可用模型**,即可询问端点它提供哪些模型。请求使用表单当前的 API 地址、协议和密钥,已保存的提供商则用已存储的密钥;响应会打开一个可搜索的选择框,列表中已有的模型默认勾选,搜索后勾选要添加的模型、取消勾选要移除的模型,再点**更新列表**。端点未列出的已配置模型保持不变。保存或创建提供商之前不会存储任何内容。 探测读取的是常见网关公开的列表格式,但并非每个端点都用这些格式作答,所以它只是便利手段而非保证:探测失败或列表为空时,手动添加模型 ID 即可,效果完全一样。内置提供商一律由已安装目录作答,即使其 API 地址指向网关也是如此,要查看网关实际提供的模型,请通过自定义提供商探测。 diff --git a/packages/client/ui-settings-models/README.i18n.yaml b/packages/client/ui-settings-models/README.i18n.yaml index 58750ae463..f6e2853f79 100644 --- a/packages/client/ui-settings-models/README.i18n.yaml +++ b/packages/client/ui-settings-models/README.i18n.yaml @@ -24,8 +24,8 @@ en: 3af73644655c8b7b zh: d689eba772100c63 /deepseek-ai-dsh-client-ui-settings-models/use-this-package/adding-and-deleting-providers: - en: 9d533c65c82cd4a4 - zh: 0a5da334e127c035 + en: 5eec53c85e2ef492 + zh: 162904aa543d30ac /deepseek-ai-dsh-client-ui-settings-models/use-this-package/first-run-dialogs: en: 589397a35f82244e zh: 342fbceee2c173a2 diff --git a/packages/client/ui-settings-models/README.md b/packages/client/ui-settings-models/README.md index 1b07faff50..5bcb805ebb 100644 --- a/packages/client/ui-settings-models/README.md +++ b/packages/client/ui-settings-models/README.md @@ -63,7 +63,7 @@ A model row on a hand-declared pi-ai route also edits its **Reasoning levels**, ### Adding and deleting providers -**Add model provider** is the one entry to the add card, offered while any editor-backed settings namespace is mounted and enabled while either mode below can proceed. The card opens on a segmented mode switch with a one-line purpose beneath it: **Third-party model provider** carries the dormant-directory provider select over the provider editor — a bare-mounted `llm-pi-ai` offers its whole installed catalog before any route exists — and **Custom model API** carries the create form for a route pi-ai does not ship: a relay, a self-hosted server, or any other OpenAI- or Anthropic-compatible endpoint. A mode is offered only while its namespace is mounted; with one mode the card shows that form alone under the mode's name, and a mode whose namespace has nothing left to adopt or no protocol to declare is disabled with the reason as its hover text. A panel mounts the first time its mode is shown and stays mounted, hidden, while the card is open and its mode stays offered, so switching modes discards neither draft; the switch locks while either panel has a write or an endpoint interrogation in flight, since a switch underneath one would orphan its answer. Each tab controls its panel through ids, and closing either panel forgets the catalog draft's target. The custom form asks for a unique **Provider ID**, an endpoint, a protocol, and at least one uniquely-identified model, because nothing can default those. The protocol picker names each protocol by its product name — OpenAI Chat Completions, OpenAI Responses, Anthropic Messages — while the stored value stays the schema identifier; a protocol the adapter adds before this page names it shows that identifier. The endpoint placeholder follows the selected protocol: OpenAI Chat Completions and Responses show `https://gateway.example/v1`, while Anthropic Messages shows `https://gateway.example` because its SDK appends `/v1/messages`. Changing the protocol preserves any address already entered. The endpoint must be a parseable HTTP or HTTPS URL; localhost, IPv4 and IPv6 literals, and custom ports remain valid. A syntax error blocks both discovery and creation at the field, while a request failure remains a separate provider error. **Fetch available models** asks the `llm/discoverModels` Remote about the endpoint the form shows, so adding a provider is one pass instead of save-then-return; the reply opens a searchable picker rather than being written, and nothing is written until **Add selected**. Each candidate displays its original model id in a monospace font to distinguish models with the same name. Rows stay on one line; hovering reveals the model name, falling back to the full id when no name is supplied. Selection and configuration retain the original model id. Each selected candidate copies its id, display name, context window, output-token cap, and disclosed input types into the editable row when disclosed, while an existing row retains its user-tuned values. Search matches model ids and optional display names without clearing hidden selections. **Select all** adds the visible results, while **Deselect all** clears the entire selection so hidden results cannot be adopted accidentally. A row is deletable only when the user layer alone carries it (removal restores the composition base), and its confirmation dialog names the provider. +**Add model provider** is the one entry to the add card, offered while any editor-backed settings namespace is mounted and enabled while either mode below can proceed. The card opens on a segmented mode switch with a one-line purpose beneath it: **Third-party model provider** carries the dormant-directory provider select over the provider editor — a bare-mounted `llm-pi-ai` offers its whole installed catalog before any route exists — and **Custom model API** carries the create form for a route pi-ai does not ship: a relay, a self-hosted server, or any other OpenAI- or Anthropic-compatible endpoint. A mode is offered only while its namespace is mounted; with one mode the card shows that form alone under the mode's name, and a mode whose namespace has nothing left to adopt or no protocol to declare is disabled with the reason as its hover text. A panel mounts the first time its mode is shown and stays mounted, hidden, while the card is open and its mode stays offered, so switching modes discards neither draft; the switch locks while either panel has a write or an endpoint interrogation in flight, since a switch underneath one would orphan its answer. Each tab controls its panel through ids, and closing either panel forgets the catalog draft's target. The custom form asks for a unique **Provider ID**, an endpoint, a protocol, and at least one uniquely-identified model, because nothing can default those. The protocol picker names each protocol by its product name — OpenAI Chat Completions, OpenAI Responses, Anthropic Messages — while the stored value stays the schema identifier; a protocol the adapter adds before this page names it shows that identifier. The endpoint placeholder follows the selected protocol: OpenAI Chat Completions and Responses show `https://gateway.example/v1`, while Anthropic Messages shows `https://gateway.example` because its SDK appends `/v1/messages`. Changing the protocol preserves any address already entered. The endpoint must be a parseable HTTP or HTTPS URL; localhost, IPv4 and IPv6 literals, and custom ports remain valid. A syntax error blocks both discovery and creation at the field, while a request failure remains a separate provider error. **Fetch available models** asks the `llm/discoverModels` Remote about the endpoint the form shows, so adding a provider is one pass instead of save-then-return; the reply opens a searchable picker rather than being written, and nothing changes until **Update list**. The picker opens with the configured models checked, or every candidate when the list is empty and the route serves its whole catalog; **Update list** appends newly checked candidates and removes unchecked configured models from the draft, while configured models the reply does not list stay. Each candidate displays its original model id in a monospace font to distinguish models with the same name. Rows stay on one line; hovering reveals the model name, falling back to the full id when no name is supplied. Selection and configuration retain the original model id. Each selected candidate copies its id, display name, context window, output-token cap, and disclosed input types into the editable row when disclosed, while an existing row retains its user-tuned values. Search matches model ids and optional display names without clearing hidden selections. **Select all** and **Deselect all** change only the visible results, so a hidden configured model is not removed unseen. A row is deletable only when the user layer alone carries it (removal restores the composition base), and its confirmation dialog names the provider. ### First-run dialogs diff --git a/packages/client/ui-settings-models/README.zh.md b/packages/client/ui-settings-models/README.zh.md index f272908232..1731aca2b6 100644 --- a/packages/client/ui-settings-models/README.zh.md +++ b/packages/client/ui-settings-models/README.zh.md @@ -63,7 +63,7 @@ API 密钥输入框初始为空,并通过 `autocomplete="new-password"` 请求 ### 新增与删除提供商 -**添加模型提供商**是新增卡片的唯一入口:只要任一带编辑器的设置 namespace 已挂载就会出现,下述两种方式任一可继续时才可点击。卡片打开后顶部是分段式的方式切换,下方一行小字说明所选方式的用途:**第三方模型提供商**承载休眠目录提供商选择框与提供商编辑器——裸挂载的 `llm-pi-ai` 在任何路由存在之前就能提供其完整的已安装 catalog;**自定义模型 API**承载为 pi-ai 不提供的路由(中转站、自部署服务或其他兼容 OpenAI / Anthropic 协议的接口)准备的创建表单。某种方式只在其 namespace 已挂载时提供;只剩一种方式时卡片以该方式为标题直接显示表单;namespace 已无可采用的提供商或没有可声明的协议时,对应方式会被禁用并把原因作为悬停提示。面板在其方式首次显示时挂载,之后在卡片打开且该方式仍被提供期间保持挂载但隐藏,因此切换方式不会丢掉任何一边的草稿;任一面板有写入或端点探测进行中时滑块锁定,因为此时切换会让结果落到看不见的面板上。每个 tab 通过 id 控制其面板,关闭任一面板都会忘记目录草稿的目标。自定义表单会索要唯一的 **Provider ID**、端点、协议与至少一个可唯一识别的模型,因为没有东西能为它们兜底。协议选择框以产品名称显示各协议——OpenAI Chat Completions、OpenAI Responses、Anthropic Messages——存储的值仍是 schema 标识符;适配器新增而本页尚未命名的协议直接显示其标识符。端点占位示例随所选协议变化:OpenAI Chat Completions 与 Responses 显示 `https://gateway.example/v1`,Anthropic Messages 显示 `https://gateway.example`,因为其 SDK 会追加 `/v1/messages`。切换协议会保留已输入的地址。端点必须是可解析的 HTTP 或 HTTPS URL;localhost、IPv4 与 IPv6 字面地址以及自定义端口仍然有效。语法错误会在字段处阻止询问与创建,请求失败则继续作为独立的提供商错误显示。**获取可用模型**通过 `llm/discoverModels` Remote 查询表单显示的端点,因此新增提供商一次即可完成,而非先保存再返回;回复打开的是可搜索选择器而非直接写入,只有点击**添加所选**才会写入。候选项使用等宽字体显示原始模型 id,以区分同名模型。每行保持单行,悬停可查看模型名称,未提供名称时回退到完整 id。勾选与配置仍使用原始模型 id。每个选中候选会在提供商公布相应信息时,把 id、显示名、上下文窗口、最大输出 token 数和已公布的输入类型复制进可编辑行;已经存在的行保留用户调整过的值。搜索会匹配模型 id 与可选显示名称,且不会清除隐藏项的勾选状态。**全选**会加入可见结果,而**取消全选**会清空全部勾选,以免意外采用隐藏结果。只有用户层单独携带某行时,该行才可删除(删除会恢复组合基线),其确认对话框会指名该提供商。 +**添加模型提供商**是新增卡片的唯一入口:只要任一带编辑器的设置 namespace 已挂载就会出现,下述两种方式任一可继续时才可点击。卡片打开后顶部是分段式的方式切换,下方一行小字说明所选方式的用途:**第三方模型提供商**承载休眠目录提供商选择框与提供商编辑器——裸挂载的 `llm-pi-ai` 在任何路由存在之前就能提供其完整的已安装 catalog;**自定义模型 API**承载为 pi-ai 不提供的路由(中转站、自部署服务或其他兼容 OpenAI / Anthropic 协议的接口)准备的创建表单。某种方式只在其 namespace 已挂载时提供;只剩一种方式时卡片以该方式为标题直接显示表单;namespace 已无可采用的提供商或没有可声明的协议时,对应方式会被禁用并把原因作为悬停提示。面板在其方式首次显示时挂载,之后在卡片打开且该方式仍被提供期间保持挂载但隐藏,因此切换方式不会丢掉任何一边的草稿;任一面板有写入或端点探测进行中时滑块锁定,因为此时切换会让结果落到看不见的面板上。每个 tab 通过 id 控制其面板,关闭任一面板都会忘记目录草稿的目标。自定义表单会索要唯一的 **Provider ID**、端点、协议与至少一个可唯一识别的模型,因为没有东西能为它们兜底。协议选择框以产品名称显示各协议——OpenAI Chat Completions、OpenAI Responses、Anthropic Messages——存储的值仍是 schema 标识符;适配器新增而本页尚未命名的协议直接显示其标识符。端点占位示例随所选协议变化:OpenAI Chat Completions 与 Responses 显示 `https://gateway.example/v1`,Anthropic Messages 显示 `https://gateway.example`,因为其 SDK 会追加 `/v1/messages`。切换协议会保留已输入的地址。端点必须是可解析的 HTTP 或 HTTPS URL;localhost、IPv4 与 IPv6 字面地址以及自定义端口仍然有效。语法错误会在字段处阻止询问与创建,请求失败则继续作为独立的提供商错误显示。**获取可用模型**通过 `llm/discoverModels` Remote 查询表单显示的端点,因此新增提供商一次即可完成,而非先保存再返回;回复打开的是可搜索选择器而非直接写入,只有点击**更新列表**才会改变草稿。选择器打开时勾选已配置的模型;列表为空、路由提供完整 catalog 时勾选全部候选。**更新列表**会追加新勾选的候选,并从草稿中移除取消勾选的已配置模型;回复中未列出的已配置模型保持不变。候选项使用等宽字体显示原始模型 id,以区分同名模型。每行保持单行,悬停可查看模型名称,未提供名称时回退到完整 id。勾选与配置仍使用原始模型 id。每个选中候选会在提供商公布相应信息时,把 id、显示名、上下文窗口、最大输出 token 数和已公布的输入类型复制进可编辑行;已经存在的行保留用户调整过的值。搜索会匹配模型 id 与可选显示名称,且不会清除隐藏项的勾选状态。**全选**与**取消全选**只改变可见结果,以免在看不见时移除已配置的模型。只有用户层单独携带某行时,该行才可删除(删除会恢复组合基线),其确认对话框会指名该提供商。 ### 首次运行弹窗 diff --git a/packages/client/ui-settings-models/src/client/ModelListEditor.tsx b/packages/client/ui-settings-models/src/client/ModelListEditor.tsx index c80f638f7c..b85f71c053 100644 --- a/packages/client/ui-settings-models/src/client/ModelListEditor.tsx +++ b/packages/client/ui-settings-models/src/client/ModelListEditor.tsx @@ -7,7 +7,10 @@ * catalog, so a row is only ever added deliberately. Fetching asks the endpoint * **the form currently shows** — including a key typed but not yet saved — so * adding a provider is one pass instead of save-then-return; the reply is - * candidates the user picks from, never configuration written behind them. + * candidates the user picks from, never configuration written behind them. The + * picker opens with the configured models checked, so confirming it adds the + * newly checked candidates and removes the unchecked configured ones in the + * draft; configured rows the reply does not list are kept. * * A provider that cannot be interrogated (an unreachable endpoint, a protocol * with no readable listing) is not a dead end: the failure is shown next to the @@ -197,19 +200,31 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode { const capacityText = (model: ModelDraft, index: number, field: CapacityField): string => editing.get(bufferKey(index, field)) ?? capacitySpelling(numberOf(model, field)) - /** Drop one row's entries and shift the rows after it down, in one pass. */ - const reindexOnRemove = ( - current: ReadonlyMap, - index: number, - ): Map => { - const next = new Map() - for (const [key, value] of current) { - const at = Number(key.slice(0, key.indexOf(':'))) - if (at === index) continue - // Only the row number moves; the field half of the key is untouched. - next.set(at > index ? key.replace(/^\d+/, String(at - 1)) : key, value) - } - return next + /** + * Drop the removed rows' disclosure and buffer entries and shift the rows + * after them down, so per-row state follows its row. + */ + const forgetRows = (removed: ReadonlySet): void => { + /** The surviving row's new position, or `undefined` for a removed row. */ + const moved = (at: number): number | undefined => + removed.has(at) ? undefined : at - [...removed].filter(gone => gone < at).length + setExpanded((current) => { + const next = new Set() + for (const at of current) { + const to = moved(at) + if (to !== undefined) next.add(to) + } + return next + }) + setEditing((current) => { + const next = new Map() + for (const [key, value] of current) { + const to = moved(Number(key.slice(0, key.indexOf(':')))) + // Only the row number moves; the field half of the key is untouched. + if (to !== undefined) next.set(key.replace(/^\d+/, String(to)), value) + } + return next + }) } const toggleExpanded = (index: number): void => { @@ -257,12 +272,13 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode { setFailure(t('fetchEmpty')) return } - // Everything already configured starts unchecked, so adopting a - // selection never silently rewrites a capacity the user corrected. + // The picker opens on the current list so a few models can be added or + // removed in one place. An empty list serves the whole catalog, so every + // candidate starts checked. const known = new Set(models.map(model => textOf(model, 'id'))) setCandidateQuery('') setCandidates(found) - setPicked(new Set(found.filter(model => !known.has(model.id)).map(model => model.id))) + setPicked(new Set(found.filter(model => models.length === 0 || known.has(model.id)).map(model => model.id))) } finally { setBusy(false) } @@ -277,16 +293,20 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode { const adoptPicked = (): void => { /* v8 ignore next -- the dialog only renders with candidates loaded */ if (candidates === undefined) return - const byId = new Map(models.map(model => [textOf(model, 'id'), model])) - for (const candidate of candidates) { - if (!picked.has(candidate.id)) continue - // A row the user already tuned wins over the provider's own numbers. - // Keyed by id, so a half-typed row whose id is still empty is not a - // match and the candidate joins as its own row — correct, since a row - // without an id is not yet a model and the create/apply gates refuse it. - byId.set(candidate.id, byId.get(candidate.id) ?? adopt(candidate)) - } - onChange([...byId.values()]) + const listed = new Set(candidates.map(candidate => candidate.id)) + // An unchecked candidate leaves the list. A row the listing does not name, + // including a half-typed row without an id, is outside the picker and stays. + const removed = new Set() + models.forEach((model, at) => { + const id = textOf(model, 'id') + if (listed.has(id) && !picked.has(id)) removed.add(at) + }) + const kept = models.filter((_model, at) => !removed.has(at)) + const keptIds = new Set(kept.map(model => textOf(model, 'id'))) + // A kept row the user already tuned wins over the provider's own numbers. + const added = candidates.filter(candidate => picked.has(candidate.id) && !keptIds.has(candidate.id)) + onChange([...kept, ...added.map(adopt)]) + forgetRows(removed) closePicker() } @@ -308,12 +328,15 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode { && visibleCandidates.every(candidate => picked.has(candidate.id)) const toggleVisibleCandidates = (): void => { + // Only visible candidates change: a hidden checked one may be a configured + // model, and clearing it would remove that model unseen. setPicked((current) => { - if (visibleCandidates.every(candidate => current.has(candidate.id))) { - return new Set() - } const next = new Set(current) - for (const candidate of visibleCandidates) next.add(candidate.id) + const clear = visibleCandidates.every(candidate => current.has(candidate.id)) + for (const candidate of visibleCandidates) { + if (clear) next.delete(candidate.id) + else next.add(candidate.id) + } return next }) } @@ -387,15 +410,7 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode { onToggle={() => { toggleExpanded(index) }} onRemove={() => { onChange(models.filter((_model, at) => at !== index)) - setExpanded((current) => { - const next = new Set() - for (const at of current) { - if (at < index) next.add(at) - else if (at > index) next.add(at - 1) - } - return next - }) - setEditing(current => reindexOnRemove(current, index)) + forgetRows(new Set([index])) }} /> ))} diff --git a/packages/client/ui-settings-models/src/client/locales.ts b/packages/client/ui-settings-models/src/client/locales.ts index 9bcfa18e27..9dba0e5b68 100644 --- a/packages/client/ui-settings-models/src/client/locales.ts +++ b/packages/client/ui-settings-models/src/client/locales.ts @@ -100,13 +100,13 @@ export const en = { fetching: 'Asking the provider\u2026', fetchNeedsBaseUrl: 'Enter the base URL first, then fetch.', fetchEmpty: 'The provider listed no models. Add them by hand.', - fetchTitle: 'Choose models to add', - fetchDescription: 'These are the models this provider has available. Choose the ones to add.', + fetchTitle: 'Choose models', + fetchDescription: 'These are the models this provider has available. Checked models will be in the list; uncheck a configured model to remove it.', fetchSearch: 'Search models', fetchNoMatches: 'No matching models.', fetchSelectAll: 'Select all', fetchDeselectAll: 'Deselect all', - fetchAdopt: 'Add selected', + fetchAdopt: 'Update list', customTag: 'Custom', customRoute: 'Provider ID', customRouteHint: 'Lowercase identifier, starting with a letter, that uniquely names this provider in requests and as its credential name.', @@ -241,13 +241,13 @@ export const zh: { [Key in keyof typeof en]: string } = { fetching: '正在询问提供商\u2026', fetchNeedsBaseUrl: '请先填写 API 地址,再获取。', fetchEmpty: '该提供商没有列出任何模型,请手动添加。', - fetchTitle: '选择要添加的模型', - fetchDescription: '以下是模型提供商的可用模型,勾选要添加的模型。', + fetchTitle: '选择模型', + fetchDescription: '以下是模型提供商的可用模型。已勾选的模型会保留在列表中,取消勾选已配置的模型即可移除。', fetchSearch: '搜索模型', fetchNoMatches: '没有匹配的模型。', fetchSelectAll: '全选', fetchDeselectAll: '取消全选', - fetchAdopt: '添加所选', + fetchAdopt: '更新列表', customTag: '自定义', customRoute: 'Provider ID', customRouteHint: '以小写字母开头的标识,在请求中唯一标识该提供商,并用于派生凭据名。', diff --git a/packages/client/ui-settings-models/tests/provider-form.client.spec.tsx b/packages/client/ui-settings-models/tests/provider-form.client.spec.tsx index 7a49fa4505..47e25dac5a 100644 --- a/packages/client/ui-settings-models/tests/provider-form.client.spec.tsx +++ b/packages/client/ui-settings-models/tests/provider-form.client.spec.tsx @@ -621,7 +621,7 @@ describe('endpoint interrogation', () => { }) }) - it('adopts only the picked candidates, keeping a row the user already tuned', async () => { + it('opens on the configured models and appends a newly checked one, keeping a row the user already tuned', async () => { const discover = vi.fn(() => Promise.resolve(ok([ { id: 'kept', contextWindow: 999, inputModalities: ['text', 'image'] }, { id: 'fresh', contextWindow: 4096, maxTokens: 2048, name: 'Fresh', inputModalities: ['text', 'image'] }, @@ -634,11 +634,10 @@ describe('endpoint interrogation', () => { fireEvent.click(screen.getByText(en.fetchModels)) await screen.findByText(en.fetchTitle) - expect(screen.getByRole('checkbox', { name: 'fresh' }).checked).toBe(true) expect(screen.queryByRole('checkbox', { name: 'Fresh' })).toBeNull() - // The already-configured row starts unchecked; the new one starts checked. const boxes = [...document.querySelectorAll('input[type="checkbox"]')] - expect(boxes.map(box => box.checked)).toEqual([false, true]) + expect(boxes.map(box => box.checked)).toEqual([true, false]) + fireEvent.click(screen.getByRole('checkbox', { name: 'fresh' })) fireEvent.click(screen.getByText(en.fetchAdopt)) expect(screen.getByLabelText(`${en.modelId} 2`).value).toBe('fresh') @@ -656,6 +655,31 @@ describe('endpoint interrogation', () => { ]) }) + it('removes an unchecked configured model and keeps one the provider does not list', async () => { + const discover = vi.fn(() => Promise.resolve(ok([{ id: 'gone' }, { id: 'stays' }, { id: 'new' }]))) + const { mutate } = await mountSection({ + discover, + providers: { openai: { baseURL: 'https://proxy.example/v1', models: [ + { id: 'gone' }, { id: 'unlisted', maxTokens: 512 }, { id: 'stays', contextWindow: 111 }, + ] } }, + }) + openEditor('openai') + expandModel(3) + + fireEvent.click(screen.getByText(en.fetchModels)) + await screen.findByText(en.fetchTitle) + fireEvent.click(screen.getByRole('checkbox', { name: 'gone' })) + fireEvent.click(screen.getByText(en.fetchAdopt)) + + // The disclosure follows its row up the list. + expect(screen.getByLabelText(`${en.contextWindow} 2`).value).toBe('111') + fireEvent.click(screen.getByText(en.apply)) + await waitFor(() => { expect(mutate).toHaveBeenCalled() }) + expect(firstMutate(mutate).ops[0]?.value).toEqual([ + { id: 'unlisted', maxTokens: 512 }, { id: 'stays', contextWindow: 111 }, + ]) + }) + it('keeps the rows editable when the provider cannot be interrogated', async () => { const discover = vi.fn(() => Promise.resolve( fail('https://proxy.example/v1/models answered 401; check the API key', 'llm/model-discovery-rejected'), @@ -765,7 +789,7 @@ describe('endpoint interrogation', () => { expect(firstMutate(mutate).ops[0]?.value).toEqual([{ id: 'a' }, { id: 'b', maxTokens: 2048 }]) }) - it('filters by model id or name, selects visible candidates, and clears every selection', async () => { + it('filters by model id or name and selects or deselects only visible candidates', async () => { const discover = vi.fn(() => Promise.resolve(ok([ { id: 'alpha' }, { id: 'opaque-id', name: 'Beta Display' }, { id: 'gamma' }, ]))) @@ -796,10 +820,14 @@ describe('endpoint interrogation', () => { expect([...dialog.querySelectorAll('input[type="checkbox"]')] .map(box => box.checked)).toEqual([false]) - // Deselecting a filtered result must also clear hidden selections so they - // cannot be adopted accidentally. + // Hidden candidates keep their state, so a configured model is never + // removed unseen. fireEvent.change(search, { target: { value: '' } }) const boxes = [...dialog.querySelectorAll('input[type="checkbox"]')] + expect(boxes.map(box => box.checked)).toEqual([true, false, true]) + fireEvent.click(within_(dialog, en.fetchSelectAll)) + expect(boxes.map(box => box.checked)).toEqual([true, true, true]) + fireEvent.click(within_(dialog, en.fetchDeselectAll)) expect(boxes.map(box => box.checked)).toEqual([false, false, false]) // Selecting while filtered adds only visible candidates. From 491fa4a3948f0ee1f82dab7ac94898e3cde66660 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Thu, 15 Oct 2026 20:12:09 +0800 Subject: [PATCH 069/369] refactor(session-projection): clarify retained handle names --- packages/api/session-controller/src/agent.ts | 6 +++--- packages/context/harness-dev-context/src/index.ts | 4 ++-- packages/context/time-context/src/index.ts | 4 ++-- packages/core/agent-loop/src/index.ts | 8 ++++---- packages/core/agent/src/index.ts | 6 +++--- packages/experimental/agent-team/src/index.ts | 4 ++-- packages/experimental/agent-team/src/journal.ts | 6 +++--- packages/goal/goal/src/index.ts | 8 ++++---- packages/interaction/permission-presets/src/index.ts | 6 +++--- packages/interaction/user-questions/src/index.ts | 8 ++++---- packages/llm/llm-retry/src/index.ts | 4 ++-- packages/plan/plan-mode/src/index.ts | 6 +++--- packages/preset/agent-preset-registry/src/index.ts | 8 ++++---- packages/sandbox/sandbox-policy/src/index.ts | 6 +++--- packages/session/session-projection/README.md | 4 ++-- packages/session/session-projection/README.zh.md | 4 ++-- packages/session/session-title/src/index.ts | 6 +++--- packages/session/working-directory/src/index.ts | 8 ++++---- packages/subagent/tool-subagent/src/index.ts | 8 ++++---- .../tool-subagent/src/model-selection-state.ts | 12 ++++++------ 20 files changed, 63 insertions(+), 63 deletions(-) diff --git a/packages/api/session-controller/src/agent.ts b/packages/api/session-controller/src/agent.ts index 6c967c5c10..4dee6de84a 100644 --- a/packages/api/session-controller/src/agent.ts +++ b/packages/api/session-controller/src/agent.ts @@ -170,7 +170,7 @@ export async function inspectApiSession( /** Owns every operation that may create, resume, or configure a Web Agent. */ export class ApiSessionAgentController { - private modelSelection!: ProjectionHandle + private modelSelectionProjectionHandle!: ProjectionHandle private readonly resumes = new Map>() private readonly claims = new Map>() private readonly selections = new WeakMap() @@ -185,7 +185,7 @@ export class ApiSessionAgentController { */ async initialize(): Promise { const ctx = this.ctx - this.modelSelection = await ctx.sessionProjections.register(modelSelectionProjection) + this.modelSelectionProjectionHandle = await ctx.sessionProjections.register(modelSelectionProjection) ctx.typert.lookups.configure('agent', async (sessionId: SessionId) => { const found = await this.resolveAgent(sessionId) if ('error' in found) throw found.error @@ -300,7 +300,7 @@ export class ApiSessionAgentController { selectionFor(agent: Agent): InstalledSelection { const installed = this.selections.get(agent) if (installed !== undefined) return installed - const projectionState = this.modelSelection.stateOf(agent.session) + const projectionState = this.modelSelectionProjectionHandle.stateOf(agent.session) let picked = projectionState.pending === null ? undefined : agentModelSelection(projectionState.pending) diff --git a/packages/context/harness-dev-context/src/index.ts b/packages/context/harness-dev-context/src/index.ts index d823735a2a..588bb84b9b 100644 --- a/packages/context/harness-dev-context/src/index.ts +++ b/packages/context/harness-dev-context/src/index.ts @@ -127,7 +127,7 @@ const ADMITS_NOTHING: LegacyPrompt = { source: null, web: null } * @returns after the projection reader and prompt registrations are ready. */ export async function apply(ctx: Context): Promise { - const legacyPrompt = await ctx.sessionProjections.register({ + const legacyPromptProjectionHandle = await ctx.sessionProjections.register({ key: LEGACY_PROMPT, stateVersion: 1, stateSchema: legacyPromptSchema, @@ -136,7 +136,7 @@ export async function apply(ctx: Context): Promise { }) const admitted = (context: AssembleContext): LegacyPrompt => { const session = context.agent?.session - return session === undefined ? ADMITS_NOTHING : legacyPrompt.stateOf(session) + return session === undefined ? ADMITS_NOTHING : legacyPromptProjectionHandle.stateOf(session) } ctx.systemPrompt.section({ name: HARNESS_SOURCE, diff --git a/packages/context/time-context/src/index.ts b/packages/context/time-context/src/index.ts index e38ff615fa..25cc4b484d 100644 --- a/packages/context/time-context/src/index.ts +++ b/packages/context/time-context/src/index.ts @@ -155,7 +155,7 @@ export async function apply(ctx: Context, config: Config): Promise { return created } - const projection = await ctx.sessionProjections.register({ + const timeContextProjectionHandle = await ctx.sessionProjections.register({ key: 'timeContext', stateVersion: 2, stateSchema: timeContextStateSchema, @@ -190,7 +190,7 @@ export async function apply(ctx: Context, config: Config): Promise { const decision = await next() if (decision.kind === 'reject' || signal.aborted) return decision const now = Date.now() - const state = projection.stateOf(agent.session) + const state = timeContextProjectionHandle.stateOf(agent.session) if (refreshIntervalMs > 0) { const lastInjection = state.lastInjectionTime if (lastInjection != null diff --git a/packages/core/agent-loop/src/index.ts b/packages/core/agent-loop/src/index.ts index 54476ac913..dc68fef4e0 100644 --- a/packages/core/agent-loop/src/index.ts +++ b/packages/core/agent-loop/src/index.ts @@ -308,7 +308,7 @@ export class AgentLoop extends Service implements AgentFactory { private readonly ownership: FactoryOwnership /** Plain holder prevents Cordis from re-tracing the factory's dependency context through a caller shadow. */ private readonly runtime: { ctx: Context } - private inbox!: ProjectionHandle + private inboxProjectionHandle!: ProjectionHandle constructor(ctx: Context, config: Config) { super(ctx, 'agentLoop') @@ -327,7 +327,7 @@ export class AgentLoop extends Service implements AgentFactory { const ownership = this.ownership // The inbox remains readable until agent teardown finishes. ctx.effect(() => () => ownership.dispose(), 'agentLoop.transactions()') - this.inbox = await ctx.sessionProjections.register(inboxProjectionDefinition) + this.inboxProjectionHandle = await ctx.sessionProjections.register(inboxProjectionDefinition) ctx.effect(() => ctx.agents.setFactory(this), 'agentLoop.setFactory()') ctx.systemPrompt.variable('provider', context => context.agent?.options.provider) ctx.systemPrompt.variable('model', context => context.agent?.options.model) @@ -543,10 +543,10 @@ export class AgentLoop extends Service implements AgentFactory { } } const untrack = this.ownership.track(dispose) - const inbox = this.inbox + const inboxProjectionHandle = this.inboxProjectionHandle try { unfollowOwner = ownerCtx.effect(function* () { - const created = machine = new ReactLoopAgent(loopCtx, id, options, session, () => inbox.stateOf(session), lastTurn) + const created = machine = new ReactLoopAgent(loopCtx, id, options, session, () => inboxProjectionHandle.stateOf(session), lastTurn) machineReady.resolve() // Adopt the scope on its original fiber so it cannot unload before teardown. // A loop already unloading during minting owns the provisional scope itself. diff --git a/packages/core/agent/src/index.ts b/packages/core/agent/src/index.ts index 270aadbef9..d1515665e4 100644 --- a/packages/core/agent/src/index.ts +++ b/packages/core/agent/src/index.ts @@ -247,7 +247,7 @@ interface FactorySlot { export class AgentRegistry extends Service { static inject = ['sessionProjections'] - private turnBoundaryProjection!: ProjectionHandle + private turnBoundaryProjectionHandle!: ProjectionHandle private store = new Map() private factory: FactorySlot | undefined private readonly initiators = new AsyncLocalStorage() @@ -290,7 +290,7 @@ export class AgentRegistry extends Service { } protected async [Service.init](): Promise { - this.turnBoundaryProjection = await this.ctx.sessionProjections.register(turnBoundaryProjectionDefinition) + this.turnBoundaryProjectionHandle = await this.ctx.sessionProjections.register(turnBoundaryProjectionDefinition) } /** @@ -299,7 +299,7 @@ export class AgentRegistry extends Service { * @returns the current turn and step positions; do not mutate the returned value. */ turnBoundaryOf(session: Session): TurnBoundaryProjection { - return this.turnBoundaryProjection.stateOf(session) + return this.turnBoundaryProjectionHandle.stateOf(session) } /** diff --git a/packages/experimental/agent-team/src/index.ts b/packages/experimental/agent-team/src/index.ts index a37e4e22b3..c0da4c9a24 100644 --- a/packages/experimental/agent-team/src/index.ts +++ b/packages/experimental/agent-team/src/index.ts @@ -90,8 +90,8 @@ export class TeamService extends Service { protected async [Service.init](): Promise { const ctx = this.ctx - const projection = await ctx.sessionProjections.register(teamProjectionDefinition) - this.journal = new TeamJournal(ctx, projection, (root) => { this.activity.notify(TeamId(root.id)) }) + const teamProjectionHandle = await ctx.sessionProjections.register(teamProjectionDefinition) + this.journal = new TeamJournal(ctx, teamProjectionHandle, (root) => { this.activity.notify(TeamId(root.id)) }) this.roster = new TeamRoster(ctx, this.journal, this.lifecycle, this.config.maxMembers) this.tasks = new TeamTaskBoard(this.journal, this.config.maxTasks) ctx.effect(() => () => this.disposeRuntime(), 'agentTeams.runtimeLifecycle()') diff --git a/packages/experimental/agent-team/src/journal.ts b/packages/experimental/agent-team/src/journal.ts index 6a6002bb72..d57fcca30b 100644 --- a/packages/experimental/agent-team/src/journal.ts +++ b/packages/experimental/agent-team/src/journal.ts @@ -15,12 +15,12 @@ export class TeamJournal { /** * @param ctx - Team service context with the injected Session service. - * @param projection - Team state registration owned by the Team service. + * @param teamProjectionHandle - Team state registration owned by the Team service. * @param onCommit - synchronous notification after the Team event flush succeeds. */ constructor( private readonly ctx: Context, - private readonly projection: ProjectionHandle, + private readonly teamProjectionHandle: ProjectionHandle, private readonly onCommit: (root: Agent) => void, ) {} @@ -31,7 +31,7 @@ export class TeamJournal { * @throws when the durable Team projection has failed. */ state(root: Agent): TeamState { - const projection = this.projection.stateOf(root.session) + const projection = this.teamProjectionHandle.stateOf(root.session) if (projection.failure !== undefined) throw new Error(projection.failure) return projection } diff --git a/packages/goal/goal/src/index.ts b/packages/goal/goal/src/index.ts index cbc0344b95..275b757eba 100644 --- a/packages/goal/goal/src/index.ts +++ b/packages/goal/goal/src/index.ts @@ -247,7 +247,7 @@ export class GoalService extends TypertRemoteService { private readonly resolved: ResolvedConfig private readonly runtimeStates = new WeakMap() - private projection!: ProjectionHandle + private goalProjectionHandle!: ProjectionHandle constructor(ctx: Context, config: Config = {}) { super(ctx, 'goals') @@ -258,7 +258,7 @@ export class GoalService extends TypertRemoteService { protected async [Service.init](): Promise { const ctx = this.ctx - this.projection = await ctx.sessionProjections.register(goalProjectionDefinition) + this.goalProjectionHandle = await ctx.sessionProjections.register(goalProjectionDefinition) ctx.on('agent/created', ({ agent }) => { this.setActivation(agent.session, 'disarmed') }) @@ -480,7 +480,7 @@ export class GoalService extends TypertRemoteService { /** Read the current durable projection maintained by the registry. */ private state(session: Session): GoalProjection | null { - const state = this.projection.stateOf(session) + const state = this.goalProjectionHandle.stateOf(session) if (state.failure !== null) throw new Error(state.failure) return state.current } @@ -502,7 +502,7 @@ export class GoalService extends TypertRemoteService { const runtime = this.runtimeState(session) if (runtime.activation === activation) return runtime.activation = activation - const state = this.projection.stateOf(session) + const state = this.goalProjectionHandle.stateOf(session) if (state.failure !== null) return const goal = this.view(state.current, runtime) this.ctx.emit('goal/activation-changed', { diff --git a/packages/interaction/permission-presets/src/index.ts b/packages/interaction/permission-presets/src/index.ts index e9cb9a38c3..580443d7c3 100644 --- a/packages/interaction/permission-presets/src/index.ts +++ b/packages/interaction/permission-presets/src/index.ts @@ -204,7 +204,7 @@ export class PermissionPresetService extends TypertRemoteService { private autoAdmit: (() => void) | undefined private defaultSettings: () => PermissionSettings - private projection!: ProjectionHandle + private permissionProjectionHandle!: ProjectionHandle constructor(ctx: Context, config: Config) { super(ctx, 'permissionPresets') @@ -239,7 +239,7 @@ export class PermissionPresetService extends TypertRemoteService { const selectionSchema = zod.object({ currentValue: zod.string().min(1), }) as zod.ZodType - this.projection = await ctx.sessionProjections.register({ + this.permissionProjectionHandle = await ctx.sessionProjections.register({ key: 'permissions', stateVersion: 2, stateSchema: permissionStateSchema, @@ -331,7 +331,7 @@ export class PermissionPresetService extends TypertRemoteService { } private permissionState(session: Session): PermissionProjectionState { - return this.projection.stateOf(session) + return this.permissionProjectionHandle.stateOf(session) } /** diff --git a/packages/interaction/user-questions/src/index.ts b/packages/interaction/user-questions/src/index.ts index 1a93231646..846a8a65be 100644 --- a/packages/interaction/user-questions/src/index.ts +++ b/packages/interaction/user-questions/src/index.ts @@ -78,7 +78,7 @@ type QueuedReply = { messageId: MessageId; claimedTurn?: number } /** `ctx.userQuestions`: validation plus the scoped answerer waterfall. */ export class UserQuestionService extends TypertRemoteService { static Config = z.object({}) - private projection: ProjectionHandle | undefined + private userQuestionProjectionHandle: ProjectionHandle | undefined private readonly waits = new Map>() private readonly queuedReplies = new WeakMap>() @@ -128,8 +128,8 @@ export class UserQuestionService extends TypertRemoteService { protected async [Service.init](): Promise { const binding = this.ctx.inject(['sessionProjections'], async (ctx) => { - this.projection = await ctx.sessionProjections.register(userQuestionProjectionDefinition) - return () => { this.projection = undefined } + this.userQuestionProjectionHandle = await ctx.sessionProjections.register(userQuestionProjectionDefinition) + return () => { this.userQuestionProjectionHandle = undefined } }) if (this.ctx.get('sessionProjections') !== undefined) await binding } @@ -150,7 +150,7 @@ export class UserQuestionService extends TypertRemoteService { } private continued(agent: Agent): readonly PendingUserQuestion[] { - return this.projection?.stateOf(agent.session).questions.active.filter(question => question.state === 'continued') ?? [] + return this.userQuestionProjectionHandle?.stateOf(agent.session).questions.active.filter(question => question.state === 'continued') ?? [] } /** diff --git a/packages/llm/llm-retry/src/index.ts b/packages/llm/llm-retry/src/index.ts index 016d19f400..6b7fb6e682 100644 --- a/packages/llm/llm-retry/src/index.ts +++ b/packages/llm/llm-retry/src/index.ts @@ -123,7 +123,7 @@ declare module '@deepseek-ai/dsh-session-projection/types' { */ export async function apply(ctx: Context, config: Config = {}, internals: RetryInternals = {}): Promise { validateConfig(config) - const projection = await ctx.sessionProjections.register({ + const llmRetryProjectionHandle = await ctx.sessionProjections.register({ key: 'llmRetry', stateVersion: 1, stateSchema: llmRetryStateSchema, @@ -218,7 +218,7 @@ export async function apply(ctx: Context, config: Config = {}, internals: RetryI } const policyKey = retryPolicyKey(policy) - const retryState = projection.stateOf(agent.session) + const retryState = llmRetryProjectionHandle.stateOf(agent.session) const previous = retryState[retryStateKey(provider, policyKey)] const previousRetry = previous?.retry ?? 0 if (policy.mode === 'normal' && previousRetry >= policy.maxRetries) return next() diff --git a/packages/plan/plan-mode/src/index.ts b/packages/plan/plan-mode/src/index.ts index 603810713a..e4034fa549 100644 --- a/packages/plan/plan-mode/src/index.ts +++ b/packages/plan/plan-mode/src/index.ts @@ -177,7 +177,7 @@ export class PlanModeController extends Service { /** Validated deployment-owned guidance. */ private readonly section: string - private projection!: ProjectionHandle + private planProjectionHandle!: ProjectionHandle /** * Latest selection per session awaiting the next accepted in-turn pre-step. @@ -193,7 +193,7 @@ export class PlanModeController extends Service { protected async [Service.init](): Promise { const ctx = this.ctx - this.projection = await ctx.sessionProjections.register(planProjectionDefinition) + this.planProjectionHandle = await ctx.sessionProjections.register(planProjectionDefinition) let disposed = false // Pre-step is outside Session.append publication, so it can append the // log-only mode event inside an open turn without re-entering the session. @@ -389,7 +389,7 @@ export class PlanModeController extends Service { } private planState(session: Session): PlanUnitState { - return this.projection.stateOf(session) + return this.planProjectionHandle.stateOf(session) } /** diff --git a/packages/preset/agent-preset-registry/src/index.ts b/packages/preset/agent-preset-registry/src/index.ts index 4b4bb467d0..5205a58228 100644 --- a/packages/preset/agent-preset-registry/src/index.ts +++ b/packages/preset/agent-preset-registry/src/index.ts @@ -57,7 +57,7 @@ export class AgentPresetRegistry extends TypertRemoteService { default: z.string().required(), selectedDefault: z.string().volatile(), }) - private agentPreset!: ProjectionHandle + private agentPresetProjectionHandle!: ProjectionHandle private readonly owner: Context private readonly definitions = new Map() private readonly generations = new Map() @@ -71,7 +71,7 @@ export class AgentPresetRegistry extends TypertRemoteService { protected async [Service.init](): Promise { const ctx = this.owner - this.agentPreset = await ctx.sessionProjections.register(agentPresetProjectionDefinition) + this.agentPresetProjectionHandle = await ctx.sessionProjections.register(agentPresetProjectionDefinition) ctx.inject(['settings'], (child) => { child.effect(() => child.settings.configure({ auto: false }, ctx.fiber)) }) ctx.on('session/event', (session, event) => { if (event.type === 'agent-preset/selected') ctx.emit('agent-preset/selected', session.id, event.data.agentPreset) @@ -87,7 +87,7 @@ export class AgentPresetRegistry extends TypertRemoteService { * @returns the recorded preset, or null when none was selected. */ presetForSession(session: Session): string | null { - return this.agentPreset.stateOf(session) + return this.agentPresetProjectionHandle.stateOf(session) } /** Register and eagerly load a definition; activation failure remains visible in the roster. @@ -370,7 +370,7 @@ export class AgentPresetRegistry extends TypertRemoteService { } const current = this.definitions.get(agentPreset)?.generation if (current !== undefined && this.generationFor(agent.ctx) === current - && this.agentPreset.stateOf(agent.session) === agentPreset) return agentPreset + && this.agentPresetProjectionHandle.stateOf(agent.session) === agentPreset) return agentPreset const preset = await this.recompose(agent.ctx, agentPreset) agent.session.append('agent-preset/selected', { agentPreset: preset.id }) return preset.id diff --git a/packages/sandbox/sandbox-policy/src/index.ts b/packages/sandbox/sandbox-policy/src/index.ts index c966e6a4ea..942838ca36 100644 --- a/packages/sandbox/sandbox-policy/src/index.ts +++ b/packages/sandbox/sandbox-policy/src/index.ts @@ -122,7 +122,7 @@ export class SandboxPolicyService extends Service { readonly defaultMode: SandboxMode /** The absolute `workspace-write` fallback root for calls without a session cwd. */ readonly workspaceRoot: string - private projection!: ProjectionHandle + private sandboxModeProjectionHandle!: ProjectionHandle constructor(ctx: Context, config: Config) { super(ctx, 'sandboxPolicy') @@ -135,7 +135,7 @@ export class SandboxPolicyService extends Service { protected async [Service.init](): Promise { const ctx = this.ctx - this.projection = await ctx.sessionProjections.register({ + this.sandboxModeProjectionHandle = await ctx.sessionProjections.register({ key: 'sandboxMode', stateVersion: 1, stateSchema: sandboxModeStateSchema, @@ -181,7 +181,7 @@ export class SandboxPolicyService extends Service { * @returns the last logged mode, or `undefined` without one. */ overrideOf(session: Session): SandboxMode | undefined { - return this.projection.stateOf(session) ?? undefined + return this.sandboxModeProjectionHandle.stateOf(session) ?? undefined } } diff --git a/packages/session/session-projection/README.md b/packages/session/session-projection/README.md index 4ddaed7c6b..930de0955d 100644 --- a/packages/session/session-projection/README.md +++ b/packages/session/session-projection/README.md @@ -58,8 +58,8 @@ const definition = { `await register(definition)` installs the unit and returns a handle with synchronous `stateOf(session)` reads and `dispose(): Promise`. Await `dispose()` to release the registration; repeated calls are safe. Registrants with the same key and `stateVersion` share cells; incompatible or invalid versions reject registration. Registration belongs to the calling fiber. The last unregister removes the unit from global event drives, snapshots, checkpoints, and change notifications. Existing handles remain readable: each retains its original unit and folds new Session events on demand, even if another version later registers the same key. Reading a handle does not initialize unrelated units or register it again. Carriers use `snapshot(session)` for a consistent client-view cut and `onChanged(listener)` for changes to registered units. ```text -const projection = await ctx.sessionProjections.register(definition) -const state = projection.stateOf(session) +const todoProjectionHandle = await ctx.sessionProjections.register(definition) +const state = todoProjectionHandle.stateOf(session) const { asOfSeq, values } = ctx.sessionProjections.snapshot(session) ``` diff --git a/packages/session/session-projection/README.zh.md b/packages/session/session-projection/README.zh.md index de05300b76..e0adbadde0 100644 --- a/packages/session/session-projection/README.zh.md +++ b/packages/session/session-projection/README.zh.md @@ -58,8 +58,8 @@ const definition = { `await register(definition)` 安装单元,返回提供同步 `stateOf(session)` 读取和 `dispose(): Promise` 的 handle。等待 `dispose()` 完成即可释放注册;重复调用是安全的。具有相同 key 和 `stateVersion` 的注册方共享 cell;版本不兼容或非法时注册会拒绝。注册归调用方 fiber 所有。最后一个注册方注销后,单元退出全局事件驱动、快照、检查点和变更通知。已有 handle 仍可读取:它保留原有单元,按需折叠新增的 Session 事件,即使同一 key 后来注册了其他版本也不受影响。读取 handle 不会初始化无关单元,也不会重新注册。载体用 `snapshot(session)` 读取一致的客户端视图切面,用 `onChanged(listener)` 订阅已注册单元的变更。 ```text -const projection = await ctx.sessionProjections.register(definition) -const state = projection.stateOf(session) +const todoProjectionHandle = await ctx.sessionProjections.register(definition) +const state = todoProjectionHandle.stateOf(session) const { asOfSeq, values } = ctx.sessionProjections.snapshot(session) ``` diff --git a/packages/session/session-title/src/index.ts b/packages/session/session-title/src/index.ts index 8b888b80d4..b2d43669d1 100644 --- a/packages/session/session-title/src/index.ts +++ b/packages/session/session-title/src/index.ts @@ -306,7 +306,7 @@ export class SessionTitleService extends Service { maxTitleBytes: z.number().step(1).min(1).required(), }) - private titleInput!: ProjectionHandle + private titleInputProjectionHandle!: ProjectionHandle private readonly config: ResolvedConfig private readonly ownerFiber: Fiber private registration: ProviderRegistration | undefined @@ -347,7 +347,7 @@ export class SessionTitleService extends Service { await ctx.sessionProjections.register(titleProjectionDefinition) - this.titleInput = await ctx.sessionProjections.register<'titleInput', TitleInputState>({ + this.titleInputProjectionHandle = await ctx.sessionProjections.register<'titleInput', TitleInputState>({ key: 'titleInput', stateVersion: 3, stateSchema: titleInputStateSchema, @@ -734,7 +734,7 @@ export class SessionTitleService extends Service { } private titleInputOf(session: Session): TitleInputState { - return this.titleInput.stateOf(session) + return this.titleInputProjectionHandle.stateOf(session) } /** Queue detached service work and retain it through service disposal. */ diff --git a/packages/session/working-directory/src/index.ts b/packages/session/working-directory/src/index.ts index 60c8ad9c2a..61ca035d70 100644 --- a/packages/session/working-directory/src/index.ts +++ b/packages/session/working-directory/src/index.ts @@ -72,7 +72,7 @@ export class WorkingDirectoryService extends Service { private readonly operations = new Set>() private readonly lifecycle = new AbortController() - private projection!: ProjectionHandle + private workingDirectoryProjectionHandle!: ProjectionHandle constructor(ctx: Context, config: Config) { super(ctx, 'workingDirectory') @@ -84,7 +84,7 @@ export class WorkingDirectoryService extends Service { protected async [Service.init](): Promise { const ctx = this.ctx const directorySchema = zod.string().min(1).refine(isAbsolute).nullable() - this.projection = await ctx.sessionProjections.register({ + this.workingDirectoryProjectionHandle = await ctx.sessionProjections.register({ key: 'workingDirectory', stateVersion: 1, stateSchema: directorySchema, @@ -125,7 +125,7 @@ export class WorkingDirectoryService extends Service { * @returns its effective absolute directory. */ get(session: Session): string { - return this.projection.stateOf(session) ?? this.defaultDirectory + return this.workingDirectoryProjectionHandle.stateOf(session) ?? this.defaultDirectory } /** @@ -144,7 +144,7 @@ export class WorkingDirectoryService extends Service { control.throwIfAborted() agent.ctx.fiber.assertActive() if (info?.type === 'directory') { - if (this.projection.stateOf(agent.session) === null) { + if (this.workingDirectoryProjectionHandle.stateOf(agent.session) === null) { agent.session.append('working-directory/change', { cwd: current }) } return current diff --git a/packages/subagent/tool-subagent/src/index.ts b/packages/subagent/tool-subagent/src/index.ts index 9ca378d63d..bd0b19da01 100644 --- a/packages/subagent/tool-subagent/src/index.ts +++ b/packages/subagent/tool-subagent/src/index.ts @@ -160,7 +160,7 @@ export async function apply(ctx: Context, config: Config, session?: Session): Pr const toolName = config.toolName ?? 'subagent' const modelSelectionCapable = config.modelSelectionSettings === true - const modelSelectionProjection = await ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition) + const subagentModelSelectionProjectionHandle = await ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition) const assertSubagentProviderConfiguration = (subagentProvider: SubagentProvider): void => { presets.assertProvider(subagentProvider) @@ -416,7 +416,7 @@ export async function apply(ctx: Context, config: Config, session?: Session): Pr const freshSession = target.firstLiveSeq === 0 // oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred. && target.eventAt(SessionSeq(0))?.type !== 'session/end-seed' - let allowedModels = subagentModelSelectionPolicy(modelSelectionProjection, target) + let allowedModels = subagentModelSelectionPolicy(subagentModelSelectionProjectionHandle, target) if (allowedModels === undefined) { const parentId = target.header.origin === 'subagent' ? target.header.parentSession @@ -429,14 +429,14 @@ export async function apply(ctx: Context, config: Config, session?: Session): Pr const parent = sessions.get(parentId) allowedModels = parent === undefined ? undefined - : subagentModelSelectionPolicy(modelSelectionProjection, parent) + : subagentModelSelectionPolicy(subagentModelSelectionProjectionHandle, parent) } else if (freshSession) { const current = settings.current() allowedModels = current.enabled ? current.allowedModels : undefined } } if (allowedModels !== undefined) { - recordSubagentModelSelection(modelSelectionProjection, target, allowedModels) + recordSubagentModelSelection(subagentModelSelectionProjectionHandle, target, allowedModels) } return allowedModels === undefined ? undefined : { routes: allowedModels } } diff --git a/packages/subagent/tool-subagent/src/model-selection-state.ts b/packages/subagent/tool-subagent/src/model-selection-state.ts index 9b51f384f7..92cdac56bb 100644 --- a/packages/subagent/tool-subagent/src/model-selection-state.ts +++ b/packages/subagent/tool-subagent/src/model-selection-state.ts @@ -52,29 +52,29 @@ export const subagentModelSelectionProjectionDefinition = { /** * Read the exact route list captured for a model-selectable definition. - * @param projection - registered policy state reader. + * @param subagentModelSelectionProjectionHandle - registered policy state reader. * @param session - session whose durable decision is read. * @returns a detached route list, or undefined for the fixed-route definition. */ export function subagentModelSelectionPolicy( - projection: Pick, 'stateOf'>, + subagentModelSelectionProjectionHandle: Pick, 'stateOf'>, session: Session, ): AllowedModelRoute[] | undefined { - return projection.stateOf(session)?.map(route => ({ ...route })) + return subagentModelSelectionProjectionHandle.stateOf(session)?.map(route => ({ ...route })) } /** * Append the route policy once, before its definition can reach a model request. - * @param projection - registered policy state reader. + * @param subagentModelSelectionProjectionHandle - registered policy state reader. * @param session - session receiving the model-selectable definition. * @param allowedModels - exact routes the definition may select explicitly. */ export function recordSubagentModelSelection( - projection: Pick, 'stateOf'>, + subagentModelSelectionProjectionHandle: Pick, 'stateOf'>, session: Session, allowedModels: readonly AllowedModelRoute[], ): void { - if (subagentModelSelectionPolicy(projection, session) !== undefined) return + if (subagentModelSelectionPolicy(subagentModelSelectionProjectionHandle, session) !== undefined) return session.append('subagent/model-selection-policy', { allowedModels: allowedModels.map(route => ({ ...route })), }) From 5ee64b7005922171821ae2b5d1ee459fa7bb3f70 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Thu, 15 Oct 2026 20:12:24 +0800 Subject: [PATCH 070/369] refactor(agent): use a type-only SessionSeq import --- packages/core/agent/package.json | 1 + packages/core/agent/src/projection.ts | 11 +++++++---- packages/core/agent/tsconfig.json | 3 +++ pnpm-lock.yaml | 3 +++ 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/core/agent/package.json b/packages/core/agent/package.json index afb858d995..694bcf96f5 100644 --- a/packages/core/agent/package.json +++ b/packages/core/agent/package.json @@ -62,6 +62,7 @@ "@deepseek-ai/cordis": "workspace:~" }, "dependencies": { + "@deepseek-ai/dsh-brand": "workspace:*", "zod": "4.4.3" } } diff --git a/packages/core/agent/src/projection.ts b/packages/core/agent/src/projection.ts index 64333a692f..c4a56449f3 100644 --- a/packages/core/agent/src/projection.ts +++ b/packages/core/agent/src/projection.ts @@ -1,7 +1,8 @@ /** Shared pure fold of the Session's turn and step events. */ import type { TurnBoundaryProjection } from './types.ts' import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection' -import { SessionSeq } from '@deepseek-ai/dsh-session' +import type { SessionSeq } from '@deepseek-ai/dsh-session/types' +import { brandNumber } from '@deepseek-ai/dsh-brand' import { z as zod } from 'zod' declare module '@deepseek-ai/dsh-session-projection/types' { @@ -11,12 +12,14 @@ declare module '@deepseek-ai/dsh-session-projection/types' { } } +const sessionSeqSchema = zod.number().int().nonnegative().transform(brandNumber) + const turnBoundaryProjectionSchema: zod.ZodType = zod.object({ - openTurnStartSeq: zod.number().int().nonnegative().transform(SessionSeq).nullable(), - lastStepStartSeq: zod.number().int().nonnegative().transform(SessionSeq).nullable(), + openTurnStartSeq: sessionSeqSchema.nullable(), + lastStepStartSeq: sessionSeqSchema.nullable(), lastStepBoundary: zod.object({ kind: zod.union([zod.literal('start'), zod.literal('end')]), - seq: zod.number().int().nonnegative().transform(SessionSeq), + seq: sessionSeqSchema, }).nullable(), lastTurn: zod.number().int().nonnegative(), }) diff --git a/packages/core/agent/tsconfig.json b/packages/core/agent/tsconfig.json index b597fc7068..a84b932abe 100644 --- a/packages/core/agent/tsconfig.json +++ b/packages/core/agent/tsconfig.json @@ -14,6 +14,9 @@ { "path": "../../../vendor/cordis" }, + { + "path": "../../util/brand" + }, { "path": "../../core/scope" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e3b75b9858..9e2ccb8ae2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7055,6 +7055,9 @@ importers: packages/core/agent: dependencies: + '@deepseek-ai/dsh-brand': + specifier: workspace:* + version: link:../../util/brand zod: specifier: 4.4.3 version: 4.4.3 From e152f17bb2304039ac9058bf7ec4ce3e32270967 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Thu, 15 Oct 2026 20:12:36 +0800 Subject: [PATCH 071/369] refactor(token-meter): read pressure facts from retained state --- docs/subsystems/token-meter.i18n.yaml | 4 ++-- docs/subsystems/token-meter.md | 6 +++--- docs/subsystems/token-meter.zh.md | 6 +++--- packages/extensions/tool-cordis/src/api-catalog.ts | 4 ++-- packages/llm/token-meter/README.i18n.yaml | 4 ++-- packages/llm/token-meter/README.md | 2 +- packages/llm/token-meter/README.zh.md | 2 +- packages/llm/token-meter/src/index.ts | 14 +++++++++----- .../tests/token-usage-projection.spec.ts | 3 ++- 9 files changed, 25 insertions(+), 20 deletions(-) diff --git a/docs/subsystems/token-meter.i18n.yaml b/docs/subsystems/token-meter.i18n.yaml index eff44c4e64..f205395403 100644 --- a/docs/subsystems/token-meter.i18n.yaml +++ b/docs/subsystems/token-meter.i18n.yaml @@ -12,5 +12,5 @@ en: e445d12b10f651d0 zh: e93be5fb9da1da15 /token-meter/contextpressureprojection: - en: 78c8308e9f54fc38 - zh: f76658e7a2739807 + en: 37e477a82a9c3a83 + zh: ad7143becb29afe9 diff --git a/docs/subsystems/token-meter.md b/docs/subsystems/token-meter.md index 9476165e45..1aa56a8d24 100644 --- a/docs/subsystems/token-meter.md +++ b/docs/subsystems/token-meter.md @@ -55,7 +55,7 @@ Surface order is authoritative; replacement nodes can have higher durable seqs t ## `ContextPressureProjection` -`ctx.tokenMeter.contextPressure(session)` synchronously reads the latest recorded context pressure and capacity. These display values do not price a new request; use `measure()` for request pricing. +`ctx.tokenMeter.contextPressure(session)` synchronously reads the latest logged `pressureTokens` and `contextWindow`; the client projection also derives `projectedTokens` from surface changes. These display values do not price a new request; use `measure()` for request pricing. Source: [`packages/llm/token-meter/src/projection.ts`](../../packages/llm/token-meter/src/projection.ts) @@ -107,11 +107,11 @@ Replay owner for one service-wide estimator and isolated per-session folds. ```ts cordis-catalog /** - * Read the latest logged context occupancy and its projected surface adjustment. + * Read the latest logged provider prompt size and route capacity. * @param session - Session whose committed usage and request capacity are read. * @returns context pressure; fields remain absent until their source events are recorded. */ -contextPressure(session: Session): ContextPressureProjection +contextPressure(session: Session): Pick /** * Measure current request pressure and surface through the durable tail. diff --git a/docs/subsystems/token-meter.zh.md b/docs/subsystems/token-meter.zh.md index 07893eacce..970f90d9dd 100644 --- a/docs/subsystems/token-meter.zh.md +++ b/docs/subsystems/token-meter.zh.md @@ -55,7 +55,7 @@ interface TokenSurfaceNode { ## `ContextPressureProjection` -`ctx.tokenMeter.contextPressure(session)` 同步读取最新记录的上下文压力与容量。这些显示值不为新请求计价;请求计价使用 `measure()`。 +`ctx.tokenMeter.contextPressure(session)` 同步读取最新记录的 `pressureTokens` 和 `contextWindow`;客户端投影还根据表层变化派生 `projectedTokens`。这些显示值不为新请求计价;请求计价使用 `measure()`。 来源: [`packages/llm/token-meter/src/projection.ts`](../../packages/llm/token-meter/src/projection.ts) @@ -107,11 +107,11 @@ Replay owner for one service-wide estimator and isolated per-session folds. ```ts cordis-catalog /** - * Read the latest logged context occupancy and its projected surface adjustment. + * Read the latest logged provider prompt size and route capacity. * @param session - Session whose committed usage and request capacity are read. * @returns context pressure; fields remain absent until their source events are recorded. */ -contextPressure(session: Session): ContextPressureProjection +contextPressure(session: Session): Pick /** * Measure current request pressure and surface through the durable tail. diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 20c5ec129d..1c1190b1e0 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -3373,8 +3373,8 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ description: 'Replay owner for one service-wide estimator and isolated per-session folds.', methods: [ { - signature: 'contextPressure(session: Session): ContextPressureProjection', - description: 'Read the latest logged context occupancy and its projected surface adjustment.', + signature: 'contextPressure(session: Session): Pick', + description: 'Read the latest logged provider prompt size and route capacity.', parameters: [{ name: 'session', description: 'Session whose committed usage and request capacity are read.' }], returns: 'context pressure; fields remain absent until their source events are recorded.', }, diff --git a/packages/llm/token-meter/README.i18n.yaml b/packages/llm/token-meter/README.i18n.yaml index ba794c93da..fe4b0d57b1 100644 --- a/packages/llm/token-meter/README.i18n.yaml +++ b/packages/llm/token-meter/README.i18n.yaml @@ -24,8 +24,8 @@ en: 58de3b364ab5af6b zh: c5440af43d321e56 /deepseek-ai-dsh-token-meter/use-this-package/session-projections: - en: afa4b026b55ec1ba - zh: 9805b2924b6a786b + en: acd064f47010ced9 + zh: ef5a456695050242 /deepseek-ai-dsh-token-meter/use-this-package/composition: en: 1a0b7f49e01cba08 zh: 666213a1024767c5 diff --git a/packages/llm/token-meter/README.md b/packages/llm/token-meter/README.md index 214970a2f1..8730acfd18 100644 --- a/packages/llm/token-meter/README.md +++ b/packages/llm/token-meter/README.md @@ -46,7 +46,7 @@ The measurement anchor includes the priced surface immediately before the succes ### Session projections -Token-meter registers three projection units through its required `ctx.sessionProjections` service. Host consumers call `ctx.tokenMeter.contextPressure(session)` to read the current pressure fields synchronously; the registration handle stays private. `tokenUsage` carries the complete durable log's `uncachedInputTokens`, `outputTokens`, `cacheReadTokens`, and `cacheWriteTokens`. A final assistant-message sample replaces streaming usage from the same attempt; `llm/retry-started` ends that replacement scope, so a retry in the same step contributes another billed attempt. `contextPressure` carries optional `pressureTokens` (the newest provider-reported prompt size), optional `projectedTokens` (what the next request's prompt would cost), and optional `contextWindow` from the newest `request/context` record. `contextBreakdown` carries heuristic `systemTokens`, `toolsTokens`, and `messageTokens` — the context's composition, not its provider-billed size. Unloading the plugin removes all three keys. +Token-meter registers three projection units through its required `ctx.sessionProjections` service. Host consumers call `ctx.tokenMeter.contextPressure(session)` to read the latest provider-reported prompt size and route capacity synchronously; the registration handle stays private. `tokenUsage` carries the complete durable log's `uncachedInputTokens`, `outputTokens`, `cacheReadTokens`, and `cacheWriteTokens`. A final assistant-message sample replaces streaming usage from the same attempt; `llm/retry-started` ends that replacement scope, so a retry in the same step contributes another billed attempt. `contextPressure` carries optional `pressureTokens` (the newest provider-reported prompt size), optional `projectedTokens` (what the next request's prompt would cost), and optional `contextWindow` from the newest `request/context` record. `contextBreakdown` carries heuristic `systemTokens`, `toolsTokens`, and `messageTokens` — the context's composition, not its provider-billed size. Unloading the plugin removes all three keys. Image offload reprices existing node identities while preserving prior usage anchors. The fixed reference heuristic excludes `offloaded` metadata, so an offload decision does not change `contextBreakdown` or the scalar heuristic total; route-aware measurement replaces the selected visual prices with placeholder text. diff --git a/packages/llm/token-meter/README.zh.md b/packages/llm/token-meter/README.zh.md index 8f8d4f8a7f..e2a86c6528 100644 --- a/packages/llm/token-meter/README.zh.md +++ b/packages/llm/token-meter/README.zh.md @@ -46,7 +46,7 @@ const price = ctx.tokenMeter.estimateMessage(message) ### 会话投影 -token-meter 通过必需的 `ctx.sessionProjections` 服务注册三个投影单元。Host 消费方调用 `ctx.tokenMeter.contextPressure(session)` 同步读取当前压力字段;注册句柄保留在内部。`tokenUsage` 携带完整持久日志中的 `uncachedInputTokens`、`outputTokens`、`cacheReadTokens` 与 `cacheWriteTokens`。最终 assistant 消息样本会替换同一次尝试的流式用量;`llm/retry-started` 会结束该替换范围,因此同一步骤中的重试会贡献另一次计费用量。`contextPressure` 携带可选 `pressureTokens`(提供方报告的最新提示词规模)、可选 `projectedTokens`(下一个请求的提示词将花费多少)与来自最新一条 `request/context` 记录的可选 `contextWindow`。`contextBreakdown` 携带启发式 `systemTokens`、`toolsTokens` 与 `messageTokens`——上下文的构成,而非提供方计费规模。卸载插件会移除全部三个键。 +token-meter 通过必需的 `ctx.sessionProjections` 服务注册三个投影单元。Host 消费方调用 `ctx.tokenMeter.contextPressure(session)` 同步读取提供方报告的最新提示词规模和路由容量;注册句柄保留在内部。`tokenUsage` 携带完整持久日志中的 `uncachedInputTokens`、`outputTokens`、`cacheReadTokens` 与 `cacheWriteTokens`。最终 assistant 消息样本会替换同一次尝试的流式用量;`llm/retry-started` 会结束该替换范围,因此同一步骤中的重试会贡献另一次计费用量。`contextPressure` 携带可选 `pressureTokens`(提供方报告的最新提示词规模)、可选 `projectedTokens`(下一个请求的提示词将花费多少)与来自最新一条 `request/context` 记录的可选 `contextWindow`。`contextBreakdown` 携带启发式 `systemTokens`、`toolsTokens` 与 `messageTokens`——上下文的构成,而非提供方计费规模。卸载插件会移除全部三个键。 图片省略重新计算现有节点的价格,同时保留此前的用量锚点。固定引用启发式规则不计入 `offloaded` 元数据,因此一次省略决定不改变 `contextBreakdown` 或标量启发式总量,按路由的测量则把所选图片的视觉价格换成占位文本价格。 diff --git a/packages/llm/token-meter/src/index.ts b/packages/llm/token-meter/src/index.ts index 367a0cab76..27768fa394 100644 --- a/packages/llm/token-meter/src/index.ts +++ b/packages/llm/token-meter/src/index.ts @@ -107,7 +107,7 @@ export class TokenMeter extends Service { static inject = ['sessionProjections'] private readonly states = new WeakMap() - private contextPressureProjection!: ProjectionHandle + private contextPressureProjectionHandle!: ProjectionHandle constructor(ctx: Context, config: TokenMeterConfig = {}) { super(ctx, 'tokenMeter') @@ -117,7 +117,7 @@ export class TokenMeter extends Service { protected async [Service.init](): Promise { const ctx = this.ctx await ctx.sessionProjections.register(tokenUsageProjectionDefinition) - this.contextPressureProjection = await ctx.sessionProjections.register(contextPressureProjectionDefinition) + this.contextPressureProjectionHandle = await ctx.sessionProjections.register(contextPressureProjectionDefinition) await ctx.sessionProjections.register(contextBreakdownProjectionDefinition) // Readers catch up independently, while eager observation bounds ordinary @@ -128,12 +128,16 @@ export class TokenMeter extends Service { } /** - * Read the latest logged context occupancy and its projected surface adjustment. + * Read the latest logged provider prompt size and route capacity. * @param session - Session whose committed usage and request capacity are read. * @returns context pressure; fields remain absent until their source events are recorded. */ - contextPressure(session: Session): ContextPressureProjection { - return contextPressureProjectionDefinition.wire.view(this.contextPressureProjection.stateOf(session)) + contextPressure(session: Session): Pick { + const { pressureTokens, contextWindow } = this.contextPressureProjectionHandle.stateOf(session) + return { + ...pressureTokens === undefined ? {} : { pressureTokens }, + ...contextWindow === undefined ? {} : { contextWindow }, + } } /** diff --git a/packages/llm/token-meter/tests/token-usage-projection.spec.ts b/packages/llm/token-meter/tests/token-usage-projection.spec.ts index fee8d43ba5..d14e3dd387 100644 --- a/packages/llm/token-meter/tests/token-usage-projection.spec.ts +++ b/packages/llm/token-meter/tests/token-usage-projection.spec.ts @@ -382,7 +382,8 @@ describe('contextPressure session projection', () => { }, 1, 1) // Output is deliberately absent: occupancy describes the prompt that was // sent, so it holds still while the response streams. - expect(ctx.tokenMeter.contextPressure(session)).toEqual({ pressureTokens: 125, projectedTokens: 125 }) + expect(ctx.tokenMeter.contextPressure(session)).toEqual({ pressureTokens: 125 }) + expect(pressure(ctx, session)).toEqual({ pressureTokens: 125, projectedTokens: 125 }) }) it('replaces pressure with the newest request rather than accumulating', async () => { From f150f2178d95a201ae131f047da9768d39903fc2 Mon Sep 17 00:00:00 2001 From: Turtle Date: Thu, 15 Oct 2026 20:17:52 +0800 Subject: [PATCH 072/369] fix(ui-settings-models): keep model picker selection current and non-empty Derive the picker's checked state from the list at render so a row drafted during the fetch is not removed. Disable Update list when it would empty a non-empty list, because an empty list serves the whole catalog. List a repeated candidate id once and treat rows without an id as unconfigured. --- .../models-settings/model-picker.expected.md | 2 +- .../ui-settings-models/README.i18n.yaml | 4 +- packages/client/ui-settings-models/README.md | 2 +- .../client/ui-settings-models/README.zh.md | 2 +- .../src/client/ModelListEditor.tsx | 86 ++++++++++++------- .../ui-settings-models/src/client/locales.ts | 6 +- .../tests/provider-form.client.spec.tsx | 63 +++++++++++++- 7 files changed, 124 insertions(+), 41 deletions(-) diff --git a/apps/web/tests/expected/models-settings/model-picker.expected.md b/apps/web/tests/expected/models-settings/model-picker.expected.md index a46d19c495..2d5d1cf617 100644 --- a/apps/web/tests/expected/models-settings/model-picker.expected.md +++ b/apps/web/tests/expected/models-settings/model-picker.expected.md @@ -1,7 +1,7 @@ - dialog "选择模型": - heading "选择模型" [level=2] - button "关闭" - - paragraph: 以下是模型提供商的可用模型。已勾选的模型会保留在列表中,取消勾选已配置的模型即可移除。 + - paragraph: 以下是模型提供商的可用模型,勾选的模型即为模型列表。 - searchbox "搜索模型" - button "取消全选" - list: diff --git a/packages/client/ui-settings-models/README.i18n.yaml b/packages/client/ui-settings-models/README.i18n.yaml index f6e2853f79..aa703c0bfc 100644 --- a/packages/client/ui-settings-models/README.i18n.yaml +++ b/packages/client/ui-settings-models/README.i18n.yaml @@ -24,8 +24,8 @@ en: 3af73644655c8b7b zh: d689eba772100c63 /deepseek-ai-dsh-client-ui-settings-models/use-this-package/adding-and-deleting-providers: - en: 5eec53c85e2ef492 - zh: 162904aa543d30ac + en: c0b04b2b11af60d3 + zh: d195eec281e445cb /deepseek-ai-dsh-client-ui-settings-models/use-this-package/first-run-dialogs: en: 589397a35f82244e zh: 342fbceee2c173a2 diff --git a/packages/client/ui-settings-models/README.md b/packages/client/ui-settings-models/README.md index 5bcb805ebb..acb0b40a65 100644 --- a/packages/client/ui-settings-models/README.md +++ b/packages/client/ui-settings-models/README.md @@ -63,7 +63,7 @@ A model row on a hand-declared pi-ai route also edits its **Reasoning levels**, ### Adding and deleting providers -**Add model provider** is the one entry to the add card, offered while any editor-backed settings namespace is mounted and enabled while either mode below can proceed. The card opens on a segmented mode switch with a one-line purpose beneath it: **Third-party model provider** carries the dormant-directory provider select over the provider editor — a bare-mounted `llm-pi-ai` offers its whole installed catalog before any route exists — and **Custom model API** carries the create form for a route pi-ai does not ship: a relay, a self-hosted server, or any other OpenAI- or Anthropic-compatible endpoint. A mode is offered only while its namespace is mounted; with one mode the card shows that form alone under the mode's name, and a mode whose namespace has nothing left to adopt or no protocol to declare is disabled with the reason as its hover text. A panel mounts the first time its mode is shown and stays mounted, hidden, while the card is open and its mode stays offered, so switching modes discards neither draft; the switch locks while either panel has a write or an endpoint interrogation in flight, since a switch underneath one would orphan its answer. Each tab controls its panel through ids, and closing either panel forgets the catalog draft's target. The custom form asks for a unique **Provider ID**, an endpoint, a protocol, and at least one uniquely-identified model, because nothing can default those. The protocol picker names each protocol by its product name — OpenAI Chat Completions, OpenAI Responses, Anthropic Messages — while the stored value stays the schema identifier; a protocol the adapter adds before this page names it shows that identifier. The endpoint placeholder follows the selected protocol: OpenAI Chat Completions and Responses show `https://gateway.example/v1`, while Anthropic Messages shows `https://gateway.example` because its SDK appends `/v1/messages`. Changing the protocol preserves any address already entered. The endpoint must be a parseable HTTP or HTTPS URL; localhost, IPv4 and IPv6 literals, and custom ports remain valid. A syntax error blocks both discovery and creation at the field, while a request failure remains a separate provider error. **Fetch available models** asks the `llm/discoverModels` Remote about the endpoint the form shows, so adding a provider is one pass instead of save-then-return; the reply opens a searchable picker rather than being written, and nothing changes until **Update list**. The picker opens with the configured models checked, or every candidate when the list is empty and the route serves its whole catalog; **Update list** appends newly checked candidates and removes unchecked configured models from the draft, while configured models the reply does not list stay. Each candidate displays its original model id in a monospace font to distinguish models with the same name. Rows stay on one line; hovering reveals the model name, falling back to the full id when no name is supplied. Selection and configuration retain the original model id. Each selected candidate copies its id, display name, context window, output-token cap, and disclosed input types into the editable row when disclosed, while an existing row retains its user-tuned values. Search matches model ids and optional display names without clearing hidden selections. **Select all** and **Deselect all** change only the visible results, so a hidden configured model is not removed unseen. A row is deletable only when the user layer alone carries it (removal restores the composition base), and its confirmation dialog names the provider. +**Add model provider** is the one entry to the add card, offered while any editor-backed settings namespace is mounted and enabled while either mode below can proceed. The card opens on a segmented mode switch with a one-line purpose beneath it: **Third-party model provider** carries the dormant-directory provider select over the provider editor — a bare-mounted `llm-pi-ai` offers its whole installed catalog before any route exists — and **Custom model API** carries the create form for a route pi-ai does not ship: a relay, a self-hosted server, or any other OpenAI- or Anthropic-compatible endpoint. A mode is offered only while its namespace is mounted; with one mode the card shows that form alone under the mode's name, and a mode whose namespace has nothing left to adopt or no protocol to declare is disabled with the reason as its hover text. A panel mounts the first time its mode is shown and stays mounted, hidden, while the card is open and its mode stays offered, so switching modes discards neither draft; the switch locks while either panel has a write or an endpoint interrogation in flight, since a switch underneath one would orphan its answer. Each tab controls its panel through ids, and closing either panel forgets the catalog draft's target. The custom form asks for a unique **Provider ID**, an endpoint, a protocol, and at least one uniquely-identified model, because nothing can default those. The protocol picker names each protocol by its product name — OpenAI Chat Completions, OpenAI Responses, Anthropic Messages — while the stored value stays the schema identifier; a protocol the adapter adds before this page names it shows that identifier. The endpoint placeholder follows the selected protocol: OpenAI Chat Completions and Responses show `https://gateway.example/v1`, while Anthropic Messages shows `https://gateway.example` because its SDK appends `/v1/messages`. Changing the protocol preserves any address already entered. The endpoint must be a parseable HTTP or HTTPS URL; localhost, IPv4 and IPv6 literals, and custom ports remain valid. A syntax error blocks both discovery and creation at the field, while a request failure remains a separate provider error. **Fetch available models** asks the `llm/discoverModels` Remote about the endpoint the form shows, so adding a provider is one pass instead of save-then-return; the reply opens a searchable picker rather than being written, and nothing changes until **Update list**. The picker opens with the configured models checked, or every candidate when the list is empty and the route serves its whole catalog; **Update list** appends newly checked candidates and removes unchecked configured models from the draft, while configured models the reply does not list stay. Rows without an id do not count as configured, a repeated candidate id is listed once, and **Update list** is disabled when it would empty a non-empty list, since an empty list serves the whole catalog. Each candidate displays its original model id in a monospace font to distinguish models with the same name. Rows stay on one line; hovering reveals the model name, falling back to the full id when no name is supplied. Selection and configuration retain the original model id. Each selected candidate copies its id, display name, context window, output-token cap, and disclosed input types into the editable row when disclosed, while an existing row retains its user-tuned values. Search matches model ids and optional display names without clearing hidden selections. **Select all** and **Deselect all** change only the visible results, so a hidden configured model is not removed unseen. A row is deletable only when the user layer alone carries it (removal restores the composition base), and its confirmation dialog names the provider. ### First-run dialogs diff --git a/packages/client/ui-settings-models/README.zh.md b/packages/client/ui-settings-models/README.zh.md index 1731aca2b6..55856756bf 100644 --- a/packages/client/ui-settings-models/README.zh.md +++ b/packages/client/ui-settings-models/README.zh.md @@ -63,7 +63,7 @@ API 密钥输入框初始为空,并通过 `autocomplete="new-password"` 请求 ### 新增与删除提供商 -**添加模型提供商**是新增卡片的唯一入口:只要任一带编辑器的设置 namespace 已挂载就会出现,下述两种方式任一可继续时才可点击。卡片打开后顶部是分段式的方式切换,下方一行小字说明所选方式的用途:**第三方模型提供商**承载休眠目录提供商选择框与提供商编辑器——裸挂载的 `llm-pi-ai` 在任何路由存在之前就能提供其完整的已安装 catalog;**自定义模型 API**承载为 pi-ai 不提供的路由(中转站、自部署服务或其他兼容 OpenAI / Anthropic 协议的接口)准备的创建表单。某种方式只在其 namespace 已挂载时提供;只剩一种方式时卡片以该方式为标题直接显示表单;namespace 已无可采用的提供商或没有可声明的协议时,对应方式会被禁用并把原因作为悬停提示。面板在其方式首次显示时挂载,之后在卡片打开且该方式仍被提供期间保持挂载但隐藏,因此切换方式不会丢掉任何一边的草稿;任一面板有写入或端点探测进行中时滑块锁定,因为此时切换会让结果落到看不见的面板上。每个 tab 通过 id 控制其面板,关闭任一面板都会忘记目录草稿的目标。自定义表单会索要唯一的 **Provider ID**、端点、协议与至少一个可唯一识别的模型,因为没有东西能为它们兜底。协议选择框以产品名称显示各协议——OpenAI Chat Completions、OpenAI Responses、Anthropic Messages——存储的值仍是 schema 标识符;适配器新增而本页尚未命名的协议直接显示其标识符。端点占位示例随所选协议变化:OpenAI Chat Completions 与 Responses 显示 `https://gateway.example/v1`,Anthropic Messages 显示 `https://gateway.example`,因为其 SDK 会追加 `/v1/messages`。切换协议会保留已输入的地址。端点必须是可解析的 HTTP 或 HTTPS URL;localhost、IPv4 与 IPv6 字面地址以及自定义端口仍然有效。语法错误会在字段处阻止询问与创建,请求失败则继续作为独立的提供商错误显示。**获取可用模型**通过 `llm/discoverModels` Remote 查询表单显示的端点,因此新增提供商一次即可完成,而非先保存再返回;回复打开的是可搜索选择器而非直接写入,只有点击**更新列表**才会改变草稿。选择器打开时勾选已配置的模型;列表为空、路由提供完整 catalog 时勾选全部候选。**更新列表**会追加新勾选的候选,并从草稿中移除取消勾选的已配置模型;回复中未列出的已配置模型保持不变。候选项使用等宽字体显示原始模型 id,以区分同名模型。每行保持单行,悬停可查看模型名称,未提供名称时回退到完整 id。勾选与配置仍使用原始模型 id。每个选中候选会在提供商公布相应信息时,把 id、显示名、上下文窗口、最大输出 token 数和已公布的输入类型复制进可编辑行;已经存在的行保留用户调整过的值。搜索会匹配模型 id 与可选显示名称,且不会清除隐藏项的勾选状态。**全选**与**取消全选**只改变可见结果,以免在看不见时移除已配置的模型。只有用户层单独携带某行时,该行才可删除(删除会恢复组合基线),其确认对话框会指名该提供商。 +**添加模型提供商**是新增卡片的唯一入口:只要任一带编辑器的设置 namespace 已挂载就会出现,下述两种方式任一可继续时才可点击。卡片打开后顶部是分段式的方式切换,下方一行小字说明所选方式的用途:**第三方模型提供商**承载休眠目录提供商选择框与提供商编辑器——裸挂载的 `llm-pi-ai` 在任何路由存在之前就能提供其完整的已安装 catalog;**自定义模型 API**承载为 pi-ai 不提供的路由(中转站、自部署服务或其他兼容 OpenAI / Anthropic 协议的接口)准备的创建表单。某种方式只在其 namespace 已挂载时提供;只剩一种方式时卡片以该方式为标题直接显示表单;namespace 已无可采用的提供商或没有可声明的协议时,对应方式会被禁用并把原因作为悬停提示。面板在其方式首次显示时挂载,之后在卡片打开且该方式仍被提供期间保持挂载但隐藏,因此切换方式不会丢掉任何一边的草稿;任一面板有写入或端点探测进行中时滑块锁定,因为此时切换会让结果落到看不见的面板上。每个 tab 通过 id 控制其面板,关闭任一面板都会忘记目录草稿的目标。自定义表单会索要唯一的 **Provider ID**、端点、协议与至少一个可唯一识别的模型,因为没有东西能为它们兜底。协议选择框以产品名称显示各协议——OpenAI Chat Completions、OpenAI Responses、Anthropic Messages——存储的值仍是 schema 标识符;适配器新增而本页尚未命名的协议直接显示其标识符。端点占位示例随所选协议变化:OpenAI Chat Completions 与 Responses 显示 `https://gateway.example/v1`,Anthropic Messages 显示 `https://gateway.example`,因为其 SDK 会追加 `/v1/messages`。切换协议会保留已输入的地址。端点必须是可解析的 HTTP 或 HTTPS URL;localhost、IPv4 与 IPv6 字面地址以及自定义端口仍然有效。语法错误会在字段处阻止询问与创建,请求失败则继续作为独立的提供商错误显示。**获取可用模型**通过 `llm/discoverModels` Remote 查询表单显示的端点,因此新增提供商一次即可完成,而非先保存再返回;回复打开的是可搜索选择器而非直接写入,只有点击**更新列表**才会改变草稿。选择器打开时勾选已配置的模型;列表为空、路由提供完整 catalog 时勾选全部候选。**更新列表**会追加新勾选的候选,并从草稿中移除取消勾选的已配置模型;回复中未列出的已配置模型保持不变。没有 id 的行不算已配置,重复的候选 id 只列出一次;若会把非空列表清空,**更新列表**会被禁用,因为空列表表示提供完整 catalog。候选项使用等宽字体显示原始模型 id,以区分同名模型。每行保持单行,悬停可查看模型名称,未提供名称时回退到完整 id。勾选与配置仍使用原始模型 id。每个选中候选会在提供商公布相应信息时,把 id、显示名、上下文窗口、最大输出 token 数和已公布的输入类型复制进可编辑行;已经存在的行保留用户调整过的值。搜索会匹配模型 id 与可选显示名称,且不会清除隐藏项的勾选状态。**全选**与**取消全选**只改变可见结果,以免在看不见时移除已配置的模型。只有用户层单独携带某行时,该行才可删除(删除会恢复组合基线),其确认对话框会指名该提供商。 ### 首次运行弹窗 diff --git a/packages/client/ui-settings-models/src/client/ModelListEditor.tsx b/packages/client/ui-settings-models/src/client/ModelListEditor.tsx index b85f71c053..3c2999db88 100644 --- a/packages/client/ui-settings-models/src/client/ModelListEditor.tsx +++ b/packages/client/ui-settings-models/src/client/ModelListEditor.tsx @@ -10,7 +10,8 @@ * candidates the user picks from, never configuration written behind them. The * picker opens with the configured models checked, so confirming it adds the * newly checked candidates and removes the unchecked configured ones in the - * draft; configured rows the reply does not list are kept. + * draft; configured rows the reply does not list are kept, and a confirmation + * that would empty the list is refused. * * A provider that cannot be interrogated (an unreachable endpoint, a protocol * with no readable listing) is not a dead end: the failure is shown next to the @@ -177,7 +178,10 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode { const catalog = inheritedCatalog?.provider === catalogProvider ? inheritedCatalog?.models : undefined const inputDefaults = useMemo(() => new Map(catalog?.map(model => [model.id, model.inputModalities])), [catalog]) const [candidates, setCandidates] = useState(undefined) - const [picked, setPicked] = useState>(new Set()) + // Candidates whose checkbox the user flipped from its default. The default + // is read from the list at render, so a row drafted while the fetch was in + // flight opens checked rather than being removed by a stale selection. + const [flipped, setFlipped] = useState>(new Set()) const [candidateQuery, setCandidateQuery] = useState('') const [expanded, setExpanded] = useState>(new Set()) // Capacities are edited as text, so a field's keystrokes are held here rather @@ -205,6 +209,7 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode { * after them down, so per-row state follows its row. */ const forgetRows = (removed: ReadonlySet): void => { + if (removed.size === 0) return /** The surviving row's new position, or `undefined` for a removed row. */ const moved = (at: number): number | undefined => removed.has(at) ? undefined : at - [...removed].filter(gone => gone < at).length @@ -272,13 +277,10 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode { setFailure(t('fetchEmpty')) return } - // The picker opens on the current list so a few models can be added or - // removed in one place. An empty list serves the whole catalog, so every - // candidate starts checked. - const known = new Set(models.map(model => textOf(model, 'id'))) setCandidateQuery('') - setCandidates(found) - setPicked(new Set(found.filter(model => models.length === 0 || known.has(model.id)).map(model => model.id))) + setFlipped(new Set()) + // A listing may repeat an id; the picker and the list hold each model once. + setCandidates([...new Map(found.map(model => [model.id, model])).values()]) } finally { setBusy(false) } @@ -286,55 +288,66 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode { const closePicker = (): void => { setCandidates(undefined) - setPicked(new Set()) + setFlipped(new Set()) setCandidateQuery('') } + // The picker opens on the current list so a few models can be added or + // removed in one place. A list with no identified model serves the whole + // catalog or nothing yet, so every candidate starts checked. + const configuredIds = new Set(models.map(model => textOf(model, 'id')).filter(id => id.length > 0)) + const pickedByDefault = (id: string): boolean => configuredIds.size === 0 || configuredIds.has(id) + const isPicked = (id: string): boolean => pickedByDefault(id) !== flipped.has(id) + + const activeCandidates = candidates ?? [] + const listed = new Set(activeCandidates.map(candidate => candidate.id)) + // An unchecked candidate leaves the list. A row the listing does not name, + // including a half-typed row without an id, is outside the picker and stays. + const removedRows = new Set() + models.forEach((model, at) => { + const id = textOf(model, 'id') + if (listed.has(id) && !isPicked(id)) removedRows.add(at) + }) + // A kept row the user already tuned wins over the provider's own numbers. + const keptRows = models.filter((_model, at) => !removedRows.has(at)) + const keptIds = new Set(keptRows.map(model => textOf(model, 'id'))) + const addedRows = activeCandidates + .filter(candidate => isPicked(candidate.id) && !keptIds.has(candidate.id)) + .map(adopt) + // An empty list serves the whole catalog, so emptying a non-empty list + // would restore every model instead of removing the unchecked ones. + const emptiesList = models.length > 0 && keptRows.length === 0 && addedRows.length === 0 + const adoptPicked = (): void => { - /* v8 ignore next -- the dialog only renders with candidates loaded */ - if (candidates === undefined) return - const listed = new Set(candidates.map(candidate => candidate.id)) - // An unchecked candidate leaves the list. A row the listing does not name, - // including a half-typed row without an id, is outside the picker and stays. - const removed = new Set() - models.forEach((model, at) => { - const id = textOf(model, 'id') - if (listed.has(id) && !picked.has(id)) removed.add(at) - }) - const kept = models.filter((_model, at) => !removed.has(at)) - const keptIds = new Set(kept.map(model => textOf(model, 'id'))) - // A kept row the user already tuned wins over the provider's own numbers. - const added = candidates.filter(candidate => picked.has(candidate.id) && !keptIds.has(candidate.id)) - onChange([...kept, ...added.map(adopt)]) - forgetRows(removed) + onChange([...keptRows, ...addedRows]) + forgetRows(removedRows) closePicker() } const toggle = (id: string): void => { - setPicked((current) => { + setFlipped((current) => { const next = new Set(current) if (!next.delete(id)) next.add(id) return next }) } - const activeCandidates = candidates ?? [] const normalizedCandidateQuery = candidateQuery.trim().toLowerCase() const visibleCandidates = normalizedCandidateQuery.length === 0 ? activeCandidates : activeCandidates.filter(candidate => candidate.id.toLowerCase().includes(normalizedCandidateQuery) || candidate.name?.toLowerCase().includes(normalizedCandidateQuery) === true) const allVisibleCandidatesPicked = visibleCandidates.length > 0 - && visibleCandidates.every(candidate => picked.has(candidate.id)) + && visibleCandidates.every(candidate => isPicked(candidate.id)) const toggleVisibleCandidates = (): void => { // Only visible candidates change: a hidden checked one may be a configured // model, and clearing it would remove that model unseen. - setPicked((current) => { + const target = !allVisibleCandidatesPicked + setFlipped((current) => { const next = new Set(current) - const clear = visibleCandidates.every(candidate => current.has(candidate.id)) for (const candidate of visibleCandidates) { - if (clear) next.delete(candidate.id) + if (pickedByDefault(candidate.id) === target) next.delete(candidate.id) else next.add(candidate.id) } return next @@ -435,7 +448,14 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode { footer={( <> - + )} > @@ -466,7 +486,7 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode {
From 69207c4629c41e266695d1b87b313ad703b2243c Mon Sep 17 00:00:00 2001 From: winewill Date: Mon, 19 Oct 2026 17:40:55 +0800 Subject: [PATCH 295/369] fix(desktop): harden installer rendering lifecycle and tests --- apps/desktop/README.i18n.yaml | 4 +- apps/desktop/README.md | 2 +- apps/desktop/README.zh.md | 2 +- apps/desktop/installer/lifecycle.nsh | 2 +- apps/desktop/installer/theme.nsh | 9 +- apps/desktop/installer/window-frame.cpp | 41 ++++--- .../scripts/prepare-windows-installer.ps1 | 4 - .../scripts/test-windows-installer.mjs | 10 +- .../tests/windows-installer-render.cpp | 104 +++++++++++++----- 9 files changed, 121 insertions(+), 57 deletions(-) diff --git a/apps/desktop/README.i18n.yaml b/apps/desktop/README.i18n.yaml index 084720fb4f..a53151b185 100644 --- a/apps/desktop/README.i18n.yaml +++ b/apps/desktop/README.i18n.yaml @@ -54,8 +54,8 @@ en: ac31ebdd9de54f34 zh: 921e534d5c4b3a16 /deepseek-harness-desktop/windows-installer-interface: - en: 23c57225f3de2350 - zh: b65b5363791f4c03 + en: 9b90a2651a6d2b52 + zh: 86d4d9c4358aad80 /deepseek-harness-desktop/updates: en: b83a37681e23a3cc zh: e11ab8381fa4362d diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 6154b40e74..d15bb74c36 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -255,7 +255,7 @@ The theme follows Windows at startup; `/THEME=light`, `/THEME=dark`, and `/THEME Windows packaging compiles an x86 Win32/GDI+ helper with Visual C++ Build Tools and a Windows SDK; signed builds sign this helper through the configured Windows signer. The progress page keeps one opaque pixel buffer for its lifetime, draws static artwork once, and copies repainted pixels with GDI. The preparation hook leaves production dependency collection to electron-builder on every platform. -Run `pnpm --dir apps/desktop run test:installer` from the repository root on an interactive Windows x64 desktop to build and exercise a small native test payload through the production installer configuration. Each run uses a unique product identity and sequentially exercises English-only and Chinese-only installer variants, selecting test labels from the displayed welcome button. Both variants install into private directories and uninstall after testing; screenshots and results remain under `.desktop-build/installer-tests/`. The checks include upgrades to registered paths with trailing separators and rejection of drive roots. Native rendering checks cover light/dark palettes at 100% and 200% scale, the painted 100% completion state, caption-button hover/pressed colors and release/cancellation, minimize/restore geometry, and GDI resource release; repaint timings are diagnostics in `render-results.log`, not CI performance limits. Run the generated `ui/render-test.exe [light|dark] [interactive-title]` with a title to inspect the real progress page without installing a payload. The optional `--signed` flag uses the [Windows EV configuration](windows-signing/README.md#signing-configuration) to sign test executables and the helper before embedding them; it does not enable an update feed. +Run `pnpm --dir apps/desktop run test:installer` from the repository root on an interactive Windows x64 desktop to build and exercise a small native test payload through the production installer configuration. Each run uses a unique product identity and sequentially exercises English-only and Chinese-only installer variants, selecting test labels from the displayed welcome button. Both variants install into private directories and uninstall after testing; screenshots and results remain under `.desktop-build/installer-tests/`. The checks include upgrades to registered paths with trailing separators and rejection of drive roots. The renderer receives caption colors from the NSIS theme; pixel checks print into an off-screen bitmap and also run with the progress child hidden. Native rendering checks cover light/dark palettes at 100% and 200% scale, the painted 100% completion state, caption-button hover/pressed colors and release/cancellation, minimize/restore geometry, and GDI resource release; repaint timings are diagnostics in `render-results.log`, not CI performance limits. Run the generated `ui/render-test.exe [interactive-title]` with a title to inspect the real progress page without installing a payload. The optional `--signed` flag uses the [Windows EV configuration](windows-signing/README.md#signing-configuration) to sign test executables and the helper before embedding them; it does not enable an update feed. The Windows uninstaller removes the Electron user-data directory (browser storage and caches under `%APPDATA%`, nested under the package scope), the `%APPDATA%` product directory, and the updater download cache under `%LOCALAPPDATA%` together with the application, then removes ordinary empty scope directories below `%APPDATA%`. The Harness home (`~/.dsh` or `DSH_HOME`: sessions, settings, credentials, plugins) is never touched, and a `DSH_HOME` published as a Windows environment variable additionally protects any target overlapping it. Silent uninstalls remove the same data; an uninstaller started with `--updated` or `/KEEP_APP_DATA`, as electron-builder does for in-place updates and for replacing an older installation from another directory, retains it. Removal runs through the native helper, which refuses protected Windows folders and paths overlapping the installation or the home, requires a fixed local drive, leaves a linked root or ancestor in place, unlinks reparse points without entering their targets, clears read-only attributes, and keeps deleting siblings after a locked file; residue never stops uninstallation and is not reported. Installation records `InstallLocation` on the Windows uninstall entry as standard inventory metadata; on Windows 11, Uninstall in the Start menu context menu opens the installed-apps list for every Win32 application, and only MSIX packages uninstall directly from there. The uninstaller declares DPI awareness and uses Microsoft YaHei UI for Chinese. This behavior is Windows-only. diff --git a/apps/desktop/README.zh.md b/apps/desktop/README.zh.md index e35133ba31..bd2ebf5c30 100644 --- a/apps/desktop/README.zh.md +++ b/apps/desktop/README.zh.md @@ -255,7 +255,7 @@ Windows 安装程序使用原生 NSIS 页面,提供亮暗配色、系统阴影 Windows 打包使用 Visual C++ Build Tools 和 Windows SDK 编译 x86 Win32/GDI+ 辅助库;签名构建通过已配置的 Windows 签名器对该库签名。进度页在其生命周期内保留一个不透明像素缓冲,只绘制一次静态图像,并通过 GDI 复制重绘的像素。准备钩子在所有平台上均由 electron-builder 继续负责收集生产依赖。 -在有交互式桌面的 Windows x64 上,从仓库根目录运行 `pnpm --dir apps/desktop run test:installer`,可将小型原生测试载荷接入正式安装配置并执行验证。每次运行使用独立产品身份,依次验证仅英文和仅中文的安装器变体,并根据实际显示的欢迎页按钮选择测试文案。两个变体均安装到私有目录并在测试后卸载;截图和结果保留在 `.desktop-build/installer-tests/` 下。检查包含末尾带分隔符的已登记路径升级,以及磁盘根目录拒绝。原生绘制检查覆盖 100% 和 200% 缩放下的浅色/深色配色、绘制到 100% 的完成状态、标题栏按钮的悬停/按下颜色和松开触发与取消、最小化恢复后的窗口位置和尺寸,以及 GDI 资源释放;`render-results.log` 中的重绘耗时仅供诊断,不作为 CI 性能阈值。运行生成的 `ui/render-test.exe [light|dark] [interactive-title]` 并指定标题,可在不安装载荷的情况下查看真实进度页。可选的 `--signed` 标志使用[Windows EV 配置](windows-signing/README.zh.md#signing-configuration),在嵌入前对测试程序和辅助库签名;它不会启用更新源。 +在有交互式桌面的 Windows x64 上,从仓库根目录运行 `pnpm --dir apps/desktop run test:installer`,可将小型原生测试载荷接入正式安装配置并执行验证。每次运行使用独立产品身份,依次验证仅英文和仅中文的安装器变体,并根据实际显示的欢迎页按钮选择测试文案。两个变体均安装到私有目录并在测试后卸载;截图和结果保留在 `.desktop-build/installer-tests/` 下。检查包含末尾带分隔符的已登记路径升级,以及磁盘根目录拒绝。绘制器接收 NSIS 主题传入的标题栏颜色;像素检查绘制到离屏位图,并在隐藏进度子窗口时验证。原生绘制检查覆盖 100% 和 200% 缩放下的浅色/深色配色、绘制到 100% 的完成状态、标题栏按钮的悬停/按下颜色和松开触发与取消、最小化恢复后的窗口位置和尺寸,以及 GDI 资源释放;`render-results.log` 中的重绘耗时仅供诊断,不作为 CI 性能阈值。运行生成的 `ui/render-test.exe [interactive-title]` 并指定标题,可在不安装载荷的情况下查看真实进度页。可选的 `--signed` 标志使用[Windows EV 配置](windows-signing/README.zh.md#signing-configuration),在嵌入前对测试程序和辅助库签名;它不会启用更新源。 Windows 卸载程序会随应用一起删除 Electron 用户数据目录(`%APPDATA%` 下按包作用域嵌套的浏览器存储与缓存)、`%APPDATA%` 下的产品目录,以及 `%LOCALAPPDATA%` 下的更新下载缓存,随后移除 `%APPDATA%` 之下普通且已空的作用域目录。Harness 主目录(`~/.dsh` 或 `DSH_HOME`:会话、设置、凭据、插件)不会被触碰;以 Windows 环境变量发布的 `DSH_HOME` 还会保护所有与其重叠的目标。静默卸载删除相同的数据;以 `--updated` 或 `/KEEP_APP_DATA` 启动的卸载程序保留数据,electron-builder 在原地更新和从其他目录替换旧安装时正是这样启动它。删除通过原生辅助程序执行:它拒绝受保护的 Windows 目录以及与安装目录或主目录重叠的路径,要求固定的本地驱动器,链接的根目录或祖先目录原样保留,遇到重解析点只解除链接而不进入目标,清除只读属性,并在遇到被占用文件后继续删除其余兄弟项;残余不会中止卸载,也不会提示。安装时在 Windows 卸载注册项上记录 `InstallLocation` 作为标准的清单元数据;在 Windows 11 上,开始菜单右键菜单中的“卸载”对所有 Win32 应用都会打开已安装应用列表,只有 MSIX 包能从那里直接卸载。卸载程序声明 DPI 感知,中文使用微软雅黑 UI。此行为仅适用于 Windows。 diff --git a/apps/desktop/installer/lifecycle.nsh b/apps/desktop/installer/lifecycle.nsh index 525fde4080..521bfe20fe 100644 --- a/apps/desktop/installer/lifecycle.nsh +++ b/apps/desktop/installer/lifecycle.nsh @@ -62,7 +62,7 @@ Function InstallerProgressShow StrCpy $0 1 StrCpy $1 "$PLUGINSDIR\brand-dark-2x.bmp" ${EndIf} - System::Call '$PLUGINSDIR\window-frame.dll::InstallerShowProgress(p $HWNDPARENT, p $mui.InstFilesPage.ProgressBar, i r0, i $InstallerDpi, w r1, w "$(INSTALLER_NATIVE_PROGRESS)", w "$(INSTALLER_PROGRESS_EXTRACT)", w "$(INSTALLER_PROGRESS_COPY)", w "$(INSTALLER_PROGRESS_REGISTER)", w "$(INSTALLER_PROGRESS_CLEAN)") p.s ?c' + System::Call '$PLUGINSDIR\window-frame.dll::InstallerShowProgress(p $HWNDPARENT, p $mui.InstFilesPage.ProgressBar, i r0, i $InstallerDpi, i $InstallerControlHover, i $InstallerControlPressed, w r1, w "$(INSTALLER_NATIVE_PROGRESS)", w "$(INSTALLER_PROGRESS_EXTRACT)", w "$(INSTALLER_PROGRESS_COPY)", w "$(INSTALLER_PROGRESS_REGISTER)", w "$(INSTALLER_PROGRESS_CLEAN)") p.s ?c' Pop $InstallerProgressWindow ${If} $InstallerProgressWindow == 0 MessageBox MB_OK|MB_ICONSTOP "$(INSTALLER_UI_ERROR)" diff --git a/apps/desktop/installer/theme.nsh b/apps/desktop/installer/theme.nsh index ff3b1bbfef..a80c5761b0 100644 --- a/apps/desktop/installer/theme.nsh +++ b/apps/desktop/installer/theme.nsh @@ -22,6 +22,9 @@ !define INSTALLER_PRIMARY_HOVER 0xFF2D3135 !define INSTALLER_PRIMARY_PRESSED 0xFF000000 !define INSTALLER_CONTROL_HOVER 0xFFEEF0F2 +!define INSTALLER_CONTROL_PRESSED 0xFFD4DBE9 +!define INSTALLER_DARK_CONTROL_HOVER 0xFF303034 +!define INSTALLER_DARK_CONTROL_PRESSED 0xFF45454B !define INSTALLER_TRACK_COLOR 0xFFE9ECF2 !define INSTALLER_TEXT_COLORREF 0x15110F @@ -71,8 +74,8 @@ Function InstallerResolveTheme StrCpy $InstallerPrimary 0xFFF9FAFB StrCpy $InstallerPrimaryHover 0xFFE9ECF2 StrCpy $InstallerPrimaryPressed 0xFFD4DBE9 - StrCpy $InstallerControlHover 0xFF303034 - StrCpy $InstallerControlPressed 0xFF45454B + StrCpy $InstallerControlHover ${INSTALLER_DARK_CONTROL_HOVER} + StrCpy $InstallerControlPressed ${INSTALLER_DARK_CONTROL_PRESSED} StrCpy $InstallerTrack 0xFF61666B StrCpy $InstallerButtonText 0x15110F StrCpy $InstallerBorder 0xFF61666B @@ -86,7 +89,7 @@ Function InstallerResolveTheme StrCpy $InstallerPrimaryHover ${INSTALLER_PRIMARY_HOVER} StrCpy $InstallerPrimaryPressed ${INSTALLER_PRIMARY_PRESSED} StrCpy $InstallerControlHover ${INSTALLER_CONTROL_HOVER} - StrCpy $InstallerControlPressed 0xFFD4DBE9 + StrCpy $InstallerControlPressed ${INSTALLER_CONTROL_PRESSED} StrCpy $InstallerTrack ${INSTALLER_TRACK_COLOR} StrCpy $InstallerButtonText 0xFFFFFF StrCpy $InstallerBorder 0xFFBBC0C8 diff --git a/apps/desktop/installer/window-frame.cpp b/apps/desktop/installer/window-frame.cpp index 38dfc7261b..9c9a9e4f81 100644 --- a/apps/desktop/installer/window-frame.cpp +++ b/apps/desktop/installer/window-frame.cpp @@ -119,6 +119,8 @@ struct ProgressPage { HGDIOBJ previousBitmap; UINT pressedCommand; UINT hoveredCommand; + ARGB controlHover; + ARGB controlPressed; WCHAR captions[5][128]; ~ProgressPage() { @@ -173,10 +175,8 @@ static void PaintCaptionButtons(Graphics& graphics, const ProgressPage& page) { for (UINT command : {SC_MINIMIZE, SC_CLOSE}) { const int x = command == SC_MINIMIZE ? 504 : 548; if (page.hoveredCommand == command && (!page.pressedCommand || page.pressedCommand == command)) { - // Match the secondary caption controls in theme.nsh and pages.nsh. const bool pressed = page.pressedCommand == command; - SolidBrush fill(page.dark ? (pressed ? Color(255, 69, 69, 75) : Color(255, 48, 48, 52)) - : (pressed ? Color(255, 212, 219, 233) : Color(255, 238, 240, 242))); + SolidBrush fill(Color(pressed ? page.controlPressed : page.controlHover)); GraphicsPath path; path.AddArc(x, 8, 20, 20, 180, 90); path.AddArc(x + 20, 8, 20, 20, 270, 90); @@ -253,9 +253,11 @@ static LRESULT CALLBACK ProgressProc(HWND window, UINT message, WPARAM wparam, L if (message == WM_CANCELMODE && GetCapture() == window) ReleaseCapture(); return 0; } - if (message == WM_PAINT && page) { - PAINTSTRUCT paint; - HDC dc = BeginPaint(window, &paint); + if ((message == WM_PAINT || message == WM_PRINTCLIENT) && page) { + PAINTSTRUCT paint = {}; + const bool printing = message == WM_PRINTCLIENT; + HDC dc = printing ? reinterpret_cast(wparam) : BeginPaint(window, &paint); + if (printing) GetClientRect(window, &paint.rcPaint); { Graphics graphics(page->buffer); graphics.ScaleTransform(page->dpi / 96.0f, page->dpi / 96.0f); @@ -285,7 +287,7 @@ static LRESULT CALLBACK ProgressProc(HWND window, UINT message, WPARAM wparam, L BitBlt(dc, paint.rcPaint.left, paint.rcPaint.top, paint.rcPaint.right - paint.rcPaint.left, paint.rcPaint.bottom - paint.rcPaint.top, page->bufferDc, paint.rcPaint.left, paint.rcPaint.top, SRCCOPY); - EndPaint(window, &paint); + if (!printing) EndPaint(window, &paint); if (page->progress.value == 100) SetPropW(GetParent(window), L"HarnessInstaller.CompletedPercent", reinterpret_cast(100)); return 0; } @@ -297,8 +299,9 @@ static LRESULT CALLBACK ProgressProc(HWND window, UINT message, WPARAM wparam, L } // Runs on the NSIS UI thread; the stock installation section runs on its worker. +// Caption colors are opaque ARGB from NSIS; failure preserves the stock page's visibility. extern "C" __declspec(dllexport) HWND __cdecl InstallerShowProgress(HWND parent, HWND source, - BOOL dark, UINT dpi, const WCHAR* brand, const WCHAR* preparing, const WCHAR* extracting, + BOOL dark, UINT dpi, ARGB controlHover, ARGB controlPressed, const WCHAR* brand, const WCHAR* preparing, const WCHAR* extracting, const WCHAR* copying, const WCHAR* registering, const WCHAR* cleaning) { HINSTANCE module = GetModuleHandleW(nullptr); WNDCLASSW type = {}; @@ -311,17 +314,14 @@ extern "C" __declspec(dllexport) HWND __cdecl InstallerShowProgress(HWND parent, if (!page) return nullptr; GdiplusStartupInput startup; if (GdiplusStartup(&page->gdiplus, &startup, nullptr) != Ok) { delete page; return nullptr; } - HWND stockPage = GetParent(source); - if (!SetWindowSubclass(stockPage, HiddenPageProc, 1, 0)) { - delete page; return nullptr; - } - ShowWindow(stockPage, SW_HIDE); page->dark = dark != FALSE; page->dpi = dpi; + page->controlHover = controlHover; + page->controlPressed = controlPressed; const WCHAR* captions[] = {preparing, extracting, copying, registering, cleaning}; for (int i = 0; i < 5; ++i) lstrcpynW(page->captions[i], captions[i], 128); page->brand = new Image(brand); - if (page->brand->GetLastStatus() != Ok) { + if (!page->brand || page->brand->GetLastStatus() != Ok) { delete page; return nullptr; } const int size = MulDiv(600, dpi, 96); @@ -340,7 +340,7 @@ extern "C" __declspec(dllexport) HWND __cdecl InstallerShowProgress(HWND parent, if (!page->previousBitmap) { delete page; return nullptr; } // Opaque pixels are shared by GDI+ drawing and the window's GDI blit. page->buffer = new Bitmap(size, size, size * 4, PixelFormat32bppPARGB, static_cast(pixels)); - if (page->buffer->GetLastStatus() != Ok) { delete page; return nullptr; } + if (!page->buffer || page->buffer->GetLastStatus() != Ok) { delete page; return nullptr; } { // Static artwork stays in the buffer for this page's lifetime. Graphics graphics(page->buffer); @@ -354,9 +354,16 @@ extern "C" __declspec(dllexport) HWND __cdecl InstallerShowProgress(HWND parent, } delete page->brand; page->brand = nullptr; - HWND window = CreateWindowExW(0, type.lpszClassName, L"", WS_CHILD | WS_VISIBLE, + HWND window = CreateWindowExW(0, type.lpszClassName, L"", WS_CHILD, 0, 0, MulDiv(600, dpi, 96), MulDiv(600, dpi, 96), parent, nullptr, module, page); - if (!window) delete page; + if (!window) { delete page; return nullptr; } + // Keep the stock page available until the replacement is fully constructed. + HWND stockPage = GetParent(source); + if (!SetWindowSubclass(stockPage, HiddenPageProc, 1, 0)) { + DestroyWindow(window); return nullptr; + } + ShowWindow(stockPage, SW_HIDE); + ShowWindow(window, SW_SHOW); return window; } diff --git a/apps/desktop/scripts/prepare-windows-installer.ps1 b/apps/desktop/scripts/prepare-windows-installer.ps1 index b848f9bef4..5195891033 100644 --- a/apps/desktop/scripts/prepare-windows-installer.ps1 +++ b/apps/desktop/scripts/prepare-windows-installer.ps1 @@ -83,8 +83,4 @@ foreach ($asset in @('brand', 'brand-2x', 'brand-dark', 'brand-dark-2x', 'uninst } finally { $bitmap.Dispose() } } finally { $image.Dispose() } } -if ($TestProgress) { - & $renderExecutable (Join-Path $output 'brand-2x.bmp') 192 - if ($LASTEXITCODE -ne 0) { throw 'Installer rendering regression failed.' } -} Write-Output "Prepared native installer resources: $output" diff --git a/apps/desktop/scripts/test-windows-installer.mjs b/apps/desktop/scripts/test-windows-installer.mjs index c28a1688d9..27c6381a95 100644 --- a/apps/desktop/scripts/test-windows-installer.mjs +++ b/apps/desktop/scripts/test-windows-installer.mjs @@ -72,10 +72,18 @@ try { await execute(progressTest, [], childOptions) await execute(presentationTest, [], childOptions) const renderResults = [] + const themeSource = await readFile(join(appRoot, 'installer', 'theme.nsh'), 'utf8') + const captionColor = (name) => { + const match = themeSource.match(new RegExp(`^!define ${name} (0x[0-9A-Fa-f]{8})\\r?$`, 'm')) + if (!match) throw new Error(`Missing installer caption color: ${name}`) + return match[1] + } for (const dpi of [96, 192]) { for (const theme of ['light', 'dark']) { const brand = theme === 'dark' ? 'brand-dark-2x.bmp' : 'brand-2x.bmp' - const result = await execute(renderTest, [join(output, 'ui', brand), String(dpi), theme], childOptions) + const prefix = theme === 'dark' ? 'INSTALLER_DARK_CONTROL' : 'INSTALLER_CONTROL' + const result = await execute(renderTest, [join(output, 'ui', brand), String(dpi), theme, + captionColor(`${prefix}_HOVER`), captionColor(`${prefix}_PRESSED`)], childOptions) renderResults.push(result.stdout) } } diff --git a/apps/desktop/tests/windows-installer-render.cpp b/apps/desktop/tests/windows-installer-render.cpp index 19c8a1662c..8d9d8ef91e 100644 --- a/apps/desktop/tests/windows-installer-render.cpp +++ b/apps/desktop/tests/windows-installer-render.cpp @@ -9,7 +9,7 @@ extern "C" __declspec(dllimport) HRESULT __cdecl InstallerApplyFrame(HWND window); extern "C" __declspec(dllimport) HWND __cdecl InstallerShowProgress(HWND parent, HWND source, - BOOL dark, UINT dpi, const WCHAR* brand, const WCHAR* preparing, const WCHAR* extracting, + BOOL dark, UINT dpi, DWORD controlHover, DWORD controlPressed, const WCHAR* brand, const WCHAR* preparing, const WCHAR* extracting, const WCHAR* copying, const WCHAR* registering, const WCHAR* cleaning); extern "C" __declspec(dllimport) BOOL __cdecl InstallerFinishProgress(HWND window); @@ -19,14 +19,20 @@ static LRESULT CALLBACK WindowProc(HWND window, UINT message, WPARAM wparam, LPA } int wmain(int argc, wchar_t** argv) { - if (argc < 3) { - std::fprintf(stderr, "Usage: render-test.exe [light|dark] [interactive-title]\n"); + if (argc < 6) { + std::fprintf(stderr, "Usage: render-test.exe [interactive-title]\n"); return 1; } const int dpi = _wtoi(argv[2]); if (dpi < 96 || dpi > 384) return 2; - const bool dark = argc > 3 && wcscmp(argv[3], L"dark") == 0; - const bool interactive = argc > 4; + const bool dark = wcscmp(argv[3], L"dark") == 0; + if (!dark && wcscmp(argv[3], L"light") != 0) return 2; + wchar_t* end = nullptr; + const DWORD hoverArgb = wcstoul(argv[4], &end, 0); + if (!*argv[4] || *end || (hoverArgb >> 24) != 255) return 2; + const DWORD pressedArgb = wcstoul(argv[5], &end, 0); + if (!*argv[5] || *end || (pressedArgb >> 24) != 255) return 2; + const bool interactive = argc > 6; SetProcessDPIAware(); WNDCLASSW type = {}; type.lpfnWndProc = WindowProc; @@ -34,19 +40,30 @@ int wmain(int argc, wchar_t** argv) { type.lpszClassName = L"HarnessInstallerRenderTest"; type.hCursor = LoadCursorW(nullptr, IDC_ARROW); if (!RegisterClassW(&type)) return 3; - HWND window = CreateWindowW(type.lpszClassName, interactive ? argv[4] : L"Installer rendering test", + HWND window = CreateWindowW(type.lpszClassName, interactive ? argv[6] : L"Installer rendering test", WS_POPUP | WS_SYSMENU | WS_MINIMIZEBOX, 100, 100, MulDiv(600, dpi, 96), MulDiv(600, dpi, 96), nullptr, nullptr, type.hInstance, nullptr); if (!window || FAILED(InstallerApplyFrame(window))) return 4; - HWND stock = CreateWindowW(L"STATIC", L"", WS_CHILD, 0, 0, 1, 1, window, nullptr, type.hInstance, nullptr); + HWND stock = CreateWindowW(L"STATIC", L"", WS_CHILD | WS_VISIBLE, 0, 0, 1, 1, window, nullptr, type.hInstance, nullptr); HWND source = CreateWindowW(L"STATIC", L"", WS_CHILD, 0, 0, 1, 1, stock, nullptr, type.hInstance, nullptr); SetPropW(window, L"HarnessInstaller.Stage", reinterpret_cast(1)); SetPropW(window, L"HarnessInstaller.ExtractProgress", reinterpret_cast(50)); const auto createProgress = [&]() { - return InstallerShowProgress(window, source, dark, static_cast(dpi), argv[1], + return InstallerShowProgress(window, source, dark, static_cast(dpi), hoverArgb, pressedArgb, argv[1], L"Preparing %d%%", L"Extracting %d%%", L"Copying %d%%", L"Registering %d%%", L"Cleaning %d%%"); }; ShowWindow(window, SW_SHOWNOACTIVATE); + bool clientFrame = true; + for (int x : {8, 300, 580}) { + POINT point{MulDiv(x, dpi, 96), MulDiv(24, dpi, 96)}; + ClientToScreen(window, &point); + clientFrame = SendMessageW(window, WM_NCHITTEST, 0, MAKELPARAM(point.x, point.y)) == HTCLIENT && clientFrame; + } + HWND failed = InstallerShowProgress(window, source, dark, static_cast(dpi), hoverArgb, pressedArgb, L"", + L"Preparing %d%%", L"Extracting %d%%", L"Copying %d%%", L"Registering %d%%", L"Cleaning %d%%"); + ShowWindow(stock, SW_SHOW); + const bool failurePreservesStock = !failed && IsWindowVisible(stock); + if (failed) DestroyWindow(failed); // Warm retained font and window-animation resources before checking page-owned GDI objects. HWND warmup = createProgress(); if (!warmup) { DestroyWindow(window); return 5; } @@ -70,33 +87,64 @@ int wmain(int argc, wchar_t** argv) { } RECT originalBounds, restoredBounds; GetWindowRect(window, &originalBounds); - const auto captionPixel = [&](int x) { - UpdateWindow(progress); - HDC dc = GetDC(progress); + const auto captionPixel = [&](int x) -> COLORREF { + // Print into a private bitmap: visible-window clipping must not affect pixel assertions. + HDC dc = CreateCompatibleDC(nullptr); + BITMAPINFO info = {}; + info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + info.bmiHeader.biWidth = MulDiv(600, dpi, 96); + info.bmiHeader.biHeight = -MulDiv(600, dpi, 96); + info.bmiHeader.biPlanes = 1; + info.bmiHeader.biBitCount = 32; + void* pixels = nullptr; + HBITMAP bitmap = CreateDIBSection(nullptr, &info, DIB_RGB_COLORS, &pixels, nullptr, 0); + if (!dc || !bitmap) { + if (dc) DeleteDC(dc); + if (bitmap) DeleteObject(bitmap); + return CLR_INVALID; + } + HGDIOBJ previous = SelectObject(dc, bitmap); + if (!previous) { DeleteObject(bitmap); DeleteDC(dc); return CLR_INVALID; } + SendMessageW(progress, WM_PRINTCLIENT, reinterpret_cast(dc), PRF_CLIENT); const COLORREF pixel = GetPixel(dc, MulDiv(x + 8, dpi, 96), MulDiv(24, dpi, 96)); - ReleaseDC(progress, dc); + SelectObject(dc, previous); + DeleteObject(bitmap); + DeleteDC(dc); return pixel; }; const COLORREF normalColor = dark ? RGB(21, 21, 23) : RGB(255, 255, 255); - const COLORREF hoverColor = dark ? RGB(48, 48, 52) : RGB(238, 240, 242); - const COLORREF pressedColor = dark ? RGB(69, 69, 75) : RGB(212, 219, 233); + const auto colorref = [](DWORD argb) { return RGB((argb >> 16) & 255, (argb >> 8) & 255, argb & 255); }; + const COLORREF hoverColor = colorref(hoverArgb); + const COLORREF pressedColor = colorref(pressedArgb); + const auto expectPixel = [&](int x, COLORREF expected) { + const COLORREF actual = captionPixel(x); + if (actual != CLR_INVALID && actual == expected) return true; + std::fprintf(stderr, "caption pixel x=%d dpi=%d expected=0x%08lx actual=0x%08lx\n", x, dpi, expected, actual); + return false; + }; bool captionStates = true; + ShowWindow(progress, SW_HIDE); for (int x : {504, 548}) { const LPARAM point = MAKELPARAM(MulDiv(x + 20, dpi, 96), MulDiv(24, dpi, 96)); - captionStates = captionStates && captionPixel(x) == normalColor; + captionStates = expectPixel(x, normalColor) && captionStates; SendMessageW(progress, WM_MOUSEMOVE, 0, point); - captionStates = captionStates && captionPixel(x) == hoverColor; + captionStates = expectPixel(x, hoverColor) && captionStates; SendMessageW(progress, WM_LBUTTONDOWN, MK_LBUTTON, point); - captionStates = captionStates && captionPixel(x) == pressedColor; + captionStates = expectPixel(x, pressedColor) && captionStates; + const int other = x == 504 ? 548 : 504; + SendMessageW(progress, WM_MOUSEMOVE, MK_LBUTTON, MAKELPARAM(MulDiv(other + 20, dpi, 96), MulDiv(24, dpi, 96))); + captionStates = expectPixel(other, normalColor) && captionStates; + captionStates = expectPixel(x, normalColor) && captionStates; SendMessageW(progress, WM_MOUSEMOVE, MK_LBUTTON, MAKELPARAM(-1, -1)); - captionStates = captionStates && captionPixel(x) == normalColor; + captionStates = expectPixel(x, normalColor) && captionStates; SendMessageW(progress, WM_MOUSEMOVE, MK_LBUTTON, point); - captionStates = captionStates && captionPixel(x) == pressedColor; + captionStates = expectPixel(x, pressedColor) && captionStates; SendMessageW(progress, WM_CANCELMODE, 0, 0); - captionStates = captionStates && captionPixel(x) == hoverColor; + captionStates = expectPixel(x, hoverColor) && captionStates; SendMessageW(progress, WM_MOUSELEAVE, 0, 0); - captionStates = captionStates && captionPixel(x) == normalColor; + captionStates = expectPixel(x, normalColor) && captionStates; } + ShowWindow(progress, SW_SHOW); const LPARAM minimizePoint = MAKELPARAM(MulDiv(524, dpi, 96), MulDiv(24, dpi, 96)); SendMessageW(progress, WM_LBUTTONDOWN, MK_LBUTTON, minimizePoint); bool buttonRelease = !IsIconic(window) && GetCapture() == progress; @@ -129,7 +177,8 @@ int wmain(int argc, wchar_t** argv) { QueryPerformanceFrequency(&frequency); std::vector samples; // Full repaint is the exposure/restore path; fixture startup is excluded. - for (int frame = 0; frame < 300; ++frame) { + constexpr int frameCount = 300; + for (int frame = 0; frame < frameCount; ++frame) { InvalidateRect(progress, nullptr, FALSE); QueryPerformanceCounter(&started); UpdateWindow(progress); @@ -144,8 +193,9 @@ int wmain(int argc, wchar_t** argv) { } std::printf("]\n"); std::sort(samples.begin(), samples.end()); - std::printf("dpi=%d dark=%d frames=300 mean_ms=%.3f p50_ms=%.3f p95_ms=%.3f max_ms=%.3f\n", - dpi, dark, total / samples.size(), samples[150], samples[285], samples.back()); + std::printf("fixture=standalone-progress dpi=%d dark=%d frames=%d screen=%dx%d foreground=%d mean_ms=%.3f p50_ms=%.3f p95_ms=%.3f max_ms=%.3f\n", + dpi, dark, frameCount, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), GetForegroundWindow() == window, + total / samples.size(), samples[samples.size() / 2], samples[samples.size() * 95 / 100], samples.back()); const bool completed = InstallerFinishProgress(progress) && GetPropW(window, L"HarnessInstaller.CompletedPercent") == reinterpret_cast(100); WCHAR caption[160] = {}; @@ -154,7 +204,7 @@ int wmain(int argc, wchar_t** argv) { DestroyWindow(progress); const DWORD finalGdi = GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS); DestroyWindow(window); - std::printf("completed=%d final_caption=%d caption_states=%d button_release=%d close_release=%d minimized=%d restored=%d gdi_before=%lu gdi_after=%lu\n", - completed, finalCaption, captionStates, buttonRelease, closeRelease, minimized, restored, initialGdi, finalGdi); - return completed && finalCaption && captionStates && buttonRelease && closeRelease && minimized && restored && finalGdi <= initialGdi ? 0 : 6; + std::printf("client_frame=%d stock_preserved=%d completed=%d final_caption=%d caption_states=%d button_release=%d close_release=%d minimized=%d restored=%d gdi_before=%lu gdi_after=%lu\n", + clientFrame, failurePreservesStock, completed, finalCaption, captionStates, buttonRelease, closeRelease, minimized, restored, initialGdi, finalGdi); + return clientFrame && failurePreservesStock && completed && finalCaption && captionStates && buttonRelease && closeRelease && minimized && restored && finalGdi <= initialGdi ? 0 : 6; } From 54eed06c6f96fe3dcc91607056586e786a6f4f49 Mon Sep 17 00:00:00 2001 From: zhc Date: Mon, 19 Oct 2026 14:15:21 +0800 Subject: [PATCH 296/369] feat(gui): support image panning and original-file drag sharing --- apps/desktop/README.i18n.yaml | 4 +- apps/desktop/README.md | 2 + apps/desktop/README.zh.md | 2 + apps/desktop/src/image-drag-files.ts | 96 ++++++++++++++ apps/desktop/src/image-drag.ts | 74 +++++++++++ apps/desktop/src/ipc.ts | 8 ++ apps/desktop/src/main.ts | 14 ++- apps/desktop/src/preload-app.ts | 7 ++ apps/desktop/tests/image-drag-files.spec.ts | 48 +++++++ apps/desktop/tests/image-drag.spec.ts | 88 +++++++++++++ apps/desktop/tests/main-startup.spec.ts | 4 +- apps/desktop/tests/preload-app.spec.ts | 16 +++ apps/web/tests/document-preview.e2e.ts | 23 +++- apps/web/tests/markdown-images.e2e.ts | 9 ++ .../client/ui-attachment/README.i18n.yaml | 4 +- packages/client/ui-attachment/README.md | 2 + packages/client/ui-attachment/README.zh.md | 2 + .../client/ui-attachment/src/MessageImage.tsx | 8 +- .../src/client/ComposerAttachments.tsx | 7 +- .../ui-attachment/src/client/drop-events.ts | 8 +- .../client/ui-attachment/src/client/labels.ts | 1 + .../tests/message-image.client.spec.tsx | 1 + .../ui-chat/src/client/chat/ChatView.tsx | 2 +- packages/client/ui-chat/src/client/locale.ts | 2 + .../src/client/contract/slots.ts | 2 + .../ui-conversation/src/client/locales.ts | 2 + packages/client/ui-layout/package.json | 3 +- .../client/ui-layout/src/client/AppFrame.tsx | 5 +- .../client/ui-primitives/README.i18n.yaml | 4 +- packages/client/ui-primitives/README.md | 2 + packages/client/ui-primitives/README.zh.md | 2 + .../ui-primitives/src/ImageDragProvider.tsx | 60 +++++++++ .../client/ui-primitives/src/image-drag.ts | 110 ++++++++++++++++ packages/client/ui-primitives/src/index.ts | 3 + .../src/markdown/MarkdownDelegate.tsx | 2 +- .../ui-primitives/src/markdown/render.tsx | 6 +- .../tests/image-drag.client.spec.tsx | 119 ++++++++++++++++++ .../markdown-image-entries.client.spec.tsx | 2 +- .../README.i18n.yaml | 4 +- .../ui-sidebar-documentpreview/README.md | 2 +- .../ui-sidebar-documentpreview/README.zh.md | 2 +- .../src/client/image/ImageBody.module.css | 3 + .../src/client/image/ImageBody.tsx | 25 +++- .../src/client/image/locales.ts | 2 + .../src/client/image/pan.ts | 78 ++++++++++++ .../tests/image-body.client.spec.tsx | 4 +- .../tests/image-pan.client.spec.ts | 84 +++++++++++++ .../src/client/slot-catalog.ts | 54 ++++---- pnpm-lock.yaml | 3 + .../web/document-preview/document.expected.md | 2 + 50 files changed, 954 insertions(+), 63 deletions(-) create mode 100644 apps/desktop/src/image-drag-files.ts create mode 100644 apps/desktop/src/image-drag.ts create mode 100644 apps/desktop/tests/image-drag-files.spec.ts create mode 100644 apps/desktop/tests/image-drag.spec.ts create mode 100644 packages/client/ui-primitives/src/ImageDragProvider.tsx create mode 100644 packages/client/ui-primitives/src/image-drag.ts create mode 100644 packages/client/ui-primitives/tests/image-drag.client.spec.tsx create mode 100644 packages/client/ui-sidebar-documentpreview/src/client/image/pan.ts create mode 100644 packages/client/ui-sidebar-documentpreview/tests/image-pan.client.spec.ts diff --git a/apps/desktop/README.i18n.yaml b/apps/desktop/README.i18n.yaml index a46786a046..89ac4aea9f 100644 --- a/apps/desktop/README.i18n.yaml +++ b/apps/desktop/README.i18n.yaml @@ -33,8 +33,8 @@ en: 372297005b57a08b zh: 074a635ac17e4362 /deepseek-harness-desktop/installation-ownership: - en: 22153a298a6662e1 - zh: 3600803c8c9d0274 + en: fffd3892a309aeae + zh: 4127b7f511c70e75 /deepseek-harness-desktop/installation-ownership/runtime-and-plugin-activation: en: a1a47b09f4a72ba4 zh: 7b1cf27f0b72cf31 diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 14bfd38f14..34f0946ec2 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -142,6 +142,8 @@ Electron owns `$DSH_HOME/profiles/desktop`. Its `dependencies` contains packages The application preload exposes boot readiness, fatal startup reporting, native directory selection, the `__DSH_HOST_PATHS__` bridge for composer path references, and the lease-scoped Browser bridge only to `dsh-app://app` documents. The same preload forwards the main-process machine description from `dshDesktop.deviceInfo()` as `name=value` fields separated by `; `: `platform`, `os`, `app_arch` (the architecture the application binary runs as, which differs from the hardware architecture under emulation), `cpu`, and `memory_gib` (total physical memory in GiB with one decimal). A field is omitted when its source is unavailable. The description carries no hostname, user name, or serial number. Product documents use the shared authenticated HTTP APIs and receive the Desktop marker, update presentation, and an action that opens native confirmation; they cannot choose artifacts or authorize installation. Plugin management uses the Web application's authenticated HTTP APIs. Electron serves update-dialog documents and assets locally at `dsh-app://shell/`, independently of Host readiness. Electron exposes no plugin-management IPC or separate management document. No renderer receives filesystem access, raw Electron IPC, a shell, or arbitrary pnpm arguments. +The image-drag bridge accepts original PNG, JPEG, GIF, WebP, BMP, ICO, or SVG bytes up to 64 MiB from the owned application main frame. It prepares a private temporary file and icon before `startDrag`, exposes only an opaque handle, and never accepts renderer-supplied filesystem paths. Unused preparations are released with their source image; files handed to the OS remain readable until application exit because receivers may read them after the drag returns. Shutdown removes the temporary files. Native drag errors are reported once by the application’s localized image feedback. + Only the main application window enables ``. Guest attachment must match a main-issued lease and partition; guests keep sandbox, context isolation and Web security without Node integration or guest preload. Browser IPC listeners are created only for the application document. [Sidebar Browser](../../packages/client/ui-sidebar-browser/README.md) describes storage grouping and guest limitations; Host authentication remains required independently of URL filtering. The `dsh-app://shell/` origin serves packaged update documents, scripts, and styles without contacting the Host. Static requests retain GET/HEAD, path-containment, and MIME handling; each update document keeps its isolated preload and owned-window IPC checks. diff --git a/apps/desktop/README.zh.md b/apps/desktop/README.zh.md index 64cc5f7b22..7f26bd720a 100644 --- a/apps/desktop/README.zh.md +++ b/apps/desktop/README.zh.md @@ -142,6 +142,8 @@ Electron 拥有 `$DSH_HOME/profiles/desktop`。其 `dependencies` 包含 pnpm 应用 preload 只向 `dsh-app://app` 文档暴露启动就绪、致命启动失败上报、原生目录选择、用于 composer 路径引用的 `__DSH_HOST_PATHS__` 桥接和租约范围内的 Browser 桥接。同一个 preload 通过 `dshDesktop.deviceInfo()` 转发主进程采集的机器描述,按 `name=value` 字段以 `; ` 分隔:`platform`、`os`、`app_arch`(应用二进制实际运行的架构,模拟运行时与硬件架构不同)、`cpu` 和 `memory_gib`(物理内存总量,GiB,保留一位小数)。取值不可用时省略对应字段。该描述不包含主机名、用户名或序列号。产品页面还获得 Desktop 标记、更新展示数据和打开原生确认的操作,不能选择安装产物或授权安装。插件管理使用 Web 应用经过认证的 HTTP API;Electron 在 `dsh-app://shell/` 本地提供更新弹窗文档和资源,不依赖 Host 就绪。Electron 不提供插件管理 IPC 或独立管理页面。任何渲染进程都不会获得文件系统访问、原始 Electron IPC、shell 或任意 pnpm 参数。 +图片拖放桥接只接受主应用所属主框架传入的 PNG、JPEG、GIF、WebP、BMP、ICO 或 SVG 原图字节,上限为 64 MiB。桥接在 `startDrag` 前准备私有临时文件和图标,只暴露不透明句柄,绝不接受渲染进程指定的文件系统路径。未使用的准备结果随来源图片释放;已交给操作系统的文件保留到应用退出,因为接收方可能在拖动返回后继续读取。退出时删除临时文件。原生拖放错误由应用的本地化图片反馈统一报告一次。 + 只有主应用窗口启用 ``。guest 挂载必须匹配主进程签发的租约和分区;guest 保持 sandbox、context isolation 和 Web security,不启用 Node integration 或 guest preload。Browser IPC 监听只为应用文档创建。[Sidebar Browser](../../packages/client/ui-sidebar-browser/README.zh.md) 说明存储分组和 guest 限制;Host 鉴权仍独立于 URL 过滤而必需。 `dsh-app://shell/` 无需联系 Host 即可提供打包的更新文档、脚本和样式。静态请求保留 GET/HEAD、路径范围和 MIME 处理;每个更新文档继续使用隔离 preload 和所属窗口的 IPC 校验。 diff --git a/apps/desktop/src/image-drag-files.ts b/apps/desktop/src/image-drag-files.ts new file mode 100644 index 0000000000..df01eeaad2 --- /dev/null +++ b/apps/desktop/src/image-drag-files.ts @@ -0,0 +1,96 @@ +/** Process-owned original image files staged for native copy drags. */ +import { randomUUID } from 'node:crypto' +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { join } from 'node:path' + +const IMAGE_EXTENSIONS: Readonly> = { + 'image/png': 'png', 'image/jpeg': 'jpg', 'image/gif': 'gif', 'image/webp': 'webp', + 'image/bmp': 'bmp', 'image/x-icon': 'ico', 'image/vnd.microsoft.icon': 'ico', 'image/svg+xml': 'svg', +} +// Limits the memory and temporary storage accepted from one renderer IPC request. +const MAX_IMAGE_BYTES = 64 * 1024 * 1024 + +interface ImageDragFile { + readonly directory: string + readonly path: string + used: boolean +} + +/** Original files belong to the application; handed-off files remain readable until app shutdown. */ +export class ImageDragFiles { + private readonly files = new Map() + private readonly pending = new Set>() + private disposed = false + + /** @param temporaryDirectory - OS temporary directory in which private export directories are allocated. */ + constructor(private readonly temporaryDirectory: string) {} + + /** + * Validate renderer bytes and stage one original file without accepting renderer filesystem paths. + * @param bytes - complete image bytes. + * @param name - suggested filename; directory characters and platform-reserved characters are removed. + * @param mediaType - supported image MIME type, which owns the output suffix. + * @returns application-local opaque handle; failures retain no usable handle or partial file. + */ + prepare(bytes: unknown, name: unknown, mediaType: unknown): Promise { + const extension = typeof mediaType === 'string' && Object.hasOwn(IMAGE_EXTENSIONS, mediaType) + ? IMAGE_EXTENSIONS[mediaType] : undefined + if (this.disposed || !(bytes instanceof Uint8Array) || bytes.byteLength === 0 || bytes.byteLength > MAX_IMAGE_BYTES + || typeof name !== 'string' || typeof mediaType !== 'string' || extension === undefined) { + return Promise.reject(new Error('Invalid image transfer')) + } + const stem = name.replace(/\.[^.]*$/u, '').replace(/[<>:"/\\|?*\x00-\x1f]/gu, '_').replace(/^[. ]+|[. ]+$/gu, '').slice(0, 120) || 'image' + const operation = this.write(bytes, `${stem}.${extension}`) + this.pending.add(operation) + void operation.then(() => { this.pending.delete(operation) }, () => { this.pending.delete(operation) }) + return operation + } + + private async write(bytes: Uint8Array, name: string): Promise { + const directory = await mkdtemp(join(this.temporaryDirectory, 'dsh-image-drag-')) + try { + const path = join(directory, name) + await writeFile(path, bytes, { flag: 'wx', mode: 0o600 }) + if (this.disposed) throw new Error('Image transfer closed') + const handle = randomUUID() + this.files.set(handle, { directory, path, used: false }) + return handle + } catch (error) { + await rm(directory, { recursive: true, force: true }) + throw error + } + } + + /** + * Resolve and retain a prepared file for an OS-owned drag. + * @param handle - handle created by this application's preparation operation. + * @param retain - whether the OS is taking ownership; false only during preparation. + * @returns original file path, or undefined after release/disposal or for a foreign handle. + */ + take(handle: unknown, retain = true): string | undefined { + const file = typeof handle === 'string' ? this.files.get(handle) : undefined + if (file === undefined || this.disposed) return undefined + if (retain) file.used = true + return file.path + } + + /** + * Release a preparation that no longer has a mounted image consumer. + * @param handle - application-local handle; OS-used files stay until disposal. + */ + async release(handle: unknown): Promise { + const file = typeof handle === 'string' ? this.files.get(handle) : undefined + if (file === undefined || file.used) return + this.files.delete(handle as string) + await rm(file.directory, { recursive: true, force: true }) + } + + /** Stop preparation, await pending writes, and remove all owned files after native drags finish. */ + async dispose(): Promise { + this.disposed = true + await Promise.allSettled(this.pending) + const files = [...this.files.values()] + this.files.clear() + await Promise.all(files.map(file => rm(file.directory, { recursive: true, force: true }))) + } +} diff --git a/apps/desktop/src/image-drag.ts b/apps/desktop/src/image-drag.ts new file mode 100644 index 0000000000..da549f1108 --- /dev/null +++ b/apps/desktop/src/image-drag.ts @@ -0,0 +1,74 @@ +/** Native copy drags authorized only for the owning application window. */ +import { app, ipcMain, nativeImage, type BrowserWindow, type IpcMainInvokeEvent, type NativeImage } from 'electron' +import { DESKTOP_IPC, assertDesktopSender } from './ipc.ts' +import { ImageDragFiles } from './image-drag-files.ts' + +/** + * Register native image preparation and drag operations for the application window. + * @param getWindow - current application window; subframes and other windows are rejected. + * @returns cleanup that waits for preparation and removes all exported temporary files. + */ +export function installDesktopImageDrag(getWindow: () => BrowserWindow | undefined): () => Promise { + let disposed = false + const files = new ImageDragFiles(app.getPath('temp')) + const pending = new Set>() + const icons = new Map() + const owner = (event: IpcMainInvokeEvent): BrowserWindow => { + const window = getWindow() + if (disposed || window === undefined || window.isDestroyed() || event.sender !== window.webContents + || event.senderFrame !== window.webContents.mainFrame) throw new Error('Unowned image drag request') + assertDesktopSender(event, ['app']) + return window + } + const prepare = async (event: IpcMainInvokeEvent, bytes: unknown, name: unknown, mediaType: unknown): Promise => { + owner(event) + const handle = await files.prepare(bytes, name, mediaType) + try { + const file = files.take(handle, false) + if (file === undefined) throw new Error('Image transfer closed') + let icon = nativeImage.createFromPath(file) + if (icon.isEmpty()) icon = await app.getFileIcon(file, { size: 'normal' }) + else icon = icon.resize({ width: 64 }) + owner(event) + icons.set(handle, icon) + return handle + } catch (error) { + await files.release(handle) + throw error + } + } + ipcMain.handle(DESKTOP_IPC.imageDragPrepare, (event, bytes: unknown, name: unknown, mediaType: unknown) => { + const operation = prepare(event, bytes, name, mediaType) + pending.add(operation) + void operation.then(() => { pending.delete(operation) }, () => { pending.delete(operation) }) + return operation + }) + ipcMain.handle(DESKTOP_IPC.imageDragStart, (event, handle: unknown) => { + const window = owner(event) + const icon = typeof handle === 'string' ? icons.get(handle) : undefined + if (icon === undefined) return false + const file = files.take(handle) + if (file === undefined) return false + try { + window.webContents.startDrag({ file, icon }) + return true + } catch (_error) { + // The renderer reports one localized failure; no filesystem paths cross IPC. + return false + } + }) + ipcMain.handle(DESKTOP_IPC.imageDragRelease, async (event, handle: unknown) => { + owner(event) + if (typeof handle === 'string') icons.delete(handle) + await files.release(handle) + }) + return async () => { + disposed = true + ipcMain.removeHandler(DESKTOP_IPC.imageDragPrepare) + ipcMain.removeHandler(DESKTOP_IPC.imageDragStart) + ipcMain.removeHandler(DESKTOP_IPC.imageDragRelease) + await Promise.allSettled(pending) + icons.clear() + await files.dispose() + } +} diff --git a/apps/desktop/src/ipc.ts b/apps/desktop/src/ipc.ts index fc19021fa6..30218cc211 100644 --- a/apps/desktop/src/ipc.ts +++ b/apps/desktop/src/ipc.ts @@ -21,6 +21,9 @@ export const DESKTOP_IPC = { browserRelease: 'dsh-desktop:browser-release', browserOpenRequested: 'dsh-desktop:browser-open-requested', directoryPick: 'dsh-desktop:directory-pick', + imageDragPrepare: 'dsh-desktop:image-drag-prepare', + imageDragStart: 'dsh-desktop:image-drag-start', + imageDragRelease: 'dsh-desktop:image-drag-release', deviceInfo: 'dsh-desktop:device-info', localeBootstrap: 'dsh-desktop:locale-bootstrap', localeChanged: 'dsh-desktop:locale-changed', @@ -72,6 +75,11 @@ export interface DesktopUpdatePresentation { export interface DshDesktopProductApi { readonly protocolVersion: 1 readonly browser: DesktopBrowserBridge + readonly imageDrag: { + prepare(bytes: Uint8Array, name: string, mediaType: string): Promise + start(handle: string): Promise + release(handle: string): void + } readonly keyboard: DesktopKeyboardApi readonly shortcuts: DesktopShortcutsApi /** diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 2afdfd7194..cd93618175 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -26,6 +26,7 @@ import { resolveDesktopPaths } from './paths.ts' import { DesktopProjectManager } from './project-manager.ts' import { DesktopHostFatalError, DesktopHostProcess, DesktopHostUncleanExitError } from './host-process.ts' import { DesktopPlatformView, PLATFORM_IPC, platformBounds } from './platform-view.ts' +import { installDesktopImageDrag } from './image-drag.ts' import { installDesktopDirectoryPicker } from './directory-picker.ts' import { installMicrophonePermissions } from './microphone-permissions.ts' import { DesktopBackendController } from './backend-controller.ts' @@ -682,6 +683,9 @@ async function main(): Promise { }) installDesktopDirectoryPicker(() => mainWindow) + const disposeImageDrag = installDesktopImageDrag(() => mainWindow) + let installerImageCleanup: Promise | undefined + let installerImagesReleased = false installMicrophonePermissions(session.defaultSession, () => mainWindow?.webContents) const shortcuts = installDesktopShortcuts(() => mainWindow, app.getPath('userData'), process.platform === 'darwin' ? 'macos' : process.platform === 'win32' ? 'windows' : 'linux', () => { refreshApplicationMenu() }, window => updateOverlays.input(window)) @@ -1260,10 +1264,18 @@ async function main(): Promise { mandatoryUI?.dispose() void Promise.all([Promise.resolve(mandatoryPolicy?.dispose()).then(() => policyAuth?.dispose()), backend.close(), // A Platform cleanup failure is logged without cutting the remaining Host shutdown short. - platformView.dispose().catch((error: unknown) => { console.error(error) })]) + platformView.dispose().catch((error: unknown) => { console.error(error) }), disposeImageDrag()]) .catch((error: unknown) => { console.error(error) }).finally(() => { app.quit() }) } app.on('before-quit', (event) => { + if (shellInstallerOwnsQuit && !installerImagesReleased) { + event.preventDefault() + installerImageCleanup ??= disposeImageDrag().catch((error: unknown) => { console.error(error) }).then(() => { + installerImagesReleased = true + app.quit() + }) + return + } if (shellInstallerOwnsQuit) { shuttingDown = true quitConfirmation.dispose() diff --git a/apps/desktop/src/preload-app.ts b/apps/desktop/src/preload-app.ts index f590b8bc8b..0e7f4c0fc9 100644 --- a/apps/desktop/src/preload-app.ts +++ b/apps/desktop/src/preload-app.ts @@ -14,6 +14,13 @@ function createProductApi(): DshDesktopProductApi { return { protocolVersion: 1, browser: createDesktopBrowserBridge(), + imageDrag: { + prepare: (bytes, name, mediaType) => ipcRenderer.invoke(DESKTOP_IPC.imageDragPrepare, bytes, name, mediaType) as Promise, + start: handle => ipcRenderer.invoke(DESKTOP_IPC.imageDragStart, handle) as Promise, + release: (handle) => { void ipcRenderer.invoke(DESKTOP_IPC.imageDragRelease, handle).catch((_error: unknown) => { + // Main-process shutdown also releases every prepared file. + }) }, + }, deviceInfo: () => ipcRenderer.invoke(DESKTOP_IPC.deviceInfo) as Promise, keyboard: { closeWindow: revision => ipcRenderer.invoke(DESKTOP_IPC.shortcutsCloseWindow, revision) as Promise, diff --git a/apps/desktop/tests/image-drag-files.spec.ts b/apps/desktop/tests/image-drag-files.spec.ts new file mode 100644 index 0000000000..f231cea286 --- /dev/null +++ b/apps/desktop/tests/image-drag-files.spec.ts @@ -0,0 +1,48 @@ +import { mkdtemp, readFile, readdir, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { basename, join } from 'node:path' +import { expect, it, onTestFinished } from 'vitest' +import { ImageDragFiles } from '../src/image-drag-files.ts' + +async function files() { + const root = await mkdtemp(join(tmpdir(), 'dsh-drag-test-')) + const store = new ImageDragFiles(root) + onTestFinished(async () => { await store.dispose(); await rm(root, { recursive: true, force: true }) }) + return { root, store } +} + +it('exports original bytes and keeps a used file readable until disposal', async () => { + const { root, store } = await files() + const bytes = new Uint8Array([137, 80, 78, 71]) + const handle = await store.prepare(bytes, '../../original.png', 'image/png') + const path = store.take(handle)! + expect(basename(path)).toBe('_.._original.png') + expect(new Uint8Array(await readFile(path))).toEqual(bytes) + await store.release(handle) + expect(new Uint8Array(await readFile(path))).toEqual(bytes) + await store.dispose() + expect(await readdir(root)).toEqual([]) + expect(store.take(handle)).toBeUndefined() +}) + +it('rejects non-images, foreign handles and writes after disposal', async () => { + const { store } = await files() + await expect(store.prepare(new Uint8Array([1]), 'x.html', 'text/html')).rejects.toThrow('Invalid image transfer') + await expect(store.prepare(new Uint8Array(), 'x.png', 'image/png')).rejects.toThrow('Invalid image transfer') + await expect(store.prepare(new Uint8Array([1]), 'x.png', 'constructor')).rejects.toThrow('Invalid image transfer') + expect(store.take('foreign')).toBeUndefined() + await store.dispose() + await expect(store.prepare(new Uint8Array([1]), 'x.png', 'image/png')).rejects.toThrow('Invalid image transfer') +}) + +it('releases unused preparations and waits for a concurrent write before cleanup', async () => { + const { root, store } = await files() + const handle = await store.prepare(new Uint8Array([1]), 'x.png', 'image/png') + await store.release(handle) + expect(await readdir(root)).toEqual([]) + const pending = store.prepare(new Uint8Array([2]), 'y.png', 'image/png') + const rejected = expect(pending).rejects.toThrow('Image transfer closed') + await store.dispose() + await rejected + expect(await readdir(root)).toEqual([]) +}) diff --git a/apps/desktop/tests/image-drag.spec.ts b/apps/desktop/tests/image-drag.spec.ts new file mode 100644 index 0000000000..d9460fb02c --- /dev/null +++ b/apps/desktop/tests/image-drag.spec.ts @@ -0,0 +1,88 @@ +import { mkdtemp, readFile, readdir, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, expect, it, onTestFinished, vi } from 'vitest' +import type { BrowserWindow, IpcMainInvokeEvent, NativeImage, WebContents, WebFrameMain } from 'electron' +import { DESKTOP_IPC } from '../src/ipc.ts' + +const electron = vi.hoisted(() => ({ + handle: vi.fn(), removeHandler: vi.fn(), getPath: vi.fn(), getFileIcon: vi.fn(), createFromPath: vi.fn(), +})) +vi.mock('electron', () => ({ + app: electron, + ipcMain: electron, + nativeImage: { createFromPath: electron.createFromPath }, +})) +const { installDesktopImageDrag } = await import('../src/image-drag.ts') +afterEach(() => { vi.resetAllMocks() }) + +type Handler = (event: IpcMainInvokeEvent, ...args: unknown[]) => unknown +async function fixture() { + const root = await mkdtemp(join(tmpdir(), 'dsh-native-image-drag-')) + onTestFinished(async () => { await rm(root, { recursive: true, force: true }) }) + electron.getPath.mockReturnValue(root) + const icon: NativeImage = Object.assign({} as NativeImage, { isEmpty: () => false, resize: () => icon }) + electron.createFromPath.mockReturnValue(icon) + const frame = Object.assign({} as WebFrameMain, { url: 'dsh-app://app/' }) + const window = Object.assign({} as BrowserWindow, { isDestroyed: () => false, + webContents: Object.assign({} as WebContents, { mainFrame: frame, startDrag: vi.fn() }) }) + let current: BrowserWindow | undefined = window + const dispose = installDesktopImageDrag(() => current) + onTestFinished(dispose) + const handlers = new Map(electron.handle.mock.calls.map(([channel, handler]) => [channel, handler])) + const event = Object.assign({} as IpcMainInvokeEvent, { sender: window.webContents, senderFrame: frame }) + const call = (channel: string, ...args: unknown[]) => (handlers.get(channel) as Handler)(event, ...args) + return { root, window, frame, event, icon, call, dispose, detach: () => { current = undefined } } +} + +it('stages original bytes before the gesture and synchronously starts an OS copy drag', async () => { + const f = await fixture() + const bytes = new Uint8Array([137, 80, 78, 71]) + const handle = await f.call(DESKTOP_IPC.imageDragPrepare, bytes, 'original.png', 'image/png') + expect(f.window.webContents.startDrag).not.toHaveBeenCalled() + expect(f.call(DESKTOP_IPC.imageDragStart, handle)).toBe(true) + const [request] = f.window.webContents.startDrag.mock.calls[0] as [{ file: string; icon: NativeImage }] + expect(request.icon).toBe(f.icon) + expect(new Uint8Array(await readFile(request.file))).toEqual(bytes) + await f.call(DESKTOP_IPC.imageDragRelease, handle) + expect(new Uint8Array(await readFile(request.file))).toEqual(bytes) + expect(f.call(DESKTOP_IPC.imageDragStart, handle)).toBe(false) + await f.dispose() + expect(await readdir(f.root)).toEqual([]) +}) + +it('rejects foreign senders, subframes, revoked handles and native failures', async () => { + const f = await fixture() + expect(f.call(DESKTOP_IPC.imageDragStart, 'foreign')).toBe(false) + const handle = await f.call(DESKTOP_IPC.imageDragPrepare, new Uint8Array([1]), 'x.png', 'image/png') + f.window.webContents.startDrag.mockImplementationOnce(() => { throw new Error('OS rejected drag') }) + expect(f.call(DESKTOP_IPC.imageDragStart, handle)).toBe(false) + f.frame.url = 'https://other.test/' + expect(() => f.call(DESKTOP_IPC.imageDragStart, handle)).toThrow('unowned renderer') + f.frame.url = 'dsh-app://app/' + Object.assign(f.event, { senderFrame: { url: 'dsh-app://app/' } }) + expect(() => f.call(DESKTOP_IPC.imageDragStart, handle)).toThrow('Unowned image drag request') + Object.assign(f.event, { senderFrame: f.frame, sender: {} }) + expect(() => f.call(DESKTOP_IPC.imageDragStart, handle)).toThrow('Unowned image drag request') + f.detach() + expect(() => f.call(DESKTOP_IPC.imageDragStart, handle)).toThrow('Unowned image drag request') +}) + +it('prepares the fallback icon before accepting a gesture and cancels late work on shutdown', async () => { + const f = await fixture() + electron.createFromPath.mockReturnValue({ isEmpty: () => true }) + const pending = Promise.withResolvers() + electron.getFileIcon.mockReturnValue(pending.promise) + const prepare = f.call(DESKTOP_IPC.imageDragPrepare, new Uint8Array([1]), 'x.svg', 'image/svg+xml') + const rejected = expect(prepare).rejects.toThrow('Unowned image drag request') + await vi.waitFor(() => { expect(electron.getFileIcon).toHaveBeenCalledOnce() }) + let stopped = false + const stopping = f.dispose().then(() => { stopped = true }) + await Promise.resolve() + expect(stopped).toBe(false) + pending.resolve(f.icon) + await rejected + await stopping + expect(f.window.webContents.startDrag).not.toHaveBeenCalled() + expect(await readdir(f.root)).toEqual([]) +}) diff --git a/apps/desktop/tests/main-startup.spec.ts b/apps/desktop/tests/main-startup.spec.ts index d9a8f3103b..5e60f0a3e6 100644 --- a/apps/desktop/tests/main-startup.spec.ts +++ b/apps/desktop/tests/main-startup.spec.ts @@ -1870,7 +1870,7 @@ describe('desktop main startup', () => { harness.app.quit() await harness.quitCompleted.promise expect(modal.isDestroyed()).toBe(true) - expect(harness.app.quit).toHaveBeenCalledTimes(2) + expect(harness.app.quit).toHaveBeenCalledTimes(3) // The installer owns the exit, so Platform cleanup starts without holding the quit open. expect(harness.platformDispose).toHaveBeenCalledOnce() inspected.resolve({ activeTasks: true, scheduledTasks: true }) @@ -1878,7 +1878,7 @@ describe('desktop main startup', () => { await vi.advanceTimersByTimeAsync(0) expect(harness.dialog.showMessageBox).toHaveBeenCalledTimes(pendingPhase === 'dialog' ? 1 : 0) expect(harness.platformDispose).toHaveBeenCalledOnce() - expect(harness.app.quit).toHaveBeenCalledTimes(2) + expect(harness.app.quit).toHaveBeenCalledTimes(3) disposal.resolve() }) diff --git a/apps/desktop/tests/preload-app.spec.ts b/apps/desktop/tests/preload-app.spec.ts index e238f583c1..b16b6e2341 100644 --- a/apps/desktop/tests/preload-app.spec.ts +++ b/apps/desktop/tests/preload-app.spec.ts @@ -254,3 +254,19 @@ it('forwards browser guest input only for the focused live webview lease', async expect(listener).toHaveBeenCalledOnce() off() }) + +it('prepares original image bytes and starts native drags without renderer filesystem paths', async () => { + vi.stubGlobal('location', new URL('dsh-app://app/')) + await import('../src/preload-app.ts') + const api = electron.contextBridge.exposeInMainWorld.mock.calls.find(([name]) => name === 'dshDesktop')?.[1] as DshDesktopProductApi + const bytes = new Uint8Array([137, 80, 78, 71]) + electron.ipcRenderer.invoke.mockResolvedValueOnce('image-handle').mockResolvedValueOnce(true).mockResolvedValueOnce(undefined) + await expect(api.imageDrag.prepare(bytes, 'original.png', 'image/png')).resolves.toBe('image-handle') + await expect(api.imageDrag.start('image-handle')).resolves.toBe(true) + api.imageDrag.release('image-handle') + expect(electron.ipcRenderer.invoke.mock.calls).toEqual([ + [DESKTOP_IPC.imageDragPrepare, bytes, 'original.png', 'image/png'], + [DESKTOP_IPC.imageDragStart, 'image-handle'], + [DESKTOP_IPC.imageDragRelease, 'image-handle'], + ]) +}) diff --git a/apps/web/tests/document-preview.e2e.ts b/apps/web/tests/document-preview.e2e.ts index 9953e112cc..c7326999f7 100644 --- a/apps/web/tests/document-preview.e2e.ts +++ b/apps/web/tests/document-preview.e2e.ts @@ -1082,7 +1082,7 @@ else process.exit(1); width: (node as HTMLImageElement).naturalWidth, height: (node as HTMLImageElement).naturalHeight, draggable: (node as HTMLImageElement).draggable, - }))).toEqual({ width: 1, height: 1, draggable: false }) + }))).toEqual({ width: 1, height: 1, draggable: true }) const centering = await tinyImage.evaluate((node) => { const image = node.getBoundingClientRect() const scroller = node.closest('[data-textpreview-body]')?.getBoundingClientRect() @@ -1105,6 +1105,14 @@ else process.exit(1); await page.getByRole('menuitem', { name: 'Fit width', exact: true }).click() await expect.poll(() => imageZoom.innerText()).toBe('100%') + await tinyImage.dragTo(input) + const draggedPreview = page.getByRole('group', { name: 'Pending attachments', exact: true }).getByRole('img') + await draggedPreview.waitFor({ state: 'visible', timeout: 5000 }) + const draggedBytes = await draggedPreview.evaluate(async (node: HTMLImageElement) => + Array.from(new Uint8Array(await (await fetch(node.src)).arrayBuffer()))) + expect(Buffer.from(draggedBytes)).toEqual(TINY_PNG) + expect(await input.innerText()).toBe('') + await page.getByRole('button', { name: 'Remove image tiny.png', exact: true }).click() await openFile('large.svg') await expect.poll(() => viewer.innerText()).toBe('Image') await viewer.click() @@ -1143,6 +1151,17 @@ else process.exit(1); await page.getByRole('menuitem', { name: '100%', exact: true }).click() await expect.poll(async () => (await largeImage.boundingBox())!.width).toBeCloseTo(1200, 0) expect(await imageScrollport.evaluate(node => node.scrollWidth > node.clientWidth)).toBe(true) + await imageScrollport.evaluate((node) => { node.scrollLeft = 0; node.scrollTop = 0 }) + const panBox = await imageScrollport.boundingBox() + if (panBox === null) throw new Error('image viewport missing') + await expect.poll(() => largeImage.evaluate(node => getComputedStyle(node).cursor)).toBe('grab') + await page.mouse.move(panBox.x + 180, panBox.y + 180) + await page.mouse.down() + await page.mouse.move(panBox.x + 100, panBox.y + 100, { steps: 8 }) + await expect.poll(() => imageScrollport.evaluate(node => [node.scrollLeft, node.scrollTop])).toEqual([80, 80]) + await expect.poll(() => largeImage.evaluate(node => getComputedStyle(node).cursor)).toBe('grabbing') + await page.mouse.up() + await expect.poll(() => largeImage.evaluate(node => getComputedStyle(node).cursor)).toBe('grab') svgZoom = await revealDocumentZoom(page, preview) await svgZoom.click() await page.getByRole('menuitem', { name: 'Fit width', exact: true }).click() @@ -1151,9 +1170,11 @@ else process.exit(1); expect(await page.locator('html').getAttribute('data-image-preview-escape')).toBeNull() sections.push([ '## Image zoom', '', + '- Contained sidebar PNG drags into the composer as an original-byte image attachment', '- Small PNG fit width remains at intrinsic size; 200% doubles it', '- SVG fit width -> 100% -> fit width toggles horizontal overflow: false -> true -> false', '- Image and Blob identities remain stable while zoom changes', + '- Overflowing image drag pans both axes by 80px; releasing restores the grab cursor', ].join('\n')) const releaseRead = Promise.withResolvers() diff --git a/apps/web/tests/markdown-images.e2e.ts b/apps/web/tests/markdown-images.e2e.ts index db3cad441a..d521b59dce 100644 --- a/apps/web/tests/markdown-images.e2e.ts +++ b/apps/web/tests/markdown-images.e2e.ts @@ -319,6 +319,15 @@ describe('web e2e: Markdown image rendering', () => { expect(tripwire.pageErrors).toEqual([]) expect(tripwire.warnings).toEqual([]) await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md']) + const input = page.locator('[data-composer-input]').first() + await workspaceImage.dragTo(input) + const dragged = page.getByRole('group', { name: 'Pending attachments', exact: true }).getByRole('img') + await dragged.waitFor({ state: 'visible' }) + expect(Buffer.from(await dragged.evaluate(async (node: HTMLImageElement) => + Array.from(new Uint8Array(await (await fetch(node.src)).arrayBuffer()))))).toEqual(PNG) + expect(await input.innerText()).toBe('') + expect(await page.getByRole('dialog', { name: 'Image preview', exact: true }).count()).toBe(0) + await page.getByRole('button', { name: 'Remove image valid.png', exact: true }).click() await link.evaluate((element) => { element.scrollIntoView({ block: 'end' }) }) await page.mouse.move(0, 0) await link.hover() diff --git a/packages/client/ui-attachment/README.i18n.yaml b/packages/client/ui-attachment/README.i18n.yaml index 123b686b4e..bac5a4d946 100644 --- a/packages/client/ui-attachment/README.i18n.yaml +++ b/packages/client/ui-attachment/README.i18n.yaml @@ -21,8 +21,8 @@ en: a77c8ec48e219aec zh: 756d4fe881403006 /deepseek-ai-dsh-client-ui-attachment/use-this-package/message-images-and-the-lightbox: - en: 8b3d7729152d8060 - zh: 815f932ccc9653f5 + en: 10518a60d83df5c9 + zh: f50541b6662ab72b /deepseek-ai-dsh-client-ui-attachment/use-this-package/drop-overlay: en: 836a62c8f73366ac zh: 35da7836eaf68a67 diff --git a/packages/client/ui-attachment/README.md b/packages/client/ui-attachment/README.md index 4fee29e123..330be4a286 100644 --- a/packages/client/ui-attachment/README.md +++ b/packages/client/ui-attachment/README.md @@ -37,6 +37,8 @@ In Chat, one user message presents files and images in a right-aligned wrapping Trajectory attachment rows request 48px square thumbnails that contain the complete image without cropping. Loading and retry icons keep the same box, with localized tooltips and accessible names; the image opens the same lightbox. A slot owner may supply a presentation-only image label for the thumbnail and lightbox without changing the durable reference or the cache lookup. +Loaded conversation images can be dragged into the composer as image attachments or to another application as original files. Dragging does not open the lightbox. The shared image-transfer owner retains Web drag bytes until the gesture ends; Desktop uses the system file drag. A failed preparation or drag keeps the image visible and reports a localized retry message. + ### Drop overlay While a file drag is over the page, the full-viewport overlay announces the drop: illustration, title, and a limits line when drops are accepted. The overlay only shows state — the owner's document-level listeners decide accept or reject, and the drop handler reports which dropped members are directories through the entry API so the owner can cite or refuse them. diff --git a/packages/client/ui-attachment/README.zh.md b/packages/client/ui-attachment/README.zh.md index 706a6d97c7..3a4d79a336 100644 --- a/packages/client/ui-attachment/README.zh.md +++ b/packages/client/ui-attachment/README.zh.md @@ -37,6 +37,8 @@ Chat 中的一条用户消息把文件与图片放在同一个靠右、可换行 Trajectory 附件行使用 48px 方形缩略图,完整缩放图片而不裁剪。加载与重试图标保持相同尺寸,提供本地化的工具提示和可访问名称;图片打开同一个灯箱。插槽持有方可以为缩略图与灯箱提供仅用于展示的图片名称,而不改变持久化引用或缓存查询。 +已加载的会话图片可拖入输入框作为图片附件,也可将原文件拖到其他应用。拖动不会打开灯箱。共享图片传递组件将 Web 拖动的原图字节保留到手势结束;Desktop 使用系统文件拖放。准备或拖放失败时保留图片,并显示本地化的重试提示。 + ### 拖放遮罩 文件拖到页面上方时,全视口遮罩显示拖放提示,包括插画和标题;接受拖放时还会显示一行限制说明。遮罩只呈现状态,是否接受由持有方的文档级监听器决定,拖放处理器通过 entry API 报告放下的成员中哪些是文件夹,以便持有方引用或拒绝它们。 diff --git a/packages/client/ui-attachment/src/MessageImage.tsx b/packages/client/ui-attachment/src/MessageImage.tsx index a067ffd446..62afb5b279 100644 --- a/packages/client/ui-attachment/src/MessageImage.tsx +++ b/packages/client/ui-attachment/src/MessageImage.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import type { ImageAttachmentRef } from '@deepseek-ai/dsh-attachment' -import { IconLoadingOutlineRegular, IconRefreshOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives' +import { IconLoadingOutlineRegular, IconRefreshOutlineRegular, useImageDrag } from '@deepseek-ai/dsh-client-ui-primitives' import { ImageLightbox } from '@deepseek-ai/dsh-client-ui-primitives' import type { MessageImageLabels } from '@deepseek-ai/dsh-client-ui-conversation/client' import css from './MessageImage.module.css' @@ -108,6 +108,7 @@ export function MessageImage({ image, load, variant, labels }: { }, [attachment, load, attempt]) const src = preview?.url ?? loaded + const drag = useImageDrag(src ?? '', preview?.name ?? attachment?.name ?? 'image', labels.dragFailed) const label = ('attachment' in image ? image.label : undefined) ?? preview?.name ?? attachment?.name ?? labels.image const loadingThumbnail = variant === 'thumbnail' && src === null @@ -135,6 +136,7 @@ export function MessageImage({ image, load, variant, labels }: { title={loadingThumbnail ? labels.loading : labels.open} aria-label={loadingThumbnail ? labels.loading : labels.openNamed(label)} aria-busy={loadingThumbnail || undefined} + onClickCapture={drag.onClickCapture} onClick={() => { if (src !== null) setOpen(true) }} > {src === null @@ -143,7 +145,9 @@ export function MessageImage({ image, load, variant, labels }: { {loadingThumbnail ? : labels.loading} ) - : {label}} + : {label}} {open && src !== null && } diff --git a/packages/client/ui-attachment/src/client/ComposerAttachments.tsx b/packages/client/ui-attachment/src/client/ComposerAttachments.tsx index 0065410b9d..e172778335 100644 --- a/packages/client/ui-attachment/src/client/ComposerAttachments.tsx +++ b/packages/client/ui-attachment/src/client/ComposerAttachments.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import type { ComposerAttachment, ComposerAttachmentsProps, ComposerImageAttachment, } from '@deepseek-ai/dsh-client-ui-conversation/client' -import { IconCloseFillRegular } from '@deepseek-ai/dsh-client-ui-primitives' +import { IconCloseFillRegular, useImageDrop } from '@deepseek-ai/dsh-client-ui-primitives' import { AttachmentRail } from '../AttachmentRail.tsx' import type { AttachmentRailItem } from '../AttachmentRail.tsx' import { DropOverlay } from '../DropOverlay.tsx' @@ -23,6 +23,7 @@ export function ComposerAttachments({ }: ComposerAttachmentsProps) { const [preview, setPreview] = useState(null) const [dragActive, setDragActive] = useState(false) + const readImageDrag = useImageDrop() const dragDepth = useRef(0) const closePreview = useCallback(() => { setPreview(null) }, []) useEffect(() => { @@ -30,8 +31,8 @@ export function ComposerAttachments({ }, [attachments, preview]) useEffect(() => { - return installDocumentDropEvents(canAcceptDrop, onAddFiles, dragDepth, setDragActive) - }, [canAcceptDrop, onAddFiles]) + return installDocumentDropEvents(canAcceptDrop, onAddFiles, dragDepth, setDragActive, readImageDrag) + }, [canAcceptDrop, onAddFiles, readImageDrag]) const railItems = useMemo(() => attachments.map(attachment => ({ id: attachment.id, diff --git a/packages/client/ui-attachment/src/client/drop-events.ts b/packages/client/ui-attachment/src/client/drop-events.ts index 2f3da69372..938cd3bada 100644 --- a/packages/client/ui-attachment/src/client/drop-events.ts +++ b/packages/client/ui-attachment/src/client/drop-events.ts @@ -1,4 +1,5 @@ /** Document drag-and-drop listeners owned by one mounted attachment view. */ +import { IMAGE_DRAG_TYPE } from '@deepseek-ai/dsh-client-ui-primitives' import type { ComposerAttachmentsProps } from '@deepseek-ai/dsh-client-ui-conversation/client' /** @@ -25,6 +26,7 @@ function droppedDirectories(dataTransfer: DataTransfer, files: readonly File[]): * @param onAddFiles - attachment intake callback. * @param dragDepth - the view's retained nested-drag counter. * @param setDragActive - publish whether a file drag is active. + * @param readImageDrag - resolve an original image retained by this application during a browser drag. * @returns cleanup for exactly these listeners. */ export function installDocumentDropEvents( @@ -32,10 +34,11 @@ export function installDocumentDropEvents( onAddFiles: ComposerAttachmentsProps['onAddFiles'], dragDepth: { current: number }, setDragActive: (active: boolean) => void, + readImageDrag: (transfer: DataTransfer) => File | undefined, ): () => void { const fileTransfer = (event: globalThis.DragEvent): DataTransfer | null => { const dataTransfer = event.dataTransfer - if (dataTransfer === null || !dataTransfer.types.includes('Files')) return null + if (dataTransfer === null || (!dataTransfer.types.includes('Files') && !dataTransfer.types.includes(IMAGE_DRAG_TYPE))) return null return dataTransfer } const reset = (): void => { @@ -68,7 +71,8 @@ export function installDocumentDropEvents( event.preventDefault() reset() if (canAcceptDrop) { - const files = [...dataTransfer.files] + const image = dataTransfer.types.includes(IMAGE_DRAG_TYPE) ? readImageDrag(dataTransfer) : undefined + const files = image === undefined ? [...dataTransfer.files] : [image] onAddFiles(files, droppedDirectories(dataTransfer, files)) } } diff --git a/packages/client/ui-attachment/src/client/labels.ts b/packages/client/ui-attachment/src/client/labels.ts index 257a362d97..6e684ed6b0 100644 --- a/packages/client/ui-attachment/src/client/labels.ts +++ b/packages/client/ui-attachment/src/client/labels.ts @@ -21,6 +21,7 @@ export function lightboxLabels(t: TranslateNS<'conversation'>): ImageLightboxLab */ export function messageImageLabels(t: TranslateNS<'conversation'>): MessageImageLabels { return { + dragFailed: t('image.dragFailed'), image: t('image.label'), open: t('image.openOriginal'), openNamed: label => t('image.openOriginalLabel', { label }), diff --git a/packages/client/ui-attachment/tests/message-image.client.spec.tsx b/packages/client/ui-attachment/tests/message-image.client.spec.tsx index d447df5fd9..b55c65ecfc 100644 --- a/packages/client/ui-attachment/tests/message-image.client.spec.tsx +++ b/packages/client/ui-attachment/tests/message-image.client.spec.tsx @@ -18,6 +18,7 @@ afterEach(cleanup) const labels: MessageImageLabels = { image: '图片', + dragFailed: '图片尚未准备好拖出,请重试', open: '查看原图', openNamed: label => `${label},点击查看原图`, loading: '图片加载中…', diff --git a/packages/client/ui-chat/src/client/chat/ChatView.tsx b/packages/client/ui-chat/src/client/chat/ChatView.tsx index 1e0741f31f..6c38f26abd 100644 --- a/packages/client/ui-chat/src/client/chat/ChatView.tsx +++ b/packages/client/ui-chat/src/client/chat/ChatView.tsx @@ -86,7 +86,7 @@ export function ChatView({ const fileImages = useMemo(() => ({ resolve: (path: string) => fileMediaUrl(document.baseURI, resolveWorkspacePath(cwd, path)), labels: { - open: t('image.open'), loading: t('image.loading'), failed: t('image.failed'), + dragFailed: t('image.dragFailed'), open: t('image.open'), loading: t('image.loading'), failed: t('image.failed'), dialog: t('image.dialog'), close: t('image.close'), }, }), [cwd, t]) diff --git a/packages/client/ui-chat/src/client/locale.ts b/packages/client/ui-chat/src/client/locale.ts index 7a950132e2..095f62dd68 100644 --- a/packages/client/ui-chat/src/client/locale.ts +++ b/packages/client/ui-chat/src/client/locale.ts @@ -67,6 +67,7 @@ export const zh = { 'view.chat': '对话', 'image.open': '查看大图', 'image.loading': '加载图片…', + 'image.dragFailed': '图片尚未准备好拖出,请重试', 'image.failed': '图片无法预览', 'image.dialog': '图片预览', 'image.close': '关闭图片预览', @@ -269,6 +270,7 @@ export const en = { 'view.chat': 'Chat', 'image.open': 'View full image', 'image.loading': 'Loading image…', + 'image.dragFailed': 'The image is not ready to drag. Try again.', 'image.failed': 'Image preview unavailable', 'image.dialog': 'Image preview', 'image.close': 'Close image preview', diff --git a/packages/client/ui-conversation/src/client/contract/slots.ts b/packages/client/ui-conversation/src/client/contract/slots.ts index a02f5aa662..a5b99922eb 100644 --- a/packages/client/ui-conversation/src/client/contract/slots.ts +++ b/packages/client/ui-conversation/src/client/contract/slots.ts @@ -131,6 +131,8 @@ export interface MessageImageLabels { loading: string /** Retry-control label shown when the load fails. */ loadFailed: string + /** Failed original-image drag, with retry guidance. */ + dragFailed: string /** Lightbox strings forwarded to the opened preview. */ lightbox: ImageLightboxLabels } diff --git a/packages/client/ui-conversation/src/client/locales.ts b/packages/client/ui-conversation/src/client/locales.ts index f1f708073b..d7e432fdd6 100644 --- a/packages/client/ui-conversation/src/client/locales.ts +++ b/packages/client/ui-conversation/src/client/locales.ts @@ -46,6 +46,7 @@ export const zh = { 'image.label': '图片', 'image.loadFailed': '图片加载失败,点击重试', 'image.loading': '图片加载中…', + 'image.dragFailed': '图片尚未准备好拖出,请重试', 'image.preview': '原图预览', 'image.closePreview': '关闭原图预览', 'image.unsupportedType': '仅支持 PNG、JPG、WebP、GIF 格式的图片', @@ -426,6 +427,7 @@ export const en = { 'image.label': 'Image', 'image.loadFailed': 'Image failed to load; click to retry', 'image.loading': 'Loading image…', + 'image.dragFailed': 'The image is not ready to drag. Try again.', 'image.preview': 'Original image preview', 'image.closePreview': 'Close original image preview', 'image.unsupportedType': 'Only PNG, JPG, WebP, and GIF images are supported', diff --git a/packages/client/ui-layout/package.json b/packages/client/ui-layout/package.json index a233d3410a..ce216ab52f 100644 --- a/packages/client/ui-layout/package.json +++ b/packages/client/ui-layout/package.json @@ -56,7 +56,8 @@ "@deepseek-ai/dsh-client-ui-theme": "workspace:*", "@deepseek-ai/cordis": "workspace:~", "react": "^18.2.0", - "@deepseek-ai/dsh-client-shortcuts": "workspace:*" + "@deepseek-ai/dsh-client-shortcuts": "workspace:*", + "@deepseek-ai/dsh-client-ui-primitives": "workspace:*" }, "files": [ "locale/*.json", diff --git a/packages/client/ui-layout/src/client/AppFrame.tsx b/packages/client/ui-layout/src/client/AppFrame.tsx index 5614947447..2a076acb14 100644 --- a/packages/client/ui-layout/src/client/AppFrame.tsx +++ b/packages/client/ui-layout/src/client/AppFrame.tsx @@ -20,6 +20,7 @@ import type { PropsLocale, PropsRenderSlots, PropsRuntime, PropsStore, } from '@deepseek-ai/dsh-client-ui-slots' import { CENTER_MIN, clampWidth, computeColumns, RIGHTBAR_DEFAULT_RATIO, RIGHTBAR_MAX_RATIO, RIGHTBAR_MIN, SIDEBAR_AUTO_COLLAPSE, SIDEBAR_COLLAPSED, SIDEBAR_DEFAULT } from './columns.ts' +import { ImageDragProvider } from '@deepseek-ai/dsh-client-ui-primitives' import { DocumentTitle } from './DocumentTitle.tsx' import type { createLayoutStore } from './stores.ts' import css from './AppFrame.module.css' @@ -256,7 +257,7 @@ export function AppFrame({ const leadingMounted = darwin && sidebarCollapsed return ( -
0 && ( )} -
+
) } diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index 2e59aaf715..4599e98497 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/README.i18n.yaml @@ -24,8 +24,8 @@ en: 28ac8f0b1edbcf81 zh: 5c4c324bf84a282f /deepseek-ai-dsh-client-ui-primitives/use-this-package/rendering-agent-output: - en: 12fe9d9b1410edef - zh: f2dfdc01170d7cce + en: 40a6ad9b8840aaf2 + zh: 2997fa3df59a4ec8 /deepseek-ai-dsh-client-ui-primitives/use-this-package/localizing-copy: en: 4aef65dea11de083 zh: abc22cd7c65cdf38 diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index 5b29187be1..f65d3900c3 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -111,6 +111,8 @@ The nearest `MarkdownDelegateProvider` supplies optional `openExternalLink` and `ImageLightbox` is the shared original-image modal with focus restoration and Escape dismissal. Like `Modal`, its mask leaves the Windows caption unpainted, and its close control sits 20px below the caption. The internal thumbnail renderer receives loading/failure labels from its owner. `HoverCard.inline` keeps a file link in the text flow and uses the shared menu material, keyboard-visible focus, and placement above or below the anchor without covering it. Escape closes an open thumbnail even when focus is elsewhere; subsequent Escape presses reach the owner. `MarkdownDelegateProvider.fileImages` supplies a decoded-path resolver and complete image labels: settled image links gain hover previews, and standalone images gain lightbox activation. Image-only anchors retain their single navigation target. The parser recovers only complete, unescaped, standalone local image references with bare spaces and an unambiguous supported image suffix; code and ambiguous destinations remain literal. +`ImageDragProvider` wraps the application and retains the active Web image transfer and failure toast. `useImageDrag` prepares original bytes from an authorized source or supplied `File`, cancels stale preparation, and uses the Desktop bridge when present. Web transfers carry an application-local token plus Chromium’s `DownloadURL`; `useImageDrop` resolves only the active token to its original `File`. External browser drops depend on the receiving application and browser support. No drag places an internal URL or alt text in `text/plain`. + ### Localizing copy The atoms cannot read the application locale, so every piece of user-facing copy arrives through required label props. `HoverCard`, `TerminalBlock`, `JsonTree`, `CodeBlock`, `MarkdownText`, `JsonBlock`, `ConnectionIndicator`, `Modal`, `DiffBlock`, `ReadBlock`, `SearchBlock`, and `WebBlock` accept complete localized labels. The package owns no language fallback; omission fails typechecking, and each feature maps its typed `t` seat into the primitive's label interface. diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index c1593e273b..fac88aa250 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -111,6 +111,8 @@ kind: "package-library" `ImageLightbox` 是共享原图浮层,支持焦点恢复与 Esc 关闭。与 `Modal` 相同,其遮罩不覆盖 Windows 顶栏,关闭按钮位于顶栏下方 20px。包内缩略图渲染器由调用方提供加载及失败文案。`HoverCard.inline` 使文件链接保持在文本流内,并使用共享菜单材质、键盘可见焦点,并在锚点上方或下方定位而不遮挡锚点。即使焦点位于其他位置,Esc 也会关闭已打开的缩略图;后续 Esc 按键继续传给 owner。`MarkdownDelegateProvider.fileImages` 提供已解码路径解析器与完整图片文案:消息落定后的图片链接支持悬停预览,独立图片支持点击放大。仅包含图片的链接保留单一导航目标。解析器仅恢复完整、未转义、独立成段且带明确图片扩展名的含裸空格本地图片引用;代码与有歧义的目标保持原文。 +`ImageDragProvider` 包裹应用,持有当前 Web 图片传递状态和失败提示。`useImageDrag` 从已授权来源或传入的 `File` 准备原图字节,取消过期准备,并在 Desktop 桥接存在时使用它。Web 拖放携带应用内令牌和 Chromium 的 `DownloadURL`;`useImageDrop` 仅将当前令牌解析为原始 `File`。浏览器向外部拖放的结果取决于浏览器和接收应用的支持。拖放不会将内部 URL 或 alt 文本放入 `text/plain`。 + ### 本地化文案 这些原子组件无法读取应用 locale,因此每段面向用户的文案都必须通过 label prop 提供。`HoverCard`、`TerminalBlock`、`JsonTree`、`CodeBlock`、`MarkdownText`、`JsonBlock`、`ConnectionIndicator`、`Modal`、`DiffBlock`、`ReadBlock`、`SearchBlock` 与 `WebBlock` 接收完整的本地化 label。本包不拥有语言回退;遗漏会导致类型检查失败,各功能会把带类型的 `t` 席位映射到 primitive 的 label 接口。 diff --git a/packages/client/ui-primitives/src/ImageDragProvider.tsx b/packages/client/ui-primitives/src/ImageDragProvider.tsx new file mode 100644 index 0000000000..f6c2cece0a --- /dev/null +++ b/packages/client/ui-primitives/src/ImageDragProvider.tsx @@ -0,0 +1,60 @@ +/** Application-lifetime feedback for image drag operations. */ +import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from 'react' +import { Toast } from './Toast.tsx' + +/** Browser drag token; file bytes remain in the owning mounted application. */ +export const IMAGE_DRAG_TYPE = 'application/x-dsh-image' + +interface ImageTransfer { + report: (text: string) => void + begin: (file: File) => string + end: () => void + read: (transfer: DataTransfer) => File | undefined +} + +const missingProvider = (): never => { throw new Error('Image dragging requires ImageDragProvider') } +const ImageDragFeedback = createContext({ + report: missingProvider, begin: missingProvider, end: missingProvider, read: missingProvider, +}) + +/** + * Keep drag failures visible when their source image or conversation unmounts. + * @param props - application children. + * @returns children and the current localized failure toast. + */ +export function ImageDragProvider({ children }: { readonly children: ReactNode }): ReactNode { + const active = useRef<{ token: string; file: File }>() + const sequence = useRef(0) + const [failure, setFailure] = useState<{ text: string; sequence: number }>() + const report = useCallback((text: string) => { setFailure({ text, sequence: ++sequence.current }) }, []) + const dismiss = useCallback(() => { setFailure(undefined) }, []) + const begin = useCallback((file: File): string => { + const token = Array.from(crypto.getRandomValues(new Uint8Array(16)), byte => byte.toString(16).padStart(2, '0')).join('') + active.current = { token, file } + return token + }, []) + const end = useCallback(() => { active.current = undefined }, []) + useEffect(() => { + window.addEventListener('dragend', end) + return () => { window.removeEventListener('dragend', end) } + }, [end]) + const read = useCallback((transfer: DataTransfer): File | undefined => { + const current = active.current + return current !== undefined && current.token === transfer.getData(IMAGE_DRAG_TYPE) ? current.file : undefined + }, []) + const value = useMemo(() => ({ report, begin, end, read }), [report, begin, end, read]) + return + {children} + {failure !== undefined && } + +} + +/** @returns the mounted application's image transfer owner. */ +export function useImageDragTransfer(): ImageTransfer { + return useContext(ImageDragFeedback) +} + +/** @returns a reader that resolves only this application's active original-image drag. */ +export function useImageDrop(): (transfer: DataTransfer) => File | undefined { + return useImageDragTransfer().read +} diff --git a/packages/client/ui-primitives/src/image-drag.ts b/packages/client/ui-primitives/src/image-drag.ts new file mode 100644 index 0000000000..80db3219bf --- /dev/null +++ b/packages/client/ui-primitives/src/image-drag.ts @@ -0,0 +1,110 @@ +/** Original image files supplied to browser and native drag sessions. */ +import { useCallback, useEffect, useRef, type DragEvent, type MouseEvent, type PointerEvent } from 'react' + +import { IMAGE_DRAG_TYPE, useImageDragTransfer } from './ImageDragProvider.tsx' + +/** Application-owned native image transfer; prepared handles never expose host paths. */ +export interface DesktopImageDrag { + /** @returns an opaque handle for the original file, retained until release or application exit. */ + prepare: (bytes: Uint8Array, name: string, mediaType: string) => Promise + /** Start a system copy drag from an already prepared handle. */ + start: (handle: string) => Promise + /** Release an unused handle; files already handed to the OS survive until application exit. */ + release: (handle: string) => void +} + +interface PreparedImage { + readonly file: File + readonly handle?: string +} + +interface ImageDragHandlers { + onPointerEnter: () => void + onPointerDown: (event: PointerEvent) => void + onDragStart: (event: DragEvent) => void + onDragEnd: () => void + onClickCapture: (event: MouseEvent) => void +} + +/** + * Prepare original bytes on hover and supply them synchronously when dragging begins. + * @param src - authorized image URL; changing it retires pending preparation. + * @param name - suggested original filename, without a directory. + * @param failureText - localized failure message, including a retry action. + * @param original - already loaded original file; omitted sources are read from their image URL. + * @returns drag handlers; cancellation never starts a late drag. + */ +export function useImageDrag(src: string, name: string, failureText: string, original?: File): ImageDragHandlers { + const desktop = (globalThis as typeof globalThis & { dshDesktop?: { imageDrag?: DesktopImageDrag } }).dshDesktop?.imageDrag + const state = useRef<{ source: string; prepared?: PreparedImage; pending?: Promise | undefined; abort: AbortController }>() + const dragged = useRef(false) + const transfer = useImageDragTransfer() + const reportFailure = transfer.report + const prepare = useCallback((): void => { + const current = state.current + if (src === '' || current === undefined || current.source !== src || current.pending !== undefined || current.prepared !== undefined) return + current.pending = (async () => { + let file = original + if (file === undefined) { + const response = await fetch(src, { signal: current.abort.signal, referrerPolicy: 'no-referrer' }) + if (!response.ok) throw new Error('Image read failed') + const blob = await response.blob() + if (!blob.type.startsWith('image/')) throw new Error('Image media type unavailable') + file = new File([blob], name, { type: blob.type }) + } + if (current.abort.signal.aborted) return + const handle = desktop === undefined ? undefined + : await desktop.prepare(new Uint8Array(await file.arrayBuffer()), file.name, file.type) + if (state.current !== current) { + if (handle !== undefined) desktop?.release(handle) + return + } + current.prepared = { file, ...handle === undefined ? {} : { handle } } + })().catch((_error: unknown) => { + // Preparation is speculative; a failed user gesture reports through the owning image. + }).finally(() => { current.pending = undefined }) + }, [src, name, original, desktop]) + useEffect(() => { + const current: NonNullable = { source: src, abort: new AbortController() } + state.current = current + if (src !== '' && (original !== undefined || new URL(src, location.href).origin === location.origin)) prepare() + return () => { + current.abort.abort() + const prepared = current.prepared + if (prepared?.handle !== undefined) desktop?.release(prepared.handle) + state.current = undefined + } + }, [src, name, original, desktop, prepare]) + const onDragStart = useCallback((event: DragEvent): void => { + if (event.defaultPrevented) return + dragged.current = true + const prepared = state.current?.source === src ? state.current.prepared : undefined + if (prepared === undefined) { + event.preventDefault() + prepare() + reportFailure(failureText) + return + } + event.dataTransfer.items.clear() + event.dataTransfer.effectAllowed = 'copy' + if (desktop !== undefined && prepared.handle !== undefined) { + event.preventDefault() + void desktop.start(prepared.handle).then((started) => { if (!started) reportFailure(failureText) }, + () => { reportFailure(failureText) }) + return + } + event.dataTransfer.setData(IMAGE_DRAG_TYPE, transfer.begin(prepared.file)) + event.dataTransfer.setData('DownloadURL', `${prepared.file.type}:${prepared.file.name}:${src}`) + }, [src, desktop, prepare, reportFailure, failureText, transfer]) + const onPointerDown = useCallback((event: PointerEvent): void => { + dragged.current = false + if (event.button === 0) prepare() + }, [prepare]) + const onClickCapture = useCallback((event: MouseEvent): void => { + if (!dragged.current) return + event.preventDefault() + event.stopPropagation() + dragged.current = false + }, []) + return { onPointerEnter: prepare, onPointerDown, onDragStart, onDragEnd: transfer.end, onClickCapture } +} diff --git a/packages/client/ui-primitives/src/index.ts b/packages/client/ui-primitives/src/index.ts index 18f9544405..28a3a71fd0 100644 --- a/packages/client/ui-primitives/src/index.ts +++ b/packages/client/ui-primitives/src/index.ts @@ -67,6 +67,9 @@ export { pointerModality } from './input-modality.ts' export { Toast } from './Toast.tsx' export { fileSizeText } from './file-size.ts' export { writeClipboard } from './clipboard.ts' +export { IMAGE_DRAG_TYPE, ImageDragProvider, useImageDrop } from './ImageDragProvider.tsx' +export { useImageDrag } from './image-drag.ts' +export type { DesktopImageDrag } from './image-drag.ts' export { SettingsForm } from './settings-form/SettingsForm.tsx' export type { SettingsFormLabels, SettingsFormProps } from './settings-form/SettingsForm.tsx' export { SettingsSecretField, SettingsValueField } from './settings-form/fields.tsx' diff --git a/packages/client/ui-primitives/src/markdown/MarkdownDelegate.tsx b/packages/client/ui-primitives/src/markdown/MarkdownDelegate.tsx index 681e8f5608..d65c435fea 100644 --- a/packages/client/ui-primitives/src/markdown/MarkdownDelegate.tsx +++ b/packages/client/ui-primitives/src/markdown/MarkdownDelegate.tsx @@ -14,7 +14,7 @@ export interface MarkdownDelegate { /** Image previews for decoded local paths in this owner's workspace. */ readonly fileImages?: { resolve: (path: string) => string | undefined - labels: ImageLightboxLabels & { open: string; loading: string; failed: string } + labels: ImageLightboxLabels & { open: string; loading: string; failed: string; dragFailed: string } } | undefined /** Ordinary HTTP(S) activation; absent handlers retain native anchor behavior. */ readonly openExternalLink?: MarkdownExternalLinkHandler | undefined diff --git a/packages/client/ui-primitives/src/markdown/render.tsx b/packages/client/ui-primitives/src/markdown/render.tsx index cdb9afcbda..05362b438e 100644 --- a/packages/client/ui-primitives/src/markdown/render.tsx +++ b/packages/client/ui-primitives/src/markdown/render.tsx @@ -29,6 +29,7 @@ import { renderTexToReact } from './katex.tsx' import { LinkIconMedium, classifyLinkPath } from '../LinkIcon.tsx' import { useMarkdownDelegate } from './MarkdownDelegate.tsx' import { HoverCard } from '../HoverCard.tsx' +import { useImageDrag } from '../image-drag.ts' import { ImageLightbox } from '../ImageLightbox.tsx' import { ImagePreview } from '../ImagePreview.tsx' import type { PositionedBlock } from './incremental.ts' @@ -650,14 +651,17 @@ function LoadedMarkdownImage({ src, alt, destination, preview }: { const [failed, setFailed] = useState(false) const [open, setOpen] = useState(false) const close = useCallback(() => { setOpen(false) }, []) + const drag = useImageDrag(preview === undefined ? '' : src, destination.split('/').at(-1)?.split(/[?#]/u)[0] || 'image', preview?.labels.dragFailed ?? '') if (failed) return {preview === undefined ? '' : `${preview.labels.failed} · `}{alt || destination} const img = {alt} { setFailed(true) }} loading="lazy" decoding="async" referrerPolicy="no-referrer" /> if (preview === undefined) return img return <> + onClickCapture={drag.onClickCapture} onClick={() => { setOpen(true) }}>{img} {open && } } diff --git a/packages/client/ui-primitives/tests/image-drag.client.spec.tsx b/packages/client/ui-primitives/tests/image-drag.client.spec.tsx new file mode 100644 index 0000000000..740178bd33 --- /dev/null +++ b/packages/client/ui-primitives/tests/image-drag.client.spec.tsx @@ -0,0 +1,119 @@ +// @vitest-environment jsdom +import { File as NodeFile } from 'node:buffer' +import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' +import { afterEach, expect, it, vi } from 'vitest' +import { IMAGE_DRAG_TYPE, ImageDragProvider, useImageDrop } from '../src/ImageDragProvider.tsx' +import { useImageDrag, type DesktopImageDrag } from '../src/image-drag.ts' + +afterEach(() => { cleanup(); vi.unstubAllGlobals(); vi.restoreAllMocks() }) + +function Image({ src, file, onOpen }: { src: string; file?: File; onOpen?: () => void }) { + const drag = useImageDrag(src, 'original.png', 'Image drag failed. Try again.', file) + return +} +function transfer() { + const add = vi.fn() + const values = new Map() + return { items: { add, clear: vi.fn() }, getData: (type: string) => values.get(type) ?? '', + setData: vi.fn((type: string, value: string) => { values.set(type, value) }), effectAllowed: 'none' } +} +function native() { + const bridge: DesktopImageDrag = { prepare: vi.fn(async () => 'handle'), start: vi.fn(async () => true), release: vi.fn() } + vi.stubGlobal('dshDesktop', { imageDrag: bridge }) + return bridge +} +function original() { + return new NodeFile([new Uint8Array([137, 80, 78, 71])], 'original.png', { type: 'image/png' }) as File +} + +it('hands original bytes to the browser without alt text or an internal URI payload', async () => { + const file = original() + const open = vi.fn() + render() + const dataTransfer = transfer() + await act(async () => { fireEvent.dragStart(screen.getByRole('img'), { dataTransfer }) }) + expect(dataTransfer.items.add).not.toHaveBeenCalled() + expect(dataTransfer.items.clear).toHaveBeenCalledOnce() + expect(dataTransfer.setData).toHaveBeenCalledWith(IMAGE_DRAG_TYPE, expect.any(String)) + expect(dataTransfer.effectAllowed).toBe('copy') + expect(dataTransfer.setData).toHaveBeenCalledWith('DownloadURL', 'image/png:original.png:blob:original') + fireEvent.click(screen.getByRole('button')) + expect(open).not.toHaveBeenCalled() + fireEvent.pointerDown(screen.getByRole('img'), { button: 0 }) + fireEvent.click(screen.getByRole('button')) + expect(open).toHaveBeenCalledOnce() +}) + +it('prepares before the gesture, starts native file dragging and releases its handle on unmount', async () => { + const bridge = native() + const file = original() + const view = render() + await act(async () => { await Promise.resolve() }) + expect(bridge.prepare).toHaveBeenCalledExactlyOnceWith(new Uint8Array([137, 80, 78, 71]), 'original.png', 'image/png') + const dataTransfer = transfer() + await act(async () => { expect(fireEvent.dragStart(screen.getByRole('img'), { dataTransfer })).toBe(false) }) + expect(bridge.start).toHaveBeenCalledExactlyOnceWith('handle') + expect(dataTransfer.items.add).not.toHaveBeenCalled() + view.unmount() + expect(bridge.release).toHaveBeenCalledExactlyOnceWith('handle') +}) + +it('cancels an unready gesture and releases late preparation without starting a delayed drag', async () => { + const bridge = native() + const pending = Promise.withResolvers() + vi.mocked(bridge.prepare).mockReturnValue(pending.promise) + const view = render() + await act(async () => { await Promise.resolve() }) + fireEvent.dragStart(screen.getByRole('img'), { dataTransfer: transfer() }) + expect(screen.getByRole('alert').textContent).toBe('Image drag failed. Try again.') + view.rerender(Image closed) + expect(screen.getByRole('alert').textContent).toBe('Image drag failed. Try again.') + await act(async () => { pending.resolve('late'); await pending.promise }) + expect(bridge.release).toHaveBeenCalledExactlyOnceWith('late') + expect(bridge.start).not.toHaveBeenCalled() +}) + +it('reports native failure even after its source image closes', async () => { + const bridge = native() + const pending = Promise.withResolvers() + vi.mocked(bridge.start).mockReturnValue(pending.promise) + const view = render() + await act(async () => { await Promise.resolve() }) + fireEvent.dragStart(screen.getByRole('img'), { dataTransfer: transfer() }) + view.rerender(Image closed) + await act(async () => { pending.resolve(false); await pending.promise }) + expect(screen.getByRole('alert').textContent).toBe('Image drag failed. Try again.') +}) + +it('never exports a failed read or starts a native drag after fetch cancellation', async () => { + const bridge = native() + const pending = Promise.withResolvers() + const fetchImage = vi.fn().mockReturnValue(pending.promise) + vi.stubGlobal('fetch', fetchImage) + const view = render() + fireEvent.pointerEnter(screen.getByRole('img')) + const request = fetchImage.mock.calls[0]?.[1] + view.rerender(Closed) + expect(request?.signal?.aborted).toBe(true) + await act(async () => { pending.resolve(new Response('unavailable', { status: 404 })); await pending.promise }) + expect(bridge.prepare).not.toHaveBeenCalled() + expect(bridge.start).not.toHaveBeenCalled() +}) + +function DropTarget({ receive }: { receive: (file: File | undefined) => void }) { + const read = useImageDrop() + return
{ event.preventDefault(); receive(read(event.dataTransfer)) }} /> +} + +it('resolves only the active application drag token and clears it after dragging ends', () => { + const file = original() + const receive = vi.fn() + render() + const dataTransfer = transfer() + fireEvent.dragStart(screen.getByRole('img'), { dataTransfer }) + fireEvent.drop(screen.getByTestId('drop'), { dataTransfer }) + expect(receive).toHaveBeenLastCalledWith(file) + fireEvent.dragEnd(screen.getByRole('img'), { dataTransfer }) + fireEvent.drop(screen.getByTestId('drop'), { dataTransfer }) + expect(receive).toHaveBeenLastCalledWith(undefined) +}) diff --git a/packages/client/ui-primitives/tests/markdown-image-entries.client.spec.tsx b/packages/client/ui-primitives/tests/markdown-image-entries.client.spec.tsx index 61626a483e..72cff3c14e 100644 --- a/packages/client/ui-primitives/tests/markdown-image-entries.client.spec.tsx +++ b/packages/client/ui-primitives/tests/markdown-image-entries.client.spec.tsx @@ -7,7 +7,7 @@ import { MarkdownText } from './markdown-test-components.tsx' import { parseGfm, parseGfmWithMath } from '../src/markdown/parse.ts' afterEach(() => { cleanup(); vi.useRealTimers(); vi.restoreAllMocks() }) -const labels = { open: 'View full image', dialog: 'Image preview', close: 'Close preview', loading: 'Loading image', failed: 'Image unavailable' } +const labels = { dragFailed: 'Image drag failed', open: 'View full image', dialog: 'Image preview', close: 'Close preview', loading: 'Loading image', failed: 'Image unavailable' } const fileImages = { resolve: (path: string) => `https://example.com/api/file?path=${encodeURIComponent('/workspace/' + path)}`, labels } function mount(text: string, streaming = false) { const openFile = vi.fn() diff --git a/packages/client/ui-sidebar-documentpreview/README.i18n.yaml b/packages/client/ui-sidebar-documentpreview/README.i18n.yaml index 97adbc6c21..88793e2a34 100644 --- a/packages/client/ui-sidebar-documentpreview/README.i18n.yaml +++ b/packages/client/ui-sidebar-documentpreview/README.i18n.yaml @@ -21,8 +21,8 @@ en: 57942270e23d3a8e zh: 6617124e652b3f67 /deepseek-ai-dsh-client-ui-sidebar-documentpreview/how-it-reads: - en: b056bbe0f74fe328 - zh: fd9ac88f08825afc + en: e197303166b5a86f + zh: 8d151e49321d67aa /deepseek-ai-dsh-client-ui-sidebar-documentpreview/excel-preview: en: b99b0e0b8efc2853 zh: 4327352d1334fc6b diff --git a/packages/client/ui-sidebar-documentpreview/README.md b/packages/client/ui-sidebar-documentpreview/README.md index 43ac41c129..31e5e58923 100644 --- a/packages/client/ui-sidebar-documentpreview/README.md +++ b/packages/client/ui-sidebar-documentpreview/README.md @@ -59,7 +59,7 @@ The body reads its record, navigation and lifetime through `useTabInfo().tab`. ` When Coding Tools is enabled, HTML fills the body edge to edge in a Blob iframe with exactly `sandbox="allow-scripts"`, without `allow-same-origin`; scripts cannot access the parent application's origin or file reader. The renderer loads directly declared relative `.js` classic scripts and `.css` stylesheets through its injected Remote callback, with fixed safety limits of 4 MiB per asset, 32 MiB total, and 64 distinct assets. Host code resolves the related path, and `readBytes` with `baseFile` returns native bytes. The dependency Resource is added after the read returns, using its `absolutePath` or a string `error.details.path` on failure; without a Host path, the Client does not guess one. Inside the renderer, base64 is used only to embed the iframe bootstrap payload in script text. A `` leaves dependency resolution to the browser, as do HTTPS resources. Local module imports, CSS `url()`/`@import`, and dynamic `fetch` do not use Host file access. Read failures, invalid UTF-8, or exceeded limits fail the preview rather than publishing a partial asset package. Replacing or unmounting the document releases its Blob URL. -PNG, JPEG, GIF, WebP, BMP, ICO, and SVG render through Blob URLs in an `` static-image context, rounded inside a 12px inset. Images default to fit width without enlarging content smaller than the pane; 100% uses the image's intrinsic CSS-pixel width. The shared zoom controls can produce horizontal and vertical scrolling, but do not provide drag-to-pan. Zoom changes retain the same `` and Blob URL, so animated images continue playing. Raster formats can soften above their intrinsic size, while SVG remains on the browser's vector rendering path. SVG markup never enters the application DOM or an iframe, so its scripts cannot execute or reach the parent page. Replacing or unmounting the image revokes its Blob URL. +PNG, JPEG, GIF, WebP, BMP, ICO, and SVG render through Blob URLs in an `` static-image context, rounded inside a 12px inset. Images default to fit width without enlarging content smaller than the pane; 100% uses the image's intrinsic CSS-pixel width. When either axis overflows the viewport, left-drag pans the image with grab/grabbing cursors. A fully contained image instead drags its original file into the composer or an external application; the gesture mode remains fixed until release. Ctrl+wheel and trackpad pinch retain the shared zoom behavior. Zoom changes retain the same `` and Blob URL, so animated images continue playing. Raster formats can soften above their intrinsic size, while SVG remains on the browser's vector rendering path. SVG markup never enters the application DOM or an iframe, so its scripts cannot execute or reach the parent page. Replacing or unmounting the image revokes its Blob URL. Shared copy comes from `sidebarDocumentPreview`; each builtin renderer owns its localized labels. PDF and converted Office previews use a cool light-grey background in light mode and a matte-black background in dark mode, with subtle page shadows and original document colors. PDF and image previews share a floating control with **Fit width**, 25%, 50%, 100%, 150%, and 200% choices plus 25% incremental zoom across the 25%–400% fixed range. Fit width is the default and tracks pane resizing; 100% means the PDF's 96-DPI page size or the image's intrinsic CSS-pixel size. On fine pointers, the control starts hidden, slides up when the pointer enters the bottom 72 CSS pixels, and slides down after a 420 ms leave delay; hover, keyboard focus, an open menu, and an active gesture keep it visible, while devices without hover keep it visible continuously. Chromium maps a macOS trackpad pinch to the same live, pointer-anchored path as Ctrl+wheel; the control displays the gesture percentage, and the settled value survives body remounts until the tab closes. HTML previews do not expose zoom controls. After zoom settles or fit width changes, nearby PDF pages redraw at the resolved scale multiplied by the device pixel ratio. The previous bitmap and selectable text stay visible until the replacement bitmap is ready; offscreen pages refresh when they approach the viewport. diff --git a/packages/client/ui-sidebar-documentpreview/README.zh.md b/packages/client/ui-sidebar-documentpreview/README.zh.md index daaf6288ab..c6f61e9db3 100644 --- a/packages/client/ui-sidebar-documentpreview/README.zh.md +++ b/packages/client/ui-sidebar-documentpreview/README.zh.md @@ -59,7 +59,7 @@ Web 和桌面端均通过代码工作工具选择 HTML 预览策略。渲染器 开启代码工作工具时,HTML 以贴合正文四边的 Blob iframe 运行,沙箱属性严格为 `sandbox="allow-scripts"`,不含 `allow-same-origin`;脚本无法访问父应用的源或文件读取接口。渲染器通过注入的 Remote 回调,加载直接声明的相对 `.js` 经典脚本和 `.css` 样式表;固定安全上限为单个资源 4 MiB、总计 32 MiB、64 个不同资源。Host 代码解析关联路径,带 `baseFile` 的 `readBytes` 返回原生字节。依赖 Resource 在读取返回后加入,使用返回的 `absolutePath`,失败时则使用字符串类型的 `error.details.path`;没有 Host 路径时,Client 不自行猜测。在渲染器内部,base64 仅用于把 iframe 引导载荷嵌入脚本文本。`` 将依赖解析交给浏览器,HTTPS 资源也由浏览器处理。本地模块 import、CSS `url()`/`@import` 和动态 `fetch` 不使用 Host 文件访问。读取失败、无效 UTF-8 或超出上限都使预览失败,不发布部分资源包。替换或卸载文档会释放其 Blob URL。 -PNG、JPEG、GIF、WebP、BMP、ICO 和 SVG 通过 Blob URL 在 `` 静态图片上下文中渲染,带 12px 内边距和圆角。图片默认适应宽度,但不会放大小于面板的内容;100% 使用图片的固有 CSS 像素宽度。共享缩放控件可产生横向和纵向滚动,但不提供拖拽平移。缩放不会替换 `` 或 Blob URL,因此动画图片会继续播放。位图超过固有尺寸后可能变虚,SVG 则继续使用浏览器的矢量渲染路径。SVG 标记绝不进入应用 DOM 或 iframe,因此其中的脚本无法执行,也无法访问父页面。替换或卸载图片会撤销其 Blob URL。 +PNG、JPEG、GIF、WebP、BMP、ICO 和 SVG 通过 Blob URL 在 `` 静态图片上下文中渲染,带 12px 内边距和圆角。图片默认适应宽度,但不会放大小于面板的内容;100% 使用图片的固有 CSS 像素宽度。图片在任一方向超出视口时,按住鼠标左键拖动可平移,指针显示为抓手或抓取状态。图片完全位于视口内时,拖动会将原文件交给输入框或外部应用;一次手势的模式保持到松开为止。Ctrl+滚轮和触控板捏合沿用共享缩放行为。缩放不会替换 `` 或 Blob URL,因此动画图片会继续播放。位图超过固有尺寸后可能变虚,SVG 则继续使用浏览器的矢量渲染路径。SVG 标记绝不进入应用 DOM 或 iframe,因此其中的脚本无法执行,也无法访问父页面。替换或卸载图片会撤销其 Blob URL。 共享文案来自 `sidebarDocumentPreview`;各内置渲染器拥有自己的本地化标签。PDF 与转换后的 Office 预览在浅色模式下使用冷调浅灰底色,在深色模式下使用哑黑底色,页面带有轻微阴影并保留文档原色。PDF 与图片预览共用浮动控件,提供**适应宽度**、25%、50%、100%、150% 和 200% 选项,并在 25%–400% 固定范围内按 25% 逐级缩放。适应宽度是默认模式并跟随面板尺寸;100% 表示 PDF 的 96 DPI 页面尺寸或图片的固有 CSS 像素尺寸。在精细指针设备上,控件初始隐藏;指针进入底部 72 个 CSS 像素时向上滑入,离开 420 ms 后向下退出;控件悬停、键盘焦点、菜单展开和手势进行期间保持显示,无 hover 的设备则始终显示。Chromium 将 macOS 触控板捏合映射到与 Ctrl+滚轮相同的实时指针锚定路径;控件同步显示手势比例,静止后的值在正文重新挂载后继续保留,直至 tab 关闭。HTML 预览不显示缩放控件。缩放稳定或适应宽度发生变化后,视口附近的 PDF 页面按最终缩放比例乘以设备像素比重绘。新位图完成前保留原有画面和可选文字层;屏幕外的页面在接近视口时更新。 diff --git a/packages/client/ui-sidebar-documentpreview/src/client/image/ImageBody.module.css b/packages/client/ui-sidebar-documentpreview/src/client/image/ImageBody.module.css index 414054d2aa..422f480d64 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/image/ImageBody.module.css +++ b/packages/client/ui-sidebar-documentpreview/src/client/image/ImageBody.module.css @@ -52,3 +52,6 @@ line-height: 1.5; white-space: normal; } + +.image[data-pan="available"] { cursor: grab; } +.image[data-pan="active"] { cursor: grabbing; } diff --git a/packages/client/ui-sidebar-documentpreview/src/client/image/ImageBody.tsx b/packages/client/ui-sidebar-documentpreview/src/client/image/ImageBody.tsx index 770c806bd5..8da0834551 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/image/ImageBody.tsx +++ b/packages/client/ui-sidebar-documentpreview/src/client/image/ImageBody.tsx @@ -1,5 +1,7 @@ /** Complete image bytes rendered in a shared zoom viewport. */ -import { useCallback, useEffect, useMemo, useState, type CSSProperties, type ReactNode } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from 'react' +import { useImageDrag } from '@deepseek-ai/dsh-client-ui-primitives' +import { bindImagePan } from './pan.ts' import type { PropsLocale, PropsStore } from '@deepseek-ai/dsh-client-ui-slots' import { pathPartsOf } from '@deepseek-ai/dsh-util-workspace-path' import type { DocumentPreviewProps } from '../document/contract.ts' @@ -33,6 +35,7 @@ type ImageSource = readonly data: Uint8Array readonly mediaType: ImageMediaType readonly url: string + readonly file: File } | { readonly kind: 'failed'; readonly data: Uint8Array; readonly mediaType: ImageMediaType } @@ -72,14 +75,14 @@ export function ImageBody(props: ImageBodyProps): ReactNode { let url: string | undefined try { url = URL.createObjectURL(new Blob([data], { type: mediaType })) - setSource({ kind: 'ready', data, mediaType, url }) + setSource({ kind: 'ready', data, mediaType, url, file: new File([data], pathPartsOf(path).name, { type: mediaType }) }) } catch { setSource({ kind: 'failed', data, mediaType }) } return () => { if (url !== undefined) URL.revokeObjectURL(url) } - }, [data, mediaType]) + }, [data, mediaType, path]) if (data === undefined || mediaType === undefined) { return

{t('unsupported')}

@@ -93,15 +96,16 @@ export function ImageBody(props: ImageBodyProps): ReactNode { controls: t('zoomControls'), menu: t('zoomMenu'), out: t('zoomOut'), into: t('zoomIn'), fitWidth: t('zoomFitWidth'), value: percent => t('zoomValue', { percent }), } - return } /** SVG stays in the browser's static image mode because its bytes only reach an img Blob URL. */ -function LoadedImage({ url, name, preference, onPreference, labels, signal, scrollportRef, t }: { +function LoadedImage({ url, name, file, preference, onPreference, labels, signal, scrollportRef, t }: { readonly url: string readonly name: string + readonly file: File readonly preference: ZoomPreference readonly onPreference: (preference: ZoomPreference) => void readonly labels: ZoomLabels @@ -109,6 +113,12 @@ function LoadedImage({ url, name, preference, onPreference, labels, signal, scro readonly scrollportRef: ImageBodyProps['scrollportRef'] readonly t: ImageBodyProps['t'] }): ReactNode { + const drag = useImageDrag(url, name, t('dragFailed'), file) + const releasePan = useRef<() => void>() + const attachImage = useCallback((image: HTMLImageElement | null): void => { + releasePan.current?.() + releasePan.current = image === null ? undefined : bindImagePan(image) + }, []) const [state, setState] = useState<'loading' | 'ready' | 'failed'>('loading') const [width, setWidth] = useState() return
0 || undefined} - > - -
- {sidebar} -
- <> - {main} - - {renderSlot('rightbar', { width: normal.rightbar, viewportWidth: viewport, canShow: normal.rightbar > 0 })} - - -
- {renderSlot('shell.bottom', {})} -
-
- {overlays} -
- {leadingMounted && ( -
- {leading} + +
0 || undefined} + > + +
+ {sidebar}
- )} - {/* The collapsed rail is fixed-width: no resize handle while closed. */} - {!sidebarCollapsed && } - {layoutInfo.rightbarShown && !layoutInfo.rightbarFullscreen && normal.rightbar > 0 && ( - - )} -
+ <> + {main} + + {renderSlot('rightbar', { width: normal.rightbar, viewportWidth: viewport, canShow: normal.rightbar > 0 })} + + +
+ {renderSlot('shell.bottom', {})} +
+
+ {overlays} +
+ {leadingMounted && ( +
+ {leading} +
+ )} + {/* The collapsed rail is fixed-width: no resize handle while closed. */} + {!sidebarCollapsed && } + {layoutInfo.rightbarShown && !layoutInfo.rightbarFullscreen && normal.rightbar > 0 && ( + + )} +
+ ) } diff --git a/packages/client/ui-primitives/README.i18n.yaml b/packages/client/ui-primitives/README.i18n.yaml index 4599e98497..6de9fdb214 100644 --- a/packages/client/ui-primitives/README.i18n.yaml +++ b/packages/client/ui-primitives/README.i18n.yaml @@ -18,14 +18,14 @@ en: 7bae72c97d0449f5 zh: 4589ecc2b216edc5 /deepseek-ai-dsh-client-ui-primitives/use-this-package/component-catalog: - en: 6f5457b84fc9db09 - zh: 85993f1c44bfee2c + en: bfcb301478943364 + zh: 5e88e5f3592da502 /deepseek-ai-dsh-client-ui-primitives/use-this-package/controls-and-icons: en: 28ac8f0b1edbcf81 zh: 5c4c324bf84a282f /deepseek-ai-dsh-client-ui-primitives/use-this-package/rendering-agent-output: - en: 40a6ad9b8840aaf2 - zh: 2997fa3df59a4ec8 + en: bf371fb93897a3db + zh: a63bed4089e2aac9 /deepseek-ai-dsh-client-ui-primitives/use-this-package/localizing-copy: en: 4aef65dea11de083 zh: abc22cd7c65cdf38 @@ -54,8 +54,8 @@ en: ca75c51c89c2c9b0 zh: de54f3467ef3b9be /deepseek-ai-dsh-client-ui-primitives/known-limitations-and-deferred-work: - en: 22fc14aa3a224552 - zh: aa6309e84ad8c2a3 + en: 843daa09327f1aad + zh: 434560a9da1e6ddf /deepseek-ai-dsh-client-ui-primitives/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/client/ui-primitives/README.md b/packages/client/ui-primitives/README.md index f65d3900c3..d5fc349313 100644 --- a/packages/client/ui-primitives/README.md +++ b/packages/client/ui-primitives/README.md @@ -60,6 +60,7 @@ Check this table before writing a control in a feature package. A plugin cannot | `RiskConfirmation` | Sensitive action gated behind an explicit checkbox. | | `Tooltip` | Hover and keyboard-focus text anchored to a cloned child; optional `delayMs` controls hover delay and `focusDelayMs` controls keyboard-focus delay, both defaulting to 0 ms. Optional `portal` rendering escapes clipping containers and ancestor stacking contexts that cap the bubble's z-index. | | `HoverCard`, `HoverCardGroup` | Readable, optionally copyable hover preview; a physical list can opt cards into shared first-display dwell and exclusive display. | +| `DraggableImage`, `ImageDrop`, `ImageDragProvider` | Original-image gestures, drop binding, and application-owned transfer feedback. | | `ImageLightbox` | Shared image modal with focus restoration and Escape dismissal. | | `Toast` | Transient top-center banner held for the owner's `holdMs`. | | `SettingsForm`, `SettingsValueField`, `SettingsSecretField` | The frame and the controls of a plugin's settings page: the frame takes its copy as `labels`, saves only on its button, and discards on unmount; a value field shows staged text with the overridden badge and reset; a secret field starts blank, requests no saved-password autofill, and reports only whether a value is configured. | @@ -111,7 +112,7 @@ The nearest `MarkdownDelegateProvider` supplies optional `openExternalLink` and `ImageLightbox` is the shared original-image modal with focus restoration and Escape dismissal. Like `Modal`, its mask leaves the Windows caption unpainted, and its close control sits 20px below the caption. The internal thumbnail renderer receives loading/failure labels from its owner. `HoverCard.inline` keeps a file link in the text flow and uses the shared menu material, keyboard-visible focus, and placement above or below the anchor without covering it. Escape closes an open thumbnail even when focus is elsewhere; subsequent Escape presses reach the owner. `MarkdownDelegateProvider.fileImages` supplies a decoded-path resolver and complete image labels: settled image links gain hover previews, and standalone images gain lightbox activation. Image-only anchors retain their single navigation target. The parser recovers only complete, unescaped, standalone local image references with bare spaces and an unambiguous supported image suffix; code and ambiguous destinations remain literal. -`ImageDragProvider` wraps the application and retains the active Web image transfer and failure toast. `useImageDrag` prepares original bytes from an authorized source or supplied `File`, cancels stale preparation, and uses the Desktop bridge when present. Web transfers carry an application-local token plus Chromium’s `DownloadURL`; `useImageDrop` resolves only the active token to its original `File`. External browser drops depend on the receiving application and browser support. No drag places an internal URL or alt text in `text/plain`. +`ImageDragProvider` owns transient file transfers and failure feedback for one application. `DraggableImage` and `ImageDrop` require this ancestor and fail during rendering if it is absent; feature components pass image props or a listener-installation callback without reading React context. The provider resolves the versioned Desktop bridge once. Original bytes are prepared on hover or primary pointer down, never on mount, and obsolete preparations are cancelled. Managed Web transfers carry an application-local token plus Chromium’s `DownloadURL`; only the owning application resolves the token to the original `File`. These managed transfers do not place internal URLs or alt text in `text/plain`. ### Localizing copy @@ -201,6 +202,7 @@ None; this package neither assembles nor sends a provider request. These limits define how the atoms behave at the edges; they are current package constraints, not a component roadmap. +- **Browser image transfers** — external targets depend on Chromium `DownloadURL` and the receiving application; native file delivery is provided by Desktop. Web tokens do not transfer between windows or tabs. Unreadable cross-origin images retain browser-native dragging, as do linked Markdown images and Markdown without `fileImages`; those native paths may include URLs. - **Diff search is bounded, input processing is linear** — the edit-distance limit trades precise alignment for a coarse replacement on heavily changed fragments. Normalization, fallback rows, and copied output still scale with input size; the height cap limits visible rows, not those allocations. - **Known-site marks are a fixed list** — only the named hosts resolve to their own mark, and every other external host keeps the globe; recognizing an arbitrary site would require fetching its icon over the network. - **Streaming defers cross-boundary reference resolution** — a reference-style link or footnote whose definition sits on the other side of the incremental freeze boundary renders as literal text while the reply streams; the settled full parse at finalize resolves it. diff --git a/packages/client/ui-primitives/README.zh.md b/packages/client/ui-primitives/README.zh.md index fac88aa250..c6819de4f8 100644 --- a/packages/client/ui-primitives/README.zh.md +++ b/packages/client/ui-primitives/README.zh.md @@ -60,6 +60,7 @@ kind: "package-library" | `RiskConfirmation` | 以显式复选框把关的敏感操作确认。 | | `Tooltip` | 锚定在克隆子元素上的悬停与键盘聚焦提示;可选的 `delayMs` 控制悬停延迟,`focusDelayMs` 控制键盘聚焦延迟,均默认为 0 毫秒。可通过 `portal` 渲染到外层,避免被容器裁剪,或受祖先层叠上下文限制其 z-index。 | | `HoverCard`、`HoverCardGroup` | 可阅读、可选带复制按钮的悬停预览;物理列表可选择让卡片共享首次显示等待,并互斥显示。 | +| `DraggableImage`、`ImageDrop`、`ImageDragProvider` | 原图拖放、接收监听绑定与应用持有的传递反馈。 | | `ImageLightbox` | 共享图片浮层,支持焦点恢复与 Esc 关闭。 | | `Toast` | 顶部居中的瞬时横幅,保持时长由所有者的 `holdMs` 决定。 | | `SettingsForm`、`SettingsValueField`、`SettingsSecretField` | 插件设置页的框架与控件:框架以 `labels` 接收文案,只在按钮点击时保存,卸载即丢弃;值字段显示暂存文本以及已覆盖标签和重置;密文字段每次为空,请求浏览器不要自动填入已保存的密码,只报告是否已配置。 | @@ -111,7 +112,7 @@ kind: "package-library" `ImageLightbox` 是共享原图浮层,支持焦点恢复与 Esc 关闭。与 `Modal` 相同,其遮罩不覆盖 Windows 顶栏,关闭按钮位于顶栏下方 20px。包内缩略图渲染器由调用方提供加载及失败文案。`HoverCard.inline` 使文件链接保持在文本流内,并使用共享菜单材质、键盘可见焦点,并在锚点上方或下方定位而不遮挡锚点。即使焦点位于其他位置,Esc 也会关闭已打开的缩略图;后续 Esc 按键继续传给 owner。`MarkdownDelegateProvider.fileImages` 提供已解码路径解析器与完整图片文案:消息落定后的图片链接支持悬停预览,独立图片支持点击放大。仅包含图片的链接保留单一导航目标。解析器仅恢复完整、未转义、独立成段且带明确图片扩展名的含裸空格本地图片引用;代码与有歧义的目标保持原文。 -`ImageDragProvider` 包裹应用,持有当前 Web 图片传递状态和失败提示。`useImageDrag` 从已授权来源或传入的 `File` 准备原图字节,取消过期准备,并在 Desktop 桥接存在时使用它。Web 拖放携带应用内令牌和 Chromium 的 `DownloadURL`;`useImageDrop` 仅将当前令牌解析为原始 `File`。浏览器向外部拖放的结果取决于浏览器和接收应用的支持。拖放不会将内部 URL 或 alt 文本放入 `text/plain`。 +`ImageDragProvider` 持有单个应用的临时文件传递状态和失败提示。`DraggableImage` 与 `ImageDrop` 要求该祖先组件,缺失时在渲染阶段报错;业务组件只传图片属性或监听器安装回调,不读取 React context。Provider 只解析一次带版本的 Desktop 桥接。原图在悬停或主指针按下时准备,不在挂载时准备;过期准备会取消。受管理的 Web 拖放携带应用内令牌及 Chromium 的 `DownloadURL`,只有持有令牌的应用能将其解析为原始 `File`。这些受管理的拖放不会将内部 URL 或 alt 文本放入 `text/plain`。 ### 本地化文案 @@ -201,6 +202,7 @@ kind: "package-library" 这些限制说明原子组件在边缘情况下的行为;它们是当前包约束,不是组件路线图。 +- **浏览器图片传递**:外部接收依赖 Chromium 的 `DownloadURL` 与接收应用;原生文件交付由 Desktop 提供。Web 令牌不跨窗口或标签页传递。不可读取的跨源图片保留浏览器原生拖动,链接内的 Markdown 图片及未配置 `fileImages` 的 Markdown 也保留原生行为;这些原生路径可能携带 URL。 - **Diff 搜索有上限,输入处理仍为线性**:编辑距离上限使大量改动的片段采用粗粒度替换,不再精确对齐。规范化、回退行及复制内容仍随输入大小增长;高度限制只约束可见行数,不限制这些分配。 - **已知站点标记是固定列表**:只有列名的主机解析为自己的标记,其余外部主机仍使用地球;要识别任意站点需要通过网络抓取它的图标。 - **流式期间跨边界引用解析被推迟**:定义落在增量冻结边界另一侧的引用式链接或脚注,在回复流式输出期间渲染为字面文本;定稿时的全量解析会将其解析。 diff --git a/packages/client/ui-primitives/src/DraggableImage.tsx b/packages/client/ui-primitives/src/DraggableImage.tsx new file mode 100644 index 0000000000..6c99e0ab51 --- /dev/null +++ b/packages/client/ui-primitives/src/DraggableImage.tsx @@ -0,0 +1,24 @@ +/** Original-image drag gestures owned by a presentation primitive. */ +import { forwardRef, type ImgHTMLAttributes } from 'react' +import { useImageDrag } from './image-drag.ts' + +/** Image presentation plus the original bytes and localized drag failure. */ +export interface DraggableImageProps extends Omit, 'src' | 'draggable' | 'onDragStart' | 'onDragEnd' | 'onPointerEnter' | 'onPointerDown' | 'onClickCapture'> { + readonly src: string + readonly name: string + readonly failureText: string + readonly original?: File | undefined +} + +/** + * Render an image with original-file dragging; requires an ancestor ImageDragProvider. + * @param props - image attributes, original filename and localized failure text. + * @param ref - image element for measurement and pointer-mode bindings. + * @returns a draggable image that suppresses only the mouse click following its drag. + */ +export const DraggableImage = forwardRef(function DraggableImage( + { src, name, failureText, original, ...props }, ref, +) { + const drag = useImageDrag(src, name, failureText, original) + return +}) diff --git a/packages/client/ui-primitives/src/ImageDragProvider.tsx b/packages/client/ui-primitives/src/ImageDragProvider.tsx index f6c2cece0a..2027011017 100644 --- a/packages/client/ui-primitives/src/ImageDragProvider.tsx +++ b/packages/client/ui-primitives/src/ImageDragProvider.tsx @@ -1,21 +1,20 @@ /** Application-lifetime feedback for image drag operations. */ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from 'react' import { Toast } from './Toast.tsx' +import type { DesktopImageDrag } from './image-drag.ts' /** Browser drag token; file bytes remain in the owning mounted application. */ export const IMAGE_DRAG_TYPE = 'application/x-dsh-image' interface ImageTransfer { + desktop: DesktopImageDrag | undefined report: (text: string) => void begin: (file: File) => string end: () => void read: (transfer: DataTransfer) => File | undefined } -const missingProvider = (): never => { throw new Error('Image dragging requires ImageDragProvider') } -const ImageDragFeedback = createContext({ - report: missingProvider, begin: missingProvider, end: missingProvider, read: missingProvider, -}) +const ImageDragFeedback = createContext(undefined) /** * Keep drag failures visible when their source image or conversation unmounts. @@ -23,6 +22,10 @@ const ImageDragFeedback = createContext({ * @returns children and the current localized failure toast. */ export function ImageDragProvider({ children }: { readonly children: ReactNode }): ReactNode { + const [desktop] = useState(() => { + const bridge = (globalThis as typeof globalThis & { dshDesktop?: { protocolVersion: number; imageDrag?: DesktopImageDrag } }).dshDesktop + return bridge?.protocolVersion === 1 ? bridge.imageDrag : undefined + }) const active = useRef<{ token: string; file: File }>() const sequence = useRef(0) const [failure, setFailure] = useState<{ text: string; sequence: number }>() @@ -42,19 +45,33 @@ export function ImageDragProvider({ children }: { readonly children: ReactNode } const current = active.current return current !== undefined && current.token === transfer.getData(IMAGE_DRAG_TYPE) ? current.file : undefined }, []) - const value = useMemo(() => ({ report, begin, end, read }), [report, begin, end, read]) + const value = useMemo(() => ({ desktop, report, begin, end, read }), [desktop, report, begin, end, read]) return {children} {failure !== undefined && } } -/** @returns the mounted application's image transfer owner. */ +/** Requires ImageDragProvider; @returns the mounted application's image transfer owner. */ export function useImageDragTransfer(): ImageTransfer { - return useContext(ImageDragFeedback) + const transfer = useContext(ImageDragFeedback) + if (transfer === undefined) throw new Error('Image dragging requires ImageDragProvider') + return transfer } -/** @returns a reader that resolves only this application's active original-image drag. */ +/** Requires ImageDragProvider; @returns a reader for this application's active original-image drag. */ export function useImageDrop(): (transfer: DataTransfer) => File | undefined { return useImageDragTransfer().read } + +/** + * Bind a drop target to its application's original-image reader without exposing React context. + * Requires an ancestor ImageDragProvider. + * @param props - listener installer; its cleanup runs on replacement or unmount. + * @returns no visible content. + */ +export function ImageDrop({ bind }: { readonly bind: (read: (transfer: DataTransfer) => File | undefined) => () => void }): ReactNode { + const read = useImageDrop() + useEffect(() => bind(read), [bind, read]) + return null +} diff --git a/packages/client/ui-primitives/src/image-drag.ts b/packages/client/ui-primitives/src/image-drag.ts index 80db3219bf..ad56f51b25 100644 --- a/packages/client/ui-primitives/src/image-drag.ts +++ b/packages/client/ui-primitives/src/image-drag.ts @@ -16,6 +16,7 @@ export interface DesktopImageDrag { interface PreparedImage { readonly file: File readonly handle?: string + readonly release?: () => void } interface ImageDragHandlers { @@ -28,6 +29,7 @@ interface ImageDragHandlers { /** * Prepare original bytes on hover and supply them synchronously when dragging begins. + * Requires an ancestor ImageDragProvider; it owns transfer bytes and feedback across source unmounts. * @param src - authorized image URL; changing it retires pending preparation. * @param name - suggested original filename, without a directory. * @param failureText - localized failure message, including a retry action. @@ -35,11 +37,16 @@ interface ImageDragHandlers { * @returns drag handlers; cancellation never starts a late drag. */ export function useImageDrag(src: string, name: string, failureText: string, original?: File): ImageDragHandlers { - const desktop = (globalThis as typeof globalThis & { dshDesktop?: { imageDrag?: DesktopImageDrag } }).dshDesktop?.imageDrag - const state = useRef<{ source: string; prepared?: PreparedImage; pending?: Promise | undefined; abort: AbortController }>() + const state = useRef<{ + source: string + prepared?: PreparedImage | undefined + pending?: Promise | undefined + abort: AbortController + }>() const dragged = useRef(false) const transfer = useImageDragTransfer() const reportFailure = transfer.report + const desktop = transfer.desktop const prepare = useCallback((): void => { const current = state.current if (src === '' || current === undefined || current.source !== src || current.pending !== undefined || current.prepared !== undefined) return @@ -53,13 +60,16 @@ export function useImageDrag(src: string, name: string, failureText: string, ori file = new File([blob], name, { type: blob.type }) } if (current.abort.signal.aborted) return - const handle = desktop === undefined ? undefined - : await desktop.prepare(new Uint8Array(await file.arrayBuffer()), file.name, file.type) - if (state.current !== current) { - if (handle !== undefined) desktop?.release(handle) + if (desktop === undefined) { + current.prepared = { file } return } - current.prepared = { file, ...handle === undefined ? {} : { handle } } + const handle = await desktop.prepare(new Uint8Array(await file.arrayBuffer()), file.name, file.type) + if (state.current !== current) { + desktop.release(handle) + return + } + current.prepared = { file, handle, release: () => { desktop.release(handle) } } })().catch((_error: unknown) => { // Preparation is speculative; a failed user gesture reports through the owning image. }).finally(() => { current.pending = undefined }) @@ -67,19 +77,19 @@ export function useImageDrag(src: string, name: string, failureText: string, ori useEffect(() => { const current: NonNullable = { source: src, abort: new AbortController() } state.current = current - if (src !== '' && (original !== undefined || new URL(src, location.href).origin === location.origin)) prepare() return () => { current.abort.abort() - const prepared = current.prepared - if (prepared?.handle !== undefined) desktop?.release(prepared.handle) + current.prepared?.release?.() state.current = undefined } }, [src, name, original, desktop, prepare]) const onDragStart = useCallback((event: DragEvent): void => { - if (event.defaultPrevented) return + if (event.defaultPrevented || src === '') return dragged.current = true const prepared = state.current?.source === src ? state.current.prepared : undefined if (prepared === undefined) { + const url = new URL(src, location.href) + if (original === undefined && (url.protocol === 'https:' || url.protocol === 'http:') && url.origin !== location.origin) return event.preventDefault() prepare() reportFailure(failureText) @@ -89,8 +99,11 @@ export function useImageDrag(src: string, name: string, failureText: string, ori event.dataTransfer.effectAllowed = 'copy' if (desktop !== undefined && prepared.handle !== undefined) { event.preventDefault() - void desktop.start(prepared.handle).then((started) => { if (!started) reportFailure(failureText) }, - () => { reportFailure(failureText) }) + const failed = (): void => { + if (state.current?.prepared === prepared) state.current.prepared = undefined + reportFailure(failureText) + } + void desktop.start(prepared.handle).then((started) => { if (!started) failed() }, failed) return } event.dataTransfer.setData(IMAGE_DRAG_TYPE, transfer.begin(prepared.file)) @@ -101,6 +114,7 @@ export function useImageDrag(src: string, name: string, failureText: string, ori if (event.button === 0) prepare() }, [prepare]) const onClickCapture = useCallback((event: MouseEvent): void => { + if (event.detail === 0) { dragged.current = false; return } if (!dragged.current) return event.preventDefault() event.stopPropagation() diff --git a/packages/client/ui-primitives/src/index.ts b/packages/client/ui-primitives/src/index.ts index 28a3a71fd0..bdd9d37e14 100644 --- a/packages/client/ui-primitives/src/index.ts +++ b/packages/client/ui-primitives/src/index.ts @@ -67,8 +67,9 @@ export { pointerModality } from './input-modality.ts' export { Toast } from './Toast.tsx' export { fileSizeText } from './file-size.ts' export { writeClipboard } from './clipboard.ts' -export { IMAGE_DRAG_TYPE, ImageDragProvider, useImageDrop } from './ImageDragProvider.tsx' -export { useImageDrag } from './image-drag.ts' +export { IMAGE_DRAG_TYPE, ImageDragProvider, ImageDrop } from './ImageDragProvider.tsx' +export { DraggableImage } from './DraggableImage.tsx' +export type { DraggableImageProps } from './DraggableImage.tsx' export type { DesktopImageDrag } from './image-drag.ts' export { SettingsForm } from './settings-form/SettingsForm.tsx' export type { SettingsFormLabels, SettingsFormProps } from './settings-form/SettingsForm.tsx' diff --git a/packages/client/ui-primitives/src/markdown/render.tsx b/packages/client/ui-primitives/src/markdown/render.tsx index 05362b438e..3793d0f181 100644 --- a/packages/client/ui-primitives/src/markdown/render.tsx +++ b/packages/client/ui-primitives/src/markdown/render.tsx @@ -29,7 +29,7 @@ import { renderTexToReact } from './katex.tsx' import { LinkIconMedium, classifyLinkPath } from '../LinkIcon.tsx' import { useMarkdownDelegate } from './MarkdownDelegate.tsx' import { HoverCard } from '../HoverCard.tsx' -import { useImageDrag } from '../image-drag.ts' +import { DraggableImage } from '../DraggableImage.tsx' import { ImageLightbox } from '../ImageLightbox.tsx' import { ImagePreview } from '../ImagePreview.tsx' import type { PositionedBlock } from './incremental.ts' @@ -651,17 +651,16 @@ function LoadedMarkdownImage({ src, alt, destination, preview }: { const [failed, setFailed] = useState(false) const [open, setOpen] = useState(false) const close = useCallback(() => { setOpen(false) }, []) - const drag = useImageDrag(preview === undefined ? '' : src, destination.split('/').at(-1)?.split(/[?#]/u)[0] || 'image', preview?.labels.dragFailed ?? '') if (failed) return {preview === undefined ? '' : `${preview.labels.failed} · `}{alt || destination} - const img = {alt} { setFailed(true) }} loading="lazy" decoding="async" referrerPolicy="no-referrer" /> - if (preview === undefined) return img + const imageProps = { className: css.image, src, alt, onError: () => { setFailed(true) }, + loading: 'lazy' as const, decoding: 'async' as const, referrerPolicy: 'no-referrer' as const } + if (preview === undefined) return + const img = return <> + onClick={() => { setOpen(true) }}>{img} {open && } } diff --git a/packages/client/ui-primitives/tests/image-drag.client.spec.tsx b/packages/client/ui-primitives/tests/image-drag.client.spec.tsx index 740178bd33..712683b863 100644 --- a/packages/client/ui-primitives/tests/image-drag.client.spec.tsx +++ b/packages/client/ui-primitives/tests/image-drag.client.spec.tsx @@ -1,15 +1,16 @@ // @vitest-environment jsdom +import { useEffect } from 'react' import { File as NodeFile } from 'node:buffer' import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterEach, expect, it, vi } from 'vitest' import { IMAGE_DRAG_TYPE, ImageDragProvider, useImageDrop } from '../src/ImageDragProvider.tsx' +import { DraggableImage } from '../src/DraggableImage.tsx' import { useImageDrag, type DesktopImageDrag } from '../src/image-drag.ts' afterEach(() => { cleanup(); vi.unstubAllGlobals(); vi.restoreAllMocks() }) function Image({ src, file, onOpen }: { src: string; file?: File; onOpen?: () => void }) { - const drag = useImageDrag(src, 'original.png', 'Image drag failed. Try again.', file) - return + return } function transfer() { const add = vi.fn() @@ -19,7 +20,7 @@ function transfer() { } function native() { const bridge: DesktopImageDrag = { prepare: vi.fn(async () => 'handle'), start: vi.fn(async () => true), release: vi.fn() } - vi.stubGlobal('dshDesktop', { imageDrag: bridge }) + vi.stubGlobal('dshDesktop', { protocolVersion: 1, imageDrag: bridge }) return bridge } function original() { @@ -31,16 +32,17 @@ it('hands original bytes to the browser without alt text or an internal URI payl const open = vi.fn() render() const dataTransfer = transfer() + await act(async () => { fireEvent.pointerEnter(screen.getByRole('img')) }) await act(async () => { fireEvent.dragStart(screen.getByRole('img'), { dataTransfer }) }) expect(dataTransfer.items.add).not.toHaveBeenCalled() expect(dataTransfer.items.clear).toHaveBeenCalledOnce() expect(dataTransfer.setData).toHaveBeenCalledWith(IMAGE_DRAG_TYPE, expect.any(String)) expect(dataTransfer.effectAllowed).toBe('copy') expect(dataTransfer.setData).toHaveBeenCalledWith('DownloadURL', 'image/png:original.png:blob:original') - fireEvent.click(screen.getByRole('button')) + fireEvent.click(screen.getByRole('img'), { detail: 1 }) expect(open).not.toHaveBeenCalled() fireEvent.pointerDown(screen.getByRole('img'), { button: 0 }) - fireEvent.click(screen.getByRole('button')) + fireEvent.click(screen.getByRole('img'), { detail: 1 }) expect(open).toHaveBeenCalledOnce() }) @@ -48,7 +50,7 @@ it('prepares before the gesture, starts native file dragging and releases its ha const bridge = native() const file = original() const view = render() - await act(async () => { await Promise.resolve() }) + await act(async () => { fireEvent.pointerEnter(screen.getByRole('img')) }) expect(bridge.prepare).toHaveBeenCalledExactlyOnceWith(new Uint8Array([137, 80, 78, 71]), 'original.png', 'image/png') const dataTransfer = transfer() await act(async () => { expect(fireEvent.dragStart(screen.getByRole('img'), { dataTransfer })).toBe(false) }) @@ -63,7 +65,7 @@ it('cancels an unready gesture and releases late preparation without starting a const pending = Promise.withResolvers() vi.mocked(bridge.prepare).mockReturnValue(pending.promise) const view = render() - await act(async () => { await Promise.resolve() }) + await act(async () => { fireEvent.pointerEnter(screen.getByRole('img')) }) fireEvent.dragStart(screen.getByRole('img'), { dataTransfer: transfer() }) expect(screen.getByRole('alert').textContent).toBe('Image drag failed. Try again.') view.rerender(Image closed) @@ -78,7 +80,7 @@ it('reports native failure even after its source image closes', async () => { const pending = Promise.withResolvers() vi.mocked(bridge.start).mockReturnValue(pending.promise) const view = render() - await act(async () => { await Promise.resolve() }) + await act(async () => { fireEvent.pointerEnter(screen.getByRole('img')) }) fireEvent.dragStart(screen.getByRole('img'), { dataTransfer: transfer() }) view.rerender(Image closed) await act(async () => { pending.resolve(false); await pending.promise }) @@ -105,11 +107,12 @@ function DropTarget({ receive }: { receive: (file: File | undefined) => void }) return
{ event.preventDefault(); receive(read(event.dataTransfer)) }} /> } -it('resolves only the active application drag token and clears it after dragging ends', () => { +it('resolves only the active application drag token and clears it after dragging ends', async () => { const file = original() const receive = vi.fn() render() const dataTransfer = transfer() + await act(async () => { fireEvent.pointerEnter(screen.getByRole('img')) }) fireEvent.dragStart(screen.getByRole('img'), { dataTransfer }) fireEvent.drop(screen.getByTestId('drop'), { dataTransfer }) expect(receive).toHaveBeenLastCalledWith(file) @@ -117,3 +120,127 @@ it('resolves only the active application drag token and clears it after dragging fireEvent.drop(screen.getByTestId('drop'), { dataTransfer }) expect(receive).toHaveBeenLastCalledWith(undefined) }) + +it('does not prepare on mount, ignores secondary buttons, and keeps keyboard activation after dragging', async () => { + const bridge = native() + const open = vi.fn() + render() + expect(bridge.prepare).not.toHaveBeenCalled() + fireEvent.pointerDown(screen.getByRole('img'), { button: 2 }) + expect(bridge.prepare).not.toHaveBeenCalled() + await act(async () => { fireEvent.pointerEnter(screen.getByRole('img')) }) + fireEvent.dragStart(screen.getByRole('img'), { dataTransfer: transfer() }) + fireEvent.dragEnd(screen.getByRole('img'), { dataTransfer: transfer() }) + fireEvent.click(screen.getByRole('button'), { detail: 0 }) + expect(open).toHaveBeenCalledOnce() + fireEvent.click(screen.getByRole('img'), { detail: 0 }) + expect(open).toHaveBeenCalledTimes(2) +}) + +it('reports rejected native starts and accepts later preparation attempts', async () => { + const bridge = native() + vi.mocked(bridge.prepare).mockRejectedValueOnce(new Error('read failed')) + vi.mocked(bridge.start).mockRejectedValueOnce(new Error('drag failed')) + render() + await act(async () => { fireEvent.pointerEnter(screen.getByRole('img')) }) + await act(async () => { fireEvent.pointerEnter(screen.getByRole('img')) }) + await act(async () => { fireEvent.dragStart(screen.getByRole('img'), { dataTransfer: transfer() }) }) + expect(screen.getByRole('alert').textContent).toBe('Image drag failed. Try again.') +}) + +it.each(['image/png', 'text/plain'])('cancels late fetched %s responses after unmount', async (type) => { + const bridge = native() + const pending = Promise.withResolvers() + vi.stubGlobal('fetch', vi.fn().mockReturnValue(pending.promise)) + const view = render() + fireEvent.pointerEnter(screen.getByRole('img')) + view.unmount() + await act(async () => { pending.resolve(new Response('bytes', { headers: { 'Content-Type': type } })); await pending.promise }) + expect(bridge.prepare).not.toHaveBeenCalled() +}) + +it('fetches a readable source once and rejects a foreign drag token', async () => { + const fetchImage = vi.fn().mockResolvedValue(new Response('bytes', { headers: { 'Content-Type': 'image/png' } })) + vi.stubGlobal('fetch', fetchImage) + const receive = vi.fn() + render() + await act(async () => { fireEvent.pointerEnter(screen.getByRole('img')); fireEvent.pointerEnter(screen.getByRole('img')) }) + fireEvent.pointerEnter(screen.getByRole('img')) + expect(fetchImage).toHaveBeenCalledOnce() + const dataTransfer = transfer() + fireEvent.dragStart(screen.getByRole('img'), { dataTransfer }) + dataTransfer.setData(IMAGE_DRAG_TYPE, 'foreign') + fireEvent.drop(screen.getByTestId('drop'), { dataTransfer }) + expect(receive).toHaveBeenCalledWith(undefined) +}) + +it.each(['https://other.test/image.png', 'http://other.test/image.png'])('leaves unreadable external %s drags native', async (src) => { + vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new TypeError('CORS'))) + render() + await act(async () => { fireEvent.pointerEnter(screen.getByRole('img')) }) + const dataTransfer = transfer() + expect(fireEvent.dragStart(screen.getByRole('img'), { dataTransfer })).toBe(true) + expect(dataTransfer.items.clear).not.toHaveBeenCalled() + expect(screen.queryByRole('alert')).toBeNull() +}) + +it('keeps disabled and already-cancelled gestures inert', () => { + const bridge = native() + const view = render() + fireEvent.pointerEnter(screen.getByRole('img')) + const dataTransfer = transfer() + expect(fireEvent.dragStart(screen.getByRole('img'), { dataTransfer })).toBe(true) + view.rerender(
{ event.preventDefault() }}>
) + expect(fireEvent.dragStart(screen.getByRole('img'), { dataTransfer })).toBe(false) + expect(bridge.prepare).not.toHaveBeenCalled() + expect(bridge.start).not.toHaveBeenCalled() +}) + +it('ignores unsupported desktop protocols and requires a provider for drop resolution', () => { + vi.stubGlobal('dshDesktop', { protocolVersion: 2 }) + const receive = vi.fn() + const view = render() + fireEvent.drop(screen.getByTestId('drop'), { dataTransfer: transfer() }) + expect(receive).toHaveBeenCalledWith(undefined) + view.unmount() + function WithoutProvider() { useImageDrop(); return null } + const error = vi.spyOn(console, 'error').mockImplementation(() => {}) + expect(() => render()).toThrow('Image dragging requires ImageDragProvider') + error.mockRestore() +}) + +it('dismisses failure feedback after its presentation lifetime', async () => { + vi.useFakeTimers() + try { + render() + fireEvent.dragStart(screen.getByRole('img'), { dataTransfer: transfer() }) + expect(screen.getByRole('alert')).toBeTruthy() + await act(async () => { await vi.runAllTimersAsync() }) + expect(screen.queryByRole('alert')).toBeNull() + } finally { vi.useRealTimers() } +}) + +it('reports an unready same-origin HTTP image instead of exporting its URL', async () => { + const pending = Promise.withResolvers() + vi.stubGlobal('fetch', vi.fn().mockReturnValue(pending.promise)) + render() + expect(fireEvent.dragStart(screen.getByRole('img'), { dataTransfer: transfer() })).toBe(false) + expect(screen.getByRole('alert')).toBeTruthy() + await act(async () => { pending.resolve(new Response('failure', { status: 404 })); await pending.promise }) +}) + + +it('cancels a handler retained after its image unmounts', () => { + let handlers!: ReturnType + function Source() { + const drag = useImageDrag('blob:original', 'original.png', 'Image drag failed') + useEffect(() => { handlers = drag }, [drag]) + return null + } + const view = render() + view.rerender(Closed) + const event = { defaultPrevented: false, preventDefault: vi.fn(), dataTransfer: transfer() } + const captured: Parameters[0] = Object.assign({} as Parameters[0], event) + act(() => { handlers.onDragStart(captured) }) + expect(event.preventDefault).toHaveBeenCalledOnce() +}) diff --git a/packages/client/ui-primitives/tests/markdown-image-entries.client.spec.tsx b/packages/client/ui-primitives/tests/markdown-image-entries.client.spec.tsx index 72cff3c14e..686f44b75c 100644 --- a/packages/client/ui-primitives/tests/markdown-image-entries.client.spec.tsx +++ b/packages/client/ui-primitives/tests/markdown-image-entries.client.spec.tsx @@ -1,5 +1,7 @@ // @vitest-environment jsdom -import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' +import { act, cleanup, fireEvent, render as renderBare, screen } from '@testing-library/react' +import { ImageDragProvider } from '@deepseek-ai/dsh-client-ui-primitives' +const render = (ui: import('react').ReactNode) => renderBare(ui, { wrapper: ImageDragProvider }) import { afterEach, expect, it, vi } from 'vitest' import { fileMediaUrl } from '@deepseek-ai/dsh-util-workspace-path' import { MarkdownDelegateProvider } from '../src/markdown/MarkdownDelegate.tsx' @@ -147,3 +149,8 @@ it.each([parseGfm, parseGfmWithMath])('uses the shared image categories for bare } expect(JSON.stringify(parse('![report](/work/test dir/report.pdf)'))).not.toContain('"type":"image"') }) + +it('uses the fallback filename for an external image URL without a path', () => { + mount('![Remote](https://example.com/)') + expect(screen.getByRole('img').getAttribute('src')).toBe('https://example.com/') +}) diff --git a/packages/client/ui-sidebar-documentpreview/src/client/image/ImageBody.tsx b/packages/client/ui-sidebar-documentpreview/src/client/image/ImageBody.tsx index 8da0834551..649a4cee6d 100644 --- a/packages/client/ui-sidebar-documentpreview/src/client/image/ImageBody.tsx +++ b/packages/client/ui-sidebar-documentpreview/src/client/image/ImageBody.tsx @@ -1,6 +1,6 @@ /** Complete image bytes rendered in a shared zoom viewport. */ import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from 'react' -import { useImageDrag } from '@deepseek-ai/dsh-client-ui-primitives' +import { DraggableImage } from '@deepseek-ai/dsh-client-ui-primitives' import { bindImagePan } from './pan.ts' import type { PropsLocale, PropsStore } from '@deepseek-ai/dsh-client-ui-slots' import { pathPartsOf } from '@deepseek-ai/dsh-util-workspace-path' @@ -74,8 +74,9 @@ export function ImageBody(props: ImageBodyProps): ReactNode { if (data === undefined || mediaType === undefined) return let url: string | undefined try { - url = URL.createObjectURL(new Blob([data], { type: mediaType })) - setSource({ kind: 'ready', data, mediaType, url, file: new File([data], pathPartsOf(path).name, { type: mediaType }) }) + const file = new File([data], pathPartsOf(path).name, { type: mediaType }) + url = URL.createObjectURL(file) + setSource({ kind: 'ready', data, mediaType, url, file }) } catch { setSource({ kind: 'failed', data, mediaType }) } @@ -113,7 +114,6 @@ function LoadedImage({ url, name, file, preference, onPreference, labels, signal readonly scrollportRef: ImageBodyProps['scrollportRef'] readonly t: ImageBodyProps['t'] }): ReactNode { - const drag = useImageDrag(url, name, t('dragFailed'), file) const releasePan = useRef<() => void>() const attachImage = useCallback((image: HTMLImageElement | null): void => { releasePan.current?.() @@ -128,15 +128,13 @@ function LoadedImage({ url, name, file, preference, onPreference, labels, signal {state === 'failed' &&

{t('failed')}

}