@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.
- package/dist/app/chat-turn-workflow.d.ts +3 -1
- package/dist/app/chat-turn-workflow.js +18 -3
- package/dist/app/chat-turn.js +27 -1
- package/dist/app/in-process-turn.js +3 -6
- package/dist/contracts/chat-events.d.ts +45 -0
- package/dist/contracts/chat-frame.js +11 -0
- package/dist/contracts/chat-parts.d.ts +19 -1
- package/dist/contracts/chat-parts.js +1 -0
- package/dist/contracts/index.d.ts +2 -2
- package/dist/contracts/message.d.ts +7 -1
- package/dist/contracts/part-registry.js +1 -0
- package/dist/contracts/schemas/chat-events.d.ts +321 -0
- package/dist/contracts/schemas/chat-events.js +30 -0
- package/dist/contracts/schemas/chat-parts.d.ts +29 -1
- package/dist/contracts/schemas/chat-parts.js +10 -0
- package/dist/execution/run-synthesis.js +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -0
- package/dist/input-sanitization.d.ts +30 -2
- package/dist/input-sanitization.js +187 -2
- package/dist/loop/chat-stream.js +1 -1
- package/dist/loop/run-loop.js +66 -5
- package/dist/loop/tool-adapter.d.ts +8 -0
- package/dist/loop/tool-adapter.js +75 -4
- package/dist/loop/tool-detail.d.ts +5 -1
- package/dist/loop/tool-detail.js +7 -3
- package/dist/loop/tool-value.d.ts +57 -0
- package/dist/loop/tool-value.js +249 -0
- package/dist/loop/types.d.ts +9 -2
- package/dist/memory/conversation-display-storage.d.ts +3 -2
- package/dist/memory/conversation-display-storage.js +14 -2
- package/dist/memory/count-tokens.js +29 -2
- package/dist/providers/ai-sdk.d.ts +17 -14
- package/dist/providers/ai-sdk.js +14 -16
- package/dist/sandbox/sandbox-step-translate.d.ts +5 -9
- package/dist/sandbox/sandbox-step-translate.js +5 -9
- package/dist/tasks/data-profile-activity.d.ts +3 -7
- package/dist/tasks/data-profile-activity.js +1 -1
- package/dist/tasks/data-profile.js +3 -6
- package/dist/tools/ad-hoc-router.js +1 -0
- package/dist/tools/bio/alphafold-prediction.js +1 -0
- package/dist/tools/bio/chembl.js +1 -0
- package/dist/tools/bio/comptox.js +1 -0
- package/dist/tools/bio/drug-gene-interactions.js +1 -0
- package/dist/tools/bio/gene-disease-evidence.js +1 -0
- package/dist/tools/bio/gene-preclinical-profile.js +1 -0
- package/dist/tools/bio/lookup-annotation.js +1 -0
- package/dist/tools/bio/opentargets.js +1 -0
- package/dist/tools/bio/pubchem.js +1 -0
- package/dist/tools/bio/pubmed.js +1 -0
- package/dist/tools/bio/search-clinical-trials.js +1 -0
- package/dist/tools/bio/search-faers.js +1 -0
- package/dist/tools/bio/search-gene.js +1 -0
- package/dist/tools/bio/search-geo-datasets.js +1 -0
- package/dist/tools/bio/search-interactions.js +1 -0
- package/dist/tools/bio/search-patents.js +1 -0
- package/dist/tools/bio/search-protein.js +1 -0
- package/dist/tools/bio/search-regulatory-approvals.js +1 -0
- package/dist/tools/bio/target-safety.js +1 -0
- package/dist/tools/capability-data.js +5 -0
- package/dist/tools/define-tool.d.ts +18 -0
- package/dist/tools/define-tool.js +2 -0
- package/dist/tools/display/show-user.js +1 -0
- package/dist/tools/execute-analysis.js +4 -0
- package/dist/tools/knowledge/check.d.ts +47 -1
- package/dist/tools/knowledge/check.js +10 -1
- package/dist/tools/knowledge/client.d.ts +1 -1
- package/dist/tools/knowledge/recommend.js +4 -3
- package/dist/tools/knowledge/situation.d.ts +39 -1
- package/dist/tools/knowledge/situation.js +80 -8
- package/dist/tools/knowledge/skeleton.d.ts +7 -0
- package/dist/tools/knowledge/skeleton.js +88 -2
- package/dist/tools/knowledge/template.js +1 -0
- package/dist/tools/memory/update-working-memory.js +5 -0
- package/dist/tools/read-tool-output.js +4 -0
- package/dist/tools/report-authoring/authoring-tools.js +8 -0
- package/dist/tools/report-session/examine-page.js +5 -0
- package/dist/tools/report-session/list-artifacts.js +5 -0
- package/dist/tools/report-session/preview-report.js +1 -0
- package/dist/tools/report-session/record-version.js +1 -0
- package/dist/tools/report-session/script-tools.js +2 -0
- package/dist/tools/research/context7-docs.js +2 -0
- package/dist/tools/research/generate-analogy-report.js +3 -0
- package/dist/tools/research/generate-plan.js +6 -0
- package/dist/tools/research/inspect-data-profile.js +5 -0
- package/dist/tools/research/inspect-run.js +5 -0
- package/dist/tools/research/resolve-citation.js +1 -0
- package/dist/tools/research/search-arxiv.js +1 -0
- package/dist/tools/research/search-github-repos.js +1 -0
- package/dist/tools/research/search-semantic-scholar.js +1 -0
- package/dist/tools/research/web-fetch.js +1 -0
- package/dist/tools/research/web-search.js +1 -0
- package/dist/tools/sandbox/lease-tools.js +2 -0
- package/dist/tools/sandbox/link-packages.js +1 -0
- package/dist/tools/sandbox/list-available-packages.js +5 -0
- package/dist/tools/sandbox/list-available-refs.js +5 -0
- package/dist/tools/sandbox/report-blocker.js +1 -0
- package/dist/tools/sandbox/skills.js +2 -0
- package/dist/tools/sandbox/submit-file-metadata.js +1 -0
- package/dist/tools/script/file-tools.js +6 -0
- package/dist/tools/script/run-script.js +20 -1
- package/dist/tools/start-report-session.js +1 -0
- package/dist/tools/tool-view.d.ts +63 -0
- package/dist/tools/tool-view.js +68 -0
- package/dist/tools/workspace/edit-file.js +5 -0
- package/dist/tools/workspace/execute-command.js +5 -2
- package/dist/tools/workspace/file-stat.js +5 -0
- package/dist/tools/workspace/grep.js +4 -0
- package/dist/tools/workspace/list-files.js +5 -0
- package/dist/tools/workspace/read-file.js +4 -0
- package/dist/tools/workspace/scan-inputs.js +5 -0
- package/dist/tools/workspace/show-file.js +1 -0
- package/dist/tools/workspace/show-plan.js +1 -0
- package/dist/tools/workspace/workspace-search.js +5 -0
- package/dist/tools/workspace/write-file.js +7 -2
- package/dist/workflows/execute-analysis.js +2 -1
- package/dist/workflows/sandbox-step.js +3 -7
- package/package.json +1 -1
- package/dist/tools/detail-resolver.d.ts +0 -40
- 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
|
+
}
|
package/dist/loop/types.d.ts
CHANGED
|
@@ -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 :
|
|
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 +=
|
|
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
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
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
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
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
|
|
269
|
-
*
|
|
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
|
/**
|
package/dist/providers/ai-sdk.js
CHANGED
|
@@ -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
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
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
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
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
|
|
125
|
-
*
|
|
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`,
|
|
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
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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
|
-
*
|
|
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,
|
|
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
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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
|
-
*
|
|
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,
|
|
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
|
-
* `
|
|
68
|
-
*
|
|
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,
|
|
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,
|
|
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),
|