@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
@@ -70,6 +70,7 @@ export function createLinkPackagesTool(deps) {
70
70
  "After a `collision` of one name that BOTH tracks hold, call this tool again for that package with the prefixed form, `python:<name>` or `r:<name>`. " +
71
71
  "A collision is terminal only after that second call also refuses, or when it names two versions of one distribution — then report it and continue without the package.",
72
72
  inputSchema: LinkPackagesInputSchema,
73
+ view: "none",
73
74
  describeCall: ({ packages }) => packages.join(", "),
74
75
  execute: async (input) => {
75
76
  // Every entry parses before any link lands. A partial link over a
@@ -495,6 +495,11 @@ export function createListAvailablePackagesTool(deps = {}) {
495
495
  "`limit`: cap the packages listed; the response always carries the true `total` and a `hasMore` flag, so truncation is never silent. " +
496
496
  "Each entry renders as `name==version` where the source pins a version, and a `names` check also reports the store directory and the content hash where the source records them.",
497
497
  inputSchema: ListAvailablePackagesInputSchema,
498
+ view: {
499
+ input: (input) => input,
500
+ result: (_input, result) => result,
501
+ error: true,
502
+ },
498
503
  // `queryPackages` returns on `names` before it reads another field, thus
499
504
  // the presence check comes first. An empty array is not a presence check.
500
505
  // `names` are exact identifiers a caller checks for, thus they name the
@@ -600,6 +600,11 @@ export function createListAvailableRefsTool(deps) {
600
600
  "An empty result means the dataset is absent right now — not that it is unobtainable. Say so; never guess a path, and never silently substitute a different dataset. " +
601
601
  "The store is provisioned by the host and is NOT frozen: datasets can be added through the host's own provisioning path between runs, and whatever is added shows up in a later run. Within one run the listing does not change, thus one call per question is enough.",
602
602
  inputSchema: ListAvailableRefsInputSchema,
603
+ view: {
604
+ input: (input) => input,
605
+ result: (_input, result) => result,
606
+ error: true,
607
+ },
603
608
  // `execute` resolves `path ?? category` for the subtree it inspects, and
604
609
  // it ignores `category` when `path` is present. `query` is an ADDITIVE
605
610
  // filter over that subtree, thus the target keeps naming the call and the
@@ -37,6 +37,7 @@ export function createReportBlockerToolFor(deps) {
37
37
  id: REPORT_BLOCKER_TOOL_ID,
38
38
  description: `Terminal. ${deps.blockedWhen} Pass a clear reason. Stop immediately after calling — take no further actions.`,
39
39
  inputSchema: ReportBlockerInputSchema,
40
+ view: "none",
40
41
  describeCall: "none",
41
42
  execute: (input) => Promise.resolve(ok({ kind: "blocker", reason: input.reason, message: "Blocker recorded. You are done — stop now and do not take further actions." })),
42
43
  });
@@ -63,6 +63,7 @@ export function createSkillTools(deps) {
63
63
  "and the first line that holds a query word — ranked by how many distinct query words the file " +
64
64
  "holds. Then `skill_read` the file. No matches returns a data variant.",
65
65
  inputSchema: SkillSearchInputSchema,
66
+ view: "none",
66
67
  describeCall: "none",
67
68
  execute: async ({ query, topK }) => {
68
69
  if (allowed.size === 0) {
@@ -121,6 +122,7 @@ export function createSkillTools(deps) {
121
122
  "found via skill_search). Confined to your declared skills. Undeclared " +
122
123
  "skills and missing paths return a data variant.",
123
124
  inputSchema: SkillReadInputSchema,
125
+ view: "none",
124
126
  describeCall: "none",
125
127
  execute: async ({ skill, path }) => {
126
128
  if (!allowed.has(skill)) {
@@ -43,6 +43,7 @@ export function createSubmitFileMetadataTool(cell) {
43
43
  "accepted=true means every file now has metadata — STOP. Otherwise drop " +
44
44
  "the unknownPaths and describe the remaining files.",
45
45
  inputSchema: SubmitFileMetadataInputSchema,
46
+ view: "none",
46
47
  describeCall: "none",
47
48
  execute: (input) => {
48
49
  const knownPaths = cell.knownPaths;
@@ -165,6 +165,7 @@ function createReadTableTool(deps) {
165
165
  "A larger table, or another format such as h5ad, is work for an analysis run.",
166
166
  inputSchema: readTableInput,
167
167
  outputSchema: ReadTableOutputSchema,
168
+ view: "none",
168
169
  describeCall: ({ path, entry }) => (entry === undefined ? path : `${path} : ${entry}`),
169
170
  describeResult: (_input, result) => `${String(result.rowCount)} rows`,
170
171
  execute: async ({ path, entry, columns }, ctx) => {
@@ -201,6 +202,7 @@ function createListArchiveTool(deps) {
201
202
  "`read_table` and `read_pdf` then read one of them as their `entry`.",
202
203
  inputSchema: listArchiveInput,
203
204
  outputSchema: ListArchiveOutputSchema,
205
+ view: "none",
204
206
  describeCall: ({ path }) => path,
205
207
  describeResult: (_input, result) => `${String(result.entries.length)} files`,
206
208
  execute: async ({ path }, ctx) => {
@@ -266,6 +268,7 @@ export function createReadPdfTool(deps) {
266
268
  "A scanned page with no text layer reads as an empty text.",
267
269
  inputSchema: readPdfInput,
268
270
  outputSchema: ReadPdfOutputSchema,
271
+ view: "none",
269
272
  describeCall: ({ path, entry, firstPage, lastPage }) => {
270
273
  const file = entry === undefined ? path : `${path} : ${entry}`;
271
274
  return firstPage === undefined && lastPage === undefined ? file : `${file}, pages ${String(firstPage ?? 1)}-${String(lastPage ?? "")}`;
@@ -359,6 +362,7 @@ function createWriteDerivedTool(deps) {
359
362
  "A name holds one file: a second write of the name with other content is refused.",
360
363
  inputSchema: writeDerivedInput,
361
364
  outputSchema: WrittenOutputSchema,
365
+ view: "none",
362
366
  describeCall: ({ name }) => name,
363
367
  describeResult: (_input, result) => result.path,
364
368
  execute: async ({ name, content, rows }, ctx) => {
@@ -400,6 +404,7 @@ function createRenderChartTool(deps) {
400
404
  "To show a chart in the chat, use `show_user`.",
401
405
  inputSchema: renderChartInput,
402
406
  outputSchema: WrittenOutputSchema,
407
+ view: "none",
403
408
  describeCall: ({ name }) => name,
404
409
  describeResult: (_input, result) => result.path,
405
410
  execute: async ({ name, spec, width }, ctx) => {
@@ -447,6 +452,7 @@ function createExportPdfTool(deps) {
447
452
  "Charts that `render_chart` wrote can appear in it as images. The PDF reads no URL.",
448
453
  inputSchema: exportPdfInput,
449
454
  outputSchema: ExportPdfOutputSchema,
455
+ view: "none",
450
456
  describeCall: ({ name }) => name,
451
457
  describeResult: (_input, result) => result.path,
452
458
  execute: async ({ name, markdown, title }, ctx) => {
@@ -24,6 +24,7 @@ import { parseUnknownRecord, UnknownArraySchema } from "../../lib/unknown-contai
24
24
  import { computeDetail, computeResultDetail } from "../../loop/tool-detail.js";
25
25
  import { buildFileReferenceCardData, MAX_FILES } from "../../memory/card-builders.js";
26
26
  import { defineTool, readToolCallRecord, readToolResultImages, withToolCallRecord, } from "../define-tool.js";
27
+ import { viewCode, viewFields, viewJson, viewText } from "../tool-view.js";
27
28
  import { ScriptFileEffectSchema } from "./file-tools.js";
28
29
  import { recordScriptRun } from "./metrics.js";
29
30
  export const RUN_SCRIPT_TOOL_ID = "run_script";
@@ -141,7 +142,7 @@ async function runNestedCall(tool, ctx, state, log, args, signal) {
141
142
  const invocationId = state.nextInvocationId();
142
143
  const detail = computeDetail(tool, args, log);
143
144
  const started = performance.now();
144
- const startFrame = { type: "tool-started", source: state.source, toolUseId: invocationId, name: tool.id, input: args, detail };
145
+ const startFrame = { type: "tool-started", source: state.source, toolUseId: invocationId, name: tool.id, detail };
145
146
  state.observe(startFrame);
146
147
  await ctx.emit(startFrame);
147
148
  const finish = async (outcome, finishedDetail) => {
@@ -245,6 +246,7 @@ function descriptionOf(tools) {
245
246
  return ("Run a JavaScript script that calls the tools below as functions, in a JavaScript VM on the host with no files and no network. " +
246
247
  "Use it for lookups and for light work on tables: call several tools in parallel with Promise.all, filter, join, and count their results, and print only what you need. " +
247
248
  "Only the printed output and the returned value come back to you.\n" +
249
+ "Each file that the script writes is already shown to the user in one card with the script.\n" +
248
250
  "API: `await tools.<name>(args)` returns the JSON result of the tool and throws an Error on a failed call. " +
249
251
  '`await describe("<name>")` returns the full description of a tool, with the meaning of each argument. ' +
250
252
  "`text(value)` and `console.log` print. `return` gives a value. There are no timers, no fetch, and no modules.\n" +
@@ -273,6 +275,18 @@ function filesCard(description, writes, script) {
273
275
  shown.push({ path: script.path, caption: "The script that derived these files." });
274
276
  return buildFileReferenceCardData({ title: description ?? "Derived files", files: shown });
275
277
  }
278
+ /**
279
+ * The shown result of a script. A failed script puts its failure first, because the failure is what the reader looks for.
280
+ * The nested calls and the files stay out: the activity part and the file card of the call carry them. A script that
281
+ * printed nothing and returned nothing gives no result.
282
+ */
283
+ function scriptResultView(result) {
284
+ const output = result.output === "" ? undefined : viewText(result.output);
285
+ const fields = !result.ok
286
+ ? viewFields({ error: result.error === undefined ? undefined : viewText(result.error), output })
287
+ : viewFields({ output, value: result.value === undefined ? undefined : viewJson(result.value) });
288
+ return fields.fields.length === 0 ? undefined : fields;
289
+ }
276
290
  /** The `run_script` tool over the script tools of an agent. */
277
291
  export function createRunScriptTool(scriptTools, runtime = {}, options = {}) {
278
292
  const vm = sandboxRuntimeOf(runtime);
@@ -283,6 +297,11 @@ export function createRunScriptTool(scriptTools, runtime = {}, options = {}) {
283
297
  id: RUN_SCRIPT_TOOL_ID,
284
298
  description: descriptionOf(scriptTools),
285
299
  inputSchema,
300
+ view: {
301
+ input: ({ code }) => viewCode("javascript", code),
302
+ result: (_input, result) => scriptResultView(result),
303
+ error: true,
304
+ },
286
305
  describeCall: ({ description }) => description,
287
306
  describeResult: ({ description }, result) => {
288
307
  const tools = result.calls.length === 0 ? undefined : result.calls.map((call) => call.tool).join(", ");
@@ -141,6 +141,7 @@ export function createStartReportSessionTool(deps) {
141
141
  "`no_browser` means that this deployment has no browser for the report agent, thus no report session can start; tell the user. " +
142
142
  "`refused`, `parent_not_found`, `parent_not_a_conversation`, and `failed` mean that nothing started, and `detail` gives the reason where there is one.",
143
143
  inputSchema: startReportSessionInput,
144
+ view: "none",
144
145
  describeCall: "none",
145
146
  execute: async (input, ctx) => {
146
147
  const brief = {
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The view of a tool: what a call of the tool shows to a host.
3
+ *
4
+ * A tool declares a `ToolCallView`, or `"none"`, in `defineTool`. A member gives the value to show, and the emit site of
5
+ * the loop (`loop/tool-value.ts`) cleans it: control characters out, each listed secret form out, then the cap. A member
6
+ * gives a string for a `text` value, any other plain value for a `json` value, or the result of one of the helpers
7
+ * below for a value of a kind that the author chooses.
8
+ */
9
+ export declare class ViewText {
10
+ readonly text: string;
11
+ readonly kind = "text";
12
+ constructor(text: string);
13
+ }
14
+ export declare class ViewJson {
15
+ readonly value: unknown;
16
+ readonly kind = "json";
17
+ constructor(value: unknown);
18
+ }
19
+ export declare class ViewCode {
20
+ readonly language: string;
21
+ readonly text: string;
22
+ readonly kind = "code";
23
+ constructor(language: string, text: string);
24
+ }
25
+ /** A value of one kind that holds no other value. */
26
+ export type ViewLeaf = ViewText | ViewJson | ViewCode;
27
+ export declare class ViewFields {
28
+ readonly fields: ReadonlyArray<{
29
+ readonly name: string;
30
+ readonly leaf: ViewLeaf;
31
+ }>;
32
+ readonly kind = "fields";
33
+ constructor(fields: ReadonlyArray<{
34
+ readonly name: string;
35
+ readonly leaf: ViewLeaf;
36
+ }>);
37
+ }
38
+ export type ViewValue = ViewLeaf | ViewFields;
39
+ /**
40
+ * What a tool shows of its calls. Each member is synchronous and pure, and it never reaches the model. A member that is
41
+ * absent shows nothing. A member that throws, or that gives a value that breaks a rule of the kinds, shows nothing, and
42
+ * the call runs as before.
43
+ */
44
+ export type ToolCallView<Input, Output> = {
45
+ /** The shown input. It reads the input after the parse with the input schema of the tool. */
46
+ input?(input: Input): unknown;
47
+ /** The shown result of a call that ended `ok`. It reads the ok value that the tool returned. */
48
+ result?(input: Input, result: Output): unknown;
49
+ /** Show the error text of a call that ended `error`. */
50
+ readonly error?: true;
51
+ };
52
+ /** A plain text. */
53
+ export declare function viewText(text: string): ViewText;
54
+ /** The JSON text of a value. A string shows as a JSON string, with its quotes. */
55
+ export declare function viewJson(value: unknown): ViewJson;
56
+ /** Source code. `language` has 1 to 20 characters of lowercase letters, digits, `+`, `#`, `.`, and `-`. */
57
+ export declare function viewCode(language: string, text: string): ViewCode;
58
+ /**
59
+ * An ordered list of named values, at most 4. A name has 1 to 40 characters of letters, digits, `_`, and `-`. A field
60
+ * whose leaf is `undefined` is left out. The order is the order of the keys, except that a name made of digits only
61
+ * goes first, as a JavaScript object orders such a key.
62
+ */
63
+ export declare function viewFields(leaves: Readonly<Record<string, ViewLeaf | undefined>>): ViewFields;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The view of a tool: what a call of the tool shows to a host.
3
+ *
4
+ * A tool declares a `ToolCallView`, or `"none"`, in `defineTool`. A member gives the value to show, and the emit site of
5
+ * the loop (`loop/tool-value.ts`) cleans it: control characters out, each listed secret form out, then the cap. A member
6
+ * gives a string for a `text` value, any other plain value for a `json` value, or the result of one of the helpers
7
+ * below for a value of a kind that the author chooses.
8
+ */
9
+ /*
10
+ * Each helper returns an instance of one of these classes, and the emit site tests the class with `instanceof`. A tool
11
+ * result with a field named `kind` is a plain object, thus it is never read as a kind. The constructors stay out of the
12
+ * public barrel, thus only the helpers make an instance.
13
+ */
14
+ export class ViewText {
15
+ text;
16
+ kind = "text";
17
+ constructor(text) {
18
+ this.text = text;
19
+ }
20
+ }
21
+ export class ViewJson {
22
+ value;
23
+ kind = "json";
24
+ constructor(value) {
25
+ this.value = value;
26
+ }
27
+ }
28
+ export class ViewCode {
29
+ language;
30
+ text;
31
+ kind = "code";
32
+ constructor(language, text) {
33
+ this.language = language;
34
+ this.text = text;
35
+ }
36
+ }
37
+ export class ViewFields {
38
+ fields;
39
+ kind = "fields";
40
+ constructor(fields) {
41
+ this.fields = fields;
42
+ }
43
+ }
44
+ /** A plain text. */
45
+ export function viewText(text) {
46
+ return new ViewText(text);
47
+ }
48
+ /** The JSON text of a value. A string shows as a JSON string, with its quotes. */
49
+ export function viewJson(value) {
50
+ return new ViewJson(value);
51
+ }
52
+ /** Source code. `language` has 1 to 20 characters of lowercase letters, digits, `+`, `#`, `.`, and `-`. */
53
+ export function viewCode(language, text) {
54
+ return new ViewCode(language, text);
55
+ }
56
+ /**
57
+ * An ordered list of named values, at most 4. A name has 1 to 40 characters of letters, digits, `_`, and `-`. A field
58
+ * whose leaf is `undefined` is left out. The order is the order of the keys, except that a name made of digits only
59
+ * goes first, as a JavaScript object orders such a key.
60
+ */
61
+ export function viewFields(leaves) {
62
+ const fields = [];
63
+ for (const [name, leaf] of Object.entries(leaves)) {
64
+ if (leaf !== undefined)
65
+ fields.push({ name, leaf });
66
+ }
67
+ return new ViewFields(fields);
68
+ }
@@ -153,6 +153,11 @@ export function createEditFileTool(deps) {
153
153
  "`invalid_pattern` / `invalid_arguments` data variants — never throws on expected outcomes. " +
154
154
  "A data variant other than `ok` writes nothing.",
155
155
  inputSchema: EditFileInputSchema,
156
+ view: {
157
+ input: (input) => input,
158
+ result: (_input, result) => result,
159
+ error: true,
160
+ },
156
161
  // The path only. The replaced text is file content, not a description.
157
162
  describeCall: ({ path }) => path,
158
163
  execute: async ({ path, old_string, new_string, replace_all: replaceAll, regex: useRegex, expected_matches }, ctx) => {
@@ -75,6 +75,7 @@ export function createExecuteCommandTool(deps) {
75
75
  "so use them to confirm that a command ran. A result that you compute with 'python -c' or an inline " +
76
76
  "one-liner and read from stdout is lost. Write a script and persist its outputs to output/.",
77
77
  inputSchema: ExecuteCommandInputSchema,
78
+ view: "none",
78
79
  // The script is what a reader recognises, and it survives a long
79
80
  // invocation whose flags would otherwise push it past the length cap.
80
81
  // When nothing in the argv looks like a script, the joined argv is the
@@ -83,8 +84,10 @@ export function createExecuteCommandTool(deps) {
83
84
  // `env` never rides along. It is the channel this tool documents for
84
85
  // secrets, so it is the one field guaranteed to hold them. An argv can
85
86
  // hold one too (`--password …` is a real habit), and the emit-site
86
- // redaction only matches the structured, prefixed formats — so the join
87
- // is a deliberate trade, not a claim of safety.
87
+ // redaction removes only the secret forms that it lists, so the join
88
+ // is a deliberate trade, not a claim of safety. The view is `"none"` for
89
+ // the same reason: the argv, the env, and the output of a command show
90
+ // no value.
88
91
  //
89
92
  // Know what the trade costs. This tool is sandbox-gated, so the only
90
93
  // surface that reads its detail is a workflow's live-activity line — and
@@ -22,6 +22,11 @@ export function createFileStatTool(fs, workingDir) {
22
22
  "in bytes. It reads the workspace directly and runs no sandbox command. Use before reading to size a file. Missing and out-of-scope " +
23
23
  "paths return a data variant, not an error.",
24
24
  inputSchema: FileStatInputSchema,
25
+ view: {
26
+ input: (input) => input,
27
+ result: (_input, result) => result,
28
+ error: true,
29
+ },
25
30
  describeCall: "none",
26
31
  execute: async ({ path }, ctx) => {
27
32
  const result = unwrapOrThrow(await fs.stat({ session: ctx.session, path, workingDir }));
@@ -45,6 +45,10 @@ export function createGrepTool(fs, workingDir, lineage) {
45
45
  "in its first KiB is skipped as binary. A `truncated` status means the match cap or the file cap cut the search; " +
46
46
  "narrow your path or pattern. No matches, missing paths, and out-of-scope paths return data variants, not errors.",
47
47
  inputSchema: GrepInputSchema,
48
+ view: {
49
+ input: (input) => input,
50
+ error: true,
51
+ },
48
52
  // Both fields, because either alone loses the call: the same pattern over
49
53
  // two trees and two patterns over one tree are both ordinary sequences.
50
54
  describeCall: ({ pattern, path }) => `${pattern} in ${path}`,
@@ -24,6 +24,11 @@ export function createListFilesTool(fs, workingDir) {
24
24
  "else: it never reaches a directory outside the analysis, including the host process's current " +
25
25
  "working directory. Missing and out-of-scope paths return a data variant, not an error.",
26
26
  inputSchema: ListFilesInputSchema,
27
+ view: {
28
+ input: (input) => input,
29
+ result: (_input, result) => result,
30
+ error: true,
31
+ },
27
32
  // The directory IS the call — two listings differ by nothing else.
28
33
  describeCall: ({ path }) => path,
29
34
  execute: async ({ path }, ctx) => {
@@ -69,6 +69,10 @@ export function createReadFileTool(fs, workingDir, lineage) {
69
69
  "back as a `binary` data variant rather than decoded garbage. Missing " +
70
70
  "paths and paths outside the analysis tree return a data variant — not an error.",
71
71
  inputSchema: ReadFileInputSchema,
72
+ view: {
73
+ input: (input) => input,
74
+ error: true,
75
+ },
72
76
  describeCall: ({ path, headLines, tailLines }) => {
73
77
  if (headLines !== undefined)
74
78
  return `${path} (first ${headLines} lines)`;
@@ -44,6 +44,11 @@ export function createScanInputsTool(deps) {
44
44
  "or to check a grouping you are unsure about. " +
45
45
  "It reports observations only: which files are one KIND of data, and what a varying position MEANS, are yours to decide.",
46
46
  inputSchema: ScanInputsInputSchema,
47
+ view: {
48
+ input: (input) => input,
49
+ result: (_input, result) => result,
50
+ error: true,
51
+ },
47
52
  describeCall: ({ path }) => path,
48
53
  execute: async ({ path }, ctx) => {
49
54
  const scan = await scanInputTree({
@@ -42,6 +42,7 @@ export const showFileTool = defineTool({
42
42
  "calls in one batch, in reading order: a section, then the `show_file` for its figures, then the next section " +
43
43
  "(markdown image syntax pointing at a workspace file does NOT render — this tool is the only way to show one).",
44
44
  inputSchema: ShowFileInputSchema,
45
+ view: "none",
45
46
  // `.min(1)` on `files` makes index 0 total after the parse.
46
47
  describeCall: ({ files }) => (files.length === 1 ? presentFirst(files, "the shown file").path : `${files.length} files`),
47
48
  execute: async (input, ctx) => {
@@ -34,6 +34,7 @@ export function createShowPlanTool(pool) {
34
34
  "card — that renders a second, divergent copy of the same plan. " +
35
35
  "`shown: false` with `plan_not_found` means that the planId is no longer valid: generate a new plan, because a retry of the same planId fails the same way.",
36
36
  inputSchema: ShowPlanInputSchema,
37
+ view: "none",
37
38
  describeCall: ({ planId }) => planId,
38
39
  execute: async ({ planId, title }, ctx) => {
39
40
  const resourceId = scopeResource(ctx.session.scope).resourceId;
@@ -89,6 +89,11 @@ export function createWorkspaceSearchTool(pool, embedding) {
89
89
  "its members. Most input files carry no entry of their own — find them through their " +
90
90
  "group and the listing tools.",
91
91
  inputSchema: WorkspaceSearchInputSchema,
92
+ view: {
93
+ input: (input) => input,
94
+ result: (_input, result) => result,
95
+ error: true,
96
+ },
92
97
  describeCall: ({ query, type }) => (type === undefined ? query : `${query} (${type})`),
93
98
  execute: async ({ query, type, limit }, ctx) => {
94
99
  const [vector] = unwrapOrThrow(await embedding.embed([query], ctx.session));
@@ -30,8 +30,13 @@ export function createWriteFileTool(deps) {
30
30
  "`out_of_scope`, and a symbolic link on the path returns `symlink_denied`. A path under the staged " +
31
31
  "inputs returns `read_only` with the reason.",
32
32
  inputSchema: WriteFileInputSchema,
33
- // The path only. `content` is a whole file and must never ride a display
34
- // channel, which the emit-site length cap enforces regardless.
33
+ view: {
34
+ input: (input) => input,
35
+ result: (_input, result) => result,
36
+ error: true,
37
+ },
38
+ // The path only: the detail is a one-line label, and `content` is a whole file. The view shows the
39
+ // content, and the cap of the emit site bounds it.
35
40
  describeCall: ({ path }) => path,
36
41
  execute: async ({ path, content }, ctx) => {
37
42
  const write = await deps.mutator.writeFile({
@@ -82,6 +82,7 @@ import { addChatUsage, hasReportedUsage } from "../loop/metrics.js";
82
82
  import { flattenDataPart } from "../contracts/chat-frame.js";
83
83
  import { SYNTHESIS_AGENT_ID, loadStepSummariesFromDisk } from "../execution/run-synthesis.js";
84
84
  import { MAX_UPSTREAM_ARTIFACTS, composeStepBriefing, emptyStepMemory, } from "../prompts/briefing.js";
85
+ import { withoutToolValues } from "../loop/tool-value.js";
85
86
  import { SYNTHESIS_STEP_ID, runDir, runStepDir, stepSubdir, stepWritePrefix, toSandboxPath } from "../workspace/paths.js";
86
87
  import { isChatDataPart, synthesisEmission } from "../sandbox/sandbox-step-translate.js";
87
88
  import { synthesizeRun } from "../app/synthesize-run.js";
@@ -516,7 +517,7 @@ export function synthesizeFindings(args) {
516
517
  ? flattenDataPart(event)
517
518
  : {
518
519
  type: "data-loop-event",
519
- data: { stepId: "synthesis", event },
520
+ data: { stepId: "synthesis", event: withoutToolValues(event) },
520
521
  };
521
522
  return DBOS.writeStream("events", part);
522
523
  };
@@ -38,9 +38,9 @@ import { unwrapOrThrow } from "../lib/result.js";
38
38
  import { maskExcept } from "../loop/call-policy.js";
39
39
  import { runLoop, withRunScript } from "../loop/run-loop.js";
40
40
  import { READ_TOOL_OUTPUT_TOOL_ID } from "../loop/tool-output.js";
41
+ import { withoutToolValues } from "../loop/tool-value.js";
41
42
  import { lastExecOutcome } from "../sandbox/exec-outcome.js";
42
43
  import { activityEmission, activityForTool, applyTreeDelta, isChatDataPart, sandboxTreeDelta, stepPartId, toolActivityKey, } from "../sandbox/sandbox-step-translate.js";
43
- import { createDetailResolver } from "../tools/detail-resolver.js";
44
44
  import { forSubAgent } from "../auth/types.js";
45
45
  import { walkStepArtifacts } from "../execution/post-step.js";
46
46
  import { collectStepOutputs, describeStepRegistrationFailure, generateStepFileMetadata, generateStepSummaryAndWrite, reconcileAndRegisterStepArtifacts, vectorIndexStepOutputs, } from "../execution/post-step-pipeline.js";
@@ -196,11 +196,7 @@ export async function runSandboxStepBody(input, deps) {
196
196
  if (deps.toolOutputStore !== undefined && !agent.tools.some((tool) => tool.id === READ_TOOL_OUTPUT_TOOL_ID)) {
197
197
  logger.warn("the step agent declares no read_tool_output, thus its loops keep no text of a cut tool result");
198
198
  }
199
- // Built over THIS agent's tools, not a shared table: a sandbox agent's roster
200
- // differs per agent type, and each tool describes its own calls.
201
- // The declared tools and `run_script` describe the calls of the agent, and its script tools describe the nested calls.
202
199
  const declared = withRunScript(agent).tools;
203
- const resolveDetail = createDetailResolver([...declared, ...(agent.scriptTools ?? [])], logger);
204
200
  // Forward every loop event onto the child's own DBOS `"events"` stream
205
201
  // (the route fans-in parent + every active child stream — Pattern A).
206
202
  // Pre-shaped chat data parts (e.g. `data-sandbox-event` from `run-exec.ts`)
@@ -306,9 +302,9 @@ export async function runSandboxStepBody(input, deps) {
306
302
  }
307
303
  // Forward the raw orchestration / model-delta event under the loop-event
308
304
  // envelope, and surface each tool call as live step activity.
309
- await safeEmit({ type: "data-loop-event", data: { stepId: input.stepId, event } });
305
+ await safeEmit({ type: "data-loop-event", data: { stepId: input.stepId, event: withoutToolValues(event) } });
310
306
  if (event.type === "tool-started") {
311
- await emitActivity("executing", activityForTool(event.name, event.input, resolveDetail), event.toolUseId);
307
+ await emitActivity("executing", activityForTool(event.name, event.detail), event.toolUseId);
312
308
  }
313
309
  };
314
310
  const initial = [{ role: "user", content: input.prompt }];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inflexa-ai/harness",
3
- "version": "0.48.1",
3
+ "version": "0.48.3",
4
4
  "description": "Host-agnostic bioinformatics agent harness — an agent loop on the AI SDK, DBOS-durable workflows, sandboxed compute.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,40 +0,0 @@
1
- /**
2
- * The name-plus-input detail resolver.
3
- *
4
- * The live loop holds the resolved `Tool` at dispatch and needs nothing. Every
5
- * other surface — the reload converter, a workflow's live-activity line — has a
6
- * tool NAME and a persisted input, so it needs a way back to the tool. This is
7
- * that way, and it is deliberately built over a tool list the CALLER supplies:
8
- *
9
- * - A name map internal to the harness could never see an embedder's tools. A
10
- * host contributes tools through the host-tools seam, and those calls are as
11
- * ambiguous in a transcript as any other.
12
- * - Two harness tools legitimately share one id (`write_file` exists in both the
13
- * workspace surface and the report version store). A resolver built from one
14
- * agent's list cannot hold both, because a tool list rejects a duplicate id at
15
- * registry construction.
16
- *
17
- * It resolves through the same `computeDetail` the loop uses, so a reloaded
18
- * thread shows the same CALL detail the live turn showed.
19
- *
20
- * The bound is the input. A reload reconstructs from the persisted call alone, and a
21
- * tool that describes its own RESULT names a fact that the input cannot carry — a page
22
- * path, a recorded version. That line reaches the live surface on the finished event and
23
- * it does not reappear here, thus a reloaded call falls back to its call detail. Nothing
24
- * is persisted: storage stays the pure model transcript.
25
- */
26
- import { type ToolCallDetail } from "../loop/tool-detail.js";
27
- import type { Logger } from "../lib/logger.js";
28
- import type { Tool } from "./define-tool.js";
29
- /**
30
- * Maps a tool name plus a call's input to that call's detail. Yields
31
- * `undefined` for an unknown tool, a tool with no hook, and an input the tool's
32
- * schema rejects.
33
- */
34
- export type ToolDetailResolver = (toolName: string, input: unknown) => ToolCallDetail | undefined;
35
- /**
36
- * Build a resolver over `tools`. A duplicate id resolves to the first tool
37
- * declaring it; callers pass one agent's composed list, which a registry has
38
- * already rejected duplicates from.
39
- */
40
- export declare function createDetailResolver(tools: readonly Tool[], logger?: Logger): ToolDetailResolver;
@@ -1,44 +0,0 @@
1
- /**
2
- * The name-plus-input detail resolver.
3
- *
4
- * The live loop holds the resolved `Tool` at dispatch and needs nothing. Every
5
- * other surface — the reload converter, a workflow's live-activity line — has a
6
- * tool NAME and a persisted input, so it needs a way back to the tool. This is
7
- * that way, and it is deliberately built over a tool list the CALLER supplies:
8
- *
9
- * - A name map internal to the harness could never see an embedder's tools. A
10
- * host contributes tools through the host-tools seam, and those calls are as
11
- * ambiguous in a transcript as any other.
12
- * - Two harness tools legitimately share one id (`write_file` exists in both the
13
- * workspace surface and the report version store). A resolver built from one
14
- * agent's list cannot hold both, because a tool list rejects a duplicate id at
15
- * registry construction.
16
- *
17
- * It resolves through the same `computeDetail` the loop uses, so a reloaded
18
- * thread shows the same CALL detail the live turn showed.
19
- *
20
- * The bound is the input. A reload reconstructs from the persisted call alone, and a
21
- * tool that describes its own RESULT names a fact that the input cannot carry — a page
22
- * path, a recorded version. That line reaches the live surface on the finished event and
23
- * it does not reappear here, thus a reloaded call falls back to its call detail. Nothing
24
- * is persisted: storage stays the pure model transcript.
25
- */
26
- import { computeDetail } from "../loop/tool-detail.js";
27
- import { createNoopLogger } from "../lib/console-logger.js";
28
- /**
29
- * Build a resolver over `tools`. A duplicate id resolves to the first tool
30
- * declaring it; callers pass one agent's composed list, which a registry has
31
- * already rejected duplicates from.
32
- */
33
- export function createDetailResolver(tools, logger) {
34
- const log = (logger ?? createNoopLogger()).named("tool-detail");
35
- const byId = new Map();
36
- for (const tool of tools) {
37
- if (!byId.has(tool.id))
38
- byId.set(tool.id, tool);
39
- }
40
- return (toolName, input) => {
41
- const tool = byId.get(toolName);
42
- return tool === undefined ? undefined : computeDetail(tool, input, log);
43
- };
44
- }