agent-accelerator 0.6.0 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +62 -16
  2. package/package.json +1 -1
  3. package/src/agent/agent.ts +96 -26
  4. package/src/agent/architecture.md +15 -12
  5. package/src/agent/context.ts +1 -0
  6. package/src/agent/defaults.ts +88 -0
  7. package/src/agent/delegation.ts +8 -3
  8. package/src/agent/loop.ts +34 -13
  9. package/src/agent/subagents-tool.ts +14 -21
  10. package/src/agent/usage-limit.ts +92 -0
  11. package/src/agent/worker.ts +17 -3
  12. package/src/agent/workers.ts +13 -1
  13. package/src/architecture.md +1 -0
  14. package/src/index.ts +9 -9
  15. package/src/prompts/additional-context.md +2 -0
  16. package/src/prompts/architecture.md +19 -0
  17. package/src/prompts/prompts.ts +81 -0
  18. package/src/prompts/reasoning-directive.md +5 -0
  19. package/src/prompts/structured-output.md +2 -0
  20. package/src/prompts/tool-bridge.md +1 -0
  21. package/src/prompts/tool-interrupted.md +1 -0
  22. package/src/prompts/tool-not-found.md +1 -0
  23. package/src/prompts/tool-repeat-blocked.md +1 -0
  24. package/src/prompts/tool-schema-error.md +1 -0
  25. package/src/prompts/tool-truncated-arguments.md +1 -0
  26. package/src/prompts/worker-failed.md +1 -0
  27. package/src/prompts/worker-replied.md +2 -0
  28. package/src/providers/architecture.md +8 -8
  29. package/src/providers/google.ts +82 -11
  30. package/src/providers/openai-compat.ts +17 -7
  31. package/src/providers/openai.ts +10 -4
  32. package/src/providers/openrouter.ts +185 -18
  33. package/src/providers.ts +59 -9
  34. package/src/scheduler/architecture.md +7 -7
  35. package/src/scheduler/scheduler.ts +40 -0
  36. package/src/scheduler/tool.ts +5 -9
  37. package/src/session/journal.ts +1 -0
  38. package/src/session/store.ts +4 -0
  39. package/src/tools/architecture.md +7 -6
  40. package/src/tools/executor.ts +60 -15
  41. package/src/tools/tool-content.ts +32 -0
  42. package/src/types/agent.ts +40 -6
  43. package/src/types/core.ts +4 -0
  44. package/src/types/message.ts +7 -0
  45. package/src/types/model.ts +4 -1
  46. package/src/types/scheduler.ts +4 -4
  47. package/src/types/tool.ts +2 -0
  48. package/src/utils/errors.ts +3 -3
  49. package/src/utils/headers.ts +1 -1
  50. /package/src/{scheduler/scheduled-actions.md → prompts/scheduled-actions-tool.md} +0 -0
  51. /package/src/{agent/subagents.md → prompts/subagents-tool.md} +0 -0
@@ -15,6 +15,7 @@ import { nowMs } from "../utils/session.ts";
15
15
  import { newRunId } from "../utils/session.ts";
16
16
  import { newTurnId } from "../utils/session.ts";
17
17
  import { sanitizeToolName, createLiveWorker } from "./worker.ts";
18
+ import type { UsageLimit } from "./usage-limit.ts";
18
19
 
19
20
  /** Builds a random child session id that fits the 64-char provider limit, embedding a hash of the parent when it must be cut; in: a parent id + tag from scheduled runs, out: the worker's session id. */
