@inflexa-ai/harness 0.39.2 → 0.41.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 (184) hide show
  1. package/README.md +2 -3
  2. package/dist/agents/conversation-agent.d.ts +3 -0
  3. package/dist/agents/conversation-agent.js +13 -2
  4. package/dist/agents/report-session-agent.d.ts +3 -0
  5. package/dist/agents/report-session-agent.js +2 -0
  6. package/dist/agents/sandbox/cheminformatics-agent.js +0 -3
  7. package/dist/agents/sandbox/shared.d.ts +11 -2
  8. package/dist/agents/sandbox/shared.js +4 -0
  9. package/dist/app/chat-turn.d.ts +104 -12
  10. package/dist/app/chat-turn.js +180 -21
  11. package/dist/app/message-assembly.d.ts +16 -52
  12. package/dist/app/message-assembly.js +41 -68
  13. package/dist/app/run-activity.d.ts +4 -6
  14. package/dist/app/run-activity.js +7 -27
  15. package/dist/app/spawn-report-session.d.ts +6 -11
  16. package/dist/app/spawn-report-session.js +5 -6
  17. package/dist/app/synthesize-run.d.ts +3 -0
  18. package/dist/app/synthesize-run.js +1 -0
  19. package/dist/billing/usage-recorder.d.ts +3 -4
  20. package/dist/billing/usage-recorder.js +3 -4
  21. package/dist/contracts/chat-parts.d.ts +11 -1
  22. package/dist/contracts/chat-parts.js +1 -0
  23. package/dist/contracts/index.d.ts +1 -1
  24. package/dist/contracts/part-registry.js +1 -0
  25. package/dist/contracts/report-blocks.d.ts +8 -8
  26. package/dist/contracts/schemas/chat-parts.d.ts +32 -9
  27. package/dist/contracts/schemas/chat-parts.js +10 -0
  28. package/dist/execution/artifact-metadata.d.ts +26 -33
  29. package/dist/execution/artifact-metadata.js +44 -106
  30. package/dist/execution/post-step-pipeline.d.ts +23 -16
  31. package/dist/execution/post-step-pipeline.js +19 -21
  32. package/dist/execution/run-synthesis.d.ts +3 -0
  33. package/dist/execution/run-synthesis.js +11 -1
  34. package/dist/execution/step-summary.d.ts +21 -30
  35. package/dist/execution/step-summary.js +30 -57
  36. package/dist/index.d.ts +9 -8
  37. package/dist/index.js +15 -34
  38. package/dist/lib/zod-issues.js +2 -4
  39. package/dist/loop/compaction.d.ts +20 -0
  40. package/dist/loop/compaction.js +2 -0
  41. package/dist/loop/continue-agent.d.ts +40 -0
  42. package/dist/loop/continue-agent.js +38 -0
  43. package/dist/loop/metrics.d.ts +30 -22
  44. package/dist/loop/metrics.js +51 -22
  45. package/dist/loop/run-agent.d.ts +111 -13
  46. package/dist/loop/run-agent.js +467 -220
  47. package/dist/loop/run-to-terminal.d.ts +16 -17
  48. package/dist/loop/run-to-terminal.js +24 -35
  49. package/dist/loop/tool-mask.d.ts +23 -0
  50. package/dist/loop/tool-mask.js +46 -0
  51. package/dist/loop/tool-output.d.ts +48 -0
  52. package/dist/loop/tool-output.js +79 -0
  53. package/dist/loop/types.d.ts +6 -3
  54. package/dist/memory/ai-sdk-message-storage.d.ts +49 -2
  55. package/dist/memory/ai-sdk-message-storage.js +78 -2
  56. package/dist/memory/conversation-display-recorder.d.ts +6 -0
  57. package/dist/memory/conversation-display-recorder.js +64 -22
  58. package/dist/memory/conversation-display-replay.d.ts +5 -14
  59. package/dist/memory/conversation-display-replay.js +66 -32
  60. package/dist/memory/conversation-display-storage.d.ts +2 -1
  61. package/dist/memory/conversation-display-storage.js +2 -1
  62. package/dist/memory/conversation-view.d.ts +53 -0
  63. package/dist/memory/conversation-view.js +130 -0
  64. package/dist/memory/count-tokens.d.ts +5 -7
  65. package/dist/memory/count-tokens.js +7 -9
  66. package/dist/memory/thread-history.d.ts +74 -102
  67. package/dist/memory/thread-history.js +181 -246
  68. package/dist/memory/thread-store.d.ts +3 -3
  69. package/dist/memory/thread-store.js +7 -2
  70. package/dist/memory/tool-call-integrity.d.ts +15 -21
  71. package/dist/memory/tool-call-integrity.js +31 -38
  72. package/dist/memory/working-memory.d.ts +2 -5
  73. package/dist/memory/working-memory.js +2 -5
  74. package/dist/prompts/SOUL.d.ts +2 -2
  75. package/dist/prompts/SOUL.js +2 -7
  76. package/dist/prompts/analogical-reasoner.d.ts +1 -1
  77. package/dist/prompts/analogical-reasoner.js +16 -32
  78. package/dist/prompts/briefing.js +1 -1
  79. package/dist/prompts/compaction.d.ts +4 -0
  80. package/dist/prompts/compaction.js +24 -0
  81. package/dist/prompts/conversation.d.ts +1 -1
  82. package/dist/prompts/conversation.js +26 -35
  83. package/dist/prompts/execute-analysis/step-summary.js +7 -5
  84. package/dist/prompts/literature-reviewer.d.ts +1 -1
  85. package/dist/prompts/literature-reviewer.js +29 -34
  86. package/dist/prompts/planner.js +15 -29
  87. package/dist/prompts/report-session.d.ts +1 -1
  88. package/dist/prompts/report-session.js +2 -33
  89. package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
  90. package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +1 -2
  91. package/dist/prompts/sandbox/cheminformatics-agent.d.ts +1 -1
  92. package/dist/prompts/sandbox/cheminformatics-agent.js +10 -29
  93. package/dist/prompts/sandbox/chromatin-agent.d.ts +1 -1
  94. package/dist/prompts/sandbox/chromatin-agent.js +1 -2
  95. package/dist/prompts/sandbox/data-profiler.js +6 -3
  96. package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
  97. package/dist/prompts/sandbox/dna-methylation-agent.js +2 -7
  98. package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
  99. package/dist/prompts/sandbox/drug-repurposing-agent.js +20 -24
  100. package/dist/prompts/sandbox/enrichment-agent.d.ts +1 -1
  101. package/dist/prompts/sandbox/enrichment-agent.js +3 -2
  102. package/dist/prompts/sandbox/genomic-variant-agent.d.ts +1 -1
  103. package/dist/prompts/sandbox/genomic-variant-agent.js +0 -2
  104. package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
  105. package/dist/prompts/sandbox/immune-profiling-agent.js +7 -14
  106. package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
  107. package/dist/prompts/sandbox/metabolomics-agent.js +0 -2
  108. package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
  109. package/dist/prompts/sandbox/microbiome-agent.js +14 -21
  110. package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
  111. package/dist/prompts/sandbox/multi-omics-integration-agent.js +9 -19
  112. package/dist/prompts/sandbox/multimodal-sc-agent.d.ts +1 -1
  113. package/dist/prompts/sandbox/multimodal-sc-agent.js +4 -5
  114. package/dist/prompts/sandbox/network-agent.d.ts +1 -1
  115. package/dist/prompts/sandbox/network-agent.js +7 -8
  116. package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
  117. package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +19 -19
  118. package/dist/prompts/sandbox/proteomics-agent.d.ts +1 -1
  119. package/dist/prompts/sandbox/proteomics-agent.js +3 -4
  120. package/dist/prompts/sandbox/scientific-executor.d.ts +1 -1
  121. package/dist/prompts/sandbox/scientific-executor.js +7 -10
  122. package/dist/prompts/sandbox/single-cell-agent.d.ts +1 -1
  123. package/dist/prompts/sandbox/single-cell-agent.js +2 -4
  124. package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
  125. package/dist/prompts/sandbox/spatial-omics-agent.js +3 -5
  126. package/dist/prompts/sandbox/statistical-modeling-agent.d.ts +1 -1
  127. package/dist/prompts/sandbox/statistical-modeling-agent.js +1 -1
  128. package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
  129. package/dist/prompts/sandbox/translational-safety-agent.js +10 -19
  130. package/dist/prompts/sandbox-standards.d.ts +7 -7
  131. package/dist/prompts/sandbox-standards.js +74 -76
  132. package/dist/prompts/synthesis-agent.d.ts +1 -1
  133. package/dist/prompts/synthesis-agent.js +16 -42
  134. package/dist/providers/ai-sdk.d.ts +39 -6
  135. package/dist/providers/ai-sdk.js +117 -18
  136. package/dist/providers/errors.d.ts +2 -0
  137. package/dist/providers/errors.js +5 -0
  138. package/dist/providers/prompt-cache.d.ts +43 -41
  139. package/dist/providers/prompt-cache.js +47 -41
  140. package/dist/providers/reasoning.d.ts +10 -21
  141. package/dist/providers/reasoning.js +10 -21
  142. package/dist/providers/types.d.ts +21 -5
  143. package/dist/report-model/authoring-grammar.d.ts +2 -2
  144. package/dist/runtime/assemble.d.ts +9 -7
  145. package/dist/runtime/assemble.js +7 -3
  146. package/dist/sandbox/create-sandbox.d.ts +2 -2
  147. package/dist/sandbox/submit-exec.d.ts +1 -1
  148. package/dist/schemas/artifact-manifest.d.ts +4 -4
  149. package/dist/state/init.js +39 -4
  150. package/dist/state/purge-analysis.js +4 -0
  151. package/dist/state/tool-outputs.d.ts +4 -0
  152. package/dist/state/tool-outputs.js +30 -0
  153. package/dist/tasks/data-profile.d.ts +3 -0
  154. package/dist/tasks/data-profile.js +2 -0
  155. package/dist/tools/ad-hoc-router.d.ts +8 -0
  156. package/dist/tools/ad-hoc-router.js +21 -2
  157. package/dist/tools/display/show-user.d.ts +1 -1
  158. package/dist/tools/execute-analysis.d.ts +3 -0
  159. package/dist/tools/execute-analysis.js +9 -1
  160. package/dist/tools/read-tool-output.d.ts +7 -0
  161. package/dist/tools/read-tool-output.js +130 -0
  162. package/dist/tools/research/generate-analogy-report.d.ts +10 -26
  163. package/dist/tools/research/generate-analogy-report.js +76 -163
  164. package/dist/tools/research/generate-plan.d.ts +3 -0
  165. package/dist/tools/research/generate-plan.js +4 -1
  166. package/dist/tools/research/literature-reviewer.d.ts +3 -0
  167. package/dist/tools/research/literature-reviewer.js +3 -0
  168. package/dist/tools/sandbox/report-blocker.d.ts +18 -5
  169. package/dist/tools/sandbox/report-blocker.js +54 -6
  170. package/dist/tools/sandbox/submit-file-metadata.d.ts +30 -0
  171. package/dist/tools/sandbox/submit-file-metadata.js +67 -0
  172. package/dist/tools/start-report-session.d.ts +1 -3
  173. package/dist/tools/start-report-session.js +0 -2
  174. package/dist/tools/workspace/execute-command.js +7 -4
  175. package/dist/tools/workspace/result-bounds.d.ts +4 -4
  176. package/dist/tools/workspace/result-bounds.js +6 -5
  177. package/dist/tools/workspace/workspace-search.d.ts +1 -1
  178. package/dist/workflows/execute-analysis.d.ts +3 -0
  179. package/dist/workflows/execute-analysis.js +1 -0
  180. package/dist/workflows/sandbox-step.d.ts +30 -9
  181. package/dist/workflows/sandbox-step.js +37 -8
  182. package/package.json +9 -9
  183. package/dist/providers/anthropic.d.ts +0 -25
  184. package/dist/providers/anthropic.js +0 -28
