@inflexa-ai/harness 0.45.0 → 0.45.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/dist/agents/conversation-agent.d.ts +10 -0
  2. package/dist/agents/conversation-agent.js +19 -1
  3. package/dist/agents/sandbox/shared.d.ts +23 -2
  4. package/dist/agents/sandbox/shared.js +51 -18
  5. package/dist/agents/sandbox/translational-safety-agent.js +26 -0
  6. package/dist/agents/sandbox/types.d.ts +24 -1
  7. package/dist/agents/sandbox-catalog.d.ts +14 -0
  8. package/dist/agents/sandbox-catalog.js +19 -0
  9. package/dist/app/approve-plan.d.ts +5 -2
  10. package/dist/app/approve-plan.js +1 -1
  11. package/dist/app/chat-turn.js +6 -3
  12. package/dist/config/environment-stores.d.ts +4 -2
  13. package/dist/contracts/chat-events.d.ts +12 -2
  14. package/dist/contracts/chat-events.js +1 -1
  15. package/dist/contracts/chat-frame.js +6 -2
  16. package/dist/contracts/index.d.ts +1 -1
  17. package/dist/contracts/schemas/chat-events.d.ts +25 -0
  18. package/dist/contracts/schemas/chat-events.js +7 -0
  19. package/dist/contracts/schemas/chat-parts.d.ts +39 -0
  20. package/dist/contracts/schemas/chat-parts.js +17 -0
  21. package/dist/contracts/schemas/index.d.ts +1 -1
  22. package/dist/contracts/schemas/index.js +1 -1
  23. package/dist/execution/dbos-workflow-purger.d.ts +2 -0
  24. package/dist/execution/dbos-workflow-purger.js +21 -14
  25. package/dist/execution/run-canceler.d.ts +33 -11
  26. package/dist/execution/run-canceler.js +129 -27
  27. package/dist/execution/run-end-record.d.ts +30 -0
  28. package/dist/execution/run-end-record.js +27 -0
  29. package/dist/index.d.ts +9 -5
  30. package/dist/index.js +11 -1
  31. package/dist/loop/call-guard.d.ts +50 -0
  32. package/dist/loop/call-guard.js +90 -0
  33. package/dist/loop/compaction.d.ts +5 -0
  34. package/dist/loop/compaction.js +17 -0
  35. package/dist/loop/run-agent.d.ts +9 -0
  36. package/dist/loop/run-agent.js +15 -1
  37. package/dist/loop/run-to-terminal.js +4 -1
  38. package/dist/loop/types.d.ts +6 -0
  39. package/dist/memory/conversation-display-recorder.d.ts +0 -2
  40. package/dist/memory/conversation-display-recorder.js +28 -3
  41. package/dist/memory/thread-history.d.ts +34 -7
  42. package/dist/memory/thread-history.js +151 -42
  43. package/dist/prompts/briefing.d.ts +57 -3
  44. package/dist/prompts/briefing.js +129 -2
  45. package/dist/prompts/planner.js +13 -2
  46. package/dist/provenance/seam.d.ts +23 -15
  47. package/dist/provenance/seam.js +7 -5
  48. package/dist/providers/ai-sdk.js +87 -7
  49. package/dist/providers/types.d.ts +6 -0
  50. package/dist/reference-data/catalog.js +141 -2
  51. package/dist/report-model/authoring-grammar.d.ts +26 -26
  52. package/dist/sandbox/image-packages.d.ts +26 -4
  53. package/dist/sandbox/image-packages.js +18 -4
  54. package/dist/schemas/data-profile-schemas.d.ts +12 -12
  55. package/dist/schemas/plan-schemas.d.ts +57 -3
  56. package/dist/schemas/plan-schemas.js +1 -1
  57. package/dist/schemas/workflow-state.d.ts +113 -0
  58. package/dist/schemas/workflow-state.js +54 -0
  59. package/dist/state/artifacts.d.ts +17 -1
  60. package/dist/state/artifacts.js +21 -3
  61. package/dist/state/index.d.ts +2 -2
  62. package/dist/state/index.js +1 -1
  63. package/dist/state/runs.d.ts +43 -9
  64. package/dist/state/runs.js +85 -14
  65. package/dist/state/step-executions.d.ts +9 -0
  66. package/dist/state/step-executions.js +22 -0
  67. package/dist/tools/ad-hoc-router.d.ts +2 -0
  68. package/dist/tools/ad-hoc-router.js +18 -2
  69. package/dist/tools/capability-data.d.ts +36 -0
  70. package/dist/tools/capability-data.js +73 -0
  71. package/dist/tools/execute-analysis.d.ts +37 -9
  72. package/dist/tools/execute-analysis.js +131 -21
  73. package/dist/tools/knowledge/check.d.ts +60 -0
  74. package/dist/tools/knowledge/check.js +92 -0
  75. package/dist/tools/knowledge/client.d.ts +382 -0
  76. package/dist/tools/knowledge/client.js +302 -0
  77. package/dist/tools/knowledge/environment.d.ts +78 -0
  78. package/dist/tools/knowledge/environment.js +105 -0
  79. package/dist/tools/knowledge/index.d.ts +49 -0
  80. package/dist/tools/knowledge/index.js +70 -0
  81. package/dist/tools/knowledge/local-slots.d.ts +62 -0
  82. package/dist/tools/knowledge/local-slots.js +177 -0
  83. package/dist/tools/knowledge/pin.d.ts +14 -0
  84. package/dist/tools/knowledge/pin.js +17 -0
  85. package/dist/tools/knowledge/recommend.d.ts +92 -0
  86. package/dist/tools/knowledge/recommend.js +98 -0
  87. package/dist/tools/knowledge/situation.d.ts +143 -0
  88. package/dist/tools/knowledge/situation.js +164 -0
  89. package/dist/tools/knowledge/skeleton.d.ts +69 -0
  90. package/dist/tools/knowledge/skeleton.js +202 -0
  91. package/dist/tools/knowledge/template.d.ts +137 -0
  92. package/dist/tools/knowledge/template.js +267 -0
  93. package/dist/tools/research/generate-plan.d.ts +23 -0
  94. package/dist/tools/research/generate-plan.js +203 -12
  95. package/dist/tools/research/guarded-address.d.ts +20 -0
  96. package/dist/tools/research/guarded-address.js +148 -0
  97. package/dist/tools/research/searxng-engine.d.ts +40 -0
  98. package/dist/tools/research/searxng-engine.js +120 -0
  99. package/dist/tools/research/web-fetch.d.ts +124 -0
  100. package/dist/tools/research/web-fetch.js +627 -0
  101. package/dist/tools/research/web-search.d.ts +75 -0
  102. package/dist/tools/research/web-search.js +112 -0
  103. package/dist/tools/sandbox/list-available-packages.d.ts +7 -4
  104. package/dist/tools/sandbox/list-available-packages.js +12 -4
  105. package/dist/tools/sandbox/list-available-refs.d.ts +21 -0
  106. package/dist/tools/sandbox/list-available-refs.js +60 -3
  107. package/dist/tools/sandbox/report-blocker.js +5 -1
  108. package/dist/tools/terminal-text.d.ts +11 -0
  109. package/dist/tools/terminal-text.js +19 -0
  110. package/dist/tools/workspace/decision-record.d.ts +81 -0
  111. package/dist/tools/workspace/decision-record.js +117 -0
  112. package/dist/tools/workspace/edit-file.d.ts +3 -1
  113. package/dist/tools/workspace/edit-file.js +20 -2
  114. package/dist/tools/workspace/execute-command.d.ts +17 -0
  115. package/dist/tools/workspace/execute-command.js +28 -1
  116. package/dist/tools/workspace/index.d.ts +1 -0
  117. package/dist/tools/workspace/index.js +1 -0
  118. package/dist/tools/workspace/mutator.d.ts +1 -1
  119. package/dist/workflows/execute-analysis.d.ts +58 -18
  120. package/dist/workflows/execute-analysis.js +273 -58
  121. package/dist/workflows/sandbox-step.d.ts +12 -0
  122. package/package.json +5 -1
