@sema-agent/core 7.17.7 → 7.19.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 (201) hide show
  1. package/CHANGELOG.md +233 -13
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +1 -0
  4. package/dist/agents/subagent.js +5 -0
  5. package/dist/agents/teacher.js +1 -0
  6. package/dist/agents/verify.js +3 -0
  7. package/dist/brain/anthropic.js +6 -3
  8. package/dist/brain/open-responses.js +10 -4
  9. package/dist/brain/openai.js +5 -2
  10. package/dist/brain/reasoning.d.ts +13 -13
  11. package/dist/brain/reasoning.js +2 -2
  12. package/dist/brain/route-adjudicator.d.ts +18 -0
  13. package/dist/brain/route-adjudicator.js +16 -0
  14. package/dist/brain/stream-engine.js +2 -1
  15. package/dist/brain/stream-shared.d.ts +3 -0
  16. package/dist/brain/stream-shared.js +1 -0
  17. package/dist/core/agent-definition.d.ts +10 -0
  18. package/dist/core/artifact-host.d.ts +41 -4
  19. package/dist/core/artifact-host.js +18 -2
  20. package/dist/core/ask-origin.d.ts +16 -0
  21. package/dist/core/ask-origin.js +8 -0
  22. package/dist/core/auto-mode-arming.d.ts +6 -0
  23. package/dist/core/auto-mode-arming.js +5 -5
  24. package/dist/core/auto-mode-defaults.d.ts +43 -15
  25. package/dist/core/auto-mode-defaults.js +4 -1
  26. package/dist/core/auto-mode.d.ts +184 -59
  27. package/dist/core/auto-mode.js +188 -13
  28. package/dist/core/checkpoint-store.d.ts +23 -2
  29. package/dist/core/checkpoint-store.js +2 -2
  30. package/dist/core/engine-notice.d.ts +77 -1
  31. package/dist/core/engine-notice.js +7 -0
  32. package/dist/core/gate-fold.js +5 -1
  33. package/dist/core/gate-lanes.js +9 -8
  34. package/dist/core/gate-outcome.d.ts +42 -2
  35. package/dist/core/gate-outcome.js +23 -0
  36. package/dist/core/governance-codes.d.ts +1 -1
  37. package/dist/core/governance-codes.js +14 -0
  38. package/dist/core/hook-seats.d.ts +66 -0
  39. package/dist/core/hook-seats.js +37 -0
  40. package/dist/core/hooks.d.ts +1 -1
  41. package/dist/core/read-root-candidate.d.ts +12 -0
  42. package/dist/core/read-root-candidate.js +24 -0
  43. package/dist/core/roles.d.ts +9 -1
  44. package/dist/core/roles.js +4 -0
  45. package/dist/core/runner/advertised-writable-dirs.d.ts +8 -3
  46. package/dist/core/runner/assemble-result.d.ts +2 -1
  47. package/dist/core/runner/assemble-result.js +1 -1
  48. package/dist/core/runner/classifier-ladder.d.ts +227 -0
  49. package/dist/core/runner/classifier-ladder.js +214 -0
  50. package/dist/core/runner/contracts.d.ts +71 -13
  51. package/dist/core/runner/decide-continuation.d.ts +1 -3
  52. package/dist/core/runner/decide-continuation.js +1 -7
  53. package/dist/core/runner/deferral-census.d.ts +52 -0
  54. package/dist/core/runner/deferral-census.js +44 -0
  55. package/dist/core/runner/denial-limit-arms.js +2 -2
  56. package/dist/core/runner/derived-route-fallback.d.ts +8 -4
  57. package/dist/core/runner/derived-route-fallback.js +8 -3
  58. package/dist/core/runner/gate-exit.d.ts +12 -0
  59. package/dist/core/runner/gate-exit.js +6 -3
  60. package/dist/core/runner/prepare-artifact.d.ts +10 -2
  61. package/dist/core/runner/prepare-artifact.js +5 -4
  62. package/dist/core/runner/prepare-ask-lane.js +1 -0
  63. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
  64. package/dist/core/runner/prepare-caps-and-workflow.js +43 -55
  65. package/dist/core/runner/prepare-config-doors.d.ts +7 -1
  66. package/dist/core/runner/prepare-config-doors.js +13 -13
  67. package/dist/core/runner/prepare-defer-classify.d.ts +13 -19
  68. package/dist/core/runner/prepare-defer-classify.js +8 -3
  69. package/dist/core/runner/prepare-gate-stations.d.ts +26 -0
  70. package/dist/core/runner/prepare-gate-stations.js +1 -1
  71. package/dist/core/runner/prepare-hands-readface.d.ts +24 -7
  72. package/dist/core/runner/prepare-hands-readface.js +60 -24
  73. package/dist/core/runner/prepare-inherited-gate.d.ts +17 -4
  74. package/dist/core/runner/prepare-inherited-gate.js +10 -2
  75. package/dist/core/runner/prepare-lsp.d.ts +2 -2
  76. package/dist/core/runner/prepare-lsp.js +1 -1
  77. package/dist/core/runner/prepare-policy-chain.js +2 -0
  78. package/dist/core/runner/prepare-project-context.d.ts +5 -1
  79. package/dist/core/runner/prepare-project-context.js +7 -4
  80. package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -2
  81. package/dist/core/runner/prepare-prompt-inputs.js +4 -1
  82. package/dist/core/runner/prepare-run-refs.d.ts +9 -1
  83. package/dist/core/runner/prepare-run-refs.js +3 -2
  84. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -0
  85. package/dist/core/runner/prepare-suspend-saga.js +5 -2
  86. package/dist/core/runner/prepare-task.js +19 -14
  87. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -0
  88. package/dist/core/runner/prepare-tool-disclosure-mount.js +10 -2
  89. package/dist/core/runner/prepare-turn-wiring.d.ts +8 -4
  90. package/dist/core/runner/prepare-turn-wiring.js +3 -2
  91. package/dist/core/runner/prepare-wiring-manifest.d.ts +5 -5
  92. package/dist/core/runner/prepare-wiring-manifest.js +5 -2
  93. package/dist/core/runner/resume-apply.js +3 -3
  94. package/dist/core/runner/resume-checkpoint-screen.js +7 -0
  95. package/dist/core/runner/run-harness-handlers.js +4 -1
  96. package/dist/core/runner/run-identity-wiring.js +6 -2
  97. package/dist/core/runner/run-leg.d.ts +2 -0
  98. package/dist/core/runner/run-leg.js +45 -45
  99. package/dist/core/runner/run-notification-lane.js +55 -27
  100. package/dist/core/runner/run-settle-and-teardown.js +1 -0
  101. package/dist/core/runner/run-terminal-adoption.js +16 -13
  102. package/dist/core/runner/run-turn-boundary.js +15 -5
  103. package/dist/core/runner/runtask.d.ts +6 -5
  104. package/dist/core/runner/runtask.js +26 -14
  105. package/dist/core/runner/skill-base-dir.d.ts +54 -0
  106. package/dist/core/runner/skill-base-dir.js +42 -0
  107. package/dist/core/runner/synthetic-tools.d.ts +83 -48
  108. package/dist/core/runner/synthetic-tools.js +69 -32
  109. package/dist/core/runner/tool-defer-gate.d.ts +22 -9
  110. package/dist/core/runner/tool-defer-gate.js +15 -5
  111. package/dist/core/runner/tool-disclosure.d.ts +5 -0
  112. package/dist/core/runner/tool-disclosure.js +2 -0
  113. package/dist/core/runner/tool-progress-lane.d.ts +42 -0
  114. package/dist/core/runner/tool-progress-lane.js +61 -0
  115. package/dist/core/runner/turn-attachments.d.ts +43 -12
  116. package/dist/core/runner/turn-attachments.js +58 -14
  117. package/dist/core/runner-deps.d.ts +48 -7
  118. package/dist/core/sensitive-path-policy.d.ts +58 -17
  119. package/dist/core/sensitive-path-policy.js +33 -5
  120. package/dist/core/skill-listing-budget.d.ts +112 -0
  121. package/dist/core/skill-listing-budget.js +58 -0
  122. package/dist/core/swappable-deps.d.ts +9 -7
  123. package/dist/core/swappable-deps.js +7 -3
  124. package/dist/core/task-event.d.ts +96 -29
  125. package/dist/core/task-notification.d.ts +142 -16
  126. package/dist/core/task-notification.js +106 -0
  127. package/dist/core/task-result.d.ts +9 -1
  128. package/dist/core/task-spec.d.ts +46 -3
  129. package/dist/core/tighten-task-spec.js +7 -5
  130. package/dist/core/tool-model-gate.d.ts +1 -1
  131. package/dist/core/tool-policy.d.ts +19 -4
  132. package/dist/core/tool-policy.js +6 -7
  133. package/dist/core/tool-search-setting.d.ts +78 -0
  134. package/dist/core/tool-search-setting.js +90 -0
  135. package/dist/core/tool-spec.d.ts +45 -4
  136. package/dist/core/types.d.ts +2 -2
  137. package/dist/core/types.js +1 -1
  138. package/dist/core/wiring-manifest.d.ts +46 -0
  139. package/dist/core/wiring-manifest.js +9 -1
  140. package/dist/core/write-protect.d.ts +8 -3
  141. package/dist/core/write-protect.js +8 -5
  142. package/dist/engine/execution-env/node-execution-env.js +10 -4
  143. package/dist/engine/harness/agent-harness.d.ts +42 -0
  144. package/dist/engine/harness/agent-harness.js +61 -29
  145. package/dist/engine/loop/agent-loop.d.ts +68 -3
  146. package/dist/engine/loop/agent-loop.js +109 -16
  147. package/dist/index.d.ts +13 -8
  148. package/dist/index.js +11 -7
  149. package/dist/internal/harness.d.ts +1 -1
  150. package/dist/orchestration/run-workflow-tool.d.ts +3 -0
  151. package/dist/orchestration/run-workflow-tool.js +6 -1
  152. package/dist/orchestration/workflow-governance.d.ts +7 -1
  153. package/dist/orchestration/workflow-governance.js +9 -5
  154. package/dist/orchestration/workflow-primitives.d.ts +3 -0
  155. package/dist/orchestration/workflow-primitives.js +6 -1
  156. package/dist/orchestration/workflow-types.d.ts +48 -13
  157. package/dist/orchestration/workflow-types.js +1 -1
  158. package/dist/orchestration/workflow.js +45 -17
  159. package/dist/prompt-assembly/epoch.js +2 -0
  160. package/dist/prompt-assembly/packs/sema-default.js +2 -2
  161. package/dist/prompt-assembly/section-usage.d.ts +26 -0
  162. package/dist/prompt-assembly/section-usage.js +10 -0
  163. package/dist/prompt-assembly/types.d.ts +8 -6
  164. package/dist/prompts/default.d.ts +13 -0
  165. package/dist/prompts/default.js +16 -8
  166. package/dist/tools/artifact/artifact-text.d.ts +10 -1
  167. package/dist/tools/artifact/artifact-text.js +6 -3
  168. package/dist/tools/artifact/artifact-tool.d.ts +12 -2
  169. package/dist/tools/artifact/artifact-tool.js +203 -70
  170. package/dist/tools/artifact/conformance.d.ts +56 -0
  171. package/dist/tools/artifact/conformance.js +349 -0
  172. package/dist/tools/artifact/local-stub.js +1 -1
  173. package/dist/tools/artifact/reject-breaker.d.ts +28 -0
  174. package/dist/tools/artifact/reject-breaker.js +42 -0
  175. package/dist/tools/fs/bash-lexer.d.ts +132 -0
  176. package/dist/tools/fs/bash-lexer.js +563 -0
  177. package/dist/tools/fs/bash-program-position.d.ts +142 -0
  178. package/dist/tools/fs/bash-program-position.js +125 -0
  179. package/dist/tools/fs/bash-readonly-classifier.d.ts +81 -16
  180. package/dist/tools/fs/bash-readonly-classifier.js +180 -558
  181. package/dist/tools/fs/containment-face.d.ts +89 -0
  182. package/dist/tools/fs/containment-face.js +81 -0
  183. package/dist/tools/fs/fs-bash.d.ts +46 -1
  184. package/dist/tools/fs/fs-bash.js +165 -25
  185. package/dist/tools/fs/fs-read.d.ts +1 -1
  186. package/dist/tools/fs/fs-search-tools.d.ts +3 -3
  187. package/dist/tools/fs/fs-write.d.ts +4 -3
  188. package/dist/tools/fs/fs-write.js +9 -9
  189. package/dist/tools/fs/index.d.ts +24 -5
  190. package/dist/tools/fs/index.js +18 -6
  191. package/dist/tools/fs/read-deny.d.ts +4 -3
  192. package/dist/tools/fs/repo-map.d.ts +2 -2
  193. package/dist/tools/fs/safety.d.ts +18 -3
  194. package/dist/tools/fs/safety.js +11 -4
  195. package/package.json +1 -1
  196. package/test/export-surface.snapshot.json +99 -13
  197. package/wire-changes/core@7.17.7.json +49 -0
  198. package/wire-changes/core@7.18.0.json +1273 -0
  199. package/wire-changes/core@7.19.0.json +457 -0
  200. package/dist/tools/fs/read-face.d.ts +0 -68
  201. package/dist/tools/fs/read-face.js +0 -47