@@ -4,17 +4,17 @@
4
4
  * Some agents communicate their result EXCLUSIVELY through a terminal tool
5
5
  * (`submit_plan`, `submit_profile`, `submit_synthesis`, …):
6
6
  * the loop's text reply is discarded and the outcome is read from a closure
7
- * cell the terminal tool writes. Plain `runAgent` ends a capped run on a
8
- * tool-LESS wrap-up turn, so an agent that burns its whole iteration budget —
9
- * or simply stops on prose — without ever submitting leaves that cell empty
10
- * and forces the caller to hard-fail.
7
+ * cell the terminal tool writes. The wrap-up of a capped run lets no tool run,
8
+ * so an agent that burns its whole iteration budget — or simply stops on
9
+ * prose — without ever submitting leaves that cell empty and forces the caller
10
+ * to hard-fail.
11
11
  *
12
12
  * `runToTerminal` runs the agent, then — if the outcome cell is still empty
13
- * and the run was not aborted — grants ONE focused salvage continuation whose
14
- * only tools are the terminal tools, opened by a corrective nudge. Restricting
15
- * the surface to the terminal tools removes every distraction; a small
16
- * `maxIterations` lets the model fix a single validation rejection and
17
- * resubmit.
13
+ * and the run was not aborted — runs ONE salvage continuation of the same
14
+ * conversation (`continueAgent`), opened by a corrective nudge. The salvage
15
+ * still declares every tool of the agent, masked to the terminal tools only,
16
+ * because the tool set is part of the prefix a signed thinking block and the
17
+ * prompt cache bind to.
18
18
  *
