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.
- package/README.md +62 -16
- package/package.json +1 -1
- package/src/agent/agent.ts +96 -26
- package/src/agent/architecture.md +15 -12
- package/src/agent/context.ts +1 -0
- package/src/agent/defaults.ts +88 -0
- package/src/agent/delegation.ts +8 -3
- package/src/agent/loop.ts +34 -13
- package/src/agent/subagents-tool.ts +14 -21
- package/src/agent/usage-limit.ts +92 -0
- package/src/agent/worker.ts +17 -3
- package/src/agent/workers.ts +13 -1
- package/src/architecture.md +1 -0
- package/src/index.ts +9 -9
- package/src/prompts/additional-context.md +2 -0
- package/src/prompts/architecture.md +19 -0
- package/src/prompts/prompts.ts +81 -0
- package/src/prompts/reasoning-directive.md +5 -0
- package/src/prompts/structured-output.md +2 -0
- package/src/prompts/tool-bridge.md +1 -0
- package/src/prompts/tool-interrupted.md +1 -0
- package/src/prompts/tool-not-found.md +1 -0
- package/src/prompts/tool-repeat-blocked.md +1 -0
- package/src/prompts/tool-schema-error.md +1 -0
- package/src/prompts/tool-truncated-arguments.md +1 -0
- package/src/prompts/worker-failed.md +1 -0
- package/src/prompts/worker-replied.md +2 -0
- package/src/providers/architecture.md +8 -8
- package/src/providers/google.ts +82 -11
- package/src/providers/openai-compat.ts +17 -7
- package/src/providers/openai.ts +10 -4
- package/src/providers/openrouter.ts +185 -18
- package/src/providers.ts +59 -9
- package/src/scheduler/architecture.md +7 -7
- package/src/scheduler/scheduler.ts +40 -0
- package/src/scheduler/tool.ts +5 -9
- package/src/session/journal.ts +1 -0
- package/src/session/store.ts +4 -0
- package/src/tools/architecture.md +7 -6
- package/src/tools/executor.ts +60 -15
- package/src/tools/tool-content.ts +32 -0
- package/src/types/agent.ts +40 -6
- package/src/types/core.ts +4 -0
- package/src/types/message.ts +7 -0
- package/src/types/model.ts +4 -1
- package/src/types/scheduler.ts +4 -4
- package/src/types/tool.ts +2 -0
- package/src/utils/errors.ts +3 -3
- package/src/utils/headers.ts +1 -1
- /package/src/{scheduler/scheduled-actions.md → prompts/scheduled-actions-tool.md} +0 -0
- /package/src/{agent/subagents.md → prompts/subagents-tool.md} +0 -0
package/src/agent/delegation.ts
CHANGED
|
@@ -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(
|
|
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 =
|
|
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"`
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
?
|
|
27
|
-
:
|
|
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:
|
|
38
|
-
.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
+
}
|
package/src/agent/worker.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|
package/src/agent/workers.ts
CHANGED
|
@@ -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);
|
package/src/architecture.md
CHANGED
|
@@ -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,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
|
+
}
|