@@ -24,14 +24,20 @@
24
24
  import { type WorkingMemory } from "../memory/working-memory.js";
25
25
  import type { DataProfileResult } from "../state/data-profile.js";
26
26
  import type { AnalysisStep } from "../schemas/workflow-state.js";
27
+ import type { TemplateParameter } from "../tools/knowledge/client.js";
28
+ import type { TemplateBindingValue } from "../tools/knowledge/template.js";
27
29
  /** Completed dependencies rendered as upstream blocks; the tail is counted, not rendered. */
28
30
  export declare const MAX_UPSTREAM_DEPS = 5;
29
31
  /** Characters of each dependency's summary carried in the seed — the gist, not the document. */
30
32
  export declare const UPSTREAM_SUMMARY_MAX_CHARS = 500;
31
33
  /** Artifact paths listed per dependency; the rest are reachable from its output directory. */
32
34
  export declare const MAX_UPSTREAM_ARTIFACTS = 8;
35
+ /** Adaptable slots rendered in the Template contract section; the tail is counted, not rendered. */
36
+ export declare const MAX_TEMPLATE_SLOTS = 24;
37
+ /** Characters of one slot description carried in the seed — the constraint, not the manual. */
38
+ export declare const TEMPLATE_SLOT_DESCRIPTION_MAX_CHARS = 160;
33
39
  /** Fields whose content is agent-facing and MUST appear in the rendered task. */
34
- export declare const STEP_TASK_FIELDS: readonly ["name", "question", "description", "context", "constraints", "acceptance_criteria", "caveats"];
40
+ export declare const STEP_TASK_FIELDS: readonly ["name", "question", "description", "context", "constraints", "acceptance_criteria", "caveats", "grounding"];
35
41
  /**
36
42
  * Fields deliberately excluded from the task section: identity/DAG structure
37
43
  * threaded separately through the workflow input, execution knobs, and result
@@ -41,7 +47,7 @@ export declare const STEP_TASK_FIELDS: readonly ["name", "question", "descriptio
41
47
  * {@link renderResources} section, because the budget is an execution bound,
42
48
  * not a task instruction.
43
49
  */