19
19
  * Salvage steps are namespaced (`salvage:…`) so a durable (DBOS) caller does
20
20
  * not collide the continuation's `llm-*` / `tool-*` cache keys with the first
@@ -50,18 +50,17 @@ export interface RunToTerminalResult extends RunAgentResult {
50
50
  }
51
51
  /** Describes how to salvage a run that never reached its terminal tool. */
52
52
  export interface TerminalSalvage {
53
- /** Terminal tools offered on the salvage turn (submit / blocker / …). Must be
54
- * the same instances the first run used — they close over the outcome cell. */
53
+ /** Their ids become the mask of the salvage continuation, so each must already be a declared tool of the agent. */
55
54
  readonly tools: readonly Tool[];
56
- /** Corrective user message that opens the salvage continuation. */
55
+ /** Opens the salvage continuation as a synthetic user message. */
57
56
  readonly nudge: string;
58
- /** Salvage iteration budget. Defaults to {@link DEFAULT_SALVAGE_ITERATIONS}. */
57
+ /** The cap of salvage requests. Defaults to {@link DEFAULT_SALVAGE_ITERATIONS}. */
59
58
  readonly maxIterations?: number;
60
59
  }
61
60
  /**
62
- * Drive `agent` to its terminal tool, salvaging once if it doesn't get there.
63
- * Returns the salvage run's result when a salvage occurred (its message array
64
- * already includes the first run's, and its token rollups are summed across both
65
- * attempts), otherwise the first run's result.
61
+ * Drive `agent` to its terminal tool, salvaging once if it does not get there.
62
+ * A salvage result carries both runs' messages and summed rollups, with the
63
+ * salvage's own finish. Throws first if a terminal tool is not declared on the
64
+ * agent.
66
65
  */
67
66
  export declare function runToTerminal(agent: AgentDefinition, initial: readonly LoopMessage[], session: AgentSession, opts: RunAgentOptions, salvage: TerminalSalvage): Promise<RunToTerminalResult>;
@@ -4,46 +4,45 @@
4
4
  * Some agents communicate their result EXCLUSIVELY through a terminal tool
5
5
  * (`submit_plan`, `submit_profile`, `submit_synthesis`, …):
6
6
  * the loop's text reply is discarded and the outcome is read from a closure
7
- * cell the terminal tool writes. Plain `runAgent` ends a capped run on a
8
- * tool-LESS wrap-up turn, so an agent that burns its whole iteration budget —
9
- * or simply stops on prose — without ever submitting leaves that cell empty
10
- * and forces the caller to hard-fail.
7
+ * cell the terminal tool writes. The wrap-up of a capped run lets no tool run,
8
+ * so an agent that burns its whole iteration budget — or simply stops on
9
+ * prose — without ever submitting leaves that cell empty and forces the caller
10
+ * to hard-fail.
11
11
  *
12
12
  * `runToTerminal` runs the agent, then — if the outcome cell is still empty
13
- * and the run was not aborted — grants ONE focused salvage continuation whose
14
- * only tools are the terminal tools, opened by a corrective nudge. Restricting
15
- * the surface to the terminal tools removes every distraction; a small
16
- * `maxIterations` lets the model fix a single validation rejection and
17
- * resubmit.
13
+ * and the run was not aborted — runs ONE salvage continuation of the same
14
+ * conversation (`continueAgent`), opened by a corrective nudge. The salvage
15
+ * still declares every tool of the agent, masked to the terminal tools only,
16
+ * because the tool set is part of the prefix a signed thinking block and the
17
+ * prompt cache bind to.
18
18
  *
19
19
  * Salvage steps are namespaced (`salvage:…`) so a durable (DBOS) caller does
20
20
  * not collide the continuation's `llm-*` / `tool-*` cache keys with the first
21
21
  * run's — replaying them would silently return the first run's cached results.
22
22
  */
23
23
  import { createNoopLogger } from "../lib/console-logger.js";
24
+ import { continueAgent } from "./continue-agent.js";
24
25
  import { addChatUsage } from "./metrics.js";
25
- import { DEFAULT_STEP_NAME_FORMATTER, runAgent } from "./run-agent.js";
26
+ import { runAgent } from "./run-agent.js";
26
27
  /** Default salvage budget: one submit plus a validation-fix retry or two. */
27
28
  export const DEFAULT_SALVAGE_ITERATIONS = 3;
