agent-accelerator 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -57
- package/bunfig.toml +1 -0
- package/package.json +2 -3
- package/src/agent/agent.ts +414 -118
- package/src/agent/context.ts +11 -16
- package/src/agent/delegation.ts +111 -274
- package/src/agent/loop.ts +327 -99
- package/src/agent/subagent.ts +4 -12
- package/src/agent/worker.ts +435 -0
- package/src/index.ts +58 -5
- package/src/models/catalog-cache.ts +65 -25
- package/src/models/catalog.ts +47 -2
- package/src/providers/google.ts +338 -81
- package/src/providers/openai-compat.ts +171 -61
- package/src/providers/openai.ts +174 -44
- package/src/providers/openrouter.ts +129 -44
- package/src/providers/shared.ts +169 -17
- package/src/providers.ts +375 -11
- package/src/scheduler/scheduled-actions.md +41 -0
- package/src/scheduler/scheduler.ts +814 -0
- package/src/scheduler/store.ts +263 -0
- package/src/scheduler/time.ts +148 -0
- package/src/scheduler/tool.ts +151 -0
- package/src/session/store.ts +229 -113
- package/src/streaming/event-stream.ts +6 -4
- package/src/telemetry-file.ts +31 -0
- package/src/telemetry.ts +65 -0
- package/src/tools/executor.ts +163 -38
- package/src/tools/subagent-result.ts +60 -0
- package/src/types/agent.ts +74 -22
- package/src/types/model.ts +39 -0
- package/src/types/response.ts +9 -0
- package/src/types/scheduler.ts +81 -0
- package/src/types/tool.ts +15 -0
- package/src/utils/cache.ts +14 -5
- package/src/utils/documents.ts +112 -52
- package/src/utils/env.ts +21 -7
- package/src/utils/errors.ts +26 -1
- package/src/utils/headers.ts +15 -15
- package/src/utils/home.ts +30 -0
- package/src/utils/media.ts +57 -12
- package/src/utils/raw-capture.ts +76 -0
- package/src/utils/retry.ts +11 -3
- package/src/utils/safe-io.ts +252 -0
- package/src/utils/session.ts +7 -8
- package/src/data/README.md +0 -84
package/src/tools/executor.ts
CHANGED
|
@@ -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 `{
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
132
|
-
*
|
|
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
|
|
135
|
-
if (!value || typeof value !== "object" ||
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
"stack"
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
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 `
|
|
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
|
-
|
|
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
|
-
//
|
|
565
|
-
//
|
|
566
|
-
|
|
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,
|
|
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
|
-
//
|
|
579
|
-
//
|
|
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
|
+
}
|
package/src/types/agent.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
} from "./core.ts";
|
|
8
8
|
import type { ModelProviderInstance } from "../providers/registry.ts";
|
|
9
9
|
import type { Agent } from "../agent/agent.ts";
|
|
10
|
+
import type { z } from "zod";
|
|
10
11
|
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
12
13
|
// Q-52/Q-53/Q-54 canonical option types (all additive; nothing renamed).
|
|
@@ -29,12 +30,6 @@ export type ModelRef = string | ModelSpec | ModelProviderInstance;
|
|
|
29
30
|
/** Canonical reasoning option (Q-52): bare level, or level + explicit token budget. */
|
|
30
31
|
export type ThinkingOption = ThinkingLevel | { level?: ThinkingLevel; budgetTokens?: number };
|
|
31
32
|
|
|
32
|
-
/** Canonical sampling knobs (Q-54). Read by the loop via `(config as any).sampling`. */
|
|
33
|
-
export interface SamplingConfig {
|
|
34
|
-
temperature?: number;
|
|
35
|
-
topP?: number;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
33
|
/** Canonical retry policy (Q-54). Read by the loop via `(config as any).retry`. */
|
|
39
34
|
export interface RetryPolicyConfig {
|
|
40
35
|
maxRetries?: number;
|
|
@@ -61,6 +56,17 @@ export type ToolChoiceOption =
|
|
|
61
56
|
/** Canonical stop control (Q-54): sequence(s) halting generation. */
|
|
62
57
|
export type StopOption = string | string[];
|
|
63
58
|
|
|
59
|
+
/** Canonical structured-output input (Q-61). Zod, JSON Schema, or explicit form. */
|
|
60
|
+
export type StructuredOutputOption =
|
|
61
|
+
| z.ZodType
|
|
62
|
+
| Record<string, unknown>
|
|
63
|
+
| {
|
|
64
|
+
name?: string;
|
|
65
|
+
description?: string;
|
|
66
|
+
schema: z.ZodType | Record<string, unknown>;
|
|
67
|
+
strict?: boolean;
|
|
68
|
+
};
|
|
69
|
+
|
|
64
70
|
/** Configuration used to construct an {@link Agent}. */
|
|
65
71
|
export interface AgentConfig {
|
|
66
72
|
/** Display name used in tool and sub-agent metadata. */
|
|
@@ -127,24 +133,39 @@ export interface AgentConfig {
|
|
|
127
133
|
* after every step (user turn, assistant turn, tool batch, sub-agent
|
|
128
134
|
* step) so a crash loses at most the in-flight step. Pass `{ dir }` for
|
|
129
135
|
* `sessions/<sessionId>/session.json` layout or `{ file }` for a single
|
|
130
|
-
* pretty `.session.json` file.
|
|
136
|
+
* pretty `.session.json` file. `persist: {}` (neither) uses `<home>/sessions`.
|
|
137
|
+
* Writes are atomic and never fail a turn.
|
|
131
138
|
*/
|
|
132
139
|
persist?: { dir?: string; file?: string };
|
|
140
|
+
/**
|
|
141
|
+
* Folder for everything Agent Accelerator writes to disk (`sessions/`,
|
|
142
|
+
* `scheduled_actions/`, `telemetry/`). Defaults to `setHome()`, then the `AGENT_ACCEL_HOME` env var, then `.agent-accel/` in the working directory.
|
|
143
|
+
* An explicit `persist.dir` / `persist.file` still wins for sessions.
|
|
144
|
+
*/
|
|
145
|
+
home?: string;
|
|
133
146
|
// Q-54 missing knobs (all optional, additive; loop reads via `(config as any).*`).
|
|
134
|
-
/**
|
|
147
|
+
/**
|
|
148
|
+
* Canonical tool-selection control (`"auto"` default). `"none"` applies to
|
|
149
|
+
* every turn of a run; `"required"` and a pinned tool (`{ tool: "name" }`)
|
|
150
|
+
* apply to the FIRST model turn only, since forcing a tool call on every
|
|
151
|
+
* turn would make a run unable to finish.
|
|
152
|
+
*/
|
|
135
153
|
toolChoice?: ToolChoiceOption;
|
|
154
|
+
/** Google Interactions storage tier for the chain-TTL gate: `"free"` (23h, default) or `"paid"` (54d). */
|
|
155
|
+
googleTier?: "free" | "paid";
|
|
136
156
|
/** Maximum output tokens for one model call. */
|
|
137
157
|
maxOutputTokens?: number;
|
|
138
|
-
/** Sampling knobs. */
|
|
139
|
-
sampling?: SamplingConfig;
|
|
140
158
|
/** Stop sequence(s). */
|
|
141
159
|
stop?: StopOption;
|
|
142
160
|
/** Deterministic seed, when the provider supports it. */
|
|
143
161
|
seed?: number;
|
|
144
162
|
/**
|
|
145
|
-
* Optional output schema (Zod
|
|
163
|
+
* Optional output schema (Zod, JSON Schema, or `{ name, schema, strict }`,
|
|
164
|
+
* see `StructuredOutputOption`, stored as `unknown` so `zod`
|
|
146
165
|
* stays optional for type-only consumers). Agent-level only; per-run
|
|
147
|
-
* overrides are intentionally unsupported.
|
|
166
|
+
* overrides are intentionally unsupported. When set, the model answer is
|
|
167
|
+
* constrained to the schema on all providers and `AgentResponse.parsed`
|
|
168
|
+
* carries the validated value.
|
|
148
169
|
*/
|
|
149
170
|
output?: unknown;
|
|
150
171
|
/** Retry policy for provider calls. */
|
|
@@ -153,19 +174,46 @@ export interface AgentConfig {
|
|
|
153
174
|
fetch?: typeof fetch;
|
|
154
175
|
/** Provider/env overrides forwarded to key resolution. */
|
|
155
176
|
env?: Record<string, string>;
|
|
156
|
-
/**
|
|
177
|
+
/** @deprecated Not implemented yet; subscribe with `telemetryBus.onEvent` instead. Passing it emits a `config.unsupported_option` warning. */
|
|
157
178
|
logger?: AgentLogger;
|
|
158
|
-
/**
|
|
179
|
+
/** @deprecated Not implemented yet (accepted for forward compatibility; passing it emits a `config.unsupported_option` warning). */
|
|
159
180
|
hooks?: unknown;
|
|
160
|
-
/**
|
|
181
|
+
/** @deprecated Not implemented yet (accepted for forward compatibility; passing it emits a `config.unsupported_option` warning). */
|
|
161
182
|
contextManagement?: unknown;
|
|
162
|
-
/**
|
|
163
|
-
|
|
164
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Telemetry options. `persist: true` appends every `telemetryBus` event (except full reasoning
|
|
185
|
+
* text) to `<home>/telemetry/<date>.ndjson`. Off by default: the SDK writes nothing unless asked.
|
|
186
|
+
*/
|
|
187
|
+
telemetry?: { persist?: boolean };
|
|
188
|
+
/**
|
|
189
|
+
* Local file policy. With `roots` set, every local-path media part and the
|
|
190
|
+
* built-in `convert_document_to_markdown` tool must resolve inside them
|
|
191
|
+
* (`[]` denies all local paths). Without it, developer-supplied parts are
|
|
192
|
+
* unrestricted and the model-callable tool is limited to `process.cwd()`.
|
|
193
|
+
*/
|
|
165
194
|
fileAccess?: { roots?: string[] };
|
|
166
|
-
/**
|
|
195
|
+
/**
|
|
196
|
+
* Conversation handling when a run FAILS (aborts always keep partial
|
|
197
|
+
* history). `"auto"` (default) restores the pre-run state unless a tool
|
|
198
|
+
* already ran, since rolling back would erase the record of real side
|
|
199
|
+
* effects and a retry would repeat them. `"rollback"` always restores;
|
|
200
|
+
* `"keep"` never does.
|
|
201
|
+
*/
|
|
202
|
+
onFailure?: "auto" | "rollback" | "keep";
|
|
203
|
+
/**
|
|
204
|
+
* What `AgentResponse.raw` keeps of the wire exchange. `"redacted"` (default):
|
|
205
|
+
* URL, method, redacted headers and HTTP status, with request/response BODIES
|
|
206
|
+
* replaced by a content-free summary so prompts, files and model output never
|
|
207
|
+
* leak into logs via `toJSON()`. `true`: full bodies (debugging). `false`:
|
|
208
|
+
* URL, method and status only.
|
|
209
|
+
*/
|
|
167
210
|
captureRaw?: false | "redacted" | true;
|
|
168
|
-
/**
|
|
211
|
+
/**
|
|
212
|
+
* Opt-in budgets, enforced PER RUN (counters restart with every `run`/`stream`)
|
|
213
|
+
* and including sub-agent usage. Exceeding a cap throws `BudgetExceededError`
|
|
214
|
+
* with the partial usage attached. `maxCostUsd` counts only priced usage:
|
|
215
|
+
* models with unknown pricing contribute $0, never a guess.
|
|
216
|
+
*/
|
|
169
217
|
budget?: Budget;
|
|
170
218
|
/** Run + network limits (Q-22). Turn/budget caps unlimited by default; network timeouts finite. */
|
|
171
219
|
limits?: AgentRunLimits;
|
|
@@ -211,6 +259,12 @@ export interface MidSessionConfig {
|
|
|
211
259
|
export interface DynamicSubagentsConfig {
|
|
212
260
|
/** Enables the automatic `spawn_subagents` tool. Defaults to true when the object is present. */
|
|
213
261
|
enabled?: boolean;
|
|
262
|
+
/**
|
|
263
|
+
* Lets the Main Agent schedule dynamic sub-agents to run later (once or on
|
|
264
|
+
* repeat) via the `scheduled_actions` and `get_datetime` tools. Off by
|
|
265
|
+
* default: without it no tool, clock or timer exists. Needs a worker model.
|
|
266
|
+
*/
|
|
267
|
+
allowScheduledActions?: boolean;
|
|
214
268
|
/** Developer-only worker model override. Never choosable by the Main Agent LLM. Falls back to top-level `subagentModel`, then `SUB_AGENT_MODEL` env, then the parent model. */
|
|
215
269
|
model?: string | ModelSpec | ModelProviderInstance;
|
|
216
270
|
/** Max workers the Main Agent may spawn in a single `spawn_subagents` call. Extras are trimmed safely. Defaults to 4. */
|
|
@@ -278,8 +332,6 @@ export interface AgentRunOptions {
|
|
|
278
332
|
toolChoice?: ToolChoiceOption;
|
|
279
333
|
/** Per-run output-token cap. */
|
|
280
334
|
maxOutputTokens?: number;
|
|
281
|
-
/** Per-run sampling override. */
|
|
282
|
-
sampling?: SamplingConfig;
|
|
283
335
|
/** Per-run stop sequence(s). */
|
|
284
336
|
stop?: StopOption;
|
|
285
337
|
/** Per-run seed. */
|
package/src/types/model.ts
CHANGED
|
@@ -87,6 +87,7 @@ export interface ModelCapabilities {
|
|
|
87
87
|
supportsLongCacheRetention?: boolean;
|
|
88
88
|
supportsParallelToolCalls?: boolean;
|
|
89
89
|
supportsStreaming?: boolean;
|
|
90
|
+
supportsStructuredOutput?: boolean;
|
|
90
91
|
modalities?: ("text" | "image" | "audio" | "video" | "pdf")[];
|
|
91
92
|
// derived
|
|
92
93
|
supportsReasoningToggle?: boolean;
|
|
@@ -138,6 +139,24 @@ export interface ModelSpec {
|
|
|
138
139
|
raw?: RawModelData;
|
|
139
140
|
}
|
|
140
141
|
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Structured outputs — normalized, provider-agnostic spec.
|
|
144
|
+
// Canonical input is `AgentConfig.output` (Zod, JSON Schema, or explicit
|
|
145
|
+
// `{ name, schema, strict }`); `normalizeStructuredOutput` in
|
|
146
|
+
// `src/providers.ts` produces this shape for adapters.
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
/** Normalized structured-output request passed from Agent to providers. */
|
|
149
|
+
export interface StructuredOutputSpec {
|
|
150
|
+
/** Format name (1-64 chars, `[a-zA-Z0-9_-]`). Defaults to `structured_output`. */
|
|
151
|
+
name: string;
|
|
152
|
+
/** JSON Schema for the final answer (already converted from Zod when needed). */
|
|
153
|
+
schema: Record<string, unknown>;
|
|
154
|
+
/** Strict schema adherence (OpenAI/OpenRouter only; ignored by Google). Defaults to true. */
|
|
155
|
+
strict?: boolean;
|
|
156
|
+
/** Optional description of what the format is for. */
|
|
157
|
+
description?: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
141
160
|
// ---------------------------------------------------------------------------
|
|
142
161
|
// Provider plumbing — unchanged API
|
|
143
162
|
// ---------------------------------------------------------------------------
|
|
@@ -156,6 +175,26 @@ export interface ProviderRequestOptions {
|
|
|
156
175
|
env?: Record<string, string>;
|
|
157
176
|
maxRetries?: number;
|
|
158
177
|
maxRetryDelayMs?: number;
|
|
178
|
+
/** Normalized structured-output spec (see `normalizeStructuredOutput`). */
|
|
179
|
+
output?: StructuredOutputSpec;
|
|
180
|
+
/**
|
|
181
|
+
* Google Interactions storage tier for the proactive chain-TTL gate
|
|
182
|
+
* (free: 23h local gate; paid: 54d). Default `free` (safe). Display-only
|
|
183
|
+
* cache behavior is unaffected; unknown values fall back to `free`.
|
|
184
|
+
*/
|
|
185
|
+
googleTier?: "free" | "paid";
|
|
186
|
+
/** Upper bound on generated tokens for one model call. */
|
|
187
|
+
maxOutputTokens?: number;
|
|
188
|
+
/** Stop sequence(s). Chat Completions only; other wires warn+drop. */
|
|
189
|
+
stop?: string[];
|
|
190
|
+
/** Deterministic-sampling seed. Chat Completions only; other wires warn+drop. */
|
|
191
|
+
seed?: number;
|
|
192
|
+
/** `fetch` implementation used for the provider request (defaults to global `fetch`). */
|
|
193
|
+
fetch?: typeof fetch;
|
|
194
|
+
/** Base backoff delay in ms for provider retries (default 250). */
|
|
195
|
+
retryBaseDelayMs?: number;
|
|
196
|
+
/** Total time budget in ms for provider retries (default 30000). */
|
|
197
|
+
retryBudgetMs?: number;
|
|
159
198
|
}
|
|
160
199
|
|
|
161
200
|
/** Auditable request/response wire payload captured in AgentResponse. */
|