@sema-agent/core 5.65.0 → 7.0.0

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.
Files changed (165) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/agents/subagent.d.ts +2 -2
  3. package/dist/agents/subagent.js +11 -0
  4. package/dist/agents/verify.d.ts +1 -1
  5. package/dist/brain/anthropic.js +1 -1
  6. package/dist/brain/errors.d.ts +29 -0
  7. package/dist/brain/errors.js +20 -0
  8. package/dist/brain/open-responses.js +2 -2
  9. package/dist/brain/route-adjudicator.d.ts +8 -1
  10. package/dist/brain/route-adjudicator.js +1 -0
  11. package/dist/brain/status-sink.js +12 -1
  12. package/dist/brain/stream-engine.js +17 -6
  13. package/dist/core/auto-compaction.d.ts +26 -0
  14. package/dist/core/auto-compaction.js +7 -2
  15. package/dist/core/auto-mode-arming.d.ts +138 -0
  16. package/dist/core/auto-mode-arming.js +181 -0
  17. package/dist/core/auto-mode-defaults.d.ts +13 -0
  18. package/dist/core/auto-mode-defaults.js +5 -0
  19. package/dist/core/auto-mode-prompt.d.ts +14 -3
  20. package/dist/core/auto-mode-prompt.js +10 -7
  21. package/dist/core/auto-mode-rebuild.d.ts +75 -0
  22. package/dist/core/auto-mode-rebuild.js +41 -0
  23. package/dist/core/auto-mode.d.ts +15 -0
  24. package/dist/core/auto-mode.js +4 -2
  25. package/dist/core/checkpoint-store.d.ts +18 -0
  26. package/dist/core/context-edit.d.ts +47 -5
  27. package/dist/core/context-guard.d.ts +1 -1
  28. package/dist/core/file-history-retention.d.ts +106 -0
  29. package/dist/core/file-history-retention.js +36 -0
  30. package/dist/core/file-history-store.d.ts +768 -0
  31. package/dist/core/file-history-store.js +880 -0
  32. package/dist/core/governance-codes.d.ts +13 -3
  33. package/dist/core/governance-codes.js +15 -0
  34. package/dist/core/hooks.d.ts +39 -7
  35. package/dist/core/hooks.js +38 -21
  36. package/dist/core/lsp.d.ts +2 -2
  37. package/dist/core/mcp.d.ts +29 -7
  38. package/dist/core/memory-engine/consolidation-driver.d.ts +11 -0
  39. package/dist/core/memory-engine/consolidation-driver.js +71 -4
  40. package/dist/core/memory-engine/consolidation.d.ts +25 -2
  41. package/dist/core/memory-engine/consolidation.js +4 -1
  42. package/dist/core/memory-engine/distiller.d.ts +84 -1
  43. package/dist/core/memory-engine/distiller.js +68 -0
  44. package/dist/core/memory-engine/dual-root.js +3 -0
  45. package/dist/core/memory-engine/engine.d.ts +328 -15
  46. package/dist/core/memory-engine/engine.js +355 -29
  47. package/dist/core/memory-engine/file-backend.d.ts +30 -0
  48. package/dist/core/memory-engine/file-backend.js +14 -13
  49. package/dist/core/memory-engine/frontmatter.d.ts +22 -1
  50. package/dist/core/memory-engine/frontmatter.js +3 -0
  51. package/dist/core/memory-engine/header-hints.d.ts +5 -0
  52. package/dist/core/memory-engine/index.d.ts +5 -4
  53. package/dist/core/memory-engine/index.js +5 -4
  54. package/dist/core/memory-engine/layout.d.ts +88 -2
  55. package/dist/core/memory-engine/layout.js +112 -3
  56. package/dist/core/memory-engine/provenance-wording.d.ts +7 -0
  57. package/dist/core/memory-engine/provenance-wording.js +3 -0
  58. package/dist/core/memory-engine/tools.d.ts +89 -8
  59. package/dist/core/memory-engine/tools.js +263 -22
  60. package/dist/core/memory-engine/types.d.ts +64 -1
  61. package/dist/core/memory-recall.d.ts +6 -0
  62. package/dist/core/memory.d.ts +27 -1
  63. package/dist/core/memory.js +16 -2
  64. package/dist/core/permission-rule-consent.d.ts +62 -2
  65. package/dist/core/permission-rule-consent.js +105 -14
  66. package/dist/core/permission-rule-model.d.ts +118 -16
  67. package/dist/core/permission-rule-model.js +57 -9
  68. package/dist/core/permission-rule-session.d.ts +124 -0
  69. package/dist/core/permission-rule-session.js +121 -0
  70. package/dist/core/permission-rule-store.d.ts +65 -2
  71. package/dist/core/permission-rule-store.js +75 -16
  72. package/dist/core/permission-rule-sync.d.ts +9 -0
  73. package/dist/core/permission-rule-sync.js +52 -19
  74. package/dist/core/retention-policy.d.ts +9 -0
  75. package/dist/core/retention-policy.js +5 -2
  76. package/dist/core/retention.d.ts +13 -2
  77. package/dist/core/runner/assemble-result.d.ts +19 -1
  78. package/dist/core/runner/assemble-result.js +17 -2
  79. package/dist/core/runner/compaction-call-options.d.ts +93 -0
  80. package/dist/core/runner/compaction-call-options.js +3 -0
  81. package/dist/core/runner/memory-capture-optout.d.ts +80 -0
  82. package/dist/core/runner/memory-capture-optout.js +53 -0
  83. package/dist/core/runner/prepare-config-doors.d.ts +5 -0
  84. package/dist/core/runner/prepare-config-doors.js +16 -0
  85. package/dist/core/runner/prepare-hands-readface.d.ts +110 -5
  86. package/dist/core/runner/prepare-hands-readface.js +99 -7
  87. package/dist/core/runner/prepare-memory.d.ts +88 -0
  88. package/dist/core/runner/prepare-memory.js +305 -24
  89. package/dist/core/runner/prepare-task.d.ts +141 -1
  90. package/dist/core/runner/prepare-task.js +478 -81
  91. package/dist/core/runner/runtask.d.ts +9 -20
  92. package/dist/core/runner/runtask.js +133 -96
  93. package/dist/core/runner/session-file-state-replay.d.ts +18 -10
  94. package/dist/core/runner/session-file-state-replay.js +52 -1
  95. package/dist/core/runner/tool-disclosure.js +2 -1
  96. package/dist/core/runner/turn-attachments.d.ts +22 -12
  97. package/dist/core/session-store.d.ts +1 -1
  98. package/dist/core/session-store.js +6 -1
  99. package/dist/core/session.d.ts +34 -1
  100. package/dist/core/store-contracts/file-history-store-contract.d.ts +3 -0
  101. package/dist/core/store-contracts/file-history-store-contract.js +720 -0
  102. package/dist/core/store-contracts/permission-rule-sync-contract.js +15 -1
  103. package/dist/core/task-registry-shared.js +11 -1
  104. package/dist/core/tool-errors.js +1 -0
  105. package/dist/core/tool-policy.d.ts +172 -1
  106. package/dist/core/tool-policy.js +32 -1
  107. package/dist/core/tool-result-store.js +2 -1
  108. package/dist/core/trace.d.ts +31 -2
  109. package/dist/core/types.d.ts +795 -89
  110. package/dist/core/types.js +4 -3
  111. package/dist/core/untrusted-text.d.ts +1 -1
  112. package/dist/core/untrusted-text.js +8 -0
  113. package/dist/core/workflow-run-store-contract.js +6 -2
  114. package/dist/core/workflow-run-store.d.ts +4 -1
  115. package/dist/engine/compaction/compaction.d.ts +88 -10
  116. package/dist/engine/compaction/compaction.js +109 -30
  117. package/dist/engine/execution-env/node-execution-env.d.ts +9 -1
  118. package/dist/engine/execution-env/node-execution-env.js +28 -0
  119. package/dist/engine/harness/agent-harness.d.ts +52 -1
  120. package/dist/engine/harness/agent-harness.js +36 -1
  121. package/dist/engine/harness/types.d.ts +26 -1
  122. package/dist/engine/llm/types.d.ts +50 -4
  123. package/dist/engine/loop/agent-loop.d.ts +5 -1
  124. package/dist/engine/loop/agent-loop.js +25 -0
  125. package/dist/engine/loop/types.d.ts +19 -0
  126. package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
  127. package/dist/engine/session/session.js +1 -1
  128. package/dist/index.d.ts +20 -9
  129. package/dist/index.js +16 -7
  130. package/dist/orchestration/run-workflow-tool.d.ts +20 -2
  131. package/dist/orchestration/run-workflow-tool.js +22 -3
  132. package/dist/orchestration/workflow-governance.d.ts +59 -1
  133. package/dist/orchestration/workflow-governance.js +61 -8
  134. package/dist/orchestration/workflow-meta.d.ts +4 -2
  135. package/dist/orchestration/workflow-primitives.js +56 -13
  136. package/dist/orchestration/workflow-types.d.ts +78 -2
  137. package/dist/orchestration/workflow.d.ts +20 -0
  138. package/dist/orchestration/workflow.js +163 -14
  139. package/dist/prompt-assembly/event-registry.js +1 -1
  140. package/dist/prompts/default.d.ts +7 -7
  141. package/dist/stores/file/file-history-store.d.ts +368 -0
  142. package/dist/stores/file/file-history-store.js +1248 -0
  143. package/dist/stores/file/index.d.ts +22 -13
  144. package/dist/stores/file/index.js +4 -4
  145. package/dist/stores/file/permission-rule-store.d.ts +11 -0
  146. package/dist/stores/file/permission-rule-store.js +23 -9
  147. package/dist/stores/file/strategy-store.d.ts +3 -3
  148. package/dist/tools/fs/bash-readonly-classifier.d.ts +87 -3
  149. package/dist/tools/fs/bash-readonly-classifier.js +106 -4
  150. package/dist/tools/fs/fs-bash.js +9 -5
  151. package/dist/tools/fs/fs-shared.d.ts +52 -1
  152. package/dist/tools/fs/fs-shared.js +14 -0
  153. package/dist/tools/fs/fs-write.d.ts +5 -5
  154. package/dist/tools/fs/fs-write.js +71 -14
  155. package/dist/tools/fs/index.d.ts +6 -1
  156. package/dist/tools/fs/index.js +1 -1
  157. package/dist/tools/web.js +2 -1
  158. package/package.json +5 -1
  159. package/test/export-surface.snapshot.json +169 -23
  160. package/dist/core/file-snapshot-store.d.ts +0 -165
  161. package/dist/core/file-snapshot-store.js +0 -259
  162. package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +0 -13
  163. package/dist/core/store-contracts/file-snapshot-store-contract.js +0 -134
  164. package/dist/stores/file/file-snapshot-store.d.ts +0 -58
  165. package/dist/stores/file/file-snapshot-store.js +0 -353