28
- function salvageStepNames(base) {
29
- return {
30
- llm: (i) => `salvage:${base.llm(i)}`,
31
- tool: (name, id) => `salvage:${base.tool(name, id)}`,
32
- };
33
- }
34
29
  /**
35
- * Drive `agent` to its terminal tool, salvaging once if it doesn't get there.
36
- * Returns the salvage run's result when a salvage occurred (its message array
37
- * already includes the first run's, and its token rollups are summed across both
38
- * attempts), otherwise the first run's result.
30
+ * Drive `agent` to its terminal tool, salvaging once if it does not get there.
31
+ * A salvage result carries both runs' messages and summed rollups, with the
32
+ * salvage's own finish. Throws first if a terminal tool is not declared on the
33
+ * agent.
39
34
  */
40
35
  export async function runToTerminal(agent, initial, session, opts, salvage) {
36
+ const undeclared = salvage.tools.find((terminal) => !agent.tools.some((tool) => tool.id === terminal.id));
37
+ if (undeclared !== undefined) {
38
+ throw new Error(`The terminal tool "${undeclared.id}" is not a declared tool of the agent "${agent.id}", thus the salvage mask cannot let it run.`);
39
+ }
41
40
  const first = await runAgent(agent, initial, session, opts);
42
41
  if (opts.resolved?.() || opts.signal.aborted)
43
42
  return { ...first, salvage: null };
44
43
  const salvageBudget = salvage.maxIterations ?? DEFAULT_SALVAGE_ITERATIONS;
45
44
  // Reported here rather than in `runAgent` because the loop cannot know it is being
46
- // salvaged: it sees an ordinary run with a small budget and a restricted tool set.
45
+ // salvaged: it sees a continuation with a small cap and a mask.
47
46
  // Only this wrapper holds the fact that a first attempt ended without its outcome.
48
47
  // The first run's finish rides along because it is the whole diagnosis of WHY a
49
48
  // salvage was needed, and it is the field the second run's result overwrites.
@@ -55,26 +54,16 @@ export async function runToTerminal(agent, initial, session, opts, salvage) {
55
54
  salvageTools: salvage.tools.map((t) => t.id),
56
55
  salvageMaxIterations: salvageBudget,
57
56
  });
58
- const salvageAgent = {
59
- ...agent,
60
- tools: [...salvage.tools],
61
- maxIterations: salvageBudget,
62
- };
63
- const salvageOpts = {
64
- ...opts,
65
- formatStepName: salvageStepNames(opts.formatStepName ?? DEFAULT_STEP_NAME_FORMATTER),
66
- };
67
- const salvaged = await runAgent(salvageAgent, [...first.messages, { role: "user", content: salvage.nudge }], session, salvageOpts);
68
- // The continuation is the same logical run as the first attempt — its
69
- // message array already carries the first run's — so its rollups must too,
70
- // or the caller reads the salvage turn's tokens as the whole cost.
57
+ const salvaged = await continueAgent(agent, first.messages, { text: salvage.nudge, mask: { allow: salvage.tools.map((t) => t.id) }, maxRequests: salvageBudget, stepNamespace: "salvage" }, session, opts);
58
+ // The continuation is the same logical run as the first attempt, so its
59
+ // rollups must cover both, or the caller reads the salvage's tokens as the whole cost.
71
60
  //
72
61
  // `salvage` deliberately keeps each attempt's OWN finish, unsummed: it is the
73
62
  // per-attempt diagnosis, not a second accounting of the same tokens. The two
74
63
  // views are therefore NOT additive — `finish` is the run's total and already
75
64
  // covers everything `salvage.firstFinish` and `salvage.finish` report.
