@qvac/harness 0.7.0 → 1.0.0-rc.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 (204) hide show
  1. package/CHANGELOG.md +42 -1
  2. package/README.md +239 -2
  3. package/dist-lib/index.d.ts +58 -3
  4. package/dist-lib/index.js +736 -239
  5. package/dist-lib/lib/capabilities/completion-engine/abort-polyfill.d.ts +1 -0
  6. package/dist-lib/lib/capabilities/completion-engine/abort-polyfill.js +10 -0
  7. package/dist-lib/lib/capabilities/completion-engine/actors/answering.d.ts +3 -0
  8. package/dist-lib/lib/capabilities/completion-engine/actors/answering.js +17 -0
  9. package/dist-lib/lib/capabilities/completion-engine/actors/approval.d.ts +4 -0
  10. package/dist-lib/lib/capabilities/completion-engine/actors/approval.js +29 -0
  11. package/dist-lib/lib/capabilities/completion-engine/actors/direct-search.d.ts +4 -0
  12. package/dist-lib/lib/capabilities/completion-engine/actors/direct-search.js +73 -0
  13. package/dist-lib/lib/capabilities/completion-engine/actors/execute.d.ts +41 -0
  14. package/dist-lib/lib/capabilities/completion-engine/actors/execute.js +49 -0
  15. package/dist-lib/lib/capabilities/completion-engine/actors/executing.d.ts +3 -0
  16. package/dist-lib/lib/capabilities/completion-engine/actors/executing.js +102 -0
  17. package/dist-lib/lib/capabilities/completion-engine/actors/execution-plan.d.ts +47 -0
  18. package/dist-lib/lib/capabilities/completion-engine/actors/execution-plan.js +87 -0
  19. package/dist-lib/lib/capabilities/completion-engine/actors/finishing.d.ts +2 -0
  20. package/dist-lib/lib/capabilities/completion-engine/actors/finishing.js +32 -0
  21. package/dist-lib/lib/capabilities/completion-engine/actors/inspect-preload.d.ts +3 -0
  22. package/dist-lib/lib/capabilities/completion-engine/actors/inspect-preload.js +75 -0
  23. package/dist-lib/lib/capabilities/completion-engine/actors/inspecting.d.ts +3 -0
  24. package/dist-lib/lib/capabilities/completion-engine/actors/inspecting.js +152 -0
  25. package/dist-lib/lib/capabilities/completion-engine/actors/loading-skill.d.ts +3 -0
  26. package/dist-lib/lib/capabilities/completion-engine/actors/loading-skill.js +94 -0
  27. package/dist-lib/lib/capabilities/completion-engine/actors/preload-failure.d.ts +9 -0
  28. package/dist-lib/lib/capabilities/completion-engine/actors/preload-failure.js +11 -0
  29. package/dist-lib/lib/capabilities/completion-engine/actors/preloading.d.ts +3 -0
  30. package/dist-lib/lib/capabilities/completion-engine/actors/preloading.js +114 -0
  31. package/dist-lib/lib/capabilities/completion-engine/actors/preparing.d.ts +3 -0
  32. package/dist-lib/lib/capabilities/completion-engine/actors/preparing.js +198 -0
  33. package/dist-lib/lib/capabilities/completion-engine/actors/rejecting.d.ts +3 -0
  34. package/dist-lib/lib/capabilities/completion-engine/actors/rejecting.js +71 -0
  35. package/dist-lib/lib/capabilities/completion-engine/actors/rejection.d.ts +18 -0
  36. package/dist-lib/lib/capabilities/completion-engine/actors/rejection.js +70 -0
  37. package/dist-lib/lib/capabilities/completion-engine/actors/resolving.d.ts +3 -0
  38. package/dist-lib/lib/capabilities/completion-engine/actors/resolving.js +60 -0
  39. package/dist-lib/lib/capabilities/completion-engine/actors/stream.d.ts +21 -0
  40. package/dist-lib/lib/capabilities/completion-engine/actors/stream.js +48 -0
  41. package/dist-lib/lib/capabilities/completion-engine/actors/streaming.d.ts +3 -0
  42. package/dist-lib/lib/capabilities/completion-engine/actors/streaming.js +156 -0
  43. package/dist-lib/lib/capabilities/completion-engine/actors/wrap-up.d.ts +7 -0
  44. package/dist-lib/lib/capabilities/completion-engine/actors/wrap-up.js +61 -0
  45. package/dist-lib/lib/capabilities/completion-engine/driver.d.ts +25 -0
  46. package/dist-lib/lib/capabilities/completion-engine/driver.js +553 -0
  47. package/dist-lib/lib/capabilities/completion-engine/durable.d.ts +61 -0
  48. package/dist-lib/lib/capabilities/completion-engine/durable.js +150 -0
  49. package/dist-lib/lib/capabilities/completion-engine/operations.d.ts +6 -0
  50. package/dist-lib/lib/capabilities/completion-engine/operations.js +239 -0
  51. package/dist-lib/lib/capabilities/completion-engine/ports.d.ts +3 -0
  52. package/dist-lib/lib/capabilities/completion-engine/ports.js +267 -0
  53. package/dist-lib/lib/capabilities/completion-engine/session.d.ts +76 -0
  54. package/dist-lib/lib/capabilities/completion-engine/session.js +70 -0
  55. package/dist-lib/lib/capabilities/completion-loop/index.d.ts +80 -0
  56. package/dist-lib/lib/capabilities/completion-loop/index.js +158 -0
  57. package/dist-lib/lib/capabilities/completion-loop/metrics.d.ts +5 -0
  58. package/dist-lib/lib/capabilities/completion-loop/metrics.js +29 -0
  59. package/dist-lib/lib/capabilities/completion-loop/round.d.ts +33 -0
  60. package/dist-lib/lib/capabilities/completion-loop/round.js +353 -0
  61. package/dist-lib/lib/capabilities/completion-loop/tool-execution.d.ts +60 -0
  62. package/dist-lib/lib/capabilities/completion-loop/tool-execution.js +270 -0
  63. package/dist-lib/lib/capabilities/completion-loop/turns.d.ts +23 -0
  64. package/dist-lib/lib/capabilities/completion-loop/turns.js +112 -0
  65. package/dist-lib/lib/capabilities/completion-runtime.d.ts +46 -0
  66. package/dist-lib/lib/capabilities/completion-runtime.js +74 -0
  67. package/dist-lib/lib/capabilities/completion.d.ts +18 -335
  68. package/dist-lib/lib/capabilities/completion.js +20 -1301
  69. package/dist-lib/lib/capabilities/download.d.ts +1 -1
  70. package/dist-lib/lib/capabilities/download.js +7 -2
  71. package/dist-lib/lib/capabilities/invoke.js +5 -3
  72. package/dist-lib/lib/capabilities/tool-plan.d.ts +35 -0
  73. package/dist-lib/lib/capabilities/tool-plan.js +66 -0
  74. package/dist-lib/lib/capabilities/tool-turns.d.ts +11 -2
  75. package/dist-lib/lib/capabilities/tool-turns.js +30 -2
  76. package/dist-lib/lib/capabilities/types.d.ts +287 -0
  77. package/dist-lib/lib/capabilities/types.js +1 -0
  78. package/dist-lib/lib/completion-lifecycle.d.ts +14 -0
  79. package/dist-lib/lib/completion-lifecycle.js +126 -0
  80. package/dist-lib/lib/connect.d.ts +10 -3
  81. package/dist-lib/lib/connect.js +329 -23
  82. package/dist-lib/lib/contract-types.d.ts +19 -2
  83. package/dist-lib/lib/harness-lifecycle.d.ts +66 -0
  84. package/dist-lib/lib/harness-lifecycle.js +129 -0
  85. package/dist-lib/lib/helpers/detach-json.d.ts +1 -0
  86. package/dist-lib/lib/helpers/detach-json.js +146 -0
  87. package/dist-lib/lib/helpers/markup-scanner.d.ts +26 -0
  88. package/dist-lib/lib/helpers/markup-scanner.js +156 -0
  89. package/dist-lib/lib/helpers/markup-stream-filter.js +138 -35
  90. package/dist-lib/lib/helpers/race-abort.d.ts +9 -4
  91. package/dist-lib/lib/helpers/race-abort.js +2 -7
  92. package/dist-lib/lib/helpers/recovery-required.d.ts +1 -0
  93. package/dist-lib/lib/helpers/recovery-required.js +1 -0
  94. package/dist-lib/lib/helpers/run-outcome.d.ts +10 -2
  95. package/dist-lib/lib/helpers/run-outcome.js +8 -5
  96. package/dist-lib/lib/helpers/tool-call-markup.d.ts +2 -0
  97. package/dist-lib/lib/helpers/tool-call-markup.js +197 -82
  98. package/dist-lib/lib/hooks/built-in/arg-repair.d.ts +1 -0
  99. package/dist-lib/lib/hooks/built-in/arg-repair.js +1 -0
  100. package/dist-lib/lib/hooks/built-in/egress.d.ts +1 -0
  101. package/dist-lib/lib/hooks/built-in/egress.js +2 -1
  102. package/dist-lib/lib/hooks/built-in/sandbox.d.ts +1 -0
  103. package/dist-lib/lib/hooks/built-in/sandbox.js +6 -1
  104. package/dist-lib/lib/hooks/built-in/skill-catalog.d.ts +1 -0
  105. package/dist-lib/lib/hooks/built-in/skill-catalog.js +2 -1
  106. package/dist-lib/lib/hooks/pipeline.d.ts +4 -1
  107. package/dist-lib/lib/hooks/pipeline.js +24 -2
  108. package/dist-lib/lib/hooks/run.d.ts +2 -1
  109. package/dist-lib/lib/hooks/run.js +19 -7
  110. package/dist-lib/lib/hooks/types.d.ts +17 -6
  111. package/dist-lib/lib/machine/completion/actions/approval.d.ts +3 -0
  112. package/dist-lib/lib/machine/completion/actions/approval.js +16 -0
  113. package/dist-lib/lib/machine/completion/actions/completion.d.ts +23 -0
  114. package/dist-lib/lib/machine/completion/actions/completion.js +142 -0
  115. package/dist-lib/lib/machine/completion/actions/skills.d.ts +9 -0
  116. package/dist-lib/lib/machine/completion/actions/skills.js +83 -0
  117. package/dist-lib/lib/machine/completion/actions/tools.d.ts +16 -0
  118. package/dist-lib/lib/machine/completion/actions/tools.js +173 -0
  119. package/dist-lib/lib/machine/completion/context.d.ts +9 -0
  120. package/dist-lib/lib/machine/completion/context.js +44 -0
  121. package/dist-lib/lib/machine/completion/contract.d.ts +6 -0
  122. package/dist-lib/lib/machine/completion/contract.js +1 -0
  123. package/dist-lib/lib/machine/completion/guards/retry.d.ts +9 -0
  124. package/dist-lib/lib/machine/completion/guards/retry.js +25 -0
  125. package/dist-lib/lib/machine/completion/implementations.d.ts +81 -0
  126. package/dist-lib/lib/machine/completion/implementations.js +152 -0
  127. package/dist-lib/lib/machine/completion/machine.d.ts +375 -0
  128. package/dist-lib/lib/machine/completion/machine.js +511 -0
  129. package/dist-lib/lib/machine/completion/ports.d.ts +65 -0
  130. package/dist-lib/lib/machine/completion/ports.js +224 -0
  131. package/dist-lib/lib/machine/completion/rules.d.ts +39 -0
  132. package/dist-lib/lib/machine/completion/rules.js +115 -0
  133. package/dist-lib/lib/machine/completion/types.d.ts +493 -0
  134. package/dist-lib/lib/machine/completion/types.js +1 -0
  135. package/dist-lib/lib/machine/engine.d.ts +93 -0
  136. package/dist-lib/lib/machine/engine.js +483 -0
  137. package/dist-lib/lib/machine/errors.d.ts +44 -0
  138. package/dist-lib/lib/machine/errors.js +373 -0
  139. package/dist-lib/lib/machine/guards.d.ts +2 -0
  140. package/dist-lib/lib/machine/guards.js +6 -0
  141. package/dist-lib/lib/machine/index.d.ts +20 -0
  142. package/dist-lib/lib/machine/index.js +11 -0
  143. package/dist-lib/lib/machine/replay-output.d.ts +24 -0
  144. package/dist-lib/lib/machine/replay-output.js +50 -0
  145. package/dist-lib/lib/machine/stable-stringify.d.ts +1 -0
  146. package/dist-lib/lib/machine/stable-stringify.js +11 -0
  147. package/dist-lib/lib/models/model-handle.d.ts +5 -1
  148. package/dist-lib/lib/models/model-handle.js +8 -3
  149. package/dist-lib/lib/models/model-list.d.ts +5 -3
  150. package/dist-lib/lib/models/model-pool.d.ts +18 -7
  151. package/dist-lib/lib/models/model-pool.js +126 -38
  152. package/dist-lib/lib/run-log/history-codec.d.ts +23 -0
  153. package/dist-lib/lib/run-log/history-codec.js +973 -0
  154. package/dist-lib/lib/run-log/patch.d.ts +6 -0
  155. package/dist-lib/lib/run-log/patch.js +93 -0
  156. package/dist-lib/lib/run-log/persisted-actor.d.ts +18 -0
  157. package/dist-lib/lib/run-log/persisted-actor.js +218 -0
  158. package/dist-lib/lib/run-log/protocol-types.d.ts +153 -0
  159. package/dist-lib/lib/run-log/protocol-types.js +1 -0
  160. package/dist-lib/lib/run-log/protocol.d.ts +27 -0
  161. package/dist-lib/lib/run-log/protocol.js +1056 -0
  162. package/dist-lib/lib/run-log/replay.d.ts +25 -0
  163. package/dist-lib/lib/run-log/replay.js +129 -0
  164. package/dist-lib/lib/run-log/run-journal.d.ts +125 -0
  165. package/dist-lib/lib/run-log/run-journal.js +783 -0
  166. package/dist-lib/lib/run-log-inspect.d.ts +25 -0
  167. package/dist-lib/lib/run-log-inspect.js +627 -0
  168. package/dist-lib/lib/run-log-protocol.d.ts +2 -0
  169. package/dist-lib/lib/run-log-protocol.js +1 -0
  170. package/dist-lib/lib/run-log-redact.d.ts +27 -0
  171. package/dist-lib/lib/run-log-redact.js +1107 -0
  172. package/dist-lib/lib/serve.js +308 -12
  173. package/dist-lib/lib/spawn.js +3 -2
  174. package/dist-lib/lib/tool-call-display.d.ts +1 -21
  175. package/dist-lib/lib/tools/exec.d.ts +2 -1
  176. package/dist-lib/lib/tools/exec.js +57 -2
  177. package/dist-lib/lib/tools/generate-image.js +1 -1
  178. package/dist-lib/lib/tools/generate-music.js +1 -1
  179. package/dist-lib/lib/tools/http-request.d.ts +2 -1
  180. package/dist-lib/lib/tools/http-request.js +2 -1
  181. package/dist-lib/lib/tools/mcp-call.js +8 -1
  182. package/dist-lib/lib/tools/operations/builders.d.ts +1 -0
  183. package/dist-lib/lib/tools/operations/builders.js +1 -0
  184. package/dist-lib/lib/tools/operations/compile.js +11 -1
  185. package/dist-lib/lib/tools/operations/manifest.js +1 -0
  186. package/dist-lib/lib/tools/outbound-guard.d.ts +8 -6
  187. package/dist-lib/lib/tools/outbound-guard.js +25 -8
  188. package/dist-lib/lib/tools/skills/catalog.d.ts +1 -0
  189. package/dist-lib/lib/tools/skills/catalog.js +35 -0
  190. package/dist-lib/lib/tools/skills/cli-schema-types.d.ts +1 -0
  191. package/dist-lib/lib/tools/skills/cli-schema.js +25 -0
  192. package/dist-lib/lib/wire.d.ts +5 -0
  193. package/dist-lib/lib/wire.js +128 -5
  194. package/dist-lib/schema/harness.js +22 -6
  195. package/dist-lib/spec/hrpc/messages.js +100 -18
  196. package/dist-lib/spec/hyperschema/index.js +100 -18
  197. package/dist-lib/spec/hyperschema/schema.json +74 -1
  198. package/dist-lib/spec/hyperschema/types.d.ts +11 -3
  199. package/package.json +19 -1
  200. package/patches/@qvac%2Finference@0.20.0.patch +67 -0
  201. package/patches/bare-stow@0.1.5.patch +68 -0
  202. package/patches/hypercore-blob-server@1.15.0.patch +15 -0
  203. package/patches/localwatch@1.4.3.patch +13 -0
  204. package/patches/pyodide@314.0.2.patch +144 -0