@@ -22,6 +22,7 @@
22
22
  */
23
23
  import type { RunnerDeps, TaskSpec } from "./types.js";
24
24
  import { type StoreDurability } from "./checkpoint-store.js";
25
+ import type { WiringManifestHookEntry } from "./hook-seats.js";
25
26
  import type { McpDelivered, McpFailureKind } from "./mcp-failure.js";
26
27
  export type { StoreDurability };
27
28
  /** The manifest-face projection of {@link StoreDurability}: `"declared_durable"` (the name says it —
@@ -110,11 +111,27 @@ export interface WiringManifestMcpEntry {
110
111
  error?: string;
111
112
  toolCount: number;
112
113
  }
114
+ /** Which LSP lanes this leg armed. The two lanes are INDEPENDENT and the word is their product: `tool` = the
115
+ * code-intelligence tool mounted and nothing more; `diagnostics` = the push lane armed (a registry-feeding
116
+ * manager, write-capable hands, no opt-out) while the tool is NOT on the roster; `tool_and_diagnostics` =
117
+ * both. A leg whose `LSP` tool was excluded can still be pushing diagnostics. */
118
+ export declare const LSP_LANES: readonly ["tool", "diagnostics", "tool_and_diagnostics"];
119
+ export type LspLane = (typeof LSP_LANES)[number];
120
+ /** The contract id the LSP tool mounts under (`createLspTool`) — the roster's structural discriminator, which
121
+ * a disclosure keys on instead of re-spelling the wire name. */
122
+ export declare const LSP_CONTRACT_ID = "core.lsp@1";
123
+ /** The manifest's LSP seam from its two INDEPENDENT facts (see {@link WiringManifest.lsp}): the lane word is
124
+ * their product, and is absent only when neither lane is live. */
125
+ export declare function deriveLspSeam(mounted: boolean, diagnosticsLive: boolean): {
126
+ mounted: boolean;
127
+ lane?: LspLane;
128
+ };
113
129
  /**
114
130
  * design/173 §2.2/§8.4 — the wiring manifest. Consumers MUST ignore unknown fields
115
131
  * (`schemaVersion` bumps only on a semantic break of an EXISTING field). The TaskEvent face is the
116
132
  * host/operator plane — the manifest never enters model context.
117
133
  */