76
65
  return {
77
- ...salvaged,
66
+ messages: [...first.messages, ...salvaged.messages],
78
67
  finish: {
79
68
  ...salvaged.finish,
80
69
  ...sumUsage("usage", first.finish, salvaged.finish),
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A signed thinking block is bound to the exact prefix of its request, so a
3
+ * request always declares the full tool set
4
+ * of its agent; the mask and the budget act at dispatch instead. Provider-native
5
+ * masking is out of scope, because the vendors do it differently and an
6
+ * OpenAI-compatible endpoint has neither form.
7
+ */
8
+ import type { ToolCallPart } from "ai";
9
+ import type { Tool } from "../tools/define-tool.js";
10
+ /** An absent mask lets each declared tool run. */
11
+ export type ToolMask = "none" | {
12
+ readonly allow: readonly string[];
13
+ };
14
+ /** A tool with no entry has no limit. */
15
+ export type ToolBudget = Readonly<Record<string, number>>;
16
+ export declare function maskExcept(tools: readonly Tool[], ids: readonly string[]): ToolMask;
17
+ /**
18
+ * `undefined` for a call that passes, otherwise the model-visible refusal
19
+ * text. A passing call takes one unit from `used` regardless of its later
20
+ * result, because the check reads only the mask, the budget, and the run's
21
+ * calls — so a durable replay refuses the same calls again.
22
+ */
23
+ export declare function refusalsFor(calls: readonly ToolCallPart[], mask: ToolMask | undefined, budget: ToolBudget | undefined, used: Map<string, number>): (string | undefined)[];
@@ -0,0 +1,46 @@
1
+ /**
2
+ * A signed thinking block is bound to the exact prefix of its request, so a
3
+ * request always declares the full tool set
4
+ * of its agent; the mask and the budget act at dispatch instead. Provider-native
5
+ * masking is out of scope, because the vendors do it differently and an
6
+ * OpenAI-compatible endpoint has neither form.
7
+ */
8
+ export function maskExcept(tools, ids) {
9
+ return { allow: tools.map((t) => t.id).filter((id) => !ids.includes(id)) };
10
+ }
11
+ function maskLets(mask, toolName) {
12
+ if (mask === undefined)
13
+ return true;
14
+ if (mask === "none")
15
+ return false;
16
+ return mask.allow.includes(toolName);
17
+ }
18
+ /** Matches the `{ error, retryable }` shape of every other tool error. */
19
+ function refusal(error) {
20
+ return JSON.stringify({ error, retryable: false });
21
+ }
22
+ function maskRefusal(mask, toolName) {
23
+ if (mask === "none")
24
+ return refusal(`No tool can run for this request, thus ${toolName} did not run. Answer in text.`);
25
+ const available = mask.allow.length === 0 ? "none" : mask.allow.join(", ");
26
+ return refusal(`The tool ${toolName} is not available for this request, thus it did not run. The tools that can run now: ${available}.`);
27
+ }
28
+ /**
29
+ * `undefined` for a call that passes, otherwise the model-visible refusal
30
+ * text. A passing call takes one unit from `used` regardless of its later
31
+ * result, because the check reads only the mask, the budget, and the run's
32
+ * calls — so a durable replay refuses the same calls again.
33
+ */
34
+ export function refusalsFor(calls, mask, budget, used) {
35
+ return calls.map((call) => {
36
+ if (mask !== undefined && !maskLets(mask, call.toolName))
37
+ return maskRefusal(mask, call.toolName);
38
+ const limit = budget?.[call.toolName];
39
+ const count = used.get(call.toolName) ?? 0;
40
+ if (limit !== undefined && count >= limit) {
41
+ return refusal(`The tool ${call.toolName} reached its limit of ${limit} calls in this run, thus this call did not run.`);
42
+ }
43
+ used.set(call.toolName, count + 1);
44
+ return undefined;
45
+ });
46
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The cut of a long tool result: the excerpt that the model reads, and the store that keeps the whole text.
3
+ * Each length and each offset counts UTF-16 code units, the unit of a JavaScript string length.
4
+ */
5
+ import type { ToolResultPart } from "ai";
6
+ import type { ResultAsync } from "neverthrow";
7
+ import type { DomainError } from "../lib/result.js";
8
+ /** Keeps each skill file whole, because the largest one gives about 29,300 characters. */
9
+ export declare const TOOL_RESULT_CAP: number;
10
+ /** The start of an excerpt holds the shape of a result: its status, its first keys, or a header row. */
11
+ export declare const EXCERPT_HEAD_CHARS: number;
12
+ /** The end of an excerpt holds the errors and the stream flags of a command, thus it gets two times the start. */
13
+ export declare const EXCERPT_TAIL_CHARS: number;
14
+ /** The bound of a file that `grep` reads, thus a pattern search over a kept text costs no more than a `grep`. */
15
+ export declare const TOOL_OUTPUT_KEEP_MAX: number;
16
+ export declare const READ_TOOL_OUTPUT_TOOL_ID = "read_tool_output";
17
+ export interface KeptToolOutput {
18
+ readonly analysisId: string;
19
+ readonly ref: string;
20
+ readonly toolName: string;
21
+ readonly toolCallId: string;
22
+ /** Absent for a loop of a run, because a run belongs to its analysis and not to a thread. */
23
+ readonly threadId?: string;
24
+ /** The whole text, or its two halves with a marker line between them. */
25
+ readonly content: string;
26
+ /** The length of the whole text of the result. */
27
+ readonly totalLength: number;
28
+ }
29
+ /** Keeps the text of each cut tool result for `read_tool_output`. */
30
+ export interface ToolOutputStore {
31
+ /** A second put of the same analysis id and reference replaces the text. */
32
+ put(output: KeptToolOutput): ResultAsync<void, DomainError>;
33
+ get(analysisId: string, ref: string): ResultAsync<KeptToolOutput | null, DomainError>;
34
+ }
35
+ /** The reference of one kept text. It is short, because the model copies it. */
36
+ export declare function toolOutputRef(key: string): string;
37
+ /** The text that the model reads for a result, or `undefined` for a denial, which is never cut. */
38
+ export declare function resultTextOf(output: ToolResultPart["output"]): string | undefined;
39
+ /** Move `index` by one unit toward `direction` when it falls between the two halves of a surrogate pair. */
40
+ export declare function pairSafeIndex(text: string, index: number, direction: -1 | 1): number;
41
+ export declare function keptTextOf(text: string): string;
42
+ /** The excerpt of a text longer than `TOOL_RESULT_CAP`. `kept` is absent when the loop kept no text. */
43
+ export declare function excerptOf(text: string, kept: {
44
+ readonly ref: string;
45
+ readonly keptLength: number;
46
+ } | undefined): string;
47
+ /** The result with `excerpt` in place of its text. A part of a JSON text is not valid JSON, thus a JSON result becomes text. */
48
+ export declare function withExcerpt(part: ToolResultPart, excerpt: string): ToolResultPart;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The cut of a long tool result: the excerpt that the model reads, and the store that keeps the whole text.
3
+ * Each length and each offset counts UTF-16 code units, the unit of a JavaScript string length.
4
+ */
5
+ import { createHash } from "node:crypto";
6
+ /** Keeps each skill file whole, because the largest one gives about 29,300 characters. */
7
+ export const TOOL_RESULT_CAP = 32 * 1024;
8
+ /** The start of an excerpt holds the shape of a result: its status, its first keys, or a header row. */
9
+ export const EXCERPT_HEAD_CHARS = 4 * 1024;
10
+ /** The end of an excerpt holds the errors and the stream flags of a command, thus it gets two times the start. */
11
+ export const EXCERPT_TAIL_CHARS = 8 * 1024;
12
+ /** The bound of a file that `grep` reads, thus a pattern search over a kept text costs no more than a `grep`. */
13
+ export const TOOL_OUTPUT_KEEP_MAX = 1024 * 1024;
14
+ export const READ_TOOL_OUTPUT_TOOL_ID = "read_tool_output";
15
+ /** The reference of one kept text. It is short, because the model copies it. */
16
+ export function toolOutputRef(key) {
17
+ return `to_${createHash("sha256").update(key).digest("hex").slice(0, 20)}`;
18
+ }
19
+ /** The text that the model reads for a result, or `undefined` for a denial, which is never cut. */
20
+ export function resultTextOf(output) {
21
+ switch (output.type) {
22
+ case "json":
23
+ case "error-json":
24
+ return JSON.stringify(output.value);
25
+ case "text":
26
+ case "error-text":
27
+ return output.value;
28
+ case "content":
29
+ return output.value.map((part) => (part.type === "text" ? part.text : "")).join("");
30
+ case "execution-denied":
31
+ return undefined;
32
+ }
33
+ }
34
+ /** Move `index` by one unit toward `direction` when it falls between the two halves of a surrogate pair. */
35
+ export function pairSafeIndex(text, index, direction) {
36
+ if (index <= 0 || index >= text.length)
37
+ return index;
38
+ const before = text.charCodeAt(index - 1);
39
+ const after = text.charCodeAt(index);
40
+ const splitsPair = before >= 0xd800 && before <= 0xdbff && after >= 0xdc00 && after <= 0xdfff;
41
+ return splitsPair ? index + direction : index;
42
+ }
43
+ export function keptTextOf(text) {
44
+ if (text.length <= TOOL_OUTPUT_KEEP_MAX)
45
+ return text;
46
+ const headEnd = pairSafeIndex(text, TOOL_OUTPUT_KEEP_MAX / 2, -1);
47
+ const tailStart = pairSafeIndex(text, text.length - TOOL_OUTPUT_KEEP_MAX / 2, 1);
48
+ return `${text.slice(0, headEnd)}\n[... ${tailStart - headEnd} characters not kept ...]\n${text.slice(tailStart)}`;
49
+ }
50
+ /** The excerpt of a text longer than `TOOL_RESULT_CAP`. `kept` is absent when the loop kept no text. */
51
+ export function excerptOf(text, kept) {
52
+ const headEnd = pairSafeIndex(text, EXCERPT_HEAD_CHARS, -1);
53
+ const tailStart = pairSafeIndex(text, text.length - EXCERPT_TAIL_CHARS, 1);
54
+ const shown = `Shown: the first ${headEnd} and the last ${text.length - tailStart} characters.`;
55
+ const lines = [
56
+ `[Tool result cut: ${text.length} characters, over the limit of ${TOOL_RESULT_CAP}. ${shown}${kept === undefined ? " The rest is not kept." : ""}]`,
57
+ ];
58
+ if (kept !== undefined) {
59
+ lines.push(`[The harness kept ${kept.keptLength} characters as reference "${kept.ref}". Call ${READ_TOOL_OUTPUT_TOOL_ID} with this reference and an offset and a limit, or with a pattern. Offsets start at 0.]`);
60
+ }
61
+ lines.push(text.slice(0, headEnd), `[... ${tailStart - headEnd} characters not shown ...]`, text.slice(tailStart));
62
+ return lines.join("\n");
63
+ }
64
+ /** The result with `excerpt` in place of its text. A part of a JSON text is not valid JSON, thus a JSON result becomes text. */
65
+ export function withExcerpt(part, excerpt) {
66
+ const { output } = part;
67
+ switch (output.type) {
68
+ case "json":
69
+ case "text":
70
+ return { ...part, output: { type: "text", value: excerpt } };
71
+ case "error-json":
72
+ case "error-text":
73
+ return { ...part, output: { type: "error-text", value: excerpt } };
74
+ case "content":
75
+ return { ...part, output: { type: "content", value: [{ type: "text", text: excerpt }, ...output.value.filter((p) => p.type !== "text")] } };
76
+ case "execution-denied":
77
+ return part;
78
+ }
79
+ }
@@ -26,7 +26,7 @@ export interface AgentDefinition {
26
26
  readonly systemPrompt: string;
27
27
  readonly model: string;
28
28
  readonly tools: readonly Tool[];
29
- /** Runaway guard — at the cap the loop forces a wrap-up call that forbids tools. */
29
+ /** Runaway guard: at the cap the loop runs up to two masked wrap-up requests, until it gets a text answer. */
30
30
  readonly maxIterations: number;
31
31
  }
32
32
  /**
@@ -52,9 +52,12 @@ export interface EventSource {
52
52
  export type EmitEvent = {
53
53
  readonly type: "iteration";
54
54
  readonly source: EventSource;
55
- /** Zero-based iteration index; equals `maxIterations` for the wrap-up. */
55
+ /** Zero-based. Wrap-up request `k` has index `maxIterations + k`; a continuation counts from 0. */
56
56
  readonly index: number;
57
- /** True when this iteration produced the loop's terminal reply. */
57
+ /**
58
+ * True when this iteration produced the loop's terminal reply, including
59
+ * the last wrap-up even when its reply called a tool.
60
+ */
58
61
  readonly final: boolean;
59
62
  } | {
60
63
  readonly type: "tool-started";
@@ -35,6 +35,23 @@ export declare const SYNTHETIC_MESSAGE_KEY = "synthetic";
35
35
  export declare const SYNTHETIC_RECORD_KEY = "syntheticRecord";
36
36
  /** The {@link HARNESS_PROVIDER_NAMESPACE} key marking an assistant message whose production a client abort cut off. */
37
37
  export declare const INTERRUPTED_MESSAGE_KEY = "interrupted";
38
+ /** The {@link HARNESS_PROVIDER_NAMESPACE} key that holds the kind of a context record. */
39
+ export declare const CONTEXT_KIND_KEY = "contextKind";
40
+ /** The {@link HARNESS_PROVIDER_NAMESPACE} key that holds the SHA-256 hex hash of the text of a context record. */
41
+ export declare const CONTEXT_HASH_KEY = "contextHash";
42
+ declare const CONTEXT_KINDS: readonly ["analysis-context", "run-activity", "working-memory"];
43
+ /** The kinds of per-turn context that a chat turn stores after its user message. */
44
+ export type ContextKind = (typeof CONTEXT_KINDS)[number];
45
+ /**
46
+ * A context record: a synthetic `user` message, and no host record, thus it opens no turn and the
47
+ * display does not show it.
48
+ */
49
+ export declare function contextRecordMessage(kind: ContextKind, text: string): ModelMessage;
50
+ /** The kind and the hash of a context record, or `undefined` for a message that is no record. */
51
+ export declare function contextRecordOf(message: ModelMessage): {
52
+ readonly kind: ContextKind;
53
+ readonly hash: string;
54
+ } | undefined;
38
55
  /**
39
56
  * Return a copy of `message` carrying the interruption marker in the harness namespace.
40
57
  *
@@ -57,7 +74,7 @@ export declare function isInterruptedMessage(message: ModelMessage): boolean;
57
74
  * It has to carry the `user` role because the wire format requires a user turn after a truncated
58
75
  * assistant message, but it is not user input, and the difference is load-bearing: a `user` message is
59
76
  * what OPENS a conversation turn, so an unmarked synthetic one reads as a spurious turn boundary —
60
- * splitting one turn into two for the token window and, worse, giving a tail-turn removal a cut point in
77
+ * splitting one turn into two for the view and, worse, giving a tail-turn removal a cut point in
61
78
  * the middle of a turn. Marking it is what lets {@link isSyntheticUserMessage} keep those readers honest.
62
79
  *
63
80
  * `content` takes prose or the parts of a user message, because a picture rides a file part and no
@@ -74,7 +91,7 @@ export declare function syntheticUserMessage(content: UserContent): ModelMessage
74
91
  * outcome, which the model should read on its next turn but which nobody said.
75
92
  *
76
93
  * Synthetic in every sense that matters to a turn-boundary reader ({@link isSyntheticUserMessage} is
77
- * true of it), so it cannot split a turn for the token window nor hand a tail retraction a mid-turn
94
+ * true of it), so it cannot split a turn for the view nor hand a tail retraction a mid-turn
78
95
  * cut point. It differs from {@link syntheticUserMessage} in exactly one respect, and it is the
79
96
  * decisive one: this is a fact the reader is entitled to see, so the display reconstruction emits it
80
97
  * — as a `system` message, which is what it is — rather than dropping it as loop machinery.
@@ -89,6 +106,36 @@ export declare function syntheticRecordMessage(text: string): ModelMessage;
89
106
  export declare function isSyntheticUserMessage(message: ModelMessage): boolean;
90
107
  /** Whether this is a host-appended record of out-of-band work (see {@link syntheticRecordMessage}) — synthetic, but displayed. */
91
108
  export declare function isSyntheticRecordMessage(message: ModelMessage): boolean;
109
+ /** The {@link HARNESS_PROVIDER_NAMESPACE} key that holds the compaction id on each message of its exchange, which the view rule skips. */
110
+ export declare const COMPACTION_EXCHANGE_KEY = "compactionExchange";
111
+ /** The {@link HARNESS_PROVIDER_NAMESPACE} key that holds the figures of a compaction marker, which the view rule reads. */
112
+ export declare const COMPACTION_MARKER_KEY = "compactionMarker";
113
+ declare const CompactionMarkerSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
114
+ id: z.ZodString;
115
+ tokensBefore: z.ZodNumber;
116
+ tokensAfter: z.ZodNumber;
117
+ durationMs: z.ZodNumber;
118
+ kind: z.ZodLiteral<"summary">;
119
+ }, z.core.$strip>, z.ZodObject<{
120
+ keptTurns: z.ZodNumber;
121
+ id: z.ZodString;
122
+ tokensBefore: z.ZodNumber;
123
+ tokensAfter: z.ZodNumber;
124
+ durationMs: z.ZodNumber;
125
+ kind: z.ZodLiteral<"drop">;
126
+ }, z.core.$strip>], "kind">;
127
+ export type CompactionMarker = z.infer<typeof CompactionMarkerSchema>;
128
+ /** A copy of `message` whose harness namespace, which no provider reads, holds the compaction id. */
129
+ export declare function markCompactionExchange(message: ModelMessage, id: string): ModelMessage;
130
+ export declare function compactionExchangeOf(message: ModelMessage): string | undefined;
131
+ export declare function summaryMarkerMessage(summary: string, marker: Extract<CompactionMarker, {
132
+ kind: "summary";
133
+ }>): ModelMessage;
134
+ /** The marker of a compaction that gave no summary. No view holds it. */
135
+ export declare function dropMarkerMessage(marker: Extract<CompactionMarker, {
136
+ kind: "drop";
137
+ }>): ModelMessage;
138
+ export declare function compactionMarkerOf(message: ModelMessage): CompactionMarker | undefined;
92
139
  type LegacyContent = string | Array<Record<string, unknown>>;
93
140
  export interface LegacyMessageRow {
94
141
  readonly threadId: string;
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { modelMessageSchema } from "ai";
2
3
  import { z } from "zod";
3
4
  export const SUPPORTED_AI_SDK_MAJOR = 7;
@@ -46,6 +47,35 @@ export const SYNTHETIC_MESSAGE_KEY = "synthetic";
46
47
  export const SYNTHETIC_RECORD_KEY = "syntheticRecord";
47
48
  /** The {@link HARNESS_PROVIDER_NAMESPACE} key marking an assistant message whose production a client abort cut off. */
48
49
  export const INTERRUPTED_MESSAGE_KEY = "interrupted";
50
+ /** The {@link HARNESS_PROVIDER_NAMESPACE} key that holds the kind of a context record. */
51
+ export const CONTEXT_KIND_KEY = "contextKind";
52
+ /** The {@link HARNESS_PROVIDER_NAMESPACE} key that holds the SHA-256 hex hash of the text of a context record. */
53
+ export const CONTEXT_HASH_KEY = "contextHash";
54
+ const CONTEXT_KINDS = ["analysis-context", "run-activity", "working-memory"];
55
+ /**
56
+ * A context record: a synthetic `user` message, and no host record, thus it opens no turn and the
57
+ * display does not show it.
58
+ */
59
+ export function contextRecordMessage(kind, text) {
60
+ const hash = createHash("sha256").update(text).digest("hex");
61
+ return {
62
+ role: "user",
63
+ content: text,
64
+ providerOptions: { [HARNESS_PROVIDER_NAMESPACE]: { [SYNTHETIC_MESSAGE_KEY]: true, [CONTEXT_KIND_KEY]: kind, [CONTEXT_HASH_KEY]: hash } },
65
+ };
66
+ }
67
+ function isContextKind(value) {
68
+ return CONTEXT_KINDS.some((kind) => kind === value);
69
+ }
70
+ /** The kind and the hash of a context record, or `undefined` for a message that is no record. */
71
+ export function contextRecordOf(message) {
72
+ const harness = message.providerOptions?.[HARNESS_PROVIDER_NAMESPACE];
73
+ const kind = harness?.[CONTEXT_KIND_KEY];
74
+ const hash = harness?.[CONTEXT_HASH_KEY];
75
+ if (typeof hash !== "string" || !isContextKind(kind))
76
+ return undefined;
77
+ return { kind, hash };
78
+ }
49
79
  /**
50
80
  * Return a copy of `message` carrying the interruption marker in the harness namespace.
51
81
  *
@@ -80,7 +110,7 @@ export function isInterruptedMessage(message) {
80
110
  * It has to carry the `user` role because the wire format requires a user turn after a truncated
81
111
  * assistant message, but it is not user input, and the difference is load-bearing: a `user` message is
82
112
  * what OPENS a conversation turn, so an unmarked synthetic one reads as a spurious turn boundary —
83
- * splitting one turn into two for the token window and, worse, giving a tail-turn removal a cut point in
113
+ * splitting one turn into two for the view and, worse, giving a tail-turn removal a cut point in
84
114
  * the middle of a turn. Marking it is what lets {@link isSyntheticUserMessage} keep those readers honest.
85
115
  *
86
116
  * `content` takes prose or the parts of a user message, because a picture rides a file part and no
@@ -103,7 +133,7 @@ export function syntheticUserMessage(content) {
103
133
  * outcome, which the model should read on its next turn but which nobody said.
104
134
  *
105
135
  * Synthetic in every sense that matters to a turn-boundary reader ({@link isSyntheticUserMessage} is
106
- * true of it), so it cannot split a turn for the token window nor hand a tail retraction a mid-turn
136
+ * true of it), so it cannot split a turn for the view nor hand a tail retraction a mid-turn
107
137
  * cut point. It differs from {@link syntheticUserMessage} in exactly one respect, and it is the
108
138
  * decisive one: this is a fact the reader is entitled to see, so the display reconstruction emits it
109
139
  * — as a `system` message, which is what it is — rather than dropping it as loop machinery.
@@ -128,6 +158,52 @@ export function isSyntheticUserMessage(message) {
128
158
  export function isSyntheticRecordMessage(message) {
129
159
  return message.providerOptions?.[HARNESS_PROVIDER_NAMESPACE]?.[SYNTHETIC_RECORD_KEY] === true;
130
160
  }
161
+ /** The {@link HARNESS_PROVIDER_NAMESPACE} key that holds the compaction id on each message of its exchange, which the view rule skips. */
162
+ export const COMPACTION_EXCHANGE_KEY = "compactionExchange";
163
+ /** The {@link HARNESS_PROVIDER_NAMESPACE} key that holds the figures of a compaction marker, which the view rule reads. */
164
+ export const COMPACTION_MARKER_KEY = "compactionMarker";
165
+ const compactionFigures = { id: z.string(), tokensBefore: z.number(), tokensAfter: z.number(), durationMs: z.number() };
166
+ const CompactionMarkerSchema = z.discriminatedUnion("kind", [
167
+ z.object({ kind: z.literal("summary"), ...compactionFigures }),
168
+ z.object({ kind: z.literal("drop"), ...compactionFigures, keptTurns: z.number() }),
169
+ ]);
170
+ /** A copy of `message` whose harness namespace, which no provider reads, holds the compaction id. */
171
+ export function markCompactionExchange(message, id) {
172
+ const existingOptions = message.providerOptions ?? {};
173
+ return {
174
+ ...message,
175
+ providerOptions: {
176
+ ...existingOptions,
177
+ [HARNESS_PROVIDER_NAMESPACE]: { ...existingOptions[HARNESS_PROVIDER_NAMESPACE], [COMPACTION_EXCHANGE_KEY]: id },
178
+ },
179
+ };
180
+ }
181
+ export function compactionExchangeOf(message) {
182
+ const id = message.providerOptions?.[HARNESS_PROVIDER_NAMESPACE]?.[COMPACTION_EXCHANGE_KEY];
183
+ return typeof id === "string" ? id : undefined;
184
+ }
185
+ // A marker is synthetic, thus it opens no turn. It is no host record, because its divider shows it.
186
+ function compactionMarkerMessage(text, marker) {
187
+ return {
188
+ role: "user",
189
+ content: text,
190
+ providerOptions: { [HARNESS_PROVIDER_NAMESPACE]: { [SYNTHETIC_MESSAGE_KEY]: true, [COMPACTION_MARKER_KEY]: marker } },
191
+ };
192
+ }
193
+ export function summaryMarkerMessage(summary, marker) {
194
+ return compactionMarkerMessage(`[Conversation Summary]\n${summary}`, marker);
195
+ }
196
+ /** The marker of a compaction that gave no summary. No view holds it. */
197
+ export function dropMarkerMessage(marker) {
198
+ return compactionMarkerMessage("[Compaction Failed]", marker);
199
+ }
200
+ export function compactionMarkerOf(message) {
201
+ const value = message.providerOptions?.[HARNESS_PROVIDER_NAMESPACE]?.[COMPACTION_MARKER_KEY];
202
+ if (value === undefined)
203
+ return undefined;
204
+ const parsed = CompactionMarkerSchema.safeParse(value);
205
+ return parsed.success ? parsed.data : undefined;
206
+ }
131
207
  function providerOptionsFromLegacy(block) {
132
208
  // A block may carry both a signed thinking signature and cache_control;
133
209
  // both are required for valid continuation, so both must survive.
@@ -1,7 +1,13 @@
1
+ import type { ModelMessage } from "ai";
1
2
  import type { EmitFn } from "../loop/types.js";
2
3
  import { type ConversationUIMessage } from "./conversation-display-storage.js";
3
4
  export interface ConversationDisplayRecorder {
4
5
  readonly emit: EmitFn;
6
+ /** The user message of the turn. */
7
+ takeOpening(): ConversationUIMessage[];
8
+ /** The assistant message of one round, when the recorder got parts after the last take, and the divider of a marker of the round. */
9
+ takeRound(messages: readonly ModelMessage[]): ConversationUIMessage[];
10
+ /** The whole turn in one projection. `runChatTurn` stores each round with `takeOpening` and `takeRound` instead. */
5
11
  finish(options?: {
6
12
  readonly fallbackText?: string;
7
13
  readonly interrupted?: boolean;