@@ -330,8 +330,12 @@ export async function createRunWorkflowTool(d) {
330
330
  "until the next phase() call. Scoped — await phase('Fix', async () => { ...agents... }); groups just the " +
331
331
  "body (equivalently, pass { phase: 'Fix' } in an agent's opts). Give each stage a phase so progress " +
332
332
  "renders as stage groups; use the same titles you declared in meta.phases.\n" +
333
- "• spec.modelName works only when the deployment configured a workflow model allowlist; if unsure, omit " +
334
- "modelName and the agent runs on the deployment's default role.\n" +
333
+ "• spec.modelName works only when the deployment configured a workflow model allowlist, and its legal " +
334
+ "values are that deployment's own model CATALOG KEYS not generic tier words (a bare 'opus'/'sonnet'/" +
335
+ "'haiku' is a name the catalog almost certainly does not hold, and is refused rather than approximated). " +
336
+ "It also goes on the SPEC (the first argument), never in an agent()'s options object; `model` is accepted " +
337
+ "there as an alias for the same field. If unsure, omit it and the agent runs on the deployment's default " +
338
+ "role.\n" +
335
339
  "\n" +
336
340
  "Don't race. After launching, you know nothing about what the workflow found. Never fabricate or predict its " +
337
341
  "results in any format — not as prose, summary, or structured output. When a completion notifier is wired " +
@@ -507,6 +511,19 @@ export async function createRunWorkflowTool(d) {
507
511
  parentToolCallId: ctx.toolCallId,
508
512
  ...(sourceTaskId !== undefined ? { parentTaskId: sourceTaskId } : {}),
509
513
  ...((ctx.interactionPosture ?? d.parentInteractionPosture) !== undefined ? { interactionPosture: (ctx.interactionPosture ?? d.parentInteractionPosture) } : {}),
514
+ ...(() => {
515
+ if ("memoryCaptureOptedOut" in ctx) {
516
+ return {
517
+ parentMemoryCaptureState: () => ({
518
+ optedOut: ctx.memoryCaptureOptedOut === true,
519
+ indeterminate: ctx.memoryCaptureIndeterminate === true,
520
+ ...(ctx.memoryCaptureControlDir !== undefined ? { controlDir: ctx.memoryCaptureControlDir } : {}),
521
+ ...(ctx.memoryCaptureAncestors !== undefined ? { ancestors: ctx.memoryCaptureAncestors } : {}),
522
+ }),
523
+ };
524
+ }
525
+ return d.parentMemoryCaptureState !== undefined ? { parentMemoryCaptureState: d.parentMemoryCaptureState } : {};
526
+ })(),
510
527
  ...(d.parentModel !== undefined ? { defaultModel: d.parentModel } : {}),
511
528
  ...(d.parentGetApiKeyAndHeaders !== undefined ? { defaultGetApiKeyAndHeaders: d.parentGetApiKeyAndHeaders } : {}),
512
529
  ...(parentCenterArtifactDigest !== undefined ? { parentCenterArtifactDigest } : {}),
@@ -607,12 +624,14 @@ export async function createRunWorkflowTool(d) {
607
624
  .catch(() => { });
608
625
  }
609
626
  return {
610
- details: { type: "workflow-run", status: "backgrounded", workflowRunId: runId },
627
+ details: { type: "workflow-run", status: "backgrounded", workflowRunId: runId, task_id: runId },
611
628
  content: JSON.stringify({
612
629
  runId,
613
630
  task_id: runId,
614
631
  status: "started",
615
632
  ...(persistedScriptPath !== undefined ? { scriptPath: persistedScriptPath } : {}),
633
+ handling: "This tool result is internal metadata — never quote or paste any part of it (the ids above, and scriptPath when present) into a user-facing reply. " +
634
+ "In your own words, briefly tell the user what you launched; do not echo this result.",
616
635
  note: (() => {
617
636
  const pollExpr = d.taskRegistry ? `TaskOutput({ task_id: "${runId}" })` : undefined;
618
637
  const blockingPollExpr = d.taskRegistry ? `TaskOutput({ task_id: "${runId}", block: true })` : undefined;
@@ -37,6 +37,14 @@ export declare class WorkflowModelNotAllowedError extends Error {
37
37
  */
38
38
  export interface WorkflowAgentSpec {
39
39
  objective: string;
40
+ /**
41
+ * The canonical spelling, and the only one this type expresses. At RUNTIME the spec ALSO accepts
42
+ * `model` as an alias for this field (see {@link SPEC_KEY_ALIASES}) — the same axis is spelled
43
+ * `model` by the Agent tool and by agents frontmatter, and a script author who reaches for the
44
+ * spelling those two taught should not silently get the deployment's default model instead. The
45
+ * alias carries a NAME exactly like this field does; a `Model` OBJECT is refused under either
46
+ * spelling.
47
+ */
40
48
  modelName?: string;
41
49
  thinking?: ThinkingLevel;
42
50
  systemPrompt?: string;
@@ -83,6 +91,28 @@ export interface WorkflowAgentSpec {
83
91
  * for a BREAKING window.
84
92
  */
85
93
  export declare const WHITELIST_KEYS: readonly ["objective", "modelName", "thinking", "systemPrompt", "images", "limits", "readFace", "readDenyPatterns"];
94
+ /**
95
+ * #505 ② — ACCEPTED ALIAS SPELLINGS on the spec: `alias → the {@link WHITELIST_KEYS} member it means`.
96
+ *
97
+ * One axis, three faces, two words: the Agent tool takes `model`, agents frontmatter takes `model`, and
98
+ * this seam takes `modelName` (the name is not free to change here — `model` on a `TaskSpec` is the
99
+ * resolved `Model` OBJECT, and the phases meta of the workflow tool already spends the word too). Renaming
100
+ * across three surfaces buys uniformity at the price of a breaking churn in two of them; MUTUAL
101
+ * RECOGNITION buys the same authoring outcome for the price of this map. So the alias is normalized here,
102
+ * at the single door untrusted spec objects come through, and NOTHING downstream of {@link pickWhitelist}
103
+ * ever sees the alias spelling — the rest of this module, `buildGovernedChildSpec` and `resolveModelName`
104
+ * included, keeps reading exactly one field.
105
+ *
106
+ * An alias is NOT a whitelist key: `WHITELIST_KEYS` stays the closed set of fields that map onto `TaskSpec`,
107
+ * so the "no control-plane key on the whitelist" property is untouched and a `Model` object written as
108
+ * `model` is still structurally unable to reach the child (it is type-refused, not copied — see the
109
+ * normalization at the foot of `pickWhitelist`).
110
+ *
111
+ * A test pins that this map has exactly ONE member, because the normalization arm below is written for that
112
+ * one member by name: reading it generically would mean re-reading `s[canonical]` a second time, and the
113
+ * single-read discipline every other field in this file follows is worth more than a loop over one entry.
114
+ */
115
+ export declare const SPEC_KEY_ALIASES: ReadonlyMap<string, string>;
86
116
  /**
87
117
  * One field of an untrusted agent spec that did NOT reach the child, and why.
88
118
  * - `not_whitelisted` — the field is outside {@link WHITELIST_KEYS}: a control-plane field, a work field
@@ -99,8 +129,36 @@ export interface StrippedSpecKeyNote {
99
129
  /** The {@link EngineNotice} family for the strip announcement (see {@link WHITELIST_KEYS}). ONE notice per
100
130
  * governed build, listing every field that did not cross the seam — a governed script can carry an
101
131
  * arbitrary number of unrecognized fields, and a per-field notice would turn one authoring mistake into a
102
- * flood. Not de-duplicated across builds: each spawn is a distinct fact about a distinct spec. */
132
+ * flood. Not de-duplicated across builds: each spawn is a distinct fact about a distinct spec — and
133
+ * `detail.occurrenceId` (#495) is what makes that sentence checkable by a consumer, which otherwise saw
134
+ * two builds of one spec as one repeated notice with every key equal. */
103
135
  export declare const STRIPPED_KEYS_NOTICE_CODE = "workflow.governance_key_stripped";
136
+ /**
137
+ * #505 ① — the OPTIONS-side twin of {@link STRIPPED_KEYS_NOTICE_CODE}. `agent(spec, opts)`'s SECOND
138
+ * argument crosses its own membrane (`safeAgentOptions` in workflow-primitives.ts), which copies a small
139
+ * accepted set and dropped everything else in silence — a mistyped `lable`, an option that belongs on
140
+ * another seam, or a known option carrying a value its type check rejects (`label: 42`) all vanished with
141
+ * the call still succeeding.
142
+ *
143
+ * A SEPARATE code from the spec-side strip on purpose: the two membranes accept different key sets and the
144
+ * message names the set, so an operator reading `workflow.governance_key_stripped` about an OPTIONS key
145
+ * would look for it in the spec whitelist and correctly not find it. The severe half of this family — a
146
+ * SPEC field written on the options object — is not announced at all but REFUSED at the membrane (a
147
+ * silently ignored `model` runs the child on the deployment default, which is a different agent than the
148
+ * author asked for); this notice covers only the residue that is genuinely just ignored.
149
+ */
150
+ export declare const IGNORED_AGENT_OPTIONS_NOTICE_CODE = "workflow.agent_option_ignored";
151
+ /** The announcement budget, shared with the spec-side survey: one bound, one renderer, one place. Exported
152
+ * so the surveying caller can stop COLLECTING at the same number this emitter would render. */
153
+ export declare const MAX_ANNOUNCED_KEYS = 20;
154
+ /**
155
+ * Announce the ignored option keys — one aggregated notice per `agent()` call, never one per key (an
156
+ * options object's key count is the script's to choose). `sample` is expected pre-bounded by the caller to
157
+ * {@link MAX_ANNOUNCED_KEYS}; `total` is exact. Key names are script-authored text landing on an operator
158
+ * surface, so they cross {@link renderStrippedKey} — the same scrubber the spec-side line uses, never a
159
+ * second one — and the caller quotes nothing itself.
160
+ */
161
+ export declare function emitIgnoredAgentOptionsNotice(total: number, sample: readonly string[], accepted: readonly string[], onNotice?: (n: EngineNotice) => void): void;
104
162
  /** Per-child workflow ceilings the engine forces onto every spawned agent (design/98 §D.6), independent of
105
163
  * what the script asks for. The child's effective limits = min(script, baseline, these). */
106
164
  export interface WorkflowChildCaps {
@@ -3,6 +3,7 @@ import { tightenTaskSpec } from "../core/tighten-task-spec.js";
3
3
  import { sanitizeUntrustedText } from "../core/untrusted-text.js";
4
4
  import { compileReadDeny } from "../tools/fs/read-deny.js";
5
5
  import { WorkflowScriptError } from "./workflow-meta.js";
6
+ import { uuidv7 } from "../engine/session/uuid.js";
6
7
  export class WorkflowModelNotAllowedError extends Error {
7
8
  modelName;
8
9
  code = "workflow.model_not_allowed";
@@ -23,6 +24,7 @@ export const WHITELIST_KEYS = [
23
24
  "readDenyPatterns",
24
25
  ];
25
26
  const WHITELIST_KEY_SET = new Set(WHITELIST_KEYS);
27
+ export const SPEC_KEY_ALIASES = new Map([["model", "modelName"]]);
26
28
  export const STRIPPED_KEYS_NOTICE_CODE = "workflow.governance_key_stripped";
27
29
  const MAX_STRIPPED_KEYS_ANNOUNCED = 20;
28
30
  const MAX_STRIPPED_KEY_CHARS = 64;
@@ -50,7 +52,27 @@ function emitStrippedKeysNotice(survey, onNotice) {
50
52
  deliverEngineNotice(onNotice, {
51
53
  code: STRIPPED_KEYS_NOTICE_CODE,
52
54
  message,
53
- detail: { total: survey.total, stripped: shown, ...(omitted > 0 ? { omitted } : {}) },
55
+ detail: { total: survey.total, stripped: shown, ...(omitted > 0 ? { omitted } : {}), occurrenceId: uuidv7() },
56
+ });
57
+ }
58
+ catch {
59
+ }
60
+ }
61
+ export const IGNORED_AGENT_OPTIONS_NOTICE_CODE = "workflow.agent_option_ignored";
62
+ export const MAX_ANNOUNCED_KEYS = MAX_STRIPPED_KEYS_ANNOUNCED;
63
+ export function emitIgnoredAgentOptionsNotice(total, sample, accepted, onNotice) {
64
+ const shown = sample.map(renderStrippedKey);
65
+ const omitted = total - shown.length;
66
+ const list = shown.map((k) => JSON.stringify(k)).join(", ") + (omitted > 0 ? `, +${omitted} more` : "");
67
+ const message = `workflow: ${total} key(s) of an \`agent(spec, opts)\` OPTIONS object (the SECOND argument) were not applied (${list}). ` +
68
+ `The options object accepts only: ${accepted.join(", ")} — an unrecognized key, or a known one carrying a value its ` +
69
+ `option does not accept, is ignored and the agent runs without it. Everything about the CHILD's behavior belongs on ` +
70
+ `the spec (the first argument).`;
71
+ try {
72
+ deliverEngineNotice(onNotice, {
73
+ code: IGNORED_AGENT_OPTIONS_NOTICE_CODE,
74
+ message,
75
+ detail: { total, ignored: shown, ...(omitted > 0 ? { omitted } : {}), occurrenceId: uuidv7() },
54
76
  });
55
77
  }
56
78
  catch {
@@ -177,17 +199,38 @@ function pickWhitelist(scriptSpec) {
177
199
  }
178
200
  const s = scriptSpec;
179
201
  const stripped = { total: 0, sample: [] };
202
+ let canonicalIsOwn = false;
203
+ let aliasIsOwn = false;
180
204
  for (const k of Object.keys(s)) {
181
- if (WHITELIST_KEY_SET.has(k))
205
+ if (k === "modelName")
206
+ canonicalIsOwn = true;
207
+ else if (k === "model")
208
+ aliasIsOwn = true;
209
+ if (WHITELIST_KEY_SET.has(k) || SPEC_KEY_ALIASES.has(k))
182
210
  continue;
183
211
  stripped.total++;
184
212
  if (stripped.sample.length < MAX_STRIPPED_KEYS_ANNOUNCED)
185
213
  stripped.sample.push({ key: k, reason: "not_whitelisted" });
186
214
  }
215
+ const canonicalDesc = Object.getOwnPropertyDescriptor(s, "modelName");
216
+ const aliasDesc = Object.getOwnPropertyDescriptor(s, "model");
217
+ if ((canonicalIsOwn && canonicalDesc === undefined) || (aliasIsOwn && aliasDesc === undefined)) {
218
+ throw new WorkflowScriptError("agent(spec): this spec reports its own properties inconsistently between two reads of the same object — " +
219
+ "the model field cannot be resolved from a shape that changes while it is being read. Pass a plain object.");
220
+ }
221
+ const hiddenOwnSpelling = !canonicalIsOwn && canonicalDesc !== undefined ? "modelName" : !aliasIsOwn && aliasDesc !== undefined ? "model" : undefined;
222
+ if (hiddenOwnSpelling !== undefined) {
223
+ throw new WorkflowScriptError(`agent(spec): \`${hiddenOwnSpelling}\` is an OWN property of this spec that its own-key enumeration does not report ` +
224
+ `(a non-enumerable own key, or an object that answers differently across two reads). A model spelling the spec ` +
225
+ `hides from its own shape is refused rather than read as an inherited default — write it as an ordinary property.`);
226
+ }
227
+ const valueOfSnapshot = (d) => (d === undefined ? undefined : d.get !== undefined ? d.get.call(s) : d.value);
228
+ const modelNameInheritedRaw = canonicalIsOwn ? undefined : s.modelName;
229
+ const modelNameOwnRaw = valueOfSnapshot(canonicalDesc);
230
+ const modelAliasRaw = valueOfSnapshot(aliasDesc);
187
231
  const objective = s.objective;
188
232
  const thinking = s.thinking;
189
233
  const systemPrompt = s.systemPrompt;
190
- const modelNameRaw = s.modelName;
191
234
  if (typeof objective !== "string" || objective.length === 0) {
192
235
  throw new WorkflowScriptError("agent(spec): `objective` is required and must be a non-empty string");
193
236
  }
@@ -257,12 +300,22 @@ function pickWhitelist(scriptSpec) {
257
300
  }
258
301
  safe.readDenyPatterns = entries;
259
302
  }
260
- let modelName;
261
- if (modelNameRaw !== undefined) {
262
- if (typeof modelNameRaw !== "string")
263
- throw new WorkflowScriptError("agent(spec): `modelName` must be a string (a model NAME, never a Model object)");
264
- modelName = modelNameRaw;
303
+ const readModelName = (raw, spelling) => {
304
+ if (raw === undefined)
305
+ return undefined;
306
+ if (typeof raw !== "string") {
307
+ throw new WorkflowScriptError(`agent(spec): \`${spelling}\` must be a string (a model NAME, never a Model object)` +
308
+ (spelling === "modelName" ? "" : ` — \`${spelling}\` is accepted as an alias for \`modelName\`, and carries a name just like it`));
309
+ }
310
+ return raw;
311
+ };
312
+ const authoredCanonical = readModelName(modelNameOwnRaw, "modelName");
313
+ const aliasModelName = readModelName(modelAliasRaw, "model");
314
+ if (authoredCanonical !== undefined && aliasModelName !== undefined && authoredCanonical !== aliasModelName) {
315
+ throw new WorkflowScriptError(`agent(spec): \`modelName\` and \`model\` are two spellings of ONE field but name DIFFERENT models ` +
316
+ `("${authoredCanonical}" vs "${aliasModelName}") — write exactly one of them (\`modelName\` is the canonical spelling).`);
265
317
  }
318
+ const modelName = authoredCanonical ?? aliasModelName ?? readModelName(modelNameInheritedRaw, "modelName");
266
319
  return { safe, modelName, stripped };
267
320
  }
268
321
  function clampResourceLimits(safe, base, caps) {
@@ -12,8 +12,10 @@
12
12
  * "fix your meta" error BEFORE the (hard) sandbox ever runs the body.
13
13
  */
14
14
  import type { WorkflowMeta } from "./workflow-script-runner.js";
15
- /** Thrown when an LLM-authored workflow script is malformed at the static layer (bad meta, oversized source,
16
- * a forbidden construct). Carries a `code` so the tool can return a structured error the model can act on. */
15
+ /** Thrown when a workflow script is the party at fault: malformed at the static layer (bad meta, oversized
16
+ * source, a forbidden construct), or at RUN time calling a primitive with a shape it cannot run (a
17
+ * host-API stub in the sandbox, an unusable agent spec, a `parallel`/`pipeline` member that is not a
18
+ * function). Carries a `code` so the tool can return a structured error the model can act on. */
17
19
  export declare class WorkflowScriptError extends Error {
18
20
  readonly code = "workflow.script_error";
19
21
  constructor(message: string);
@@ -1,21 +1,64 @@
1
1
  import { assertSupportedAgentIsolation } from "./workflow.js";
2
- import { buildGovernedChildSpec, overlayWorktreeBaseline } from "./workflow-governance.js";
3
- function safeAgentOptions(opts) {
2
+ import { WorkflowScriptError } from "./workflow-meta.js";
3
+ import { buildGovernedChildSpec, emitIgnoredAgentOptionsNotice, overlayWorktreeBaseline, MAX_ANNOUNCED_KEYS, SPEC_KEY_ALIASES, WHITELIST_KEYS, } from "./workflow-governance.js";
4
+ const ACCEPTED_AGENT_OPTION_KEYS = ["label", "phase", "schema", "agentType", "isolation"];
5
+ const ACCEPTED_AGENT_OPTION_KEY_SET = new Set(ACCEPTED_AGENT_OPTION_KEYS);
6
+ const SPEC_KEYS_MISPLACED_ON_OPTIONS = new Map([
7
+ ...WHITELIST_KEYS.map((k) => [k, k]),
8
+ ...SPEC_KEY_ALIASES,
9
+ ["effort", "thinking"],
10
+ ]);
11
+ function safeAgentOptions(opts, onNotice) {
4
12
  if (typeof opts !== "object" || opts === null)
5
13
  return {};
6
14
  const o = opts;
15
+ const names = Object.keys(o);
16
+ for (const name of names) {
17
+ const spelling = SPEC_KEYS_MISPLACED_ON_OPTIONS.get(name);
18
+ if (spelling === undefined || o[name] === undefined)
19
+ continue;
20
+ throw new WorkflowScriptError(`agent(spec, opts): \`${name}\` is a SPEC field written on the OPTIONS object (the SECOND argument), where nothing ` +
21
+ `reads it — move it into the spec (the FIRST argument)${name === spelling ? "" : `, spelled \`${spelling}\``}. ` +
22
+ `The options object accepts only: ${ACCEPTED_AGENT_OPTION_KEYS.join(", ")}. The agent was NOT started: silently ` +
23
+ `ignoring \`${name}\` would have run a different child than the one asked for.`);
24
+ }
7
25
  const out = {};
8
- if (typeof o.label === "string")
9
- out.label = o.label;
10
- if (typeof o.phase === "string")
11
- out.phase = o.phase;
12
- if (o.schema !== undefined)
13
- out.schema = o.schema;
14
- if (typeof o.agentType === "string")
15
- out.agentType = o.agentType;
16
- assertSupportedAgentIsolation(o.isolation);
17
- if (o.isolation === "worktree")
26
+ const rejected = new Set();
27
+ const label = o.label;
28
+ if (typeof label === "string")
29
+ out.label = label;
30
+ else if (label !== undefined)
31
+ rejected.add("label");
32
+ const phase = o.phase;
33
+ if (typeof phase === "string")
34
+ out.phase = phase;
35
+ else if (phase !== undefined)
36
+ rejected.add("phase");
37
+ const schema = o.schema;
38
+ if (schema !== undefined)
39
+ out.schema = schema;
40
+ const agentType = o.agentType;
41
+ if (typeof agentType === "string")
42
+ out.agentType = agentType;
43
+ else if (agentType !== undefined)
44
+ rejected.add("agentType");
45
+ const isolation = o.isolation;
46
+ assertSupportedAgentIsolation(isolation);
47
+ if (isolation === "worktree")
18
48
  out.isolation = "worktree";
49
+ else if (isolation !== undefined)
50
+ rejected.add("isolation");
51
+ let total = 0;
52
+ const sample = [];
53
+ for (const name of names) {
54
+ if (ACCEPTED_AGENT_OPTION_KEY_SET.has(name) && !rejected.has(name))
55
+ continue;
56
+ total++;
57
+ if (sample.length < MAX_ANNOUNCED_KEYS)
58
+ sample.push(name);
59
+ }
60
+ if (total > 0)
61
+ emitIgnoredAgentOptionsNotice(total, sample, ACCEPTED_AGENT_OPTION_KEYS, onNotice);
19
62
  return out;
20
63
  }
21
64
  function formatResourceClampNote(notes) {
@@ -26,7 +69,7 @@ export function buildWorkflowPrimitives(ctx, governance, onAgentSpawn, parentThi
26
69
  const agent = (spec, opts) => {
27
70
  if (typeof spec === "string")
28
71
  spec = { objective: spec };
29
- const agentOpts = safeAgentOptions(opts);
72
+ const agentOpts = safeAgentOptions(opts, governance?.onNotice);
30
73
  const effectiveBaseline = (b) => agentOpts.isolation === "worktree" && b.worktreeBase != null ? { ...b, base: overlayWorktreeBaseline(b.base, b.worktreeBase) } : b;
31
74
  const childSpec = governance
32
75
  ? buildGovernedChildSpec(spec, effectiveBaseline(governance.baseline), governance.models, governance.caps, (notes) => ctx.log(formatResourceClampNote(notes)), governance.onNotice)
@@ -88,13 +88,60 @@ export interface WorkflowAgentRun {
88
88
  /** F5/B4: total attempts this call made (set only when > 1 — stall/throttle retries happened). The record's
89
89
  * `stats` are the FINAL attempt's; burned retry spend rolls into `run.stats`/budget (CC semantics). */
90
90
  attempts?: number;
91
- /** F5/B4: why the LAST retry happened — "stalled" (progress watchdog) or "throttled" (degraded response). */
91
+ /** F5/B4: why the LAST retry happened — "stalled" (progress watchdog), "throttled" (degraded response),
92
+ * or (#481) "awaiting_approval": the watchdog fired while this leg had an outstanding human approval, so
93
+ * the retry happened but was NOT charged against the stall budget. Kept a free-form `string` (it always
94
+ * was) — a consumer that switched on the first two words must treat an unknown word as "some retry
95
+ * happened", which is what the field has always meant. */
92
96
  lastAttemptReason?: string;
97
+ /** #481 — the leg's outstanding human approvals as the DEPLOYMENT's approval plane last reported them
98
+ * (verbatim from {@link TaskResult.pendingApproval}: core mints no ask ids and estimates nothing). Written
99
+ * LAST-WINS on every attempt terminal and on the leg's own terminal, and REMOVED when a later report
100
+ * carries none — so a settled leg never keeps advertising a wait that has since been decided. Absent
101
+ * whenever the deployment does not report it, which is the ordinary world. */
102
+ pendingApproval?: {
103
+ askIds: readonly string[];
104
+ oldestCreatedAtMs: number;
105
+ };
106
+ /** #485 — how long this leg spent BLOCKED on human approvals, in ms, as the deployment's approval plane
107
+ * measured it ({@link TaskResult.approvalWaitedMs}, carried verbatim). The engine starts no approval
108
+ * clock and does no arithmetic on this: a receipt's figure is already that leg's cumulative wait, so a
109
+ * later receipt SUPERSEDES an earlier one (last-wins) and is never added to it — summing successive
110
+ * reports of one cumulative counter double-counts. Absent when the deployment does not report it.
111
+ *
112
+ * A REPLAYED leg ({@link replayed}) never carries this, nor {@link pendingApproval}, even when the
113
+ * journaled result it was minted from did. The wait was paid by the run that actually made the call; a
114
+ * resume that served the answer from cache waited for nobody, and copying the figure forward would let
115
+ * this run's total-timeout disclosure report a wait it never experienced. */
116
+ approvalWaitedMs?: number;
93
117
  /** design/114 — the session id of this agent's run (= its `TaskResult.sessionId`). Surfaces the conversation
94
118
  * handle to the observation layer so an external initiator can WARM-resume a failed/timed-out agent
95
119
  * (`fork(sessionId)→runTask`, or `runTask({sessionId})`) instead of a fresh re-run. An opaque id (not
96
120
  * content) → scope-gated via `getWorkflowRun`, no redaction needed. Absent for a stub run that minted none. */
97
121
  sessionId?: string;
122
+ /**
123
+ * #499 — the ENGINE-minted identity of the task run behind this record (= its `TaskResult.runId`).
124
+ *
125
+ * Named `taskRunId`, NOT `runId`: in this file and in every `WorkflowEvent`, `runId` already means
126
+ * the WORKFLOW run's id, and two different runs wearing one field name on sibling objects is the
127
+ * "同名异形" hazard {@link WorkflowRun} documents elsewhere. This one names the CHILD leg's engine run.
128
+ *
129
+ * Why it is not covered by {@link sessionId}: retry attempts of one leg REUSE the session
130
+ * (the attempt spec carries the same `sessionId`), so a record whose {@link attempts} > 1 cannot be
131
+ * attributed to a specific attempt by session — and the last-wins seats beside it
132
+ * ({@link pendingApproval} / {@link approvalWaitedMs}) are reported per attempt. This is the axis
133
+ * that says WHICH run produced the values on this row.
134
+ *
135
+ * Written LAST-WINS at four folds: the leg's terminal, the salvaged stall-exhaustion tail, the
136
+ * REPLAYED path (where — like the session handle beside it — it names the ORIGINAL run that produced
137
+ * the cached result, a true and joinable fact about where those bytes came from), and each RETRY
138
+ * ATTEMPT's report. The last one is load-bearing rather than tidy: the attempt fold writes
139
+ * {@link pendingApproval} / {@link approvalWaitedMs} from a specific attempt and persists the row
140
+ * immediately, so while an approval is outstanding the durable row holds one run's figures — and
141
+ * without this it held them under no id able to name that run. Absent whenever the underlying result
142
+ * carried none (a stub run, or a leg whose prepare was never entered).
143
+ */
144
+ taskRunId?: string;
98
145
  /** RB-393① — the WORKTREE directory this agent ran in, present ONLY for an
99
146
  * `isolation: "worktree"` agent whose worktree the engine actually minted (reported by the runner's
100
147
  * workspace-observation seam and folded on while the agent is still RUNNING, so an interrupted run's
@@ -220,12 +267,41 @@ export interface WorkflowRun {
220
267
  *
221
268
  * The seat is an OBJECT rather than a flat pair of members so a later disclosure about the same terminal
222
269
  * (how much of the deadline was spent waiting on something outside the run, say) joins it additively with
223
- * no shape change on any of the three faces. Nothing beyond the four members below is declared today. */
270
+ * no shape change on any of the three faces. `approvalWaitedMs` is that later disclosure, landed:
271
+ *
272
+ * #481/#485 `approvalWaitedMs` — the LARGEST single leg's `WorkflowAgentRun.approvalWaitedMs`, i.e. the
273
+ * longest any one agent spent blocked on a human approval, as the deployment's approval plane itself
274
+ * measured it. It answers the one question the four counts above cannot: was this run SLOW, or was it
275
+ * WAITING? A value near `timeoutMs` says the window went on a person, not on work — raise the deadline
276
+ * and you buy nothing; get the approval answered faster and you buy the whole run.
277
+ *
278
+ * Read it as a LOWER BOUND on the window spent blocked, never as a total. It is deliberately the MAX and
279
+ * never a SUM: legs wait in parallel, so summing would report more waiting than the window contains and
280
+ * break the only comparison the number is for. Every figure is the deployment's own — core starts no
281
+ * approval clock and does no arithmetic on the values it is handed (see
282
+ * {@link WorkflowAgentRun.approvalWaitedMs}).
283
+ *
284
+ * ABSENT when no leg reported one, which includes every deployment that does not report at all. Absence
285
+ * is NOT zero: "this deployment does not supply the fact" and "nobody waited" are different states and a
286
+ * zero would collapse them.
287
+ *
288
+ * ⚠️ SAME POINT-IN-TIME DISCIPLINE AS THE COUNTS, and it has a real edge worth stating plainly: only
289
+ * figures ALREADY ON the agent records when the deadline fired can contribute. A leg still IN FLIGHT at
290
+ * that moment is abandoned — its final receipt reaches the finalized-run guard and is never folded — so
291
+ * it contributes the LAST figure it reported, not its closing one. In practice a leg that is waiting
292
+ * reports once per stall window (each aborted attempt's receipt folds, see
293
+ * `WorkflowAgentRun.approvalWaitedMs`), so the seat carries a figure that lags by at most one window;
294
+ * a deployment with the progress watchdog disabled, or a deadline that fires inside the first window,
295
+ * can leave it absent while a person was in fact being waited on. This is why the member is documented
296
+ * as a LOWER BOUND and never as an accounting of the window. Folding an abandoned leg's closing receipt
297
+ * instead would mean the deadline waiting on the very work it exists to abandon, and would make these
298
+ * five members describe a moment that never existed. */
224
299
  timeoutInterruption?: {
225
300
  timeoutMs: number;
226
301
  agentsCompleted: number;
227
302
  agentsFailed: number;
228
303
  agentsInFlight: number;
304
+ approvalWaitedMs?: number;
229
305
  };
230
306
  phases: WorkflowPhase[];
231
307
  agents: WorkflowAgentRun[];
@@ -386,6 +386,26 @@ export interface RunWorkflowOptions {
386
386
  * spec) so a root's per-run posture override governs its workflow children too. Set by the
387
387
  * `run_workflow` tool wiring from `ToolExecuteContext.interactionPosture`. */
388
388
  interactionPosture?: "interactive" | "headless";
389
+ /**
390
+ * design/383 §2.5 (rescan post-6.0.0-RC) — the HOST session's capture opt-out state, read AT
391
+ * EACH SPAWN (call-time getter, deliberately not a launch snapshot: `optedOut` is live — a
392
+ * mid-run `optOutMemoryCapture` flip must floor agents spawned after it). Threaded onto both
393
+ * spawn legs' internals as the same floor/indeterminate/queryDir/ancestor seats the subagent
394
+ * lane forwards, so a workflow child of an opted-out host commits nothing — the design's ruled
395
+ * disposition for the workflow HOST lane. Set by the `run_workflow` tool wiring (enriched ctx
396
+ * getters, else the auto-mount dep). Absent ⇒ no capture seat rides (a direct `runWorkflow`
397
+ * caller with no memory-mounted host — `originatingSessionId` still rides as `parentSessionId`
398
+ * for the record-query leg).
399
+ */
400
+ parentMemoryCaptureState?: () => {
401
+ optedOut: boolean;
402
+ indeterminate: boolean;
403
+ controlDir?: string;
404
+ ancestors?: ReadonlyArray<{
405
+ sessionId: string;
406
+ controlDir?: string;
407
+ }>;
408
+ };
389
409
  /** Call-time getter for the HOST run's RESOLVED Model object. A spawned agent whose
390
410
  * fold chain (script spec → agentType → governance baseline) produced NO model inherits the
391
411
  * parent's full object — baseUrl/key routing included — instead of falling to a string/role