@inflexa-ai/harness 0.48.1 → 0.48.3

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 (120) hide show
  1. package/dist/app/chat-turn-workflow.d.ts +3 -1
  2. package/dist/app/chat-turn-workflow.js +18 -3
  3. package/dist/app/chat-turn.js +27 -1
  4. package/dist/app/in-process-turn.js +3 -6
  5. package/dist/contracts/chat-events.d.ts +45 -0
  6. package/dist/contracts/chat-frame.js +11 -0
  7. package/dist/contracts/chat-parts.d.ts +19 -1
  8. package/dist/contracts/chat-parts.js +1 -0
  9. package/dist/contracts/index.d.ts +2 -2
  10. package/dist/contracts/message.d.ts +7 -1
  11. package/dist/contracts/part-registry.js +1 -0
  12. package/dist/contracts/schemas/chat-events.d.ts +321 -0
  13. package/dist/contracts/schemas/chat-events.js +30 -0
  14. package/dist/contracts/schemas/chat-parts.d.ts +29 -1
  15. package/dist/contracts/schemas/chat-parts.js +10 -0
  16. package/dist/execution/run-synthesis.js +1 -0
  17. package/dist/index.d.ts +4 -2
  18. package/dist/index.js +1 -0
  19. package/dist/input-sanitization.d.ts +30 -2
  20. package/dist/input-sanitization.js +187 -2
  21. package/dist/loop/chat-stream.js +1 -1
  22. package/dist/loop/run-loop.js +66 -5
  23. package/dist/loop/tool-adapter.d.ts +8 -0
  24. package/dist/loop/tool-adapter.js +75 -4
  25. package/dist/loop/tool-detail.d.ts +5 -1
  26. package/dist/loop/tool-detail.js +7 -3
  27. package/dist/loop/tool-value.d.ts +57 -0
  28. package/dist/loop/tool-value.js +249 -0
  29. package/dist/loop/types.d.ts +9 -2
  30. package/dist/memory/conversation-display-storage.d.ts +3 -2
  31. package/dist/memory/conversation-display-storage.js +14 -2
  32. package/dist/memory/count-tokens.js +29 -2
  33. package/dist/providers/ai-sdk.d.ts +17 -14
  34. package/dist/providers/ai-sdk.js +14 -16
  35. package/dist/sandbox/sandbox-step-translate.d.ts +5 -9
  36. package/dist/sandbox/sandbox-step-translate.js +5 -9
  37. package/dist/tasks/data-profile-activity.d.ts +3 -7
  38. package/dist/tasks/data-profile-activity.js +1 -1
  39. package/dist/tasks/data-profile.js +3 -6
  40. package/dist/tools/ad-hoc-router.js +1 -0
  41. package/dist/tools/bio/alphafold-prediction.js +1 -0
  42. package/dist/tools/bio/chembl.js +1 -0
  43. package/dist/tools/bio/comptox.js +1 -0
  44. package/dist/tools/bio/drug-gene-interactions.js +1 -0
  45. package/dist/tools/bio/gene-disease-evidence.js +1 -0
  46. package/dist/tools/bio/gene-preclinical-profile.js +1 -0
  47. package/dist/tools/bio/lookup-annotation.js +1 -0
  48. package/dist/tools/bio/opentargets.js +1 -0
  49. package/dist/tools/bio/pubchem.js +1 -0
  50. package/dist/tools/bio/pubmed.js +1 -0
  51. package/dist/tools/bio/search-clinical-trials.js +1 -0
  52. package/dist/tools/bio/search-faers.js +1 -0
  53. package/dist/tools/bio/search-gene.js +1 -0
  54. package/dist/tools/bio/search-geo-datasets.js +1 -0
  55. package/dist/tools/bio/search-interactions.js +1 -0
  56. package/dist/tools/bio/search-patents.js +1 -0
  57. package/dist/tools/bio/search-protein.js +1 -0
  58. package/dist/tools/bio/search-regulatory-approvals.js +1 -0
  59. package/dist/tools/bio/target-safety.js +1 -0
  60. package/dist/tools/capability-data.js +5 -0
  61. package/dist/tools/define-tool.d.ts +18 -0
  62. package/dist/tools/define-tool.js +2 -0
  63. package/dist/tools/display/show-user.js +1 -0
  64. package/dist/tools/execute-analysis.js +4 -0
  65. package/dist/tools/knowledge/check.d.ts +47 -1
  66. package/dist/tools/knowledge/check.js +10 -1
  67. package/dist/tools/knowledge/client.d.ts +1 -1
  68. package/dist/tools/knowledge/recommend.js +4 -3
  69. package/dist/tools/knowledge/situation.d.ts +39 -1
  70. package/dist/tools/knowledge/situation.js +80 -8
  71. package/dist/tools/knowledge/skeleton.d.ts +7 -0
  72. package/dist/tools/knowledge/skeleton.js +88 -2
  73. package/dist/tools/knowledge/template.js +1 -0
  74. package/dist/tools/memory/update-working-memory.js +5 -0
  75. package/dist/tools/read-tool-output.js +4 -0
  76. package/dist/tools/report-authoring/authoring-tools.js +8 -0
  77. package/dist/tools/report-session/examine-page.js +5 -0
  78. package/dist/tools/report-session/list-artifacts.js +5 -0
  79. package/dist/tools/report-session/preview-report.js +1 -0
  80. package/dist/tools/report-session/record-version.js +1 -0
  81. package/dist/tools/report-session/script-tools.js +2 -0
  82. package/dist/tools/research/context7-docs.js +2 -0
  83. package/dist/tools/research/generate-analogy-report.js +3 -0
  84. package/dist/tools/research/generate-plan.js +6 -0
  85. package/dist/tools/research/inspect-data-profile.js +5 -0
  86. package/dist/tools/research/inspect-run.js +5 -0
  87. package/dist/tools/research/resolve-citation.js +1 -0
  88. package/dist/tools/research/search-arxiv.js +1 -0
  89. package/dist/tools/research/search-github-repos.js +1 -0
  90. package/dist/tools/research/search-semantic-scholar.js +1 -0
  91. package/dist/tools/research/web-fetch.js +1 -0
  92. package/dist/tools/research/web-search.js +1 -0
  93. package/dist/tools/sandbox/lease-tools.js +2 -0
  94. package/dist/tools/sandbox/link-packages.js +1 -0
  95. package/dist/tools/sandbox/list-available-packages.js +5 -0
  96. package/dist/tools/sandbox/list-available-refs.js +5 -0
  97. package/dist/tools/sandbox/report-blocker.js +1 -0
  98. package/dist/tools/sandbox/skills.js +2 -0
  99. package/dist/tools/sandbox/submit-file-metadata.js +1 -0
  100. package/dist/tools/script/file-tools.js +6 -0
  101. package/dist/tools/script/run-script.js +20 -1
  102. package/dist/tools/start-report-session.js +1 -0
  103. package/dist/tools/tool-view.d.ts +63 -0
  104. package/dist/tools/tool-view.js +68 -0
  105. package/dist/tools/workspace/edit-file.js +5 -0
  106. package/dist/tools/workspace/execute-command.js +5 -2
  107. package/dist/tools/workspace/file-stat.js +5 -0
  108. package/dist/tools/workspace/grep.js +4 -0
  109. package/dist/tools/workspace/list-files.js +5 -0
  110. package/dist/tools/workspace/read-file.js +4 -0
  111. package/dist/tools/workspace/scan-inputs.js +5 -0
  112. package/dist/tools/workspace/show-file.js +1 -0
  113. package/dist/tools/workspace/show-plan.js +1 -0
  114. package/dist/tools/workspace/workspace-search.js +5 -0
  115. package/dist/tools/workspace/write-file.js +7 -2
  116. package/dist/workflows/execute-analysis.js +2 -1
  117. package/dist/workflows/sandbox-step.js +3 -7
  118. package/package.json +1 -1
  119. package/dist/tools/detail-resolver.d.ts +0 -40
  120. package/dist/tools/detail-resolver.js +0 -44
