agent-accelerator 0.4.5 → 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 (45) 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 +401 -120
  5. package/src/agent/context.ts +11 -16
  6. package/src/agent/delegation.ts +111 -274
  7. package/src/agent/loop.ts +253 -98
  8. package/src/agent/subagent.ts +4 -12
  9. package/src/agent/worker.ts +435 -0
  10. package/src/index.ts +39 -5
  11. package/src/models/catalog-cache.ts +65 -25
  12. package/src/models/catalog.ts +21 -2
  13. package/src/providers/google.ts +282 -74
  14. package/src/providers/openai-compat.ts +61 -41
  15. package/src/providers/openai.ts +46 -31
  16. package/src/providers/openrouter.ts +42 -31
  17. package/src/providers/shared.ts +110 -16
  18. package/src/providers.ts +119 -6
  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 +211 -104
  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 +57 -20
  31. package/src/types/model.ts +18 -0
  32. package/src/types/response.ts +2 -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/errors.ts +11 -0
  38. package/src/utils/headers.ts +15 -15
  39. package/src/utils/home.ts +30 -0
  40. package/src/utils/media.ts +57 -12
  41. package/src/utils/raw-capture.ts +76 -0
  42. package/src/utils/retry.ts +11 -3
  43. package/src/utils/safe-io.ts +252 -0
  44. package/src/utils/session.ts +7 -8
  45. 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
+ }
@@ -30,12 +30,6 @@ export type ModelRef = string | ModelSpec | ModelProviderInstance;
30
30
  /** Canonical reasoning option (Q-52): bare level, or level + explicit token budget. */
31
31
  export type ThinkingOption = ThinkingLevel | { level?: ThinkingLevel; budgetTokens?: number };
32
32
 
33
- /** Canonical sampling knobs (Q-54). Read by the loop via `(config as any).sampling`. */
34
- export interface SamplingConfig {
35
- temperature?: number;
36
- topP?: number;
37
- }
38
-
39
33
  /** Canonical retry policy (Q-54). Read by the loop via `(config as any).retry`. */