44
- export declare const STEP_NON_TASK_FIELDS: readonly ["id", "track", "step_type", "depends_on", "status", "resources", "agent", "timeout", "maxSteps", "summary", "artifactIds", "error", "packages"];
50
+ export declare const STEP_NON_TASK_FIELDS: readonly ["id", "track", "step_type", "depends_on", "status", "resources", "agent", "timeout", "maxSteps", "summary", "artifactIds", "error", "packages", "capability"];
45
51
  /**
46
52
  * The step's own instructions. Only `question` is guaranteed present; the
47
53
  * remaining task fields are appended as dedicated sections when populated, so a
@@ -50,7 +56,7 @@ export declare const STEP_NON_TASK_FIELDS: readonly ["id", "track", "step_type",
50
56
  * folded in here — otherwise the agent runs against the bare question and
51
57
  * improvises the rest.
52
58
  */
53
- export declare function renderTask(step: AnalysisStep): string;
59
+ export declare function renderTask(step: AnalysisStep, contract?: Pick<StepBriefing, "template" | "templateNotRetrieved">): string;
54
60
  /**
55
61
  * The heading of the analysis-memory section. The orient core of the sandbox
56
62
  * prompt names this same text, thus a test pins the two together.
@@ -75,6 +81,46 @@ export declare function emptyStepMemory(): StepMemory;
75
81
  * entry — it has no memory tool.
76
82
  */
77
83
  export declare function renderMemory(memory: StepMemory): string;
84
+ /** A file the script of the template reads. `path` names the slot that gives it, for example `{{counts_path}}`. */
85
+ export interface TemplateBriefInput {
86
+ readonly name: string;
87
+ readonly path: string;
88
+ readonly description?: string;
89
+ }
90
+ /** One plan setting bound to an adaptable slot of the template. */
91
+ export interface TemplateBoundSetting {
92
+ readonly name: string;
93
+ readonly value: TemplateBindingValue;
94
+ /** The source of the value (a doi or a document), or `plan` when the procedure names none. */
95
+ readonly source: string;
96
+ }
97
+ /**
98
+ * The contract of the template a step renders, projected for the seed. The
99
+ * parent fetches it at dispatch and binds the plan settings to it; the
100
+ * section is what lets an agent that has never seen the template make one
101
+ * valid render call.
102
+ */
103
+ export interface TemplateBrief {
104
+ /** The template reference of the plan step, with its version when the plan names one. */
105
+ readonly ref: string;
106
+ /** The version the service serves. Differs from the version of `ref` when the snapshot moved on. */
107
+ readonly version_served: string;
108
+ /** The adaptable slots of the contract, in the order the template declares them. A pinned slot is never rendered. */
109
+ readonly slots: readonly TemplateParameter[];
110
+ readonly inputs: readonly TemplateBriefInput[];
111
+ /** The plan settings bound to a slot, in plan order. Empty when the served version differs from the plan. */
112
+ readonly bound: readonly TemplateBoundSetting[];
113
+ /** The plan settings no bound slot carries, one line each with the reason. */
114
+ readonly unbound_settings: readonly string[];
115
+ }
116
+ /**
117
+ * The Template contract section. Only the adaptable slots render, bounded by
118
+ * {@link MAX_TEMPLATE_SLOTS} and the description clamp, so a wide template
119
+ * cannot blow the seed. A served version that differs from the plan renders
120
+ * as a caveat: the render refuses the plan version, and the settings are not
121
+ * bound to a version the plan did not name.
122
+ */
123
+ export declare function renderTemplateContract(brief: TemplateBrief | undefined): string;
78
124
  /**
79
125
  * What anchors every path the agent writes or reads: the id the tree is mounted
80
126
  * at, and the step's writable directory.
@@ -136,6 +182,14 @@ export interface StepBriefing {
136
182
  readonly upstream: readonly UpstreamHandoff[];
137
183
  /** The goal and the constraints of the analysis, read from the working memory at dispatch. */
138
184
  readonly memory: StepMemory;
185
+ /** The contract of the template the step renders, with the plan settings bound to it. Absent when none was retrieved. */
186
+ readonly template?: TemplateBrief;
187
+ /**
188
+ * Why the seed carries no template contract for a step whose grounding
189
+ * names one: no client is bound, the service did not answer, or the
190
+ * service does not hold the template. Read only when `template` is absent.
191
+ */
192
+ readonly templateNotRetrieved?: string;
139
193
  }
140
194
  /**
141
195
  * Compose the seed. Absent sections collapse out entirely — an independent step
@@ -30,6 +30,10 @@ export const MAX_UPSTREAM_DEPS = 5;
30
30
  export const UPSTREAM_SUMMARY_MAX_CHARS = 500;
31
31
  /** Artifact paths listed per dependency; the rest are reachable from its output directory. */
32
32
  export const MAX_UPSTREAM_ARTIFACTS = 8;
33
+ /** Adaptable slots rendered in the Template contract section; the tail is counted, not rendered. */
34
+ export const MAX_TEMPLATE_SLOTS = 24;
35
+ /** Characters of one slot description carried in the seed — the constraint, not the manual. */
36
+ export const TEMPLATE_SLOT_DESCRIPTION_MAX_CHARS = 160;
33
37
  // ── Field partition (the coverage guard's two halves) ─────────────────
34
38
  /** Fields whose content is agent-facing and MUST appear in the rendered task. */