@@ -0,0 +1,249 @@
1
+ /**
2
+ * The shown values of a tool call: its input, its result, and its error.
3
+ *
4
+ * The emit site makes each value one time, and a host only shows it. A tool declares what it shows through its view
5
+ * (`tools/tool-view.ts`), and {@link makeToolCallValue} turns the output of a view member into a value with a kind.
6
+ * Each leaf of a value gets the steps below on its own. The steps run in this order, because each step
7
+ * protects the next one: the control characters go first, thus a control byte inside a secret cannot stop a pattern.
8
+ * The secrets go before the cut, thus a cut cannot divide a secret and let one half pass. The shown text can differ
9
+ * from the text that the model reads.
10
+ */
11
+ import { err, ok } from "neverthrow";
12
+ import { z } from "zod";
13
+ import { TOOL_CALL_FIELDS_MAX, ToolCallValueSchema } from "../contracts/schemas/chat-events.js";
14
+ import { isUnknownArray } from "../lib/unknown-array.js";
15
+ import { parseJsonAs } from "../lib/parse-json.js";
16
+ import { describeZodIssueShapes } from "../lib/zod-issue-shape.js";
17
+ import { parseUnknownRecord } from "../lib/unknown-containers.js";
18
+ import { normalizeUnicode, redactSecretFields, redactSecretForms, redactSecrets } from "../input-sanitization.js";
19
+ import { ViewCode, ViewFields, ViewJson, ViewText } from "../tools/tool-view.js";
20
+ import { pairSafeIndex } from "./tool-output.js";
21
+ /** A text longer than this is cut. The unit is the UTF-16 code unit. */
22
+ export const TOOL_VALUE_CAP = 8 * 1024;
23
+ /** The start of a cut text holds the shape of a value. */
24
+ export const TOOL_VALUE_HEAD_CHARS = 3 * 1024;
25
+ /** The end of a cut text holds the errors of a command, thus it gets more room than the start. */
26
+ export const TOOL_VALUE_TAIL_CHARS = 5 * 1024;
27
+ /**
28
+ * The text past each shown part that the rules also read. A secret at the edge of a shown part is whole in its window
29
+ * when it is shorter than this margin, thus the rules see it as they see it in the whole text.
30
+ */
31
+ export const TOOL_VALUE_CLEAN_MARGIN = 8 * 1024;
32
+ const HEAD_WINDOW = TOOL_VALUE_HEAD_CHARS + TOOL_VALUE_CLEAN_MARGIN;
33
+ const TAIL_WINDOW = TOOL_VALUE_TAIL_CHARS + TOOL_VALUE_CLEAN_MARGIN;
34
+ /**
35
+ * A text up to this length gets the rules whole. A longer text gets them on its two windows only, thus the cost of a
36
+ * value does not grow with the size of a result.
37
+ */
38
+ const WHOLE_CLEAN_MAX = HEAD_WINDOW + TAIL_WINDOW;
39
+ function cleanText(text) {
40
+ return redactSecrets(redactSecretForms(normalizeUnicode(text)));
41
+ }
42
+ function cleanStrings(value) {
43
+ if (typeof value === "string")
44
+ return cleanText(value);
45
+ if (isUnknownArray(value))
46
+ return value.map(cleanStrings);
47
+ const record = parseUnknownRecord(value);
48
+ if (record === undefined)
49
+ return value;
50
+ const out = {};
51
+ for (const [name, field] of Object.entries(record))
52
+ out[cleanText(name)] = cleanStrings(field);
53
+ return out;
54
+ }
55
+ /** The shown start of `head` and the shown end of `tail`, with a marker that counts the code units of `length` not shown. */
56
+ function cutParts(head, tail, length) {
57
+ const shownHead = head.slice(0, pairSafeIndex(head, TOOL_VALUE_HEAD_CHARS, -1));
58
+ const shownTail = tail.slice(pairSafeIndex(tail, Math.max(0, tail.length - TOOL_VALUE_TAIL_CHARS), 1));
59
+ const marker = `[... ${Math.max(0, length - shownHead.length - shownTail.length)} characters not shown ...]`;
60
+ return { text: `${shownHead}\n${marker}\n${shownTail}`, cut: true };
61
+ }
62
+ function cutText(clean) {
63
+ return clean.length <= TOOL_VALUE_CAP ? { text: clean, cut: false } : cutParts(clean, clean, clean.length);
64
+ }
65
+ /** The shown text of a plain text. A long text gets the rules on its two windows, and its marker counts the original text. */
66
+ function shownText(text) {
67
+ if (text.length <= WHOLE_CLEAN_MAX)
68
+ return cutText(cleanText(text));
69
+ const head = cleanText(text.slice(0, pairSafeIndex(text, HEAD_WINDOW, -1)));
70
+ const tail = cleanText(text.slice(pairSafeIndex(text, text.length - TAIL_WINDOW, 1)));
71
+ return cutParts(head, tail, text.length);
72
+ }
73
+ /** The JSON text of a value, or `undefined` for a function, a symbol, and `undefined`, which the lib type omits. */
74
+ function jsonText(value) {
75
+ return JSON.stringify(value);
76
+ }
77
+ function serializeClean(value) {
78
+ return jsonText(cleanStrings(redactSecretFields(value)) ?? null) ?? "null";
79
+ }
80
+ /** The fields of a plain record whose JSON text is its own fields, or `undefined` for each other value. */
81
+ function plainFields(value) {
82
+ if (value === null || value === undefined)
83
+ return undefined;
84
+ const prototype = Object.getPrototypeOf(value);
85
+ if (prototype !== Object.prototype && prototype !== null)
86
+ return undefined;
87
+ const fields = Object.entries(value);
88
+ return fields.some(([name, field]) => name === "toJSON" && typeof field === "function") ? undefined : fields;
89
+ }
90
+ /**
91
+ * A copy of one end of `value`: the JSON text of the copy starts (`head`) or ends (`tail`) with at least `budget.left`
92
+ * code units of the JSON text of `value`. The count of a string ignores its escapes, and an escape only makes the text
93
+ * longer, thus the copy never holds less than the budget. A value that is not an array, a plain record, or a string
94
+ * stays whole, because its JSON text comes from its own `toJSON`.
95
+ */
96
+ function endOf(value, budget, end) {
97
+ if (typeof value === "string") {
98
+ const keep = Math.max(0, budget.left);
99
+ const kept = end === "head" ? value.slice(0, keep) : value.slice(Math.max(0, value.length - keep));
100
+ budget.left -= kept.length + 2;
101
+ return kept;
102
+ }
103
+ if (isUnknownArray(value)) {
104
+ budget.left -= 2;
105
+ const kept = [];
106
+ for (let index = 0; index < value.length && budget.left > 0; index += 1) {
107
+ kept.push(endOf(value[end === "head" ? index : value.length - 1 - index], budget, end));
108
+ budget.left -= 1;
109
+ }
110
+ return end === "head" ? kept : kept.reverse();
111
+ }
112
+ const fields = plainFields(value);
113
+ if (fields !== undefined) {
114
+ budget.left -= 2;
115
+ const kept = [];
116
+ for (let index = 0; index < fields.length && budget.left > 0; index += 1) {
117
+ const entry = fields[end === "head" ? index : fields.length - 1 - index];
118
+ // JSON text omits such a field, and a test of the field by its own serialization would read all of it.
119
+ if (entry === undefined || entry[1] === undefined || typeof entry[1] === "function" || typeof entry[1] === "symbol")
120
+ continue;
121
+ budget.left -= entry[0].length + 4;
122
+ kept.push([entry[0], endOf(entry[1], budget, end)]);
123
+ }
124
+ return Object.fromEntries(end === "head" ? kept : kept.reverse());
125
+ }
126
+ budget.left -= jsonText(value)?.length ?? 0;
127
+ return value;
128
+ }
129
+ /**
130
+ * The shown JSON text of a value: `ok(undefined)` when the value has no JSON text, and an error with the reason when
131
+ * the value does not serialize, for example a cycle.
132
+ */
133
+ function shownJson(value) {
134
+ try {
135
+ const raw = jsonText(value);
136
+ if (raw === undefined)
137
+ return ok(undefined);
138
+ if (raw.length <= WHOLE_CLEAN_MAX)
139
+ return ok(cutText(serializeClean(value)));
140
+ const head = serializeClean(endOf(value, { left: HEAD_WINDOW }, "head"));
141
+ const tail = serializeClean(endOf(value, { left: TAIL_WINDOW }, "tail"));
142
+ return ok(cutParts(head, tail, raw.length));
143
+ }
144
+ catch {
145
+ return err("the value does not serialize to JSON");
146
+ }
147
+ }
148
+ function jsonLeaf(value) {
149
+ return shownJson(value).map((shown) => (shown === undefined ? undefined : { kind: "json", ...shown }));
150
+ }
151
+ /** The leaf of a plain text: control characters out, each secret out, then the cut. */
152
+ export function textToolValue(text) {
153
+ return { kind: "text", ...shownText(text) };
154
+ }
155
+ /** The leaf of source code. The rules and the cut are those of {@link textToolValue}. `language` is checked with the whole value. */
156
+ export function codeToolValue(language, text) {
157
+ return { kind: "code", language, ...shownText(text) };
158
+ }
159
+ /**
160
+ * The leaf of a JSON value. A string shows as a JSON string, and each other value shows as its JSON text.
161
+ * The field-name rule runs first, then the text rules on each string, then the serialization, then the cut: a text
162
+ * rule never meets a JSON escape, which would hide the end of a quoted secret.
163
+ * A long JSON text gets the rules on a copy of each end of the value only, and its marker counts the JSON text of the
164
+ * original value. Only the serialization of the whole value grows with its size, and the model path pays it already.
165
+ * A value with no JSON text, and a value that does not serialize, for example a cycle, give no leaf: a value is a
166
+ * diagnostic and never fails a call.
167
+ */
168
+ export function jsonToolValue(value) {
169
+ return jsonLeaf(value).unwrapOr(undefined);
170
+ }
171
+ const AdapterErrorSchema = z.object({ error: z.string() });
172
+ /**
173
+ * The leaf of an error text. The adapter writes the error of a tool as the JSON text `{ "error": message, ... }`, and
174
+ * the leaf shows the message. Each other error text, for example a refusal of the call policy, shows whole.
175
+ */
176
+ export function errorToolValue(text) {
177
+ try {
178
+ return textToolValue(parseJsonAs(text, AdapterErrorSchema).error);
179
+ }
180
+ catch {
181
+ return textToolValue(text);
182
+ }
183
+ }
184
+ function leafValue(leaf) {
185
+ if (leaf instanceof ViewText)
186
+ return ok(textToolValue(leaf.text));
187
+ if (leaf instanceof ViewCode)
188
+ return ok(codeToolValue(leaf.language, leaf.text));
189
+ return jsonLeaf(leaf.value);
190
+ }
191
+ function fieldsValue(view) {
192
+ if (view.fields.length > TOOL_CALL_FIELDS_MAX)
193
+ return err(`a fields value holds more than ${TOOL_CALL_FIELDS_MAX} fields`);
194
+ const fields = [];
195
+ for (const { name, leaf } of view.fields) {
196
+ // A leaf that is not a leaf class is a nested `fields` value, which the kinds refuse.
197
+ if (!(leaf instanceof ViewText || leaf instanceof ViewCode || leaf instanceof ViewJson))
198
+ return err("a fields value holds a fields value");
199
+ const value = leafValue(leaf);
200
+ if (value.isErr())
201
+ return err(`the field ${name.slice(0, 40)}: ${value.error}`);
202
+ if (value.value !== undefined)
203
+ fields.push({ name, ...value.value });
204
+ }
205
+ return ok(fields.length === 0 ? undefined : { kind: "fields", fields });
206
+ }
207
+ function leafOfOutput(output) {
208
+ if (typeof output === "string")
209
+ return ok(textToolValue(output));
210
+ if (output instanceof ViewText || output instanceof ViewCode || output instanceof ViewJson)
211
+ return leafValue(output);
212
+ return jsonLeaf(output).andThen((leaf) => (leaf === undefined ? err("the value has no JSON text") : ok(leaf)));
213
+ }
214
+ /**
215
+ * The value of the output of a view member: a helper object gives its kind, a string gives a `text` leaf, and each other
216
+ * value gives a `json` leaf. Each leaf gets the cleaning and the cap of its kind. The result holds no value for an output
217
+ * that is absent (silent), and for a `fields` value with no field. It holds the reason for an output that cannot show: a
218
+ * value with no JSON text, a value that does not serialize, and a value that breaks a rule of the kinds (more fields than
219
+ * the limit, a nested `fields` value, a bad field name, a bad language). The caller logs the reason. It never throws.
220
+ */
221
+ export function makeToolCallValue(output) {
222
+ if (output === undefined)
223
+ return ok(undefined);
224
+ const made = output instanceof ViewFields ? fieldsValue(output) : leafOfOutput(output);
225
+ return made.andThen((value) => {
226
+ if (value === undefined)
227
+ return ok(undefined);
228
+ const checked = ToolCallValueSchema.safeParse(value);
229
+ return checked.success ? ok(checked.data) : err(describeZodIssueShapes(checked.error, ToolCallValueSchema).join("; "));
230
+ });
231
+ }
232
+ /**
233
+ * The event without the three values. A forwarder of loop events to the stream of a run calls it, because no reader
234
+ * of that stream shows a value, and a value costs bytes on each event.
235
+ */
236
+ export function withoutToolValues(event) {
237
+ switch (event.type) {
238
+ case "tool-started": {
239
+ const { input: _input, ...rest } = event;
240
+ return rest;
241
+ }
242
+ case "tool-finished": {
243
+ const { input: _input, result: _result, error: _error, ...rest } = event;
244
+ return rest;
245
+ }
246
+ default:
247
+ return event;
248
+ }
249
+ }
@@ -3,7 +3,7 @@
3
3
  * messages, and the flat event sink.
