agent-accelerator 0.4.5 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -57
- package/bunfig.toml +1 -0
- package/package.json +2 -3
- package/src/agent/agent.ts +401 -120
- package/src/agent/context.ts +11 -16
- package/src/agent/delegation.ts +111 -274
- package/src/agent/loop.ts +253 -98
- package/src/agent/subagent.ts +4 -12
- package/src/agent/worker.ts +435 -0
- package/src/index.ts +39 -5
- package/src/models/catalog-cache.ts +65 -25
- package/src/models/catalog.ts +21 -2
- package/src/providers/google.ts +282 -74
- package/src/providers/openai-compat.ts +61 -41
- package/src/providers/openai.ts +46 -31
- package/src/providers/openrouter.ts +42 -31
- package/src/providers/shared.ts +110 -16
- package/src/providers.ts +119 -6
- 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 +211 -104
- 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 +57 -20
- package/src/types/model.ts +18 -0
- package/src/types/response.ts +2 -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/errors.ts +11 -0
- 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
|
@@ -30,12 +30,6 @@ export type ModelRef = string | ModelSpec | ModelProviderInstance;
|
|
|
30
30
|
/** Canonical reasoning option (Q-52): bare level, or level + explicit token budget. */
|
|
31
31
|
export type ThinkingOption = ThinkingLevel | { level?: ThinkingLevel; budgetTokens?: number };
|
|
32
32
|
|
|
33
|
-
/** Canonical sampling knobs (Q-54). Read by the loop via `(config as any).sampling`. */
|
|
34
|
-
export interface SamplingConfig {
|
|
35
|
-
temperature?: number;
|
|
36
|
-
topP?: number;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
33
|
/** Canonical retry policy (Q-54). Read by the loop via `(config as any).retry`. */
|
|
40
34
|
export interface RetryPolicyConfig {
|
|
41
35
|
maxRetries?: number;
|
|
@@ -139,16 +133,28 @@ export interface AgentConfig {
|
|
|
139
133
|
* after every step (user turn, assistant turn, tool batch, sub-agent
|
|
140
134
|
* step) so a crash loses at most the in-flight step. Pass `{ dir }` for
|
|
141
135
|
* `sessions/<sessionId>/session.json` layout or `{ file }` for a single
|
|
142
|
-
* pretty `.session.json` file.
|
|
136
|
+
* pretty `.session.json` file. `persist: {}` (neither) uses `<home>/sessions`.
|
|
137
|
+
* Writes are atomic and never fail a turn.
|
|
143
138
|
*/
|
|
144
139
|
persist?: { dir?: string; file?: string };
|
|
140
|
+
/**
|
|
141
|
+
* Folder for everything Agent Accelerator writes to disk (`sessions/`,
|
|
142
|
+
* `scheduled_actions/`, `telemetry/`). Defaults to `setHome()`, then the `AGENT_ACCEL_HOME` env var, then `.agent-accel/` in the working directory.
|
|
143
|
+
* An explicit `persist.dir` / `persist.file` still wins for sessions.
|
|
144
|
+
*/
|
|
145
|
+
home?: string;
|
|
145
146
|
// Q-54 missing knobs (all optional, additive; loop reads via `(config as any).*`).
|
|
146
|
-
/**
|
|
147
|
+
/**
|
|
148
|
+
* Canonical tool-selection control (`"auto"` default). `"none"` applies to
|
|
149
|
+
* every turn of a run; `"required"` and a pinned tool (`{ tool: "name" }`)
|
|
150
|
+
* apply to the FIRST model turn only, since forcing a tool call on every
|
|
151
|
+
* turn would make a run unable to finish.
|
|
152
|
+
*/
|
|
147
153
|
toolChoice?: ToolChoiceOption;
|
|
154
|
+
/** Google Interactions storage tier for the chain-TTL gate: `"free"` (23h, default) or `"paid"` (54d). */
|
|
155
|
+
googleTier?: "free" | "paid";
|
|
148
156
|
/** Maximum output tokens for one model call. */
|
|
149
157
|
maxOutputTokens?: number;
|
|
150
|
-
/** Sampling knobs. */
|
|
151
|
-
sampling?: SamplingConfig;
|
|
152
158
|
/** Stop sequence(s). */
|
|
153
159
|
stop?: StopOption;
|
|
154
160
|
/** Deterministic seed, when the provider supports it. */
|
|
@@ -168,19 +174,46 @@ export interface AgentConfig {
|
|
|
168
174
|
fetch?: typeof fetch;
|
|
169
175
|
/** Provider/env overrides forwarded to key resolution. */
|
|
170
176
|
env?: Record<string, string>;
|
|
171
|
-
/**
|
|
177
|
+
/** @deprecated Not implemented yet; subscribe with `telemetryBus.onEvent` instead. Passing it emits a `config.unsupported_option` warning. */
|
|
172
178
|
logger?: AgentLogger;
|
|
173
|
-
/**
|
|
179
|
+
/** @deprecated Not implemented yet (accepted for forward compatibility; passing it emits a `config.unsupported_option` warning). */
|
|
174
180
|
hooks?: unknown;
|
|
175
|
-
/**
|
|
181
|
+
/** @deprecated Not implemented yet (accepted for forward compatibility; passing it emits a `config.unsupported_option` warning). */
|
|
176
182
|
contextManagement?: unknown;
|
|
177
|
-
/**
|
|
178
|
-
|
|
179
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Telemetry options. `persist: true` appends every `telemetryBus` event (except full reasoning
|
|
185
|
+
* text) to `<home>/telemetry/<date>.ndjson`. Off by default: the SDK writes nothing unless asked.
|
|
186
|
+
*/
|
|
187
|
+
telemetry?: { persist?: boolean };
|
|
188
|
+
/**
|
|
189
|
+
* Local file policy. With `roots` set, every local-path media part and the
|
|
190
|
+
* built-in `convert_document_to_markdown` tool must resolve inside them
|
|
191
|
+
* (`[]` denies all local paths). Without it, developer-supplied parts are
|
|
192
|
+
* unrestricted and the model-callable tool is limited to `process.cwd()`.
|
|
193
|
+
*/
|
|
180
194
|
fileAccess?: { roots?: string[] };
|
|
181
|
-
/**
|
|
195
|
+
/**
|
|
196
|
+
* Conversation handling when a run FAILS (aborts always keep partial
|
|
197
|
+
* history). `"auto"` (default) restores the pre-run state unless a tool
|
|
198
|
+
* already ran, since rolling back would erase the record of real side
|
|
199
|
+
* effects and a retry would repeat them. `"rollback"` always restores;
|
|
200
|
+
* `"keep"` never does.
|
|
201
|
+
*/
|
|
202
|
+
onFailure?: "auto" | "rollback" | "keep";
|
|
203
|
+
/**
|
|
204
|
+
* What `AgentResponse.raw` keeps of the wire exchange. `"redacted"` (default):
|
|
205
|
+
* URL, method, redacted headers and HTTP status, with request/response BODIES
|
|
206
|
+
* replaced by a content-free summary so prompts, files and model output never
|
|
207
|
+
* leak into logs via `toJSON()`. `true`: full bodies (debugging). `false`:
|
|
208
|
+
* URL, method and status only.
|
|
209
|
+
*/
|
|
182
210
|
captureRaw?: false | "redacted" | true;
|
|
183
|
-
/**
|
|
211
|
+
/**
|
|
212
|
+
* Opt-in budgets, enforced PER RUN (counters restart with every `run`/`stream`)
|
|
213
|
+
* and including sub-agent usage. Exceeding a cap throws `BudgetExceededError`
|
|
214
|
+
* with the partial usage attached. `maxCostUsd` counts only priced usage:
|
|
215
|
+
* models with unknown pricing contribute $0, never a guess.
|
|
216
|
+
*/
|
|
184
217
|
budget?: Budget;
|
|
185
218
|
/** Run + network limits (Q-22). Turn/budget caps unlimited by default; network timeouts finite. */
|
|
186
219
|
limits?: AgentRunLimits;
|
|
@@ -226,6 +259,12 @@ export interface MidSessionConfig {
|
|
|
226
259
|
export interface DynamicSubagentsConfig {
|
|
227
260
|
/** Enables the automatic `spawn_subagents` tool. Defaults to true when the object is present. */
|
|
228
261
|
enabled?: boolean;
|
|
262
|
+
/**
|
|
263
|
+
* Lets the Main Agent schedule dynamic sub-agents to run later (once or on
|
|
264
|
+
* repeat) via the `scheduled_actions` and `get_datetime` tools. Off by
|
|
265
|
+
* default: without it no tool, clock or timer exists. Needs a worker model.
|
|
266
|
+
*/
|
|
267
|
+
allowScheduledActions?: boolean;
|
|
229
268
|
/** Developer-only worker model override. Never choosable by the Main Agent LLM. Falls back to top-level `subagentModel`, then `SUB_AGENT_MODEL` env, then the parent model. */
|
|
230
269
|
model?: string | ModelSpec | ModelProviderInstance;
|
|
231
270
|
/** Max workers the Main Agent may spawn in a single `spawn_subagents` call. Extras are trimmed safely. Defaults to 4. */
|
|
@@ -293,8 +332,6 @@ export interface AgentRunOptions {
|
|
|
293
332
|
toolChoice?: ToolChoiceOption;
|
|
294
333
|
/** Per-run output-token cap. */
|
|
295
334
|
maxOutputTokens?: number;
|
|
296
|
-
/** Per-run sampling override. */
|
|
297
|
-
sampling?: SamplingConfig;
|
|
298
335
|
/** Per-run stop sequence(s). */
|
|
299
336
|
stop?: StopOption;
|
|
300
337
|
/** Per-run seed. */
|
package/src/types/model.ts
CHANGED
|
@@ -177,6 +177,24 @@ export interface ProviderRequestOptions {
|
|
|
177
177
|
maxRetryDelayMs?: number;
|
|
178
178
|
/** Normalized structured-output spec (see `normalizeStructuredOutput`). */
|
|
179
179
|
output?: StructuredOutputSpec;
|
|
180
|
+
/**
|
|
181
|
+
* Google Interactions storage tier for the proactive chain-TTL gate
|
|
182
|
+
* (free: 23h local gate; paid: 54d). Default `free` (safe). Display-only
|
|
183
|
+
* cache behavior is unaffected; unknown values fall back to `free`.
|
|
184
|
+
*/
|
|
185
|
+
googleTier?: "free" | "paid";
|
|
186
|
+
/** Upper bound on generated tokens for one model call. */
|
|
187
|
+
maxOutputTokens?: number;
|
|
188
|
+
/** Stop sequence(s). Chat Completions only; other wires warn+drop. */
|
|
189
|
+
stop?: string[];
|
|
190
|
+
/** Deterministic-sampling seed. Chat Completions only; other wires warn+drop. */
|
|
191
|
+
seed?: number;
|
|
192
|
+
/** `fetch` implementation used for the provider request (defaults to global `fetch`). */
|
|
193
|
+
fetch?: typeof fetch;
|
|
194
|
+
/** Base backoff delay in ms for provider retries (default 250). */
|
|
195
|
+
retryBaseDelayMs?: number;
|
|
196
|
+
/** Total time budget in ms for provider retries (default 30000). */
|
|
197
|
+
retryBudgetMs?: number;
|
|
180
198
|
}
|
|
181
199
|
|
|
182
200
|
/** Auditable request/response wire payload captured in AgentResponse. */
|
package/src/types/response.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface SubAgentStep {
|
|
|
9
9
|
name?: string;
|
|
10
10
|
text?: string;
|
|
11
11
|
thinking?: string;
|
|
12
|
+
/** Opaque provider thought signature captured with this step's reasoning, when present. */
|
|
13
|
+
thoughtSignature?: string;
|
|
12
14
|
isError?: boolean;
|
|
13
15
|
timestamp: number;
|
|
14
16
|
/** True while the worker is still streaming this step; finalized on turn end. */
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** Lifecycle of a scheduled action as stored in `tasks.json`. */
|
|
2
|
+
export type ScheduledState = "waiting" | "cooking" | "missed" | "done";
|
|
3
|
+
|
|
4
|
+
/** Outcome of one run. */
|
|
5
|
+
export type ScheduledRunStatus = "ok" | "failed";
|
|
6
|
+
|
|
7
|
+
/** Schedule fields shared by `create` input and the stored record. */
|
|
8
|
+
export interface ScheduleSpec {
|
|
9
|
+
/** Delivery clock time `DD:HH:MM`: `DD` days from today, `HH:MM` on a 12-hour clock (see `meridiem`). */
|
|
10
|
+
timeToDeliver: string;
|
|
11
|
+
meridiem: "AM" | "PM";
|
|
12
|
+
/** Duration `DD:HH:MM` before delivery at which the run starts. Default 10 minutes (`00:00:10`). */
|
|
13
|
+
startCookingInAdvance?: string;
|
|
14
|
+
/** Duration `DD:HH:MM` between deliveries (`01:00:00` daily). Omit to run once. */
|
|
15
|
+
repeatEvery?: string;
|
|
16
|
+
/** Extra attempts if a run fails, only while time remains before delivery. Default 0. */
|
|
17
|
+
retries?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** What `createScheduledAction` receives: dynamic sub-agent fields plus the schedule. */
|
|
21
|
+
export interface ScheduledActionInput {
|
|
22
|
+
role?: string;
|
|
23
|
+
instructions: string;
|
|
24
|
+
task: string;
|
|
25
|
+
/** Names from the developer's `dynamicSubagents.tools` pool. */
|
|
26
|
+
tools?: string[];
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
scheduled_action: ScheduleSpec & {
|
|
29
|
+
/** Human label. */
|
|
30
|
+
name: string;
|
|
31
|
+
/** Lowercase kebab-case, max 48 chars. The SDK appends 6 random chars. */
|
|
32
|
+
id: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** What `updateScheduledAction` may change. `id` and the creating agent are immutable. */
|
|
37
|
+
export type ScheduledActionPatch = Partial<Omit<ScheduledActionInput, "scheduled_action">> & {
|
|
38
|
+
scheduled_action?: Partial<Omit<ScheduledActionInput["scheduled_action"], "id">>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** One entry of `tasks.json`. All times are local clock strings `YYYY-MM-DDTHH:mm:ss` (no offset). */
|
|
42
|
+
export interface ScheduledActionRecord {
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
/** Name of the creating Agent; it runs the worker. */
|
|
46
|
+
agent: string;
|
|
47
|
+
role?: string;
|
|
48
|
+
instructions: string;
|
|
49
|
+
task: string;
|
|
50
|
+
/** Tool NAMES only; resolved against the live agent's pool at run time. */
|
|
51
|
+
tools: string[];
|
|
52
|
+
timeoutMs?: number;
|
|
53
|
+
/** The schedule as given. The stored truth is `nextCookAt` / `nextDeliverAt`. */
|
|
54
|
+
schedule: ScheduleSpec & { startCookingInAdvance: string };
|
|
55
|
+
state: ScheduledState;
|
|
56
|
+
/** `null` for a finished one-shot. */
|
|
57
|
+
nextCookAt: string | null;
|
|
58
|
+
nextDeliverAt: string | null;
|
|
59
|
+
createdAt: string;
|
|
60
|
+
/** Present only while `state` is `cooking`. */
|
|
61
|
+
current?: { cookStartedAt: string; deliverAt: string };
|
|
62
|
+
lastRun?: { status: ScheduledRunStatus; file: string; deliverAt: string; deliveredAt: string; late: boolean };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Payload of the `scheduled_action.delivered` telemetry event. Plain JSON. */
|
|
66
|
+
export interface ScheduledDelivery {
|
|
67
|
+
id: string;
|
|
68
|
+
name: string;
|
|
69
|
+
status: ScheduledRunStatus;
|
|
70
|
+
/** Absolute path of the run file. */
|
|
71
|
+
file: string;
|
|
72
|
+
readyAt: string;
|
|
73
|
+
deliverAt: string;
|
|
74
|
+
late: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Result of `killScheduledAction`. */
|
|
78
|
+
export type KillResult =
|
|
79
|
+
| { ok: true }
|
|
80
|
+
| { ok: false; reason: "not_found" }
|
|
81
|
+
| { ok: false; reason: "running"; message: string };
|
package/src/types/tool.ts
CHANGED
|
@@ -136,3 +136,18 @@ export interface ToolResultRecord {
|
|
|
136
136
|
isError?: boolean;
|
|
137
137
|
durationMs?: number;
|
|
138
138
|
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Key of the marker object that replaces tool arguments whose streamed JSON
|
|
142
|
+
* never parsed (`{ __truncated_raw: "<partial text>" }`). It is deliberately
|
|
143
|
+
* not a plausible parameter name, so a real tool with a `raw` parameter is
|
|
144
|
+
* never mistaken for a truncated call. Such calls are never executed.
|
|
145
|
+
*/
|
|
146
|
+
export const TRUNCATED_ARGS_KEY = "__truncated_raw";
|
|
147
|
+
|
|
148
|
+
/** True when `args` is exactly the truncated-arguments marker. */
|
|
149
|
+
export function isTruncatedArguments(args: unknown): boolean {
|
|
150
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return false;
|
|
151
|
+
const keys = Object.keys(args as Record<string, unknown>);
|
|
152
|
+
return keys.length === 1 && keys[0] === TRUNCATED_ARGS_KEY && typeof (args as Record<string, unknown>)[TRUNCATED_ARGS_KEY] === "string";
|
|
153
|
+
}
|