40
34
  export interface RetryPolicyConfig {
41
35
  maxRetries?: number;
@@ -139,16 +133,28 @@ export interface AgentConfig {
139
133
  * after every step (user turn, assistant turn, tool batch, sub-agent
140
134
  * step) so a crash loses at most the in-flight step. Pass `{ dir }` for
141
135
  * `sessions/<sessionId>/session.json` layout or `{ file }` for a single
142
- * 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.
143
138
  */
144
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;
145
146
  // Q-54 missing knobs (all optional, additive; loop reads via `(config as any).*`).
146
- /** 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
+ */
147
153
  toolChoice?: ToolChoiceOption;
154
+ /** Google Interactions storage tier for the chain-TTL gate: `"free"` (23h, default) or `"paid"` (54d). */
155
+ googleTier?: "free" | "paid";
148
156
  /** Maximum output tokens for one model call. */
149
157
  maxOutputTokens?: number;
150
- /** Sampling knobs. */
151
- sampling?: SamplingConfig;
152
158
  /** Stop sequence(s). */
153
159
  stop?: StopOption;
154
160
  /** Deterministic seed, when the provider supports it. */
@@ -168,19 +174,46 @@ export interface AgentConfig {
168
174
  fetch?: typeof fetch;
169
175
  /** Provider/env overrides forwarded to key resolution. */
170
176
  env?: Record<string, string>;
171
- /** 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. */
172
178
  logger?: AgentLogger;
173
- /** 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). */
174
180
  hooks?: unknown;
175
- /** 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). */
176
182
  contextManagement?: unknown;
177
- /** Telemetry bus (Q-41). Default no-op; pretty logger lives in /cli. */
178
- telemetry?: { onEvent?: (e: any) => void } | unknown;
179
- /** 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
+ */
180
194
  fileAccess?: { roots?: string[] };
181
- /** 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
+ */
182
210
  captureRaw?: false | "redacted" | true;
183
- /** 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
+ */
184
217
  budget?: Budget;
185
218
  /** Run + network limits (Q-22). Turn/budget caps unlimited by default; network timeouts finite. */
186
219
  limits?: AgentRunLimits;
@@ -226,6 +259,12 @@ export interface MidSessionConfig {
226
259
  export interface DynamicSubagentsConfig {
227
260
  /** Enables the automatic `spawn_subagents` tool. Defaults to true when the object is present. */
228
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;
229
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. */
230
269
  model?: string | ModelSpec | ModelProviderInstance;
231
270
  /** Max workers the Main Agent may spawn in a single `spawn_subagents` call. Extras are trimmed safely. Defaults to 4. */
@@ -293,8 +332,6 @@ export interface AgentRunOptions {
293
332
  toolChoice?: ToolChoiceOption;
294
333
  /** Per-run output-token cap. */
295
334
  maxOutputTokens?: number;
296
- /** Per-run sampling override. */
297
- sampling?: SamplingConfig;
298
335
  /** Per-run stop sequence(s). */
299
336
  stop?: StopOption;
300
337
  /** Per-run seed. */
@@ -177,6 +177,24 @@ export interface ProviderRequestOptions {
177
177
  maxRetryDelayMs?: number;
178
178
  /** Normalized structured-output spec (see `normalizeStructuredOutput`). */
179
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;
180
198
  }
181
199
 
182
200
  /** Auditable request/response wire payload captured in AgentResponse. */
@@ -9,6 +9,8 @@ export interface SubAgentStep {
9
9
  name?: string;
10
10
  text?: string;
11
11
  thinking?: string;
12
+ /** Opaque provider thought signature captured with this step's reasoning, when present. */
13
+ thoughtSignature?: string;
12
14
  isError?: boolean;
13
15
  timestamp: number;
14
16
  /** True while the worker is still streaming this step; finalized on turn end. */
@@ -0,0 +1,81 @@
1
+ /** Lifecycle of a scheduled action as stored in `tasks.json`. */
2
+ export type ScheduledState = "waiting" | "cooking" | "missed" | "done";
3
+
4
+ /** Outcome of one run. */
5
+ export type ScheduledRunStatus = "ok" | "failed";
6
+
7
+ /** Schedule fields shared by `create` input and the stored record. */
8
+ export interface ScheduleSpec {
9
+ /** Delivery clock time `DD:HH:MM`: `DD` days from today, `HH:MM` on a 12-hour clock (see `meridiem`). */
10
+ timeToDeliver: string;
11
+ meridiem: "AM" | "PM";
12
+ /** Duration `DD:HH:MM` before delivery at which the run starts. Default 10 minutes (`00:00:10`). */
13
+ startCookingInAdvance?: string;
14
+ /** Duration `DD:HH:MM` between deliveries (`01:00:00` daily). Omit to run once. */
15
+ repeatEvery?: string;
16
+ /** Extra attempts if a run fails, only while time remains before delivery. Default 0. */
17
+ retries?: number;
18
+ }
19
+
20
+ /** What `createScheduledAction` receives: dynamic sub-agent fields plus the schedule. */
21
+ export interface ScheduledActionInput {
22
+ role?: string;
23
+ instructions: string;
24
+ task: string;
25
+ /** Names from the developer's `dynamicSubagents.tools` pool. */
26
+ tools?: string[];
27
+ timeoutMs?: number;
28
+ scheduled_action: ScheduleSpec & {
29
+ /** Human label. */
30
+ name: string;
31
+ /** Lowercase kebab-case, max 48 chars. The SDK appends 6 random chars. */
32
+ id: string;
33
+ };
34
+ }
35
+
36
+ /** What `updateScheduledAction` may change. `id` and the creating agent are immutable. */
37
+ export type ScheduledActionPatch = Partial<Omit<ScheduledActionInput, "scheduled_action">> & {
38
+ scheduled_action?: Partial<Omit<ScheduledActionInput["scheduled_action"], "id">>;
39
+ };
40
+
41
+ /** One entry of `tasks.json`. All times are local clock strings `YYYY-MM-DDTHH:mm:ss` (no offset). */
42
+ export interface ScheduledActionRecord {
43
+ id: string;
44
+ name: string;
45
+ /** Name of the creating Agent; it runs the worker. */
46
+ agent: string;
47
+ role?: string;
48
+ instructions: string;
49
+ task: string;
50
+ /** Tool NAMES only; resolved against the live agent's pool at run time. */
51
+ tools: string[];
52
+ timeoutMs?: number;
53
+ /** The schedule as given. The stored truth is `nextCookAt` / `nextDeliverAt`. */
54
+ schedule: ScheduleSpec & { startCookingInAdvance: string };
55
+ state: ScheduledState;
56
+ /** `null` for a finished one-shot. */
57
+ nextCookAt: string | null;
58
+ nextDeliverAt: string | null;
59
+ createdAt: string;
60
+ /** Present only while `state` is `cooking`. */
61
+ current?: { cookStartedAt: string; deliverAt: string };
62
+ lastRun?: { status: ScheduledRunStatus; file: string; deliverAt: string; deliveredAt: string; late: boolean };
63
+ }
64
+
65
+ /** Payload of the `scheduled_action.delivered` telemetry event. Plain JSON. */
66
+ export interface ScheduledDelivery {
67
+ id: string;
68
+ name: string;
69
+ status: ScheduledRunStatus;
70
+ /** Absolute path of the run file. */
71
+ file: string;
72
+ readyAt: string;
73
+ deliverAt: string;
74
+ late: boolean;
75
+ }
76
+
77
+ /** Result of `killScheduledAction`. */
78
+ export type KillResult =
79
+ | { ok: true }
80
+ | { ok: false; reason: "not_found" }
81
+ | { ok: false; reason: "running"; message: string };
package/src/types/tool.ts CHANGED
@@ -136,3 +136,18 @@ export interface ToolResultRecord {
136
136
  isError?: boolean;
137
137
  durationMs?: number;
138
138
  }
139
+
140
+ /**
141
+ * Key of the marker object that replaces tool arguments whose streamed JSON
142
+ * never parsed (`{ __truncated_raw: "<partial text>" }`). It is deliberately
143
+ * not a plausible parameter name, so a real tool with a `raw` parameter is
144
+ * never mistaken for a truncated call. Such calls are never executed.
145
+ */
146
+ export const TRUNCATED_ARGS_KEY = "__truncated_raw";
147
+
148
+ /** True when `args` is exactly the truncated-arguments marker. */
149
+ export function isTruncatedArguments(args: unknown): boolean {
150
+ if (!args || typeof args !== "object" || Array.isArray(args)) return false;
151
+ const keys = Object.keys(args as Record<string, unknown>);
152
+ return keys.length === 1 && keys[0] === TRUNCATED_ARGS_KEY && typeof (args as Record<string, unknown>)[TRUNCATED_ARGS_KEY] === "string";
153
+ }