4
4
  */
5
5
  import type { ModelMessage } from "ai";
6
- import type { ToolCallDetail, ToolOutcome } from "../contracts/chat-events.js";
6
+ import type { ToolCallDetail, ToolCallValue, ToolOutcome } from "../contracts/chat-events.js";
7
7
  import type { ChatStreamEvent, ProviderOptions } from "../providers/types.js";
8
8
  import type { Tool } from "../tools/define-tool.js";
9
9
  import type { CompactionRules } from "./compaction.js";
@@ -75,13 +75,14 @@ export type EmitEvent = {
75
75
  readonly source: EventSource;
76
76
  readonly toolUseId: string;
77
77
  readonly name: string;
78
- readonly input: unknown;
79
78
  /**
80
79
  * One line naming what this call is doing, from the tool's own
81
80
  * `describeCall` hook. Absent — never empty — when the tool declares no
82
81
  * hook or the detail could not be produced (see `tool-detail.ts`).
83
82
  */
84
83
  readonly detail?: ToolCallDetail;
84
+ /** The input that the view of the tool declares, with each listed secret form removed and a cap. Absent for a call that is not a root call. */
85
+ readonly input?: ToolCallValue;
85
86
  } | {
86
87
  readonly type: "tool-finished";
87
88
  readonly source: EventSource;
@@ -101,6 +102,12 @@ export type EmitEvent = {
101
102
  * can fall back to its own observation.
102
103
  */
103
104
  readonly durationMs?: number;
105
+ /** The input again: a replay of a tool step does not give its `tool-started`. */
106
+ readonly input?: ToolCallValue;
107
+ /** The result of a call with the outcome `ok`. */
108
+ readonly result?: ToolCallValue;
109
+ /** The error text of a call with the outcome `error`. */
110
+ readonly error?: ToolCallValue;
104
111
  } | {
105
112
  /** The display recorder of a chat turn emits it, not the loop. See `MessageStartEvent`. */
106
113
  readonly type: "message-start";
@@ -15,7 +15,7 @@
15
15
  * reconstruction.
16
16
  */
17
17
  import { type DataUIPart, type UIMessage } from "ai";
18
- import type { AskPart, ChildSessionStartedPart, CompactionPart, FileReferencePart, PlanApprovalPart, PlanPart, PresentationPart, ReportRenderedPart, RunCardPart, SubAgentActivityEntry, SubAgentActivityPart } from "../contracts/chat-parts.js";
18
+ import type { AskPart, ChildSessionStartedPart, CompactionPart, FileReferencePart, ModelRefusal, ModelRefusalPart, PlanApprovalPart, PlanPart, PresentationPart, ReportRenderedPart, RunCardPart, SubAgentActivityEntry, SubAgentActivityPart } from "../contracts/chat-parts.js";
19
19
  import type { ToolCallOutcome } from "../contracts/chat-events.js";
20
20
  import type { ChatMessage, MessagePart, ToolCallPart } from "../contracts/message.js";
21
21
  import type { Logger } from "../lib/logger.js";
@@ -43,6 +43,7 @@ export type ConversationUIData = {
43
43
  "child-session-started": Payload<ChildSessionStartedPart>;
44
44
  "report-rendered": Payload<ReportRenderedPart>;
45
45
  compaction: Payload<CompactionPart>;
46
+ "model-refusal": ModelRefusal;
46
47
  "subagent-activity": Omit<Payload<SubAgentActivityPart>, "entries"> & {
47
48
  entries: (Omit<SubAgentActivityEntry, "outcome"> & {
48
49
  outcome: ToolCallOutcome;
@@ -65,7 +66,7 @@ export declare function parseStoredDisplayEnvelope(value: unknown, identity: str
65
66
  export type StoredSourcePart = Extract<Exclude<MessagePart, {
66
67
  type: "text";
67
68
  }>, {
68
- type: ToolCallPart["type"] | PresentationPart["type"] | PlanPart["type"] | RunCardPart["type"] | PlanApprovalPart["type"] | FileReferencePart["type"] | AskPart["type"] | ChildSessionStartedPart["type"] | ReportRenderedPart["type"] | CompactionPart["type"] | SubAgentActivityPart["type"];
69
+ type: ToolCallPart["type"] | PresentationPart["type"] | PlanPart["type"] | RunCardPart["type"] | PlanApprovalPart["type"] | FileReferencePart["type"] | AskPart["type"] | ChildSessionStartedPart["type"] | ReportRenderedPart["type"] | CompactionPart["type"] | ModelRefusalPart["type"] | SubAgentActivityPart["type"];
69
70
  }>;
70
71
  /** Whether `part` is an arm the stored vocabulary can hold; anything else cannot become a stored card. */
71
72
  export declare function isStoredSourcePart(part: Exclude<MessagePart, {
@@ -19,8 +19,8 @@ import { textOf } from "../lib/unknown-text.js";
19
19
  import { isDataUIPart, validateUIMessages } from "ai";
20
20
  import { z } from "zod";
21
21
  import { parseUnknownRecord } from "../lib/unknown-containers.js";
22
- import { AskPartSchema, ChildSessionStartedPartSchema, CompactionPartSchema, FileReferencePartSchema, PlanApprovalPartSchema, PlanPartSchema, PresentationPartSchema, ReportRenderedPartSchema, RunCardPartSchema, SubAgentActivityEntrySchema, SubAgentActivityPartSchema, } from "../contracts/schemas/chat-parts.js";
23
- import { ToolCallOutcomeSchema } from "../contracts/schemas/chat-events.js";
22
+ import { AskPartSchema, ChildSessionStartedPartSchema, CompactionPartSchema, FileReferencePartSchema, ModelRefusalPayloadSchema, PlanApprovalPartSchema, PlanPartSchema, PresentationPartSchema, ReportRenderedPartSchema, RunCardPartSchema, SubAgentActivityEntrySchema, SubAgentActivityPartSchema, } from "../contracts/schemas/chat-parts.js";
23
+ import { ToolCallOutcomeSchema, ToolCallValueSchema } from "../contracts/schemas/chat-events.js";
24
24
  export const SUPPORTED_DISPLAY_SCHEMA_VERSION = 1;
25
25
  const DisplayEnvelopeHeaderSchema = z.object({
26
26
  kind: z.literal("ai-sdk-ui-messages"),
@@ -48,6 +48,9 @@ const ToolCallDisplaySchema = z.object({
48
48
  outcome: ToolCallOutcomeSchema,
49
49
  detail: z.string().optional(),
50
50
  durationMs: z.number().optional(),
51
+ input: ToolCallValueSchema.optional(),
52
+ result: ToolCallValueSchema.optional(),
53
+ error: ToolCallValueSchema.optional(),
51
54
  });
52
55
  /**
53
56
  * The schema behind each stored part key.
@@ -74,6 +77,7 @@ const dataSchemas = {
74
77
  "child-session-started": ChildSessionStartedPartSchema.omit({ type: true }),
75
78
  "report-rendered": ReportRenderedPartSchema.omit({ type: true }),
76
79
  compaction: CompactionPartSchema.omit({ type: true }),
80
+ "model-refusal": ModelRefusalPayloadSchema,
77
81
  "subagent-activity": SubAgentActivityPartSchema.omit({ type: true }).extend({
78
82
  entries: z.array(SubAgentActivityEntrySchema.extend({ outcome: ToolCallOutcomeSchema })),
79
83
  }),
@@ -179,6 +183,7 @@ const STORED_SOURCE_TYPES = [
179
183
  "data-child-session-started",
180
184
  "data-report-rendered",
181
185
  "data-compaction",
186
+ "data-model-refusal",
182
187
  "data-subagent-activity",
183
188
  ];
184
189
  const STORED_SOURCE_TYPE_SET = new Set(STORED_SOURCE_TYPES);
@@ -252,6 +257,10 @@ export function conversationDisplayPart(part) {
252
257
  const { type: _type, ...data } = part;
253
258
  return { type: "data-compaction", id, data };
254
259
  }
260
+ case "data-model-refusal": {
261
+ const { type: _type, ...data } = part;
262
+ return { type: "data-model-refusal", id, data };
263
+ }
255
264
  case "data-subagent-activity": {
256
265
  const { type: _type, ...payload } = part;
257
266
  return { type: "data-subagent-activity", id, data: { ...payload, entries: part.entries.map(closedEntry) } };
@@ -312,6 +321,9 @@ export function conversationUIToChatMessages(messages) {
312
321
  case "data-compaction":
313
322
  parts.push({ type: "data-compaction", ...part.data });
314
323
  break;
324
+ case "data-model-refusal":
325
+ parts.push({ type: "data-model-refusal", ...part.data });
326
+ break;
315
327
  case "data-subagent-activity":
316
328
  parts.push({ type: "data-subagent-activity", ...part.data });
317
329
  break;
@@ -16,6 +16,33 @@ function enc() {
16
16
  encoder ??= getEncoding("cl100k_base");
17
17
  return encoder;
18
18
  }
19
+ /**
20
+ * A run of this many characters with no whitespace is counted in chunks. The BPE merge of `js-tiktoken` is
21
+ * quadratic in the length of one pre-token, and a run of one repeated character of 20,000 characters takes
22
+ * about 13 s whole. A base64 blob or a sequence with no line break is such a run in a tool result.
23
+ */
24
+ // oxlint-disable-next-line eslint-js/no-restricted-syntax -- a run of 256 or more characters with no whitespace; no builtin finds a run by its length.
25
+ const LONG_RUN = /\S{256,}/g;
26
+ /** The chunk of a long run. A chunk of 64 characters costs well under a millisecond in the worst case. */
27
+ const RUN_CHUNK = 64;
28
+ /**
29
+ * The token count of one text. A text with no long run is one encoding, thus its count is exact. Each long run
30
+ * is counted in chunks, which can add a token at each chunk edge: the count is an approximation already.
31
+ */
32
+ function encodedLength(text) {
33
+ let total = 0;
34
+ let from = 0;
35
+ for (const run of text.matchAll(LONG_RUN)) {
36
+ if (run.index > from)
37
+ total += enc().encode(text.slice(from, run.index)).length;
38
+ for (let at = 0; at < run[0].length; at += RUN_CHUNK)
39
+ total += enc().encode(run[0].slice(at, at + RUN_CHUNK)).length;
40
+ from = run.index + run[0].length;
41
+ }
42
+ if (from < text.length)
43
+ total += enc().encode(text.slice(from)).length;
44
+ return total;
45
+ }
19
46
  /**
20
47
  * The token-bearing text of one tool-result output.
21
48
  *
@@ -82,13 +109,13 @@ function tokenizableText(block) {
82
109
  */
83
110
  export function countTokens(content) {
84
111
  if (typeof content === "string") {
85
- return content.length === 0 ? 0 : enc().encode(content).length;
112
+ return content.length === 0 ? 0 : encodedLength(content);
86
113
  }
87
114
  let total = 0;
88
115
  for (const block of content) {
89
116
  const text = tokenizableText(block);
90
117
  if (text.length > 0)
91
- total += enc().encode(text).length;
118
+ total += encodedLength(text);
92
119
  }
93
120
  return total;
94
121
  }
@@ -118,6 +118,12 @@ export type AiSdkProviderConfig = {
118
118
  * Needs the `thinking-binding-controls-2026-08-01` beta header.
119
119
  */
120
120
  readonly thinkingBinding?: "drop_block" | "error" | "off";
121
+ /**
122
+ * `default`: when a safety classifier declines a request, the API sends it again to the model that it
123
+ * recommends for the category of the refusal, inside the same call. When the field is absent, a declined
124
+ * request ends with the content filter. A model with no fallback target keeps its refusal.
125
+ */
126
+ readonly fallbacks?: "default";
121
127
  } | {
122
128
  readonly kind: "openai";
123
129
  readonly baseURL?: string;
@@ -252,22 +258,19 @@ export declare function sessionKeyDigestOf(session: Pick<AgentSession, "scope" |
252
258
  * A model can refuse a request by category — a biology prompt is one such category —
253
259
  * and the API then re-serves it on another model. It reports the hop TWICE: as a
254
260
  * `fallback` content block inside the assistant turn, and on
255
- * `providerMetadata.anthropic.iterations`. `@ai-sdk/anthropic` keeps only the second,
256
- * because the AI SDK has no part for a model hop, and it drops the block from the
257
- * content it returns (`anthropic-messages-language-model`, the `content_block_start`
258
- * case). Thus the turn the harness holds is not the turn the API produced, and every
259
- * signed thinking block of that turn is rejected from the next request on.
261
+ * `providerMetadata.anthropic.iterations`. `@ai-sdk/anthropic` keeps the block as a
262
+ * custom part and sends it back on the next request, but it keeps each thinking block
263
+ * before it too. The API accepts no thinking block from before the hop, and a turn
264
+ * whose thinking blocks changed earns `thinking ... blocks in the latest assistant
265
+ * message cannot be modified`, which is a 400 that no retry can pass.
260
266
  *
261
- * The API holds the whole turn against the signature of each of its thinking blocks, so
262
- * echoing that turn earns `thinking ... blocks in the latest assistant message cannot be
263
- * modified`, which is a 400 that no retry can pass. A thinking block is optional on the
264
- * way back, thus the loop records the reply of a hop with no thinking block, and the next
265
- * request continues. The cost is the reasoning of this one turn and the cache write that
266
- * its new prefix pays for.
267
+ * A thinking block is optional on the way back, thus the loop records the reply of a
268
+ * hop with no thinking block, and the next request continues. The cost is the
269
+ * reasoning of this one turn and the cache write that its new prefix pays for.
267
270
  *
268
- * The response metadata of the package carries `message_start.message.model`, and that
269
- * field keeps the REQUESTED model across a hop. The iteration names the model that
270
- * answered, thus it wins for the usage record.
271
+ * The response metadata of the package carries `message_start.message.model`, and
272
+ * after a hop in the middle of a reply that field keeps the REQUESTED model. The
273
+ * iteration names the model that answered, thus it wins for the usage record.
271
274
  */
272
275
  export declare function fallbackModelOf(metadata: ProviderMetadata | undefined): string | undefined;
273
276
  /**
@@ -108,22 +108,19 @@ function thinkingWithBinding(reasoning, mode) {
108
108
  * A model can refuse a request by category — a biology prompt is one such category —
109
109
  * and the API then re-serves it on another model. It reports the hop TWICE: as a
110
110
  * `fallback` content block inside the assistant turn, and on
111
- * `providerMetadata.anthropic.iterations`. `@ai-sdk/anthropic` keeps only the second,
112
- * because the AI SDK has no part for a model hop, and it drops the block from the
113
- * content it returns (`anthropic-messages-language-model`, the `content_block_start`
114
- * case). Thus the turn the harness holds is not the turn the API produced, and every
115
- * signed thinking block of that turn is rejected from the next request on.
111
+ * `providerMetadata.anthropic.iterations`. `@ai-sdk/anthropic` keeps the block as a
112
+ * custom part and sends it back on the next request, but it keeps each thinking block
113
+ * before it too. The API accepts no thinking block from before the hop, and a turn
114
+ * whose thinking blocks changed earns `thinking ... blocks in the latest assistant
115
+ * message cannot be modified`, which is a 400 that no retry can pass.
116
116
  *
117
- * The API holds the whole turn against the signature of each of its thinking blocks, so
118
- * echoing that turn earns `thinking ... blocks in the latest assistant message cannot be
119
- * modified`, which is a 400 that no retry can pass. A thinking block is optional on the
120
- * way back, thus the loop records the reply of a hop with no thinking block, and the next
121
- * request continues. The cost is the reasoning of this one turn and the cache write that
122
- * its new prefix pays for.
117
+ * A thinking block is optional on the way back, thus the loop records the reply of a
118
+ * hop with no thinking block, and the next request continues. The cost is the
119
+ * reasoning of this one turn and the cache write that its new prefix pays for.
123
120
  *
124
- * The response metadata of the package carries `message_start.message.model`, and that
125
- * field keeps the REQUESTED model across a hop. The iteration names the model that
126
- * answered, thus it wins for the usage record.
121
+ * The response metadata of the package carries `message_start.message.model`, and
122
+ * after a hop in the middle of a reply that field keeps the REQUESTED model. The
123
+ * iteration names the model that answered, thus it wins for the usage record.
127
124
  */
128
125
  export function fallbackModelOf(metadata) {
129
126
  const entries = metadata?.["anthropic"]?.["iterations"];
@@ -717,12 +714,13 @@ export function createConfiguredAiSdkProvider(deps) {
717
714
  maxRetries: config.maxRetries,
718
715
  requestTimeoutMs,
719
716
  reasoning: config.reasoning,
720
- // The package sends the key as `metadata.user_id`, and the binding as
721
- // `thinking.block_binding`.
717
+ // The package sends the key as `metadata.user_id`, the binding as
718
+ // `thinking.block_binding`, and the default fallback with its beta header.
722
719
  providerOptionsFor: ({ session, reasoning }) => ({
723
720
  anthropic: {
724
721
  metadata: { userId: sessionKeyDigestOf(session) },
725
722
  thinking: binding === undefined ? undefined : thinkingWithBinding(reasoning, binding),
723
+ fallbacks: config.fallbacks,
726
724
  },
727
725
  }),
728
726
  });
@@ -7,7 +7,6 @@
7
7
  */
8
8
  import type { ActivityEntryPart, StepActivityPart, StepPhase, SynthesisPhase, SynthesisProgressPart } from "../contracts/chat-parts.js";
9
9
  import type { ChatDataPart, EmitFn } from "../loop/types.js";
10
- import type { ToolDetailResolver } from "../tools/detail-resolver.js";
11
10
  /**
12
11
  * Stable reconciliation id for a step's reconciling parts
13
12
  * (`data-step-activity`, `data-step-file-tree`). One id per (runId, stepId)
@@ -49,11 +48,9 @@ export declare function synthesisEmission(runId: string, stepId: string, seq: nu
49
48
  * The live-activity phrase for one tool call: the tool's name, then its own
50
49
  * description of this call when it has one.
51
50
  *
52
- * The description comes from the tool's `describeCall` hook, resolved through
53
- * `resolveDetail` — the same resolver the chat chip and the reloaded transcript
54
- * use. A caller supplies the resolver for the agent whose calls it is
55
- * describing, because a sandbox agent's tool list is not the conversation
56
- * roster's and no single list serves both.
51
+ * The description is the `detail` of the loop event of the call, which the loop
52
+ * computed with the `describeCall` hook of the tool. The phrase never reads the
53
+ * input of the call.
57
54
  *
58
55
  * The name always leads, and that is load-bearing rather than cosmetic. This
59
56
  * phrase is rendered alone, with no tool name beside it — unlike the chat chip,
@@ -61,11 +58,10 @@ export declare function synthesisEmission(runId: string, stepId: string, seq: nu
61
58
  * `edit_file` both describe a call by its path, so a detail on its own would
62
59
  * render a write and an edit of the same file identically.
63
60
  *
64
- * A tool with no hook, an unknown tool, and an input its schema rejects all fall
65
- * back to the name alone. No verb is added: the phrase is carried on a part that
61
+ * An event with no detail falls back to the name alone. No verb is added: the phrase is carried on a part that
66
62
  * already states the phase, so "Running" would only restate it.
67
63
  */
68
- export declare function activityForTool(name: string, input?: unknown, resolveDetail?: ToolDetailResolver): string;
64
+ export declare function activityForTool(name: string, detail?: string): string;
69
65
  /** On-change working-tree delta the sandbox executor posts per exec (paths
70
66
  * only; the executor skips directories). */
71
67
  export type SandboxTreeDelta = {
@@ -54,11 +54,9 @@ export function synthesisEmission(runId, stepId, seq, phase, activity, at, extra
54
54
  * The live-activity phrase for one tool call: the tool's name, then its own
55
55
  * description of this call when it has one.
56
56
  *
57
- * The description comes from the tool's `describeCall` hook, resolved through
58
- * `resolveDetail` — the same resolver the chat chip and the reloaded transcript
59
- * use. A caller supplies the resolver for the agent whose calls it is
60
- * describing, because a sandbox agent's tool list is not the conversation
61
- * roster's and no single list serves both.
57
+ * The description is the `detail` of the loop event of the call, which the loop
58
+ * computed with the `describeCall` hook of the tool. The phrase never reads the
59
+ * input of the call.
62
60
  *
63
61
  * The name always leads, and that is load-bearing rather than cosmetic. This
64
62
  * phrase is rendered alone, with no tool name beside it — unlike the chat chip,
@@ -66,12 +64,10 @@ export function synthesisEmission(runId, stepId, seq, phase, activity, at, extra
66
64
  * `edit_file` both describe a call by its path, so a detail on its own would
67
65
  * render a write and an edit of the same file identically.
68
66
  *
69
- * A tool with no hook, an unknown tool, and an input its schema rejects all fall
70
- * back to the name alone. No verb is added: the phrase is carried on a part that
67
+ * An event with no detail falls back to the name alone. No verb is added: the phrase is carried on a part that
71
68
  * already states the phase, so "Running" would only restate it.
72
69
  */
73
- export function activityForTool(name, input, resolveDetail) {
74
- const detail = resolveDetail?.(name, input);
70
+ export function activityForTool(name, detail) {
75
71
  return detail === undefined ? name : `${name} ${detail}`;
76
72
  }
77
73
  const SandboxTreeDeltaSchema = z.object({
@@ -20,7 +20,6 @@
20
20
  * no terminal at all.
21
21
  */
22
22
  import type { Logger } from "../lib/logger.js";
23
- import type { ToolDetailResolver } from "../tools/detail-resolver.js";
24
23
  /**
25
24
  * Writes one part to the run-event stream. Injected rather than imported so this
26
25
  * module stays free of the durability engine and so a test can record what a
@@ -64,15 +63,12 @@ export type ProfileActivityEmitter = {
64
63
  * Reported per tool call the profiler agent makes, phrased by the shared
65
64
  * translator.
66
65
  *
67
- * `resolveDetail` is built over the profiler agent's own tool list, so the
68
- * phrase comes from the called tool's `describeCall` hook. It is passed per
69
- * call rather than held by the emitter because the emitter exists before the
70
- * agent does — `sandboxInit` is reported before the sandbox the agent's tools
71
- * are bound to. The call site supplies a RESOLVER, never a phrase: the
66
+ * `detail` is the detail of the loop event, which the `describeCall` hook of
67
+ * the called tool made. The call site supplies a detail, never a phrase: the
72
68
  * phrases are this capability's observable contract, and a call site that
73
69
  * composed its own is a call site that can drift from it.
74
70
  */
75
- forTool(toolUseId: string, name: string, input: unknown, resolveDetail?: ToolDetailResolver): Promise<void>;
71
+ forTool(toolUseId: string, name: string, detail?: string): Promise<void>;
76
72
  /** Reported for the vector-store pass that indexes per-file descriptions. */
77
73
  indexing(): Promise<void>;
78
74
  /** The success terminal. Emitted only after the terminal ledger write has landed. */
@@ -58,7 +58,7 @@ export function createProfileActivityEmitter(write, frame, logger, now) {
58
58
  sandboxInit: () => emit("sandbox-init", "Starting sandbox"),
59
59
  scanning: () => emit("executing", "Scanning input files"),
60
60
  agentStarting: () => emit("executing", "Running data-profiler"),
61
- forTool: (toolUseId, name, input, resolveDetail) => emit("executing", activityForTool(name, input, resolveDetail), toolUseId),
61
+ forTool: (toolUseId, name, detail) => emit("executing", activityForTool(name, detail), toolUseId),
62
62
  indexing: () => emit("indexing", "Indexing input descriptions for search"),
63
63
  complete: () => emit("complete", "Profile complete"),
64
64
  failed: (reason) => emit("failed", reason),