@sema-agent/core 7.0.0 → 7.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/dist/agents/launch-receipt-contract.d.ts +34 -0
- package/dist/agents/launch-receipt-contract.js +5 -0
- package/dist/agents/subagent.d.ts +134 -2
- package/dist/agents/subagent.js +132 -31
- package/dist/core/file-history-store.js +24 -2
- package/dist/core/roles.js +1 -1
- package/dist/core/runner/runtask.js +2 -0
- package/dist/core/task-notification.d.ts +20 -0
- package/dist/core/types.d.ts +74 -1
- package/dist/core/wiring-manifest.d.ts +18 -1
- package/dist/orchestration/run-workflow-tool.js +2 -2
- package/dist/orchestration/workflow.js +18 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 7.0.1 — 2026-08-30
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **`task_progress.model` + `BackgroundChildEvent.model`** (#508①, cli [5725] item-B): every spawn-progress tick and background child event now carries the SERVED model id (PREPARED semantics — the id the child's prepare actually resolved, not the word the caller wrote). The fleet row publishes the same served id, so row/tick/card lanes state one fact; a replayed workflow leg reports the cached model on every face.
|
|
7
|
+
- **`apiFailure` on the completed-agent details card and both notification lanes** (#506 residual): terminal API-failure evidence now reaches every reader of a settled child, not only the host-face `TaskResult`.
|
|
8
|
+
|
|
9
|
+
### Changed — narrowing (named)
|
|
10
|
+
- **Unknown/empty delegation `model` argument: soft note → typed reject** (#508②, CC $L closed-set anchored): `model.unknown` (echoes the accepted catalog) and `model.empty` replace the old accept-with-note behavior. A caller that previously leaned on the note-and-serve-default path now gets a refusal naming the legal values. Positive control: every cataloged id serves exactly as before. **Two lanes are exempt** (pre-ship rescan, the caller-census duty): the tier-3 revival lane feeds the durable row's MACHINE-minted recorded word back through this gate, so it keeps the soft-degrade (loud `modelNote` + inherited model — a catalog rotation must not permanently strand a named teammate and its queued mail), and its fallback chain skips string candidates the live catalog cannot resolve (a degrade that re-selects the retired word would revive a child dead on arrival); `subagent_type: "fork"` keeps the schema's own "model is ignored" promise for the empty string exactly as for any other word. Membership itself hardened: the catalog read is own-property (`Object.hasOwn`), so `Object.prototype` words (`constructor`, `toString`, …) refuse as `model.unknown` instead of passing the gate and crashing the child.
|
|
11
|
+
- **Per-call model binding is resolved AT the gate** (rescan): a judged per-call word now rides the child spec as the resolved `Model` object, so the judged roster is the authoritative binding — an explicit `opts.models` table naming a key the runner's own catalog lacks now serves on the judged binding instead of passing the gate and dying inside the child's prepare. `BackgroundChildEvent.model` / roster-row docs restated to the served-id-first rule (display label is the unresolvable-ref fallback only); the residual one-generation lag window for definition/tool-level string refs after `swapModels` is documented, not claimed away. The background roster record gains optional `modelFallback` (additive; fork lane already carried it).
|
|
12
|
+
- **File-history mode domain, both doors** (security): the import door refuses a version-row mode that is not an integer in [0, 0o7777] and refuses set-ID modes (setuid/setgid) BY NAME; on replay, `applyRecordedMode` STRIPS the set-ID bits and applies the remainder (`mode & 0o1777`, sticky preserved) under the loud `modeNotPreserved` account — set-ID bits never reach chmod (capture may legitimately record one; replay is where the escalation lives). A file already at exactly the recorded mode answers `unchanged` with no account (the equality fast path precedes the escalation arm), and `previewFileDelta` compares against what replay would actually apply, so a set-ID-recorded drift converges instead of being promised forever.
|
|
13
|
+
|
|
14
|
+
### Changed — widening (named)
|
|
15
|
+
- **The delegation tool's model roster is read at JUDGEMENT time, not frozen at mount** (#508 residual): after `Runner.swapModels`, a long-lived mount no longer hard-rejects the models the deployment just started serving (the old refusal echoed the RETIRED generation as the accepted values). Explicit `opts.models` keeps snapshot semantics; only the runner-derived roster re-reads live. The schema/announce projection remains a mount-time snapshot — the refusal therefore carries its own accepted-values echo.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Flaky-guard ledger: the three new pin files registered as bounded effect-polling (gate was red since the #508 batch landed).
|
|
19
|
+
|
|
20
|
+
### Docs
|
|
21
|
+
- #509 core half: the subagent transcript LANDING law (three store shapes + the CC 2.1.250 unconditional-landing anchor) spelled out at `SubagentToolOptions.runner`, the background placement mint gate, and `resolveSubagentTranscriptTier`; reproduced-and-located, wiring fix owned by the delegation runner's store choice.
|
|
22
|
+
- 6.0.0 release-criteria acceptance corrections (test [5719]/[5721]): G1.a added-count 65→77, F7 absent-seat claim restated.
|
|
23
|
+
|
|
3
24
|
## 7.0.0 — 2026-08-30
|
|
4
25
|
|
|
5
26
|
> Ships as its own major immediately after 6.0.0 (ruled: the three-dimensional rule scope travels alone so downstream adapts to it in isolation). The 6.0.0 artifact is this same tree with the design/382 P1 surface excised whole (type-driven surgery in an isolated worktree, tag v6.0.0); `npm run handoff:diff v6.0.0 v7.0.0` reproduces exactly this surface.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HANDLING contract shared by every tool that launches background work and hands the model a
|
|
3
|
+
* receipt: the Agent tool's async-launch card and `run_workflow`'s backgrounded launch receipt.
|
|
4
|
+
*
|
|
5
|
+
* Why a module rather than two copies. Both receipts state the same two things — "these bytes are
|
|
6
|
+
* plumbing, do not paste them at the user" and "say what you launched in your own words" — and they
|
|
7
|
+
* had drifted into two wordings of it (`; ` vs ` — `, "this result" vs "this tool result"). Neither
|
|
8
|
+
* difference carried meaning; each was just the copy that happened to be typed second. A model reading
|
|
9
|
+
* one receipt and a model reading the other should be reading the same instruction, and a later edit
|
|
10
|
+
* to one of them should not be able to leave the other behind.
|
|
11
|
+
*
|
|
12
|
+
* What deliberately stays PER-LANE (the earlier ruling, narrowed rather than reversed): each receipt
|
|
13
|
+
* hands back different artifacts, so each names its own. The Agent card names `task_id`; the workflow
|
|
14
|
+
* receipt names the ids plus the host filesystem `scriptPath` it may have written. That enumeration is
|
|
15
|
+
* a fact about the lane and cannot be centralized without one of them lying. So the SCOPE clause is the
|
|
16
|
+
* caller's, and the sentence around it is this module's. Likewise the envelopes stay as they were —
|
|
17
|
+
* the Agent lane renders prose, the workflow lane a JSON field — which is why these are string parts
|
|
18
|
+
* rather than a rendered receipt.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* The no-quote clause for one lane. `artifactScope` is the caller's own enumeration of what "any part
|
|
22
|
+
* of it" covers, spliced in verbatim INCLUDING its leading separator — a comma clause
|
|
23
|
+
* (`", including the task_id below,"`) and a parenthetical (`" (the ids above, and scriptPath when
|
|
24
|
+
* present)"`) punctuate differently and the caller owns which it uses. Pass `""` for a lane with
|
|
25
|
+
* nothing to enumerate; the sentence still reads.
|
|
26
|
+
*/
|
|
27
|
+
export declare function launchReceiptNoQuoteClause(artifactScope: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* The closing instruction, whole and identical on every launch lane: report the launch in the model's
|
|
30
|
+
* own words instead of echoing the receipt. Callers append their own lane-specific continuation (how
|
|
31
|
+
* the result will or will not arrive) after a single space; this clause is a complete sentence on its
|
|
32
|
+
* own so a lane with nothing to add can use it unchanged.
|
|
33
|
+
*/
|
|
34
|
+
export declare const LAUNCH_RECEIPT_OWN_WORDS_CLAUSE = "In your own words, briefly tell the user what you launched \u2014 do not echo this tool result.";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
const NO_QUOTE_HEAD = "This tool result is internal metadata — never quote or paste any part of it";
|
|
2
|
+
export function launchReceiptNoQuoteClause(artifactScope) {
|
|
3
|
+
return `${NO_QUOTE_HEAD}${artifactScope} into a user-facing reply.`;
|
|
4
|
+
}
|
|
5
|
+
export const LAUNCH_RECEIPT_OWN_WORDS_CLAUSE = "In your own words, briefly tell the user what you launched — do not echo this tool result.";
|
|
@@ -4,6 +4,60 @@ import { type ExecutionEnv } from "../internal/harness.js";
|
|
|
4
4
|
import type { RunInternals } from "../core/runner/prepare-task.js";
|
|
5
5
|
import type { TaskNotificationPayload } from "../core/task-notification.js";
|
|
6
6
|
import { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX } from "../config/defaults.js";
|
|
7
|
+
/**
|
|
8
|
+
* #505 residual — the two GRANULARITY facts a delegation's terminal `usage` was missing beside
|
|
9
|
+
* tokens/turns/cost, closing a real reporting asymmetry rather than adding decoration: the workflow
|
|
10
|
+
* lane's completion notification has published `tool_uses` (summed `stats.toolCalls`) and `duration_ms`
|
|
11
|
+
* (wall clock) since it was written, and the Agent tool's own SYNC report publishes both in its
|
|
12
|
+
* `<usage>` footer — only the delegation TERMINAL faces (the background/fork/revive notification and its
|
|
13
|
+
* observer twin) reported neither, so the same child answered "how much work, how long" on one lane and
|
|
14
|
+
* refused on the other.
|
|
15
|
+
*
|
|
16
|
+
* PROVENANCE is the workflow lane's — `stats.toolCalls`, the RUN's own count — because that is the
|
|
17
|
+
* quantity the other side of the comparison publishes, and because it is available on every terminal
|
|
18
|
+
* lane, including the revive lane, which has no parent-side forwarding observer at all.
|
|
19
|
+
*
|
|
20
|
+
* WHAT THIS DOES NOT CLOSE, stated because the honest scope is narrower than "one name, one meaning":
|
|
21
|
+
* the Agent tool's SYNC `<usage>` footer keeps its own `tool_uses`, counted by the parent-side
|
|
22
|
+
* `toolStatsCounter`, and the two numbers differ in BOTH directions for the same child — that counter
|
|
23
|
+
* EXCLUDES the delegation tool's own calls and INCLUDES descendants' (CC's nested fold), while
|
|
24
|
+
* `stats.toolCalls` counts the direct child's calls and counts its delegations among them. A child that
|
|
25
|
+
* itself delegates therefore reports two different `tool_uses` depending on whether it was run
|
|
26
|
+
* synchronously or in the background. That footer is a byte-pinned CC-anchored face; reconciling the
|
|
27
|
+
* two is a separate ruling, and until it is made this comment is the place the divergence is written
|
|
28
|
+
* down rather than a claim that it is gone.
|
|
29
|
+
*
|
|
30
|
+
* Knownness follows `delegatedCostField`'s rule exactly: `toolCalls` is optional on the stats face
|
|
31
|
+
* ("undefined means the gateway reported no usage", and a checkpointed resume folds it per-leg), so the
|
|
32
|
+
* key is ABSENT rather than zeroed — a fabricated 0 reads as "ran no tools", which is a claim, not a
|
|
33
|
+
* gap. Duration is always knowable (the caller holds the lane's launch instant), so it is unconditional.
|
|
34
|
+
*
|
|
35
|
+
* TWO SPELLINGS, one meaning, matching each face's established vocabulary: the observer
|
|
36
|
+
* ({@link import("../core/types.js").BackgroundChildEvent}) `usage` face is camelCase and already
|
|
37
|
+
* DECLARES `toolUses`/`durationMs`; the model-facing `TaskNotificationPayload.usage` is rendered into
|
|
38
|
+
* `<usage>` as JSON where the workflow lane and the Agent sync footer both spell them snake_case. No new
|
|
39
|
+
* word is minted on either side.
|
|
40
|
+
*
|
|
41
|
+
* Exported at MODULE level (not from the package index — the same posture as `completedAgentCard` and
|
|
42
|
+
* `classifySubagentError`, and for the same reason): the ABSENT arm is the point of the knownness rule
|
|
43
|
+
* and it is not reachable through a real child. This engine's runner initializes `stats.toolCalls` to a
|
|
44
|
+
* number on every path, so only a `TaskResult` from elsewhere — a foreign runner, an older checkpoint —
|
|
45
|
+
* can carry the optional-and-missing shape the stats face declares. A pin that only ever sees a live
|
|
46
|
+
* child cannot tell "absent" apart from "zero", which is precisely the confusion being prevented.
|
|
47
|
+
*/
|
|
48
|
+
export declare function delegatedGranularityFields(stats: {
|
|
49
|
+
toolCalls?: number;
|
|
50
|
+
}, elapsedMs: number): {
|
|
51
|
+
toolUses?: number;
|
|
52
|
+
durationMs: number;
|
|
53
|
+
};
|
|
54
|
+
/** {@link delegatedGranularityFields}'s snake_case twin for the model-facing notification `usage`. */
|
|
55
|
+
export declare function delegatedGranularityFieldsWire(stats: {
|
|
56
|
+
toolCalls?: number;
|
|
57
|
+
}, elapsedMs: number): {
|
|
58
|
+
tool_uses?: number;
|
|
59
|
+
duration_ms: number;
|
|
60
|
+
};
|
|
7
61
|
export { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX };
|
|
8
62
|
import { SubagentRetainLedger } from "./retain-ledger.js";
|
|
9
63
|
import { type PeerSelfRef } from "./peer-admission.js";
|
|
@@ -268,6 +322,17 @@ export declare function completedAgentCard(child: {
|
|
|
268
322
|
blockedReason?: string;
|
|
269
323
|
errorMessage?: string;
|
|
270
324
|
errorCode?: string;
|
|
325
|
+
/** The child run's provider-boundary fault assertion, verbatim from its `TaskResult.apiFailure`.
|
|
326
|
+
* PRESENCE is the claim ("this child's terminal failure came from the transport/provider, not from
|
|
327
|
+
* this deployment refusing to send, a limit, or unusable model output"); the members are whatever
|
|
328
|
+
* the failing attempt stated about itself (`status` when the provider answered with one, `requestId`
|
|
329
|
+
* when it named one). An EMPTY object is therefore meaningful and must not be normalized away: it
|
|
330
|
+
* says "provider fault, unlabelled", which is a different statement from absence ("not a provider
|
|
331
|
+
* fault"). Absent on every non-provider terminal and on every completed child. */
|
|
332
|
+
apiFailure?: {
|
|
333
|
+
status?: number;
|
|
334
|
+
requestId?: string;
|
|
335
|
+
};
|
|
271
336
|
retryAfterMs?: number;
|
|
272
337
|
degraded?: unknown;
|
|
273
338
|
structuredOutput?: unknown;
|
|
@@ -514,9 +579,61 @@ export declare function delegationEntryLedgerFootprint(registry: object): {
|
|
|
514
579
|
* catalog) after mounting must REBUILD the ToolSpec — call `createSubagentTool` again with the fresh
|
|
515
580
|
* roster and serve the new tool — for the change to take effect; already-running tasks keep the roster
|
|
516
581
|
* they were prepared with.
|
|
582
|
+
*
|
|
583
|
+
* The MODEL roster ({@link SubagentToolOptions.models}) deliberately does NOT follow that rule when it
|
|
584
|
+
* is left to the runner: a hot-swapped catalog is a first-class runner verb (`Runner.swapModels`) with
|
|
585
|
+
* no rebuild step, so the tool re-reads it at each judgement instead of requiring a remount. Only the
|
|
586
|
+
* schema/announce projection of it stays a mount-time snapshot (cache-prefix stability). Passing
|
|
587
|
+
* `models` explicitly opts back into snapshot semantics for that table — see the field.
|
|
517
588
|
*/
|
|
518
589
|
export interface SubagentToolOptions {
|
|
519
|
-
/**
|
|
590
|
+
/**
|
|
591
|
+
* Runner used to execute child tasks.
|
|
592
|
+
*
|
|
593
|
+
* **This runner's session store decides where a child TRANSCRIPT lives — and therefore whether one
|
|
594
|
+
* exists at all after the fact.** A SYNC or BACKGROUND child mints its session through
|
|
595
|
+
* `runner.sessions`, never through the parent leg's store, so a deployment that mounts the Agent
|
|
596
|
+
* tool on a DIFFERENT runner than the one serving root tasks has split the transcript lane away
|
|
597
|
+
* from the session lane. (The FORK lane is the deliberate exception and shows the split is a known
|
|
598
|
+
* shape: a fork branches the CALLER's session through `ctx.hostSessionFork` — the host Runner's
|
|
599
|
+
* own store — precisely because `runner` may be a split child-execution runner whose store never
|
|
600
|
+
* held the source. A fork child therefore follows the HOST store's fate, an ordinary child this
|
|
601
|
+
* runner's, and the same deployment can land one on disk and lose the other.) Three shapes for the
|
|
602
|
+
* ordinary lanes, all reachable and all silent:
|
|
603
|
+
* - durable store on THIS runner whose `release` PRESERVES history (`evict:"forget"`, the
|
|
604
|
+
* `FileStorageBackend` default), no {@link SubagentToolOptions.background}.`agentStore` — the
|
|
605
|
+
* child transcript lands UNPLACED and survives the settle leg's eager release (a cache drop),
|
|
606
|
+
* so post-hoc forensics work even though nothing can ADDRESS it through an `a*` handle. The
|
|
607
|
+
* qualifier is load-bearing, not decorative: durability and release semantics are INDEPENDENT
|
|
608
|
+
* declarations, and a store that declares itself durable while deleting on release
|
|
609
|
+
* (`new FileStorageBackend({ evict: "delete" })`, or any custom store with the same policy)
|
|
610
|
+
* destroys the transcript at settle — the settle leg calls `release`, and there is no placement
|
|
611
|
+
* to make the deletion refuse. "Durable" is a statement about restarts, never about release;
|
|
612
|
+
* - durable store + `agentStore` — the placed partition (see the mint gate in the background
|
|
613
|
+
* lane): addressable across a restart, list-excluded, joint-reaped with its row. This arm is
|
|
614
|
+
* also the one immune to the evict policy above (the settle leg keeps a confirmed row's
|
|
615
|
+
* transcript instead of releasing it);
|
|
616
|
+
* - transient/process-local store on THIS runner (e.g. a throwaway sub-task store, or a routing
|
|
617
|
+
* store that sends ordinary child acquires to a private cache) — the child transcript NEVER
|
|
618
|
+
* reaches disk whatever the root leg's store is. Nothing in the engine compensates: the
|
|
619
|
+
* completion notification's recent steps and the in-memory registry are the only readers, and
|
|
620
|
+
* both die with the process.
|
|
621
|
+
* The asymmetry this produces is worth naming, because it reads as a bug from the outside: the
|
|
622
|
+
* `run_workflow` tool is mounted by the ENGINE on the run's own runner (prepare-task's
|
|
623
|
+
* `runnerSelf`), so a workflow node's child session always follows the ROOT store, while this tool
|
|
624
|
+
* is mounted by the DEPLOYMENT and follows whatever runner it was handed. One data root can
|
|
625
|
+
* therefore hold every workflow node's transcript and none of the Agent tool's.
|
|
626
|
+
* There is deliberately NO manifest field that answers "does a child transcript reach disk here":
|
|
627
|
+
* `WiringManifest.fleet.subagentTranscripts` is the ADDRESSABILITY tier and folds to `"none"` on
|
|
628
|
+
* the absent-row-store arm before it ever looks at a session store, so it cannot separate the
|
|
629
|
+
* first shape above from the third (the landing pins carry that as an explicit arm). The facts
|
|
630
|
+
* that DO answer it are this runner's store's own declarations — `sessions.durability` and its
|
|
631
|
+
* release/evict policy — read together, which is why they are named here rather than folded into
|
|
632
|
+
* a word. Upstream anchor (CC 2.1.250): a sub-agent transcript is written unconditionally to
|
|
633
|
+
* its own file beside the parent session (`projects/<projectKey>/<sessionId>/subagents/agent-<id>
|
|
634
|
+
* .jsonl` + a `.meta.json` sidecar), so "the child conversation is readable afterwards" is a
|
|
635
|
+
* property CC never makes conditional on a store tier.
|
|
636
|
+
*/
|
|
520
637
|
runner: Runner;
|
|
521
638
|
/**
|
|
522
639
|
* design/115 P3 — background sub-agents (CC `run_in_background`). When set, the tool exposes the
|
|
@@ -599,7 +716,22 @@ export interface SubagentToolOptions {
|
|
|
599
716
|
* wins — fork-shadowing precedent).
|
|
600
717
|
*/
|
|
601
718
|
builtinAgents?: boolean;
|
|
602
|
-
/**
|
|
719
|
+
/**
|
|
720
|
+
* Model catalog the tool validates model words against — an agent definition's string `model` ref at
|
|
721
|
+
* assembly time (fail-fast), and the per-call `model` argument at call time.
|
|
722
|
+
*
|
|
723
|
+
* OMITTED is the normal deployment shape: the tool then reads the runner's CURRENT catalog
|
|
724
|
+
* (`Runner.agentCatalog.models`) at each of those moments, so a catalog hot-swapped after mount is
|
|
725
|
+
* the one a delegation is judged against. Passing this explicitly is a deliberate NARROWING and
|
|
726
|
+
* pins the tool to exactly this table for the tool's whole life — the runner's generation changes
|
|
727
|
+
* no longer reach it. And this table is AUTHORITATIVE for the per-call word, not merely advisory:
|
|
728
|
+
* a word judged against it leaves the gate as this table's resolved `Model` object and the child
|
|
729
|
+
* runs that binding — including a key the runner's own catalog does not carry, and including a
|
|
730
|
+
* same-name key the runner binds differently. (The judged-then-re-resolved split this replaced
|
|
731
|
+
* admitted such words and then killed the child inside its own prepare, or ran a binding the spawn
|
|
732
|
+
* frame never named.) A definition's string `model` ref is different: it is validated here at
|
|
733
|
+
* assembly time but still resolves at the child's own prepare through the runner's live table.
|
|
734
|
+
*/
|
|
603
735
|
models?: Record<string, Model>;
|
|
604
736
|
/** Tools available to the child (a deliberately narrowed subset). */
|
|
605
737
|
tools?: ToolSpec[];
|
package/dist/agents/subagent.js
CHANGED
|
@@ -37,10 +37,27 @@ function delegatedCostField(stats) {
|
|
|
37
37
|
const total = rollupDelegatedCost(stats, stats.nested);
|
|
38
38
|
return total !== undefined ? { costMicroUsd: total } : {};
|
|
39
39
|
}
|
|
40
|
+
export function delegatedGranularityFields(stats, elapsedMs) {
|
|
41
|
+
return { ...(stats.toolCalls !== undefined ? { toolUses: stats.toolCalls } : {}), durationMs: elapsedMs };
|
|
42
|
+
}
|
|
43
|
+
function providerFaultField(child) {
|
|
44
|
+
if (child.apiFailure === undefined)
|
|
45
|
+
return {};
|
|
46
|
+
return {
|
|
47
|
+
apiFailure: {
|
|
48
|
+
...(typeof child.apiFailure.status === "number" ? { status: child.apiFailure.status } : {}),
|
|
49
|
+
...(typeof child.apiFailure.requestId === "string" ? { requestId: child.apiFailure.requestId } : {}),
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function delegatedGranularityFieldsWire(stats, elapsedMs) {
|
|
54
|
+
return { ...(stats.toolCalls !== undefined ? { tool_uses: stats.toolCalls } : {}), duration_ms: elapsedMs };
|
|
55
|
+
}
|
|
40
56
|
export { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX };
|
|
41
57
|
import { SUBAGENT_RESUME_CAP, SubagentRetainLedger, getOrCreateSessionRetainLedger, ensureSessionReapHook, createResumePrompt, } from "./retain-ledger.js";
|
|
42
58
|
import { createPeerInboundChainRef, createPeerSelfRef } from "./peer-admission.js";
|
|
43
59
|
import { recordRosterSpawn } from "./roster-store.js";
|
|
60
|
+
import { LAUNCH_RECEIPT_OWN_WORDS_CLAUSE, launchReceiptNoQuoteClause } from "./launch-receipt-contract.js";
|
|
44
61
|
import { ObserverDigestTap, ObserverPairing, createObserverReportToolSpec, markObserverTaskId, unmarkObserverTaskId, isObserverTaskId, ObserverResumeStateError, ObserverStoppedByUserError, observerFramingPrompt, observerSlug, resolveObserverDeclaration, } from "./observer.js";
|
|
45
62
|
import { SubagentStepRecorder } from "./subagent-steps.js";
|
|
46
63
|
const BG_AGENT_RESULT_MAX = 4_000;
|
|
@@ -326,6 +343,7 @@ function resumabilityClaim(f) {
|
|
|
326
343
|
const BG_AGENT_COLLATERAL_REAP_REASON = "its parent run ended";
|
|
327
344
|
const RESERVED_AGENT_NAMES = new Set([OUTPUT_TOOL_NAME, TOOL_SEARCH_NAME, OFFLOAD_TOOL_NAME, REPORT_BLOCKED_TOOL_NAME, DEFAULT_SUBAGENT_TOOL_NAME]);
|
|
328
345
|
export const REPORT_FIELD_MAX = 300;
|
|
346
|
+
const ROSTER_ECHO_MAX = 400;
|
|
329
347
|
function configError(message, code) {
|
|
330
348
|
const e = new Error(message);
|
|
331
349
|
e.code = code;
|
|
@@ -364,6 +382,7 @@ export function completedAgentCard(child, extras) {
|
|
|
364
382
|
...(child.blockedReason !== undefined ? { blockedReason: child.blockedReason } : {}),
|
|
365
383
|
...(child.errorMessage !== undefined ? { errorMessage: child.errorMessage } : {}),
|
|
366
384
|
...(child.errorCode !== undefined ? { errorCode: child.errorCode } : {}),
|
|
385
|
+
...providerFaultField(child),
|
|
367
386
|
...(child.retryAfterMs !== undefined ? { retryAfterMs: child.retryAfterMs } : {}),
|
|
368
387
|
...(child.degraded !== undefined ? { degraded: child.degraded } : {}),
|
|
369
388
|
...(extras.handbackWarning !== undefined ? { handbackWarning: extras.handbackWarning } : {}),
|
|
@@ -711,9 +730,10 @@ export function createSubagentResume(deps) {
|
|
|
711
730
|
const completionIdRevive = deps.registry?.getCompletionId(deps.taskId ?? "");
|
|
712
731
|
const reviveName = deps.rowDescription ?? `sub-agent ${marker}`;
|
|
713
732
|
const failReasonRevive = status === "failed" ? child.errorMessage : undefined;
|
|
733
|
+
const reviveElapsedMs = Date.now() - reviveStartedAt;
|
|
714
734
|
const reviveTerminalSummary = failReasonRevive !== undefined
|
|
715
|
-
? `Agent "${reviveName}" (resumed) failed: ${failReasonRevive}${ccElapsedTag(
|
|
716
|
-
: `Agent "${reviveName}" (resumed) ${status === "killed" ? "stopped" : child.status === "completed" ? "finished" : String(child.status)}${ccElapsedTag(
|
|
735
|
+
? `Agent "${reviveName}" (resumed) failed: ${failReasonRevive}${ccElapsedTag(reviveElapsedMs)}`.slice(0, 300) + errorKindClause(errClassRevive)
|
|
736
|
+
: `Agent "${reviveName}" (resumed) ${status === "killed" ? "stopped" : child.status === "completed" ? "finished" : String(child.status)}${ccElapsedTag(reviveElapsedMs)}`;
|
|
717
737
|
const reviveDurableProbe = deps.registry !== undefined && deps.taskId !== undefined ? deps.registry.durableAgentRowProbe(deps.taskId) : undefined;
|
|
718
738
|
const reviveDurableRow = reviveDurableProbe !== undefined ? await reviveDurableProbe().catch(() => false) : false;
|
|
719
739
|
const resumableRevive = resumabilityClaim({
|
|
@@ -739,7 +759,7 @@ export function createSubagentResume(deps) {
|
|
|
739
759
|
summary: reviveTerminalSummary,
|
|
740
760
|
...resumeResidual(),
|
|
741
761
|
resumable: resumableRevive,
|
|
742
|
-
usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats) },
|
|
762
|
+
usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats), ...delegatedGranularityFields(child.stats, reviveElapsedMs) },
|
|
743
763
|
...(completionIdRevive !== undefined ? { completionId: completionIdRevive } : {}),
|
|
744
764
|
});
|
|
745
765
|
const resumeFrame = {
|
|
@@ -753,11 +773,12 @@ export function createSubagentResume(deps) {
|
|
|
753
773
|
summary: reviveTerminalSummary,
|
|
754
774
|
...(failReasonRevive !== undefined ? { error: failReasonRevive.slice(0, REPORT_FIELD_MAX) } : {}),
|
|
755
775
|
...(status === "failed" && errCodeRevive !== undefined ? { errorCode: errCodeRevive } : {}),
|
|
776
|
+
...providerFaultField(child),
|
|
756
777
|
...(resumeHandbackResult ? { result: notifyResultField(resumeHandbackResult) } : {}),
|
|
757
778
|
...(status === "killed" && child.result ? { partial: true } : {}),
|
|
758
779
|
...resumeResidual(),
|
|
759
780
|
resumable: resumableRevive,
|
|
760
|
-
usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats) },
|
|
781
|
+
usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats), ...delegatedGranularityFieldsWire(child.stats, reviveElapsedMs) },
|
|
761
782
|
...(completionIdRevive !== undefined ? { completionId: completionIdRevive } : {}),
|
|
762
783
|
};
|
|
763
784
|
const gateOwner = ownsTerminalFacesRevive && status === "completed" && deps.notify !== undefined && deps.registry !== undefined && ledger.get(deps.parentToolCallId) === entry
|
|
@@ -1206,7 +1227,7 @@ export function forkWorktreeTranslationNote(parentCwd, worktreeDir) {
|
|
|
1206
1227
|
export function asyncLaunchedReceipt(p) {
|
|
1207
1228
|
const noteLines = (p.notes ?? []).filter((n) => n !== undefined && n !== "");
|
|
1208
1229
|
const oneShotBlocked = p.notify && p.oneShot === true;
|
|
1209
|
-
return (`Async agent launched successfully. (
|
|
1230
|
+
return (`Async agent launched successfully. (${launchReceiptNoQuoteClause(", including the task_id below,")})\n` +
|
|
1210
1231
|
`task_id: ${p.taskId} (internal ID - do not mention to user. Use SendMessage with to: '${p.taskId}', summary: '<5-10 word recap>' to continue this agent.)\n` +
|
|
1211
1232
|
(oneShotBlocked
|
|
1212
1233
|
? `${p.workingLine} This is a ONE-SHOT submission — there is no later turn for a background notification to land in, so do NOT end your turn expecting one. Actively wait instead: TaskOutput(task_id: "${p.taskId}", block: true). If it is still running after the wait, wait again (bounded) rather than ending the turn, or write out your best available answer now if you are near your own time budget.\n`
|
|
@@ -1215,10 +1236,18 @@ export function asyncLaunchedReceipt(p) {
|
|
|
1215
1236
|
: `${p.workingLine} Its result is NOT pushed automatically — retrieve progress and results with TaskOutput(task_id) where mounted.\n`) +
|
|
1216
1237
|
noteLines.map((n) => `${n}\n`).join("") +
|
|
1217
1238
|
(oneShotBlocked
|
|
1218
|
-
?
|
|
1239
|
+
? `${LAUNCH_RECEIPT_OWN_WORDS_CLAUSE} Do not assume a later message will deliver the result: this submission ends after this turn, so retrieve it now with the blocking TaskOutput wait above before writing your final answer.`
|
|
1219
1240
|
: p.notify
|
|
1220
|
-
?
|
|
1221
|
-
:
|
|
1241
|
+
? `${LAUNCH_RECEIPT_OWN_WORDS_CLAUSE} Agent results will arrive in a subsequent message. If the user asks for progress, say the agent is still running.`
|
|
1242
|
+
: `${LAUNCH_RECEIPT_OWN_WORDS_CLAUSE} The result will NOT arrive on its own — retrieve it with TaskOutput(task_id) where mounted before relying on it.`));
|
|
1243
|
+
}
|
|
1244
|
+
function runnerModelCatalog(runner) {
|
|
1245
|
+
try {
|
|
1246
|
+
return runner?.agentCatalog?.models;
|
|
1247
|
+
}
|
|
1248
|
+
catch {
|
|
1249
|
+
return undefined;
|
|
1250
|
+
}
|
|
1222
1251
|
}
|
|
1223
1252
|
export function createSubagentTool(opts) {
|
|
1224
1253
|
const catalog = opts.runner.agentCatalog;
|
|
@@ -1227,8 +1256,7 @@ export function createSubagentTool(opts) {
|
|
|
1227
1256
|
if (opts.builtinAgents === undefined && catalog?.builtinAgents !== undefined) {
|
|
1228
1257
|
opts = { ...opts, builtinAgents: catalog.builtinAgents };
|
|
1229
1258
|
}
|
|
1230
|
-
|
|
1231
|
-
opts = { ...opts, models: catalog.models };
|
|
1259
|
+
const assemblyModels = opts.models ?? catalog?.models;
|
|
1232
1260
|
if (opts.agents?.length) {
|
|
1233
1261
|
opts = {
|
|
1234
1262
|
...opts,
|
|
@@ -1252,7 +1280,7 @@ export function createSubagentTool(opts) {
|
|
|
1252
1280
|
}
|
|
1253
1281
|
if (typeof a.model === "string") {
|
|
1254
1282
|
try {
|
|
1255
|
-
resolveModel(a.model,
|
|
1283
|
+
resolveModel(a.model, assemblyModels);
|
|
1256
1284
|
}
|
|
1257
1285
|
catch {
|
|
1258
1286
|
throw configError(`createSubagentTool: agent "${a.name}" references unknown model "${a.model}" (pass \`models\` to resolve string refs).`, "config.agent.unknown_model");
|
|
@@ -1308,7 +1336,9 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1308
1336
|
...(forkOffered ? [FORK_SUBAGENT_TYPE] : []),
|
|
1309
1337
|
...(generalPurposeShadowed ? [] : [GENERAL_PURPOSE_SUBAGENT_TYPE]),
|
|
1310
1338
|
];
|
|
1311
|
-
const
|
|
1339
|
+
const activeModels = () => opts.models ?? runnerModelCatalog(opts.runner);
|
|
1340
|
+
const mountModels = activeModels();
|
|
1341
|
+
const rosterNames = mountModels ? Object.keys(mountModels) : undefined;
|
|
1312
1342
|
const agentListing = [
|
|
1313
1343
|
...(generalPurposeShadowed
|
|
1314
1344
|
? []
|
|
@@ -1424,7 +1454,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1424
1454
|
})),
|
|
1425
1455
|
model: Type.Optional(Type.String({
|
|
1426
1456
|
description: rosterNames !== undefined && rosterNames.length > 0
|
|
1427
|
-
? `Optional model override for this agent. Takes precedence over the agent definition's model. If omitted, uses the agent definition's model, or inherits from the parent. Ignored for subagent_type: "${FORK_SUBAGENT_TYPE}" — forks always inherit the parent model.`
|
|
1457
|
+
? `Optional model override for this agent. Takes precedence over the agent definition's model. If omitted, uses the agent definition's model, or inherits from the parent. A value outside this deployment's model catalog is REFUSED — the call does not fall back silently. Ignored for subagent_type: "${FORK_SUBAGENT_TYPE}" — forks always inherit the parent model.`
|
|
1428
1458
|
: `Optional model override request. No model catalog is configured here, so a requested name cannot be resolved — the agent runs its definition's model (or inherits from the parent) and the result notes the override was not applied. Ignored for subagent_type: "${FORK_SUBAGENT_TYPE}".`,
|
|
1429
1459
|
})),
|
|
1430
1460
|
isolation: Type.Optional(Type.Literal("worktree", {
|
|
@@ -1560,23 +1590,54 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1560
1590
|
};
|
|
1561
1591
|
}
|
|
1562
1592
|
}
|
|
1593
|
+
if (typeof a.model === "string" && a.model.trim() === "" && !wantsFork && reviveClaim === undefined) {
|
|
1594
|
+
return {
|
|
1595
|
+
isError: true,
|
|
1596
|
+
content: "Sub-agent not started: `model` was empty. Name a model from this deployment's catalog, or omit the parameter to run the agent type's own model.",
|
|
1597
|
+
details: { error: "model.empty" },
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1563
1600
|
const requestedModel = typeof a.model === "string" && a.model.trim() !== "" ? a.model.trim() : undefined;
|
|
1564
1601
|
let perCallModel;
|
|
1565
1602
|
let modelNote;
|
|
1566
|
-
|
|
1603
|
+
const judgedModels = activeModels();
|
|
1604
|
+
const rosterKeys = judgedModels !== undefined ? Object.keys(judgedModels) : [];
|
|
1605
|
+
if (typeof a.model === "string" && requestedModel === undefined) {
|
|
1606
|
+
modelNote = wantsFork
|
|
1607
|
+
? `note: model "" was ignored — a fork always runs on the caller's model.`
|
|
1608
|
+
: `note: the durable record's model was blank and was NOT applied — the revived agent ran on its default (inherited) model.`;
|
|
1609
|
+
}
|
|
1610
|
+
else if (requestedModel !== undefined) {
|
|
1567
1611
|
if (wantsFork) {
|
|
1568
1612
|
modelNote = `note: model "${requestedModel}" was ignored — a fork always runs on the caller's model.`;
|
|
1569
1613
|
}
|
|
1570
|
-
else if (
|
|
1614
|
+
else if (rosterKeys.length === 0) {
|
|
1571
1615
|
modelNote = `note: model "${requestedModel}" was NOT applied — this delegation tool has no model roster configured; the sub-agent ran on its default (inherited) model.`;
|
|
1572
1616
|
}
|
|
1573
1617
|
else {
|
|
1574
1618
|
try {
|
|
1575
|
-
resolveModel(requestedModel,
|
|
1576
|
-
perCallModel = requestedModel;
|
|
1619
|
+
perCallModel = resolveModel(requestedModel, judgedModels);
|
|
1577
1620
|
}
|
|
1578
1621
|
catch {
|
|
1579
|
-
|
|
1622
|
+
if (reviveClaim !== undefined) {
|
|
1623
|
+
modelNote = `note: recorded model "${inlineUntrusted(requestedModel, 80)}" is not on the current model roster and was NOT applied — the revived agent ran on its default (inherited) model.`;
|
|
1624
|
+
}
|
|
1625
|
+
else {
|
|
1626
|
+
const shown = [];
|
|
1627
|
+
let budget = ROSTER_ECHO_MAX;
|
|
1628
|
+
for (const k of rosterKeys) {
|
|
1629
|
+
if (budget - k.length < 0)
|
|
1630
|
+
break;
|
|
1631
|
+
shown.push(k);
|
|
1632
|
+
budget -= k.length + 2;
|
|
1633
|
+
}
|
|
1634
|
+
const rosterEcho = shown.length === rosterKeys.length ? shown.join(", ") : `${shown.join(", ")}, … (${rosterKeys.length - shown.length} more)`;
|
|
1635
|
+
return {
|
|
1636
|
+
isError: true,
|
|
1637
|
+
content: `Sub-agent not started: unknown model "${inlineUntrusted(requestedModel, 80)}". Available model values: ${inlineUntrusted(rosterEcho, ROSTER_ECHO_MAX + 32)}. Omit \`model\` to run the agent type's own model, or inherit the caller's.`,
|
|
1638
|
+
details: { error: "model.unknown" },
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1580
1641
|
}
|
|
1581
1642
|
}
|
|
1582
1643
|
}
|
|
@@ -1690,9 +1751,39 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1690
1751
|
const childTools = def
|
|
1691
1752
|
? resolveToolSubset(childPool, def.allowTools, def.denyTools)
|
|
1692
1753
|
: resolveToolSubset(childPool, opts.allowTools, opts.denyTools);
|
|
1693
|
-
|
|
1754
|
+
let childModel = wantsFork
|
|
1694
1755
|
? ctx.model ?? opts.model
|
|
1695
1756
|
: perCallModel ?? (def ? (def.model ?? ctx.model ?? opts.model) : opts.model);
|
|
1757
|
+
if (reviveClaim !== undefined && typeof childModel === "string") {
|
|
1758
|
+
const resolvable = (ref) => {
|
|
1759
|
+
try {
|
|
1760
|
+
resolveModel(ref, runnerModelCatalog(opts.runner));
|
|
1761
|
+
return true;
|
|
1762
|
+
}
|
|
1763
|
+
catch {
|
|
1764
|
+
return false;
|
|
1765
|
+
}
|
|
1766
|
+
};
|
|
1767
|
+
if (!resolvable(childModel)) {
|
|
1768
|
+
const dead = childModel;
|
|
1769
|
+
const rest = def !== undefined ? [ctx.model, opts.model] : [opts.model];
|
|
1770
|
+
childModel = rest.find((c) => c !== undefined && (typeof c !== "string" || resolvable(c)));
|
|
1771
|
+
if (modelNote === undefined) {
|
|
1772
|
+
modelNote = `note: model "${inlineUntrusted(dead, 80)}" from this agent type's configuration is not on the current model roster and was NOT applied — the revived agent ran on its default (inherited) model.`;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
const spawnModel = typeof childModel === "string"
|
|
1777
|
+
? (() => {
|
|
1778
|
+
try {
|
|
1779
|
+
return resolveModel(childModel, judgedModels).id;
|
|
1780
|
+
}
|
|
1781
|
+
catch {
|
|
1782
|
+
return resolveModelDisplayLabel(childModel);
|
|
1783
|
+
}
|
|
1784
|
+
})()
|
|
1785
|
+
: childModel?.id;
|
|
1786
|
+
const rowModelKey = perCallModel !== undefined ? requestedModel : typeof childModel === "string" ? childModel : childModel?.id;
|
|
1696
1787
|
const childSystemPrompt = def?.systemPrompt ?? opts.systemPrompt;
|
|
1697
1788
|
const childDefaultPersona = childSystemPrompt === undefined && !wantsFork ? SUBAGENT_PROMPT : undefined;
|
|
1698
1789
|
const defMaxTurns = typeof def?.maxTurns === "number" && Number.isFinite(def.maxTurns) && def.maxTurns > 0 ? def.maxTurns : undefined;
|
|
@@ -2342,7 +2433,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2342
2433
|
...(ctx.taskId !== undefined && ctx.taskId !== ctx.sessionId ? { parentTaskId: ctx.taskId } : {}),
|
|
2343
2434
|
...(ctx.sessionId !== undefined ? { parentSessionId: ctx.sessionId } : {}),
|
|
2344
2435
|
...((ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}),
|
|
2345
|
-
...(
|
|
2436
|
+
...(rowModelKey !== undefined ? { model: rowModelKey } : {}),
|
|
2346
2437
|
deliveryChannel: "attaching",
|
|
2347
2438
|
toolUseId: ctx.toolCallId,
|
|
2348
2439
|
...(bg.agentStore !== undefined ? { store: bg.agentStore } : {}),
|
|
@@ -2403,7 +2494,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2403
2494
|
}, { priority: "next" }), "subagent.reapTerminalNotify");
|
|
2404
2495
|
});
|
|
2405
2496
|
if (agentName !== undefined) {
|
|
2406
|
-
recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, sessionId: forkedId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...(
|
|
2497
|
+
recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, sessionId: forkedId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...(rowModelKey !== undefined ? { model: rowModelKey } : {}), ...(modelFallback !== undefined ? { modelFallback } : {}), ...((ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: Date.now() }, (err) => opts.onObserverError?.(err, { site: "roster.recordSpawn" }));
|
|
2407
2498
|
}
|
|
2408
2499
|
const bgSink = ctx.onBackgroundChildEvent;
|
|
2409
2500
|
const sinkEmit = (event) => {
|
|
@@ -2422,6 +2513,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2422
2513
|
...(bgScope !== undefined ? { scope: bgScope } : {}),
|
|
2423
2514
|
description: shortDesc,
|
|
2424
2515
|
agentType: spawnAgentType,
|
|
2516
|
+
...(spawnModel !== undefined ? { model: spawnModel } : {}),
|
|
2425
2517
|
...(bgForkCycleSeq !== undefined ? { seq: bgForkCycleSeq } : {}),
|
|
2426
2518
|
...(agentName !== undefined ? { name: agentName } : {}),
|
|
2427
2519
|
sessionId: forkedId,
|
|
@@ -2451,6 +2543,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2451
2543
|
parentToolCallId: ctx.toolCallId,
|
|
2452
2544
|
progressTaskId: forkedId,
|
|
2453
2545
|
agentType: spawnAgentType,
|
|
2546
|
+
...(spawnModel !== undefined ? { model: spawnModel } : {}),
|
|
2454
2547
|
...(bgForkCycleSeq !== undefined ? { seq: bgForkCycleSeq } : {}),
|
|
2455
2548
|
...(currentAction !== undefined ? { currentAction } : {}),
|
|
2456
2549
|
...(currentTool !== undefined ? { currentTool } : {}),
|
|
@@ -2485,6 +2578,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2485
2578
|
progressTaskId: e.taskId,
|
|
2486
2579
|
...(e.parentTaskId !== undefined ? { progressParentTaskId: e.parentTaskId } : {}),
|
|
2487
2580
|
agentType: spawnAgentType,
|
|
2581
|
+
...(spawnModel !== undefined ? { model: spawnModel } : {}),
|
|
2488
2582
|
...(e.name !== undefined ? { name: e.name } : {}),
|
|
2489
2583
|
...(currentAction !== undefined ? { currentAction } : {}),
|
|
2490
2584
|
...(currentTool !== undefined ? { currentTool } : {}),
|
|
@@ -2562,9 +2656,10 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2562
2656
|
const resumableFork = false;
|
|
2563
2657
|
const forkTranscriptId = child.sessionId ?? forkedId;
|
|
2564
2658
|
const failReasonFork = settledBg === "failed" ? child.errorMessage : undefined;
|
|
2659
|
+
const forkElapsedMs = Date.now() - forkBgStartedAt;
|
|
2565
2660
|
const forkTerminalSummary = failReasonFork !== undefined
|
|
2566
|
-
? `Agent "${shortDesc}" failed: ${failReasonFork}${ccElapsedTag(
|
|
2567
|
-
: `${ccCompletionText(shortDesc, settledBg, String(child.status),
|
|
2661
|
+
? `Agent "${shortDesc}" failed: ${failReasonFork}${ccElapsedTag(forkElapsedMs)}`.slice(0, 300) + errorKindClause(errClassFork)
|
|
2662
|
+
: `${ccCompletionText(shortDesc, settledBg, String(child.status), forkElapsedMs)}`;
|
|
2568
2663
|
const ownsTerminalFacesFork = bg.registry.claimAgentTerminalNotify(taskId);
|
|
2569
2664
|
if (ownsTerminalFacesFork)
|
|
2570
2665
|
sinkEmit({
|
|
@@ -2580,7 +2675,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2580
2675
|
summary: forkTerminalSummary,
|
|
2581
2676
|
...residualFork,
|
|
2582
2677
|
resumable: resumableFork,
|
|
2583
|
-
usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats) },
|
|
2678
|
+
usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats), ...delegatedGranularityFields(child.stats, forkElapsedMs) },
|
|
2584
2679
|
...(completionIdFork !== undefined ? { completionId: completionIdFork } : {}),
|
|
2585
2680
|
});
|
|
2586
2681
|
try {
|
|
@@ -2594,12 +2689,13 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2594
2689
|
summary: forkTerminalSummary,
|
|
2595
2690
|
...(failReasonFork !== undefined ? { error: failReasonFork.slice(0, REPORT_FIELD_MAX) } : {}),
|
|
2596
2691
|
...(settledBg === "failed" && errCodeFork !== undefined ? { errorCode: errCodeFork } : {}),
|
|
2692
|
+
...providerFaultField(child),
|
|
2597
2693
|
...(child.sessionId ? { sessionId: child.sessionId } : {}),
|
|
2598
2694
|
...(forkBgResult ? { result: notifyResultField(forkBgResult) } : {}),
|
|
2599
2695
|
...(settledBg === "killed" && child.result ? { partial: true } : {}),
|
|
2600
2696
|
...residualFork,
|
|
2601
2697
|
resumable: resumableFork,
|
|
2602
|
-
usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats) },
|
|
2698
|
+
usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats), ...delegatedGranularityFieldsWire(child.stats, forkElapsedMs) },
|
|
2603
2699
|
...(completionIdFork !== undefined ? { completionId: completionIdFork } : {}),
|
|
2604
2700
|
}, { priority: "next" });
|
|
2605
2701
|
}
|
|
@@ -2912,7 +3008,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2912
3008
|
...(ctx.taskId !== undefined && ctx.taskId !== ctx.sessionId ? { parentTaskId: ctx.taskId } : {}),
|
|
2913
3009
|
...(ctx.sessionId !== undefined ? { parentSessionId: ctx.sessionId } : {}),
|
|
2914
3010
|
...((ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}),
|
|
2915
|
-
...(
|
|
3011
|
+
...(rowModelKey !== undefined ? { model: rowModelKey } : {}),
|
|
2916
3012
|
deliveryChannel: "attaching",
|
|
2917
3013
|
toolUseId: ctx.toolCallId,
|
|
2918
3014
|
...(bg.agentStore !== undefined ? { store: bg.agentStore } : {}),
|
|
@@ -2936,7 +3032,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2936
3032
|
chargeDelegationEntryHandle(bg.registry, capLedgerKey, bg.agentStore, reviveRow === undefined ? taskId : undefined);
|
|
2937
3033
|
childInternals.peerSelfRef?.addAxis("h", taskId);
|
|
2938
3034
|
if (agentName !== undefined) {
|
|
2939
|
-
recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...(
|
|
3035
|
+
recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...(reviveRow !== undefined ? (reviveRow.model !== undefined ? { model: reviveRow.model } : {}) : rowModelKey !== undefined ? { model: rowModelKey } : {}), ...(modelFallback !== undefined ? { modelFallback } : {}), ...(reviveRow !== undefined ? ((reviveRow.rootSessionId ?? reviveRow.parentSessionId) !== undefined ? { rootSessionId: reviveRow.rootSessionId ?? reviveRow.parentSessionId } : {}) : (ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: reviveRow?.spawnedAt ?? Date.now() }, (err) => opts.onObserverError?.(err, { site: "roster.recordSpawn" }));
|
|
2940
3036
|
}
|
|
2941
3037
|
const bgSink = ctx.onBackgroundChildEvent;
|
|
2942
3038
|
const sinkEmit = (event) => {
|
|
@@ -2992,6 +3088,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2992
3088
|
...(bgScope !== undefined ? { scope: bgScope } : {}),
|
|
2993
3089
|
description: reviveRow !== undefined ? `${shortDesc} (revived)` : shortDesc,
|
|
2994
3090
|
agentType: spawnAgentType,
|
|
3091
|
+
...(spawnModel !== undefined ? { model: spawnModel } : {}),
|
|
2995
3092
|
...(bgCycleSeq !== undefined ? { seq: bgCycleSeq } : {}),
|
|
2996
3093
|
...(agentName !== undefined ? { name: agentName } : {}),
|
|
2997
3094
|
sessionId: bgChildSessionId,
|
|
@@ -3065,6 +3162,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
3065
3162
|
parentToolCallId: ctx.toolCallId,
|
|
3066
3163
|
progressTaskId: bgChildSessionId,
|
|
3067
3164
|
agentType: spawnAgentType,
|
|
3165
|
+
...(spawnModel !== undefined ? { model: spawnModel } : {}),
|
|
3068
3166
|
...(bgCycleSeq !== undefined ? { seq: bgCycleSeq } : {}),
|
|
3069
3167
|
...(currentAction !== undefined ? { currentAction } : {}),
|
|
3070
3168
|
...(currentTool !== undefined ? { currentTool } : {}),
|
|
@@ -3111,6 +3209,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
3111
3209
|
progressTaskId: e.taskId,
|
|
3112
3210
|
...(e.parentTaskId !== undefined ? { progressParentTaskId: e.parentTaskId } : {}),
|
|
3113
3211
|
agentType: spawnAgentType,
|
|
3212
|
+
...(spawnModel !== undefined ? { model: spawnModel } : {}),
|
|
3114
3213
|
...(e.name !== undefined ? { name: e.name } : {}),
|
|
3115
3214
|
...(currentAction !== undefined ? { currentAction } : {}),
|
|
3116
3215
|
...(currentTool !== undefined ? { currentTool } : {}),
|
|
@@ -3399,9 +3498,10 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
3399
3498
|
named: agentName !== undefined,
|
|
3400
3499
|
});
|
|
3401
3500
|
const failReasonBg = settled === "failed" ? unparkedPauseReason ?? child.errorMessage : undefined;
|
|
3501
|
+
const bgElapsedMs = Date.now() - bgStartedAt;
|
|
3402
3502
|
const bgTerminalSummary = failReasonBg !== undefined
|
|
3403
|
-
? `Agent "${shortDesc}" failed: ${failReasonBg}${ccElapsedTag(
|
|
3404
|
-
: `${ccCompletionText(shortDesc, settled, String(child.status),
|
|
3503
|
+
? `Agent "${shortDesc}" failed: ${failReasonBg}${ccElapsedTag(bgElapsedMs)}${observerNote}`.slice(0, 300) + errorKindClause(errClassBg)
|
|
3504
|
+
: `${ccCompletionText(shortDesc, settled, String(child.status), bgElapsedMs)}${observerNote}`;
|
|
3405
3505
|
const ownsTerminalFaces = bg.registry.claimAgentTerminalNotify(taskId);
|
|
3406
3506
|
if (ownsTerminalFaces)
|
|
3407
3507
|
sinkEmit({
|
|
@@ -3416,7 +3516,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
3416
3516
|
summary: bgTerminalSummary,
|
|
3417
3517
|
...residual,
|
|
3418
3518
|
resumable: resumableBg,
|
|
3419
|
-
usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats) },
|
|
3519
|
+
usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats), ...delegatedGranularityFields(child.stats, bgElapsedMs) },
|
|
3420
3520
|
...(completionIdBg !== undefined ? { completionId: completionIdBg } : {}),
|
|
3421
3521
|
});
|
|
3422
3522
|
const completionFrame = {
|
|
@@ -3429,12 +3529,13 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
3429
3529
|
summary: bgTerminalSummary,
|
|
3430
3530
|
...(failReasonBg !== undefined ? { error: failReasonBg.slice(0, REPORT_FIELD_MAX) } : {}),
|
|
3431
3531
|
...(settled === "failed" && errCodeBg !== undefined ? { errorCode: errCodeBg } : {}),
|
|
3532
|
+
...providerFaultField(child),
|
|
3432
3533
|
...(child.sessionId ? { sessionId: child.sessionId } : {}),
|
|
3433
3534
|
...(bgHandbackResult ? { result: notifyResultField(bgHandbackResult) } : {}),
|
|
3434
3535
|
...(settled === "killed" && child.result ? { partial: true } : {}),
|
|
3435
3536
|
...residual,
|
|
3436
3537
|
resumable: resumableBg,
|
|
3437
|
-
usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats) },
|
|
3538
|
+
usage: { tokens: child.stats.tokens, turns: child.stats.turns, ...delegatedCostField(child.stats), ...delegatedGranularityFieldsWire(child.stats, bgElapsedMs) },
|
|
3438
3539
|
...(completionIdBg !== undefined ? { completionId: completionIdBg } : {}),
|
|
3439
3540
|
};
|
|
3440
3541
|
const gateOwner = ownsTerminalFaces && settled === "completed" && notify !== undefined ? bgRetain?.childSessionId : undefined;
|
|
@@ -166,6 +166,14 @@ export function validateFileHistoryExport(data) {
|
|
|
166
166
|
return bad(`tracked row ${t.relOrAbsPath} carries a non-integer version number`);
|
|
167
167
|
if (v.blobHash !== null && (typeof v.blobHash !== "string" || !/^[0-9a-f]{64}$/.test(v.blobHash)))
|
|
168
168
|
return bad(`tracked row ${t.relOrAbsPath} carries a blob reference that is not a sha256 digest`);
|
|
169
|
+
if (v.mode !== undefined) {
|
|
170
|
+
if (typeof v.mode !== "number" || !Number.isInteger(v.mode) || v.mode < 0 || v.mode > 0o7777) {
|
|
171
|
+
return bad(`tracked row ${t.relOrAbsPath} carries a mode outside the permission-bit domain [0, 0o7777] — refused, never coerced`);
|
|
172
|
+
}
|
|
173
|
+
if ((v.mode & 0o6000) !== 0) {
|
|
174
|
+
return bad(`tracked row ${t.relOrAbsPath} carries a set-ID mode (0o${v.mode.toString(8)}) — a restore replaying setuid/setgid bits would mint a privilege-escalation primitive; refused`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
169
177
|
}
|
|
170
178
|
if (new Set(t.versions.map((v) => v.version)).size !== t.versions.length)
|
|
171
179
|
return bad(`tracked row ${t.relOrAbsPath} carries the same version number twice — a chain whose versions are ambiguous cannot be published`);
|
|
@@ -284,13 +292,15 @@ export async function applyFileRestoreViaEnv(env, absPath, target, opts) {
|
|
|
284
292
|
return { outcome: "identical" };
|
|
285
293
|
if (modeOnly === "applied")
|
|
286
294
|
return { outcome: "applied" };
|
|
295
|
+
if (modeOnly === "applied_unpreserved")
|
|
296
|
+
return { outcome: "applied", modeNotPreserved: true };
|
|
287
297
|
return { outcome: "identical", modeNotPreserved: true };
|
|
288
298
|
}
|
|
289
299
|
const w = await writeRestoredBytes(env, absPath, target.bytes, signal);
|
|
290
300
|
if (!w.ok)
|
|
291
301
|
return { outcome: w.refused ? "refused" : "failed", reason: `write: ${w.code}` };
|
|
292
302
|
const modeAfter = await applyRecordedMode(env, absPath, opts, undefined);
|
|
293
|
-
return { outcome: "applied", ...(modeAfter === "unpreserved" ? { modeNotPreserved: true } : {}) };
|
|
303
|
+
return { outcome: "applied", ...(modeAfter === "unpreserved" || modeAfter === "applied_unpreserved" ? { modeNotPreserved: true } : {}) };
|
|
294
304
|
}
|
|
295
305
|
async function restorePreflight(env, absPath, opts) {
|
|
296
306
|
const info = await env.fileInfo(absPath, opts.signal);
|
|
@@ -337,7 +347,10 @@ export async function previewFileDelta(env, absPath, targetHash, targetBytes, ta
|
|
|
337
347
|
return undefined;
|
|
338
348
|
const info = await env.fileInfo(absPath);
|
|
339
349
|
const currentMode = info.ok ? info.value.mode : undefined;
|
|
340
|
-
|
|
350
|
+
const replayMode = (targetMode & 0o6000) !== 0 ? targetMode & 0o1777 : targetMode;
|
|
351
|
+
return modeProvenDifferent(targetMode, currentMode) && modeProvenDifferent(replayMode, currentMode)
|
|
352
|
+
? { insertions: 0, deletions: 0 }
|
|
353
|
+
: undefined;
|
|
341
354
|
}
|
|
342
355
|
return countRestoreLineDiff(curBytes, targetBytes);
|
|
343
356
|
}
|
|
@@ -347,6 +360,15 @@ async function applyRecordedMode(env, absPath, opts, currentMode) {
|
|
|
347
360
|
return "unchanged";
|
|
348
361
|
if (currentMode === (want & 0o7777))
|
|
349
362
|
return "unchanged";
|
|
363
|
+
if ((want & 0o6000) !== 0) {
|
|
364
|
+
const stripped = want & 0o1777;
|
|
365
|
+
if (currentMode === stripped)
|
|
366
|
+
return "unpreserved";
|
|
367
|
+
if (env.setFileMode === undefined)
|
|
368
|
+
return "unpreserved";
|
|
369
|
+
const r = await env.setFileMode(absPath, stripped, opts.signal);
|
|
370
|
+
return r.ok ? "applied_unpreserved" : "unpreserved";
|
|
371
|
+
}
|
|
350
372
|
if (env.setFileMode === undefined)
|
|
351
373
|
return "unpreserved";
|
|
352
374
|
const r = await env.setFileMode(absPath, want & 0o7777, opts.signal);
|
package/dist/core/roles.js
CHANGED
|
@@ -3,7 +3,7 @@ export function resolveModel(ref, models) {
|
|
|
3
3
|
if (typeof ref !== "string") {
|
|
4
4
|
return ref;
|
|
5
5
|
}
|
|
6
|
-
const m = models
|
|
6
|
+
const m = models !== undefined && Object.hasOwn(models, ref) ? models[ref] : undefined;
|
|
7
7
|
if (!m) {
|
|
8
8
|
throw new Error(`Unknown model ref "${ref}". Provide it in RunnerDeps.models or pass a Model object.`);
|
|
9
9
|
}
|
|
@@ -1410,6 +1410,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
|
|
|
1410
1410
|
...(internals?.cycleSeq !== undefined ? { seq: internals.cycleSeq } : {}),
|
|
1411
1411
|
...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
|
|
1412
1412
|
...(subagentName ? { name: subagentName } : {}),
|
|
1413
|
+
model: prepared.model.id,
|
|
1413
1414
|
usage: { totalTokens: stats.tokens, toolUses: stats.toolCalls, durationMs: Date.now() - rs.telemetry.taskStart },
|
|
1414
1415
|
status: "running",
|
|
1415
1416
|
...ident(),
|
|
@@ -4223,6 +4224,7 @@ export class Runner {
|
|
|
4223
4224
|
...(internals?.cycleSeq !== undefined ? { seq: internals.cycleSeq } : {}),
|
|
4224
4225
|
...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
|
|
4225
4226
|
...(subagentName ? { name: subagentName } : {}),
|
|
4227
|
+
model: prepared.model.id,
|
|
4226
4228
|
usage: { totalTokens: stats.tokens, toolUses: stats.toolCalls, durationMs: Date.now() - rs.telemetry.taskStart },
|
|
4227
4229
|
status: result.status === "completed" ? "completed" : "failed",
|
|
4228
4230
|
...ident(),
|
|
@@ -66,6 +66,19 @@ export interface TaskNotificationPayload {
|
|
|
66
66
|
* `TaskResult.errorCode` taxonomy (brain `[code]` prefixes via extractErrorCode, `limit.*`,
|
|
67
67
|
* `budget.*`, …). Engine-minted, never model text. Absent when the failure carried no code. */
|
|
68
68
|
errorCode?: string;
|
|
69
|
+
/** Terminal, delegation lanes: the settling run's PROVIDER-BOUNDARY fault assertion, mirrored from
|
|
70
|
+
* its `TaskResult.apiFailure`. Its PRESENCE is the claim — "this failure came from the transport or
|
|
71
|
+
* the provider, not from this deployment refusing to send, a limit, or unusable model output" — and
|
|
72
|
+
* `errorCode` beside it names WHICH terminal; the two answer different questions and neither implies
|
|
73
|
+
* the other. Members are whatever the failing attempt stated about itself (`status` when the provider
|
|
74
|
+
* answered with one, `requestId` when it named one), so an EMPTY object is meaningful: "provider
|
|
75
|
+
* fault, unlabelled", which is not the same statement as absence. Absent on every non-provider
|
|
76
|
+
* terminal, on every completed run, and on the non-delegation lanes (bash/monitor), whose failures do
|
|
77
|
+
* not pass through a model provider at all. */
|
|
78
|
+
apiFailure?: {
|
|
79
|
+
status?: number;
|
|
80
|
+
requestId?: string;
|
|
81
|
+
};
|
|
69
82
|
/** Structured exit code of a background command's terminal notification (bash lane): the process
|
|
70
83
|
* exited on its own with this code. Lets consumers branch on success/failure without parsing the
|
|
71
84
|
* summary wording. Absent when the process never exited by itself (killed / spawn-failed lanes). */
|
|
@@ -77,6 +90,13 @@ export interface TaskNotificationPayload {
|
|
|
77
90
|
* every other notification (a completed result is never flagged). */
|
|
78
91
|
partial?: boolean;
|
|
79
92
|
output_file?: string;
|
|
93
|
+
/** Whatever the settling lane knows about its own spend, JSON-rendered verbatim into one `<usage>`
|
|
94
|
+
* tag. Deliberately untyped — the lanes report different quantities and nothing here should force
|
|
95
|
+
* one to fabricate a figure it does not have. What IS contracted is the VOCABULARY: a name that two
|
|
96
|
+
* lanes both publish must mean the same thing on both. The delegation lanes
|
|
97
|
+
* (`background_agent`, its fork/revive cycles) and the workflow lane therefore agree on
|
|
98
|
+
* `tokens`/`turns`/`costMicroUsd`/`tool_uses`/`duration_ms`; the workflow lane's fan-out counters
|
|
99
|
+
* (`agent_count`, `agents_done`, …) and resume counters have no delegation analog and are its own. */
|
|
80
100
|
usage?: unknown;
|
|
81
101
|
/** CC `<diagnostics>` parity: ENGINE-MINTED teaching text for the "result is empty/unexpected —
|
|
82
102
|
* now what" moment (per-agent read route / journal coordinate / resume command). Producers must mint it
|
package/dist/core/types.d.ts
CHANGED
|
@@ -4646,6 +4646,31 @@ export type TaskEvent = ({
|
|
|
4646
4646
|
* nor an agent-type — the child then keeps its taskId (NOT the raw objective, which could leak a delegated
|
|
4647
4647
|
* secret to a progress-scoped consumer — dual-review Q2). Sanitized (control-char-stripped, length-capped). */
|
|
4648
4648
|
name?: string;
|
|
4649
|
+
/**
|
|
4650
|
+
* The model this sub-run was PREPARED with — the resolved id, read off the leg's prepared model
|
|
4651
|
+
* at the mint, so it is the same value `TaskResult.model` and the `task.start` trace frame carry.
|
|
4652
|
+
* (`task.end` reports the run's totals and has never named a model; there is nothing to join to
|
|
4653
|
+
* there.)
|
|
4654
|
+
*
|
|
4655
|
+
* Why it rides the live lane at all: a delegation may name a tier word (`"sonnet"`), an agent
|
|
4656
|
+
* definition's model, or NOTHING (inherit the caller's current model, or fall to the `subagent`
|
|
4657
|
+
* role) — every one of those resolves somewhere the consumer cannot see, so a shell badging a
|
|
4658
|
+
* running child previously had only the REQUESTED word (or nothing) to render, which is a
|
|
4659
|
+
* different claim from what the child runs on. This is the resolved answer, not the request.
|
|
4660
|
+
*
|
|
4661
|
+
* PREPARED, deliberately, and NOT "whatever is serving this turn" — stated in the first sentence
|
|
4662
|
+
* because the difference is observable. A mid-run DEGRADE switch (`spec.limits.degrade`) or a
|
|
4663
|
+
* gateway re-route moves the serving model without rewriting this field, exactly as
|
|
4664
|
+
* `TaskResult.model` behaves; the switch is announced on its own channel (`TaskResult.degraded`),
|
|
4665
|
+
* and the per-call served id lives on the brain-call telemetry. One name, one meaning, across the
|
|
4666
|
+
* three faces that use it — at the cost of being the leg's declared model rather than a live one.
|
|
4667
|
+
*
|
|
4668
|
+
* Present on every tick this build mints (the frame family is subagent-only by construction, so
|
|
4669
|
+
* there is no lane where a leg has no prepared model). Declared optional for the ordinary reason:
|
|
4670
|
+
* a consumer folding frames from a pre-key producer must keep compiling, and absence there means
|
|
4671
|
+
* "this producer did not state it", never "no model".
|
|
4672
|
+
*/
|
|
4673
|
+
model?: string;
|
|
4649
4674
|
/** The child's most recent tool intent as one human line ("Bash npm test",
|
|
4650
4675
|
* "Edit src/x.ts") — the SAME source/value as the registry sink tick's `currentAction` (residual
|
|
4651
4676
|
* observability, lane B), attached to the FORWARDED frame because every client wire projects this
|
|
@@ -5246,6 +5271,40 @@ export interface BackgroundChildEvent {
|
|
|
5246
5271
|
* waiting for spawn-frame forwarding; `name` stays the design/99 DISPLAY label (description-backed)
|
|
5247
5272
|
* and was never a type field. */
|
|
5248
5273
|
agentType?: string;
|
|
5274
|
+
/**
|
|
5275
|
+
* spawn + tick: the model the ROW runs on — the resolved `ModelRef` the spawner selected for this
|
|
5276
|
+
* child (a per-call `model`, an agent definition's, or the caller's own current model, inherited).
|
|
5277
|
+
* It answers with the SERVED catalog model id wherever that is knowable, because this value sits
|
|
5278
|
+
* beside the child's own `task_progress.model` (the leg's prepared id) and a consumer joins the
|
|
5279
|
+
* two: a row saying "sonnet" next to ticks naming the catalog id it routed to is indistinguishable
|
|
5280
|
+
* from two different children. A `Model` object carries its id — and a per-call word that passed
|
|
5281
|
+
* the spawn gate arrives as exactly that resolved object, so for it the row and the ticks are one
|
|
5282
|
+
* value by construction. A definition/tool-level STRING ref resolves through the catalog in force
|
|
5283
|
+
* at the spawn judgement; the child's own prepare re-resolves such a ref against the runner's live
|
|
5284
|
+
* table, so for those refs alone a catalog hot-swapped between the two reads can lag the row one
|
|
5285
|
+
* generation behind the ticks. The DISPLAY rule survives only as the FALLBACK, for a string the
|
|
5286
|
+
* judgement-time catalog cannot resolve (a CC tier alias then shows its sema tier, never the alias
|
|
5287
|
+
* verbatim). It rides ticks as well as spawn for the same reason
|
|
5288
|
+
* {@link agentType} does: a consumer that only forwards ticks must not have to wait for spawn-frame
|
|
5289
|
+
* forwarding to fill its row. Like `agentType` it is the ROW's own fact and is NEVER copied off a
|
|
5290
|
+
* forwarded frame — a nested descendant's `task_progress` names ITS model, not this row's.
|
|
5291
|
+
*
|
|
5292
|
+
* ABSENT is a fact, not a gap, and there are exactly three ways to get there:
|
|
5293
|
+
* · the delegation named no model anywhere in the chain, so the child runs on the `subagent` ROLE
|
|
5294
|
+
* and its concrete id is only decided at the child's own prepare;
|
|
5295
|
+
* · the RETAIN-LEDGER wake lane (a `SendMessage` resume of a completed child), whose frames are
|
|
5296
|
+
* projected from an `AccessibleTaskRow` — and that row has no model column, so this lane has
|
|
5297
|
+
* nothing to state and will not invent one. Stated precisely because the sibling wake lane does
|
|
5298
|
+
* NOT share the limitation: a tier-3 DURABLE revive re-enters through the ordinary background
|
|
5299
|
+
* spawn, re-derives the model like a first spawn, and its `(revived)` frames carry it whenever
|
|
5300
|
+
* that derivation lands on a model (the durable record keeps a `model` of its own; a recorded
|
|
5301
|
+
* key the current catalog no longer resolves degrades the revival to the inherited model, and a
|
|
5302
|
+
* mount with nothing to inherit then leaves these frames honestly silent);
|
|
5303
|
+
* · a pre-key producer.
|
|
5304
|
+
* In every case the child's own `task_progress` frames still carry the resolved answer, which is the
|
|
5305
|
+
* authority; this field is the row-level convenience beside it.
|
|
5306
|
+
*/
|
|
5307
|
+
model?: string;
|
|
5249
5308
|
/** spawn: the HOST task's DECLARED task id (parent attribution). Omitted when the host
|
|
5250
5309
|
* run declared no task id (the `spec.taskId ?? sessionId` fallback would launder a session id into
|
|
5251
5310
|
* a task-id field — the orphan-pointer shape); {@link parentSessionId} is the always-on linkage. */
|
|
@@ -5359,7 +5418,21 @@ export interface BackgroundChildEvent {
|
|
|
5359
5418
|
stoppedBy?: "user" | "parent" | "system" | (string & {});
|
|
5360
5419
|
/** terminal: bounded human summary (same text the task_notification carries). */
|
|
5361
5420
|
summary?: string;
|
|
5362
|
-
/** tick: live rollup (`task_progress.usage`) · terminal:
|
|
5421
|
+
/** tick: live rollup (`task_progress.usage`) · terminal: the settled
|
|
5422
|
+
* `{tokens, turns, costMicroUsd?, toolUses?, durationMs}`. `costMicroUsd` is own+nested and present
|
|
5423
|
+
* only when that total is KNOWN (RB-368); `toolUses` is the run's own `stats.toolCalls` and is
|
|
5424
|
+
* absent — never zeroed — when the gateway reported no usage. The last two joined the terminal face
|
|
5425
|
+
* to close a reporting asymmetry: the workflow lane's completion notification and the Agent tool's
|
|
5426
|
+
* own sync `<usage>` footer had published both all along, so one child answered "how much work, how
|
|
5427
|
+
* long" on one lane and refused on the other.
|
|
5428
|
+
*
|
|
5429
|
+
* `durationMs` MEASURES DIFFERENT SPANS on the two kinds, which is worth knowing before plotting it:
|
|
5430
|
+
* on a TICK it is the child RUN's own elapsed (the forwarded frame's value, from the run's start);
|
|
5431
|
+
* on the TERMINAL it is the delegation LEG's, from the lane's launch instant — which is earlier, so
|
|
5432
|
+
* a consumer graphing one series sees a step up at settle. Both are true of what they name; neither
|
|
5433
|
+
* can be computed from the other without the spawn/prepare interval, which is why they are not
|
|
5434
|
+
* reconciled into one. The intra-turn activity beat carries a third, narrower shape (`{toolUses}`
|
|
5435
|
+
* alone — there is no honest live token figure on that lane). */
|
|
5363
5436
|
usage?: {
|
|
5364
5437
|
totalTokens?: number;
|
|
5365
5438
|
toolUses?: number;
|
|
@@ -133,7 +133,12 @@ export interface WiringManifest {
|
|
|
133
133
|
* a completed/failed/killed(non-user) subagent minted through the deps-visible assembly is
|
|
134
134
|
* continuable across a process restart. A statement about the DEPS-VISIBLE assembly only: a
|
|
135
135
|
* caller-mounted Agent tool over its own runner is outside this manifest's sight (per-read
|
|
136
|
-
* honest degrade + the integrity notice own that case, never a fabricated tier)
|
|
136
|
+
* honest degrade + the integrity notice own that case, never a fabricated tier) — and that
|
|
137
|
+
* blind spot is load-bearing here, because the store this field is derived from is the ROOT
|
|
138
|
+
* leg's while the store an ordinary child transcript is actually minted through is the AGENT
|
|
139
|
+
* TOOL's runner's ({@link resolveSubagentTranscriptTier} names the rule and the fork-lane
|
|
140
|
+
* exception). The word names ADDRESSABILITY: `none`/`rows` are silent on whether a child
|
|
141
|
+
* transcript reaches disk at all, and only `full` promises the a* handle survives a restart. */
|
|
137
142
|
fleet: {
|
|
138
143
|
backgroundAgentStore: boolean;
|
|
139
144
|
hostChildEventSink: boolean;
|
|
@@ -254,6 +259,18 @@ export type SubagentTranscriptTier = "none" | "rows" | "full";
|
|
|
254
259
|
* fold picked). This is the DEPLOYMENT-level judgment only; the per-handle "did this row's
|
|
255
260
|
* transcript actually land" question belongs to `durableAgentRowProbe` (the S1b release-flip gate)
|
|
256
261
|
* — two different questions, deliberately two named faces (do not merge them back into one).
|
|
262
|
+
*
|
|
263
|
+
* WHICH session store to pass: the one on the runner the Agent tool was mounted with
|
|
264
|
+
* (`SubagentToolOptions.runner.sessions`), because that is the store an ordinary (sync/background)
|
|
265
|
+
* child transcript is minted through — the fork lane deliberately branches through the HOST store
|
|
266
|
+
* instead, see the law on `SubagentToolOptions.runner`. A deployment that serves root tasks from one
|
|
267
|
+
* runner and delegated children from another (a throwaway sub-task store, a routing store) has two
|
|
268
|
+
* different stores here, and passing the ROOT deps' store reports the root lane's fate under the
|
|
269
|
+
* child lane's name. Note what the three words do and do not promise about DISK: this tier names
|
|
270
|
+
* ADDRESSABILITY, not persistence. `none` is returned before any store is inspected, so it covers
|
|
271
|
+
* both "the transcript is on disk but nothing can address it" and "there is no transcript" — an
|
|
272
|
+
* operator asking whether a child conversation is readable afterwards must read the delegation
|
|
273
|
+
* store's own durability + release/evict semantics, not this word.
|
|
257
274
|
*/
|
|
258
275
|
export declare function resolveSubagentTranscriptTier(agentStoreWired: boolean, sessionStore: {
|
|
259
276
|
readonly placements?: {
|
|
@@ -3,6 +3,7 @@ import { defineTool, errorResult } from "../core/tools.js";
|
|
|
3
3
|
import { governanceBaselineError, governanceBaselineProblem } from "./governance-baseline-validity.js";
|
|
4
4
|
import { redactSecrets, redactHostLeaks, boundedRedactedSummary } from "../core/untrusted-egress.js";
|
|
5
5
|
import { withDelegationProvenance } from "../core/tool-policy.js";
|
|
6
|
+
import { LAUNCH_RECEIPT_OWN_WORDS_CLAUSE, launchReceiptNoQuoteClause } from "../agents/launch-receipt-contract.js";
|
|
6
7
|
import { startWorkflow } from "./workflow.js";
|
|
7
8
|
import { buildWorkflowPrimitives } from "./workflow-primitives.js";
|
|
8
9
|
import { parseWorkflowMeta, splitWorkflowMeta, workflowScriptReadsClockOrRandom } from "./workflow-meta.js";
|
|
@@ -630,8 +631,7 @@ export async function createRunWorkflowTool(d) {
|
|
|
630
631
|
task_id: runId,
|
|
631
632
|
status: "started",
|
|
632
633
|
...(persistedScriptPath !== undefined ? { scriptPath: persistedScriptPath } : {}),
|
|
633
|
-
handling: "
|
|
634
|
-
"In your own words, briefly tell the user what you launched; do not echo this result.",
|
|
634
|
+
handling: `${launchReceiptNoQuoteClause(" (the ids above, and scriptPath when present)")} ${LAUNCH_RECEIPT_OWN_WORDS_CLAUSE}`,
|
|
635
635
|
note: (() => {
|
|
636
636
|
const pollExpr = d.taskRegistry ? `TaskOutput({ task_id: "${runId}" })` : undefined;
|
|
637
637
|
const blockingPollExpr = d.taskRegistry ? `TaskOutput({ task_id: "${runId}", block: true })` : undefined;
|
|
@@ -54,11 +54,14 @@ function forReview(text) {
|
|
|
54
54
|
return text;
|
|
55
55
|
return `${text.slice(0, MAX_REVIEWED_PROMPT_CHARS)}\n[…TRUNCATED FOR REVIEW: ${text.length - MAX_REVIEWED_PROMPT_CHARS} further characters follow that the child WILL receive and this review did NOT see]`;
|
|
56
56
|
}
|
|
57
|
-
function workflowModelLabel(spec) {
|
|
57
|
+
function workflowModelLabel(spec, catalog) {
|
|
58
58
|
const model = spec.model;
|
|
59
59
|
if (model === undefined)
|
|
60
60
|
return undefined;
|
|
61
|
-
|
|
61
|
+
if (typeof model !== "string")
|
|
62
|
+
return model.id ?? model.name;
|
|
63
|
+
const served = catalog?.[model]?.id;
|
|
64
|
+
return served ?? resolveModelDisplayLabel(model);
|
|
62
65
|
}
|
|
63
66
|
export const WORKFLOW_SUBAGENT_PROMPT = `You are a subagent spawned by a workflow orchestration script. Use the tools available to complete the task.
|
|
64
67
|
|
|
@@ -441,6 +444,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
441
444
|
}
|
|
442
445
|
const concurrency = normalizeConcurrency(opts.concurrency);
|
|
443
446
|
const agentRegistry = workflowAgentRegistry(opts);
|
|
447
|
+
const modelCatalog = runner.agentCatalog?.models;
|
|
444
448
|
const store = opts.store;
|
|
445
449
|
const maxAgents = normalizeWorkflowHardCap("maxAgents", opts.maxAgents);
|
|
446
450
|
const maxLogChars = normalizeWorkflowHardCap("maxLogChars", opts.maxLogChars);
|
|
@@ -633,11 +637,11 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
633
637
|
const bceParentToolCall = opts.parentToolCallId !== undefined ? { parentToolCallId: opts.parentToolCallId } : {};
|
|
634
638
|
const waIdOf = (callKey) => `wa${createHash("sha256").update(`${runId}:${callKey}`).digest("hex").slice(0, 16)}`;
|
|
635
639
|
const bceLive = new Map();
|
|
636
|
-
const bceSpawn = (callKey, label, agentType, replayed, sessionId) => {
|
|
640
|
+
const bceSpawn = (callKey, label, agentType, replayed, sessionId, model) => {
|
|
637
641
|
if (!bceSink)
|
|
638
642
|
return;
|
|
639
643
|
const id = waIdOf(callKey);
|
|
640
|
-
bceLive.set(id, { callKey, label, ...(agentType !== undefined ? { agentType } : {}), ...(sessionId !== undefined ? { sessionId } : {}) });
|
|
644
|
+
bceLive.set(id, { callKey, label, ...(agentType !== undefined ? { agentType } : {}), ...(sessionId !== undefined ? { sessionId } : {}), ...(model !== undefined ? { model } : {}) });
|
|
641
645
|
bceEmit({
|
|
642
646
|
kind: "spawn",
|
|
643
647
|
taskId: id,
|
|
@@ -648,6 +652,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
648
652
|
...(scope !== undefined ? { scope } : {}),
|
|
649
653
|
description: replayed ? `${label} (replayed)` : label,
|
|
650
654
|
agentType: agentType ?? "workflow-agent",
|
|
655
|
+
...(model !== undefined ? { model } : {}),
|
|
651
656
|
name: label,
|
|
652
657
|
...(opts.parentTaskId !== undefined ? { parentTaskId: opts.parentTaskId } : {}),
|
|
653
658
|
workflowRunId: runId,
|
|
@@ -679,6 +684,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
679
684
|
workflowRunId: runId,
|
|
680
685
|
...(scope !== undefined ? { scope } : {}),
|
|
681
686
|
...(row.agentType !== undefined ? { agentType: row.agentType } : { agentType: "workflow-agent" }),
|
|
687
|
+
...(row.model !== undefined ? { model: row.model } : {}),
|
|
682
688
|
...(row.sessionId !== undefined ? { sessionId: row.sessionId, transcriptId: row.sessionId } : {}),
|
|
683
689
|
name: e.name ?? row.label,
|
|
684
690
|
progressTaskId: e.taskId,
|
|
@@ -1003,7 +1009,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1003
1009
|
const specForIdentity = inheritedModelSnap !== undefined ? { ...spec, model: inheritedModelSnap } : spec;
|
|
1004
1010
|
const callKey = workflowAgentCallKey(run.agents.length, specForIdentity, agentOpts);
|
|
1005
1011
|
const prompt = boundedRedactedSummary(spec.systemPrompt ? `${spec.systemPrompt}\n\n${spec.objective}` : spec.objective, MAX_TRANSCRIPT_CHARS);
|
|
1006
|
-
const
|
|
1012
|
+
const specForLabel = spec.model === undefined && typeDefModel !== undefined ? { ...spec, model: typeDefModel } : specForIdentity;
|
|
1013
|
+
const model = workflowModelLabel(specForLabel, modelCatalog);
|
|
1007
1014
|
return { label, phase, phaseInstance, groupId, inheritedModelSnap, callKey, prompt, model };
|
|
1008
1015
|
};
|
|
1009
1016
|
const reviewSpawnBeforeLaunch = async (lane, label, callKey, runSpec, effectiveSignal) => {
|
|
@@ -1161,13 +1168,14 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1161
1168
|
const rs = r.stats;
|
|
1162
1169
|
const cachedOutput = boundedRedactedSummary(r.structuredOutput ?? r.result, MAX_TRANSCRIPT_CHARS);
|
|
1163
1170
|
const at = now();
|
|
1171
|
+
const replayModel = r.model;
|
|
1164
1172
|
const replayRec = {
|
|
1165
1173
|
label,
|
|
1166
1174
|
callKey,
|
|
1167
1175
|
...(groupId !== undefined ? { groupId } : {}),
|
|
1168
1176
|
phase,
|
|
1169
1177
|
prompt,
|
|
1170
|
-
...(
|
|
1178
|
+
...(replayModel !== undefined ? { model: replayModel } : {}),
|
|
1171
1179
|
replayed: true,
|
|
1172
1180
|
status: r.status === "completed" ? "completed" : "failed",
|
|
1173
1181
|
taskStatus: r.status,
|
|
@@ -1185,9 +1193,9 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1185
1193
|
run.agents.push(replayRec);
|
|
1186
1194
|
if (phaseInstance)
|
|
1187
1195
|
agentPhaseOf.set(replayRec, phaseInstance);
|
|
1188
|
-
emit({ type: "agent_start", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), callKey, prompt, ...(
|
|
1196
|
+
emit({ type: "agent_start", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), callKey, prompt, ...(replayModel !== undefined ? { model: replayModel } : {}), replayed: true, ts: at });
|
|
1189
1197
|
emit({ type: "agent_end", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), status: replayRec.status, output: cachedOutput, ...(rs.toolCalls !== undefined ? { toolCalls: rs.toolCalls } : {}), replayed: true, ts: at });
|
|
1190
|
-
bceSpawn(callKey, label, agentOpts.agentType, true, r.sessionId || undefined);
|
|
1198
|
+
bceSpawn(callKey, label, agentOpts.agentType, true, r.sessionId || undefined, replayModel);
|
|
1191
1199
|
bceTerminal(callKey, replayRec.status === "completed" ? "completed" : "failed", cachedOutput, r.sessionId || undefined, replayRec.stats);
|
|
1192
1200
|
accumulateStats(r, false);
|
|
1193
1201
|
void persist("update");
|
|
@@ -1233,7 +1241,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1233
1241
|
}
|
|
1234
1242
|
rec.startedAt = now();
|
|
1235
1243
|
const bornChildSessionId = resolveChildSessionIdAtSpawn(spec);
|
|
1236
|
-
bceSpawn(callKey, label, agentOpts.agentType, false, bornChildSessionId);
|
|
1244
|
+
bceSpawn(callKey, label, agentOpts.agentType, false, bornChildSessionId, model);
|
|
1237
1245
|
const typedSpec0 = applyWorkflowAgentType(spec, agentOpts.agentType, agentRegistry);
|
|
1238
1246
|
const typedSpec = typedSpec0.model === undefined && inheritedModelSnap !== undefined ? { ...typedSpec0, model: inheritedModelSnap } : typedSpec0;
|
|
1239
1247
|
const framedSpec = withWorkflowChildPersona(typedSpec, agentOpts.schema ?? typedSpec.outputSchema);
|
|
@@ -1545,7 +1553,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1545
1553
|
}
|
|
1546
1554
|
rec.startedAt = now();
|
|
1547
1555
|
const childSessionId = resolveChildSessionIdAtSpawn(spec);
|
|
1548
|
-
bceSpawn(callKey, label, agentOpts.agentType, false, childSessionId);
|
|
1556
|
+
bceSpawn(callKey, label, agentOpts.agentType, false, childSessionId, model);
|
|
1549
1557
|
let stream;
|
|
1550
1558
|
try {
|
|
1551
1559
|
const typedSpec0 = applyWorkflowAgentType(spec, agentOpts.agentType, agentRegistry);
|