agent-accelerator 0.5.9 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -16
- package/examples/prompts/SYSTEM_PROMPT.md +2 -0
- package/examples/prompts/SYSTEM_PROMPT_AGENT.md +4 -0
- package/examples/prompts/SYSTEM_PROMPT_TOOLS.md +1 -0
- package/package.json +4 -1
- package/src/agent/agent.ts +447 -172
- package/src/agent/architecture.md +16 -12
- package/src/agent/config-blocks.ts +39 -12
- package/src/agent/context.ts +9 -3
- package/src/agent/defaults.ts +88 -0
- package/src/agent/delegation.ts +86 -28
- package/src/agent/loop.ts +474 -374
- package/src/agent/subagent.ts +1 -3
- package/src/agent/subagents-tool.ts +50 -19
- package/src/agent/turns.ts +3 -1
- package/src/agent/usage-limit.ts +92 -0
- package/src/agent/worker.ts +73 -30
- package/src/agent/workers.ts +126 -21
- package/src/architecture.md +17 -15
- package/src/index.ts +43 -19
- package/src/models/architecture.md +4 -3
- package/src/models/catalog-cache.ts +6 -5
- package/src/models/catalog.ts +39 -30
- package/src/prompts/additional-context.md +2 -0
- package/src/prompts/architecture.md +19 -0
- package/src/prompts/prompts.ts +81 -0
- package/src/prompts/reasoning-directive.md +5 -0
- package/src/{scheduler/scheduled-actions.md → prompts/scheduled-actions-tool.md} +4 -0
- package/src/prompts/structured-output.md +2 -0
- package/src/{agent/subagents.md → prompts/subagents-tool.md} +3 -0
- package/src/prompts/tool-bridge.md +1 -0
- package/src/prompts/tool-interrupted.md +1 -0
- package/src/prompts/tool-not-found.md +1 -0
- package/src/prompts/tool-repeat-blocked.md +1 -0
- package/src/prompts/tool-schema-error.md +1 -0
- package/src/prompts/tool-truncated-arguments.md +1 -0
- package/src/prompts/worker-failed.md +1 -0
- package/src/prompts/worker-replied.md +2 -0
- package/src/providers/architecture.md +10 -8
- package/src/providers/google.ts +228 -69
- package/src/providers/openai-compat.ts +74 -56
- package/src/providers/openai.ts +64 -41
- package/src/providers/openrouter.ts +270 -48
- package/src/providers/registry.ts +6 -21
- package/src/providers/shared.ts +87 -33
- package/src/providers.ts +115 -124
- package/src/scheduler/architecture.md +8 -7
- package/src/scheduler/scheduler.ts +226 -60
- package/src/scheduler/store.ts +23 -10
- package/src/scheduler/time.ts +17 -3
- package/src/scheduler/tool.ts +38 -16
- package/src/session/architecture.md +7 -6
- package/src/session/journal.ts +39 -12
- package/src/session/store.ts +126 -61
- package/src/session/usage.ts +58 -9
- package/src/streaming/architecture.md +4 -3
- package/src/streaming/event-stream.ts +26 -12
- package/src/telemetry.ts +6 -2
- package/src/tools/architecture.md +9 -7
- package/src/tools/executor.ts +109 -62
- package/src/tools/schema.ts +10 -6
- package/src/tools/subagent-result.ts +6 -1
- package/src/tools/tool-content.ts +32 -0
- package/src/tools/tool.ts +9 -11
- package/src/types/agent.ts +37 -22
- package/src/types/architecture.md +11 -10
- package/src/types/core.ts +5 -9
- package/src/types/errors.ts +26 -4
- package/src/types/message.ts +7 -0
- package/src/types/model.ts +4 -5
- package/src/types/scheduler.ts +4 -4
- package/src/types/tool.ts +14 -2
- package/src/update-models.ts +5 -5
- package/src/utils/architecture.md +16 -15
- package/src/utils/cache.ts +1 -3
- package/src/utils/durable.ts +17 -5
- package/src/utils/env.ts +9 -7
- package/src/utils/errors.ts +9 -17
- package/src/utils/headers.ts +3 -10
- package/src/utils/media.ts +23 -8
- package/src/utils/raw-capture.ts +9 -2
- package/src/utils/retry.ts +15 -3
- package/src/utils/safe-io.ts +29 -7
- package/src/utils/serialization.ts +1 -4
- package/src/utils/session.ts +1 -2
|
@@ -2,25 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
The agent and everything that runs under it.
|
|
4
4
|
|
|
5
|
-
| File
|
|
6
|
-
|
|
|
7
|
-
| `agent.ts`
|
|
8
|
-
| `loop.ts`
|
|
9
|
-
| `context.ts`
|
|
10
|
-
| `delegation.ts`
|
|
11
|
-
| `worker.ts`
|
|
12
|
-
| `workers.ts`
|
|
13
|
-
| `subagents-tool.ts`
|
|
14
|
-
| `subagent.ts`
|
|
15
|
-
| `turns.ts`
|
|
16
|
-
| `
|
|
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` | The one model-facing `subagents` tool; its description is the `subagentsTool` prompt (`../prompts/subagents-tool.md`). | `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
|
+
| `defaults.ts` | `configure()` and the `AGENT_ACCEL_*` env vars: developer defaults for `usageLimit` and `dynamicSubagents.scheduledActionBudget`, per field (Agent config > `configure()` > env). SDK-built workers never take them (`isWorkerConfig`). | `Agent`, barrel |
|
|
17
|
+
| `usage-limit.ts` | `UsageLimit`: the chat-wide cap (`AgentConfig.usageLimit`). The root agent owns it, registry and fixed workers share it (`agent.chat`); every model turn adds to it and is checked against it. | `Agent`, `workers.ts`, `delegation.ts` |
|
|
18
|
+
| `config-blocks.ts` | RSI blocks: only `model`, `instructions`, `tools` are editable; `allowedModels` check. | `Agent` |
|
|
17
19
|
|
|
18
20
|
## Invariants
|
|
21
|
+
|
|
19
22
|
- `runAgentLoop` and `streamAgentLoop` duplicate their turn logic: every loop change goes into both.
|
|
20
23
|
- An `Agent` runs one thing at a time; delegation forks a copy per call so parallel calls never queue.
|
|
21
24
|
- A worker is a plain `Agent` with a parent link and presets. Never duplicate logic between Agent and worker.
|
|
22
25
|
- A worker is never less constrained than its parent (`inheritableConfig()`); its children draw only from the tools it was granted.
|
|
23
26
|
- Exactly-once worker calls rest on the call key (parent run mark id + tool call id) and `meta.last`.
|
|
24
27
|
- Only results branded by `makeSubagentResult` have their usage metadata trusted.
|
|
28
|
+
- A chat-wide cap (`usageLimit`) counts every model turn of the root and of all workers through one shared `UsageLimit`; its starting point is read once per process from the saved sessions. Scheduled workers never get it.
|
|
25
29
|
- Each Agent records only its own model turns; never sum `AgentResponse.usage` for totals.
|
|
26
30
|
- `reset()` clears the conversation (messages, run marks, the run's saved tool results) but never the spending totals.
|
|
@@ -22,25 +22,37 @@ export function modelKey(model: unknown): string | undefined {
|
|
|
22
22
|
export function assertModelAllowed(allowed: unknown, model: string | undefined): void {
|
|
23
23
|
if (allowed === undefined) return;
|
|
24
24
|
if (!Array.isArray(allowed) || allowed.length === 0 || allowed.some((m) => typeof m !== "string" || !m.trim())) {
|
|
25
|
-
throw new ConfigError(
|
|
25
|
+
throw new ConfigError(
|
|
26
|
+
`[Agent Accelerator] Invalid allowedModels ${JSON.stringify(allowed)}: expected a non-empty list of "provider/model" strings, or omit it to allow any model.`
|
|
27
|
+
);
|
|
26
28
|
}
|
|
27
29
|
if (!model || !allowed.includes(model)) {
|
|
28
|
-
throw new ConfigError(
|
|
30
|
+
throw new ConfigError(
|
|
31
|
+
`[Agent Accelerator] Model ${JSON.stringify(model ?? null)} is not in allowedModels. Allowed: ${allowed.map((m) => JSON.stringify(m)).join(", ")}.`
|
|
32
|
+
);
|
|
29
33
|
}
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
// The editable part of an agent as plain JSON; in: the Agent's model, instructions and the developer's tool list (not the built-in delegation/scheduler tools), out: Agent.toConfig.
|
|
33
|
-
export function toEditableConfig(
|
|
37
|
+
export function toEditableConfig(
|
|
38
|
+
model: unknown,
|
|
39
|
+
instructions: string,
|
|
40
|
+
tools: AgentConfig["tools"]
|
|
41
|
+
): Required<EditableAgentConfig> {
|
|
34
42
|
return { model: modelKey(model) ?? "", instructions, tools: Object.keys(normalizeToolMap(tools)) };
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
// Merges what an agent edited into the developer's config: only model/instructions/tools can change; tools are names picked from base.tools; the model is checked against allowedModels when the Agent is built; in: Agent.fromConfig args, out: the AgentConfig for `new Agent`.
|
|
38
46
|
export function applyEditable(editable: EditableAgentConfig, base: AgentConfig): AgentConfig {
|
|
39
47
|
if (!editable || typeof editable !== "object" || Array.isArray(editable)) {
|
|
40
|
-
throw new ConfigError(
|
|
48
|
+
throw new ConfigError(
|
|
49
|
+
`[Agent Accelerator] Invalid editable config ${JSON.stringify(editable)}: expected an object like { model, instructions, tools } (see Agent.toConfig()).`
|
|
50
|
+
);
|
|
41
51
|
}
|
|
42
52
|
const e = editable as Record<string, unknown>;
|
|
43
|
-
const ignored = Object.keys(e).filter(
|
|
53
|
+
const ignored = Object.keys(e).filter(
|
|
54
|
+
(k) => !(EDITABLE_FIELDS as readonly string[]).includes(k) && e[k] !== undefined
|
|
55
|
+
);
|
|
44
56
|
if (ignored.length > 0) {
|
|
45
57
|
telemetryBus.emitWarning(
|
|
46
58
|
"config.not_editable",
|
|
@@ -51,26 +63,41 @@ export function applyEditable(editable: EditableAgentConfig, base: AgentConfig):
|
|
|
51
63
|
const out: AgentConfig = { ...base };
|
|
52
64
|
if (e.model !== undefined) {
|
|
53
65
|
const next = typeof e.model === "string" ? e.model.trim() : "";
|
|
54
|
-
if (!next)
|
|
66
|
+
if (!next)
|
|
67
|
+
throw new ConfigError(
|
|
68
|
+
`[Agent Accelerator] Invalid editable model ${JSON.stringify(e.model)}: expected a "provider/model" string.`
|
|
69
|
+
);
|
|
55
70
|
// A model helper's key and base URL belong to its own provider: they are never sent to another one.
|
|
56
71
|
const prev = base.model as { model?: unknown; apiKey?: string; baseUrl?: string } | string | undefined;
|
|
57
72
|
const sameProvider = next.includes("/") && next.split("/")[0] === modelKey(prev)?.split("/")[0];
|
|
58
|
-
out.model =
|
|
59
|
-
|
|
60
|
-
|
|
73
|
+
out.model =
|
|
74
|
+
prev && typeof prev === "object" && typeof prev.model === "string"
|
|
75
|
+
? { ...prev, model: next, ...(sameProvider ? {} : { apiKey: undefined, baseUrl: undefined }) }
|
|
76
|
+
: next;
|
|
61
77
|
}
|
|
62
78
|
if (e.instructions !== undefined) {
|
|
63
|
-
if (typeof e.instructions !== "string")
|
|
79
|
+
if (typeof e.instructions !== "string")
|
|
80
|
+
throw new ConfigError(
|
|
81
|
+
`[Agent Accelerator] Invalid editable instructions: expected a string, got ${typeof e.instructions}.`
|
|
82
|
+
);
|
|
64
83
|
out.instructions = e.instructions;
|
|
65
84
|
}
|
|
66
85
|
if (e.tools !== undefined) {
|
|
67
86
|
if (!Array.isArray(e.tools) || e.tools.some((n) => typeof n !== "string")) {
|
|
68
|
-
throw new ConfigError(
|
|
87
|
+
throw new ConfigError(
|
|
88
|
+
`[Agent Accelerator] Invalid editable tools ${JSON.stringify(e.tools)}: expected a list of tool names from the developer's tool pool.`
|
|
89
|
+
);
|
|
69
90
|
}
|
|
70
91
|
const pool = normalizeToolMap(base.tools);
|
|
71
92
|
const unknown = (e.tools as string[]).filter((n) => !Object.hasOwn(pool, n));
|
|
72
93
|
if (unknown.length > 0) {
|
|
73
|
-
throw new ConfigError(
|
|
94
|
+
throw new ConfigError(
|
|
95
|
+
`[Agent Accelerator] Unknown tool ${unknown.map((n) => JSON.stringify(n)).join(", ")} in the editable config. Available: ${
|
|
96
|
+
Object.keys(pool)
|
|
97
|
+
.map((n) => JSON.stringify(n))
|
|
98
|
+
.join(", ") || "none"
|
|
99
|
+
}.`
|
|
100
|
+
);
|
|
74
101
|
}
|
|
75
102
|
const picked: Record<string, ToolDefinition> = {};
|
|
76
103
|
for (const n of e.tools as string[]) picked[n] = pool[n]!;
|
package/src/agent/context.ts
CHANGED
|
@@ -3,12 +3,13 @@ import type { ToolCallRecord, ToolResultRecord } from "../types/tool.ts";
|
|
|
3
3
|
import { safeStringify } from "../utils/serialization.ts";
|
|
4
4
|
import { clearSessionRouting } from "../providers.ts";
|
|
5
5
|
import { clearInteractionChains } from "../providers/google.ts";
|
|
6
|
+
import { ValidationError } from "../types/errors.ts";
|
|
6
7
|
|
|
7
8
|
const USER_PART_TYPES = new Set(["text", "image", "audio", "video", "file"]);
|
|
8
9
|
|
|
9
10
|
// Throws the AgentContext validation error with the SDK prefix; in: the add* validators, out: a thrown Error.
|
|
10
11
|
function fail(message: string): never {
|
|
11
|
-
throw new
|
|
12
|
+
throw new ValidationError(`[Agent Accelerator] AgentContext: ${message}`);
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
/** Deep clone that preserves Uint8Array/ArrayBuffer media (structuredClone may be absent). */
|
|
@@ -142,6 +143,7 @@ export class AgentContext {
|
|
|
142
143
|
isError: res.isError,
|
|
143
144
|
...(res.logFile ? { logFile: res.logFile } : {}),
|
|
144
145
|
...(res.stopped ? { stopped: true } : {}),
|
|
146
|
+
...(res.parts && res.parts.length > 0 ? { parts: res.parts } : {}),
|
|
145
147
|
},
|
|
146
148
|
],
|
|
147
149
|
});
|
|
@@ -192,8 +194,12 @@ export class AgentContext {
|
|
|
192
194
|
if (Array.isArray(cp?.messages)) this.messages = deepCloneValue(cp.messages);
|
|
193
195
|
if (Array.isArray(cp?.signatures)) this.thoughtSignatures = [...cp.signatures];
|
|
194
196
|
if (sessionId) {
|
|
195
|
-
try {
|
|
196
|
-
|
|
197
|
+
try {
|
|
198
|
+
clearInteractionChains(sessionId);
|
|
199
|
+
} catch {}
|
|
200
|
+
try {
|
|
201
|
+
clearSessionRouting(sessionId);
|
|
202
|
+
} catch {}
|
|
197
203
|
}
|
|
198
204
|
}
|
|
199
205
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Developer defaults for the spending caps, set in code (`configure()`) or through env vars, so one place can cap every
|
|
3
|
+
* agent of a program. Precedence, per field (`maxCostUsd`, `maxTotalTokens`): the Agent's own config, then `configure()`,
|
|
4
|
+
* then the env var, then no cap. Only agents YOU construct take defaults: the workers and scheduled runs the SDK builds
|
|
5
|
+
* itself never do (a chat worker joins its parent's cap; a scheduled run has its own).
|
|
6
|
+
*/
|
|
7
|
+
import { ConfigError } from "../types/errors.ts";
|
|
8
|
+
import { getEnv } from "../utils/env.ts";
|
|
9
|
+
import { checkUsageLimitConfig, type UsageLimitConfig } from "./usage-limit.ts";
|
|
10
|
+
|
|
11
|
+
/** What `configure()` can set; every key is optional and has the same meaning as the Agent option of the same name. */
|
|
12
|
+
export interface AgentDefaults {
|
|
13
|
+
/** Default for `AgentConfig.usageLimit` (the chat-wide cap). Env: `AGENT_ACCEL_USAGE_LIMIT_USD`, `AGENT_ACCEL_USAGE_LIMIT_TOKENS`. */
|
|
14
|
+
usageLimit?: UsageLimitConfig;
|
|
15
|
+
/** Default for `dynamicSubagents.scheduledActionBudget`. Env: `AGENT_ACCEL_SCHEDULED_BUDGET_USD`, `AGENT_ACCEL_SCHEDULED_BUDGET_TOKENS`. */
|
|
16
|
+
scheduledActionBudget?: UsageLimitConfig;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type CapName = keyof AgentDefaults;
|
|
20
|
+
|
|
21
|
+
const ENV: Record<CapName, { maxCostUsd: string; maxTotalTokens: string }> = {
|
|
22
|
+
usageLimit: { maxCostUsd: "AGENT_ACCEL_USAGE_LIMIT_USD", maxTotalTokens: "AGENT_ACCEL_USAGE_LIMIT_TOKENS" },
|
|
23
|
+
scheduledActionBudget: {
|
|
24
|
+
maxCostUsd: "AGENT_ACCEL_SCHEDULED_BUDGET_USD",
|
|
25
|
+
maxTotalTokens: "AGENT_ACCEL_SCHEDULED_BUDGET_TOKENS",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let configured: AgentDefaults = {};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Sets developer defaults for the spending caps; call it once at startup, like `setHome()`. It REPLACES what an earlier
|
|
33
|
+
* call set (`configure({})` clears it). A key an Agent sets itself wins, field by field; env vars are the fallback.
|
|
34
|
+
* Throws a `ConfigError` for a value that is not a number above 0.
|
|
35
|
+
*
|
|
36
|
+
* @example `configure({ usageLimit: { maxCostUsd: 5 }, scheduledActionBudget: { maxCostUsd: 0.5 } });`
|
|
37
|
+
*/
|
|
38
|
+
export function configure(defaults: AgentDefaults = {}): void {
|
|
39
|
+
if (!defaults || typeof defaults !== "object" || Array.isArray(defaults)) {
|
|
40
|
+
throw new ConfigError("[Agent Accelerator] configure() expects an object like { usageLimit: { maxCostUsd: 5 } }.");
|
|
41
|
+
}
|
|
42
|
+
for (const key of Object.keys(defaults)) {
|
|
43
|
+
if (!(key in ENV)) {
|
|
44
|
+
throw new ConfigError(
|
|
45
|
+
`[Agent Accelerator] configure(): unknown setting "${key}" (known: ${Object.keys(ENV).join(", ")}).`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const value = (defaults as Record<string, unknown>)[key];
|
|
49
|
+
if (value === undefined) continue;
|
|
50
|
+
const problem = checkUsageLimitConfig(value);
|
|
51
|
+
if (problem) throw new ConfigError(`[Agent Accelerator] configure(): invalid ${key}: ${problem}.`);
|
|
52
|
+
}
|
|
53
|
+
configured = Object.fromEntries(
|
|
54
|
+
Object.entries(defaults)
|
|
55
|
+
.filter(([, v]) => v !== undefined)
|
|
56
|
+
.map(([k, v]) => [k, { ...(v as UsageLimitConfig) }])
|
|
57
|
+
) as AgentDefaults;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Reads one cap field from its env var: undefined when unset, a ConfigError naming the variable when it is not a number above 0; in: mergeCap, out: the value.
|
|
61
|
+
function fromEnv(variable: string): number | undefined {
|
|
62
|
+
const raw = getEnv(variable);
|
|
63
|
+
if (raw === undefined) return undefined;
|
|
64
|
+
const n = Number(raw);
|
|
65
|
+
if (!(Number.isFinite(n) && n > 0)) {
|
|
66
|
+
throw new ConfigError(
|
|
67
|
+
`[Agent Accelerator] Invalid ${variable} ${JSON.stringify(raw)}: expected a number greater than 0.`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return n;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The cap an Agent uses: its own value, field by field over `configure()` over the env vars; undefined when nothing sets one. `withDefaults: false` ignores `configure()` and the env (SDK-built workers). */
|
|
74
|
+
export function mergeCap(
|
|
75
|
+
name: CapName,
|
|
76
|
+
own: UsageLimitConfig | undefined,
|
|
77
|
+
withDefaults: boolean
|
|
78
|
+
): UsageLimitConfig | undefined {
|
|
79
|
+
const base = withDefaults ? configured[name] : undefined;
|
|
80
|
+
const maxCostUsd = own?.maxCostUsd ?? base?.maxCostUsd ?? (withDefaults ? fromEnv(ENV[name].maxCostUsd) : undefined);
|
|
81
|
+
const maxTotalTokens =
|
|
82
|
+
own?.maxTotalTokens ?? base?.maxTotalTokens ?? (withDefaults ? fromEnv(ENV[name].maxTotalTokens) : undefined);
|
|
83
|
+
if (maxCostUsd === undefined && maxTotalTokens === undefined) return undefined;
|
|
84
|
+
return {
|
|
85
|
+
...(maxCostUsd !== undefined ? { maxCostUsd } : {}),
|
|
86
|
+
...(maxTotalTokens !== undefined ? { maxTotalTokens } : {}),
|
|
87
|
+
};
|
|
88
|
+
}
|
package/src/agent/delegation.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { nowMs } from "../utils/session.ts";
|
|
|
15
15
|
import { newRunId } from "../utils/session.ts";
|
|
16
16
|
import { newTurnId } from "../utils/session.ts";
|
|
17
17
|
import { sanitizeToolName, createLiveWorker } from "./worker.ts";
|
|
18
|
+
import type { UsageLimit } from "./usage-limit.ts";
|
|
18
19
|
|
|
19
20
|
/** Builds a random child session id that fits the 64-char provider limit, embedding a hash of the parent when it must be cut; in: a parent id + tag from scheduled runs, out: the worker's session id. */
|
|
20
21
|
export function createChildSessionId(parentId: string, tag: string): string {
|
|
@@ -61,7 +62,7 @@ export function createFixedChildSessionId(parentId: string, name: string): strin
|
|
|
61
62
|
return `${parentId.slice(0, maxParent - 9)}-${parentHash}${suffix}`;
|
|
62
63
|
}
|
|
63
64
|
if (maxParent > 0) return parentId.slice(0, maxParent) + suffix;
|
|
64
|
-
return
|
|
65
|
+
return `${parentId}-sub-${name}`.slice(0, 64);
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
/**
|
|
@@ -225,7 +226,9 @@ function notifyTrace(trackingId: string): void {
|
|
|
225
226
|
if (!listeners || listeners.size === 0) return;
|
|
226
227
|
const snap = snapshotTrace(t);
|
|
227
228
|
for (const fn of [...listeners]) {
|
|
228
|
-
try {
|
|
229
|
+
try {
|
|
230
|
+
fn(snap);
|
|
231
|
+
} catch {}
|
|
229
232
|
}
|
|
230
233
|
}
|
|
231
234
|
|
|
@@ -280,15 +283,23 @@ export function subscribeToSubAgent(id: string, fn: (trace: SubAgentTrace) => vo
|
|
|
280
283
|
}
|
|
281
284
|
set.add(fn);
|
|
282
285
|
const timer = setTimeout(() => {
|
|
283
|
-
try {
|
|
286
|
+
try {
|
|
287
|
+
subagentListeners.get(key)?.delete(fn);
|
|
288
|
+
} catch {}
|
|
284
289
|
}, SUBAGENT_SUBSCRIBE_TTL_MS);
|
|
285
|
-
try {
|
|
290
|
+
try {
|
|
291
|
+
(timer as unknown as { unref?: () => void }).unref?.();
|
|
292
|
+
} catch {}
|
|
286
293
|
let done = false;
|
|
287
294
|
return () => {
|
|
288
295
|
if (done) return;
|
|
289
296
|
done = true;
|
|
290
|
-
try {
|
|
291
|
-
|
|
297
|
+
try {
|
|
298
|
+
clearTimeout(timer);
|
|
299
|
+
} catch {}
|
|
300
|
+
try {
|
|
301
|
+
subagentListeners.get(key)?.delete(fn);
|
|
302
|
+
} catch {}
|
|
292
303
|
};
|
|
293
304
|
}
|
|
294
305
|
|
|
@@ -334,7 +345,13 @@ export function openWorkerTrace(a: {
|
|
|
334
345
|
role?: string;
|
|
335
346
|
/** When given, the trace is also kept in this agent's own registry and saved with its session. */
|
|
336
347
|
parent?: Agent;
|
|
337
|
-
emit?: (event: {
|
|
348
|
+
emit?: (event: {
|
|
349
|
+
trackingId: string;
|
|
350
|
+
delta?: string;
|
|
351
|
+
thinkingDelta?: string;
|
|
352
|
+
partialText?: string;
|
|
353
|
+
partialThinking?: string;
|
|
354
|
+
}) => void;
|
|
338
355
|
}): WorkerTraceHandle {
|
|
339
356
|
const trackingId = createTrackingId();
|
|
340
357
|
const displayId = `${a.name}-${trackingId}`;
|
|
@@ -360,9 +377,19 @@ export function openWorkerTrace(a: {
|
|
|
360
377
|
trim: (t) => trimTraceSteps(t),
|
|
361
378
|
touch: (id) => touchTrace(id),
|
|
362
379
|
notify: (id) => notifyTrace(id),
|
|
363
|
-
...(a.parent
|
|
380
|
+
...(a.parent
|
|
381
|
+
? {
|
|
382
|
+
persist: () => {
|
|
383
|
+
try {
|
|
384
|
+
a.parent!.persistNow();
|
|
385
|
+
} catch {}
|
|
386
|
+
},
|
|
387
|
+
}
|
|
388
|
+
: {}),
|
|
364
389
|
emit: (ev) => {
|
|
365
|
-
try {
|
|
390
|
+
try {
|
|
391
|
+
a.emit?.(ev);
|
|
392
|
+
} catch {}
|
|
366
393
|
},
|
|
367
394
|
});
|
|
368
395
|
const settle = (): void => {
|
|
@@ -386,7 +413,11 @@ export function openWorkerTrace(a: {
|
|
|
386
413
|
t.turns = result.turns ?? t.turns;
|
|
387
414
|
t.text = result.text;
|
|
388
415
|
if (result.usage) {
|
|
389
|
-
t.usage = {
|
|
416
|
+
t.usage = {
|
|
417
|
+
inputTokens: result.usage.inputTokens ?? 0,
|
|
418
|
+
outputTokens: result.usage.outputTokens ?? 0,
|
|
419
|
+
totalTokens: result.usage.totalTokens ?? 0,
|
|
420
|
+
};
|
|
390
421
|
}
|
|
391
422
|
}
|
|
392
423
|
settle();
|
|
@@ -492,7 +523,8 @@ function ensureBudgetState(parentAgent: Agent): BudgetState {
|
|
|
492
523
|
function usageTokens(usage: unknown): number {
|
|
493
524
|
const u = usage as { totalTokens?: unknown; inputTokens?: unknown; outputTokens?: unknown } | undefined;
|
|
494
525
|
if (!u || typeof u !== "object") return 0;
|
|
495
|
-
if (typeof u.totalTokens === "number" && Number.isFinite(u.totalTokens))
|
|
526
|
+
if (typeof u.totalTokens === "number" && Number.isFinite(u.totalTokens))
|
|
527
|
+
return Math.max(0, Math.floor(u.totalTokens));
|
|
496
528
|
const inp = typeof u.inputTokens === "number" && Number.isFinite(u.inputTokens) ? u.inputTokens : 0;
|
|
497
529
|
const out = typeof u.outputTokens === "number" && Number.isFinite(u.outputTokens) ? u.outputTokens : 0;
|
|
498
530
|
return Math.max(0, Math.floor(inp + out));
|
|
@@ -532,10 +564,9 @@ export function checkBudget(
|
|
|
532
564
|
);
|
|
533
565
|
}
|
|
534
566
|
if (budget.maxTotalTokens !== undefined && totalTokens > budget.maxTotalTokens) {
|
|
535
|
-
throw new BudgetExceededError(
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
);
|
|
567
|
+
throw new BudgetExceededError(`Token budget exceeded: ${totalTokens} > maxTotalTokens ${budget.maxTotalTokens}.`, {
|
|
568
|
+
context: { partial },
|
|
569
|
+
});
|
|
539
570
|
}
|
|
540
571
|
if (budget.maxCostUsd !== undefined && totalCostUsd > budget.maxCostUsd) {
|
|
541
572
|
throw new BudgetExceededError(
|
|
@@ -601,9 +632,7 @@ export interface AgentAsToolTarget {
|
|
|
601
632
|
*
|
|
602
633
|
* @example `const researchTool = researcher.asTool("research");`
|
|
603
634
|
*/
|
|
604
|
-
export function agentToTool(
|
|
605
|
-
input: Agent | AgentAsToolTarget
|
|
606
|
-
): ToolDefinition {
|
|
635
|
+
export function agentToTool(input: Agent | AgentAsToolTarget, opts?: { chat?: UsageLimit }): ToolDefinition {
|
|
607
636
|
const agentInstance = "agent" in input ? input.agent : input;
|
|
608
637
|
const rawName =
|
|
609
638
|
("name" in input && (input as any).name) ||
|
|
@@ -635,28 +664,56 @@ export function agentToTool(
|
|
|
635
664
|
task,
|
|
636
665
|
emit: (ev) => ctx?.onSubagentEvent?.(ev),
|
|
637
666
|
});
|
|
638
|
-
const lineage = {
|
|
667
|
+
const lineage = {
|
|
668
|
+
name,
|
|
669
|
+
displayId: live.displayId,
|
|
670
|
+
sessionId: subSessionId,
|
|
671
|
+
parentSessionId,
|
|
672
|
+
task,
|
|
673
|
+
runId: callRunId,
|
|
674
|
+
turnId: callTurnId,
|
|
675
|
+
};
|
|
639
676
|
try {
|
|
640
677
|
// One isolated copy per call: the shared instance is single-flight, so
|
|
641
678
|
// calling it directly would serialize (or steer) parallel tool calls.
|
|
642
679
|
const worker: Agent =
|
|
643
|
-
typeof (agentInstance as Agent).fork === "function"
|
|
644
|
-
|
|
645
|
-
|
|
680
|
+
typeof (agentInstance as Agent).fork === "function"
|
|
681
|
+
? agentInstance.fork(subSessionId, parentSessionId)
|
|
682
|
+
: agentInstance;
|
|
683
|
+
if (worker !== agentInstance && opts?.chat) worker.chat = opts.chat; // this call counts into the parent chat's cap
|
|
684
|
+
const workerStream = worker.stream(task, {
|
|
685
|
+
signal: ctx?.signal,
|
|
686
|
+
sessionId: subSessionId,
|
|
687
|
+
onTurn: live.onWorkerTurn,
|
|
688
|
+
} as any);
|
|
689
|
+
const detachWorkerListeners = live.attachToStream(
|
|
690
|
+
workerStream as unknown as Parameters<typeof live.attachToStream>[0]
|
|
691
|
+
);
|
|
646
692
|
let response: AgentResponse;
|
|
647
693
|
try {
|
|
648
694
|
response = await workerStream.result();
|
|
649
695
|
} finally {
|
|
650
|
-
try {
|
|
696
|
+
try {
|
|
697
|
+
detachWorkerListeners();
|
|
698
|
+
} catch {}
|
|
651
699
|
// The per-call session id is never reused: drop its provider chain/routing state.
|
|
652
700
|
if (worker !== agentInstance) {
|
|
653
|
-
try {
|
|
654
|
-
|
|
701
|
+
try {
|
|
702
|
+
agentInstance.absorbUsage(worker.usageTotals);
|
|
703
|
+
} catch {} // the template keeps what its copies spent
|
|
704
|
+
try {
|
|
705
|
+
worker.reset();
|
|
706
|
+
} catch {}
|
|
655
707
|
}
|
|
656
708
|
}
|
|
657
709
|
live.finish(response);
|
|
658
710
|
return makeSubagentResult(response.text, [
|
|
659
|
-
workerMetadata({
|
|
711
|
+
workerMetadata({
|
|
712
|
+
...lineage,
|
|
713
|
+
durationMs: Math.max(0, nowMs() - startTime),
|
|
714
|
+
result: response,
|
|
715
|
+
steps: live.steps(),
|
|
716
|
+
}),
|
|
660
717
|
]);
|
|
661
718
|
} catch (err: any) {
|
|
662
719
|
const msg = err?.message || String(err);
|
|
@@ -675,13 +732,14 @@ export function agentToTool(
|
|
|
675
732
|
* @example `const tools = buildAgentTools([researcher, reviewer]);`
|
|
676
733
|
*/
|
|
677
734
|
export function buildAgentTools(
|
|
678
|
-
agents?: (Agent | AgentAsToolTarget)[]
|
|
735
|
+
agents?: (Agent | AgentAsToolTarget)[],
|
|
736
|
+
opts?: { chat?: UsageLimit }
|
|
679
737
|
): Record<string, ToolDefinition> {
|
|
680
738
|
if (!agents || agents.length === 0) return {};
|
|
681
739
|
|
|
682
740
|
const tools: Record<string, ToolDefinition> = {};
|
|
683
741
|
for (const item of agents) {
|
|
684
|
-
const t = agentToTool(item);
|
|
742
|
+
const t = agentToTool(item, opts);
|
|
685
743
|
if (t.name) {
|
|
686
744
|
let finalName = t.name;
|
|
687
745
|
let n = 1;
|