agent-accelerator 0.4.0 → 0.5.0

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 (46) hide show
  1. package/README.md +149 -57
  2. package/bunfig.toml +1 -0
  3. package/package.json +2 -3
  4. package/src/agent/agent.ts +414 -118
  5. package/src/agent/context.ts +11 -16
  6. package/src/agent/delegation.ts +111 -274
  7. package/src/agent/loop.ts +327 -99
  8. package/src/agent/subagent.ts +4 -12
  9. package/src/agent/worker.ts +435 -0
  10. package/src/index.ts +58 -5
  11. package/src/models/catalog-cache.ts +65 -25
  12. package/src/models/catalog.ts +47 -2
  13. package/src/providers/google.ts +338 -81
  14. package/src/providers/openai-compat.ts +171 -61
  15. package/src/providers/openai.ts +174 -44
  16. package/src/providers/openrouter.ts +129 -44
  17. package/src/providers/shared.ts +169 -17
  18. package/src/providers.ts +375 -11
  19. package/src/scheduler/scheduled-actions.md +41 -0
  20. package/src/scheduler/scheduler.ts +814 -0
  21. package/src/scheduler/store.ts +263 -0
  22. package/src/scheduler/time.ts +148 -0
  23. package/src/scheduler/tool.ts +151 -0
  24. package/src/session/store.ts +229 -113
  25. package/src/streaming/event-stream.ts +6 -4
  26. package/src/telemetry-file.ts +31 -0
  27. package/src/telemetry.ts +65 -0
  28. package/src/tools/executor.ts +163 -38
  29. package/src/tools/subagent-result.ts +60 -0
  30. package/src/types/agent.ts +74 -22
  31. package/src/types/model.ts +39 -0
  32. package/src/types/response.ts +9 -0
  33. package/src/types/scheduler.ts +81 -0
  34. package/src/types/tool.ts +15 -0
  35. package/src/utils/cache.ts +14 -5
  36. package/src/utils/documents.ts +112 -52
  37. package/src/utils/env.ts +21 -7
  38. package/src/utils/errors.ts +26 -1
  39. package/src/utils/headers.ts +15 -15
  40. package/src/utils/home.ts +30 -0
  41. package/src/utils/media.ts +57 -12
  42. package/src/utils/raw-capture.ts +76 -0
  43. package/src/utils/retry.ts +11 -3
  44. package/src/utils/safe-io.ts +252 -0
  45. package/src/utils/session.ts +7 -8
  46. package/src/data/README.md +0 -84
@@ -6,6 +6,9 @@ import type {
6
6
  } from "../types/tool.ts";
7
7
  import { toJsonSafe } from "../utils/serialization.ts";
8
8
  import { ConfigError } from "../types/errors.ts";
9
+ import { isSubagentResult } from "./subagent-result.ts";
10
+ import { isTruncatedArguments } from "../types/tool.ts";
11
+ import { telemetryBus } from "../telemetry.ts";
9
12
 
