@inflexa-ai/harness 0.48.4 → 0.48.5

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.
@@ -20,7 +20,6 @@ import { z } from "zod";
20
20
  import { createSubAgentActivityTracker } from "../../contracts/subagent-activity.js";
21
21
  import { createNoopLogger } from "../../lib/console-logger.js";
22
22
  import { parseJsonAs } from "../../lib/parse-json.js";
23
- import { parseUnknownRecord, UnknownArraySchema } from "../../lib/unknown-containers.js";
24
23
  import { computeDetail, computeResultDetail } from "../../loop/tool-detail.js";
25
24
  import { buildFileReferenceCardData, MAX_FILES } from "../../memory/card-builders.js";
26
25
  import { defineTool, readToolCallRecord, readToolResultImages, withToolCallRecord, } from "../define-tool.js";
@@ -59,8 +58,8 @@ function bounded(text) {
59
58
  function plainJson(value) {
60
59
  return value === undefined ? null : parseJsonAs(JSON.stringify(value), z.json());
61
60
  }
62
- function describeTool(tool) {
63
- return renderToolSample({ name: tool.id, description: tool.description, inputSchema: inputSchemaOf(tool), outputSchema: tool.outputJsonSchema });
61
+ function describeTool(tool, schemas) {
62
+ return renderToolSample({ name: tool.id, description: tool.description, inputSchema: schemas.input, outputSchema: schemas.output });
64
63
  }
65
64
  function issuesOf(error) {
66
65
  return error.issues.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`).join("; ");
@@ -129,12 +128,12 @@ function collectEffect(state, value) {
129
128
  * The script gets the JSON of a result, and the JSON has no pictures. Thus a result with pictures is a failed call: the
130
129
  * script would otherwise read the result as complete.
131
130
  */
132
- function scriptToolOf(tool, ctx, state, log, inCallContext) {
131
+ function scriptToolOf(tool, schemas, ctx, state, log, inCallContext) {
133
132
  return {
134
133
  name: tool.id,
135
134
  description: tool.description,
136
- inputSchema: inputSchemaOf(tool),
137
- outputSchema: tool.outputJsonSchema,
135
+ inputSchema: schemas.input,
136
+ outputSchema: schemas.output,
138
137
  execute: (args, options) => inCallContext(() => runNestedCall(tool, ctx, state, log, args, options.signal)),
139
138
  };
140
139
  }
@@ -187,56 +186,54 @@ async function runNestedCall(tool, ctx, state, log, args, signal) {
187
186
  await finish("ok", computeResultDetail(tool, parsed.data, result.value, log) ?? detail);
188
187
  return plainJson(result.value);
189
188
  }
190
- /** A JSON schema without its prose: the types and the constraints stay, the descriptions and the examples go. */
191
- function withoutProse(schema) {
192
- const array = UnknownArraySchema.safeParse(schema);
193
- if (array.success)
194
- return array.data.map(withoutProse);
195
- const record = parseUnknownRecord(schema);
196
- if (record === undefined)
197
- return schema;
198
- return Object.fromEntries(Object.entries(record)
199
- .filter(([key]) => key !== "description" && key !== "examples")
200
- .map(([key, value]) => [key, withoutProse(value)]));
201
- }
202
189
  /**
203
- * A JSON schema as a script reads it. A tool can union its real shape with the empty schema to keep its runtime parse
204
- * permissive (`add_block` does, thus a malformed block reaches its typed refusal). The type of such a union is
205
- * `unknown`, which hides the real shape, thus the empty member goes. The VM only renders the schema and validates
206
- * nothing, and the tool still parses each input with its own schema.
190
+ * Shape one node of a JSON schema for a script. A tool can union its real shape with the empty schema to keep its
191
+ * runtime parse permissive (`add_block` does, thus a malformed block reaches its typed refusal). The type of such a
192
+ * union is `unknown`, which hides the real shape, thus the empty member goes. The VM only renders the schema and
193
+ * validates nothing, and the tool still parses each input with its own schema. Without prose, the `description` and the
194
+ * `examples` of the node go too. Zod calls this for each schema node and never for a field name, thus a field with the
195
+ * name `description` stays.
207
196
  */
208
- function printableSchema(schema) {
209
- const array = UnknownArraySchema.safeParse(schema);
210
- if (array.success)
211
- return array.data.map(printableSchema);
212
- const record = parseUnknownRecord(schema);
213
- if (record === undefined)
214
- return schema;
215
- let out = Object.fromEntries(Object.entries(record).map(([key, value]) => [key, printableSchema(value)]));
216
- for (const key of ["anyOf", "oneOf"]) {
217
- const members = UnknownArraySchema.safeParse(out[key]);
218
- if (!members.success)
219
- continue;
220
- const shaped = members.data.filter((member) => Object.keys(parseUnknownRecord(member) ?? {}).length > 0);
221
- if (shaped.length === 0 || shaped.length === members.data.length)
222
- continue;
223
- const rest = Object.fromEntries(Object.entries(out).filter(([name]) => name !== key));
224
- out = shaped.length > 1 ? { ...rest, [key]: shaped } : { ...rest, ...parseUnknownRecord(shaped[0]) };
197
+ function shapeForScript(jsonSchema, prose) {
198
+ if (!prose) {
199
+ delete jsonSchema.description;
200
+ delete jsonSchema.examples;
201
+ }
202
+ const anyOf = shapedMembers(jsonSchema.anyOf);
203
+ if (anyOf !== undefined) {
204
+ delete jsonSchema.anyOf;
205
+ Object.assign(jsonSchema, anyOf.length > 1 ? { anyOf } : anyOf[0]);
225
206
  }
226
- return out;
207
+ const oneOf = shapedMembers(jsonSchema.oneOf);
208
+ if (oneOf !== undefined) {
209
+ delete jsonSchema.oneOf;
210
+ Object.assign(jsonSchema, oneOf.length > 1 ? { oneOf } : oneOf[0]);
211
+ }
212
+ }
213
+ /** The members of a union that are not the empty schema, or `undefined` when the union keeps each member. */
214
+ function shapedMembers(members) {
215
+ if (members === undefined)
216
+ return undefined;
217
+ const shaped = members.filter((member) => Object.keys(member).length > 0);
218
+ return shaped.length === 0 || shaped.length === members.length ? undefined : shaped;
227
219
  }
228
- /** The input schema of a tool as a script reads it. See {@link printableSchema}. */
229
- function inputSchemaOf(tool) {
230
- return parseUnknownRecord(printableSchema(tool.jsonSchema)) ?? tool.jsonSchema;
220
+ /** The schemas of a tool as a script reads them, built with the same options as the schemas of {@link defineTool}. */
221
+ function scriptSchemasOf(tool, prose) {
222
+ const override = ({ jsonSchema }) => shapeForScript(jsonSchema, prose);
223
+ const { $schema: _inputDialect, ...input } = z.toJSONSchema(tool.inputSchema, { override });
224
+ if (tool.outputSchema === undefined)
225
+ return { input };
226
+ const { $schema: _outputDialect, ...output } = z.toJSONSchema(tool.outputSchema, { io: "output", unrepresentable: "any", override });
227
+ return { input, output };
231
228
  }
232
229
  /** The longest result type that the catalog shows. A longer type shows as `unknown`, and `describe()` gives it whole. */
233
230
  const MAX_CATALOG_RESULT_TYPE_CHARS = 800;
234
231
  /** One catalog entry: the first sentence of the description, then the signature with no prose. */
235
232
  function catalogEntry(tool) {
236
- const output = tool.outputJsonSchema === undefined ? undefined : parseUnknownRecord(withoutProse(tool.outputJsonSchema));
233
+ const { input, output } = scriptSchemasOf(tool, false);
237
234
  const resultType = output === undefined ? "unknown" : schemaToType(output, { maxChars: MAX_CATALOG_RESULT_TYPE_CHARS });
238
235
  // pi renders a signature with no result schema as `...: Promise<unknown>;`, thus the catalog puts its capped type there.
239
- const untyped = renderToolSignature({ name: tool.id, inputSchema: parseUnknownRecord(withoutProse(inputSchemaOf(tool))) ?? {} });
236
+ const untyped = renderToolSignature({ name: tool.id, inputSchema: input });
240
237
  const signature = `${untyped.slice(0, -"unknown>;".length)}${resultType}>;`;
241
238
  return `/** ${firstSentence(tool.description)} */\n${signature}`;
242
239
  }
@@ -290,7 +287,7 @@ function scriptResultView(result) {
290
287
  /** The `run_script` tool over the script tools of an agent. */
291
288
  export function createRunScriptTool(scriptTools, runtime = {}, options = {}) {
292
289
  const vm = sandboxRuntimeOf(runtime);
293
- const byId = new Map(scriptTools.map((tool) => [tool.id, tool]));
290
+ const byId = new Map(scriptTools.map((tool) => [tool.id, { tool, schemas: scriptSchemasOf(tool, true) }]));
294
291
  const log = (options.logger ?? createNoopLogger()).named("run-script");
295
292
  const { scratch } = options;
296
293
  return defineTool({
@@ -312,7 +309,7 @@ export function createRunScriptTool(scriptTools, runtime = {}, options = {}) {
312
309
  scratch?.recordRun(record.run);
313
310
  for (const entry of record.nested) {
314
311
  try {
315
- byId.get(entry.tool)?.foldCallRecord?.(entry.record);
312
+ byId.get(entry.tool)?.tool.foldCallRecord?.(entry.record);
316
313
  }
317
314
  catch (err) {
318
315
  log.error("the call record of a nested call did not fold", { tool: entry.tool, ...log.errorFields(err) });
@@ -356,15 +353,15 @@ export function createRunScriptTool(scriptTools, runtime = {}, options = {}) {
356
353
  // example the durable step of the call, which DBOS keeps in an AsyncLocalStorage.
357
354
  const inCallContext = AsyncLocalStorage.snapshot();
358
355
  const sandbox = new CodemodeSandbox({
359
- tools: scriptTools.map((tool) => scriptToolOf(tool, ctx, state, log, inCallContext)),
356
+ tools: [...byId.values()].map(({ tool, schemas }) => scriptToolOf(tool, schemas, ctx, state, log, inCallContext)),
360
357
  globals: [
361
358
  {
362
359
  name: "describe",
363
360
  execute: (name) => {
364
- const tool = typeof name === "string" ? byId.get(name) : undefined;
365
- if (tool === undefined)
361
+ const entry = typeof name === "string" ? byId.get(name) : undefined;
362
+ if (entry === undefined)
366
363
  throw new Error(`no script tool ${JSON.stringify(name)}; the tools are ${[...byId.keys()].join(", ")}`);
367
- return describeTool(tool);
364
+ return describeTool(entry.tool, entry.schemas);
368
365
  },
369
366
  },
370
367
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inflexa-ai/harness",
3
- "version": "0.48.4",
3
+ "version": "0.48.5",
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",