@inflexa-ai/harness 0.48.1 → 0.48.2
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/contracts/chat-events.d.ts +40 -0
- package/dist/contracts/chat-frame.js +11 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/message.d.ts +7 -1
- package/dist/contracts/schemas/chat-events.d.ts +319 -0
- package/dist/contracts/schemas/chat-events.js +29 -0
- package/dist/execution/run-synthesis.js +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/input-sanitization.d.ts +30 -2
- package/dist/input-sanitization.js +187 -2
- package/dist/loop/run-loop.js +50 -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.js +4 -1
- package/dist/memory/count-tokens.js +29 -2
- 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";
|
|
@@ -20,7 +20,7 @@ import { isDataUIPart, validateUIMessages } from "ai";
|
|
|
20
20
|
import { z } from "zod";
|
|
21
21
|
import { parseUnknownRecord } from "../lib/unknown-containers.js";
|
|
22
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";
|
|
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.
|
|
@@ -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
|
}
|
|
@@ -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),
|
|
@@ -27,13 +27,12 @@ import { forStep, forSubAgent } from "../auth/types.js";
|
|
|
27
27
|
import { presentFirst } from "../lib/present.js";
|
|
28
28
|
import { collapseWhitespace } from "../lib/text.js";
|
|
29
29
|
import { createDataProfilerAgent } from "../agents/sandbox/data-profiler.js";
|
|
30
|
-
import { runWithSalvage
|
|
30
|
+
import { runWithSalvage } from "../loop/run-loop.js";
|
|
31
31
|
import { recordedInputOf } from "../loop/tool-adapter.js";
|
|
32
32
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
33
33
|
import { deliverNotice, passGate } from "../lib/hooks.js";
|
|
34
34
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
35
35
|
import { defineTool } from "../tools/define-tool.js";
|
|
36
|
-
import { createDetailResolver } from "../tools/detail-resolver.js";
|
|
37
36
|
import { renderResources, renderWorkspace } from "../prompts/briefing.js";
|
|
38
37
|
import { workPause } from "../workflows/pause.js";
|
|
39
38
|
import { clearPausesOfWork } from "../state/paused-work.js";
|
|
@@ -109,6 +108,7 @@ export function createSubmitProfileTool(detected, menu) {
|
|
|
109
108
|
"A submission whose operations do not resolve, or that leaves kept files unclaimed, comes back with " +
|
|
110
109
|
"the errors; the repair is a FULL resubmit of the whole operation list. The last accepted submission is the profile.",
|
|
111
110
|
inputSchema: ProfileSubmissionSchema,
|
|
111
|
+
view: "none",
|
|
112
112
|
describeCall: "none",
|
|
113
113
|
execute: (input) => {
|
|
114
114
|
const resolution = resolveProfileSubmission(input, detected, menu, { finalRound: false });
|
|
@@ -545,9 +545,6 @@ export async function runDataProfileBody(input, deps) {
|
|
|
545
545
|
const submitProfileTool = createSubmitProfileTool(detected, briefingMenu);
|
|
546
546
|
const baseAgent = createDataProfilerAgent(sandboxAgentDeps);
|
|
547
547
|
const agentDef = { ...baseAgent, tools: [...baseAgent.tools, submitProfileTool] };
|
|
548
|
-
// The profiler's own roster, `submit_profile` included, so every call it
|
|
549
|
-
// makes is phrased by the tool that was called.
|
|
550
|
-
const resolveDetail = createDetailResolver([...withRunScript(agentDef).tools, ...(agentDef.scriptTools ?? [])], logger);
|
|
551
548
|
const signal = new AbortController().signal;
|
|
552
549
|
// Covers the gap between a ready sandbox and the agent's first tool call, which would
|
|
553
550
|
// otherwise still read as `Starting sandbox` long after the container was up. The run
|
|
@@ -563,7 +560,7 @@ export async function runDataProfileBody(input, deps) {
|
|
|
563
560
|
// happening now. The events of a tool call arrive from inside its step.
|
|
564
561
|
emit: async (event) => {
|
|
565
562
|
if (event.type === "tool-started")
|
|
566
|
-
await activity.forTool(event.toolUseId, event.name, event.
|
|
563
|
+
await activity.forTool(event.toolUseId, event.name, event.detail);
|
|
567
564
|
},
|
|
568
565
|
invocationId: workflowId,
|
|
569
566
|
usageRecorder: deps.usageRecorder,
|
|
@@ -125,6 +125,7 @@ function routeTool() {
|
|
|
125
125
|
id: SUBMIT_ROUTE_TOOL_ID,
|
|
126
126
|
description: "Submit the single specialist and the packages of the step. This is the only valid response.",
|
|
127
127
|
inputSchema: routeSchema,
|
|
128
|
+
view: "none",
|
|
128
129
|
describeCall: "none",
|
|
129
130
|
execute: () => Promise.resolve(ok({ recorded: true })),
|
|
130
131
|
});
|
|
@@ -29,6 +29,7 @@ export const alphafoldPredictionTool = defineTool({
|
|
|
29
29
|
inputSchema: z.object({
|
|
30
30
|
uniprotAccession: z.string().min(1).describe("A UniProt accession, for example 'P38398' (BRCA1) or 'P69905' (hemoglobin subunit alpha)."),
|
|
31
31
|
}),
|
|
32
|
+
view: "none",
|
|
32
33
|
describeCall: ({ uniprotAccession }) => uniprotAccession,
|
|
33
34
|
execute: async ({ uniprotAccession }) => {
|
|
34
35
|
// The client trims before it builds the URL, thus a miss echoes the same
|
package/dist/tools/bio/chembl.js
CHANGED
|
@@ -127,6 +127,7 @@ export const chemblTool = defineTool({
|
|
|
127
127
|
"compounds here first. If ChEMBL misses the compound, resolve it via pubchem action='compound' and bridge back with pubchem action='crossrefs'.\n" +
|
|
128
128
|
"An empty array is valid no-data, not an error — do not retry the same call.",
|
|
129
129
|
inputSchema,
|
|
130
|
+
view: "none",
|
|
130
131
|
describeCall: "none",
|
|
131
132
|
execute: async ({ action, query, searchType, chemblId, idType, activityType, limit }) => {
|
|
132
133
|
switch (action) {
|
|
@@ -112,6 +112,7 @@ export function createComptoxTool(deps) {
|
|
|
112
112
|
"This is environmental/industrial-chemical data: for drug-like compounds prefer PubChem or ChEMBL — dataset 'chemical' returns " +
|
|
113
113
|
"`detail.pubchemCid` to bridge back.",
|
|
114
114
|
inputSchema,
|
|
115
|
+
view: "none",
|
|
115
116
|
describeCall: "none",
|
|
116
117
|
execute: async (input) => {
|
|
117
118
|
const headers = getEpaCcteHeaders(deps.apiKey);
|
|
@@ -118,6 +118,7 @@ export function createDrugGeneInteractionsTool(deps) {
|
|
|
118
118
|
"not be reached (a missing DRUGBANK_API_KEY lands here — state that the key is missing, and proceed with DGIdb). A `found: false` entry means the identifier " +
|
|
119
119
|
"is not in DGIdb at all, usually a non-HUGO symbol or a brand name. Neither is worth retrying unchanged.",
|
|
120
120
|
inputSchema,
|
|
121
|
+
view: "none",
|
|
121
122
|
describeCall: "none",
|
|
122
123
|
execute: async (input) => {
|
|
123
124
|
const inputs = typeof input.query === "string" ? [input.query] : input.query;
|
|
@@ -170,6 +170,7 @@ export function createGeneDiseaseEvidenceTool(deps) {
|
|
|
170
170
|
"could not be reached (a missing DISGENET_API_KEY lands here — state that the key is missing, and proceed with the others); 'not_applicable' means that " +
|
|
171
171
|
"corpus has no lookup for this queryType. Only 'no_data' is evidence of absence, and none is worth retrying unchanged.",
|
|
172
172
|
inputSchema,
|
|
173
|
+
view: "none",
|
|
173
174
|
describeCall: "none",
|
|
174
175
|
execute: async (input) => {
|
|
175
176
|
const applicable = applicableSources(input.queryType);
|
|
@@ -138,6 +138,7 @@ export const genePreclinicalProfileTool = defineTool({
|
|
|
138
138
|
"with no phenotype terms means the gene has not been IMPC-phenotyped, which is common; an empty hpoTerms list means Monarch curates no human " +
|
|
139
139
|
"phenotype for the gene, and a null geneCurie means the symbol anchored on no HGNC id.",
|
|
140
140
|
inputSchema,
|
|
141
|
+
view: "none",
|
|
141
142
|
describeCall: "none",
|
|
142
143
|
execute: async ({ geneSymbol, include, species, minRank, tissueLimit, phenotypeLimit }) => {
|
|
143
144
|
const halves = include ?? ["expression", "knockout"];
|
|
@@ -156,6 +156,7 @@ export const lookupAnnotationTool = defineTool({
|
|
|
156
156
|
"An empty terms / annotations / pathways array is valid no-data — report it and continue, do not retry the same input. An empty pathways array " +
|
|
157
157
|
"for a pathwayId means neither database holds that identifier.",
|
|
158
158
|
inputSchema,
|
|
159
|
+
view: "none",
|
|
159
160
|
describeCall: "none",
|
|
160
161
|
execute: async ({ vocabulary, query, pathwayId, goId, geneProductId, taxonId, organism, includeGenes, limit, }) => {
|
|
161
162
|
const cap = limit ?? DEFAULT_LIMIT;
|
|
@@ -89,6 +89,7 @@ export const openTargetsTool = defineTool({
|
|
|
89
89
|
"NO-DATA IS FINAL — do not retry the same id. Empty associations mean no evidence, or an id the platform does not hold. An empty candidates " +
|
|
90
90
|
"list means the name matched no disease: rephrase it, do not invent an id.",
|
|
91
91
|
inputSchema,
|
|
92
|
+
view: "none",
|
|
92
93
|
describeCall: "none",
|
|
93
94
|
execute: async (input) => {
|
|
94
95
|
const limit = input.limit ?? DEFAULT_LIMIT;
|
|
@@ -83,6 +83,7 @@ export const pubchemTool = defineTool({
|
|
|
83
83
|
"then chembl action='mechanism' / 'bioactivity' there for curated activity data. " +
|
|
84
84
|
"An empty results / crossRefs / assays array is valid no-data (no match; the CID is in no external registry; the compound was never screened) — do not retry.",
|
|
85
85
|
inputSchema,
|
|
86
|
+
view: "none",
|
|
86
87
|
describeCall: "none",
|
|
87
88
|
execute: async (input) => {
|
|
88
89
|
switch (input.action) {
|
package/dist/tools/bio/pubmed.js
CHANGED
|
@@ -163,6 +163,7 @@ export function createPubMedTool(deps) {
|
|
|
163
163
|
"Report it and continue, do not retry it.",
|
|
164
164
|
inputSchema,
|
|
165
165
|
outputSchema,
|
|
166
|
+
view: "none",
|
|
166
167
|
// One tool, three jobs. The action alone would still render three
|
|
167
168
|
// different searches identically, so each action names its own subject:
|
|
168
169
|
// the query, how many articles, or which article.
|
|
@@ -76,6 +76,7 @@ export const searchClinicalTrialsTool = defineTool({
|
|
|
76
76
|
message: "nctId is required when action is 'details' — one NCT ID, e.g. 'NCT00000102'. Find one with action 'search'.",
|
|
77
77
|
path: ["nctId"],
|
|
78
78
|
}),
|
|
79
|
+
view: "none",
|
|
79
80
|
describeCall: "none",
|
|
80
81
|
execute: async ({ action, query, nctId, phase, status, limit, includeSummaries }) => {
|
|
81
82
|
if (action === "details") {
|
|
@@ -70,6 +70,7 @@ export const searchFaersTool = defineTool({
|
|
|
70
70
|
message: `limit is capped at ${LABEL_MAX_LIMIT} for action 'label' — each label version carries whole prose sections`,
|
|
71
71
|
path: ["limit"],
|
|
72
72
|
}),
|
|
73
|
+
view: "none",
|
|
73
74
|
describeCall: "none",
|
|
74
75
|
execute: async ({ action, drugName, limit, serious }) => {
|
|
75
76
|
switch (action) {
|
|
@@ -66,6 +66,7 @@ export const searchGeneTool = defineTool({
|
|
|
66
66
|
.describe("Gene identifiers to look up, mixed freely (e.g. ['BRCA1', 'ENSG00000141510', 'P38398', 'HGNC:1100', 'CHEMBL5619'])"),
|
|
67
67
|
species: z.string().default("homo_sapiens").describe("Species name (e.g. 'homo_sapiens', 'mus_musculus')"),
|
|
68
68
|
}),
|
|
69
|
+
view: "none",
|
|
69
70
|
// The identifiers are the call. A large batch is summarized here rather than
|
|
70
71
|
// left to the emit-site cap: that cap cuts at a character, so a 200-symbol
|
|
71
72
|
// join arrives ending in `TP5` — a fragment that reads as a real symbol and
|