10
13
  export interface ExecuteToolsOptions {
11
14
  tools: Record<string, ToolDefinition>;
@@ -56,22 +59,16 @@ export const DEFAULT_MAX_RESULT_CHARS = 20000;
56
59
  export const REPEAT_BLOCK_THRESHOLD = 3;
57
60
 
58
61
  /**
59
- * True when a tool call carries only `{ raw: string }` arguments — the marker
60
- * shape produced by `parseStreamedToolArguments` when streamed JSON chunks
61
- * never parsed to an object (truncated/incomplete arguments). Such calls must
62
- * NOT execute: the model must retry with complete JSON.
62
+ * True when a tool call carries only the `{ __truncated_raw: string }` marker
63
+ * produced by `parseStreamedToolArguments` when streamed JSON chunks never
64
+ * parsed to an object (truncated/incomplete arguments). Such calls must NOT
65
+ * execute: the model must retry with complete JSON. The marker key is not a
66
+ * plausible parameter name, so tools with a real `raw` parameter are unaffected.
63
67
  *
64
68
  * @example `if (isTruncatedToolCall(call)) return truncatedError(call);`
65
69
  */
66
70
  export function isTruncatedToolCall(call: ToolCallRecord): boolean {
67
- const args = call?.arguments;
68
- if (!args || typeof args !== "object" || Array.isArray(args)) return false;
69
- const keys = Object.keys(args);
70
- return (
71
- keys.length === 1 &&
72
- keys[0] === "raw" &&
73
- typeof (args as Record<string, unknown>).raw === "string"
74
- );
71
+ return isTruncatedArguments(call?.arguments);
75
72
  }
76
73
 
77
74
  /**
@@ -99,6 +96,11 @@ export function assertNoToolCollisions(
99
96
  }
100
97
  }
101
98
 
99
+ /** Redacts `scheme://user:pass@` credentials. The only scrub applied to telemetry-bus error detail. */
100
+ function redactUrlCredentials(message: string): string {
101
+ return message.replace(/([A-Za-z][A-Za-z0-9+.-]*:\/\/)[^\s"'`@]+@/g, "$1[redacted]@");
102
+ }
103
+
102
104
  /**
103
105
  * Redacts secrets and machine-local details from an error message before it
104
106
  * is sent back to the model: credentials in URLs/connection strings
@@ -106,11 +108,7 @@ export function assertNoToolCollisions(
106
108
  * (`/home/...`, `/Users/...`, `C:\...` → `[redacted-path]`).
107
109
  */
108
110
  export function sanitizeErrorMessage(message: string): string {
109
- let out = message;
110
- out = out.replace(
111
- /([A-Za-z][A-Za-z0-9+.-]*:\/\/)[^\s"'`@]+@/g,
112
- "$1[redacted]@"
113
- );
111
+ let out = redactUrlCredentials(message);
114
112
  out = out.replace(/\/home\/[^\s"'`:;,)]*/g, "[redacted-path]");
115
113
  out = out.replace(/\/Users\/[^\s"'`:;,)]*/g, "[redacted-path]");
116
114
  out = out.replace(/[A-Za-z]:\\[^\s"'`:;,)]*/g, "[redacted-path]");
@@ -128,21 +126,26 @@ function truncateResultString(value: string, limit: number): string {
128
126
  }
129
127
 
130
128
  /**
131
- * Drops `stack` from an Error-shaped sanitized result for model context.
132
- * Keeps `{ name, message }` plus any custom keys; nested values untouched.
129
+ * Scrubs Error-shaped values ANYWHERE in a JSON-safe tool result before it
130
+ * reaches model context: `stack` is dropped (stack frames carry absolute
131
+ * server paths) and `message` is passed through {@link sanitizeErrorMessage}
132
+ * (credentials in URLs, absolute paths). Non-error data is untouched.
133
133
  */
134
- function stripTopLevelErrorStack(value: unknown): unknown {
135
- if (!value || typeof value !== "object" || Array.isArray(value)) return value;
134
+ function scrubErrors(value: unknown, depth = 0): unknown {
135
+ if (!value || typeof value !== "object" || depth > 32) return value;
136
+ if (Array.isArray(value)) return value.map((v) => scrubErrors(v, depth + 1));
136
137
  const record = value as Record<string, unknown>;
137
- if (
138
- typeof record.name === "string" &&
139
- typeof record.message === "string" &&
140
- "stack" in record
141
- ) {
142
- const { stack: _dropped, ...rest } = record;
143
- return rest;
138
+ const errorShaped = typeof record.name === "string" && typeof record.message === "string" && "stack" in record;
139
+ const out: Record<string, unknown> = {};
140
+ for (const [key, v] of Object.entries(record)) {
141
+ if (errorShaped && key === "stack") continue;
142
+ if (errorShaped && key === "message" && typeof v === "string") {
143
+ out[key] = sanitizeErrorMessage(v);
144
+ continue;
145
+ }
146
+ out[key] = scrubErrors(v, depth + 1);
144
147
  }
145
- return value;
148
+ return out;
146
149
  }
147
150
 
148
151
  /** Stable serialization for repeat-guard fingerprints (key order independent). */
@@ -223,6 +226,48 @@ export function makeRepeatBlockedResult(call: ToolCallRecord): ToolResultRecord
223
226
  };
224
227
  }
225
228
 
229
+ /** Error detail for the telemetry bus (developer channel): full message, code, status, stack and cause chain; only URL credentials are redacted. */
230
+ function describeError(err: unknown, depth = 0): Record<string, unknown> {
231
+ const e = (err ?? {}) as Record<string, unknown>;
232
+ const text = (v: unknown): string | undefined => (typeof v === "string" ? redactUrlCredentials(v) : undefined);
233
+ return {
234
+ name: typeof e.name === "string" ? e.name : typeof err,
235
+ message: text(e.message) ?? text(String(err)),
236
+ ...(e.code !== undefined ? { code: e.code } : {}),
237
+ ...(e.status !== undefined || e.statusCode !== undefined ? { status: e.status ?? e.statusCode } : {}),
238
+ ...(e.retryable === true || e.transient === true ? { retryable: true } : {}),
239
+ ...(text(e.stack) ? { stack: text(e.stack) } : {}),
240
+ ...(e.cause !== undefined && depth < 4 ? { cause: describeError(e.cause, depth + 1) } : {}),
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Emits one tool lifecycle event on the telemetry bus (`tool.<phase>`). The bus
246
+ * delivers synchronously, so a developer sink that appends to disk leaves a
247
+ * crash-proof trace: a `call_started` with no ending event means "in flight at
248
+ * the crash", and the last `progress` shows how far it got.
249
+ */
250
+ export function traceToolEvent(
251
+ phase: string,
252
+ level: "info" | "warn" | "error",
253
+ call: { id: string; name: string },
254
+ scope: { agentName?: string; sessionId?: string },
255
+ data: Record<string, unknown> = {}
256
+ ): void {
257
+ telemetryBus.emit({
258
+ code: `tool.${phase}`,
259
+ message: `[Agent Accelerator] tool ${call.name} (${call.id}): ${phase}`,
260
+ level,
261
+ data: { toolCallId: call.id, toolName: call.name, agentName: scope.agentName, sessionId: scope.sessionId, ...data },
262
+ });
263
+ }
264
+
265
+ function capJson(value: unknown, max = 2000): unknown {
266
+ const safe = scrubErrors(toJsonSafe(value));
267
+ const json = JSON.stringify(safe);
268
+ return typeof json === "string" && json.length > max ? `${json.slice(0, max)}…[truncated]` : safe;
269
+ }
270
+
226
271
  function levenshtein(a: string, b: string): number {
227
272
  const row = Array.from({ length: b.length + 1 }, (_, i) => i);
228
273
  for (let i = 1; i <= a.length; i++) {
@@ -271,6 +316,14 @@ function resolveTool(tools: Record<string, ToolDefinition>, requestedName: strin
271
316
  };
272
317
  }
273
318
 
319
+ /** Finds a tool definition by exact or case/format-insensitive name (no suggestion logic). */
320
+ export function findToolDefinition(
321
+ tools: Record<string, ToolDefinition>,
322
+ name: string
323
+ ): ToolDefinition | undefined {
324
+ return resolveTool(tools, name).definition;
325
+ }
326
+
274
327
  class Semaphore {
275
328
  private active = 0;
276
329
  private readonly waiters: Array<{
@@ -376,6 +429,7 @@ async function executeAttempt(
376
429
  const error: any = new Error(`Tool execution timed out after ${timeoutMs}ms`);
377
430
  error.code = "ETIMEDOUT";
378
431
  error.transient = true;
432
+ error.toolTimeout = true;
379
433
  return error;
380
434
  };
381
435
  try {
@@ -430,14 +484,20 @@ export async function executeToolCalls(
430
484
  return semaphore;
431
485
  };
432
486
 
487
+ const scope = { agentName, sessionId };
433
488
  const runSingle = async (call: ToolCallRecord): Promise<ToolResultRecord> => {
434
489
  const startTime = nowMs();
490
+ // Latest `ctx.emit({ type: "progress", ... })` snapshot: what the tool had finished before any failure.
491
+ let progress: unknown;
492
+ let attempts = 0;
493
+ let timedOut = false;
435
494
 
436
- // Truncated streamed arguments (Q-12): the `{ raw }` marker means the
495
+ // Truncated streamed arguments (Q-12): the `__truncated_raw` marker means the
437
496
  // provider's partial JSON never parsed. Never execute or overwrite wire
438
497
  // finishReason here — return a synthetic error so the model retries with
439
498
  // complete JSON.
440
499
  if (isTruncatedToolCall(call)) {
500
+ traceToolEvent("call_blocked", "warn", call, scope, { reason: "truncated_arguments" });
441
501
  return {
442
502
  id: call.id,
443
503
  name: call.name,
@@ -453,6 +513,7 @@ export async function executeToolCalls(
453
513
  const toolDef = resolved.definition;
454
514
 
455
515
  if (!toolDef) {
516
+ traceToolEvent("call_blocked", "warn", call, scope, { reason: "tool_not_found", suggestion: resolved.suggestion });
456
517
  const suffix = resolved.suggestion ? ` Did you mean '${resolved.suggestion}'?` : "";
457
518
  return {
458
519
  id: call.id,
@@ -481,6 +542,10 @@ export async function executeToolCalls(
481
542
  signal,
482
543
  sessionId,
483
544
  onSubagentEvent,
545
+ emit: (event) => {
546
+ if (event?.type === "progress") progress = event;
547
+ traceToolEvent(event?.type === "progress" ? "progress" : "event", "info", call, scope, { event: capJson(event) });
548
+ },
484
549
  };
485
550
 
486
551
  // Validate input if Zod schema is provided
@@ -500,6 +565,7 @@ export async function executeToolCalls(
500
565
  const hint = expectedParams
501
566
  ? ` Expected parameters: ${expectedParams}.`
502
567
  : "";
568
+ traceToolEvent("call_blocked", "warn", call, scope, { reason: "schema_validation", detail: parseRes.error.message });
503
569
  return {
504
570
  id: call.id,
505
571
  name: call.name,
@@ -531,13 +597,20 @@ export async function executeToolCalls(
531
597
  executionStartTime = nowMs();
532
598
  let rawResult: unknown;
533
599
  let attempt = 0;
600
+ traceToolEvent("call_started", "info", call, scope, { maxAttempts, timeoutMs });
534
601
  while (true) {
535
602
  attempt++;
603
+ attempts = attempt;
604
+ traceToolEvent("attempt_started", "info", call, scope, { attempt, maxAttempts });
536
605
  try {
537
606
  rawResult = await executeAttempt(toolDef, parsedInput, context, timeoutMs);
538
607
  break;
539
608
  } catch (err: any) {
540
609
  if (signal?.aborted) throw new Error("Tool execution aborted");
610
+ timedOut = err?.toolTimeout === true;
611
+ const willRetry =
612
+ isTransientError(err) && attempt < maxAttempts && !(err?.code === "ETIMEDOUT" && (!configuredTries || configuredTries <= 0));
613
+ traceToolEvent("attempt_failed", "warn", call, scope, { attempt, maxAttempts, willRetry, error: describeError(err), progress: capJson(progress) });
541
614
  if (!isTransientError(err) || attempt >= maxAttempts) throw err;
542
615
  // A timeout is a deliberate per-attempt deadline, not a parent
543
616
  // cancellation (which rejects via the abort signal instead and is
@@ -546,13 +619,18 @@ export async function executeToolCalls(
546
619
  // maxTries. Note ctx.signal is cooperative: the timeout races the
547
620
  // tool body, but a tool that ignores the signal still resolves.
548
621
  if (err?.code === "ETIMEDOUT" && (!configuredTries || configuredTries <= 0)) throw err;
549
- await waitForRetry(Math.min(30_000, 250 * 2 ** Math.min(attempt - 1, 7)), signal);
622
+ const delayMs = Math.min(30_000, 250 * 2 ** Math.min(attempt - 1, 7));
623
+ traceToolEvent("retry_scheduled", "info", call, scope, { attempt, nextAttempt: attempt + 1, delayMs });
624
+ await waitForRetry(delayMs, signal);
550
625
  }
551
626
  }
552
627
 
553
628
  // Undefined means "no value", serialized null-safe as "null" (changed
554
629
  // from the legacy "Success" marker so model context stays valid JSON).
630
+ const finishOk = (resultChars: number): void =>
631
+ traceToolEvent("call_succeeded", "info", call, scope, { attempts: attempt, durationMs: elapsedMs(executionStartTime), resultChars });
555
632
  if (rawResult === undefined) {
633
+ finishOk(4);
556
634
  return {
557
635
  id: call.id,
558
636
  name: resolved.name,
@@ -561,12 +639,31 @@ export async function executeToolCalls(
561
639
  durationMs: elapsedMs(executionStartTime),
562
640
  };
563
641
  }
564
- // Stack traces stay in server logs, not model context: keep
565
- // { name, message } plus custom keys, drop stack.
566
- let safeResult = stripTopLevelErrorStack(toJsonSafe(rawResult));
642
+ // Branded delegation envelopes pass through untouched: JSON-safing would
643
+ // strip the (symbol) brand the loop needs to trust their metadata.
644
+ if (isSubagentResult(rawResult)) {
645
+ finishOk(rawResult.xml.length);
646
+ return {
647
+ id: call.id,
648
+ name: resolved.name,
649
+ result: rawResult,
650
+ isError: false,
651
+ durationMs: elapsedMs(executionStartTime),
652
+ };
653
+ }
654
+ // Nested error stacks / absolute paths never reach model context.
655
+ let safeResult = scrubErrors(toJsonSafe(rawResult));
656
+ const charLimit = resultCharLimit(toolDef);
567
657
  if (typeof safeResult === "string") {
568
- safeResult = truncateResultString(safeResult, resultCharLimit(toolDef));
658
+ safeResult = truncateResultString(safeResult, charLimit);
659
+ } else {
660
+ // Structured results are capped by their serialized size too: a tool
661
+ // returning 50k rows must not blow the context window (or the bill).
662
+ // Oversized ones become a truncated JSON string with the usual marker.
663
+ const json = JSON.stringify(safeResult);
664
+ if (typeof json === "string" && json.length > charLimit) safeResult = truncateResultString(json, charLimit);
569
665
  }
666
+ finishOk(typeof safeResult === "string" ? safeResult.length : (JSON.stringify(safeResult)?.length ?? 0));
570
667
  return {
571
668
  id: call.id,
572
669
  name: resolved.name,
@@ -575,8 +672,17 @@ export async function executeToolCalls(
575
672
  durationMs: elapsedMs(executionStartTime),
576
673
  };
577
674
  } catch (err: any) {
578
- // Never leak secrets/paths to the model. A per-tool redactError hook
579
- // wins; otherwise sanitize the message (credentials, file paths).
675
+ // The developer channel (telemetry bus) gets the full error; the model gets a
676
+ // scrubbed message plus what the tool finished before failing, so it repairs
677
+ // only the remainder instead of repeating completed side effects.
678
+ const aborted = signal?.aborted === true;
679
+ const done = err?.context?.progress ?? progress;
680
+ traceToolEvent(aborted ? "call_aborted" : timedOut ? "call_timed_out" : "call_failed", "error", call, scope, {
681
+ attempts,
682
+ durationMs: elapsedMs(executionStartTime),
683
+ error: describeError(err),
684
+ progress: capJson(done),
685
+ });
580
686
  let message: string;
581
687
  const redactError = (toolDef as any)?.redactError;
582
688
  if (typeof redactError === "function") {
@@ -588,10 +694,29 @@ export async function executeToolCalls(
588
694
  } else {
589
695
  message = sanitizeErrorMessage(err?.message || String(err));
590
696
  }
697
+ const details = {
698
+ ...(typeof redactError === "function"
699
+ ? {}
700
+ : {
701
+ error: {
702
+ name: err?.name,
703
+ ...(err?.code !== undefined ? { code: err.code } : {}),
704
+ ...(err?.status !== undefined || err?.statusCode !== undefined ? { status: err.status ?? err.statusCode } : {}),
705
+ },
706
+ }),
707
+ attempts,
708
+ ...(timedOut ? { timedOut: true } : {}),
709
+ ...(aborted ? { aborted: true } : {}),
710
+ ...(done !== undefined ? { progress: capJson(done) } : {}),
711
+ hint:
712
+ done !== undefined
713
+ ? "The tool failed part-way. Work reported in progress was NOT undone: do not repeat it, retry or fix only what remains."
714
+ : "The tool failed and may have partially executed. Check before retrying side-effecting work.",
715
+ };
591
716
  return {
592
717
  id: call.id,
593
718
  name: call.name,
594
- result: `Error executing ${call.name}: ${message}`,
719
+ result: `Error executing ${call.name}: ${message}\n${JSON.stringify(details)}`,
595
720
  isError: true,
596
721
  durationMs: elapsedMs(executionStartTime),
597
722
  };
@@ -0,0 +1,60 @@
1
+ import type { SubAgentExecutionMetadata } from "../types/response.ts";
2
+
3
+ /**
4
+ * Module-private brand proving a tool result was produced by the SDK's own
5
+ * delegation tools (`agentToTool` / `spawn_subagents`). Symbols cannot be
6
+ * forged by JSON a tool fetched from the web, so only branded results get
7
+ * their `_subagentMetadata` trusted (usage/cost rolled up, XML shown to the
8
+ * model).
9
+ */
10
+ const SUBAGENT_RESULT_BRAND: unique symbol = Symbol("agent-accelerator.subagent-result") as never;
11
+
12
+ /** Branded envelope returned by delegation tools. */
13
+ export interface SubagentResult {
14
+ xml: string;
15
+ _subagentMetadata: SubAgentExecutionMetadata[];
16
+ toString(): string;
17
+ readonly [SUBAGENT_RESULT_BRAND]: true;
18
+ }
19
+
20
+ /** Wraps worker output + metadata in the branded envelope. */
21
+ export function makeSubagentResult(xml: string, metas: SubAgentExecutionMetadata[]): SubagentResult {
22
+ return {
23
+ xml,
24
+ _subagentMetadata: metas,
25
+ toString() {
26
+ return xml;
27
+ },
28
+ [SUBAGENT_RESULT_BRAND]: true,
29
+ };
30
+ }
31
+
32
+ /** True only for envelopes created by {@link makeSubagentResult}. */
33
+ export function isSubagentResult(value: unknown): value is SubagentResult {
34
+ return !!value && typeof value === "object" && (value as Record<symbol, unknown>)[SUBAGENT_RESULT_BRAND] === true;
35
+ }
36
+
37
+ const num = (v: unknown): number => (typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : 0);
38
+
39
+ /**
40
+ * Defensive copy of worker metadata: guarantees the fields the loop reads
41
+ * (`usage` numbers, string `provider`/`model`) exist, so one malformed entry
42
+ * can never crash a run.
43
+ */
44
+ export function normalizeSubagentMetadata(metas: unknown): SubAgentExecutionMetadata[] {
45
+ if (!Array.isArray(metas)) return [];
46
+ const out: SubAgentExecutionMetadata[] = [];
47
+ for (const raw of metas) {
48
+ if (!raw || typeof raw !== "object") continue;
49
+ const m = raw as Record<string, unknown>;
50
+ const u = (m.usage && typeof m.usage === "object" ? m.usage : {}) as Record<string, unknown>;
51
+ const usage = { ...u, inputTokens: num(u.inputTokens), outputTokens: num(u.outputTokens), totalTokens: num(u.totalTokens) };
52
+ out.push({
53
+ ...(m as unknown as SubAgentExecutionMetadata),
54
+ model: typeof m.model === "string" ? m.model : "unknown",
55
+ provider: typeof m.provider === "string" ? m.provider : "unknown",
56
+ usage,
57
+ });
58
+ }
59
+ return out;
60
+ }
@@ -7,6 +7,7 @@ import type {
7
7
  } from "./core.ts";
8
8
  import type { ModelProviderInstance } from "../providers/registry.ts";
9
9
  import type { Agent } from "../agent/agent.ts";
10
+ import type { z } from "zod";
10
11
 
11
12
  // ---------------------------------------------------------------------------
12
13
  // Q-52/Q-53/Q-54 canonical option types (all additive; nothing renamed).
@@ -29,12 +30,6 @@ export type ModelRef = string | ModelSpec | ModelProviderInstance;
29
30
  /** Canonical reasoning option (Q-52): bare level, or level + explicit token budget. */
30
31
  export type ThinkingOption = ThinkingLevel | { level?: ThinkingLevel; budgetTokens?: number };
31
32
 
32
- /** Canonical sampling knobs (Q-54). Read by the loop via `(config as any).sampling`. */
33
- export interface SamplingConfig {
34
- temperature?: number;
35
- topP?: number;
36
- }
37
-
38
33
  /** Canonical retry policy (Q-54). Read by the loop via `(config as any).retry`. */
39
34
  export interface RetryPolicyConfig {
40
35
  maxRetries?: number;
@@ -61,6 +56,17 @@ export type ToolChoiceOption =
61
56
  /** Canonical stop control (Q-54): sequence(s) halting generation. */
62
57
  export type StopOption = string | string[];
63
58
 
59
+ /** Canonical structured-output input (Q-61). Zod, JSON Schema, or explicit form. */
60
+ export type StructuredOutputOption =
61
+ | z.ZodType
62
+ | Record<string, unknown>
63
+ | {
64
+ name?: string;
65
+ description?: string;
66
+ schema: z.ZodType | Record<string, unknown>;
67
+ strict?: boolean;
68
+ };
69
+
64
70
  /** Configuration used to construct an {@link Agent}. */
65
71
  export interface AgentConfig {
66
72
  /** Display name used in tool and sub-agent metadata. */
@@ -127,24 +133,39 @@ export interface AgentConfig {
127
133
  * after every step (user turn, assistant turn, tool batch, sub-agent
128
134
  * step) so a crash loses at most the in-flight step. Pass `{ dir }` for
129
135
  * `sessions/<sessionId>/session.json` layout or `{ file }` for a single
130
- * pretty `.session.json` file. Writes are atomic and never fail a turn.
136
+ * pretty `.session.json` file. `persist: {}` (neither) uses `<home>/sessions`.
137
+ * Writes are atomic and never fail a turn.
131
138
  */
132
139
  persist?: { dir?: string; file?: string };
140
+ /**
141
+ * Folder for everything Agent Accelerator writes to disk (`sessions/`,
142
+ * `scheduled_actions/`, `telemetry/`). Defaults to `setHome()`, then the `AGENT_ACCEL_HOME` env var, then `.agent-accel/` in the working directory.
143
+ * An explicit `persist.dir` / `persist.file` still wins for sessions.
144
+ */
145
+ home?: string;
133
146
  // Q-54 missing knobs (all optional, additive; loop reads via `(config as any).*`).
134
- /** Canonical tool-selection control (`"auto"` default). */
147
+ /**
148
+ * Canonical tool-selection control (`"auto"` default). `"none"` applies to
149
+ * every turn of a run; `"required"` and a pinned tool (`{ tool: "name" }`)
150
+ * apply to the FIRST model turn only, since forcing a tool call on every
151
+ * turn would make a run unable to finish.
152
+ */
135
153
  toolChoice?: ToolChoiceOption;
154
+ /** Google Interactions storage tier for the chain-TTL gate: `"free"` (23h, default) or `"paid"` (54d). */
155
+ googleTier?: "free" | "paid";
136
156
  /** Maximum output tokens for one model call. */
137
157
  maxOutputTokens?: number;
138
- /** Sampling knobs. */
139
- sampling?: SamplingConfig;
140
158
  /** Stop sequence(s). */
141
159
  stop?: StopOption;
142
160
  /** Deterministic seed, when the provider supports it. */
143
161
  seed?: number;
144
162
  /**
145
- * Optional output schema (Zod or JSON Schema, stored as `unknown` so `zod`
163
+ * Optional output schema (Zod, JSON Schema, or `{ name, schema, strict }`,
164
+ * see `StructuredOutputOption`, stored as `unknown` so `zod`
146
165
  * stays optional for type-only consumers). Agent-level only; per-run
147
- * overrides are intentionally unsupported.
166
+ * overrides are intentionally unsupported. When set, the model answer is
167
+ * constrained to the schema on all providers and `AgentResponse.parsed`
168
+ * carries the validated value.
148
169
  */
149
170
  output?: unknown;
150
171
  /** Retry policy for provider calls. */
@@ -153,19 +174,46 @@ export interface AgentConfig {
153
174
  fetch?: typeof fetch;
154
175
  /** Provider/env overrides forwarded to key resolution. */
155
176
  env?: Record<string, string>;
156
- /** Structured logger for loop telemetry (defaults to no-op). */
177
+ /** @deprecated Not implemented yet; subscribe with `telemetryBus.onEvent` instead. Passing it emits a `config.unsupported_option` warning. */
157
178
  logger?: AgentLogger;
158
- /** Control hooks (Q-59). All optional, fail-open by default. */
179
+ /** @deprecated Not implemented yet (accepted for forward compatibility; passing it emits a `config.unsupported_option` warning). */
159
180
  hooks?: unknown;
160
- /** Threshold compaction policy (Q-60a). Fully configurable/replaceable. */
181
+ /** @deprecated Not implemented yet (accepted for forward compatibility; passing it emits a `config.unsupported_option` warning). */
161
182
  contextManagement?: unknown;
162
- /** Telemetry bus (Q-41). Default no-op; pretty logger lives in /cli. */
163
- telemetry?: { onEvent?: (e: any) => void } | unknown;
164
- /** Local file access roots (Q-08). Default deny; `path` sources must resolve inside roots. */
183
+ /**
184
+ * Telemetry options. `persist: true` appends every `telemetryBus` event (except full reasoning
185
+ * text) to `<home>/telemetry/<date>.ndjson`. Off by default: the SDK writes nothing unless asked.
186
+ */
187
+ telemetry?: { persist?: boolean };
188
+ /**
189
+ * Local file policy. With `roots` set, every local-path media part and the
190
+ * built-in `convert_document_to_markdown` tool must resolve inside them
191
+ * (`[]` denies all local paths). Without it, developer-supplied parts are
192
+ * unrestricted and the model-callable tool is limited to `process.cwd()`.
193
+ */
165
194
  fileAccess?: { roots?: string[] };
166
- /** Raw capture policy (Q-11). Default "redacted": no bodies. */
195
+ /**
196
+ * Conversation handling when a run FAILS (aborts always keep partial
197
+ * history). `"auto"` (default) restores the pre-run state unless a tool
198
+ * already ran, since rolling back would erase the record of real side
199
+ * effects and a retry would repeat them. `"rollback"` always restores;
200
+ * `"keep"` never does.
201
+ */
202
+ onFailure?: "auto" | "rollback" | "keep";
203
+ /**
204
+ * What `AgentResponse.raw` keeps of the wire exchange. `"redacted"` (default):
205
+ * URL, method, redacted headers and HTTP status, with request/response BODIES
206
+ * replaced by a content-free summary so prompts, files and model output never
207
+ * leak into logs via `toJSON()`. `true`: full bodies (debugging). `false`:
208
+ * URL, method and status only.
209
+ */
167
210
  captureRaw?: false | "redacted" | true;
168
- /** Opt-in run budgets (Q-39). All fields default unlimited. */
211
+ /**
212
+ * Opt-in budgets, enforced PER RUN (counters restart with every `run`/`stream`)
213
+ * and including sub-agent usage. Exceeding a cap throws `BudgetExceededError`
214
+ * with the partial usage attached. `maxCostUsd` counts only priced usage:
215
+ * models with unknown pricing contribute $0, never a guess.
216
+ */
169
217
  budget?: Budget;
170
218
  /** Run + network limits (Q-22). Turn/budget caps unlimited by default; network timeouts finite. */
171
219
  limits?: AgentRunLimits;
@@ -211,6 +259,12 @@ export interface MidSessionConfig {
211
259
  export interface DynamicSubagentsConfig {
212
260
  /** Enables the automatic `spawn_subagents` tool. Defaults to true when the object is present. */
213
261
  enabled?: boolean;
262
+ /**
263
+ * Lets the Main Agent schedule dynamic sub-agents to run later (once or on
264
+ * repeat) via the `scheduled_actions` and `get_datetime` tools. Off by
265
+ * default: without it no tool, clock or timer exists. Needs a worker model.
266
+ */
267
+ allowScheduledActions?: boolean;
214
268
  /** Developer-only worker model override. Never choosable by the Main Agent LLM. Falls back to top-level `subagentModel`, then `SUB_AGENT_MODEL` env, then the parent model. */
215
269
  model?: string | ModelSpec | ModelProviderInstance;
216
270
  /** Max workers the Main Agent may spawn in a single `spawn_subagents` call. Extras are trimmed safely. Defaults to 4. */
@@ -278,8 +332,6 @@ export interface AgentRunOptions {
278
332
  toolChoice?: ToolChoiceOption;
279
333
  /** Per-run output-token cap. */
280
334
  maxOutputTokens?: number;
281
- /** Per-run sampling override. */
282
- sampling?: SamplingConfig;
283
335
  /** Per-run stop sequence(s). */
284
336
  stop?: StopOption;
285
337
  /** Per-run seed. */
@@ -87,6 +87,7 @@ export interface ModelCapabilities {
87
87
  supportsLongCacheRetention?: boolean;
88
88
  supportsParallelToolCalls?: boolean;
89
89
  supportsStreaming?: boolean;
90
+ supportsStructuredOutput?: boolean;
90
91
  modalities?: ("text" | "image" | "audio" | "video" | "pdf")[];
91
92
  // derived
92
93
  supportsReasoningToggle?: boolean;
@@ -138,6 +139,24 @@ export interface ModelSpec {
138
139
  raw?: RawModelData;
139
140
  }
140
141
 
142
+ // ---------------------------------------------------------------------------
143
+ // Structured outputs — normalized, provider-agnostic spec.
144
+ // Canonical input is `AgentConfig.output` (Zod, JSON Schema, or explicit
145
+ // `{ name, schema, strict }`); `normalizeStructuredOutput` in
146
+ // `src/providers.ts` produces this shape for adapters.
147
+ // ---------------------------------------------------------------------------
148
+ /** Normalized structured-output request passed from Agent to providers. */
149
+ export interface StructuredOutputSpec {
150
+ /** Format name (1-64 chars, `[a-zA-Z0-9_-]`). Defaults to `structured_output`. */
151
+ name: string;
152
+ /** JSON Schema for the final answer (already converted from Zod when needed). */
153
+ schema: Record<string, unknown>;
154
+ /** Strict schema adherence (OpenAI/OpenRouter only; ignored by Google). Defaults to true. */
155
+ strict?: boolean;
156
+ /** Optional description of what the format is for. */
157
+ description?: string;
158
+ }
159
+
141
160
  // ---------------------------------------------------------------------------
142
161
  // Provider plumbing — unchanged API
143
162
  // ---------------------------------------------------------------------------
@@ -156,6 +175,26 @@ export interface ProviderRequestOptions {
156
175
  env?: Record<string, string>;
157
176
  maxRetries?: number;
158
177
  maxRetryDelayMs?: number;
178
+ /** Normalized structured-output spec (see `normalizeStructuredOutput`). */
179
+ output?: StructuredOutputSpec;
180
+ /**
181
+ * Google Interactions storage tier for the proactive chain-TTL gate
182
+ * (free: 23h local gate; paid: 54d). Default `free` (safe). Display-only
183
+ * cache behavior is unaffected; unknown values fall back to `free`.
184
+ */
185
+ googleTier?: "free" | "paid";
186
+ /** Upper bound on generated tokens for one model call. */
187
+ maxOutputTokens?: number;
188
+ /** Stop sequence(s). Chat Completions only; other wires warn+drop. */
189
+ stop?: string[];
190
+ /** Deterministic-sampling seed. Chat Completions only; other wires warn+drop. */
191
+ seed?: number;
192
+ /** `fetch` implementation used for the provider request (defaults to global `fetch`). */
193
+ fetch?: typeof fetch;
194
+ /** Base backoff delay in ms for provider retries (default 250). */
195
+ retryBaseDelayMs?: number;
196
+ /** Total time budget in ms for provider retries (default 30000). */
197
+ retryBudgetMs?: number;
159
198
  }
160
199
 
161
200
  /** Auditable request/response wire payload captured in AgentResponse. */