agent-accelerator 0.5.6 → 0.5.7
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 +6 -13
- package/package.json +1 -1
- package/src/agent/agent.ts +16 -18
- package/src/agent/architecture.md +25 -0
- package/src/agent/context.ts +4 -0
- package/src/agent/delegation.ts +18 -84
- package/src/agent/loop.ts +13 -15
- package/src/agent/worker.ts +1 -3
- package/src/architecture.md +28 -0
- package/src/index.ts +6 -73
- package/src/models/architecture.md +14 -0
- package/src/models/catalog-cache.ts +3 -0
- package/src/models/catalog.ts +7 -0
- package/src/providers/architecture.md +24 -0
- package/src/providers/google.ts +20 -0
- package/src/providers/openai-compat.ts +22 -0
- package/src/providers/openai.ts +17 -1
- package/src/providers/openrouter.ts +12 -0
- package/src/providers/registry.ts +1 -0
- package/src/providers.ts +9 -53
- package/src/scheduler/architecture.md +19 -0
- package/src/scheduler/scheduler.ts +21 -0
- package/src/scheduler/store.ts +6 -0
- package/src/scheduler/time.ts +2 -0
- package/src/session/architecture.md +19 -0
- package/src/session/store.ts +15 -0
- package/src/session/usage.ts +1 -0
- package/src/streaming/architecture.md +13 -0
- package/src/streaming/event-stream.ts +5 -0
- package/src/telemetry.ts +1 -0
- package/src/tools/architecture.md +19 -0
- package/src/tools/executor.ts +13 -0
- package/src/tools/schema.ts +1 -0
- package/src/tools/subagent-result.ts +1 -0
- package/src/types/architecture.md +20 -0
- package/src/utils/architecture.md +25 -0
- package/src/utils/base64.ts +2 -0
- package/src/utils/cache.ts +2 -0
- package/src/utils/errors.ts +4 -0
- package/src/utils/headers.ts +4 -1
- package/src/utils/media.ts +1 -0
- package/src/utils/raw-capture.ts +3 -0
- package/src/utils/retry.ts +1 -0
- package/src/utils/safe-io.ts +4 -0
- package/src/utils/session.ts +5 -161
package/README.md
CHANGED
|
@@ -386,6 +386,8 @@ telemetryBus.onEvent((e) => {
|
|
|
386
386
|
| `skipScheduledAction(id, { home? })` | Ignore a missed run and wait for the next scheduled time. |
|
|
387
387
|
| `deleteScheduledAction(id, { force?, purge?, home? })` | Delete for good. While it is cooking nothing changes unless `force: true`; `purge: true` also deletes past run files. |
|
|
388
388
|
|
|
389
|
+
Every function except `createScheduledAction` finds the home folder through `setHome()`, `AGENT_ACCEL_HOME`, or `.agent-accel/` in the working directory. If you built the Agent with its own `home`, pass the same `{ home }` to these calls (the model's `scheduled_actions` tool already does).
|
|
390
|
+
|
|
389
391
|
**How it behaves**
|
|
390
392
|
|
|
391
393
|
* **One local clock.** A single timer per home folder (guarded by a one-process lock, `<home>/.lock`) ticks once a second against the machine's own time and only asks what is overdue, so drift or sleep cannot skip a run. There is no timezone or daylight-saving logic. The timer is `unref`'d and runs only while something is pending, so actions run only while your process is alive.
|
|
@@ -657,7 +659,7 @@ Implement the `Provider` interface when adding a fully custom transport.
|
|
|
657
659
|
|
|
658
660
|
### Google
|
|
659
661
|
|
|
660
|
-
`GoogleInteractionsProvider`
|
|
662
|
+
`GoogleInteractionsProvider` and `GOOGLE_MODELS` provide Google support over the Interactions API (`POST {baseUrl}/interactions`, streaming via `?alt=sse`).
|
|
661
663
|
|
|
662
664
|
Turns chain statefully through `previous_interaction_id` per session, falling back to stateless full-history sends when the session switches providers or models.
|
|
663
665
|
|
|
@@ -683,7 +685,7 @@ Helpers:
|
|
|
683
685
|
|
|
684
686
|
### OpenAI
|
|
685
687
|
|
|
686
|
-
`OpenAIResponsesProvider`
|
|
688
|
+
`OpenAIResponsesProvider` and `OPENAI_MODELS` provide OpenAI support over the Responses API (`POST {baseUrl}/responses`).
|
|
687
689
|
|
|
688
690
|
Every turn is stateless: the full history is sent explicitly with `store: false`, so no `previous_response_id`, `background`, or conversation chaining is ever used.
|
|
689
691
|
|
|
@@ -702,7 +704,7 @@ It also:
|
|
|
702
704
|
|
|
703
705
|
### OpenRouter
|
|
704
706
|
|
|
705
|
-
`OpenRouterChatCompletionsProvider`
|
|
707
|
+
`OpenRouterChatCompletionsProvider` and `OPENROUTER_MODELS` provide OpenRouter support over Chat Completions (`POST {baseUrl}/chat/completions`).
|
|
706
708
|
|
|
707
709
|
Every turn is stateless: the full `messages[]` array is sent explicitly — no chaining primitives exist on this endpoint.
|
|
708
710
|
|
|
@@ -731,7 +733,7 @@ setAttribution({ referer: "https://my.app", title: "My App" });
|
|
|
731
733
|
|
|
732
734
|
### Custom
|
|
733
735
|
|
|
734
|
-
`
|
|
736
|
+
`OpenAICompatibleChatProvider` is also exposed through the `CustomProvider`, `createCustomProvider`, and `createOpenAICompatibleProvider(prefix, opts?)` aliases.
|
|
735
737
|
|
|
736
738
|
`CustomProviderOptions`:
|
|
737
739
|
|
|
@@ -931,15 +933,6 @@ Use:
|
|
|
931
933
|
Breaking out of `for await` cancels the stream. Your `AbortSignal` and stream cancellation share one linked controller, so `controller.abort()` and `stream.cancel()` both stop the HTTP request. Abort rejects with `AbortError` (never retried) and never resolves partial results. `agent.run(prompt, { stream: true, signal })` exposes the same
|
|
932
934
|
`cancel` handle.
|
|
933
935
|
|
|
934
|
-
`SSEParser` provides:
|
|
935
|
-
|
|
936
|
-
```ts
|
|
937
|
-
feed(chunk)
|
|
938
|
-
flush()
|
|
939
|
-
```
|
|
940
|
-
|
|
941
|
-
It parses provider SSE streams and is only needed when implementing custom transports.
|
|
942
|
-
|
|
943
936
|
---
|
|
944
937
|
## Response, Usage, and Raw Data
|
|
945
938
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-accelerator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"description": "High-performance SDK for AI agents with a unified API across AI providers. Optimized for high cache hit rates, observability, and seamless agent orchestration. Built to power production-grade agentic systems and harnesses for teams at any scale.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Sashvat Bharat",
|
package/src/agent/agent.ts
CHANGED
|
@@ -29,7 +29,7 @@ import { applyEditable, assertModelAllowed, modelKey, toEditableConfig } from ".
|
|
|
29
29
|
import { assertNoToolCollisions } from "../tools/executor.ts";
|
|
30
30
|
import { runAgentLoop, streamAgentLoop, unfinishedToolCalls, endsWithStop, accumulateUsage, type SteerEntry } from "./loop.ts";
|
|
31
31
|
import { isRetryableRun, readRetryAfterMs, sleep } from "../utils/retry.ts";
|
|
32
|
-
import { createSessionId, nowMs, newRunId } from "../utils/session.ts";
|
|
32
|
+
import { createSessionId, nowMs, newRunId, previewPrompt } from "../utils/session.ts";
|
|
33
33
|
import {
|
|
34
34
|
chargeBudgetUsage,
|
|
35
35
|
checkBudget,
|
|
@@ -590,7 +590,7 @@ export class Agent {
|
|
|
590
590
|
.map((m) => ({
|
|
591
591
|
runId: m.id,
|
|
592
592
|
startedAt: m.at,
|
|
593
|
-
prompt:
|
|
593
|
+
prompt: previewPrompt((this.context.messages[m.i]?.content ?? "") as string | ContentPart[]),
|
|
594
594
|
pendingToolCalls: calls,
|
|
595
595
|
}));
|
|
596
596
|
}
|
|
@@ -652,6 +652,7 @@ export class Agent {
|
|
|
652
652
|
this.doPersist();
|
|
653
653
|
}
|
|
654
654
|
|
|
655
|
+
// Builds the system prompt: the developer's instructions plus, when thinking is on, the fixed Reasoning Directive; in: this.instructions + thinkingConfig, out: every AgentContext.systemPrompt.
|
|
655
656
|
private getFullInstructions(): string | undefined {
|
|
656
657
|
const base = this.instructions || "";
|
|
657
658
|
let out = base;
|
|
@@ -799,12 +800,14 @@ export class Agent {
|
|
|
799
800
|
return this.midSessionConfig.mode;
|
|
800
801
|
}
|
|
801
802
|
|
|
803
|
+
// Picks steer or queue for a call that arrives while busy: the developer's enforced midSession mode wins over the per-call request; in: run()/stream() `enqueue`, out: their busy branch.
|
|
802
804
|
private resolveEnqueueMode(requested?: "steer" | "queue"): "steer" | "queue" {
|
|
803
805
|
const enforced = this.midSessionConfig.mode;
|
|
804
806
|
if (enforced === "steer" || enforced === "queue") return enforced;
|
|
805
807
|
return requested === "steer" ? "steer" : "queue";
|
|
806
808
|
}
|
|
807
809
|
|
|
810
|
+
// Throws synchronously on an empty content-part array so run() and stream() fail at the same moment; in: any prompt entry point, out: nothing.
|
|
808
811
|
private assertPromptValid(prompt: string | ContentPart[]): void {
|
|
809
812
|
if (Array.isArray(prompt) && prompt.length === 0) {
|
|
810
813
|
throw new Error(
|
|
@@ -813,19 +816,6 @@ export class Agent {
|
|
|
813
816
|
}
|
|
814
817
|
}
|
|
815
818
|
|
|
816
|
-
private previewPrompt(prompt: string | ContentPart[]): string {
|
|
817
|
-
if (typeof prompt === "string") return prompt.slice(0, 500);
|
|
818
|
-
try {
|
|
819
|
-
const texts = prompt
|
|
820
|
-
.filter((p) => (p as { type?: unknown }).type === "text")
|
|
821
|
-
.map((p) => (p as { text?: string }).text ?? "")
|
|
822
|
-
.join("\n");
|
|
823
|
-
return (texts || "[multipart]").slice(0, 500);
|
|
824
|
-
} catch {
|
|
825
|
-
return "[multipart]";
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
|
|
829
819
|
/**
|
|
830
820
|
* Stops the active run now (STOP.) and, given a message, starts it next (STOP. Do this instead). The provider
|
|
831
821
|
* request is aborted (connection closed, so the provider stops generating where it can), the stream is cancelled, and
|
|
@@ -961,13 +951,14 @@ export class Agent {
|
|
|
961
951
|
try {
|
|
962
952
|
this.currentRun?.outerStream?.push({
|
|
963
953
|
type: "queued",
|
|
964
|
-
queuedPrompt:
|
|
954
|
+
queuedPrompt: previewPrompt(prompt),
|
|
965
955
|
queueLength: this.pendingQueue.length,
|
|
966
956
|
} as any);
|
|
967
957
|
} catch {}
|
|
968
958
|
});
|
|
969
959
|
}
|
|
970
960
|
|
|
961
|
+
// Starts the next waiting prompt (urgent ones first, aborted ones rejected) as a streaming or non-streaming run once the agent is idle; in: finishCurrentRun and queue changes, out: a new run whose result settles the queued caller.
|
|
971
962
|
private pumpQueue(): void {
|
|
972
963
|
if (this.currentRun !== null) return;
|
|
973
964
|
const next = this.pendingQueue.shift();
|
|
@@ -1023,6 +1014,7 @@ export class Agent {
|
|
|
1023
1014
|
this.startNonStreamingRun(next.prompt, next.options).then(next.resolve, next.reject);
|
|
1024
1015
|
}
|
|
1025
1016
|
|
|
1017
|
+
// Ends the active run slot: leftover steers become follow-ups at the FRONT of the queue (behind urgent ones), then the queue is pumped; in: a settled run, out: pendingQueue + persistNow.
|
|
1026
1018
|
private finishCurrentRun(): void {
|
|
1027
1019
|
// Q-20: leftover steer entries become queued follow-ups instead of dropping.
|
|
1028
1020
|
let leftover: SteerEntry[] = [];
|
|
@@ -1065,6 +1057,7 @@ export class Agent {
|
|
|
1065
1057
|
queueMicrotask(() => this.pumpQueue());
|
|
1066
1058
|
}
|
|
1067
1059
|
|
|
1060
|
+
// Removes the abort listener a run's merged signal fallback installed; in: a settled run's controller, out: nothing.
|
|
1068
1061
|
private cleanupMerge(runController: AbortController): void {
|
|
1069
1062
|
try {
|
|
1070
1063
|
const dispose = mergeDisposers.get(runController);
|
|
@@ -1075,6 +1068,7 @@ export class Agent {
|
|
|
1075
1068
|
} catch {}
|
|
1076
1069
|
}
|
|
1077
1070
|
|
|
1071
|
+
// Joins the caller's AbortSignal with the run's own controller (AbortSignal.any, else a manual forward) so both stop the run; in: AgentRunOptions.signal + the run controller, out: the signal handed to the loop.
|
|
1078
1072
|
private mergeUserSignal(userSignal?: AbortSignal, runController?: AbortController): AbortSignal | undefined {
|
|
1079
1073
|
if (!runController) return userSignal;
|
|
1080
1074
|
if (!userSignal) return runController.signal;
|
|
@@ -1227,6 +1221,7 @@ export class Agent {
|
|
|
1227
1221
|
};
|
|
1228
1222
|
}
|
|
1229
1223
|
|
|
1224
|
+
// Runs one non-streaming run as a task: catalog refresh, model resolve, prompt/mark (or a resumed mark), loop with `runRetries` backoff, trace merge, run ending recorded; in: run()/queue/resume(), out: the AgentResponse promise.
|
|
1230
1225
|
private startNonStreamingRun(
|
|
1231
1226
|
prompt: string | ContentPart[],
|
|
1232
1227
|
options?: AgentRunOptions,
|
|
@@ -1328,6 +1323,7 @@ export class Agent {
|
|
|
1328
1323
|
return task;
|
|
1329
1324
|
}
|
|
1330
1325
|
|
|
1326
|
+
// Starts one streaming run: validates the model and thinking level synchronously, adds the prompt, builds the loop config and returns the loop's event stream, recording the run ending when it settles; in: stream()/pumpQueue, out: an AssistantMessageEventStream.
|
|
1331
1327
|
private startStreamingRun(
|
|
1332
1328
|
prompt: string | ContentPart[],
|
|
1333
1329
|
options?: AgentRunOptions
|
|
@@ -1655,6 +1651,7 @@ export class Agent {
|
|
|
1655
1651
|
} catch {}
|
|
1656
1652
|
}
|
|
1657
1653
|
|
|
1654
|
+
// Adds the user prompt to the target context (fresh context when stateless, stable system prompt, additionalContext as a user prefix) and marks the run start for the default session; in: a run start, out: the new SessionRunMark.
|
|
1658
1655
|
private prepareTurn(prompt: string | ContentPart[], options?: AgentRunOptions, target?: AgentContext): SessionRunMark | undefined {
|
|
1659
1656
|
// Q-20: prompt validation throws synchronously in both run() and
|
|
1660
1657
|
// stream() (same timing; documented here).
|
|
@@ -1791,7 +1788,7 @@ export class Agent {
|
|
|
1791
1788
|
try {
|
|
1792
1789
|
this.currentRun?.outerStream?.push({
|
|
1793
1790
|
type: "queued",
|
|
1794
|
-
queuedPrompt:
|
|
1791
|
+
queuedPrompt: previewPrompt(prompt),
|
|
1795
1792
|
queueLength: this.pendingQueue.length,
|
|
1796
1793
|
} as any);
|
|
1797
1794
|
} catch {}
|
|
@@ -1933,7 +1930,7 @@ export class Agent {
|
|
|
1933
1930
|
try {
|
|
1934
1931
|
this.currentRun.outerStream?.push({
|
|
1935
1932
|
type: "queued",
|
|
1936
|
-
queuedPrompt:
|
|
1933
|
+
queuedPrompt: previewPrompt(prompt),
|
|
1937
1934
|
queueLength: this.pendingQueue.length + 1,
|
|
1938
1935
|
} as any);
|
|
1939
1936
|
} catch {}
|
|
@@ -2008,6 +2005,7 @@ export class Agent {
|
|
|
2008
2005
|
return s;
|
|
2009
2006
|
}
|
|
2010
2007
|
|
|
2008
|
+
// Attaches onEvent/onDelta/onThinkingDelta to a stream and, with wrapThinking, adds display-only <think> tags and whitespace hygiene on the outer events; in: stream()/pumpQueue, out: listeners on the stream.
|
|
2011
2009
|
private wireStreamCallbacks(
|
|
2012
2010
|
s: AssistantMessageEventStream,
|
|
2013
2011
|
options?: AgentRunOptions
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# src/agent/
|
|
2
|
+
|
|
3
|
+
The agent and everything that runs under it.
|
|
4
|
+
|
|
5
|
+
| File | What it is | Used by |
|
|
6
|
+
| :-- | :-- | :-- |
|
|
7
|
+
| `agent.ts` | `Agent`: `run`/`stream`/`ask`, `steer`/`queue`/`interrupt`, persistence hooks, `resume()`/`interrupted()`, `usageReport()`, `fork()`, `toConfig()`/`fromConfig()`, the `workers` getter. | callers, workers, scheduler |
|
|
8
|
+
| `loop.ts` | `runAgentLoop` / `streamAgentLoop`: per turn budget, pairing repair, file access, provider call, tool execution, repeat guard. Also `ensureToolPairing`, `accumulateUsage`. | `Agent` |
|
|
9
|
+
| `context.ts` | `AgentContext`: the validated message list with checkpoint/rollback. | `Agent`, loops |
|
|
10
|
+
| `delegation.ts` | Fixed workers (`agentToTool`, `buildAgentTools`), the live worker trace store, session-id helpers, budget helpers, `openWorkerTrace`/`workerMetadata`. | `Agent`, `workers.ts` |
|
|
11
|
+
| `worker.ts` | Worker core: model/tool normalization, `buildDynamicWorkerConfig` (shared preset), `createLiveWorker`. | `Agent`, registry, scheduler |
|
|
12
|
+
| `workers.ts` | `WorkerRegistry` (`agent.workers`): persistent multi-turn workers. | `Agent`, `subagents` tool |
|
|
13
|
+
| `subagents-tool.ts` + `subagents.md` | The one model-facing `subagents` tool; the `.md` is its description. | `Agent` constructor |
|
|
14
|
+
| `subagent.ts` | `SubAgent` class and `agentFromEnv` (thin, kept for compatibility). | callers |
|
|
15
|
+
| `turns.ts` | Pure grouping of messages into finished turns (history cut for workers). | `workers.ts` |
|
|
16
|
+
| `config-blocks.ts` | RSI blocks: only `model`, `instructions`, `tools` are editable; `allowedModels` check. | `Agent` |
|
|
17
|
+
|
|
18
|
+
## Invariants
|
|
19
|
+
- `runAgentLoop` and `streamAgentLoop` duplicate their turn logic: every loop change goes into both.
|
|
20
|
+
- An `Agent` runs one thing at a time; delegation forks a copy per call so parallel calls never queue.
|
|
21
|
+
- A worker is a plain `Agent` with a parent link and presets. Never duplicate logic between Agent and worker.
|
|
22
|
+
- A worker is never less constrained than its parent (`inheritableConfig()`); its children draw only from the tools it was granted.
|
|
23
|
+
- Exactly-once worker calls rest on the call key (parent run mark id + tool call id) and `meta.last`.
|
|
24
|
+
- Only results branded by `makeSubagentResult` have their usage metadata trusted.
|
|
25
|
+
- Each Agent records only its own model turns; never sum `AgentResponse.usage` for totals.
|
package/src/agent/context.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { clearInteractionChains } from "../providers/google.ts";
|
|
|
6
6
|
|
|
7
7
|
const USER_PART_TYPES = new Set(["text", "image", "audio", "video", "file"]);
|
|
8
8
|
|
|
9
|
+
// Throws the AgentContext validation error with the SDK prefix; in: the add* validators, out: a thrown Error.
|
|
9
10
|
function fail(message: string): never {
|
|
10
11
|
throw new Error(`[Agent Accelerator] AgentContext: ${message}`);
|
|
11
12
|
}
|
|
@@ -32,6 +33,7 @@ export class AgentContext {
|
|
|
32
33
|
this.systemPrompt = systemPrompt;
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
// Validates a user prompt (string or media/text parts) and appends it; in: Agent.prepareTurn and drained steers, out: messages.
|
|
35
37
|
addUserMessage(content: string | ContentPart[]): void {
|
|
36
38
|
if (Array.isArray(content)) {
|
|
37
39
|
if (content.length === 0) return;
|
|
@@ -120,6 +122,7 @@ export class AgentContext {
|
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
// Appends one tool message per result, keeping logFile and stopped marks; in: the loops after a tool batch, out: messages.
|
|
123
126
|
addToolResults(results: ToolResultRecord[]): void {
|
|
124
127
|
if (!results || results.length === 0) return;
|
|
125
128
|
for (let i = 0; i < results.length; i++) {
|
|
@@ -145,6 +148,7 @@ export class AgentContext {
|
|
|
145
148
|
}
|
|
146
149
|
}
|
|
147
150
|
|
|
151
|
+
// Copies the context with media bytes preserved (structuredClone, else a deep copy); in: callers needing an isolated history, out: a new AgentContext.
|
|
148
152
|
clone(): AgentContext {
|
|
149
153
|
const next = new AgentContext(this.systemPrompt);
|
|
150
154
|
// Preserve Uint8Array/ArrayBuffer media via structuredClone when available (M8 fix)
|
package/src/agent/delegation.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { tool } from "../tools/tool.ts";
|
|
3
|
-
import { escapeXml } from "../utils/serialization.ts";
|
|
4
3
|
import type { ToolDefinition } from "../types/tool.ts";
|
|
5
4
|
import type { AgentResponse, SubAgentExecutionMetadata, SubAgentStep } from "../types/response.ts";
|
|
6
5
|
import type { TokenUsage } from "../types/core.ts";
|
|
@@ -17,8 +16,7 @@ import { newRunId } from "../utils/session.ts";
|
|
|
17
16
|
import { newTurnId } from "../utils/session.ts";
|
|
18
17
|
import { sanitizeToolName, createLiveWorker } from "./worker.ts";
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
/** 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. */
|
|
22
20
|
export function createChildSessionId(parentId: string, tag: string): string {
|
|
23
21
|
let rand: string;
|
|
24
22
|
try {
|
|
@@ -104,17 +102,18 @@ const subagentTraces = new Map<string, SubAgentTrace>();
|
|
|
104
102
|
const subagentListeners = new Map<string, Set<(trace: SubAgentTrace) => void>>();
|
|
105
103
|
|
|
106
104
|
/** Max trace keys held (LRU, evicts oldest whole trace on insert) (Q-30). */
|
|
107
|
-
|
|
105
|
+
const MAX_SUBAGENT_TRACES = 500;
|
|
108
106
|
/** Trace TTL: 1h wall-clock, lazily expired on read (Q-30). */
|
|
109
|
-
|
|
107
|
+
const SUBAGENT_TRACE_TTL_MS = 60 * 60 * 1000;
|
|
110
108
|
/** Max steps retained per trace; oldest steps are dropped beyond this (Q-30). */
|
|
111
|
-
|
|
109
|
+
const MAX_SUBAGENT_STEPS_PER_TRACE = 100;
|
|
112
110
|
/** Live subscriptions auto-expire after 5min (Q-30). */
|
|
113
|
-
|
|
111
|
+
const SUBAGENT_SUBSCRIBE_TTL_MS = 5 * 60 * 1000;
|
|
114
112
|
|
|
115
113
|
/** Last-write wall-clock per canonical trackingId (drives TTL; reads do not extend it). */
|
|
116
114
|
const traceTouchedAt = new Map<string, number>();
|
|
117
115
|
|
|
116
|
+
// Removes a trace under all its keys plus its listeners and TTL stamp; in: pruneTraces/lookupTrace, out: the trace store.
|
|
118
117
|
function deleteTraceByCanonical(canonical: string): void {
|
|
119
118
|
for (const [key, t] of [...subagentTraces]) {
|
|
120
119
|
if (t.trackingId === canonical) subagentTraces.delete(key);
|
|
@@ -123,12 +122,14 @@ function deleteTraceByCanonical(canonical: string): void {
|
|
|
123
122
|
subagentListeners.delete(canonical);
|
|
124
123
|
}
|
|
125
124
|
|
|
125
|
+
// True once a trace was not written for SUBAGENT_TRACE_TTL_MS; in: lookup and prune, out: their expiry decision.
|
|
126
126
|
function isTraceExpired(canonical: string, now = Date.now()): boolean {
|
|
127
127
|
const touched = traceTouchedAt.get(canonical);
|
|
128
128
|
if (touched === undefined) return false;
|
|
129
129
|
return now - touched > SUBAGENT_TRACE_TTL_MS;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
// Stamps a trace as just written, which restarts its TTL; in: insert and the live worker hooks, out: traceTouchedAt.
|
|
132
133
|
function touchTrace(canonical: string, now = Date.now()): void {
|
|
133
134
|
traceTouchedAt.set(canonical, now);
|
|
134
135
|
}
|
|
@@ -138,7 +139,7 @@ function touchTrace(canonical: string, now = Date.now()): void {
|
|
|
138
139
|
* Called on every insert. Whole-trace eviction removes both the canonical
|
|
139
140
|
* key and its display-id alias.
|
|
140
141
|
*/
|
|
141
|
-
|
|
142
|
+
function pruneTraces(now = Date.now()): void {
|
|
142
143
|
for (const canonical of [...new Set([...subagentTraces.values()].map((t) => t.trackingId))]) {
|
|
143
144
|
if (isTraceExpired(canonical, now)) deleteTraceByCanonical(canonical);
|
|
144
145
|
}
|
|
@@ -155,12 +156,13 @@ export function pruneTraces(now = Date.now()): void {
|
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
/** Drops oldest steps beyond {@link MAX_SUBAGENT_STEPS_PER_TRACE} (Q-30). */
|
|
158
|
-
|
|
159
|
+
function trimTraceSteps(trace: SubAgentTrace): void {
|
|
159
160
|
if (trace.steps.length > MAX_SUBAGENT_STEPS_PER_TRACE) {
|
|
160
161
|
trace.steps.splice(0, trace.steps.length - MAX_SUBAGENT_STEPS_PER_TRACE);
|
|
161
162
|
}
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
// Stores a trace under its tracking id and display id in LRU order, pruning expired and excess traces; in: openWorkerTrace, out: the trace store.
|
|
164
166
|
function insertTrace(trace: SubAgentTrace, displayId: string, now = Date.now()): void {
|
|
165
167
|
pruneTraces(now);
|
|
166
168
|
trimTraceSteps(trace);
|
|
@@ -181,6 +183,7 @@ function insertTrace(trace: SubAgentTrace, displayId: string, now = Date.now()):
|
|
|
181
183
|
pruneTraces(now);
|
|
182
184
|
}
|
|
183
185
|
|
|
186
|
+
// Finds a trace by tracking id, display id or case-insensitive id, dropping expired ones and refreshing LRU order; in: the scoped getters and subscribe, out: the live trace or undefined.
|
|
184
187
|
function lookupTrace(id: string): SubAgentTrace | undefined {
|
|
185
188
|
if (!id) return undefined;
|
|
186
189
|
const direct = subagentTraces.get(id);
|
|
@@ -209,10 +212,12 @@ function lookupTrace(id: string): SubAgentTrace | undefined {
|
|
|
209
212
|
return undefined;
|
|
210
213
|
}
|
|
211
214
|
|
|
215
|
+
// Deep-enough copy of a trace (steps and usage) so callers never see live mutation; in: the getters and notifyTrace, out: a detached SubAgentTrace.
|
|
212
216
|
function snapshotTrace(t: SubAgentTrace): SubAgentTrace {
|
|
213
217
|
return { ...t, steps: t.steps.map((s) => ({ ...s })), usage: t.usage ? { ...t.usage } : undefined };
|
|
214
218
|
}
|
|
215
219
|
|
|
220
|
+
// Sends a snapshot of a trace to its live subscribers, ignoring listener errors; in: the live worker hooks, out: subscriber callbacks.
|
|
216
221
|
function notifyTrace(trackingId: string): void {
|
|
217
222
|
const t = subagentTraces.get(trackingId);
|
|
218
223
|
if (!t) return;
|
|
@@ -224,45 +229,6 @@ function notifyTrace(trackingId: string): void {
|
|
|
224
229
|
}
|
|
225
230
|
}
|
|
226
231
|
|
|
227
|
-
/**
|
|
228
|
-
* Returns a snapshot of one worker's trace by tracking id
|
|
229
|
-
* (`SUBAGENT-NAME-{32hex}` or the raw 32-hex suffix). Used by
|
|
230
|
-
* `agent.track(id)` for realtime inspection.
|
|
231
|
-
*
|
|
232
|
-
* Prefer {@link getSubAgentTraceScoped} when the parent session is known:
|
|
233
|
-
* this compat lookup matches any parent and can confuse sibling workers
|
|
234
|
-
* whose display names collide across sessions.
|
|
235
|
-
*/
|
|
236
|
-
export function getSubAgentTrace(id: string): SubAgentTrace | undefined {
|
|
237
|
-
const t = lookupTrace(id);
|
|
238
|
-
return t ? snapshotTrace(t) : undefined;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Scoped trace lookup (Q-30, preferred): like {@link getSubAgentTrace} but
|
|
243
|
-
* only matches when `trace.parentSessionId === parentSessionId`. Use this
|
|
244
|
-
* from `Agent.track` (or any parent-bound caller) so truncated/aliased ids
|
|
245
|
-
* cannot leak across sessions.
|
|
246
|
-
*/
|
|
247
|
-
export function getSubAgentTraceScoped(id: string, parentSessionId: string): SubAgentTrace | undefined {
|
|
248
|
-
if (!id || !parentSessionId) return undefined;
|
|
249
|
-
const t = lookupTrace(id);
|
|
250
|
-
if (!t) return undefined;
|
|
251
|
-
if (t.parentSessionId !== parentSessionId) return undefined;
|
|
252
|
-
return snapshotTrace(t);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Lists tracking ids of EVERY worker trace in the process (all agents, all
|
|
257
|
-
* sessions). Diagnostics only: `Agent` never uses it, because in a
|
|
258
|
-
* multi-tenant host it would expose other tenants' workers. Use
|
|
259
|
-
* {@link listSubAgentTraceIdsForSessions} for agent-bound callers.
|
|
260
|
-
*/
|
|
261
|
-
export function listSubAgentTraceIds(): string[] {
|
|
262
|
-
pruneTraces();
|
|
263
|
-
return [...subagentTraces.keys()];
|
|
264
|
-
}
|
|
265
|
-
|
|
266
232
|
/** Scoped lookup across a set of parent session ids (an agent's default + named sessions). */
|
|
267
233
|
export function getSubAgentTraceForSessions(
|
|
268
234
|
id: string,
|
|
@@ -326,21 +292,6 @@ export function subscribeToSubAgent(id: string, fn: (trace: SubAgentTrace) => vo
|
|
|
326
292
|
};
|
|
327
293
|
}
|
|
328
294
|
|
|
329
|
-
/** Appends a step to a worker trace and notifies live subscribers. */
|
|
330
|
-
export function appendSubAgentStep(trackingId: string, step: SubAgentStep): void {
|
|
331
|
-
const t = subagentTraces.get(trackingId) ?? lookupTrace(trackingId);
|
|
332
|
-
if (!t) return;
|
|
333
|
-
if (isTraceExpired(t.trackingId)) {
|
|
334
|
-
deleteTraceByCanonical(t.trackingId);
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
t.steps.push(step);
|
|
338
|
-
trimTraceSteps(t);
|
|
339
|
-
t.turns = Math.max(t.turns, step.turn);
|
|
340
|
-
touchTrace(t.trackingId);
|
|
341
|
-
notifyTrace(trackingId);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
295
|
// ---------------------------------------------------------------------------
|
|
345
296
|
// One worker run: live trace + execution metadata (shared by fixed workers and the worker registry)
|
|
346
297
|
// ---------------------------------------------------------------------------
|
|
@@ -523,10 +474,12 @@ export function workerMetadata(a: {
|
|
|
523
474
|
// clock; TTL timestamps above use wall-clock `Date.now()`.
|
|
524
475
|
// ---------------------------------------------------------------------------
|
|
525
476
|
|
|
477
|
+
// Reads the agent's configured budget; in: budget helpers, out: Budget or undefined.
|
|
526
478
|
function readBudget(parentAgent: Agent): Budget | undefined {
|
|
527
479
|
return (parentAgent as unknown as { budget?: Budget }).budget;
|
|
528
480
|
}
|
|
529
481
|
|
|
482
|
+
// Returns the agent's per-run budget counters, creating them on first use; in: charge and reserve helpers, out: BudgetState.
|
|
530
483
|
function ensureBudgetState(parentAgent: Agent): BudgetState {
|
|
531
484
|
const host = parentAgent as unknown as { budgetState?: BudgetState };
|
|
532
485
|
if (!host.budgetState) {
|
|
@@ -535,6 +488,7 @@ function ensureBudgetState(parentAgent: Agent): BudgetState {
|
|
|
535
488
|
return host.budgetState;
|
|
536
489
|
}
|
|
537
490
|
|
|
491
|
+
// Total tokens of a usage value, defensively (total, else input + output); in: budget helpers, out: a non-negative integer.
|
|
538
492
|
function usageTokens(usage: unknown): number {
|
|
539
493
|
const u = usage as { totalTokens?: unknown; inputTokens?: unknown; outputTokens?: unknown } | undefined;
|
|
540
494
|
if (!u || typeof u !== "object") return 0;
|
|
@@ -544,6 +498,7 @@ function usageTokens(usage: unknown): number {
|
|
|
544
498
|
return Math.max(0, Math.floor(inp + out));
|
|
545
499
|
}
|
|
546
500
|
|
|
501
|
+
// Reported total cost of a usage value in USD, 0 when absent; in: budget helpers, out: a non-negative number.
|
|
547
502
|
function usageCostUsd(usage: unknown): number {
|
|
548
503
|
const cost = (usage as { cost?: { totalCost?: unknown } } | undefined)?.cost;
|
|
549
504
|
const v = cost?.totalCost;
|
|
@@ -634,27 +589,6 @@ export function reserveSpawn(parentAgent: Agent, count = 1): BudgetState {
|
|
|
634
589
|
return state;
|
|
635
590
|
}
|
|
636
591
|
|
|
637
|
-
// ---------------------------------------------------------------------------
|
|
638
|
-
// Single core loop note + streaming-loop guard (Q-44, partial).
|
|
639
|
-
// The canonical run/stream loops live in `loop.ts` + `agent.ts` (other
|
|
640
|
-
// owners) and are intentionally NOT duplicated here. This guard exists so
|
|
641
|
-
// future consolidation can branch on streaming handles without importing
|
|
642
|
-
// the loop.
|
|
643
|
-
// ---------------------------------------------------------------------------
|
|
644
|
-
|
|
645
|
-
/** Minimal shape of a streaming run handle (has subscribe + terminal promise). */
|
|
646
|
-
export interface StreamingLoopHandle {
|
|
647
|
-
on: (...args: unknown[]) => unknown;
|
|
648
|
-
result: (...args: unknown[]) => Promise<unknown>;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
/** Type guard for streaming-loop handles (Q-44). */
|
|
652
|
-
export function isStreamingLoop(value: unknown): value is StreamingLoopHandle {
|
|
653
|
-
if (!value || typeof value !== "object") return false;
|
|
654
|
-
const r = value as Record<string, unknown>;
|
|
655
|
-
return typeof r["on"] === "function" && typeof r["result"] === "function";
|
|
656
|
-
}
|
|
657
|
-
|
|
658
592
|
/** Optional name/description wrapper for converting an Agent into a tool. */
|
|
659
593
|
export interface AgentAsToolTarget {
|
|
660
594
|
name?: string;
|
package/src/agent/loop.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { enforceFileAccess } from "../utils/safe-io.ts";
|
|
|
25
25
|
import { isSubagentResult, normalizeSubagentMetadata } from "../tools/subagent-result.ts";
|
|
26
26
|
import { BudgetExceededError } from "../types/errors.ts";
|
|
27
27
|
import { applyCaptureRaw, type CaptureRawMode } from "../utils/raw-capture.ts";
|
|
28
|
+
import { previewPrompt } from "../utils/session.ts";
|
|
28
29
|
import { noteProviderTurn, normalizeStructuredOutput, parseStructuredOutput } from "../providers.ts";
|
|
29
30
|
|
|
30
31
|
/** One mid-session steer request buffered while a run is active. */
|
|
@@ -99,20 +100,6 @@ export interface AgentLoopConfig {
|
|
|
99
100
|
output?: unknown;
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
/** Short preview for `steer_injected` / `queued` event payloads (bounded, display-only). */
|
|
103
|
-
function steerPreview(prompt: string | ContentPart[]): string {
|
|
104
|
-
if (typeof prompt === "string") return prompt.slice(0, 500);
|
|
105
|
-
try {
|
|
106
|
-
const texts = prompt
|
|
107
|
-
.filter((p) => (p as { type?: unknown }).type === "text")
|
|
108
|
-
.map((p) => (p as { text?: string }).text ?? "")
|
|
109
|
-
.join("\n");
|
|
110
|
-
return (texts || "[multipart]").slice(0, 500);
|
|
111
|
-
} catch {
|
|
112
|
-
return "[multipart]";
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
103
|
/**
|
|
117
104
|
* Drains pending steer entries into the conversation as new user messages.
|
|
118
105
|
* Returns display previews for events/telemetry. Never throws: invalid
|
|
@@ -133,7 +120,7 @@ function drainSteerInbox(context: AgentContext, inbox?: SteerEntry[]): string[]
|
|
|
133
120
|
else prompt = [{ type: "text", text: prefix } as ContentPart, ...prompt];
|
|
134
121
|
}
|
|
135
122
|
context.addUserMessage(prompt);
|
|
136
|
-
injected.push(
|
|
123
|
+
injected.push(previewPrompt(entry.prompt));
|
|
137
124
|
} catch {
|
|
138
125
|
continue;
|
|
139
126
|
}
|
|
@@ -141,6 +128,7 @@ function drainSteerInbox(context: AgentContext, inbox?: SteerEntry[]): string[]
|
|
|
141
128
|
return injected;
|
|
142
129
|
}
|
|
143
130
|
|
|
131
|
+
// Reports one finished turn to the caller's onTurn hook (ids only), never throwing; in: both loops, out: AgentRunOptions.onTurn.
|
|
144
132
|
function emitTurn(
|
|
145
133
|
runOptions: AgentRunOptions | undefined,
|
|
146
134
|
turn: { turns: number; text?: string; thinking?: string; toolCalls?: ToolCallRecord[]; toolResults?: ToolResultRecord[] }
|
|
@@ -183,6 +171,7 @@ function shouldRollbackOnFailure(
|
|
|
183
171
|
return !toolsExecuted;
|
|
184
172
|
}
|
|
185
173
|
|
|
174
|
+
// Calls the persistence checkpoint hook, never throwing; in: both loops after each step, out: Agent.persistNow.
|
|
186
175
|
function emitProgress(config: { onProgress?: () => void }): void {
|
|
187
176
|
try { config.onProgress?.(); } catch {}
|
|
188
177
|
}
|
|
@@ -191,6 +180,7 @@ function emitProgress(config: { onProgress?: () => void }): void {
|
|
|
191
180
|
// Structured outputs (canonical output wiring for the loop).
|
|
192
181
|
// ---------------------------------------------------------------------------
|
|
193
182
|
|
|
183
|
+
// Normalizes the structured-output option once per run (raw value + provider spec); in: AgentLoopConfig.output, out: the final-answer validation.
|
|
194
184
|
function resolveLoopOutput(config: AgentLoopConfig): { raw: unknown; spec: import("../types/model.ts").StructuredOutputSpec | undefined } {
|
|
195
185
|
const raw = (config as { output?: unknown }).output;
|
|
196
186
|
const fromOptions = (config.options as { output?: import("../types/model.ts").StructuredOutputSpec } | undefined)?.output;
|
|
@@ -204,6 +194,7 @@ function resolveLoopOutput(config: AgentLoopConfig): { raw: unknown; spec: impor
|
|
|
204
194
|
return { raw: undefined, spec: undefined };
|
|
205
195
|
}
|
|
206
196
|
|
|
197
|
+
// Validates the final text against the structured-output option, throwing ValidationError on mismatch; in: the last model text, out: AgentResponse.parsed.
|
|
207
198
|
function parseFinalStructuredOutput(text: string, raw: unknown, spec: import("../types/model.ts").StructuredOutputSpec | undefined): unknown | undefined {
|
|
208
199
|
if (raw === undefined && spec === undefined) return undefined;
|
|
209
200
|
// Strict-fail when opted in: invalid JSON/schema throws ValidationError.
|
|
@@ -350,6 +341,7 @@ function resolveLoopLimits(config: AgentLoopConfig, fallbackMaxTurns: number): L
|
|
|
350
341
|
return { maxTurns, deadlineMs, maxToolCallsPerRun, maxTotalTokens, requestTimeoutMs, firstByteTimeoutMs, streamIdleTimeoutMs };
|
|
351
342
|
}
|
|
352
343
|
|
|
344
|
+
// The session id a run's provider calls use (run override, else agent default); in: AgentLoopConfig, out: rollback and routing.
|
|
353
345
|
function loopSessionId(config: AgentLoopConfig): string | undefined {
|
|
354
346
|
try {
|
|
355
347
|
return (
|
|
@@ -359,6 +351,7 @@ function loopSessionId(config: AgentLoopConfig): string | undefined {
|
|
|
359
351
|
} catch { return undefined; }
|
|
360
352
|
}
|
|
361
353
|
|
|
354
|
+
// Builds an AbortError that carries the run's partial progress; in: an abort reason + partial usage, out: the error the run rejects with.
|
|
362
355
|
function makeAbortError(
|
|
363
356
|
partial: { usage: TokenUsage; turns: number; text?: string },
|
|
364
357
|
reason?: unknown
|
|
@@ -373,6 +366,7 @@ function makeAbortError(
|
|
|
373
366
|
return base;
|
|
374
367
|
}
|
|
375
368
|
|
|
369
|
+
// Throws the run's AbortError as soon as its signal is aborted; in: loop checkpoints, out: nothing or a thrown abort.
|
|
376
370
|
function throwIfAborted(
|
|
377
371
|
signal: AbortSignal | undefined,
|
|
378
372
|
partial: { usage: TokenUsage; turns: number; text?: string }
|
|
@@ -382,6 +376,7 @@ function throwIfAborted(
|
|
|
382
376
|
}
|
|
383
377
|
}
|
|
384
378
|
|
|
379
|
+
// Throws BudgetExceededError when the run's tokens pass limits.maxTotalTokens; in: both loops after usage changes, out: nothing or a thrown error.
|
|
385
380
|
function checkTokenLimit(usage: TokenUsage, maxTotalTokens: number): void {
|
|
386
381
|
if (Number.isFinite(maxTotalTokens) && usage.totalTokens > maxTotalTokens) {
|
|
387
382
|
throw new BudgetExceededError(
|
|
@@ -391,12 +386,14 @@ function checkTokenLimit(usage: TokenUsage, maxTotalTokens: number): void {
|
|
|
391
386
|
}
|
|
392
387
|
}
|
|
393
388
|
|
|
389
|
+
// Throws when the run has been going longer than limits.deadlineMs; in: both loops each turn, out: nothing or a thrown error.
|
|
394
390
|
function checkDeadline(startTime: number, deadlineMs: number): void {
|
|
395
391
|
if (Number.isFinite(deadlineMs) && deadlineMs !== Infinity && Date.now() - startTime > deadlineMs) {
|
|
396
392
|
throw new Error(`[Agent Accelerator] Run deadline exceeded after ${deadlineMs}ms.`);
|
|
397
393
|
}
|
|
398
394
|
}
|
|
399
395
|
|
|
396
|
+
// Races one provider call against the request timeout (a TimeoutError), clearing its timer either way; in: runAgentLoop, out: the call's result.
|
|
400
397
|
async function generateWithTimeout<T>(task: () => Promise<T>, timeoutMs: number): Promise<T> {
|
|
401
398
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs === Infinity) return task();
|
|
402
399
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -493,6 +490,7 @@ export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: M
|
|
|
493
490
|
}
|
|
494
491
|
}
|
|
495
492
|
|
|
493
|
+
// Turns a tool result into what the model sees: a branded delegation envelope becomes its text and yields validated worker metadata, anything else passes through; in: executed results, out: history + usage roll-up.
|
|
496
494
|
function sanitizeToolResult(r: ToolResultRecord): { sanitized: ToolResultRecord; metas: SubAgentExecutionMetadata[] } {
|
|
497
495
|
// Only SDK-branded delegation envelopes are trusted. A plain object that
|
|
498
496
|
// merely carries a `_subagentMetadata` key (e.g. scraped JSON) is ordinary
|
package/src/agent/worker.ts
CHANGED
|
@@ -3,12 +3,9 @@ import type { SubAgentStep } from "../types/response.ts";
|
|
|
3
3
|
import type { AgentConfig } from "../types/agent.ts";
|
|
4
4
|
import type { Agent } from "./agent.ts";
|
|
5
5
|
import { normalizeToolName } from "../tools/executor.ts";
|
|
6
|
-
import { escapeXml } from "../utils/serialization.ts";
|
|
7
6
|
import { getModel, getSubModel } from "../utils/env.ts";
|
|
8
7
|
import { resolveModel } from "../providers/registry.ts";
|
|
9
8
|
|
|
10
|
-
export { escapeXml };
|
|
11
|
-
|
|
12
9
|
/** Minimal live-trace shape shared by fixed + dynamic workers (structural subset of `SubAgentTrace`). */
|
|
13
10
|
export interface LiveWorkerTrace {
|
|
14
11
|
trackingId: string;
|
|
@@ -200,6 +197,7 @@ export function grantWorkerTools(
|
|
|
200
197
|
return grantedTools;
|
|
201
198
|
}
|
|
202
199
|
|
|
200
|
+
// The provider id a model string resolves to, or undefined when it cannot be resolved; in: resolveWorkerCredentials, out: the provider match check.
|
|
203
201
|
function providerOf(modelStr: string | unknown | undefined): string | undefined {
|
|
204
202
|
if (!modelStr) return undefined;
|
|
205
203
|
try {
|