20
21
  export function createChildSessionId(parentId: string, tag: string): string {
@@ -631,7 +632,7 @@ export interface AgentAsToolTarget {
631
632
  *
632
633
  * @example `const researchTool = researcher.asTool("research");`
633
634
  */
634
- export function agentToTool(input: Agent | AgentAsToolTarget): ToolDefinition {
635
+ export function agentToTool(input: Agent | AgentAsToolTarget, opts?: { chat?: UsageLimit }): ToolDefinition {
635
636
  const agentInstance = "agent" in input ? input.agent : input;
636
637
  const rawName =
637
638
  ("name" in input && (input as any).name) ||
@@ -679,6 +680,7 @@ export function agentToTool(input: Agent | AgentAsToolTarget): ToolDefinition {
679
680
  typeof (agentInstance as Agent).fork === "function"
680
681
  ? agentInstance.fork(subSessionId, parentSessionId)
681
682
  : agentInstance;
683
+ if (worker !== agentInstance && opts?.chat) worker.chat = opts.chat; // this call counts into the parent chat's cap
682
684
  const workerStream = worker.stream(task, {
683
685
  signal: ctx?.signal,
684
686
  sessionId: subSessionId,
@@ -729,12 +731,15 @@ export function agentToTool(input: Agent | AgentAsToolTarget): ToolDefinition {
729
731
  *
730
732
  * @example `const tools = buildAgentTools([researcher, reviewer]);`
731
733
  */
732
- export function buildAgentTools(agents?: (Agent | AgentAsToolTarget)[]): Record<string, ToolDefinition> {
734
+ export function buildAgentTools(
735
+ agents?: (Agent | AgentAsToolTarget)[],
736
+ opts?: { chat?: UsageLimit }
737
+ ): Record<string, ToolDefinition> {
733
738
  if (!agents || agents.length === 0) return {};
734
739
 
735
740
  const tools: Record<string, ToolDefinition> = {};
736
741
  for (const item of agents) {
737
- const t = agentToTool(item);
742
+ const t = agentToTool(item, opts);
738
743
  if (t.name) {
739
744
  let finalName = t.name;
740
745
  let n = 1;
package/src/agent/loop.ts CHANGED
@@ -32,6 +32,7 @@ import { isSubagentResult, normalizeSubagentMetadata } from "../tools/subagent-r
32
32
  import { BudgetExceededError, TimeoutError } from "../types/errors.ts";
33
33
  import { applyCaptureRaw, type CaptureRawMode } from "../utils/raw-capture.ts";
34
34
  import { previewPrompt } from "../utils/session.ts";
35
+ import { DEFAULT_PROMPTS, fillPrompt, type Prompts } from "../prompts/prompts.ts";
35
36
  import { noteProviderTurn, normalizeStructuredOutput, parseStructuredOutput } from "../providers.ts";
36
37
 
37
38
  /** One mid-session steer request buffered while a run is active. */
@@ -98,6 +99,8 @@ export interface AgentLoopConfig {
98
99
  steerInbox?: SteerEntry[];
99
100
  /** Called after steer entries are injected (streaming loops also push `steer_injected` events). */
100
101
  onSteerInjected?: (injectedPrompts: string[]) => void;
102
+ /** The SDK prompts for this run (`AgentConfig.prompts` resolved); the shipped ones when unset. */
103
+ prompts?: Prompts;
101
104
  /**
102
105
  * Canonical structured-output request (raw `AgentConfig.output`).
103
106
  * Normalized per turn for the provider wire; validated once at the end
@@ -111,7 +114,7 @@ export interface AgentLoopConfig {
111
114
  * Returns display previews for events/telemetry. Never throws: invalid
112
115
  * entries are skipped so one bad steer cannot fail the active turn.
113
116
  */
114
- function drainSteerInbox(context: AgentContext, inbox?: SteerEntry[]): string[] {
117
+ function drainSteerInbox(context: AgentContext, inbox?: SteerEntry[], prompts: Prompts = DEFAULT_PROMPTS): string[] {
115
118
  if (!inbox || inbox.length === 0) return [];
116
119
  const injected: string[] = [];
117
120
  while (inbox.length > 0) {
@@ -121,7 +124,7 @@ function drainSteerInbox(context: AgentContext, inbox?: SteerEntry[]): string[]
121
124
  const opts = entry.options;
122
125
  if (Array.isArray(prompt) && prompt.length === 0) continue;
123
126
  if (opts?.additionalContext) {
124
- const prefix = `[Additional Context]\n${opts.additionalContext}\n\n`;
127
+ const prefix = `${fillPrompt(prompts.additionalContext, { context: opts.additionalContext })}\n\n`;
125
128
  if (typeof prompt === "string") prompt = prefix + prompt;
126
129
  else prompt = [{ type: "text", text: prefix } as ContentPart, ...prompt];
127
130
  }
@@ -157,7 +160,7 @@ function emitTurn(
157
160
  }
158
161
 
159
162
  /**
160
- * Per-turn `toolChoice`. `"none"` holds for the whole run; a forced choice
163
+ * Per-turn `toolChoice`. `"none"` and `"validated"` (which forces nothing) hold for the whole run; a forced choice
161
164
  * (`"required"` or a pinned function) applies to the FIRST model turn only:
162
165
  * forcing a tool call on every turn would keep the model calling tools until
163
166
  * `maxTurns`, so the run could never produce a final answer.
@@ -167,7 +170,7 @@ function toolChoiceForTurn(
167
170
  turn: number
168
171
  ): ProviderRequestOptions["toolChoice"] {
169
172
  if (choice === undefined || choice === "auto") return undefined;
170
- if (choice === "none") return "none";
173
+ if (choice === "none" || choice === "validated") return choice;
171
174
  return turn === 1 ? choice : undefined;
172
175
  }
173
176
 
@@ -275,7 +278,7 @@ function warnMaxTurnsHit(agentName: string | undefined, maxTurns: number, pendin
275
278
  * - drops results whose id the model never issued.
276
279
  * Open-weight models that reuse one id for several calls keep one result per call.
277
280
  */
278
- export function ensureToolPairing(context: AgentContext): void {
281
+ export function ensureToolPairing(context: AgentContext, prompts: Prompts = DEFAULT_PROMPTS): void {
279
282
  try {
280
283
  type ToolResultPart = Extract<ContentPart, { type: "tool_result" }>;
281
284
  const messages = context.messages;
@@ -316,7 +319,7 @@ export function ensureToolPairing(context: AgentContext): void {
316
319
  type: "tool_result",
317
320
  id: p.id,
318
321
  name,
319
- result: `Error: tool_call '${name}' (${p.id}) has no result because the run was interrupted before it completed (recovered by ensureToolPairing). It may have partially executed: check the tool trace before repeating side-effecting work.`,
322
+ result: fillPrompt(prompts.toolInterrupted, { tool: name, id: p.id }),
320
323
  isError: true,
321
324
  },
322
325
  ],
@@ -472,6 +475,14 @@ export function computeCostFromPricing(usage: TokenUsage, spec?: ModelSpec, serv
472
475
  }
473
476
  }
474
477
 
478
+ // Adds the media token details of `source` into `target` (kept only when something was reported); in: accumulateUsage, out: target.inputDetails / outputDetails.
479
+ function sumDetails(target: TokenUsage, source: TokenUsage, key: "inputDetails" | "outputDetails"): void {
480
+ const from = source[key] as Record<string, number | undefined> | undefined;
481
+ if (!from) return;
482
+ const into = ((target[key] as Record<string, number> | undefined) ??= {});
483
+ for (const [k, v] of Object.entries(from)) if (typeof v === "number") into[k] = (into[k] ?? 0) + v;
484
+ }
485
+
475
486
  // ---------------------------------------------------------------------------
476
487
  // Canonical usage accumulation (pure w.r.t. source).
477
488
  // Invariants (see assertUsageInvariants in session/store.ts):
@@ -484,6 +495,7 @@ export function computeCostFromPricing(usage: TokenUsage, spec?: ModelSpec, serv
484
495
  // This function never mutates `source`: the priced cost is cloned
485
496
  // into a local before folding into the target.
486
497
  // ---------------------------------------------------------------------------
498
+ // Adds one turn's usage (and its priced cost) into a running total without touching the source; in: both loops per model turn and finished worker, out: the run's accumulatedUsage.
487
499
  export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: ModelSpec, serviceTier?: string): void {
488
500
  target.inputTokens += source.inputTokens || 0;
489
501
  target.outputTokens += source.outputTokens || 0;
@@ -492,6 +504,8 @@ export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: M
492
504
  target.cacheReadTokens = (target.cacheReadTokens ?? 0) + (source.cacheReadTokens ?? 0);
493
505
  target.cacheWriteTokens = (target.cacheWriteTokens ?? 0) + (source.cacheWriteTokens ?? 0);
494
506
  target.thinkingTokens = (target.thinkingTokens ?? 0) + (source.thinkingTokens ?? 0);
507
+ sumDetails(target, source, "inputDetails");
508
+ sumDetails(target, source, "outputDetails");
495
509
 
496
510
  // Canonical invariant (provider-agnostic): cache hits are a subset of
497
511
  // input. Some providers occasionally report cached > input on long chained
@@ -557,6 +571,7 @@ async function executeToolCallsWithRepeatGuard(options: {
557
571
  openToolLog?: ExecuteToolsOptions["openToolLog"];
558
572
  previousFingerprints: Set<string>;
559
573
  repeatCounts: Map<string, number>;
574
+ prompts?: Prompts;
560
575
  agentName?: string;
561
576
  signal?: AbortSignal;
562
577
  sessionId?: string;
@@ -590,7 +605,7 @@ async function executeToolCallsWithRepeatGuard(options: {
590
605
  { agentName: options.agentName, sessionId: options.sessionId },
591
606
  { reason: "repeat_blocked" }
592
607
  );
593
- blocked.set(call.id, makeRepeatBlockedResult(call));
608
+ blocked.set(call.id, makeRepeatBlockedResult(call, options.prompts?.toolRepeatBlocked));
594
609
  } else {
595
610
  executable.push(call);
596
611
  }
@@ -610,6 +625,7 @@ async function executeToolCallsWithRepeatGuard(options: {
610
625
  onSubagentEvent: options.onSubagentEvent,
611
626
  onResult: options.onResult,
612
627
  openToolLog: options.openToolLog,
628
+ prompts: options.prompts,
613
629
  })
614
630
  : [];
615
631
  const byId = new Map<string, ToolResultRecord[]>();
@@ -693,6 +709,7 @@ async function finishInterruptedTools(
693
709
  sessionId: config.runOptions?.sessionId || config.options?.sessionId,
694
710
  onResult: config.onToolResult,
695
711
  openToolLog: config.openToolLog,
712
+ prompts: config.prompts,
696
713
  })
697
714
  : [];
698
715
  const ran = new Map(executed.map((r) => [r.id, r]));
@@ -776,7 +793,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
776
793
  checkDeadline(startTime, limits.deadlineMs);
777
794
  config.enforceBudget?.();
778
795
  // Repair orphan tool pairs before every request.
779
- ensureToolPairing(context);
796
+ ensureToolPairing(context, config.prompts);
780
797
  if (config.fileAccess?.roots) await enforceFileAccess(context.messages, config.fileAccess.roots);
781
798
  turns++;
782
799
 
@@ -880,7 +897,9 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
880
897
  // inject them now. Tool turns still execute first (see below) so the
881
898
  // current action finishes before redirection.
882
899
  const noToolSteered =
883
- !genResult.toolCalls || genResult.toolCalls.length === 0 ? drainSteerInbox(context, config.steerInbox) : [];
900
+ !genResult.toolCalls || genResult.toolCalls.length === 0
901
+ ? drainSteerInbox(context, config.steerInbox, config.prompts)
902
+ : [];
884
903
 
885
904
  // If no tool calls, generation is complete — unless steered, in which
886
905
  // case the injected message extends the SAME run (no new response).
@@ -918,6 +937,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
918
937
  sessionId: runOptions?.sessionId || options?.sessionId,
919
938
  onResult: config.onToolResult,
920
939
  openToolLog: config.openToolLog,
940
+ prompts: config.prompts,
921
941
  });
922
942
  const results = guarded.results;
923
943
  previousToolCallFingerprints = guarded.fingerprints;
@@ -964,7 +984,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
964
984
  // Steer drain AFTER tool results so call/result pairs stay adjacent.
965
985
  // The next loop iteration sends the injected user message to the model
966
986
  // within the same run (same usage/turn budget).
967
- const toolSteered = drainSteerInbox(context, config.steerInbox);
987
+ const toolSteered = drainSteerInbox(context, config.steerInbox, config.prompts);
968
988
  if (toolSteered.length > 0) {
969
989
  try {
970
990
  config.onSteerInjected?.(toolSteered);
@@ -1180,7 +1200,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
1180
1200
  throwIfCancelled({ usage: accumulatedUsage, turns, text: lastResponse?.text });
1181
1201
  checkDeadline(streamStartTime, streamLimits.deadlineMs);
1182
1202
  config.enforceBudget?.();
1183
- ensureToolPairing(context);
1203
+ ensureToolPairing(context, config.prompts);
1184
1204
  if (config.fileAccess?.roots) await enforceFileAccess(context.messages, config.fileAccess.roots);
1185
1205
  turns++;
1186
1206
 
@@ -1340,7 +1360,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
1340
1360
 
1341
1361
  const noToolSteered =
1342
1362
  !turnResponse.toolCalls || turnResponse.toolCalls.length === 0
1343
- ? drainSteerInbox(context, config.steerInbox)
1363
+ ? drainSteerInbox(context, config.steerInbox, config.prompts)
1344
1364
  : [];
1345
1365
  if (!turnResponse.toolCalls || turnResponse.toolCalls.length === 0) {
1346
1366
  emitTurn(runOptions, { turns, text: turnResponse.text, thinking: turnResponse.thinking });
@@ -1388,6 +1408,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
1388
1408
  sessionId: runOptions?.sessionId || options?.sessionId,
1389
1409
  onResult: config.onToolResult,
1390
1410
  openToolLog: config.openToolLog,
1411
+ prompts: config.prompts,
1391
1412
  onSubagentEvent: (ev) => {
1392
1413
  try {
1393
1414
  outerStream.push({
@@ -1459,7 +1480,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
1459
1480
  emitProgress(config);
1460
1481
  break;
1461
1482
  }
1462
- const toolSteered = drainSteerInbox(context, config.steerInbox);
1483
+ const toolSteered = drainSteerInbox(context, config.steerInbox, config.prompts);
1463
1484
  if (toolSteered.length > 0) {
1464
1485
  for (const preview of toolSteered) {
1465
1486
  try {
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * The model-facing `subagents` tool: one flat tool with a `command` field (`spawn | message | close | list`), a thin
3
- * wrapper over `agent.workers`. The tool manual lives in `subagents.md`, not in code. Results are plain text with one
3
+ * wrapper over `agent.workers`. The tool manual lives in `src/prompts/subagents-tool.md`, not in code. Results are plain text with one
4
4
  * header line naming the worker; the branded envelope under them carries usage and lineage to the loop.
5
5
  */
6
- import { readFileSync } from "node:fs";
7
6
  import { z } from "zod";
8
7
  import type { Agent } from "./agent.ts";
9
8
  import { tool } from "../tools/tool.ts";
@@ -11,20 +10,15 @@ import { makeSubagentResult } from "../tools/subagent-result.ts";
11
10
  import { ValidationError } from "../types/errors.ts";
12
11
  import type { ToolDefinition } from "../types/tool.ts";
13
12
  import type { WorkerReply } from "./workers.ts";
13
+ import { fillPrompt, type Prompts } from "../prompts/prompts.ts";
14
14
 
15
15
  const COMMANDS = ["spawn", "message", "close", "list"] as const;
16
16
 
17
- let manual: string | undefined;
18
- /** Read on first use (not at import), so a program that never enables workers never touches the file. */
19
- function loadManual(): string {
20
- return (manual ??= readFileSync(new URL("./subagents.md", import.meta.url), "utf8"));
21
- }
22
-
23
17
  // One worker reply as the model sees it: a header line naming the worker, then its text; in: a WorkerReply, out: the branded result.
24
- function asResult(reply: WorkerReply): ReturnType<typeof makeSubagentResult> {
18
+ function asResult(reply: WorkerReply, prompts: Prompts): ReturnType<typeof makeSubagentResult> {
25
19
  const text = reply.error
26
- ? `[worker ${reply.id}] failed: ${reply.error}`
27
- : `[worker ${reply.id}] replied:\n${reply.text}`;
20
+ ? fillPrompt(prompts.workerFailed, { id: reply.id, reason: reply.error })
21
+ : fillPrompt(prompts.workerReplied, { id: reply.id, text: reply.text });
28
22
  return makeSubagentResult(text, [reply.metadata]);
29
23
  }
30
24
 
@@ -34,14 +28,12 @@ export function createSubagentsTool(agent: Agent): ToolDefinition {
34
28
  const max = agent.dynamicSubagents?.maxSpawn ?? Infinity;
35
29
  return tool({
36
30
  name: "subagents",
37
- description: loadManual()
38
- .replace(
39
- "{{LIMIT}}",
40
- Number.isFinite(max)
41
- ? `You can have at most ${max} open workers. Close the ones you are done with.`
42
- : "There is no limit on open workers. Close the ones you are done with: each one keeps its whole conversation."
43
- )
44
- .replace("{{TOOLS}}", poolNames.length > 0 ? poolNames.join(", ") : "none (omit tools)"),
31
+ description: fillPrompt(agent.prompts.subagentsTool, {
32
+ LIMIT: Number.isFinite(max)
33
+ ? `You can have at most ${max} open workers. Close the ones you are done with.`
34
+ : "There is no limit on open workers. Close the ones you are done with: each one keeps its whole conversation.",
35
+ TOOLS: poolNames.length > 0 ? poolNames.join(", ") : "none (omit tools)",
36
+ }),
45
37
  input: z.object({
46
38
  command: z.enum(COMMANDS),
47
39
  id: z.string().optional().describe("The worker's full id (message, close)."),
@@ -89,10 +81,11 @@ export function createSubagentsTool(agent: Agent): ToolDefinition {
89
81
  history: a.history,
90
82
  },
91
83
  opts
92
- )
84
+ ),
85
+ agent.prompts
93
86
  );
94
87
  case "message":
95
- return asResult(await agent.workers.message(need("id"), need("task"), opts));
88
+ return asResult(await agent.workers.message(need("id"), need("task"), opts), agent.prompts);
96
89
  case "close":
97
90
  return `Worker ${agent.workers.close(need("id")).id} closed.`;
98
91
  case "list":
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The chat-wide spending cap (`AgentConfig.usageLimit`). One `UsageLimit` belongs to the root agent of a chat and is
3
+ * handed to every agent below it (registry workers at any depth, fixed `subagents` workers), so each model turn of the
4
+ * whole tree adds to one running total and every turn is checked before it starts. The total survives a restart: the
5
+ * first run rebuilds the starting point from the saved sessions of the agent and its workers (`usageReport()`), after
6
+ * which only new turns are added. Scheduled actions are separate and never count.
7
+ */
8
+ import { BudgetExceededError } from "../types/errors.ts";
9
+
10
+ /** What the cap is made of; either part may be set, the first one reached stops the chat. */
11
+ export interface UsageLimitConfig {
12
+ maxCostUsd?: number;
13
+ maxTotalTokens?: number;
14
+ }
15
+
16
+ /** A one-line problem with a `usageLimit` value, or undefined when it is valid; in: the Agent constructor, out: its ConfigError. */
17
+ export function checkUsageLimitConfig(raw: unknown): string | undefined {
18
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
19
+ return "expected an object like { maxCostUsd, maxTotalTokens }";
20
+ const r = raw as Record<string, unknown>;
21
+ const keys = Object.keys(r).filter((k) => r[k] !== undefined);
22
+ if (keys.length === 0) return "set maxCostUsd, maxTotalTokens or both";
23
+ for (const k of keys) {
24
+ if (k !== "maxCostUsd" && k !== "maxTotalTokens") return `unknown key "${k}" (use maxCostUsd or maxTotalTokens)`;
25
+ const v = r[k];
26
+ if (!(typeof v === "number" && Number.isFinite(v) && v > 0)) return `${k} must be a number greater than 0`;
27
+ }
28
+ return undefined;
29
+ }
30
+
31
+ /** The running total of one chat and its limit. */
32
+ export class UsageLimit {
33
+ private baseTokens = 0;
34
+ private baseMicroUSD = 0;
35
+ private addedTokens = 0;
36
+ private addedMicroUSD = 0;
37
+ private started = false;
38
+
39
+ constructor(readonly limit: UsageLimitConfig) {}
40
+
41
+ /** Takes the starting point (what the chat spent before this process) once; later calls do nothing until `restart()`. */
42
+ begin(startingPoint: () => { tokens: number; microUSD: number }): void {
43
+ if (this.started) return;
44
+ this.started = true;
45
+ const s = startingPoint();
46
+ this.baseTokens = s.tokens;
47
+ this.baseMicroUSD = s.microUSD;
48
+ this.addedTokens = 0;
49
+ this.addedMicroUSD = 0;
50
+ }
51
+
52
+ /** Forgets the total (a session was imported over this agent): the next `begin` takes a new starting point. */
53
+ restart(): void {
54
+ this.started = false;
55
+ }
56
+
57
+ /** Adds one model turn of any agent in the chat. Unpriced turns add tokens only (unknown pricing is never a made-up $0 of cost). */
58
+ add(tokens: number, microUSD: number): void {
59
+ this.addedTokens += tokens;
60
+ this.addedMicroUSD += microUSD;
61
+ }
62
+
63
+ /** What the chat has spent so far. */
64
+ spent(): { tokens: number; costUsd: number } {
65
+ return {
66
+ tokens: this.baseTokens + this.addedTokens,
67
+ costUsd: (this.baseMicroUSD + this.addedMicroUSD) / 1_000_000,
68
+ };
69
+ }
70
+
71
+ /** Throws a `BudgetExceededError` ("Usage limit reached") once the chat has spent its whole limit. */
72
+ check(): void {
73
+ const { tokens, costUsd } = this.spent();
74
+ const { maxCostUsd, maxTotalTokens } = this.limit;
75
+ const byCost = maxCostUsd !== undefined && costUsd >= maxCostUsd;
76
+ const byTokens = maxTotalTokens !== undefined && tokens >= maxTotalTokens;
77
+ if (!byCost && !byTokens) return;
78
+ const parts = [
79
+ ...(maxCostUsd !== undefined ? [`$${costUsd.toFixed(6)} of $${maxCostUsd}`] : []),
80
+ ...(maxTotalTokens !== undefined ? [`${tokens} of ${maxTotalTokens} tokens`] : []),
81
+ ];
82
+ throw new BudgetExceededError(
83
+ `[Agent Accelerator] Usage limit reached: ${parts.join(", ")} spent by this chat (the agent and all its workers).`,
84
+ {
85
+ context: {
86
+ limit: "usageLimit",
87
+ partial: { usage: { inputTokens: 0, outputTokens: 0, totalTokens: tokens }, turns: 0 },
88
+ },
89
+ }
90
+ );
91
+ }
92
+ }
@@ -246,6 +246,14 @@ export function resolveWorkerCredentials(
246
246
  return { apiKeyToUse: undefined, baseUrlToUse: undefined, attemptedProvider };
247
247
  }
248
248
 
249
+ // Configs built by `buildDynamicWorkerConfig`: the Agent constructor checks it so SDK-built workers never take `configure()` or env defaults.
250
+ const workerConfigs = new WeakSet<object>();
251
+
252
+ /** True for a config made by `buildDynamicWorkerConfig` (a chat worker or a scheduled run), not by the developer. */
253
+ export function isWorkerConfig(config: object): boolean {
254
+ return workerConfigs.has(config);
255
+ }
256
+
249
257
  /** How many levels of workers may exist below an agent unless `dynamicSubagents.maxDepth` says otherwise: unlimited. */
250
258
  export const DEFAULT_MAX_DEPTH = Infinity;
251
259
 
@@ -256,7 +264,7 @@ export const DEFAULT_MAX_DEPTH = Infinity;
256
264
  * `persistent` makes a multi-turn worker that keeps its history (saved under `persistent.dir` when given, and
257
265
  * continued by `resume()` after a crash, never automatically). A persistent worker with levels left below it
258
266
  * (`maxDepth` of the parent, minus one for itself) gets its own `subagents` tool; its children can only be granted
259
- * the tools it was granted. One-shot workers never spawn. `dynamicSubagents.workerBudget` becomes each worker's own
267
+ * the tools it was granted. One-shot workers never spawn. `dynamicSubagents.workerBudget` becomes each chat worker's own
260
268
  * per-run `budget` (and is passed to its children).
261
269
  */
262
270
  export function buildDynamicWorkerConfig(
@@ -274,7 +282,7 @@ export function buildDynamicWorkerConfig(
274
282
  const { apiKeyToUse, baseUrlToUse } = resolveWorkerCredentials(parent, model);
275
283
  const dyn = parent.dynamicSubagents;
276
284
  const levelsBelow = (dyn?.maxDepth ?? DEFAULT_MAX_DEPTH) - 1;
277
- return {
285
+ const config: AgentConfig = {
278
286
  name: worker.name,
279
287
  description: worker.role || `Sub-agent ${worker.name}`,
280
288
  instructions: worker.instructions,
@@ -284,7 +292,11 @@ export function buildDynamicWorkerConfig(
284
292
  thinkingLevel: parent.dynamicSubagents?.thinkingLevel ?? parent.thinkingConfig?.level,
285
293
  sessionId: worker.sessionId,
286
294
  tools: worker.tools,
287
- ...(dyn?.workerBudget ? { budget: { ...dyn.workerBudget } } : {}),
295
+ // A chat worker (persistent) is capped by `workerBudget`; a one-shot scheduled run only by `scheduledActionBudget`.
296
+ ...(() => {
297
+ const cap = worker.persistent ? dyn?.workerBudget : dyn?.scheduledActionBudget;
298
+ return cap ? { budget: { ...cap } } : {};
299
+ })(),
288
300
  stateless: !worker.persistent,
289
301
  ...(worker.persistent?.dir ? { persist: { dir: worker.persistent.dir }, resume: "explicit" as const } : {}),
290
302
  ...(worker.persistent && levelsBelow >= 1
@@ -305,6 +317,8 @@ export function buildDynamicWorkerConfig(
305
317
  maxTurns: parent.maxTurns,
306
318
  ...parent.inheritableConfig(),
307
319
  };
320
+ workerConfigs.add(config);
321
+ return config;
308
322
  }
309
323
 
310
324
  // ---------------------------------------------------------------------------
@@ -23,7 +23,7 @@ import {
23
23
  type WorkerRunResult,
24
24
  } from "./delegation.ts";
25
25
  import { finishedTurns, historyUpTo, type Turn } from "./turns.ts";
26
- import { modelName, readUsage, type UsageNode } from "../session/usage.ts";
26
+ import { mergeUsage, modelName, readUsage, type UsageNode } from "../session/usage.ts";
27
27
 
28
28
  /** What `spawn` needs to start a worker. */
29
29
  export interface WorkerSpec {
@@ -157,6 +157,17 @@ function readSavedWorker(dir: string | undefined): {
157
157
  return { model: session?.session.model || "unknown", totals: readUsage(session?.session.usage), workers };
158
158
  }
159
159
 
160
+ /** What a saved session folder and every worker saved below it spent in total (read from disk): the starting point of a chat-wide usage cap after a restart; in: Agent.beginChat, out: usage totals. */
161
+ export function savedSpend(dir: string): ReturnType<typeof readUsage> {
162
+ const total = readUsage(undefined);
163
+ const add = (node: { totals: ReturnType<typeof readUsage>; workers: UsageNode[] }): void => {
164
+ mergeUsage(total, node.totals);
165
+ for (const w of node.workers) add(w);
166
+ };
167
+ add(readSavedWorker(dir));
168
+ return total;
169
+ }
170
+
160
171
  /** The workers of one parent `Agent` (`agent.workers`). */
161
172
  export class WorkerRegistry {
162
173
  private readonly entries = new Map<string, Entry>();
@@ -384,6 +395,7 @@ export class WorkerRegistry {
384
395
  })
385
396
  );
386
397
  (agent as unknown as { parentSessionId?: string }).parentSessionId = this.parent.sessionId;
398
+ agent.chat = this.parent.chat; // the worker's turns count into the chat-wide cap and are checked against it
387
399
  // A cut-off run was already loaded by the constructor; otherwise bring back what the worker said so far.
388
400
  const saved = entry.dir && agent.context.messages.length === 0 ? loadSessionDir(entry.dir)?.session : undefined;
389
401
  if (saved) agent.importSession(saved);
@@ -15,6 +15,7 @@ The whole SDK. `index.ts` is the only public entry (`main`/`module`/`types`); ev
15
15
  | `tools/` | `tool()`, schemas, the executor. |
16
16
  | `streaming/` | The event stream and the SSE parser. |
17
17
  | `session/` | Persistence (snapshot + journal), usage accounting, tool logs. |
18
+ | `prompts/` | The SDK's own prompts as `.md` files, `DEFAULT_PROMPTS`, `AgentConfig.prompts`. |
18
19
  | `scheduler/` | Scheduled actions. |
19
20
  | `types/` | Shared contracts (no logic except small helpers). |
20
21
  | `utils/` | Small shared helpers (durable writes, home, env, media, retry, safe I/O). |
package/src/index.ts CHANGED
@@ -31,6 +31,10 @@ export {
31
31
  } from "./tools/tool.ts";
32
32
  export type { CreateToolOptions } from "./tools/tool.ts";
33
33
  export { zodToJsonSchema, cleanJsonSchema } from "./tools/schema.ts";
34
+ export { toolContent } from "./tools/tool-content.ts";
35
+ export { DEFAULT_PROMPTS } from "./prompts/prompts.ts";
36
+ export type { PromptName, PromptOverrides } from "./prompts/prompts.ts";
37
+ export type { ToolContent, ToolContentPart } from "./tools/tool-content.ts";
34
38
  /**
35
39
  * @deprecated Internal executor surface. Prefer `Agent` runs; for direct
36
40
  * execution import from the future `/internal` entry.
@@ -60,17 +64,12 @@ export {
60
64
  export type { AgentAsToolTarget, SubAgentTrace } from "./agent/delegation.ts";
61
65
  export type { WorkerRegistry, WorkerSpec, WorkerInfo, WorkerReply, WorkerCallOptions } from "./agent/workers.ts";
62
66
 
63
- // Scheduled actions: dynamic sub-agents that run later and save their answer to a file
64
- export {
65
- createScheduledAction,
66
- listScheduledActions,
67
- updateScheduledAction,
68
- runScheduledAction,
69
- skipScheduledAction,
70
- deleteScheduledAction,
71
- } from "./scheduler/scheduler.ts";
67
+ // Scheduled actions: dynamic sub-agents that run later and save their answer to a file (use `agent.scheduled`)
68
+ export type { ScheduledActions } from "./scheduler/scheduler.ts";
72
69
  export { getDatetimeTool } from "./scheduler/tool.ts";
73
70
  export { setHome, resolveHome } from "./utils/home.ts";
71
+ export { configure } from "./agent/defaults.ts";
72
+ export type { AgentDefaults } from "./agent/defaults.ts";
74
73
  export type {
75
74
  ScheduledActionInput,
76
75
  ScheduledActionPatch,
@@ -323,6 +322,7 @@ export type {
323
322
  ProviderContext,
324
323
  ProviderSteps,
325
324
  MediaResolution,
325
+ VideoProcessing,
326
326
  } from "./types/message.ts";
327
327
 
328
328
  export type {
@@ -0,0 +1,2 @@
1
+ [Additional Context]
2
+ {{context}}
@@ -0,0 +1,19 @@
1
+ # src/prompts/
2
+
3
+ The prompts the SDK itself puts in front of the model, as plain `.md` files, so they can be read, copied and replaced.
4
+
5
+ | File | What it is |
6
+ | :-------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7
+ | `prompts.ts` | `DEFAULT_PROMPTS` (the shipped text, read once at load, final newline removed), `resolvePrompts` (defaults + `AgentConfig.prompts`, validated), `fillPrompt` (`{{name}}` markers). |
8
+ | `reasoning-directive.md`, `structured-output.md`, `additional-context.md`, `tool-bridge.md` | Text added to the system prompt, a user turn or a history gap. |
9
+ | `tool-truncated-arguments.md`, `tool-not-found.md`, `tool-schema-error.md`, `tool-repeat-blocked.md`, `tool-interrupted.md` | The refusals the executor and the loop send back as tool results. |
10
+ | `worker-replied.md`, `worker-failed.md` | The header of a `subagents` answer. |
11
+ | `subagents-tool.md`, `scheduled-actions-tool.md` | The manuals that are the descriptions of the `subagents` and `scheduled_actions` tools. |
12
+
13
+ ## Invariants
14
+
15
+ - One file per prompt; the name in `FILES` (`prompts.ts`) is the public name. Add a prompt by adding a file, a `FILES` entry, its use, a row in `docs/guides/prompts.md` and a pinned-text assertion in `test/prompts.test.ts`.
16
+ - The default text is pinned exactly by a test: reflowing a file (an editor or formatter) changes what the model sees and the prompt cache, so the test must fail.
17
+ - Every user of a prompt reads it from the Agent's resolved prompts (`agent.prompts`, `loop` config `prompts`, `ProviderRequestOptions.prompts`, executor `prompts`), never from `DEFAULT_PROMPTS` directly, except as the default of an optional parameter.
18
+ - Machine-readable parts stay in code: the `repeat_blocked` `errorCode`, the `[Structured Output]` marker line.
19
+ - The system prompt carries no delegation text (cache-stable): tool manuals are tool descriptions.
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The prompts the SDK itself puts in front of the model, kept as `.md` files in this folder so they can be read,
3
+ * copied and replaced. `DEFAULT_PROMPTS` holds the shipped text; `AgentConfig.prompts` overrides single entries per
4
+ * Agent. Keeping the original is recommended: it is what the SDK was tested with, and a good starting point (and the
5
+ * reference) for writing your own. `{{name}}` marks a value the SDK fills in at use time.
6
+ */
7
+ import { readFileSync } from "node:fs";
8
+ import { ConfigError } from "../types/errors.ts";
9
+
10
+ /** One entry per prompt file: its public name and the file that holds the default text. */
11
+ const FILES = {
12
+ reasoningDirective: "reasoning-directive.md",
13
+ structuredOutput: "structured-output.md",
14
+ additionalContext: "additional-context.md",
15
+ toolBridge: "tool-bridge.md",
16
+ toolTruncatedArguments: "tool-truncated-arguments.md",
17
+ toolNotFound: "tool-not-found.md",
18
+ toolSchemaError: "tool-schema-error.md",
19
+ toolRepeatBlocked: "tool-repeat-blocked.md",
20
+ toolInterrupted: "tool-interrupted.md",
21
+ workerReplied: "worker-replied.md",
22
+ workerFailed: "worker-failed.md",
23
+ subagentsTool: "subagents-tool.md",
24
+ scheduledActionsTool: "scheduled-actions-tool.md",
25
+ } as const;
26
+
27
+ /** The name of one SDK prompt (see `DEFAULT_PROMPTS`). */
28
+ export type PromptName = keyof typeof FILES;
29
+
30
+ /** The text of every prompt, by name. */
31
+ export type Prompts = Record<PromptName, string>;
32
+
33
+ /** Per-Agent replacements for single prompts (`AgentConfig.prompts`); every other prompt keeps its default. */
34
+ export type PromptOverrides = Partial<Prompts>;
35
+
36
+ // Reads one prompt file without its final newline; in: DEFAULT_PROMPTS at load, out: the default text.
37
+ function readPrompt(file: string): string {
38
+ return readFileSync(new URL(`./${file}`, import.meta.url), "utf8").replace(/\n$/, "");
39
+ }
40
+
41
+ /**
42
+ * The prompts as shipped. Placeholders (`{{name}}`) are filled in by the SDK: `reasoningDirective` none;
43
+ * `structuredOutput` `{{schema}}`; `additionalContext` `{{context}}`; `toolTruncatedArguments`, `toolRepeatBlocked`
44
+ * `{{tool}}`; `toolNotFound` `{{tool}}`, `{{suggestion}}`; `toolSchemaError` `{{tool}}`, `{{detail}}`, `{{keys}}`,
45
+ * `{{expected}}`; `toolInterrupted` `{{tool}}`, `{{id}}`; `workerReplied` `{{id}}`, `{{text}}`; `workerFailed` `{{id}}`,
46
+ * `{{reason}}`; `subagentsTool` `{{TOOLS}}`, `{{LIMIT}}`; `scheduledActionsTool` `{{TOOLS}}`.
47
+ */
48
+ export const DEFAULT_PROMPTS: Readonly<Prompts> = Object.freeze(
49
+ Object.fromEntries(Object.entries(FILES).map(([name, file]) => [name, readPrompt(file)])) as Prompts
50
+ );
51
+
52
+ /** Defaults with `overrides` applied. Throws a `ConfigError` for an unknown name or an empty or non-string text. */
53
+ export function resolvePrompts(overrides?: PromptOverrides): Prompts {
54
+ const out: Prompts = { ...DEFAULT_PROMPTS };
55
+ if (overrides === undefined) return out;
56
+ if (!overrides || typeof overrides !== "object" || Array.isArray(overrides)) {
57
+ throw new ConfigError(
58
+ '[Agent Accelerator] Invalid prompts: expected an object like { reasoningDirective: "..." } (see DEFAULT_PROMPTS for the names).'
59
+ );
60
+ }
61
+ for (const [name, text] of Object.entries(overrides)) {
62
+ if (text === undefined) continue;
63
+ if (!Object.hasOwn(FILES, name)) {
64
+ throw new ConfigError(
65
+ `[Agent Accelerator] Unknown prompt "${name}". Valid names: ${Object.keys(FILES).join(", ")}.`
66
+ );
67
+ }
68
+ if (typeof text !== "string" || !text.trim()) {
69
+ throw new ConfigError(`[Agent Accelerator] Invalid prompt "${name}": expected a non-empty string.`);
70
+ }
71
+ out[name as PromptName] = text;
72
+ }
73
+ return out;
74
+ }
75
+
76
+ /** Fills `{{name}}` markers in a prompt from `values`; a marker with no value is left as written. */
77
+ export function fillPrompt(template: string, values: Record<string, string | number>): string {
78
+ return template.replace(/\{\{(\w+)\}\}/g, (whole, key: string) =>
79
+ Object.hasOwn(values, key) ? String(values[key]) : whole
80
+ );
81
+ }