@inflexa-ai/harness 0.19.0 → 0.19.1

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 (64) hide show
  1. package/dist/agents/report-session-agent.d.ts +25 -9
  2. package/dist/agents/report-session-agent.js +21 -6
  3. package/dist/app/report-session-runtime.js +26 -0
  4. package/dist/app/spawn-report-session.d.ts +32 -5
  5. package/dist/app/spawn-report-session.js +19 -0
  6. package/dist/config/environment-stores.d.ts +12 -7
  7. package/dist/config/resource-limits.d.ts +6 -10
  8. package/dist/config/resource-limits.js +6 -44
  9. package/dist/contracts/report-reference.d.ts +4 -0
  10. package/dist/contracts/report-reference.js +3 -0
  11. package/dist/index.d.ts +2 -1
  12. package/dist/lib/chrome.d.ts +14 -0
  13. package/dist/lib/chrome.js +7 -0
  14. package/dist/lib/page-capture.d.ts +47 -0
  15. package/dist/lib/page-capture.js +83 -0
  16. package/dist/loop/run-agent.js +104 -15
  17. package/dist/memory/ai-sdk-message-storage.d.ts +9 -4
  18. package/dist/memory/ai-sdk-message-storage.js +9 -4
  19. package/dist/memory/count-tokens.js +34 -5
  20. package/dist/prompts/report-session.d.ts +1 -1
  21. package/dist/prompts/report-session.js +21 -0
  22. package/dist/providers/ai-sdk.d.ts +81 -3
  23. package/dist/providers/ai-sdk.js +91 -10
  24. package/dist/providers/types.d.ts +17 -0
  25. package/dist/report-model/assert-rules.d.ts +44 -0
  26. package/dist/report-model/assert-rules.js +97 -0
  27. package/dist/report-model/draft-hash.d.ts +14 -0
  28. package/dist/report-model/draft-hash.js +36 -0
  29. package/dist/report-model/fixture-resolver.js +2 -71
  30. package/dist/report-model/production-resolver.d.ts +124 -0
  31. package/dist/report-model/production-resolver.js +801 -0
  32. package/dist/report-model/reference-resolver.d.ts +6 -0
  33. package/dist/report-model/validate.js +5 -0
  34. package/dist/report-render/page.d.ts +23 -0
  35. package/dist/report-render/page.js +36 -3
  36. package/dist/runtime/assemble.d.ts +28 -4
  37. package/dist/runtime/assemble.js +42 -3
  38. package/dist/sandbox/mount-plan.d.ts +7 -0
  39. package/dist/sandbox/mount-plan.js +7 -2
  40. package/dist/state/init.js +20 -7
  41. package/dist/state/report-session-state.d.ts +27 -0
  42. package/dist/state/report-session-state.js +29 -2
  43. package/dist/tasks/extract-values-script.d.ts +64 -0
  44. package/dist/tasks/extract-values-script.js +180 -0
  45. package/dist/tasks/extract-values.d.ts +124 -0
  46. package/dist/tasks/extract-values.js +193 -0
  47. package/dist/tools/define-tool.d.ts +29 -0
  48. package/dist/tools/define-tool.js +26 -0
  49. package/dist/tools/report/preview-snapshot.d.ts +1 -1
  50. package/dist/tools/report/preview-snapshot.js +11 -50
  51. package/dist/tools/report-authoring/authoring-tools.d.ts +54 -4
  52. package/dist/tools/report-authoring/authoring-tools.js +1 -1
  53. package/dist/tools/report-session/examine-page.d.ts +90 -0
  54. package/dist/tools/report-session/examine-page.js +136 -0
  55. package/dist/tools/report-session/index.d.ts +2 -0
  56. package/dist/tools/report-session/index.js +2 -0
  57. package/dist/tools/report-session/preview-report.d.ts +20 -5
  58. package/dist/tools/report-session/preview-report.js +28 -4
  59. package/dist/tools/report-session/record-version.d.ts +102 -0
  60. package/dist/tools/report-session/record-version.js +139 -0
  61. package/dist/tools/sandbox/list-available-packages.js +2 -1
  62. package/dist/tools/sandbox/list-available-refs.d.ts +5 -1
  63. package/dist/tools/sandbox/list-available-refs.js +13 -6
  64. package/package.json +10 -8
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { jsonSchema, tool as aiTool } from "ai";
2
+ import { jsonSchema, tool as aiTool, } from "ai";
3
3
  import { createNoopUsageRecorder } from "../billing/noop-usage-recorder.js";
4
4
  import { stripNulCharacters } from "../input-sanitization.js";
5
5
  import { createNoopLogger } from "../lib/console-logger.js";
@@ -9,7 +9,7 @@ import { classifyProviderError } from "../providers/errors.js";
9
9
  import { DEFAULT_PROMPT_CACHE, promptCacheProviderOptions } from "../providers/prompt-cache.js";
10
10
  import { resultStep } from "./run-step.js";
11
11
  import { AskRejectedError, UnavailableAsk } from "../tools/approval/contract.js";
12
- import { isToolError } from "../tools/define-tool.js";
12
+ import { isToolError, readToolResultImage } from "../tools/define-tool.js";
13
13
  import { addChatUsage, hasReportedUsage, recordAgentRun } from "./metrics.js";
14
14
  import { computeDetail } from "./tool-detail.js";
15
15
  import { toolOutcomeForOutputType } from "./tool-outcome.js";