@@ -1,10 +1,5 @@
1
- export class AbortedError extends Error {
2
- constructor() {
3
- super('aborted');
4
- this.name = 'AbortedError';
5
- Error.captureStackTrace?.(this, AbortedError);
6
- }
7
- }
1
+ import { AbortedError } from '../machine/index.js';
2
+ export { AbortedError };
8
3
  export function raceAbort(promise, signal) {
9
4
  if (!signal)
10
5
  return promise;
@@ -0,0 +1 @@
1
+ export { RecoveryRequired, type Operation } from '../machine/index.ts';
@@ -0,0 +1 @@
1
+ export { RecoveryRequired } from '../machine/index.js';
@@ -1,5 +1,6 @@
1
+ import type { RunOutcome } from '../machine/index.ts';
1
2
  import type { ToolExecutionOutcome } from '../tool-call-display.ts';
2
- export type RunOutcome = 'answered' | 'mute' | 'exhausted' | 'search-capped' | 'time-capped';
3
+ export type { RunOutcome };
3
4
  export declare const DEGENERATE_ANSWER_FALLBACK = "I couldn't complete that step and don't have a useful result to share.";
4
5
  export declare const ROUND_CAP_ANSWER = "I ran out of steps before I could finish that. Ask me to continue and I'll pick up where I left off.";
5
6
  export declare const SEARCH_CAPPED_ANSWER = "I searched the web but couldn't find the answer in the results I got.";
@@ -19,9 +20,16 @@ export interface RunOutcomeTracker {
19
20
  record(result: unknown, outcome: ToolExecutionOutcome): void;
20
21
  finishRound(): void;
21
22
  confirmWrite(result: unknown): void;
23
+ snapshot(): RunOutcomeState;
22
24
  facts(): RunOutcomeFacts;
23
25
  }
24
- export declare function createRunOutcomeTracker(): RunOutcomeTracker;
26
+ export interface RunOutcomeState {
27
+ confirmedWrites: string[];
28
+ roundFailure: string | null;
29
+ latestFailure: string | null;
30
+ hasRoundResult: boolean;
31
+ }
32
+ export declare function createRunOutcomeTracker(state?: RunOutcomeState): RunOutcomeTracker;
25
33
  export declare function normalizeToolExecutionOutcome(result: unknown, outcome: ToolExecutionOutcome): ToolExecutionOutcome;
26
34
  export declare function failureFromToolExecution(result: unknown, outcome: ToolExecutionOutcome): string | null;
27
35
  export declare function degenerateAnswer(outcome: Exclude<RunOutcome, 'answered'>, history: ReadonlyArray<HistoryTurn>, facts?: RunOutcomeFacts): string;
@@ -13,11 +13,11 @@ const TOOL_RESULT_HEADER_RE = /^\[Tool: [^(\]]+ \([^)]+\)\]\n/;
13
13
  const WEB_SEARCH_TURN_RE = /^\[Tool: web_search /;
14
14
  const RESULT_ENTRY_RE = /\[\d+\] ([^\n]+)\nSource: (\S+)/g;
15
15
  const WRITE_ID_RE = /[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}/gi;
16
- export function createRunOutcomeTracker() {
17
- const confirmedWrites = [];
18
- let roundFailure = null;
19
- let latestFailure = null;
20
- let hasRoundResult = false;
16
+ export function createRunOutcomeTracker(state) {
17
+ const confirmedWrites = [...(state?.confirmedWrites ?? [])];
18
+ let roundFailure = state?.roundFailure ?? null;
19
+ let latestFailure = state?.latestFailure ?? null;
20
+ let hasRoundResult = state?.hasRoundResult ?? false;
21
21
  return {
22
22
  confirmedWrites,
23
23
  beginRound() {
@@ -42,6 +42,9 @@ export function createRunOutcomeTracker() {
42
42
  confirmedWrites.push(write);
43
43
  }
44
44
  },
45
+ snapshot() {
46
+ return { confirmedWrites: [...confirmedWrites], roundFailure, latestFailure, hasRoundResult };
47
+ },
45
48
  facts() {
46
49
  return { latestFailure, confirmedWrites };
47
50
  }
@@ -10,6 +10,8 @@ export interface KnownTool {
10
10
  readonly name: string;
11
11
  }
12
12
  export declare function recoverMarkupToolCalls(text: string, knownTools: ReadonlyArray<KnownTool>): RecoveredToolCall[];
13
+ export declare function redactToolCallMarkup(text: string, redactArgs: (name: string, args: Record<string, unknown>) => Record<string, unknown>): string;
14
+ export declare function redactBareParameterMarkup(text: string, isSensitiveName: (name: string) => boolean): string;
13
15
  export declare function stripToolCallMarkup(text: string): string;
14
16
  export declare function stripToolResponseBlocks(text: string): string;
15
17
  export interface MarkupToolCall {
@@ -1,47 +1,8 @@
1
1
  import { isRecord } from '../guards.js';
2
- const FUNCTION_OPEN = '<function=';
3
- const FUNCTION_CLOSE = '</function>';
2
+ import { namedMarkupBlocks, scanNamedMarkup } from './markup-scanner.js';
4
3
  const TOOL_RESPONSE = { open: '<tool_response>', close: '</tool_response>' };
5
4
  const TOOL_CALL = { open: '<tool_call>', close: '</tool_call>' };
6
- const PARAMETER_OPEN = '<parameter=';
7
- const PARAMETER_CLOSE = '</parameter>';
8
5
  const MAX_NAME_LENGTH = 64;
9
- function* markupBlocks(text, open, close) {
10
- let cursor = 0;
11
- for (;;) {
12
- const start = text.indexOf(open, cursor);
13
- if (start === -1) {
14
- return;
15
- }
16
- const nameStart = start + open.length;
17
- cursor = nameStart;
18
- const name = nameOf(text, nameStart);
19
- if (name === null) {
20
- continue;
21
- }
22
- const bodyStart = nameStart + name.length + 1;
23
- const bodyEnd = text.indexOf(close, bodyStart);
24
- if (bodyEnd === -1) {
25
- return;
26
- }
27
- yield {
28
- name: name.trim(),
29
- body: text.slice(bodyStart, bodyEnd),
30
- start,
31
- end: bodyEnd + close.length
32
- };
33
- cursor = bodyEnd + close.length;
34
- }
35
- }
36
- function nameOf(text, nameStart) {
37
- const window = text.slice(nameStart, nameStart + MAX_NAME_LENGTH + 1);
38
- const nameEnd = window.indexOf('>');
39
- if (nameEnd === -1) {
40
- return null;
41
- }
42
- const name = window.slice(0, nameEnd);
43
- return name.includes('\n') ? null : name;
44
- }
45
6
  export const MAX_RAW_CALL_CHARS = 4096;
46
7
  export function clipRawCall(raw) {
47
8
  return raw.length > MAX_RAW_CALL_CHARS ? raw.slice(0, MAX_RAW_CALL_CHARS) : raw;
@@ -49,34 +10,172 @@ export function clipRawCall(raw) {
49
10
  export function recoverMarkupToolCalls(text, knownTools) {
50
11
  const known = new Set(knownTools.map((tool) => tool.name));
51
12
  const calls = [];
52
- for (const block of markupBlocks(text, FUNCTION_OPEN, FUNCTION_CLOSE)) {
53
- if (!block.name || !known.has(block.name))
13
+ for (const block of namedMarkupBlocks(text, 'function', MAX_NAME_LENGTH)) {
14
+ if (block.nested) {
15
+ calls.push(...recoverLegacyCalls(text.slice(block.start, block.end), known));
16
+ continue;
17
+ }
18
+ if (!block.name || !known.has(block.name)) {
54
19
  continue;
20
+ }
55
21
  calls.push({
56
- id: `recovered-markup-${calls.length}`,
22
+ id: '',
57
23
  name: block.name,
58
- arguments: decodeArguments(block.body),
24
+ arguments: decodeArguments(block.body).args,
59
25
  raw: clipRawCall(text.slice(block.start, block.end))
60
26
  });
61
27
  }
62
- return calls;
28
+ return calls.map((call, index) => ({ ...call, id: `recovered-markup-${index}` }));
29
+ }
30
+ function recoverLegacyCalls(text, known) {
31
+ const calls = [];
32
+ let cursor = 0;
33
+ for (;;) {
34
+ const start = text.indexOf('<function=', cursor);
35
+ if (start === -1) {
36
+ return calls;
37
+ }
38
+ const nameStart = start + '<function='.length;
39
+ const nameEnd = text.indexOf('>', nameStart);
40
+ const close = nameEnd === -1 ? -1 : text.indexOf('</function>', nameEnd + 1);
41
+ if (nameEnd === -1 || nameEnd - nameStart > MAX_NAME_LENGTH || close === -1) {
42
+ return calls;
43
+ }
44
+ const name = text.slice(nameStart, nameEnd).trim();
45
+ const end = close + '</function>'.length;
46
+ if (name && known.has(name)) {
47
+ calls.push({
48
+ id: '',
49
+ name,
50
+ arguments: decodeLegacyArguments(text.slice(nameEnd + 1, close), 64),
51
+ raw: clipRawCall(text.slice(start, end))
52
+ });
53
+ }
54
+ cursor = end;
55
+ }
56
+ }
57
+ export function redactToolCallMarkup(text, redactArgs) {
58
+ let out = '';
59
+ let cursor = 0;
60
+ let changed = false;
61
+ for (const block of namedMarkupBlocks(text, 'function', Infinity)) {
62
+ const replacement = redactedMarkupBlock(block, redactArgs);
63
+ if (replacement === null) {
64
+ continue;
65
+ }
66
+ out += text.slice(cursor, block.start) + replacement;
67
+ cursor = block.end;
68
+ changed = true;
69
+ }
70
+ out += text.slice(cursor);
71
+ return changed ? out : text;
72
+ }
73
+ function redactedMarkupBlock(block, redactArgs) {
74
+ if (block.nested) {
75
+ return '<function=redacted></function>';
76
+ }
77
+ const { args: decoded, ambiguous } = decodeArguments(block.body, Infinity);
78
+ if (ambiguous) {
79
+ return `<function=${block.name}></function>`;
80
+ }
81
+ const redacted = redactArgs(block.name, decoded);
82
+ const replacement = deepEqual(decoded, redacted)
83
+ ? null
84
+ : markupForCall({ name: block.name, arguments: redacted });
85
+ return replacement;
86
+ }
87
+ export function redactBareParameterMarkup(text, isSensitiveName) {
88
+ if (!/<parameter/i.test(text)) {
89
+ return text;
90
+ }
91
+ let out = '';
92
+ let cursor = 0;
93
+ let changed = false;
94
+ for (;;) {
95
+ const open = scanNamedMarkup(text, 'parameter', { from: cursor, maxNameLength: Infinity }).token;
96
+ if (!open) {
97
+ break;
98
+ }
99
+ if (open.kind === 'close' || !isSensitiveName(open.name ?? '')) {
100
+ cursor = open.end;
101
+ continue;
102
+ }
103
+ const close = scanNamedMarkup(text, 'parameter', {
104
+ from: open.end,
105
+ maxNameLength: Infinity
106
+ }).token;
107
+ out += text.slice(cursor, open.end);
108
+ changed = true;
109
+ if (close?.kind === 'close') {
110
+ out += `[redacted]${text.slice(close.start, close.end)}`;
111
+ cursor = close.end;
112
+ }
113
+ else {
114
+ out += '[redacted]';
115
+ cursor = text.length;
116
+ break;
117
+ }
118
+ }
119
+ out += text.slice(cursor);
120
+ return redactRunOnParameterOpeners(changed ? out : text, isSensitiveName);
121
+ }
122
+ function redactRunOnParameterOpeners(text, isSensitiveName) {
123
+ const opener = /<parameter\s*=\s*([^\s<>]+)\s+(?=[^\s>])/gi;
124
+ const closer = /<\/parameter>/gi;
125
+ let out = '';
126
+ let cursor = 0;
127
+ for (let match = opener.exec(text); match; match = opener.exec(text)) {
128
+ if (!isSensitiveName(match[1])) {
129
+ continue;
130
+ }
131
+ const valueStart = match.index + match[0].length;
132
+ closer.lastIndex = valueStart;
133
+ const close = closer.exec(text);
134
+ out += `${text.slice(cursor, valueStart)}[redacted]`;
135
+ if (!close) {
136
+ return out;
137
+ }
138
+ cursor = close.index;
139
+ opener.lastIndex = close.index;
140
+ }
141
+ return cursor === 0 ? text : out + text.slice(cursor);
142
+ }
143
+ function deepEqual(a, b) {
144
+ if (a === b)
145
+ return true;
146
+ if (Array.isArray(a) || Array.isArray(b)) {
147
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) {
148
+ return false;
149
+ }
150
+ return a.every((value, index) => deepEqual(value, b[index]));
151
+ }
152
+ if (!isRecord(a) || !isRecord(b)) {
153
+ return false;
154
+ }
155
+ const aKeys = Object.keys(a);
156
+ const bKeys = Object.keys(b);
157
+ if (aKeys.length !== bKeys.length) {
158
+ return false;
159
+ }
160
+ return aKeys.every((key) => Object.hasOwn(b, key) && deepEqual(a[key], b[key]));
63
161
  }
64
162
  export function stripToolCallMarkup(text) {
65
163
  let out = '';
66
164
  let cursor = 0;
67
- for (const block of markupBlocks(text, FUNCTION_OPEN, FUNCTION_CLOSE)) {
165
+ for (const block of namedMarkupBlocks(text, 'function', MAX_NAME_LENGTH)) {
68
166
  out += text.slice(cursor, block.start);
69
167
  cursor = block.end;
70
168
  }
71
169
  out += text.slice(cursor);
72
- const dangling = danglingIndex(out);
73
- const withoutCalls = dangling === -1 ? out : out.slice(0, dangling);
74
- const withoutSpans = stripSpans(stripToolResponseBlocks(withoutCalls), TOOL_CALL);
170
+ const withoutSpans = stripSpans(stripToolResponseBlocks(out), TOOL_CALL);
75
171
  return withoutSpans
76
172
  .replace(/<\/?(tool_call|tool_response)>/gi, '')
77
173
  .replace(/\n{3,}/g, '\n\n')
78
174
  .trim();
79
175
  }
176
+ function lowerAscii(char) {
177
+ return char.toLowerCase();
178
+ }
80
179
  function stripSpans(text, span) {
81
180
  const hay = text.replace(/[A-Z]/g, lowerAscii);
82
181
  let out = '';
@@ -95,26 +194,9 @@ function stripSpans(text, span) {
95
194
  }
96
195
  return out + text.slice(cursor);
97
196
  }
98
- function lowerAscii(char) {
99
- return char.toLowerCase();
100
- }
101
197
  export function stripToolResponseBlocks(text) {
102
198
  return stripSpans(text, TOOL_RESPONSE);
103
199
  }
104
- function danglingIndex(text) {
105
- let cursor = 0;
106
- for (;;) {
107
- const start = text.indexOf(FUNCTION_OPEN, cursor);
108
- if (start === -1) {
109
- return -1;
110
- }
111
- const nameStart = start + FUNCTION_OPEN.length;
112
- if (nameOf(text, nameStart) !== null) {
113
- return start;
114
- }
115
- cursor = nameStart;
116
- }
117
- }
118
200
  export function toolCallMarkup(calls) {
119
201
  return calls.map(markupForCall).join('\n');
120
202
  }
@@ -156,24 +238,55 @@ export function normalizeToolArgs(args, declaredKeys) {
156
238
  }
157
239
  return args;
158
240
  }
159
- function decodeArguments(block) {
241
+ function decodeArguments(block, maxNameLength = MAX_NAME_LENGTH) {
160
242
  const args = {};
161
- for (const param of markupBlocks(block, PARAMETER_OPEN, PARAMETER_CLOSE)) {
243
+ let ambiguous = false;
244
+ for (const param of namedMarkupBlocks(block, 'parameter', maxNameLength)) {
162
245
  if (!param.name)
163
246
  continue;
164
- const raw = param.body.trim();
165
- try {
166
- args[param.name] = JSON.parse(raw);
247
+ ambiguous ||= param.nested;
248
+ defineArgument(args, param.name, param.body);
249
+ }
250
+ return { args: ambiguous ? decodeLegacyArguments(block, maxNameLength) : args, ambiguous };
251
+ }
252
+ function decodeLegacyArguments(block, maxNameLength) {
253
+ const args = {};
254
+ let cursor = 0;
255
+ for (;;) {
256
+ const start = block.indexOf('<parameter=', cursor);
257
+ if (start === -1) {
258
+ return args;
167
259
  }
168
- catch {
169
- args[param.name] = raw;
260
+ const nameStart = start + '<parameter='.length;
261
+ const nameEnd = block.indexOf('>', nameStart);
262
+ if (nameEnd === -1 || nameEnd - nameStart > maxNameLength) {
263
+ return args;
264
+ }
265
+ const end = block.indexOf('</parameter>', nameEnd + 1);
266
+ if (end === -1) {
267
+ return args;
268
+ }
269
+ const name = block.slice(nameStart, nameEnd).trim();
270
+ if (name) {
271
+ defineArgument(args, name, block.slice(nameEnd + 1, end));
170
272
  }
273
+ cursor = end + '</parameter>'.length;
171
274
  }
172
- return args;
275
+ }
276
+ function defineArgument(args, name, body) {
277
+ const raw = body.trim();
278
+ let value;
279
+ try {
280
+ value = JSON.parse(raw);
281
+ }
282
+ catch {
283
+ value = raw;
284
+ }
285
+ Object.defineProperty(args, name, { value, enumerable: true, configurable: true, writable: true });
173
286
  }
174
287
  export function recoverSkillNamedCall(text, skillNames, skillToolName) {
175
288
  const known = new Set(skillNames);
176
- for (const block of markupBlocks(text, FUNCTION_OPEN, FUNCTION_CLOSE)) {
289
+ for (const block of namedMarkupBlocks(text, 'function', MAX_NAME_LENGTH)) {
177
290
  if (!known.has(block.name))
178
291
  continue;
179
292
  return [
@@ -190,13 +303,7 @@ export function recoverSkillNamedCall(text, skillNames, skillToolName) {
190
303
  const SKILL_LOAD_KEYS = new Set(['name', 'skill', 'skill_name', 'file', 'path', 'filename']);
191
304
  export function recoverPrintedSkillLoad(text, skillToolName) {
192
305
  for (const match of text.matchAll(/\{[^{}]*\}/g)) {
193
- let candidate;
194
- try {
195
- candidate = JSON.parse(match[0]);
196
- }
197
- catch {
198
- continue;
199
- }
306
+ const candidate = parsePrintedCandidate(match[0]);
200
307
  if (!isPlainObject(candidate)) {
201
308
  continue;
202
309
  }
@@ -223,3 +330,11 @@ export function recoverPrintedSkillLoad(text, skillToolName) {
223
330
  }
224
331
  return [];
225
332
  }
333
+ function parsePrintedCandidate(text) {
334
+ try {
335
+ return JSON.parse(text);
336
+ }
337
+ catch {
338
+ return null;
339
+ }
340
+ }
@@ -1,5 +1,6 @@
1
1
  import type { HarnessHook, HookContext, HookResult } from '../types.ts';
2
2
  export declare class ArgRepairHook implements HarnessHook {
3
+ readonly name = "arg-repair";
3
4
  readonly phase: 'preToolCall';
4
5
  run(ctx: HookContext): HookResult;
5
6
  }
@@ -3,6 +3,7 @@ import { isRecord } from '../../guards.js';
3
3
  import { hlog } from '../../log.js';
4
4
  const STRUCTURED_TYPES = new Set(['object', 'array']);
5
5
  export class ArgRepairHook {
6
+ name = 'arg-repair';
6
7
  phase = 'preToolCall';
7
8
  run(ctx) {
8
9
  if (ctx.phase !== 'preToolCall' || !ctx.parameters)
@@ -1,5 +1,6 @@
1
1
  import type { HarnessHook, HookContext, HookResult } from '../types.ts';
2
2
  export declare class EgressGuardHook implements HarnessHook {
3
+ readonly name = "egress-guard";
3
4
  readonly phase: 'preToolCall';
4
5
  run(ctx: HookContext): HookResult;
5
6
  }
@@ -1,6 +1,7 @@
1
1
  import { mcpTargetUrl } from '../../tools/mcp-call.js';
2
2
  import { checkOutboundHost } from '../../tools/url-guard.js';
3
3
  export class EgressGuardHook {
4
+ name = 'egress-guard';
4
5
  phase = 'preToolCall';
5
6
  run(ctx) {
6
7
  if (ctx.phase !== 'preToolCall')
@@ -24,7 +25,7 @@ export class EgressGuardHook {
24
25
  }
25
26
  const blocked = checkOutboundHost(url);
26
27
  if (blocked) {
27
- return { error: blocked, guidance: true };
28
+ return { kind: 'hook-rejection', error: blocked, guidance: true };
28
29
  }
29
30
  }
30
31
  }
@@ -1,5 +1,6 @@
1
1
  import type { HarnessHook, HookContext, HookResult } from '../types.ts';
2
2
  export declare class SandboxHook implements HarnessHook {
3
+ readonly name = "sandbox";
3
4
  readonly phase: 'preToolCall';
4
5
  run(ctx: HookContext): HookResult;
5
6
  }
@@ -26,6 +26,7 @@ const NETWORK_BINS = new Set([
26
26
  'openssl'
27
27
  ]);
28
28
  export class SandboxHook {
29
+ name = 'sandbox';
29
30
  phase = 'preToolCall';
30
31
  run(ctx) {
31
32
  if (ctx.phase !== 'preToolCall')
@@ -34,7 +35,11 @@ export class SandboxHook {
34
35
  return;
35
36
  const bin = bareBin(firstCommandToken(String(ctx.args.command ?? '')).replace(/^.*[/\\]/, ''));
36
37
  if (bin && NETWORK_BINS.has(bin)) {
37
- return { error: `sandbox mode: ${bin} reaches the network`, guidance: true };
38
+ return {
39
+ kind: 'hook-rejection',
40
+ error: `sandbox mode: ${bin} reaches the network`,
41
+ guidance: true
42
+ };
38
43
  }
39
44
  }
40
45
  }
@@ -2,6 +2,7 @@ import type { SkillCatalogEntry } from '../../tools/skills/catalog.ts';
2
2
  import type { HarnessHook, HookContext, HookResult } from '../types.ts';
3
3
  type SkillCatalogInput = readonly SkillCatalogEntry[] | (() => readonly SkillCatalogEntry[] | readonly string[]);
4
4
  export declare class SkillCatalogGuardHook implements HarnessHook {
5
+ readonly name = "skill-catalog";
5
6
  readonly phase: 'preToolCall';
6
7
  private readonly _catalog;
7
8
  constructor(catalog?: SkillCatalogInput);
@@ -1,4 +1,5 @@
1
1
  export class SkillCatalogGuardHook {
2
+ name = 'skill-catalog';
2
3
  phase = 'preToolCall';
3
4
  _catalog;
4
5
  constructor(catalog = []) {
@@ -18,6 +19,6 @@ export class SkillCatalogGuardHook {
18
19
  const rest = listed
19
20
  ? `The only skills are: ${listed}. Nothing else needs a skill load — answer or act with the tools you already have.`
20
21
  : 'No skill is available this turn — answer or act with the tools you already have.';
21
- return { error: `skill not found: ${name}. ${rest}`, guidance: true };
22
+ return { kind: 'hook-rejection', error: `skill not found: ${name}. ${rest}`, guidance: true };
22
23
  }
23
24
  }
@@ -1,5 +1,5 @@
1
1
  import type { ToolExecutionOutcome } from '../tool-call-display.ts';
2
- import type { HarnessHook, HookRejection, PostToolCallPayload, PreToolCallPayload, RunEndPayload, RunStartPayload } from './types.ts';
2
+ import type { HarnessHook, HookRejection, HookPhase, PostToolCallPayload, PreToolCallPayload, RunEndPayload, RunLifecyclePayload, RunStartPayload } from './types.ts';
3
3
  export type PreToolFold = {
4
4
  ok: true;
5
5
  args: Record<string, unknown>;
@@ -11,9 +11,12 @@ export interface PostToolFold {
11
11
  result: unknown;
12
12
  outcome?: ToolExecutionOutcome;
13
13
  }
14
+ export declare function snapshotHooks(hooks?: readonly HarnessHook[]): readonly HarnessHook[];
14
15
  export declare class HookPipeline {
15
16
  private readonly _behaviorHooks;
16
17
  constructor(hooks?: readonly HarnessHook[]);
18
+ hasPhase(phase: HookPhase): boolean;
19
+ onRunLifecycle(payload: RunLifecyclePayload): Promise<void>;
17
20
  onRunStart(payload: Omit<RunStartPayload, 'phase'>): Promise<void>;
18
21
  onRunEnd(payload: Omit<RunEndPayload, 'phase'>): Promise<void>;
19
22
  runPreToolFold(payload: Omit<PreToolCallPayload, 'phase'>): Promise<PreToolFold>;
@@ -2,10 +2,28 @@ import { hlog } from '../log.js';
2
2
  import { normalizeToolExecutionOutcome } from '../helpers/run-outcome.js';
3
3
  import { harnessHooks } from './registry.js';
4
4
  import { runHooks } from './run.js';
5
+ export function snapshotHooks(hooks = harnessHooks) {
6
+ const phaseCounts = new Map();
7
+ return Object.freeze(hooks.map((hook) => {
8
+ const count = (phaseCounts.get(hook.phase) ?? 0) + 1;
9
+ phaseCounts.set(hook.phase, count);
10
+ const name = hook.name?.trim() || `${hook.phase}:${count}`;
11
+ return Object.freeze({ phase: hook.phase, name, run: hook.run.bind(hook) });
12
+ }));
13
+ }
5
14
  export class HookPipeline {
6
15
  _behaviorHooks;
7
16
  constructor(hooks = harnessHooks) {
8
- this._behaviorHooks = hooks;
17
+ this._behaviorHooks = snapshotHooks(hooks);
18
+ }
19
+ hasPhase(phase) {
20
+ return this._behaviorHooks.some((hook) => hook.phase === phase);
21
+ }
22
+ async onRunLifecycle(payload) {
23
+ const folded = await runHooks(this._behaviorHooks, payload);
24
+ if ('guidance' in folded) {
25
+ hlog('run hook failed', { stage: payload.phase, error: folded.error });
26
+ }
9
27
  }
10
28
  async onRunStart(payload) {
11
29
  const folded = await runHooks(this._behaviorHooks, { phase: 'runStart', ...payload });
@@ -22,7 +40,11 @@ export class HookPipeline {
22
40
  async runPreToolFold(payload) {
23
41
  const folded = await runHooks(this._behaviorHooks, { phase: 'preToolCall', ...payload });
24
42
  if ('guidance' in folded) {
25
- hlog('tool rejected: pre-hook refused the call', { tool: payload.toolName });
43
+ hlog('tool rejected: pre-hook refused the call', {
44
+ tool: payload.toolName,
45
+ hook: folded.hook,
46
+ phase: folded.phase
47
+ });
26
48
  return { ok: false, rejection: folded };
27
49
  }
28
50
  return { ok: true, args: folded.args };
@@ -1,5 +1,6 @@
1
- import type { HarnessHook, HookRejection, PostToolCallPayload, PreToolCallPayload, RunEndPayload, RunStartPayload } from './types.ts';
1
+ import type { HarnessHook, HookRejection, PostToolCallPayload, PreToolCallPayload, RunEndPayload, RunLifecyclePayload, RunStartPayload } from './types.ts';
2
2
  export declare function runHooks(hooks: readonly HarnessHook[], ctx: PreToolCallPayload): Promise<PreToolCallPayload | HookRejection>;
3
3
  export declare function runHooks(hooks: readonly HarnessHook[], ctx: PostToolCallPayload): Promise<PostToolCallPayload | HookRejection>;
4
4
  export declare function runHooks(hooks: readonly HarnessHook[], ctx: RunStartPayload): Promise<RunStartPayload | HookRejection>;
5
5
  export declare function runHooks(hooks: readonly HarnessHook[], ctx: RunEndPayload): Promise<RunEndPayload | HookRejection>;
6
+ export declare function runHooks(hooks: readonly HarnessHook[], ctx: RunLifecyclePayload): Promise<RunLifecyclePayload | HookRejection>;
@@ -35,8 +35,8 @@ function freshContext(ctx) {
35
35
  function isRejection(value) {
36
36
  return 'error' in value;
37
37
  }
38
- function rejection(value) {
39
- return { error: String(value.error), guidance: true };
38
+ function rejection(value, hook, phase) {
39
+ return { kind: 'hook-rejection', hook, phase, error: String(value.error), guidance: true };
40
40
  }
41
41
  function merge(current, returned) {
42
42
  if (current.phase === 'preToolCall' && 'args' in returned) {
@@ -53,34 +53,46 @@ function merge(current, returned) {
53
53
  }
54
54
  return current;
55
55
  }
56
- function rejectionOf(current, err) {
56
+ function rejectionOf(current, err, hook) {
57
57
  const message = errorMessage(err);
58
- hlog('hook failed', { stage: current.phase, error: message });
58
+ hlog('hook failed', { stage: current.phase, hook, error: message });
59
59
  if (current.phase === 'postToolCall') {
60
60
  return {
61
+ kind: 'hook-rejection',
62
+ hook,
63
+ phase: current.phase,
61
64
  error: `tool executed; output withheld: post-hook failed: ${message}. Do not retry this call.`,
62
65
  guidance: true
63
66
  };
64
67
  }
65
- return { error: `hook failed in ${current.phase}: ${message}`, guidance: true };
68
+ return {
69
+ kind: 'hook-rejection',
70
+ hook,
71
+ phase: current.phase,
72
+ error: `hook failed in ${current.phase}: ${message}`,
73
+ guidance: true
74
+ };
66
75
  }
67
76
  export async function runHooks(hooks, ctx) {
68
77
  let current = ctx;
78
+ let phaseIndex = 0;
69
79
  for (const hook of hooks) {
70
80
  if (hook.phase !== current.phase)
71
81
  continue;
82
+ phaseIndex++;
83
+ const hookId = hook.name?.trim() || `${current.phase}:${phaseIndex}`;
72
84
  try {
73
85
  const returned = await hook.run(freshContext(current));
74
86
  if (!returned || typeof returned !== 'object') {
75
87
  continue;
76
88
  }
77
89
  if (isRejection(returned)) {
78
- return rejection(returned);
90
+ return rejection(returned, hookId, current.phase);
79
91
  }
80
92
  current = merge(current, returned);
81
93
  }
82
94
  catch (err) {
83
- return rejectionOf(current, err);
95
+ return rejectionOf(current, err, hookId);
84
96
  }
85
97
  }
86
98
  return current;