35
39
  export const STEP_TASK_FIELDS = [
@@ -40,6 +44,9 @@ export const STEP_TASK_FIELDS = [
40
44
  "constraints",
41
45
  "acceptance_criteria",
42
46
  "caveats",
47
+ // The step agent reads the template id and the claim identifiers from the
48
+ // step, not from prose: the grounding renders beside the task fields.
49
+ "grounding",
43
50
  ];
44
51
  /**
45
52
  * Fields deliberately excluded from the task section: identity/DAG structure
@@ -70,6 +77,11 @@ export const STEP_NON_TASK_FIELDS = [
70
77
  // the agent does need to know about them — a dropped entry, or a step that
71
78
  // declares none — rides in `caveats`.
72
79
  "packages",
80
+ // Withheld for the same reason as `packages`, one layer up: the phrase is how the router and
81
+ // the planner name what they routed on, and the launch has already resolved the reference data
82
+ // it declares. The step agent learns the work from its skill pack, which names each dataset by
83
+ // what it is. Handing it a routing phrase would invite it to treat that phrase as the method.
84
+ "capability",
73
85
  ];
74
86
  // ── Formatting primitives ─────────────────────────────────────────────
75
87
  function section(heading, body) {
@@ -95,7 +107,7 @@ function clamp(text, max) {
95
107
  * folded in here — otherwise the agent runs against the bare question and
96
108
  * improvises the rest.
97
109
  */
98
- export function renderTask(step) {
110
+ export function renderTask(step, contract = {}) {
99
111
  const parts = [`# ${step.name}`, section("Task", step.question)];
100
112
  if (step.description?.trim()) {
101
113
  parts.push(section("What this step produces", step.description.trim()));
@@ -112,8 +124,46 @@ export function renderTask(step) {
112
124
  if (step.caveats && step.caveats.length > 0) {
113
125
  parts.push(section("Caveats", bullets(step.caveats)));
114
126
  }
127
+ if (step.grounding) {
128
+ parts.push(section("Grounding", renderGrounding(step.grounding, contract)));
129
+ }
115
130
  return parts.join("\n\n");
116
131
  }
132
+ /**
133
+ * The grounding as data lines. The template id is the one value the agent
134
+ * acts on (it names it to `knowledge_template`), thus it renders first after
135
+ * the status. The claim identifiers and the digest are for the record.
136
+ *
137
+ * A grounding that names a template promises a Template contract section.
138
+ * When the seed carries none, one line says so with the reason, thus the
139
+ * agent knows the contract was not fetched rather than guessing at slots.
140
+ *
141
+ * The settings of the procedure ride here as vetted values, not as
142
+ * constraints: the agent uses each one unless the data gives a reason for
143
+ * another value, and then it states the reason. They render only when the
144
+ * seed carries no template contract, because the bound and the unbound
145
+ * lists of the contract hold every setting already.
146
+ */
147
+ function renderGrounding(grounding, contract) {
148
+ const lines = [`- Status: ${grounding.status}`];
149
+ if (grounding.template) {
150
+ lines.push(`- Template: \`${grounding.template}\``);
151
+ if (!contract.template)
152
+ lines.push(`- Template contract: not retrieved (${contract.templateNotRetrieved ?? "no contract was fetched for this step"})`);
153
+ }
154
+ lines.push(`- Snapshot: ${grounding.snapshot}`);
155
+ lines.push(`- Claims: ${grounding.claims.length > 0 ? grounding.claims.join(", ") : "none"}`);
156
+ lines.push(`- Reason: ${grounding.reason}`);
157
+ const settings = grounding.settings ?? [];
158
+ if (settings.length > 0 && !contract.template) {
159
+ lines.push("- Settings (the vetted values of the procedure; another value is permitted when your summary states the reason):");
160
+ for (const setting of settings) {
161
+ const value = Array.isArray(setting.value) ? setting.value.join(", ") : String(setting.value);
162
+ lines.push(` - ${setting.step}: ${setting.name} = ${value}${setting.source ? ` (${setting.source})` : ""}`);
163
+ }
164
+ }
165
+ return lines.join("\n");
166
+ }
117
167
  // ── (1b) Analysis memory ──────────────────────────────────────────────
118
168
  /**
119
169
  * The heading of the analysis-memory section. The orient core of the sandbox
@@ -155,6 +205,82 @@ export function renderMemory(memory) {
155
205
  }
156
206
  return section(ANALYSIS_MEMORY_HEADING, parts.join("\n\n"));
157
207
  }
208
+ /** A value as the render request carries it: JSON, thus `"apeglm"` and `10` cannot be confused. */
209
+ function slotValue(value) {
210
+ return JSON.stringify(value);
211
+ }
212
+ /** The version of a template reference (`tpl-x@1.0.0` gives `1.0.0`), or `undefined` when the reference has none. */
213
+ function versionOfRef(ref) {
214
+ const at = ref.indexOf("@");
215
+ return at < 0 ? undefined : ref.slice(at + 1);
216
+ }
217
+ /**
218
+ * One adaptable slot as a bullet: the name, the type, the default with its
219
+ * source or the required state, the description, and the constraints the
220
+ * render enforces. A slot without a default is required unless the contract
221
+ * says otherwise, which is the rule of the renderer.
222
+ */
223
+ function renderSlot(slot) {
224
+ const facts = [slot.type];
225
+ // A local slot is bound on this machine: the agent sends it as any other slot, and the value never leaves.
226
+ if (slot.local)
227
+ facts.push("local");
228
+ if (slot.default !== undefined) {
229
+ facts.push(`default ${slotValue(slot.default)}${slot.default_source ? ` [${slot.default_source}]` : ""}`);
230
+ }
231
+ else {
232
+ facts.push(slot.required === false ? "optional" : "required");
233
+ }
234
+ if (slot.minimum !== undefined)
235
+ facts.push(`min ${slot.minimum}`);
236
+ if (slot.maximum !== undefined)
237
+ facts.push(`max ${slot.maximum}`);
238
+ const tail = [];
239
+ if (slot.enum && slot.enum.length > 0)
240
+ tail.push(`Permitted: ${slot.enum.map(slotValue).join(", ")}.`);
241
+ if (slot.pattern)
242
+ tail.push(`Pattern: \`${slot.pattern}\`.`);
243
+ const description = clamp(slot.description.trim(), TEMPLATE_SLOT_DESCRIPTION_MAX_CHARS);
244
+ return `\`${slot.name}\` (${facts.join("; ")})${description ? `: ${description}` : ""}${tail.length > 0 ? ` ${tail.join(" ")}` : ""}`;
245
+ }
246
+ /**
247
+ * The Template contract section. Only the adaptable slots render, bounded by
248
+ * {@link MAX_TEMPLATE_SLOTS} and the description clamp, so a wide template
249
+ * cannot blow the seed. A served version that differs from the plan renders
250
+ * as a caveat: the render refuses the plan version, and the settings are not
251
+ * bound to a version the plan did not name.
252
+ */
253
+ export function renderTemplateContract(brief) {
254
+ if (!brief)
255
+ return "";
256
+ const refVersion = versionOfRef(brief.ref);
257
+ const lead = [
258
+ `\`${brief.ref}\` (the service serves version ${brief.version_served}). Send only the slots listed here to \`knowledge_template\`; a slot not listed is pinned or unknown, and the render refuses it.`,
259
+ ];
260
+ if (refVersion !== undefined && refVersion !== brief.version_served) {
261
+ lead.push(`Caveat: the plan names version ${refVersion}, and the service serves version ${brief.version_served}. The plan settings are not bound to this render. Render the served version, and state each plan value yourself.`);
262
+ }
263
+ const blocks = [lead.join("\n")];
264
+ const adaptable = brief.slots.filter((slot) => slot.adaptable);
265
+ if (adaptable.length > 0) {
266
+ const shown = adaptable.slice(0, MAX_TEMPLATE_SLOTS);
267
+ const lines = [bullets(shown.map(renderSlot))];
268
+ const omitted = adaptable.length - shown.length;
269
+ if (omitted > 0)
270
+ lines.push(`(+${omitted} more adaptable ${omitted === 1 ? "slot" : "slots"} not listed. The render answer names a required one that is absent.)`);
271
+ blocks.push(`Slots:\n${lines.join("\n")}`);
272
+ }
273
+ if (brief.inputs.length > 0) {
274
+ blocks.push(`Inputs the script reads:\n${bullets(brief.inputs.map((input) => `${input.name}: \`${input.path}\`${input.description ? ` — ${input.description}` : ""}`))}`);
275
+ }
276
+ if (brief.bound.length > 0) {
277
+ blocks.push(`Bound by the plan (send each value as it is, or add an \`overrides\` entry with the slot and the reason for the change; a changed value without an override is refused):\n${bullets(brief.bound.map((setting) => `\`${setting.name}\` = ${slotValue(setting.value)} (${setting.source})`))}`);
278
+ }
279
+ if (brief.unbound_settings.length > 0) {
280
+ blocks.push(`Unbound settings (the plan states them, and no bound slot carries them):\n${bullets(brief.unbound_settings)}`);
281
+ }
282
+ return section("Template contract", blocks.join("\n\n"));
283
+ }
158
284
  /** The in-sandbox analysis root of a frame — the read-only mount of the whole tree. */
159
285
  export function analysisRootOf(frame) {
160
286
  return `/${frame.analysisId}`;
@@ -240,7 +366,8 @@ function renderHandoff(handoff) {
240
366
  */
241
367
  export function composeStepBriefing(briefing) {
242
368
  return [
243
- renderTask(briefing.step),
369
+ renderTask(briefing.step, briefing),
370
+ renderTemplateContract(briefing.template),
244
371
  renderMemory(briefing.memory),
245
372
  renderWorkspace(briefing.workspace),
246
373
  renderResources(briefing.step.resources),
@@ -120,7 +120,8 @@ leaves open:
120
120
  - An inventory block that reports a failed lookup. Absence in it proves
121
121
  nothing, so read that inventory through its tool.
122
122
  - A reference census that says it is cut short, where a step needs a
123
- collection it does not show.
123
+ collection it does not show. A bounded listing is not evidence of absence,
124
+ so confirm through the tool before you call that data missing.
124
125
  - A package whose exact spelling or language track the census leaves unclear.
125
126
 
126
127
  Batch the lookups. \`names\` takes every package you are unsure of in one call,
@@ -212,10 +213,18 @@ ${agentCatalog}
212
213
  ### Routing Rules
213
214
  - Route by primary data object: if the step works on a count matrix, assign
214
215
  the transcriptomics agent; if on a gene list, assign the enrichment agent.
216
+ - A gene list does not settle the assignment by itself. Read what the step
217
+ asks of the list. A step that builds, labels, or benchmarks a gene set for a
218
+ safety or tissue-risk question goes to the specialist whose capabilities name
219
+ that work, not to the enrichment agent.
215
220
  - When a step could go to either a modality or cross-cutting agent, prefer
216
221
  cross-cutting if the input is already in generic format (CSV/TSV). Prefer
217
222
  modality if it needs modality-specific objects (AnnData, SummarizedExperiment).
218
223
  - If no agent is an exact fit, pick the closest specialist from the list.
224
+ - Name the capability you routed on. Copy one phrase out of that agent's
225
+ capabilities into the step's \`capability\` field, spelled exactly as the
226
+ catalog spells it. The launch resolves what reference data that capability
227
+ needs before the run starts, so a paraphrase costs the step that check.
219
228
 
220
229
  ## Grounding
221
230
 
@@ -287,7 +296,9 @@ safety, toxicity, or treatment outcomes:
287
296
  5. **Route safety data correctly.** If the data includes safety
288
297
  biomarkers (ALT, AST, troponin, creatinine, hERG), adverse event
289
298
  records, or toxicity classifications, route to the
290
- translational-safety-agent.
299
+ translational-safety-agent. Route a request for matched negative
300
+ controls, or for a tissue-risk benchmark set, to the same agent even
301
+ when the only input is a gene list.
291
302
 
292
303
  6. **Route immune profiling correctly.** If the research question
293
304
  involves immune cell composition, tumor microenvironment
@@ -8,14 +8,16 @@
8
8
  *
9
9
  * One surface carries the three, and each member is optional alone. Thus a composition binds the members
10
10
  * that it records, and the harness reads an absent member as absence and never as an error. The run emit
11
- * takes the `RunSession` of the run, because a durable run carries one and a persisting realization needs
12
- * its credential. A session act carries no session, thus the session emit takes the event alone.
11
+ * takes an `AgentSession`, because a persisting realization needs the credential of the actor. The
12
+ * workflow body passes the `RunSession` of its durable input, and the run canceler passes the session of
13
+ * the cancel. A session act carries no session, thus the session emit takes the event alone.
13
14
  *
14
- * Both emit members are fire-and-forget. Neither gives a result, and the harness reads none. A site emits
15
- * after its act lands, thus a refused act and a failed act each emit nothing and the record states what
16
- * happened.
15
+ * Both emit members are fire-and-forget. Neither gives a result, and the harness reads none. The writer
16
+ * that wins the conditional terminal write of the run row emits `run_completed`. The body emits it before
17
+ * its status write commits, or after a refused or failed write. The canceler emits it only when its write
18
+ * changes the row, thus a refused cancel emits nothing.
17
19
  */
18
- import type { RunSession } from "../auth/types.js";
20
+ import type { AgentSession } from "../auth/types.js";
19
21
  import type { Logger } from "../lib/logger.js";
20
22
  import type { ProvenanceExport } from "../report-render/provenance-data.js";
21
23
  import type { AuthoringBlock } from "../report-model/authoring-grammar.js";
@@ -26,14 +28,19 @@ export type { ProvenanceExport } from "../report-render/provenance-data.js";
26
28
  * Harness-owned plain union — the harness stays tsprov-free and bus-free; the
27
29
  * host maps these execution facts onto its own ledger vocabulary.
28
30
  *
29
- * Every `atMs` is epoch milliseconds read via `await DBOS.now()`, a checkpointed
30
- * step: a body re-executed by DBOS recovery reads the recorded value, so a
31
+ * In the workflow body, every `atMs` is epoch milliseconds read via `await DBOS.now()`, a
32
+ * checkpointed step: a body re-executed by DBOS recovery reads the recorded value, so a
31
33
  * re-emitted event carries the identical timestamp and merges on the host's
32
- * ledger without a value conflict. Never source these from a wall clock
34
+ * ledger without a value conflict. The body never sources these from a wall clock
33
35
  * (`Date.now()`) — that would diverge across replays and defeat the merge.
34
36
  *
35
- * `run_completed.durationMs` is the terminal `atMs` minus the `run_started`
36
- * `atMs` (both `DBOS.now()` reads) — the true workflow-observed run span.
37
+ * The run canceler is the second emit site of `step_completed` and `run_completed`, and it emits only
38
+ * when its write of the run row changes the row. It is not a workflow, thus no replay fires its emits,
39
+ * and it reads the wall clock one time for all of them.
40
+ *
41
+ * In the body, `run_completed.durationMs` is the terminal `atMs` minus the `run_started`
42
+ * `atMs` (both `DBOS.now()` reads) — the true workflow-observed run span. The canceler holds no
43
+ * `run_started` time, thus it measures from the `started_at` of the run row.
37
44
  *
38
45
  * `step_completed` fires once at EVERY scheduler-loop settlement — the only site
39
46
  * that observes every executed step (registration sees only artifact-producing
@@ -192,11 +199,12 @@ export type ProvenanceSeam = {
192
199
  * The run engine emits the facts of one run here.
193
200
  *
194
201
  * Synchronous by signature (`void`, never a promise): a host that needs input and output dispatches
195
- * the work rather than making a workflow body wait behind it. The `RunSession` rides from the durable
196
- * workflow input, because a persisting realization needs the credential, the scope, and the identity
197
- * of the run. A realization can ignore the parameter.
202
+ * the work rather than making a workflow body wait behind it. The session rides beside the event,
203
+ * because a persisting realization needs the credential, the scope, and the identity of the actor.
204
+ * The body passes the `RunSession` of its durable workflow input. The run canceler passes the
205
+ * session of the cancel, because it holds no `RunSession`. A realization can ignore the parameter.
198
206
  */
199
- readonly emitRunEvent?: (event: RunProvenanceEvent, session: RunSession) => void;
207
+ readonly emitRunEvent?: (event: RunProvenanceEvent, session: AgentSession) => void;
200
208
  /**
201
209
  * A session emits one act here. A session act runs under no authorized run, thus this member takes
202
210
  * the event alone. It is synchronous by signature for the same reason as the run emit.
@@ -8,12 +8,14 @@
8
8
  *
9
9
  * One surface carries the three, and each member is optional alone. Thus a composition binds the members
10
10
  * that it records, and the harness reads an absent member as absence and never as an error. The run emit
11
- * takes the `RunSession` of the run, because a durable run carries one and a persisting realization needs
12
- * its credential. A session act carries no session, thus the session emit takes the event alone.
11
+ * takes an `AgentSession`, because a persisting realization needs the credential of the actor. The
12
+ * workflow body passes the `RunSession` of its durable input, and the run canceler passes the session of
13
+ * the cancel. A session act carries no session, thus the session emit takes the event alone.
13
14
  *
14
- * Both emit members are fire-and-forget. Neither gives a result, and the harness reads none. A site emits
15
- * after its act lands, thus a refused act and a failed act each emit nothing and the record states what
16
- * happened.
15
+ * Both emit members are fire-and-forget. Neither gives a result, and the harness reads none. The writer
16
+ * that wins the conditional terminal write of the run row emits `run_completed`. The body emits it before
17
+ * its status write commits, or after a refused or failed write. The canceler emits it only when its write
18
+ * changes the row, thus a refused cancel emits nothing.
17
19
  */
18
20
  /**
19
21
  * Bind the optional session emit and the logger of one site into a total emit.
@@ -97,6 +97,46 @@ function thinkingWithBinding(reasoning, mode) {
97
97
  return { blockBinding };
98
98
  return { type: "adaptive", display: "summarized", blockBinding };
99
99
  }
100
+ /**
101
+ * The model that answered after a server-side fallback hop, `""` when the hop is
102
+ * reported without one, and `undefined` when no hop happened.
103
+ *
104
+ * A model can refuse a request by category — a biology prompt is one such category —
105
+ * and the API then re-serves it on another model. It reports the hop TWICE: as a
106
+ * `fallback` content block inside the assistant turn, and on
107
+ * `providerMetadata.anthropic.iterations`. `@ai-sdk/anthropic` keeps only the second,
108
+ * because the AI SDK has no part for a model hop, and it drops the block from the
109
+ * content it returns (`anthropic-messages-language-model`, the `content_block_start`
110
+ * case). Thus the turn the harness holds is not the turn the API produced, and every
111
+ * signed thinking block of that turn is rejected from the next request on. This reader
112
+ * is what lets {@link settleFallbackReply} see the hop the content no longer shows.
113
+ */
114
+ function fallbackModelOf(metadata) {
115
+ const entries = metadata?.["anthropic"]?.["iterations"];
116
+ if (!Array.isArray(entries))
117
+ return undefined;
118
+ for (const entry of entries) {
119
+ if (typeof entry !== "object" || entry === null)
120
+ continue;
121
+ const fields = entry;
122
+ if (fields["type"] !== "fallback_message")
123
+ continue;
124
+ return typeof fields["model"] === "string" ? fields["model"] : "";
125
+ }
126
+ return undefined;
127
+ }
128
+ /**
129
+ * The assistant message with no thinking block, and with `""` for content when it held
130
+ * nothing else. An empty message reaches no wire: `sanitizeMessages` drops it.
131
+ */
132
+ function withoutThinking(message) {
133
+ if (typeof message.content === "string")
134
+ return message;
135
+ const content = message.content.filter((part) => part.type !== "reasoning" && part.type !== "reasoning-file");
136
+ if (content.length === message.content.length)
137
+ return message;
138
+ return content.length === 0 ? { ...message, content: "" } : { ...message, content };
139
+ }
100
140
  /** Reads `providerMetadata.anthropic.inputTransformations`; empty for another vendor. */
101
141
  function inputTransformationsOf(metadata) {
102
142
  const entries = metadata?.["anthropic"]?.["inputTransformations"];
@@ -476,6 +516,7 @@ export function createAiSdkProvider(deps) {
476
516
  // wire that carries no picture there.
477
517
  ...(deps.capabilities?.imageToolResults !== undefined ? { imageToolResults: deps.capabilities.imageToolResults } : {}),
478
518
  ...(deps.capabilities?.imageUserMessages !== undefined ? { imageUserMessages: deps.capabilities.imageUserMessages } : {}),
519
+ ...(deps.capabilities?.contextWindow !== undefined ? { contextWindow: deps.capabilities.contextWindow } : {}),
479
520
  };
480
521
  const logger = (deps.logger ?? createNoopLogger()).named("providers.ai-sdk");
481
522
  const maxOutputTokens = deps.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS;
@@ -524,6 +565,44 @@ export function createAiSdkProvider(deps) {
524
565
  logger.warn("thinking block dropped", { workload: workloadOf(session), type: drop.type, path: drop.path, reason: drop.reason });
525
566
  }
526
567
  }
568
+ /**
569
+ * The reply as the harness must hold it when a fallback model served it: with no
570
+ * thinking block, and with the model that answered named.
571
+ *
572
+ * A reply that carries a fallback hop ({@link fallbackModelOf}) reaches the harness
573
+ * already short of the `fallback` content block the API put in the turn. The API holds
574
+ * the whole turn against the signature of each of its thinking blocks, so echoing that
575
+ * turn earns `thinking ... blocks in the latest assistant message cannot be modified`,
576
+ * which is a 400 that no retry can pass. The conversation is then dead: the loop sends
577
+ * its history on every request, and nothing downstream can repair a turn.
578
+ *
579
+ * A thinking block is optional on the way back, thus the reply without them is a valid
580
+ * turn, and the next request continues. The cost is the reasoning of this one turn and
581
+ * the cache write that its new prefix pays for. A reply of nothing but thinking carries
582
+ * no part at all, and `sanitizeMessages` drops such a message from the next request.
583
+ *
584
+ * `servedModelId` needs the same correction. {@link captureServedModelId} reads the id
585
+ * the response metadata of the package carries, which is `message_start.message.model`,
586
+ * and that field keeps the REQUESTED model across a hop. Published as it stands, it
587
+ * makes the usage ledger of an embedder attribute the tokens to the model that refused
588
+ * the work. The iteration names the model that answered, thus it wins. An iteration that
589
+ * names no model changes nothing: there is no better id to publish, and the record above
590
+ * is then the only account of the hop.
591
+ */
592
+ function settleFallbackReply(session, response, metadata) {
593
+ const servedBy = fallbackModelOf(metadata);
594
+ if (servedBy === undefined)
595
+ return response;
596
+ logger.warn("reply served by a fallback model, thus the harness drops the thinking of that turn", {
597
+ workload: workloadOf(session),
598
+ ...(servedBy === "" ? {} : { servedBy }),
599
+ });
600
+ return {
601
+ ...response,
602
+ message: withoutThinking(response.message),
603
+ ...(servedBy === "" ? {} : { servedModelId: servedBy }),
604
+ };
605
+ }
527
606
  function chat(req, session, signal) {
528
607
  const reasoning = effortOf(req);
529
608
  const providerOptions = deps.providerOptionsFor?.({ session, reasoning });
@@ -602,14 +681,14 @@ export function createAiSdkProvider(deps) {
602
681
  // reports its drops.
603
682
  const { providerMetadata: metadata, ...reply } = collected;
604
683
  logDroppedThinkingBlocks(session, metadata);
605
- return ok(responseFromMessages({
684
+ return ok(settleFallbackReply(session, responseFromMessages({
606
685
  ...reply,
607
686
  requestedModelId,
608
687
  // Read after the drain: the metadata chunk that carries the
609
688
  // served id reaches the capture only as the stream is consumed.
610
689
  servedModelId: capture.servedModelId(),
611
690
  provider: providerId,
612
- }));
691
+ }), metadata));
613
692
  }
614
693
  catch (e) {
615
694
  if (isAbortError(e) || signal?.aborted || hookCall.unsettled)
@@ -692,7 +771,7 @@ export function createAiSdkProvider(deps) {
692
771
  });
693
772
  if (opened.kind === "completed") {
694
773
  logDroppedThinkingBlocks(session, opened.providerMetadata);
695
- const response = responseFromMessages({
774
+ const response = settleFallbackReply(session, responseFromMessages({
696
775
  messages: opened.messages,
697
776
  fallbackText: "",
698
777
  finishReason: opened.finishReason,
@@ -701,7 +780,7 @@ export function createAiSdkProvider(deps) {
701
780
  requestedModelId,
702
781
  servedModelId: capture.servedModelId(),
703
782
  provider: providerId,
704
- });
783
+ }), opened.providerMetadata);
705
784
  yield { type: "done", response };
706
785
  return;
707
786
  }
@@ -721,7 +800,8 @@ export function createAiSdkProvider(deps) {
721
800
  text += pull.text;
722
801
  yield { type: "text-delta", text: pull.text };
723
802
  }
724
- const response = responseFromMessages({
803
+ const metadata = (await result.finalStep).providerMetadata;
804
+ const response = settleFallbackReply(session, responseFromMessages({
725
805
  messages: await result.responseMessages,
726
806
  fallbackText: text,
727
807
  finishReason: await result.finishReason,
@@ -732,8 +812,8 @@ export function createAiSdkProvider(deps) {
732
812
  // reaches the capture only as the stream is consumed.
733
813
  servedModelId: capture.servedModelId(),
734
814
  provider: providerId,
735
- });
736
- logDroppedThinkingBlocks(session, (await result.finalStep).providerMetadata);
815
+ }), metadata);
816
+ logDroppedThinkingBlocks(session, metadata);
737
817
  yield { type: "done", response };
738
818
  }
739
819
  catch (e) {
@@ -31,6 +31,12 @@ export interface ProviderCapabilities {
31
31
  * "unknown".
32
32
  */
33
33
  readonly imageUserMessages?: boolean;
34
+ /**
35
+ * The context window of the model in tokens, when the host knows it. A chat
36
+ * thread then compacts at 80% of the window at the latest. Absent means "at
37
+ * least the compaction budget of the agent", thus the budget stays as declared.
38
+ */
39
+ readonly contextWindow?: number;
34
40
  }
35
41
  /**
36
42
  * The request carries no free-form provider options. That would let a caller