@@ -40,6 +40,10 @@ export async function runAgent(agent, initial, session, opts) {
40
40
  // `callPath` rides as an array rather than a joined string — the queryable form;
41
41
  // rendering `parent > child` is the sink's choice, not a published package's.
42
42
  const log = (opts.logger ?? createNoopLogger()).named("loop").with({ agentId: source.agentId, callPath: source.callPath });
43
+ // The picture of a tool result rides where the wire renders it. Absent flags
44
+ // mean "cannot carry", thus the loop degrades to text and records the drop.
45
+ // The collector belongs to this run, thus two loops never share one.
46
+ const encoding = { placement: imagePlacementFor(provider.capabilities), deferredImages: [], log };
43
47
  const toolsById = new Map(agent.tools.map((t) => [t.id, t]));
44
48
  const toolDefs = Object.fromEntries(agent.tools.map((t) => [
45
49
  t.id,
@@ -238,7 +242,7 @@ export async function runAgent(agent, initial, session, opts) {
238
242
  for (const [idx, tu] of earlier.entries()) {
239
243
  await emit({ type: "tool-started", source, toolUseId: tu.toolCallId, name: tu.toolName, input: tu.input, ...detailField(earlierDetails[idx]) });
240
244
  }
241
- const { results, durations } = await dispatchTools(earlier, toolsById, toolCtx, isFatalLoopError, runStep, formatStepName.tool);
245
+ const { results, durations } = await dispatchTools(earlier, toolsById, toolCtx, isFatalLoopError, runStep, formatStepName.tool, encoding);
242
246
  const errored = await settleRound(earlier, results, earlierDetails, durations);
243
247
  results.push(errorResult(trailing, TRUNCATED_TOOL_USE_ERROR));
244
248
  // The trailing call was never dispatched, but it reaches the model as an error
@@ -250,6 +254,7 @@ export async function runAgent(agent, initial, session, opts) {
250
254
  errored.push(trailing.toolName);
251
255
  log.debug("tool results returned errors", { iteration: i, tools: errored });
252
256
  messages.push({ role: "tool", content: results });
257
+ appendDeferredImages(messages, results, encoding.deferredImages);
253
258
  if (hasDenial(results))
254
259
  return stopOnDenial(i);
255
260
  if (opts.resolved?.())
@@ -268,11 +273,12 @@ export async function runAgent(agent, initial, session, opts) {
268
273
  for (const [idx, tu] of toolCalls.entries()) {
269
274
  await emit({ type: "tool-started", source, toolUseId: tu.toolCallId, name: tu.toolName, input: tu.input, ...detailField(details[idx]) });
270
275
  }
271
- const { results, durations } = await dispatchTools(toolCalls, toolsById, toolCtx, isFatalLoopError, runStep, formatStepName.tool);
276
+ const { results, durations } = await dispatchTools(toolCalls, toolsById, toolCtx, isFatalLoopError, runStep, formatStepName.tool, encoding);
272
277
  const errored = await settleRound(toolCalls, results, details, durations);
273
278
  if (errored.length > 0)
274
279
  log.debug("tool results returned errors", { iteration: i, tools: errored });
275
280
  messages.push({ role: "tool", content: results });
281
+ appendDeferredImages(messages, results, encoding.deferredImages);
276
282
  if (hasDenial(results))
277
283
  return stopOnDenial(i);
278
284
  if (opts.resolved?.())
@@ -385,6 +391,50 @@ function markLastLoopAssistant(messages, initialCount) {
385
391
  }
386
392
  }
387
393
  }
394
+ /**
395
+ * Where a tool picture goes for this wire. The tool result wins, because it is
396
+ * the native place and the correlation to the tool call stays implicit. The user
397
+ * message is the fallback. When the wire renders neither, the loop drops the
398
+ * picture.
399
+ */
400
+ function imagePlacementFor(capabilities) {
401
+ if (capabilities.imageToolResults === true)
402
+ return "tool-result";
403
+ if (capabilities.imageUserMessages === true)
404
+ return "user-message";
405
+ return "drop";
406
+ }
407
+ /**
408
+ * Append one user message that carries each deferred picture of the round, then
409
+ * empty the collector.
410
+ *
411
+ * The tool message must come directly after the assistant message with the
412
+ * tool calls. Thus a picture rides a separate message after the whole tool
413
+ * message, and one message batches the round. The parts obey the order of
414
+ * `results`, which is the order of the tool calls. Each deferred picture has a
415
+ * result of this round, because the collector fills during the dispatch and it
416
+ * empties here. A text part names the tool call of each picture, because the wire
417
+ * holds no structural link between a user message and a tool call.
418
+ *
419
+ * The message carries the synthetic marker, because a `user` message opens a
420
+ * conversation turn. An unmarked one is loop machinery that reads as a turn
421
+ * boundary, and it splits one stored turn in two.
422
+ */
423
+ function appendDeferredImages(messages, results, deferredImages) {
424
+ if (deferredImages.length === 0)
425
+ return;
426
+ const byToolCallId = new Map(deferredImages.map((deferred) => [deferred.toolCallId, deferred]));
427
+ const content = [];
428
+ for (const result of results) {
429
+ const deferred = byToolCallId.get(result.toolCallId);
430
+ if (deferred === undefined)
431
+ continue;
432
+ content.push({ type: "text", text: `The picture of the tool result ${deferred.toolCallId} of ${deferred.toolName}.` });
433
+ content.push({ type: "file", mediaType: deferred.image.mediaType, data: { type: "data", data: deferred.image.base64 } });
434
+ }
435
+ messages.push(syntheticUserMessage(content));
436
+ deferredImages.length = 0;
437
+ }
388
438
  /**
389
439
  * Dispatch one round of tool calls, and measure the time of each call.
390
440
  *
@@ -395,7 +445,7 @@ function markLastLoopAssistant(messages, initialCount) {
395
445
  * wrapper. The wrapper is part of what the call cost, and a cached replay of a step
396
446
  * is genuinely fast. A bracket inside the step would report a body that did not run.
397
447
  */
398
- async function dispatchTools(toolUses, toolsById, toolCtx, isFatalLoopError, runStep, toolStepName) {
448
+ async function dispatchTools(toolUses, toolsById, toolCtx, isFatalLoopError, runStep, toolStepName, encoding) {
399
449
  const results = new Array(toolUses.length);
400
450
  // The array starts with holes, which read as `undefined`. The element type
401
451
  // says so, thus it agrees with what `settleRound` accepts. Every index is in
@@ -415,19 +465,19 @@ async function dispatchTools(toolUses, toolsById, toolCtx, isFatalLoopError, run
415
465
  }
416
466
  await Promise.all(stepTools.map(({ tu, idx }) => {
417
467
  const startedAt = performance.now();
418
- return runStep(toolStepName(tu.toolName, tu.toolCallId), () => dispatchTool(tu, toolsById, toolCtx(tu), isFatalLoopError)).then((r) => {
468
+ return runStep(toolStepName(tu.toolName, tu.toolCallId), () => dispatchTool(tu, toolsById, toolCtx(tu), isFatalLoopError, encoding)).then((r) => {
419
469
  durations[idx] = elapsedMs(startedAt);
420
470
  results[idx] = r;
421
471
  });
422
472
  }));
423
473
  for (const { tu, idx } of workflowTools) {
424
474
  const startedAt = performance.now();
425
- results[idx] = await dispatchTool(tu, toolsById, toolCtx(tu), isFatalLoopError);
475
+ results[idx] = await dispatchTool(tu, toolsById, toolCtx(tu), isFatalLoopError, encoding);
426
476
  durations[idx] = elapsedMs(startedAt);
427
477
  }
428
478
  for (const { tu, idx } of inlineTools) {
429
479
  const startedAt = performance.now();
430
- results[idx] = await dispatchTool(tu, toolsById, toolCtx(tu), isFatalLoopError);
480
+ results[idx] = await dispatchTool(tu, toolsById, toolCtx(tu), isFatalLoopError, encoding);
431
481
  durations[idx] = elapsedMs(startedAt);
432
482
  }
433
483
  return { results, durations };
@@ -436,14 +486,14 @@ async function dispatchTools(toolUses, toolsById, toolCtx, isFatalLoopError, run
436
486
  function elapsedMs(startedAt) {
437
487
  return Math.round(performance.now() - startedAt);
438
488
  }
439
- async function dispatchTool(tu, toolsById, ctx, isFatalLoopError) {
489
+ async function dispatchTool(tu, toolsById, ctx, isFatalLoopError, encoding) {
440
490
  const tool = toolsById.get(tu.toolName);
441
491
  if (tool === undefined) {
442
492
  return errorResult(tu, `unknown tool: ${tu.toolName}`);
443
493
  }
444
494
  const parsed = tool.inputSchema.safeParse(tu.input);
445
495
  if (parsed.success)
446
- return execute(tu, tool, parsed.data, ctx, isFatalLoopError);
496
+ return execute(tu, tool, parsed.data, ctx, isFatalLoopError, encoding);
447
497
  // A complete JSON argument can arrive as a string behind function-call
448
498
  // markup or a code fence. Repair only makes the schema reachable — the
449
499
  // repaired value is validated in full, and the tool's own semantic checks
@@ -451,15 +501,15 @@ async function dispatchTool(tu, toolsById, ctx, isFatalLoopError) {
451
501
  const repairedInput = repairToolInput(tu.input, parsed.error);
452
502
  const repaired = repairedInput === undefined ? undefined : tool.inputSchema.safeParse(repairedInput);
453
503
  if (repaired?.success === true)
454
- return execute(tu, tool, repaired.data, ctx, isFatalLoopError);
504
+ return execute(tu, tool, repaired.data, ctx, isFatalLoopError, encoding);
455
505
  return errorResult(tu, `input validation failed: ${formatZodIssues(parsed.error, tu.input)}`);
456
506
  }
457
- async function execute(tu, tool, input, ctx, isFatalLoopError) {
507
+ async function execute(tu, tool, input, ctx, isFatalLoopError, encoding) {
458
508
  try {
459
509
  const output = await tool.execute(input, ctx);
460
510
  if (output.isErr())
461
511
  return errorResult(tu, toolErrorContent(output.error));
462
- return successResult(tu, output.value);
512
+ return successResult(tu, output.value, encoding);
463
513
  }
464
514
  catch (err) {
465
515
  if (isFatalLoopError(err))
@@ -478,12 +528,51 @@ async function execute(tu, tool, input, ctx, isFatalLoopError) {
478
528
  function jsonValue(value) {
479
529
  return JSON.parse(JSON.stringify(value ?? null), (_key, v) => (typeof v === "string" ? stripNulCharacters(v) : v));
480
530
  }
481
- function successResult(toolCall, value) {
531
+ /**
532
+ * Encode a tool ok value onto a tool-result message. A value with no picture
533
+ * stays a plain JSON result, byte-identical to a value that never carried one.
534
+ *
535
+ * A value that carries a picture splits: the JSON data goes to a text part, and
536
+ * the bytes ride as an image content block. Thus the model sees the picture, and
537
+ * the JSON text holds no bytes. When the wire carries no picture, the block drops
538
+ * and the text stays, because base64 text floods the context and the model cannot
539
+ * see it. The drop rides the log, thus an operator sees that the picture did not
540
+ * reach the model.
541
+ *
542
+ * A wire that renders a picture on a user message only gets the fallback: the
543
+ * result keeps its JSON text, and the picture goes to the collector of the round.
544
+ * The round then sends the bytes after the tool message.
545
+ */
546
+ function successResult(toolCall, value, encoding) {
547
+ const jsonData = jsonValue(value);
548
+ const jsonOnly = {
549
+ type: "tool-result",
550
+ toolCallId: toolCall.toolCallId,
551
+ toolName: toolCall.toolName,
552
+ output: { type: "json", value: jsonData },
553
+ };
554
+ const image = readToolResultImage(value);
555
+ if (image === undefined)
556
+ return jsonOnly;
557
+ if (encoding.placement === "drop") {
558
+ encoding.log.warn("the wired provider carries no picture in a tool result, thus the picture is dropped", { toolName: toolCall.toolName });
559
+ return jsonOnly;
560
+ }
561
+ if (encoding.placement === "user-message") {
562
+ encoding.deferredImages.push({ toolCallId: toolCall.toolCallId, toolName: toolCall.toolName, image });
563
+ return jsonOnly;
564
+ }
482
565
  return {
483
566
  type: "tool-result",
484
567
  toolCallId: toolCall.toolCallId,
485
568
  toolName: toolCall.toolName,
486
- output: { type: "json", value: jsonValue(value) },
569
+ output: {
570
+ type: "content",
571
+ value: [
572
+ { type: "text", text: JSON.stringify(jsonData) },
573
+ { type: "file", mediaType: image.mediaType, data: { type: "data", data: image.base64 } },
574
+ ],
575
+ },
487
576
  };
488
577
  }
489
578
  // NUL is stripped from the prose BEFORE it is serialized: past the stringify it
@@ -1,4 +1,4 @@
1
- import { type ModelMessage } from "ai";
1
+ import { type ModelMessage, type UserContent } from "ai";
2
2
  import { z } from "zod";
3
3
  export declare const SUPPORTED_AI_SDK_MAJOR = 7;
4
4
  export declare const StoredMessageEnvelopeSchema: z.ZodObject<{
@@ -50,8 +50,9 @@ export declare function markInterruptedMessage(message: ModelMessage): ModelMess
50
50
  /** Whether a client abort cut off this message's production (see {@link markInterruptedMessage}); an absent key reads as not interrupted. */
51
51
  export declare function isInterruptedMessage(message: ModelMessage): boolean;
52
52
  /**
53
- * Build a `user` message the LOOP synthesized rather than a human typing it — today, the nudge that
54
- * continues a reply the model truncated at its output-token limit.
53
+ * Build a `user` message the LOOP synthesized rather than a human typing it — the nudge that
54
+ * continues a reply the model truncated at its output-token limit, and the message that carries a
55
+ * tool picture that the tool result cannot carry.
55
56
  *
56
57
  * It has to carry the `user` role because the wire format requires a user turn after a truncated
57
58
  * assistant message, but it is not user input, and the difference is load-bearing: a `user` message is
@@ -59,11 +60,15 @@ export declare function isInterruptedMessage(message: ModelMessage): boolean;
59
60
  * splitting one turn into two for the token window and, worse, giving a tail-turn removal a cut point in
60
61
  * the middle of a turn. Marking it is what lets {@link isSyntheticUserMessage} keep those readers honest.
61
62
  *
63
+ * `content` takes prose or the parts of a user message, because a picture rides a file part and no
64
+ * string can carry one. The marker is the same either way: what makes a message synthetic is who wrote
65
+ * it, not what it holds.
66
+ *
62
67
  * This one is loop MACHINERY and is not rendered: showing it would put words on screen nobody typed.
63
68
  * An embedder recording out-of-band work — an analysis run's outcome — wants
64
69
  * {@link syntheticRecordMessage} instead, which is equally non-turn-opening but IS displayed.
65
70
  */
66
- export declare function syntheticUserMessage(text: string): ModelMessage;
71
+ export declare function syntheticUserMessage(content: UserContent): ModelMessage;
67
72
  /**
68
73
  * Build a record of out-of-band work for an embedder to append between turns — an analysis run's
69
74
  * outcome, which the model should read on its next turn but which nobody said.
@@ -73,8 +73,9 @@ export function isInterruptedMessage(message) {
73
73
  return message.providerOptions?.[HARNESS_PROVIDER_NAMESPACE]?.[INTERRUPTED_MESSAGE_KEY] === true;
74
74
  }
75
75
  /**
76
- * Build a `user` message the LOOP synthesized rather than a human typing it — today, the nudge that
77
- * continues a reply the model truncated at its output-token limit.
76
+ * Build a `user` message the LOOP synthesized rather than a human typing it — the nudge that
77
+ * continues a reply the model truncated at its output-token limit, and the message that carries a
78
+ * tool picture that the tool result cannot carry.
78
79
  *
79
80
  * It has to carry the `user` role because the wire format requires a user turn after a truncated
80
81
  * assistant message, but it is not user input, and the difference is load-bearing: a `user` message is
@@ -82,14 +83,18 @@ export function isInterruptedMessage(message) {
82
83
  * splitting one turn into two for the token window and, worse, giving a tail-turn removal a cut point in
83
84
  * the middle of a turn. Marking it is what lets {@link isSyntheticUserMessage} keep those readers honest.
84
85
  *
86
+ * `content` takes prose or the parts of a user message, because a picture rides a file part and no
87
+ * string can carry one. The marker is the same either way: what makes a message synthetic is who wrote
88
+ * it, not what it holds.
89
+ *
85
90
  * This one is loop MACHINERY and is not rendered: showing it would put words on screen nobody typed.
86
91
  * An embedder recording out-of-band work — an analysis run's outcome — wants
87
92
  * {@link syntheticRecordMessage} instead, which is equally non-turn-opening but IS displayed.
88
93
  */
89
- export function syntheticUserMessage(text) {
94
+ export function syntheticUserMessage(content) {
90
95
  return {
91
96
  role: "user",
92
- content: text,
97
+ content,
93
98
  providerOptions: { [HARNESS_PROVIDER_NAMESPACE]: { [SYNTHETIC_MESSAGE_KEY]: true } },
94
99
  };
95
100
  }
@@ -17,10 +17,39 @@ function enc() {
17
17
  return encoder;
18
18
  }
19
19
  /**
20
- * The token-bearing text of one content block. Text-carrying fields are
21
- * extracted directly; structural blocks (`tool_use` input, `tool_result`
22
- * payload) are JSON-stringified. A signed `thinking` block counts only its
23
- * reasoning text — the opaque `signature` is metadata, not prompt tokens.
20
+ * The token-bearing text of one tool-result output.
21
+ *
22
+ * A `content` output holds the text parts and the attachments side by side. Only
23
+ * the text is prompt text. A file rides the wire as an attachment, and the
24
+ * provider bills it at its own rate, thus its bytes count as `0` — the same rule
25
+ * as a top-level `file` block. A file that is stringified into the count inflates
26
+ * the row of the message by tens of thousands of tokens, and the load window then
27
+ * drops the true history.
28
+ *
29
+ * Each other output arm is plain JSON, thus it is stringified whole.
30
+ */
31
+ function toolResultText(output) {
32
+ if (typeof output !== "object" || output === null)
33
+ return JSON.stringify(output ?? {});
34
+ const out = output;
35
+ if (out.type !== "content" || !Array.isArray(out.value))
36
+ return JSON.stringify(out);
37
+ const texts = [];
38
+ for (const item of out.value) {
39
+ if (typeof item !== "object" || item === null)
40
+ continue;
41
+ const nested = item;
42
+ if (nested.type === "text" && typeof nested.text === "string")
43
+ texts.push(nested.text);
44
+ }
45
+ return texts.join(" ");
46
+ }
47
+ /**
48
+ * The token-bearing text of one content block. A text-carrying field is
49
+ * extracted directly. A `tool_use` input is JSON-stringified, and a
50
+ * `tool_result` output goes through {@link toolResultText}. A signed `thinking`
51
+ * block counts only its reasoning text, because the opaque `signature` is
52
+ * metadata and not prompt tokens.
24
53
  */
25
54
  function tokenizableText(block) {
26
55
  if (typeof block === "string")
@@ -40,7 +69,7 @@ function tokenizableText(block) {
40
69
  case "tool-call":
41
70
  return `${String(part.toolName ?? "")} ${JSON.stringify(part.input ?? {})}`;
42
71
  case "tool-result": {
43
- return `${String(part.toolName ?? "")} ${JSON.stringify(part.output ?? {})}`;
72
+ return `${String(part.toolName ?? "")} ${toolResultText(part.output)}`;
44
73
  }
45
74
  default:
46
75
  return JSON.stringify(part);
@@ -1 +1 @@
1
- export declare const reportSessionPrompt = "# Report Builder\n\nYou compose a scientific report for the user, one block at a time, and you ground\nevery claim in the analysis. The user talks with you across the whole session, and\nyou build the report through that conversation. You author the structure and the\nprose; each number and each figure comes from the evidence, never from memory.\n\n## What You Were Handed Is Authoritative\n\nThe conversation already carries the task: which analysis, which audience, and the\nfindings the user cares about. Start from what is in front of you. Reach for a read\ntool only where the context is thin, and reach in a targeted way:\n\n- `inspect_data_profile` \u2014 what the dataset is: its design, its organism, its\n per-file types. Read it when you must state a fact about the data itself.\n- `inspect_run` \u2014 what a run produced, and where its outputs are. Read it when you\n must ground a claim in a run's results.\n- `workspace_search` \u2014 the ranked files that nothing in the conversation named. It\n gives descriptions and metadata, not contents.\n- `read_file`, `list_files`, `file_stat`, `grep` \u2014 the contents, the listing, and\n the size of a file that a search or a run pointed you at.\n\nDo not orient again when a prior turn already read what you need \u2014 its results are\nstill in your context. To reach further is targeted, not a fresh sweep.\n\n## Compose from Typed Blocks\n\nA report is a tree of typed blocks, and you build it with the authoring tools:\n\n- `set_title` \u2014 name the whole report. A draft starts with none, and it needs one.\n- `add_block` \u2014 add one section, or one atom inside a section. You choose the id,\n and it is unique in the draft. A section holds atoms. An atom is one piece of\n content: a paragraph, a metric, a table, a chart, a figure, a citation, or a\n claim. The block schema of `add_block` gives every kind and its fields.\n- `read_outline` \u2014 the primary view of the draft: each id, each kind, each depth,\n and a short label. Read the outline, not the whole draft, to see where you are.\n- `read_block` \u2014 one block in full, when its outline label is not enough.\n\nBuild the report as a shape first, then fill each section. Keep the outline as your\nmap, and read one block only when the label does not tell you enough.\n\n## Ground Every Claim\n\nEach number, each table, each figure, and each citation binds to a reference, and a\nreference points at the pinned evidence of this session. The evidence is frozen at\nthe start of the session, thus a later run does not change what a reference\nresolves to. Bind every value. Never transcribe a number that you remember.\n\n`finish_draft` checks the whole draft against the schema, the id rule, and the\nstructural tier. It returns each completeness gap, or the finished document. Read\nthe gaps, repair the draft, and finish again.\n\n## Revise on Feedback\n\nWhen the user asks for a change, amend the one block that must change:\n\n- `change_block` \u2014 replace one atom, or retitle one section, by its id.\n- `move_block` \u2014 move one block to a new place by its id.\n- `remove_block` \u2014 remove one block, and its subtree, by its id.\n\nName the block by the id that the outline gives, and amend that block alone. Do not\nrebuild the report to make one change.\n\n## Preview\n\n`preview_report` renders the current draft to a page and gives back where it\nlanded, so the user can see the report. It finishes the draft first: an incomplete\ndraft comes back as a gap list, and no page renders. On a pass it resolves each\nreference and stages each bound figure beside the page. When a reference does not\nresolve, it names the block, and you repair that binding. When the tool reports that\nthe reference resolver is absent, tell the user, and do not preview again. Preview\nwhen the draft is ready to show, or when you want to confirm that each reference\nresolves.\n\n## Do NOT\n\n- **Transcribe a number from memory.** Every metric, every table cell, and every\n figure binds to a reference that resolves against the pinned evidence.\n- **Start a run, or change the analysis.** You read the analysis; you never run it\n and never write to it. You hold no tool that does either, and that is by design.\n- **Invent a path.** Name a file by what a search or a run gave you. Never guess a\n location, and never hardcode one.\n- **Reach outside the pinned evidence.** A reference binds to the frozen snapshot\n of this session alone. Do not cite an artifact that the snapshot does not hold.\n- **Rebuild when one amend serves.** One feedback is one block change. Change, move,\n or remove that block by its id, and do not re-author the report.\n- **Leave a gap unread.** When `finish_draft` or `preview_report` reports a gap or\n an unresolved reference, repair it. Do not present a report that does not finish.\n";
1
+ export declare const reportSessionPrompt = "# Report Builder\n\nYou compose a scientific report for the user, one block at a time, and you ground\nevery claim in the analysis. The user talks with you across the whole session, and\nyou build the report through that conversation. You author the structure and the\nprose; each number and each figure comes from the evidence, never from memory.\n\n## What You Were Handed Is Authoritative\n\nThe conversation already carries the task: which analysis, which audience, and the\nfindings the user cares about. Start from what is in front of you. Reach for a read\ntool only where the context is thin, and reach in a targeted way:\n\n- `inspect_data_profile` \u2014 what the dataset is: its design, its organism, its\n per-file types. Read it when you must state a fact about the data itself.\n- `inspect_run` \u2014 what a run produced, and where its outputs are. Read it when you\n must ground a claim in a run's results.\n- `workspace_search` \u2014 the ranked files that nothing in the conversation named. It\n gives descriptions and metadata, not contents.\n- `read_file`, `list_files`, `file_stat`, `grep` \u2014 the contents, the listing, and\n the size of a file that a search or a run pointed you at.\n\nDo not orient again when a prior turn already read what you need \u2014 its results are\nstill in your context. To reach further is targeted, not a fresh sweep.\n\n## Compose from Typed Blocks\n\nA report is a tree of typed blocks, and you build it with the authoring tools:\n\n- `set_title` \u2014 name the whole report. A draft starts with none, and it needs one.\n- `add_block` \u2014 add one section, or one atom inside a section. You choose the id,\n and it is unique in the draft. A section holds atoms. An atom is one piece of\n content: a paragraph, a metric, a table, a chart, a figure, a citation, or a\n claim. The block schema of `add_block` gives every kind and its fields.\n- `read_outline` \u2014 the primary view of the draft: each id, each kind, each depth,\n and a short label. Read the outline, not the whole draft, to see where you are.\n- `read_block` \u2014 one block in full, when its outline label is not enough.\n\nBuild the report as a shape first, then fill each section. Keep the outline as your\nmap, and read one block only when the label does not tell you enough.\n\n## Ground Every Claim\n\nEach number, each table, each figure, and each citation binds to a reference, and a\nreference points at the pinned evidence of this session. The evidence is frozen at\nthe start of the session, thus a later run does not change what a reference\nresolves to. Bind every value. Never transcribe a number that you remember.\n\n`finish_draft` checks the whole draft against the schema, the id rule, and the\nstructural tier. It returns each completeness gap, or the finished document. Read\nthe gaps, repair the draft, and finish again.\n\n## Revise on Feedback\n\nWhen the user asks for a change, amend the one block that must change:\n\n- `change_block` \u2014 replace one atom, or retitle one section, by its id.\n- `move_block` \u2014 move one block to a new place by its id.\n- `remove_block` \u2014 remove one block, and its subtree, by its id.\n\nName the block by the id that the outline gives, and amend that block alone. Do not\nrebuild the report to make one change.\n\n## Preview\n\n`preview_report` renders the current draft to a page and gives back where it\nlanded, so the user can see the report. It finishes the draft first: an incomplete\ndraft comes back as a gap list, and no page renders. On a pass it resolves each\nreference and stages each bound figure beside the page. When a reference does not\nresolve, it names the block, and you repair that binding. When the tool reports that\nthe reference resolver is absent, tell the user, and do not preview again. Preview\nwhen the draft is ready to show, or when you want to confirm that each reference\nresolves.\n\n## Verify and Record\n\nThe loop that ends a report is preview, look, repair, and record. Run it in order:\n\n- `preview_report` renders the current draft to a page. The Preview section above\n gives its rules.\n- `examine_page` opens the rendered page in a real browser. It gives back a\n screenshot, the console errors, and the failed requests. Look at the page.\n- Repair each fault that the look shows. A broken layout, an absent chart, and a\n failed request each name a block or a binding that you repair.\n- `record_report_version` records the report as one version. It records only after\n you look at the current page. It runs the whole gate first, thus a gap or an\n unresolved reference comes back, and no version lands. A thread holds one version.\n\nLook again after each repair, because a repair changes the page. Record when the\npage reads clean.\n\n## Do NOT\n\n- **Transcribe a number from memory.** Every metric, every table cell, and every\n figure binds to a reference that resolves against the pinned evidence.\n- **Start a run, or change the analysis.** You read the analysis; you never run it\n and never write to it. You hold no tool that does either, and that is by design.\n- **Invent a path.** Name a file by what a search or a run gave you. Never guess a\n location, and never hardcode one.\n- **Reach outside the pinned evidence.** A reference binds to the frozen snapshot\n of this session alone. Do not cite an artifact that the snapshot does not hold.\n- **Rebuild when one amend serves.** One feedback is one block change. Change, move,\n or remove that block by its id, and do not re-author the report.\n- **Leave a gap unread.** When `finish_draft` or `preview_report` reports a gap or\n an unresolved reference, repair it. Do not present a report that does not finish.\n- **Spiral on a cosmetic doubt.** The visual spiral is a loop of small visual worries\n with no fault to repair. Look one time, then repair a real fault: a broken layout, an\n absent chart, or a failed request. A matter of taste is not a fault. When the page\n reads clean, record.\n";
@@ -72,6 +72,23 @@ the reference resolver is absent, tell the user, and do not preview again. Previ
72
72
  when the draft is ready to show, or when you want to confirm that each reference
73
73
  resolves.
74
74
 
75
+ ## Verify and Record
76
+
77
+ The loop that ends a report is preview, look, repair, and record. Run it in order:
78
+
79
+ - \`preview_report\` renders the current draft to a page. The Preview section above
80
+ gives its rules.
81
+ - \`examine_page\` opens the rendered page in a real browser. It gives back a
82
+ screenshot, the console errors, and the failed requests. Look at the page.
83
+ - Repair each fault that the look shows. A broken layout, an absent chart, and a
84
+ failed request each name a block or a binding that you repair.
85
+ - \`record_report_version\` records the report as one version. It records only after
86
+ you look at the current page. It runs the whole gate first, thus a gap or an
87
+ unresolved reference comes back, and no version lands. A thread holds one version.
88
+
89
+ Look again after each repair, because a repair changes the page. Record when the
90
+ page reads clean.
91
+
75
92
  ## Do NOT
76
93
 
77
94
  - **Transcribe a number from memory.** Every metric, every table cell, and every
@@ -86,4 +103,8 @@ resolves.
86
103
  or remove that block by its id, and do not re-author the report.
87
104
  - **Leave a gap unread.** When \`finish_draft\` or \`preview_report\` reports a gap or
88
105
  an unresolved reference, repair it. Do not present a report that does not finish.
106
+ - **Spiral on a cosmetic doubt.** The visual spiral is a loop of small visual worries
107
+ with no fault to repair. Look one time, then repair a real fault: a broken layout, an
108
+ absent chart, or a failed request. A matter of taste is not a fault. When the page
109
+ reads clean, record.
89
110
  `;
@@ -23,6 +23,11 @@ export declare const RETRY_MAX_DELAY_MS = 30000;
23
23
  *
24
24
  * Lower it per-config for servers that validate `prompt + max_tokens <= context`
25
25
  * (vLLM and similar) against a small-context model.
26
+ *
27
+ * The openai arm takes no value from this default. `@ai-sdk/openai` clamps
28
+ * nothing per model, and it puts the number on the wire as it is. Thus a model
29
+ * whose own cap is smaller answers 400 on each call. That arm sends no ceiling,
30
+ * and the server then enforces the true cap of the model.
26
31
  */
27
32
  export declare const DEFAULT_MAX_OUTPUT_TOKENS = 64000;
28
33
  export interface AiSdkProviderDeps {
@@ -30,8 +35,14 @@ export interface AiSdkProviderDeps {
30
35
  readonly resolveBilling: ResolveBilling;
31
36
  readonly capabilities?: Partial<ProviderCapabilities>;
32
37
  readonly logger?: Logger;
33
- /** Output-token ceiling per request. Defaults to {@link DEFAULT_MAX_OUTPUT_TOKENS}. */
34
- readonly maxOutputTokens?: number;
38
+ /**
39
+ * Output-token ceiling per request. Defaults to
40
+ * {@link DEFAULT_MAX_OUTPUT_TOKENS}. The value `"provider-maximum"` sends the
41
+ * SDK no ceiling, and the server then enforces the true cap of the model. Use
42
+ * the sentinel for a wire whose SDK puts the number on the request as it is,
43
+ * because a ceiling above the cap of the model fails the call.
44
+ */
45
+ readonly maxOutputTokens?: number | "provider-maximum";
35
46
  /** The retry limit of the harness envelope. Defaults to {@link RETRY_MAX_RETRIES}. */
36
47
  readonly maxRetries?: number;
37
48
  /**
@@ -45,7 +56,7 @@ export interface AiSdkProviderDeps {
45
56
  }
46
57
  /**
47
58
  * Connection + model configuration for one `ChatProvider`, discriminated by
48
- * wire protocol (`anthropic` | `openai-compatible`). The `model` is the wire
59
+ * wire protocol (`anthropic` | `openai` | `openai-compatible`). The `model` is the wire
49
60
  * model bound into the provider at construction: `ChatRequest` carries no model
50
61
  * field (see `./types.ts`), so a config value describes exactly one model over
51
62
  * one connection. An embedder running distinct models builds one provider per
@@ -83,6 +94,73 @@ export type AiSdkProviderConfig = {
83
94
  * keeps the default of 10.
84
95
  */
85
96
  readonly maxRetries?: number;
97
+ } | {
98
+ readonly kind: "openai";
99
+ readonly baseURL?: string;
100
+ readonly apiKey: string;
101
+ readonly model: string;
102
+ readonly fetch?: FetchLike;
103
+ readonly capabilities?: Partial<ProviderCapabilities>;
104
+ /**
105
+ * Output-token ceiling per request. An unset value sends no ceiling, and
106
+ * the model cap of the server governs the reply. The Responses wire holds
107
+ * no low default, thus the omission cannot truncate a reply. Set a number
108
+ * to hold the reply under a lower ceiling than the cap of the model.
109
+ */
110
+ readonly maxOutputTokens?: number;
111
+ /**
112
+ * The bound for each silent interval of a request attempt, in
113
+ * milliseconds. The value bounds the wait until the response starts, and
114
+ * each gap between two content chunks of a stream, per attempt. A chunk
115
+ * that carries no content, for example a keep-alive comment, does not
116
+ * reset the gap bound. When the field is absent, the provider installs no
117
+ * guard, and its behavior does not change.
118
+ *
119
+ * The harness lifts the transport floor of Bun: the guard adds
120
+ * `timeout: false` to the fetch init, thus the idle cut of 300 seconds
121
+ * does not apply. Under Node the floor of undici stays. Thus a Node
122
+ * embedder that sets a value above 300 seconds supplies a `fetch` whose
123
+ * dispatcher raises that limit.
124
+ */
125
+ readonly requestTimeoutMs?: number;
126
+ /**
127
+ * The retry limit of the harness envelope. The envelope uses this value
128
+ * in place of the default limit. When the field is absent, the envelope
129
+ * keeps the default of 10.
130
+ */
131
+ readonly maxRetries?: number;
132
+ /**
133
+ * NOTICE: this value is the retention directive of the Responses wire.
134
+ *
135
+ * An unset value lets the OpenAI server keep the response for 30 days.
136
+ * The dashboard logs show that response, and a caller can retrieve it by
137
+ * id. Thus the arm always sends an explicit value. The default is
138
+ * `false`, and an analysis payload does not persist at OpenAI.
139
+ *
140
+ * An unset value is also not neutral inside the SDK. When the SDK sees
141
+ * no explicit `false`, its input converter assumes storage. It then
142
+ * replaces a round-tripped item with an `item_reference` entry. Such a
143
+ * reference fails with a 404 under Zero Data Retention, and it fails
144
+ * after the 30-day expiry.
145
+ *
146
+ * An explicit `false` selects the stateless reasoning path of the SDK.
147
+ * The SDK adds `include: ["reasoning.encrypted_content"]` for a
148
+ * reasoning-family model id. It captures the blob on `providerMetadata`,
149
+ * and it replays the blob from the history of the thread. The SDK strips
150
+ * a reasoning part that lost its blob, and it warns. That warning reaches
151
+ * the harness logger through the SDK warning hook.
152
+ *
153
+ * One thread must keep one mode. The wire captures the history of a turn
154
+ * under the mode of that turn. If a later turn replays a history of the
155
+ * `false` mode under storage, the SDK emits a reference onto an item that
156
+ * the server never stored.
157
+ *
158
+ * An operator can override the default with `true`. The dashboard then
159
+ * shows the trail of the thread, and a caller can retrieve a response by
160
+ * id. The harness does not use `previous_response_id`, and it does not
161
+ * use `conversation`. This is true for the two modes.
162
+ */
163
+ readonly store?: boolean;
86
164
  } | {
87
165
  readonly kind: "openai-compatible";
88
166
  readonly name: string;