From 03d95d0d8499828a7719de159e6e082781097e60 Mon Sep 17 00:00:00 2001 From: Dudu-0223 Date: Mon, 27 Jul 2026 17:22:40 +0800 Subject: [PATCH] fix(subagent): keep one-shot labels optional --- ...subagent-catalog-and-list-agents.i18n.yaml | 4 +- ...urable-subagent-catalog-and-list-agents.md | 10 +-- ...ble-subagent-catalog-and-list-agents.zh.md | 10 +-- docs/core-data-structures/subagent.i18n.yaml | 4 +- docs/core-data-structures/subagent.md | 8 +-- docs/core-data-structures/subagent.zh.md | 8 +-- .../cordis/tool-cordis/src/api-catalog.ts | 8 +-- .../subagent-acp/tests/subagent-acp.e2e.ts | 2 - .../subagent-acp/tests/subagent-acp.spec.ts | 26 +++----- .../tests/multi-subagent.spec.ts | 12 +--- .../subagent-fork/tests/subagent-fork.spec.ts | 12 +--- .../tests/subagent-spawn.spec.ts | 12 +--- packages/subagent/subagent/README.i18n.yaml | 4 +- packages/subagent/subagent/README.md | 6 +- packages/subagent/subagent/README.zh.md | 6 +- packages/subagent/subagent/src/descriptor.ts | 62 +++++++++++-------- packages/subagent/subagent/src/index.ts | 2 +- .../subagent/subagent/src/list-children.ts | 35 +++++++---- packages/subagent/subagent/src/types.ts | 4 +- .../subagent/tests/list-children.spec.ts | 2 - .../subagent/subagent/tests/service.spec.ts | 16 +++-- .../tests/scripted-provider.spec.ts | 1 - packages/ui/jsonrpc/tests/server.spec.ts | 7 --- .../workflow-workerthread/src/host.ts | 1 - .../workflow-workerthread/src/runtime.ts | 1 - .../workflow-workerthread/src/types.ts | 2 - .../tests/session.spec.ts | 3 - .../tests/workflow-workerthread.spec.ts | 3 +- 28 files changed, 124 insertions(+), 147 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.i18n.yaml b/.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.i18n.yaml index e52024a383..1eb201ba6d 100644 --- a/.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md -2026-07-22-durable-subagent-catalog-and-list-agents.md: 16d81e4fa5cdbf63fde5e8c42fbe50ec18b18718 -2026-07-22-durable-subagent-catalog-and-list-agents.zh.md: 5cdce5059b85c17a4ca3b6b22e57a0bc070f3e26 +2026-07-22-durable-subagent-catalog-and-list-agents.md: b288c113d38e067d323513a3c73afb15163ef76e +2026-07-22-durable-subagent-catalog-and-list-agents.zh.md: b6050239b45d375f554b6b0f5e488fbe1a5c045b diff --git a/.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md b/.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md index 16d81e4fa5..b288c113d3 100644 --- a/.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md +++ b/.agents/notes/implemented/feature/2026-07-22-durable-subagent-catalog-and-list-agents.md @@ -21,7 +21,7 @@ Parent-to-child enumeration is a service capability with consumer-specific proje - report corpus activity separately as `running` or `inactive`, without implying completion or resumability; - return every resulting child in stable `createdAt` ascending, child-id ascending order. -Every ordinary local start receives a `one-shot` descriptor, while the continuation manager persists a `continuable` descriptor containing its additional reconstruction fields. The model-facing `list_agents` adapter filters the service result to continuable children and maps `inactive` to its existing `complete` presentation; a UI can consume both modes and decide how to render inactive one-shot history. Descriptor persistence, by-id lookup, direct-parent authorization, and provider-independent cold resume remain owned by the implemented Activation contract. Listing consumes those facts but cannot weaken them or invent a second descriptor representation. +Every ordinary local start receives a `one-shot` descriptor with an optional caller-owned display label, while the continuation manager persists a labeled `continuable` descriptor containing its additional reconstruction fields. The model-facing delegation tool already owns a short `description` and supplies it for one-shot display; lower-level callers such as workflows need not invent presentation metadata. The model-facing `list_agents` adapter filters the service result to continuable children and maps `inactive` to its existing `complete` presentation; a UI can consume both modes and choose an id-based fallback for unlabeled one-shot history. Descriptor persistence, by-id lookup, direct-parent authorization, and provider-independent cold resume remain owned by the implemented Activation contract. Listing consumes those facts but cannot weaken them or invent a second descriptor representation. ### Enumeration decision @@ -29,7 +29,7 @@ The first implementation consumes `ctx.sessionQuery.traceSession(parentSessionId Corpus construction precedes per-child descriptor inspection. A failure while building the initial trace, including persistence listing failure, a live/persisted header conflict anywhere in the observed corpus, or invalid target lineage, fails the whole `list_agents` call because no trustworthy candidate set exists. Only failures after a successful trace are isolated to one candidate; "corrupt child" in that per-child contract therefore means corrupt loaded event surface or descriptor data, not a corpus-level header conflict. -Session lineage is broader than subagent identity: an ordinary `ctx.sessions.fork()` also creates a direct child. The session header gains no `kind` discriminator; each candidate must instead contain exactly one valid `subagent/descriptor` event in its own suffix. `SubagentService.start()` resolves `{ mode: 'one-shot', provider, label }` before ordinary provider dispatch, while the continuation manager snapshots and seeds `{ mode: 'continuable', ...composition }` during initial child creation. The local in-process one-shot driver appends its resolved descriptor only during initial creation, and cold resume appends no further descriptor; a second event is corruption rather than evidence of another Activation. The event is the sole evidence that a traced child is a session-backed subagent. A candidate without it is an ordinary fork, a remote child without a local session record, or another non-subagent session and is omitted without a diagnostic. +Session lineage is broader than subagent identity: an ordinary `ctx.sessions.fork()` also creates a direct child. The session header gains no `kind` discriminator; each candidate must instead contain exactly one valid `subagent/descriptor` event in its own suffix. `SubagentService.start()` resolves `{ mode: 'one-shot', provider, label? }` before ordinary provider dispatch, while the continuation manager snapshots and seeds `{ mode: 'continuable', ...composition }` during initial child creation. The local in-process one-shot driver appends its resolved descriptor only during initial creation, and cold resume appends no further descriptor; a second event is corruption rather than evidence of another Activation. The event is the sole evidence that a traced child is a session-backed subagent. A candidate without it is an ordinary fork, a remote child without a local session record, or another non-subagent session and is omitted without a diagnostic. The published logical record is also the activity source: `SessionRecord.live` means `running`, while `live: false, persisted: true` means `inactive`. Activity comes directly from the trace and causes no additional child-log load. `inactive` encodes neither successful completion nor resumability: it may describe settled one-shot history or a continuable child for which `send_message` can materialize another Activation. Conversely, `running` says only that the session is live: a live continuable Agent outside the continuation manager's matching Activation still appears as `running`, but `send_message` rejects it as an ownership conflict. A child is not visible before its session is published, and no process-local Activation entry is added as a second candidate or activity source. Listing is a snapshot that may race publication, disposal, or a later message; `send_message` remains the authoritative delivery-time operation. @@ -45,7 +45,7 @@ If measured scale later requires an index, that index is derived state: session `SubagentService.listChildren(parentSessionId: SessionId)` returns `Promise`, one array preserving the trace's candidate order rather than separate child and diagnostic arrays. `SubagentListEntry` is a closed union discriminated by its readonly `kind`: -- `kind: 'child'` carries readonly `id: SessionId`, durable `label: string`, `mode: 'one-shot' | 'continuable'`, and `activity: 'running' | 'inactive'`; +- `kind: 'child'` carries readonly `id: SessionId`, `mode: 'one-shot' | 'continuable'`, and `activity: 'running' | 'inactive'`; a continuable child carries `label: string`, while a one-shot child carries `label?: string`; - `kind: 'diagnostic'` carries readonly `id: SessionId` and `reason: 'corrupt' | 'unsupported' | 'unavailable'`. A valid descriptor produces one child entry, a per-child inspection failure produces one diagnostic entry, and a candidate without a descriptor produces no entry. `mode` is durable creation policy; `activity` is a process-local corpus snapshot. Activity is neither `AgentStatus`, the manager's internal Activation state, nor a durable outcome, and the result does not expose the internal `createdAt` sorting key. Exact Activation states and durable outcomes such as successful completion, failure, cancellation, and stop reason require a separate durable activation record and are outside this feature. @@ -78,6 +78,8 @@ The first version has no child deletion operation. If later product behavior del **Persist a parent-session catalog event.** Direct-child headers already provide the durable enumeration seed, and the child descriptor is the reconstruction authority. A second parent log duplicates state and creates cross-session ordering and stale-entry behavior without helping by-id resume. +**Require a display label on every raw start.** This guarantees uniform UI text but makes a presentation concern part of workflow, transport, test, and programmatic start contracts. The raw request keeps the label optional; high-level delegation and continuation APIs supply one where they already own the concept, and UI consumers choose a fallback for unlabeled one-shot children. + **Fail the whole listing when one child cannot be loaded.** This makes corruption impossible to overlook, but one damaged sibling removes visibility into every healthy child. Per-child diagnostics preserve discovery while keeping each omission explicit. **Return separate child and diagnostic arrays.** Separate arrays introduce two ordering domains or require exposing another sort key to reconstruct candidate order. One discriminated entry array preserves the trace order while keeping child and diagnostic fields type-safe. @@ -88,7 +90,7 @@ The first version has no child deletion operation. If later product behavior del ## Testing -- `packages/subagent/subagent/tests/service.spec.ts` pins descriptor v2 parsing for both modes and proves raw start resolves a one-shot descriptor before provider dispatch. `packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts` proves the local driver appends that descriptor inside the initial turn. Workflow and delegation-tool tests pin the display label across their respective start boundaries. +- `packages/subagent/subagent/tests/service.spec.ts` pins descriptor v2 parsing for both modes and proves an unlabeled raw start resolves a one-shot descriptor before provider dispatch. `packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts` proves the local driver appends that descriptor inside the initial turn. Delegation-tool tests pin propagation of their existing display description. - `packages/subagent/subagent/tests/list-children.spec.ts` pins a query-only composition with sessions, `subagents`, and `sessionQuery` but no `agents`, then drives the full real stack (agent loop, JSONL persistence, spawn/fork providers, the subagent service, and a concrete session-query service) keylessly: one-shot and continuable children from one real trace; a persisted (restart-shaped) parent target; `createdAt`-then-id ordering with authored ties; ordinary-fork and fork-seed ancestor-descriptor exclusion without diagnostics; live `running` vs persisted `inactive`; duplicate-descriptor, malformed-payload, invalid-surface, mismatched-header, and changed-read-target corruption diagnostics that leave healthy siblings visible; unsupported-version and per-child unavailable diagnostics; provider absence without child omission; compacted/uncompacted twins listing identically; grandchild exclusion; trace-phase failure failing the whole call while candidate-phase failures isolate to one child; configuration/window and unrecognized failures propagating as operation failures; forwarded trace/exact-read cancellation with stable `CANCELLED` normalization; and the `SUBAGENT_CONTROL_SESSION_QUERY_UNAVAILABLE` no-service contract. `packages/subagent/subagent/tests/optional-session-query.spec.ts` rejects eager evaluation of the optional runtime while importing the ordinary subagent surface. - `packages/subagent/tool-subagent-control/tests/list-agents.spec.ts` pins the `list_agents` schema (no parameters), the continuable-only projection that omits a healthy one-shot sibling while preserving diagnostics, the fixed child/diagnostic/empty text forms, an end-to-end settled-child listing with its durable label, the no-agent rejection, load-time `sessionQuery` injection, and HMR disposal. - The keyless ACP snapshot scenario `subagent-list-agents` (examples/acp-agent) pins the model-visible transcript: a background delegation settles, and `list_agents` executes for real against the subagent service, session query, and JSONL persistence, rendering ` [complete] —