134
+ export type { WiringManifestHookEntry };
118
135
  export interface WiringManifest {
119
136
  schemaVersion: 1;
120
137
  /** Present on EFFECTIVE (per-leg) manifests only; the static half has no leg. */
@@ -280,6 +297,28 @@ export interface WiringManifest {
280
297
  * `tool_roster_delta` event, never as a re-emitted manifest.
281
298
  */
282
299
  tools?: import("./tool-roster.js").ToolRoster;
300
+ /**
301
+ * EFFECTIVE half only: the hook seats this leg wired, one entry per family, in {@link import("./hook-seats.js").HOOK_FAMILIES} order. PRESENT IFF at least one
302
+ * seat is wired — a deployment that wires none reports nothing here and its manifest (and {@link configFingerprint}) is byte-for-byte what it was; an empty
303
+ * array is never emitted, so absence reads as "this leg wired no hook RECORD of its own" — NOT as "no hook runs": a delegated child with no local record can
304
+ * still be screened (and refused) by an ancestor's `preToolUse` through the gate's constraint fold, which this face deliberately does not call a hook of
305
+ * this leg. Every entry is deployment code the engine awaits on the critical path, fail-closed on the two decision seats. EXCLUDED from
306
+ * {@link configFingerprint}: the record is a per-task seat, so one assembly's legs legitimately differ on it.
307
+ */
308
+ hooks?: readonly WiringManifestHookEntry[];
309
+ /**
310
+ * EFFECTIVE half only: the LSP code-intelligence seam. PRESENT IFF a manager seat resolved
311
+ * (`spec.lspManager ?? deps.lspManager`); a deployment that wires none reports nothing (byte-for-byte
312
+ * unchanged, fingerprint included). `mounted` is the ROSTER fact, not the seat fact — a wired manager
313
+ * whose `LSP` tool was excluded reports `false` — and `lane` is INDEPENDENT of it: the diagnostics push
314
+ * lane arms on the manager, the hands and the opt-out, never on the mount, so
315
+ * `{mounted:false, lane:"diagnostics"}` is a real and reachable state. `lane` is absent only when
316
+ * neither lane is live.
317
+ */
318
+ lsp?: {
319
+ mounted: boolean;
320
+ lane?: LspLane;
321
+ };
283
322
  /** EFFECTIVE half only — a short, non-sensitive fingerprint (sha256 prefix over the canonical
284
323
  * JSON of this manifest's own resolved facts; every field here is an enum/boolean/count, no
285
324
  * secrets) so an operator can correlate legs that ran under the same resolved assembly. */
@@ -351,6 +390,13 @@ export interface WiringFacts {
351
390
  mcp?: readonly WiringManifestMcpEntry[];
352
391
  /** Effective half only — the leg's minted tool roster (see {@link WiringManifest.tools}). */
353
392
  tools?: import("./tool-roster.js").ToolRoster;
393
+ /** Effective half only — the wired hook seats ({@link WiringManifest.hooks}); an empty array and an absent value derive the same thing (no section). */
394
+ hooks?: readonly WiringManifestHookEntry[];
395
+ /** Effective half only — the LSP seam ({@link WiringManifest.lsp}); absent ⇒ no manager wired. */
396
+ lsp?: {
397
+ mounted: boolean;
398
+ lane?: LspLane;
399
+ };
354
400
  }
355
401
  /** Named view of the deps seats the static half reads (a `Pick` of the real {@link RunnerDeps} —
356
402
  * single-source shapes, no parallel hand-copied interface). */
@@ -4,6 +4,12 @@ import { resolveCheckpointStore } from "./checkpoint-store.js";
4
4
  import { isLiveQuestionFace } from "./ask-question.js";
5
5
  import { AUTO_MODE_DENY_SOURCES, isAutoModeDenySource } from "./runtime-caps.js";
6
6
  export const AUTO_MODE_ARM_REASONS = ["armed", "no_intent", "no_face", "denied", "resolver_fault"];
7
+ export const LSP_LANES = ["tool", "diagnostics", "tool_and_diagnostics"];
8
+ export const LSP_CONTRACT_ID = "core.lsp@1";
9
+ export function deriveLspSeam(mounted, diagnosticsLive) {
10
+ const lane = mounted ? (diagnosticsLive ? "tool_and_diagnostics" : "tool") : diagnosticsLive ? "diagnostics" : undefined;
11
+ return { mounted, ...(lane !== undefined ? { lane } : {}) };
12
+ }
7
13
  export function resolveDeclaredDurability(store, storeName) {
8
14
  const declared = store?.durability;
9
15
  if (declared === undefined)
@@ -133,9 +139,11 @@ export function deriveWiringManifest(facts) {
133
139
  ...(facts.half === "effective" && facts.autoMode !== undefined ? { autoMode: readAutoModeFact(facts.autoMode) } : {}),
134
140
  ...(facts.half === "effective" && facts.mcp !== undefined ? { mcp: facts.mcp.map((e) => ({ ...e })) } : {}),
135
141
  ...(facts.half === "effective" && facts.tools !== undefined ? { tools: facts.tools } : {}),
142
+ ...(facts.half === "effective" && facts.hooks !== undefined && facts.hooks.length > 0 ? { hooks: facts.hooks.map((h) => ({ ...h })) } : {}),
143
+ ...(facts.half === "effective" && facts.lsp !== undefined ? { lsp: { ...facts.lsp } } : {}),
136
144
  };
137
145
  if (facts.half === "effective") {
138
- const { leg: _leg, mcp: _mcp, tools: _tools, ...assembly } = manifest;
146
+ const { leg: _leg, mcp: _mcp, tools: _tools, lsp: _lsp, hooks: _hooks, ...assembly } = manifest;
139
147
  const { provenance: _askSeat, ...askForHash } = assembly.ask;
140
148
  const { provenance: _questionSeat, ...questionForHash } = assembly.question;
141
149
  manifest.configFingerprint = createHash("sha256")
@@ -51,8 +51,13 @@ export interface WriteProtectionMatcher {
51
51
  /** The resolved rows this judge was compiled from (canonical names — the disclosure face). */
52
52
  readonly rows: readonly WriteProtectedRow[];
53
53
  /** Judge ONE path spelling. Returns the first matching row (basename rows first, then segment,
54
- * then segment-run — deterministic), or null. */
55
- matchPath(path: string): WriteProtectedHit | null;
54
+ * then segment-run — deterministic), or null.
55
+ *
56
+ * `cwd` = the call's LIVE tracked working directory, read at judgment time off the ONE ref the fs
57
+ * hand tools resolve with — the law every path guard here follows: judge the file the tool will write.
58
+ * Absent ⇒ judged exactly as given — the historical reading, and the honest answer for a host
59
+ * that tracks no cwd. */
60
+ matchPath(path: string, cwd?: string): WriteProtectedHit | null;
56
61
  }
57
62
  /**
58
63
  * Compile the write-protection judge for a deployment configuration. Returns `undefined` when the
@@ -82,4 +87,4 @@ export declare function compileWriteProtection(entries?: readonly WriteProtected
82
87
  */
83
88
  export declare function createWriteProtectionCheck(entries?: readonly WriteProtectedEntry[], opts?: {
84
89
  dataRoot?: string;
85
- }): ((toolName: string, args: unknown, face?: ToolCallLike["face"]) => WriteProtectedHit | null) | undefined;
90
+ }): ((toolName: string, args: unknown, face?: ToolCallLike["face"], cwd?: string) => WriteProtectedHit | null) | undefined;
@@ -1,4 +1,4 @@
1
- import { isWinFormPath, writeTargetPath, isUncPath } from "../tools/fs/safety.js";
1
+ import { isWinFormPath, writeTargetPath, isUncPath, isAbsoluteForFamily, joinForFamily, pathFamilyOf } from "../tools/fs/safety.js";
2
2
  import { isProtectedWrite } from "./tool-registry.js";
3
3
  import { dataRootSpellings } from "./sensitive-path-policy.js";
4
4
  const freezeTable = (rows) => Object.freeze(rows.map((r) => Object.freeze(r)));
@@ -182,8 +182,11 @@ export function compileWriteProtection(entries, opts) {
182
182
  }
183
183
  return {
184
184
  rows,
185
- matchPath(path) {
186
- const judged = relativeUnderDataRoot(path, rootViews) ?? path;
185
+ matchPath(path, cwd) {
186
+ const base = typeof cwd === "string" && cwd.length > 0 ? cwd : undefined;
187
+ const family = pathFamilyOf({ cwd: base });
188
+ const spelled = base !== undefined && family !== undefined && !isAbsoluteForFamily(family, path) ? joinForFamily(family, base, path) : path;
189
+ const judged = relativeUnderDataRoot(spelled, rootViews) ?? spelled;
187
190
  const judge = (raw) => {
188
191
  if (raw.length === 0)
189
192
  return null;
@@ -213,13 +216,13 @@ export function createWriteProtectionCheck(entries, opts) {
213
216
  const matcher = compileWriteProtection(entries, opts);
214
217
  if (matcher === undefined)
215
218
  return undefined;
216
- return (toolName, args, face) => {
219
+ return (toolName, args, face, cwd) => {
217
220
  const call = { toolName, args, ...(face !== undefined ? { face } : {}) };
218
221
  if (!isProtectedWrite(call))
219
222
  return null;
220
223
  const path = writeTargetPath(call);
221
224
  if (typeof path !== "string" || path.length === 0)
222
225
  return null;
223
- return matcher.matchPath(path);
226
+ return matcher.matchPath(path, cwd);
224
227
  };
225
228
  }
@@ -359,7 +359,7 @@ export class NodeExecutionEnv {
359
359
  const stderrTail = new RollingTailBuffer();
360
360
  let settled = false;
361
361
  let timedOut = false;
362
- let abortKillInFlight = false;
362
+ let terminationInFlight = false;
363
363
  let callbackError;
364
364
  let child;
365
365
  const timeoutRef = {};
@@ -391,7 +391,7 @@ export class NodeExecutionEnv {
391
391
  return { stdout: markTruncated(so.text, so.droppedBytes), stderr: markTruncated(se.text, se.droppedBytes) };
392
392
  };
393
393
  const onAbort = () => {
394
- abortKillInFlight = true;
394
+ terminationInFlight = true;
395
395
  if (child?.pid) {
396
396
  const abortTarget = child;
397
397
  killProcessTree(child.pid, { stillRunning: () => abortTarget.exitCode === null && abortTarget.signalCode === null });
@@ -480,10 +480,11 @@ export class NodeExecutionEnv {
480
480
  timeoutMs === undefined
481
481
  ? undefined
482
482
  : setTimeout(() => {
483
- if (options?.autoBackgroundOnTimeout && !abortKillInFlight && adoptOnTimeoutRef.current) {
483
+ if (options?.autoBackgroundOnTimeout && !terminationInFlight && adoptOnTimeoutRef.current) {
484
484
  adoptOnTimeoutRef.current();
485
485
  return;
486
486
  }
487
+ terminationInFlight = true;
487
488
  timedOut = true;
488
489
  if (child?.pid) {
489
490
  const timeoutTarget = child;
@@ -598,9 +599,11 @@ export class NodeExecutionEnv {
598
599
  spoolPumpRef.current.unref?.();
599
600
  }
600
601
  const onDetach = (cause) => {
601
- if (settled || !child)
602
+ if (settled || terminationInFlight || !child)
602
603
  return false;
603
604
  pumpSpoolRef.current?.();
605
+ if (settled || terminationInFlight)
606
+ return false;
604
607
  const shellId = this.adoptRunningChild(child, stdoutTail.result(), stderrTail.result(), execSpoolRef.current !== undefined ? { spool: execSpoolRef.current, cursors: { out: spoolCursor.out, err: spoolCursor.err } } : undefined);
605
608
  if (shellId === undefined)
606
609
  return false;
@@ -616,6 +619,9 @@ export class NodeExecutionEnv {
616
619
  adoptOnTimeoutRef.current = () => {
617
620
  if (onDetach("timeout"))
618
621
  return;
622
+ if (terminationInFlight)
623
+ return;
624
+ terminationInFlight = true;
619
625
  timedOut = true;
620
626
  if (child?.pid) {
621
627
  const adoptFallbackTarget = child;
@@ -1,4 +1,5 @@
1
1
  import type { ActorAssertion, AssistantMessage, ImageContent, Model, UserMessage } from "../llm/index.js";
2
+ import type { HaltRemainingDirective } from "../loop/types.js";
2
3
  import type { AgentMessage, AgentTool, LoopMalformedToolUseRecovery, LoopThinkingOnlyRecovery, LoopTruncatedOutputRecovery, QueueMode, ThinkingLevel } from "../loop/types.js";
3
4
  import { type EngineSegment } from "../../core/untrusted-text.js";
4
5
  import type { AbortResult, AgentHarnessEvent, AgentHarnessEventResultMap, AgentHarnessOptions, AgentHarnessOwnEvent, AgentHarnessResources, AgentHarnessStreamOptions, ExecutionEnv, PromptTemplate, Skill } from "./types.js";
@@ -24,6 +25,16 @@ export interface HaltConsumption {
24
25
  readonly sources: ReadonlyArray<StopRequestSource>;
25
26
  };
26
27
  }
28
+ /** `prompt()` option: settle the transcript's trailing UNANSWERED tool batch through the loop's own batch pipeline
29
+ * BEFORE the prompt is delivered (a resumed run whose suspended batch still holds unanswered calls). `halt` settles
30
+ * every unanswered call as not-executed instead (the batch-halt form). No unanswered tail ⇒ a no-op. */
31
+ export interface TrailingToolBatchOptions {
32
+ readonly halt?: HaltRemainingDirective;
33
+ }
34
+ /** `prompt()` option: called when the prompt and next-turn queue's messages enter the loop transcript, before
35
+ * their frames are emitted and committed. Not called when a trailing-batch settlement ends the run first — the
36
+ * caller then still owns whatever the prompt was carrying. */
37
+ export type PromptDeliveredCallback = () => void;
27
38
  export interface UserMessageProvenance {
28
39
  /** Exact char length of the engine-prepended prefix on `text` (first-frame reminders etc.).
29
40
  * Sugar for the single segment `[0, chars)` — when `engineSegments` is also supplied, segments win. */
@@ -124,6 +135,14 @@ export declare class AgentHarness<TSkill extends Skill = Skill, TPromptTemplate
124
135
  readonly env: ExecutionEnv;
125
136
  private session;
126
137
  private phase;
138
+ /** Can an engine injection land at a turn boundary RIGHT NOW? A producer choosing a destination
139
+ * BEFORE it commits (the session notification route) needs this as a fact, not as a rejected
140
+ * promise afterwards. Three conjuncts, each a condition a later stage enforces: a turn must EXIST
141
+ * (`steer`/`followUp` throw while idle); the run must not be UNWINDING; it must not be STOPPING
142
+ * (`halt()` / `requestStopAfterTurn()` latch a source and the loop exits at the next boundary) —
143
+ * the phase reads `"turn"` through all of that. Nothing awaits between this read and the injection
144
+ * it gates, so a `true` cannot go stale under its caller. */
145
+ get acceptsInjections(): boolean;
127
146
  /** design/74 R3-A: the one-way clean-stop latch — SET iff this set is non-empty. When set, the boundary consults
128
147
  * answer true so the loop exits CLEANLY (emit `agent_end` + return) WITHOUT an abort/interrupt — the path a
129
148
  * resource-slice suspend needs (an abort would trip the orphan-[INTERRUPTED] reconcile = corrupt resume). It is
@@ -136,6 +155,7 @@ export declare class AgentHarness<TSkill extends Skill = Skill, TPromptTemplate
136
155
  * per turn, so identity carries the generation); the loop's cut-consumption report is accepted for THIS seat only. */
137
156
  private lastPublishedSeat?;
138
157
  private readonly seatCutSources;
158
+ private trailingBatchEndedOn?;
139
159
  private turnCutConsumed?;
140
160
  /** backlog #389 — one-way latch set by {@link abort}. Read by `drainQueuedMessages`: no boundary may
141
161
  * hand a queued frame to the model once the run is unwinding, and — crucially — none may SPLICE one
@@ -208,6 +228,26 @@ export declare class AgentHarness<TSkill extends Skill = Skill, TPromptTemplate
208
228
  * clearing, so they are LEFT STANDING (the durable-park migration reads them AFTER abort()
209
229
  * returns) and the abort-side pass is the belt for an exit that never reached agent_end. */
210
230
  private sweepUndrainedEngineNotes;
231
+ /**
232
+ * How much of a PERSON's input is currently accepted-but-unserved, per lane — the one predicate behind
233
+ * both readers below, so "a person's input is waiting" cannot come to mean two things. Engine-AUTHORED
234
+ * frames are excluded, and the IN-FLIGHT slot counts (a frame spliced out of a queue and never injected
235
+ * is as pending as one that never left it).
236
+ */
237
+ private pendingUserInputsByLane;
238
+ /**
239
+ * Is a PERSON's input waiting for this run's NEXT TURN BOUNDARY? The live read face over
240
+ * {@link pendingUserInputsByLane}, for the producer that must decide whether to YIELD that boundary
241
+ * sooner — the shell's auto-backgrounding leg.
242
+ *
243
+ * THE STEER LANE ONLY: the follow-up lane's contract is delivery at the run's natural
244
+ * would-otherwise-stop seat, which backgrounding a command cannot bring forward.
245
+ * `announceUndrainedUserInputs` still reads both lanes.
246
+ *
247
+ * Synchronous and side-effect-free by contract, the same posture as {@link pendingInjectionCount}:
248
+ * nothing is awaited between the read and the action it gates. An UNWINDING run answers `false`.
249
+ */
250
+ hasPendingUserInput(): boolean;
211
251
  /** backlog #259 — what remains in the steer/follow-up queues AFTER the engine-note sweep is USER
212
252
  * input that was accepted ("queued") and will never be consumed: the run reached agent_end first.
213
253
  * User inputs have no redelivery semantics (unlike engine notes — a steer aimed at a finished run
@@ -320,6 +360,8 @@ export declare class AgentHarness<TSkill extends Skill = Skill, TPromptTemplate
320
360
  * zero-assistant ending still throws (see the executeTurn tail). */
321
361
  prompt(text: string, options?: {
322
362
  images?: ImageContent[];
363
+ settleUnansweredToolCalls?: TrailingToolBatchOptions;
364
+ onDelivered?: PromptDeliveredCallback;
323
365
  } & UserMessageProvenance): Promise<AssistantMessage | undefined>;
324
366
  /** R2-③: an empty/whitespace-only injection with no images carries ZERO information — it
325
367
  * must not mint a user frame (strict endpoints reject empty user content, and an empty follow-up would
@@ -187,6 +187,9 @@ export class AgentHarness {
187
187
  env;
188
188
  session;
189
189
  phase = "idle";
190
+ get acceptsInjections() {
191
+ return this.phase !== "idle" && !this.aborting && !this.stopRequested;
192
+ }
190
193
  stopRequestSources = new Set();
191
194
  get stopRequested() {
192
195
  return this.stopRequestSources.size > 0;
@@ -194,6 +197,7 @@ export class AgentHarness {
194
197
  boundaryStopConsumed;
195
198
  lastPublishedSeat;
196
199
  seatCutSources = new WeakMap();
200
+ trailingBatchEndedOn;
197
201
  turnCutConsumed;
198
202
  aborting = false;
199
203
  drainedPendingInjection = [];
@@ -229,12 +233,20 @@ export class AgentHarness {
229
233
  }
230
234
  }
231
235
  }
232
- announceUndrainedUserInputs() {
236
+ pendingUserInputsByLane() {
233
237
  const inFlight = (lane) => this.drainedPendingInjection.filter((m) => !engineAuthoredInjections.has(m) && drainedFromLane.get(m) === lane).length;
234
- const counts = {
238
+ return {
235
239
  steer: this.steerQueue.filter((m) => !engineAuthoredInjections.has(m)).length + inFlight("steer"),
236
240
  followUp: this.followUpQueue.filter((m) => !engineAuthoredInjections.has(m)).length + inFlight("followUp"),
237
241
  };
242
+ }
243
+ hasPendingUserInput() {
244
+ if (this.aborting)
245
+ return false;
246
+ return this.pendingUserInputsByLane().steer > 0;
247
+ }
248
+ announceUndrainedUserInputs() {
249
+ const counts = this.pendingUserInputsByLane();
238
250
  if ((counts.steer > 0 || counts.followUp > 0) && this.onUndrainedUserInputs) {
239
251
  try {
240
252
  this.onUndrainedUserInputs(counts);
@@ -826,29 +838,7 @@ export class AgentHarness {
826
838
  }
827
839
  async executeTurn(turnState, text, options) {
828
840
  let activeTurnState = turnState;
829
- let messages = [createUserMessage(text, options?.images, options)];
830
- if (this.nextTurnQueue.length > 0) {
831
- const queuedMessages = this.nextTurnQueue.splice(0);
832
- try {
833
- await this.emitQueueUpdate();
834
- }
835
- catch (error) {
836
- this.nextTurnQueue.unshift(...queuedMessages);
837
- throw normalizeHookError(error);
838
- }
839
- for (const m of queuedMessages) {
840
- const payload = engineNotePayloads.get(m);
841
- if (payload !== undefined) {
842
- try {
843
- this.onEngineNoteConsumed?.(payload);
844
- }
845
- catch {
846
- }
847
- }
848
- engineNotePayloads.delete(m);
849
- }
850
- messages = [...queuedMessages, messages[0]];
851
- }
841
+ const userMessage = createUserMessage(text, options?.images, options);
852
842
  const beforeResult = await this.emitHook({
853
843
  type: "before_agent_start",
854
844
  prompt: text,
@@ -856,9 +846,35 @@ export class AgentHarness {
856
846
  systemPrompt: turnState.systemPrompt,
857
847
  resources: turnState.resources,
858
848
  });
859
- if (beforeResult?.messages) {
860
- messages = [...messages, ...beforeResult.messages];
861
- }
849
+ const takePrompts = async () => {
850
+ let messages = [userMessage];
851
+ if (this.nextTurnQueue.length > 0) {
852
+ const queuedMessages = this.nextTurnQueue.splice(0);
853
+ try {
854
+ await this.emitQueueUpdate();
855
+ }
856
+ catch (error) {
857
+ this.nextTurnQueue.unshift(...queuedMessages);
858
+ throw normalizeHookError(error);
859
+ }
860
+ for (const m of queuedMessages) {
861
+ const payload = engineNotePayloads.get(m);
862
+ if (payload !== undefined) {
863
+ try {
864
+ this.onEngineNoteConsumed?.(payload);
865
+ }
866
+ catch {
867
+ }
868
+ }
869
+ engineNotePayloads.delete(m);
870
+ }
871
+ messages = [...queuedMessages, messages[0]];
872
+ }
873
+ if (beforeResult?.messages) {
874
+ messages = [...messages, ...beforeResult.messages];
875
+ }
876
+ return messages;
877
+ };
862
878
  const abortController = new AbortController();
863
879
  const getTurnState = () => activeTurnState;
864
880
  const setTurnState = (nextTurnState) => {
@@ -867,7 +883,20 @@ export class AgentHarness {
867
883
  this.runAbortController = abortController;
868
884
  const runResultPromise = (async () => {
869
885
  try {
870
- return await runAgentLoop(messages, this.createContext(turnState, beforeResult?.systemPrompt), this.createLoopConfig(getTurnState, setTurnState, beforeResult?.systemPrompt), (event) => this.handleAgentEvent(event, abortController.signal), abortController.signal, this.createStreamFn(getTurnState), undefined, this.loopTrace);
886
+ return await runAgentLoop(takePrompts, this.createContext(turnState, beforeResult?.systemPrompt), this.createLoopConfig(getTurnState, setTurnState, beforeResult?.systemPrompt), (event) => this.handleAgentEvent(event, abortController.signal), abortController.signal, this.createStreamFn(getTurnState), undefined, this.loopTrace, {
887
+ ...(options?.settleUnansweredToolCalls !== undefined
888
+ ? {
889
+ settle: {
890
+ ...(options.settleUnansweredToolCalls.halt !== undefined ? { halt: options.settleUnansweredToolCalls.halt } : {}),
891
+ report: (outcome) => {
892
+ if (outcome.aborted)
893
+ this.trailingBatchEndedOn = outcome.assistantMessage;
894
+ },
895
+ },
896
+ }
897
+ : {}),
898
+ ...(options?.onDelivered !== undefined ? { onPromptsDelivered: options.onDelivered } : {}),
899
+ });
871
900
  }
872
901
  catch (error) {
873
902
  try {
@@ -889,6 +918,8 @@ export class AgentHarness {
889
918
  }
890
919
  if (this.stopRequested)
891
920
  return undefined;
921
+ if (this.trailingBatchEndedOn !== undefined)
922
+ return this.trailingBatchEndedOn;
892
923
  throw new AgentHarnessError("invalid_state", "AgentHarness prompt completed without an assistant message");
893
924
  }
894
925
  finally {
@@ -904,6 +935,7 @@ export class AgentHarness {
904
935
  if (this.phase !== "idle") {
905
936
  throw new AgentHarnessError("busy", "AgentHarness is busy");
906
937
  }
938
+ this.trailingBatchEndedOn = undefined;
907
939
  this.stopRequestSources.clear();
908
940
  this.boundaryStopConsumed = undefined;
909
941
  this.turnCutConsumed = undefined;
@@ -1,5 +1,29 @@
1
+ /**
2
+ * First-party agent loop (design/118 ④a — de-vendoring 4/6).
3
+ *
4
+ * Rewritten from the vendored openclaw distillation with the loop's control flow made EXPLICIT as a
5
+ * state machine (the CC query.ts State/transition shape, sema deps injected): every "go around again"
6
+ * is a named {@link LoopContinue} and every exit is a named {@link LoopTerminal}, so tests can assert
7
+ * WHICH path ran (test/loop-transitions.test.ts) instead of inferring it from event order. ④a is
8
+ * behavior-1:1 with the vendored loop — no new recovery chains; those land in ④b as new Continue
9
+ * reasons on this skeleton.
10
+ *
11
+ * Load-bearing invariants carried over verbatim (the VENDORED EDIT provenance comments are kept at
12
+ * their sites as history):
13
+ * - A-1 layer ② abort-before-stream guard (a mid-batch abort must never reach the next provider
14
+ * request) — {@link runSingleTurn} entry guard, Terminal `aborted_before_stream`.
15
+ * - Durable double-execution gate (a deferred parallel tool closure must never run its side effect
16
+ * after a mid-batch suspend/abort) — {@link executePreparedToolCall} entry guard.
17
+ * - D4 post-hook posture (afterToolCall throw must not rewrite a committed side effect into a flat
18
+ * failure) — {@link finalizeExecutedToolCall}.
19
+ * - The TWO queue drain points (1.204 priority wiring基石): steer drains at every turn end
20
+ * (Continue `steer_injected` / mid-work boundary), followUp drains only when the agent would
21
+ * otherwise stop (Continue `follow_up_injected`). Never merge these into one queue.
22
+ * - text_delta.partial reconstruction (upstream cherry-pick 6fcc945702).
23
+ */
24
+ import type { AssistantMessage, ToolResultMessage } from "../llm/index.js";
1
25
  import { type AgentCoreStreamRuntimeDeps } from "./runtime-deps.js";
2
- import type { AgentContext, AgentEvent, AgentLoopConfig, AgentMessage, StreamFn } from "./types.js";
26
+ import type { AgentContext, AgentEvent, AgentLoopConfig, AgentMessage, HaltRemainingDirective, StreamFn } from "./types.js";
3
27
  /** Callback used by synchronous loop runners to publish agent lifecycle events. */
4
28
  export type AgentEventSink = (event: AgentEvent) => Promise<void> | void;
5
29
  /** Named reasons the loop goes around again. Assertable in tests via the trace sink. */
@@ -88,8 +112,48 @@ export type LoopStep = {
88
112
  * `next_turn`), then `next_turn`. Don't read the trace as a complete enumeration of concurrent causes.
89
113
  */
90
114
  export type LoopTraceSink = (step: LoopStep) => void;
91
- /** Run a prompt-started loop and emit events through a caller-owned sink. */
92
- export declare function runAgentLoop(prompts: AgentMessage[], context: AgentContext, config: AgentLoopConfig, emit: AgentEventSink, signal?: AbortSignal, streamFn?: StreamFn, runtime?: AgentCoreStreamRuntimeDeps, trace?: LoopTraceSink): Promise<AgentMessage[]>;
115
+ /**
116
+ * A request to settle the transcript's trailing UNANSWERED tool batch before the prompts are delivered
117
+ * (a resumed run whose suspended batch still holds calls no tool result answers). The batch is settled
118
+ * as ONE turn of the loop: the live batch pipeline under a turn-scoped interrupt seat, the interrupted-
119
+ * turn settlement when that seat is cut, then the ordinary turn boundary (`prepareNextTurn` + the stop
120
+ * consult). `halt` settles every call as not-executed instead (the batch-halt form). `report` receives
121
+ * the outcome once; `aborted` means the run ENDED before the prompts were delivered (the run signal
122
+ * fired, or the boundary / the interrupt settlement stopped the run).
123
+ */
124
+ export interface TrailingToolBatchSettlement {
125
+ halt?: HaltRemainingDirective;
126
+ report: (outcome: {
127
+ assistantMessage: AssistantMessage;
128
+ settled: number;
129
+ aborted: boolean;
130
+ }) => void;
131
+ }
132
+ /** The caller-facing seams of a prompt run beyond the prompts themselves: an optional trailing-batch
133
+ * settlement, and the moment the prompts enter the loop transcript, before their frames are emitted
134
+ * — delivery accounts precede the carrying messages' commits. */
135
+ export interface RunAgentLoopSeams {
136
+ settle?: TrailingToolBatchSettlement;
137
+ onPromptsDelivered?: () => void;
138
+ }
139
+ /** The last assistant message of `messages` when at least one of its tool calls has no tool result after it. */
140
+ export declare function findUnansweredToolBatch(messages: readonly AgentMessage[]): AssistantMessage | undefined;
141
+ /**
142
+ * Settle the unanswered tool calls of `assistantMessage` (the batch tail a suspended run left open):
143
+ * ONE pipeline with the live turn — the same partition, concurrency cap, preflight gate (a call may
144
+ * suspend again), execution and source-ordered result commit — or, under `halt`, the same not-executed
145
+ * settlement a batch-halt directive applies to never-started siblings. Every unanswered call gets a
146
+ * tool result unless the signal fires mid-batch (the dangling remainder is the suspended batch's own shape).
147
+ */
148
+ export declare function settleUnansweredToolCalls(context: AgentContext, assistantMessage: AssistantMessage, config: AgentLoopConfig, signal: AbortSignal | undefined, emit: AgentEventSink, halt?: HaltRemainingDirective): Promise<ExecutedToolCallBatch>;
149
+ /** Run a prompt-started loop and emit events through a caller-owned sink. `prompts` may be a thunk: it is
150
+ * taken only when the prompts are about to be delivered, so a caller whose prompt assembly CONSUMES queued
151
+ * input (a harness draining its next-turn queue) loses nothing when a trailing-batch settlement ends the run first. */
152
+ export declare function runAgentLoop(prompts: AgentMessage[] | (() => Promise<AgentMessage[]>), context: AgentContext, config: AgentLoopConfig, emit: AgentEventSink, signal?: AbortSignal, streamFn?: StreamFn, runtime?: AgentCoreStreamRuntimeDeps, trace?: LoopTraceSink, seams?: RunAgentLoopSeams): Promise<AgentMessage[]>;
153
+ type ExecutedToolCallBatch = {
154
+ messages: ToolResultMessage[];
155
+ terminate: boolean;
156
+ };
93
157
  /**
94
158
  * How many tool names an unknown-tool miss spells out. A miss is a NAME problem: seeing a couple of dozen
95
159
  * real names is what lets a model spot the one it meant, while a full listing grows with the deployment
@@ -110,3 +174,4 @@ export declare const TOOL_VALIDATION_REFUSED_CODE = "tool.validation_refused";
110
174
  * reported on the fault seat): a validator is a precondition read, not a network call, so a single
111
175
  * bound serves every tool; a deployment validator that needs longer is a validator doing too much. */
112
176
  export declare const TOOL_INPUT_VALIDATION_TIMEOUT_MS = 10000;
177
+ export {};