agent-accelerator 0.1.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/LICENSE +21 -0
- package/README.md +1220 -0
- package/SYSTEM_PROMPT.md +14 -0
- package/SYSTEM_PROMPT_AGENT.md +34 -0
- package/SYSTEM_PROMPT_TOOLS.md +9 -0
- package/bunfig.toml +2 -0
- package/package.json +59 -0
- package/src/agent/agent.ts +615 -0
- package/src/agent/context.ts +161 -0
- package/src/agent/delegation.ts +481 -0
- package/src/agent/loop.ts +569 -0
- package/src/agent/subagent.ts +83 -0
- package/src/ai-sdk/converters.ts +342 -0
- package/src/ai-sdk/errors.ts +122 -0
- package/src/ai-sdk/executor.ts +454 -0
- package/src/ai-sdk/index.ts +55 -0
- package/src/ai-sdk/model-provider.ts +303 -0
- package/src/ai-sdk/options.ts +306 -0
- package/src/ai-sdk/provider.ts +415 -0
- package/src/ai-sdk/registry.ts +416 -0
- package/src/data/README.md +84 -0
- package/src/index.ts +190 -0
- package/src/models/catalog-cache.ts +273 -0
- package/src/models/catalog.ts +503 -0
- package/src/streaming/event-stream.ts +211 -0
- package/src/streaming/sse-parser.ts +97 -0
- package/src/tokens/counter.ts +136 -0
- package/src/tools/executor.ts +365 -0
- package/src/tools/schema.ts +221 -0
- package/src/tools/tool.ts +101 -0
- package/src/types/agent.ts +87 -0
- package/src/types/core.ts +86 -0
- package/src/types/message.ts +115 -0
- package/src/types/model.ts +212 -0
- package/src/types/provider-payloads.ts +434 -0
- package/src/types/response.ts +158 -0
- package/src/types/tool.ts +61 -0
- package/src/utils/base64.ts +27 -0
- package/src/utils/cache.ts +146 -0
- package/src/utils/env.ts +78 -0
- package/src/utils/headers.ts +110 -0
- package/src/utils/media.ts +137 -0
- package/src/utils/serialization.ts +91 -0
- package/src/utils/session.ts +26 -0
- package/src/utils/thought-signature.ts +27 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
import type { AgentConfig, AgentRunOptions } from "../types/agent.ts";
|
|
2
|
+
import type { ToolDefinition } from "../types/tool.ts";
|
|
3
|
+
import type { ThinkingLevel, ThinkingConfig, CacheConfig, ServiceTier } from "../types/core.ts";
|
|
4
|
+
import type { ContentPart } from "../types/message.ts";
|
|
5
|
+
import { AgentResponse, type StreamEvent } from "../types/response.ts";
|
|
6
|
+
import { AssistantMessageEventStream } from "../streaming/event-stream.ts";
|
|
7
|
+
import { AgentContext } from "./context.ts";
|
|
8
|
+
import { resolveModel } from "../ai-sdk/registry.ts";
|
|
9
|
+
import { buildAgentTools, createSubagentSpawnTool } from "./delegation.ts";
|
|
10
|
+
import { runAgentLoop, streamAgentLoop } from "./loop.ts";
|
|
11
|
+
import { createSessionId } from "../utils/session.ts";
|
|
12
|
+
import { getModel, getSubModel } from "../utils/env.ts";
|
|
13
|
+
import { validateModelThinking } from "../models/catalog.ts";
|
|
14
|
+
import { resolveEffectiveThinking } from "../ai-sdk/options.ts";
|
|
15
|
+
|
|
16
|
+
/** Thrown when dynamic sub-agent spawning is enabled without an explicit model. */
|
|
17
|
+
export class SubAgentModelError extends Error {
|
|
18
|
+
readonly parentModel?: string;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates a configuration error with the missing sub-agent model and a
|
|
22
|
+
* concrete fix for the parent model setup.
|
|
23
|
+
*
|
|
24
|
+
* @param parentModel The parent model that attempted to enable delegation.
|
|
25
|
+
*/
|
|
26
|
+
constructor(parentModel?: string) {
|
|
27
|
+
const parentName = parentModel || "your main model";
|
|
28
|
+
const formatted =
|
|
29
|
+
`\x1b[31m[Agent Accelerator] Missing Configuration: a sub-agent model is required when dynamic sub-agents are enabled\x1b[0m\n` +
|
|
30
|
+
` \x1b[1mMain Agent Model:\x1b[0m ${parentName}\n` +
|
|
31
|
+
` \x1b[1mIssue:\x1b[0m Dynamic sub-agent delegation was enabled (dynamicSubagents.enabled), but no model was assigned for sub-agents.\n` +
|
|
32
|
+
` Sub-agents must never run on unverified models or default implicitly.\n\n` +
|
|
33
|
+
` \x1b[36m💡 How to fix:\x1b[0m\n` +
|
|
34
|
+
` 1. Pass a model in your Agent configuration:\n` +
|
|
35
|
+
` const agent = new Agent({\n` +
|
|
36
|
+
` model: "${parentName}",\n` +
|
|
37
|
+
` dynamicSubagents: { enabled: true, model: "provider/model-id" },\n` +
|
|
38
|
+
` });\n\n` +
|
|
39
|
+
` 2. Or set the SUB_AGENT_MODEL environment variable in your .env or shell:\n` +
|
|
40
|
+
` SUB_AGENT_MODEL="provider/model-id"`;
|
|
41
|
+
|
|
42
|
+
super(formatted);
|
|
43
|
+
this.name = "SubAgentModelError";
|
|
44
|
+
this.parentModel = parentModel;
|
|
45
|
+
|
|
46
|
+
if (Error.captureStackTrace) {
|
|
47
|
+
Error.captureStackTrace(this, SubAgentModelError);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Stateful, tool-capable LLM agent with unified provider routing.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* const agent = new Agent({
|
|
58
|
+
* model: "google/gemini-3.5-flash-lite",
|
|
59
|
+
* instructions: "Be concise and factual.",
|
|
60
|
+
* tools: { get_status },
|
|
61
|
+
* });
|
|
62
|
+
* const response = await agent.run("Check the status");
|
|
63
|
+
* console.log(response.text);
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export class Agent {
|
|
67
|
+
/** Display name. */
|
|
68
|
+
readonly name: string;
|
|
69
|
+
/** Delegation/tool description. */
|
|
70
|
+
readonly description: string;
|
|
71
|
+
/** Stable system instructions. */
|
|
72
|
+
instructions: string;
|
|
73
|
+
/** Original model string or catalog spec used for resolution. */
|
|
74
|
+
readonly modelStringOrSpec: string | any;
|
|
75
|
+
/** Configured dynamic sub-agent model, if enabled. */
|
|
76
|
+
readonly subagentModel?: string | any;
|
|
77
|
+
/** Normalized dynamic sub-agent spawning policy. */
|
|
78
|
+
readonly dynamicSubagents?: {
|
|
79
|
+
enabled: boolean;
|
|
80
|
+
model?: string | any;
|
|
81
|
+
maxSpawn: number;
|
|
82
|
+
thinkingLevel?: ThinkingLevel;
|
|
83
|
+
tools: Record<string, ToolDefinition>;
|
|
84
|
+
timeout: number;
|
|
85
|
+
};
|
|
86
|
+
/** Registered model-callable tools. */
|
|
87
|
+
readonly tools: Record<string, ToolDefinition> = {};
|
|
88
|
+
/** Normalized reasoning configuration. */
|
|
89
|
+
readonly thinkingConfig?: ThinkingConfig;
|
|
90
|
+
/** Cache retention/session settings. */
|
|
91
|
+
readonly cacheConfig?: CacheConfig;
|
|
92
|
+
/** Provider service tier. */
|
|
93
|
+
readonly serviceTier?: ServiceTier;
|
|
94
|
+
/** Maximum model/tool turns per run. */
|
|
95
|
+
readonly maxTurns: number;
|
|
96
|
+
/** Session ID used for history and cache affinity. */
|
|
97
|
+
readonly sessionId: string;
|
|
98
|
+
/** Explicit API key override. */
|
|
99
|
+
readonly apiKey?: string;
|
|
100
|
+
/** Explicit provider endpoint override. */
|
|
101
|
+
readonly baseUrl?: string;
|
|
102
|
+
/** Headers applied to requests. */
|
|
103
|
+
readonly customHeaders?: Record<string, string>;
|
|
104
|
+
/** Mutable conversation context. */
|
|
105
|
+
readonly context: AgentContext;
|
|
106
|
+
/** Whether history is cleared around each run. */
|
|
107
|
+
readonly stateless: boolean;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Creates an agent and registers its model, tools, cache, and delegation settings.
|
|
111
|
+
*
|
|
112
|
+
* @param config Agent configuration. `model` may be a provider/model string,
|
|
113
|
+
* a catalog spec, or a model-provider instance.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* const agent = new Agent({
|
|
118
|
+
* model: "google/gemini-3.5-flash-lite",
|
|
119
|
+
* instructions: "Be concise and factual.",
|
|
120
|
+
* thinkingLevel: "medium",
|
|
121
|
+
* tools: { get_status },
|
|
122
|
+
* });
|
|
123
|
+
*
|
|
124
|
+
* const response = await agent.run("Check the status");
|
|
125
|
+
* console.log(response.text);
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
constructor(config: AgentConfig) {
|
|
129
|
+
this.name = config.name || "Agent";
|
|
130
|
+
this.description = config.description || "AI Agent powered by Agent Accelerator";
|
|
131
|
+
// DX1: only instructions (no systemPrompt alias)
|
|
132
|
+
this.instructions = config.instructions || "";
|
|
133
|
+
const rawModel: any = (config.model as any)?.model ? (config.model as any) : config.model;
|
|
134
|
+
// Support ModelProviderInstance {model, apiKey, baseUrl, thinkingLevel} and ModelSpec {id, provider}
|
|
135
|
+
if (rawModel && typeof rawModel === "object" && "model" in rawModel) {
|
|
136
|
+
this.modelStringOrSpec = rawModel.model;
|
|
137
|
+
} else if (rawModel && typeof rawModel === "object" && "id" in rawModel && "provider" in rawModel) {
|
|
138
|
+
this.modelStringOrSpec = rawModel;
|
|
139
|
+
} else {
|
|
140
|
+
this.modelStringOrSpec = rawModel || getModel();
|
|
141
|
+
}
|
|
142
|
+
const dynRaw = config.dynamicSubagents;
|
|
143
|
+
const dynModelRaw = (dynRaw as any)?.model ?? config.subagentModel;
|
|
144
|
+
let dynModel: string | any | undefined;
|
|
145
|
+
if (dynModelRaw && typeof dynModelRaw === "object" && "model" in dynModelRaw) {
|
|
146
|
+
dynModel = (dynModelRaw as any).model;
|
|
147
|
+
} else if (dynModelRaw && typeof dynModelRaw === "object" && "id" in dynModelRaw && "provider" in dynModelRaw) {
|
|
148
|
+
dynModel = dynModelRaw;
|
|
149
|
+
} else {
|
|
150
|
+
dynModel = (dynModelRaw as any) || getSubModel();
|
|
151
|
+
}
|
|
152
|
+
this.subagentModel = dynModel;
|
|
153
|
+
const dynEnabled = dynRaw ? (dynRaw.enabled ?? true) : false;
|
|
154
|
+
if (dynRaw) {
|
|
155
|
+
const rawTools = (dynRaw as any)?.tools;
|
|
156
|
+
const dynTools: Record<string, ToolDefinition> = {};
|
|
157
|
+
if (rawTools) {
|
|
158
|
+
if (Array.isArray(rawTools)) {
|
|
159
|
+
for (const t of rawTools) {
|
|
160
|
+
const tName = (t as any)?.name || `tool_${Object.keys(dynTools).length}`;
|
|
161
|
+
dynTools[tName] = { ...(t as any), name: tName };
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
for (const [key, def] of Object.entries(rawTools as Record<string, ToolDefinition>)) {
|
|
165
|
+
dynTools[key] = { ...(def as any), name: (def as any)?.name || key };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const rawMax = (dynRaw as any)?.maxSpawn;
|
|
170
|
+
const maxSpawn = Number.isFinite(rawMax) ? Math.max(1, Math.floor(rawMax as number)) : 4;
|
|
171
|
+
const rawTimeout = (dynRaw as any)?.timeout;
|
|
172
|
+
const timeout = Number.isFinite(rawTimeout) ? Math.floor(rawTimeout as number) : 0;
|
|
173
|
+
this.dynamicSubagents = {
|
|
174
|
+
enabled: dynEnabled,
|
|
175
|
+
model: dynModel,
|
|
176
|
+
maxSpawn,
|
|
177
|
+
thinkingLevel: (dynRaw as any)?.thinkingLevel,
|
|
178
|
+
tools: dynTools,
|
|
179
|
+
timeout,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
this.apiKey = (rawModel as any)?.apiKey || config.apiKey;
|
|
183
|
+
this.baseUrl = (rawModel as any)?.baseUrl || config.baseUrl;
|
|
184
|
+
this.customHeaders = config.headers;
|
|
185
|
+
this.maxTurns = config.maxTurns ?? 10;
|
|
186
|
+
this.sessionId = config.sessionId || config.cache?.sessionId || createSessionId();
|
|
187
|
+
|
|
188
|
+
// DX4: single thinkingLevel flag — also inherit from ModelProviderInstance when omitted
|
|
189
|
+
const mpThinking = (rawModel as any)?.thinkingLevel;
|
|
190
|
+
this.thinkingConfig = normalizeThinking(config, mpThinking);
|
|
191
|
+
|
|
192
|
+
// DX5: cache retention short|medium|long, undefined = no explicit
|
|
193
|
+
this.cacheConfig = {
|
|
194
|
+
sessionId: this.sessionId,
|
|
195
|
+
...(config.cache ?? {}),
|
|
196
|
+
};
|
|
197
|
+
// C11: wire explicit cachedContentId into context for Google explicit cache
|
|
198
|
+
const initialCachedId = (this.cacheConfig as any)?.cachedContentId;
|
|
199
|
+
|
|
200
|
+
this.serviceTier = config.serviceTier;
|
|
201
|
+
this.stateless = config.stateless ?? false;
|
|
202
|
+
|
|
203
|
+
// Tools registration
|
|
204
|
+
if (config.tools) {
|
|
205
|
+
if (Array.isArray(config.tools)) {
|
|
206
|
+
for (const t of config.tools) {
|
|
207
|
+
const tName = t.name || `tool_${Object.keys(this.tools).length}`;
|
|
208
|
+
this.tools[tName] = { ...t, name: tName };
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
for (const [key, def] of Object.entries(config.tools)) {
|
|
212
|
+
this.tools[key] = {
|
|
213
|
+
...def,
|
|
214
|
+
name: def.name || key,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
if (Array.isArray(config.subagents)) {
|
|
222
|
+
const subagentTools = buildAgentTools(config.subagents);
|
|
223
|
+
for (const [toolName, toolDef] of Object.entries(subagentTools)) {
|
|
224
|
+
this.tools[toolName] = toolDef;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (this.dynamicSubagents?.enabled === true) {
|
|
229
|
+
if (!this.subagentModel) {
|
|
230
|
+
throw new SubAgentModelError(
|
|
231
|
+
typeof this.modelStringOrSpec === "string" ? this.modelStringOrSpec : (this.modelStringOrSpec as any)?.id
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const spawnTool = createSubagentSpawnTool(this);
|
|
235
|
+
this.tools[spawnTool.name || "spawn_subagents"] = spawnTool;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
this.context = new AgentContext(this.getFullInstructions());
|
|
239
|
+
if (initialCachedId) {
|
|
240
|
+
this.context.cachedContentId = initialCachedId;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private getFullInstructions(): string | undefined {
|
|
245
|
+
const base = this.instructions || "";
|
|
246
|
+
let out = base;
|
|
247
|
+
const isThinkingEnabled =
|
|
248
|
+
this.thinkingConfig?.enabled !== false &&
|
|
249
|
+
this.thinkingConfig?.level &&
|
|
250
|
+
this.thinkingConfig?.level !== "none";
|
|
251
|
+
if (isThinkingEnabled && !out.includes("[Reasoning Directive]")) {
|
|
252
|
+
const guidance =
|
|
253
|
+
"[Reasoning Directive]\n" +
|
|
254
|
+
"1. Use internal reasoning strictly for private planning and step-by-step thinking.\n" +
|
|
255
|
+
"2. Never attempt to execute tools or output final user deliverables inside reasoning.\n" +
|
|
256
|
+
"3. Once your reasoning is complete, output your final response or function calls directly in the standard response output.";
|
|
257
|
+
out = out ? `${out}\n\n${guidance}` : guidance;
|
|
258
|
+
}
|
|
259
|
+
if (this.dynamicSubagents?.enabled === true && !out.includes("[Dynamic Sub-Agents]")) {
|
|
260
|
+
const dyn = this.dynamicSubagents;
|
|
261
|
+
const toolNames = Object.keys(dyn.tools);
|
|
262
|
+
const timeoutNote =
|
|
263
|
+
dyn.timeout === -1
|
|
264
|
+
? "Timeout policy: set a per-sub-agent timeoutMs (ms) for each task; omit it for no limit."
|
|
265
|
+
: dyn.timeout === 0
|
|
266
|
+
? "Timeout policy: workers run with no time limit."
|
|
267
|
+
: `Timeout policy: every worker is limited to ${dyn.timeout}ms; per-task timeouts are ignored.`;
|
|
268
|
+
const policy =
|
|
269
|
+
"[Dynamic Sub-Agents]\n" +
|
|
270
|
+
`1. You may spawn at most ${dyn.maxSpawn} sub-agent(s) per spawn_subagents call. Extra tasks beyond ${dyn.maxSpawn} are ignored.\n` +
|
|
271
|
+
"2. Workers are stateless: each receives one task, returns its result, then shuts down. No conversation history is kept.\n" +
|
|
272
|
+
"3. You cannot choose worker models or reasoning levels — they are fixed by the developer.\n" +
|
|
273
|
+
(toolNames.length > 0
|
|
274
|
+
? `4. Worker-available tools: ${toolNames.join(", ")}. Grant each worker ONLY the tools its task needs via the per-task tools list; omit it for no tools.\n`
|
|
275
|
+
: "4. No worker tools are available; omit the per-task tools list.\n") +
|
|
276
|
+
`5. ${timeoutNote}`;
|
|
277
|
+
out = out ? `${out}\n\n${policy}` : policy;
|
|
278
|
+
}
|
|
279
|
+
return out || undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Clears conversation messages and provider thought signatures while keeping configuration. */
|
|
283
|
+
reset(): void {
|
|
284
|
+
this.context.messages = [];
|
|
285
|
+
this.context.thoughtSignatures = [];
|
|
286
|
+
this.context.cachedContentId = (this.cacheConfig as any)?.cachedContentId;
|
|
287
|
+
this.context.systemPrompt = this.getFullInstructions();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private prepareTurn(prompt: string | ContentPart[], options?: AgentRunOptions): void {
|
|
291
|
+
if (this.stateless) {
|
|
292
|
+
this.context.messages = [];
|
|
293
|
+
this.context.thoughtSignatures = [];
|
|
294
|
+
}
|
|
295
|
+
const fullInstructions = this.getFullInstructions();
|
|
296
|
+
// C13: keep systemPrompt stable for Google implicit cache; additionalContext goes as user prefix, not system mutation
|
|
297
|
+
if (options?.additionalContext) {
|
|
298
|
+
// Preserve stable instructions as systemPrompt
|
|
299
|
+
this.context.systemPrompt = fullInstructions;
|
|
300
|
+
const prefix = `[Additional Context]\n${options.additionalContext}\n\n`;
|
|
301
|
+
if (typeof prompt === "string") {
|
|
302
|
+
prompt = prefix + prompt;
|
|
303
|
+
} else if (Array.isArray(prompt)) {
|
|
304
|
+
prompt = [{ type: "text", text: prefix } as ContentPart, ...prompt];
|
|
305
|
+
}
|
|
306
|
+
} else if (this.context.systemPrompt !== fullInstructions) {
|
|
307
|
+
this.context.systemPrompt = fullInstructions;
|
|
308
|
+
}
|
|
309
|
+
// C11: keep context cachedContentId in sync with cacheConfig if updated via options
|
|
310
|
+
if (options?.headers && (this.cacheConfig as any)?.cachedContentId && !this.context.cachedContentId) {
|
|
311
|
+
this.context.cachedContentId = (this.cacheConfig as any).cachedContentId;
|
|
312
|
+
}
|
|
313
|
+
this.context.addUserMessage(prompt);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Runs one or more model/tool turns and resolves to a normalized AgentResponse.
|
|
318
|
+
* @example `const response = await agent.run("Summarize this document");`
|
|
319
|
+
*/
|
|
320
|
+
run(
|
|
321
|
+
prompt: string | ContentPart[],
|
|
322
|
+
options?: AgentRunOptions
|
|
323
|
+
): Promise<AgentResponse> & AssistantMessageEventStream {
|
|
324
|
+
const isStream = options?.stream === true;
|
|
325
|
+
if (isStream) {
|
|
326
|
+
const hasCallbacks = !!(options?.onDelta || options?.onThinkingDelta || options?.onEvent);
|
|
327
|
+
const s = this.stream(prompt, options) as any;
|
|
328
|
+
if (hasCallbacks) {
|
|
329
|
+
// Make `await agent.run(..., {stream:true, onDelta})` resolve to final response
|
|
330
|
+
// while still streaming via callbacks. The returned value stays iterable/on-able.
|
|
331
|
+
const resultPromise = s.result();
|
|
332
|
+
const hybrid: any = resultPromise;
|
|
333
|
+
hybrid[Symbol.asyncIterator] = s[Symbol.asyncIterator].bind(s);
|
|
334
|
+
hybrid.on = s.on.bind(s);
|
|
335
|
+
hybrid.off = s.off.bind(s);
|
|
336
|
+
hybrid.result = s.result.bind(s);
|
|
337
|
+
hybrid.cancel = s.cancel.bind(s);
|
|
338
|
+
hybrid.onCancel = s.onCancel.bind(s);
|
|
339
|
+
hybrid.isCancelled = s.isCancelled.bind(s);
|
|
340
|
+
// also expose push/end/fail for compat, though not needed by caller
|
|
341
|
+
return hybrid;
|
|
342
|
+
}
|
|
343
|
+
return s;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const resolved = resolveModel(this.modelStringOrSpec);
|
|
347
|
+
const effectiveLevel = options?.thinkingLevel || this.thinkingConfig?.level;
|
|
348
|
+
if (effectiveLevel) {
|
|
349
|
+
validateModelThinking(resolved.provider.id, resolved.modelId, effectiveLevel);
|
|
350
|
+
}
|
|
351
|
+
this.prepareTurn(prompt, options);
|
|
352
|
+
|
|
353
|
+
const providerOptions = {
|
|
354
|
+
apiKey: this.apiKey,
|
|
355
|
+
baseUrl: this.baseUrl,
|
|
356
|
+
headers: { ...(this.customHeaders ?? {}), ...(options?.headers ?? {}) },
|
|
357
|
+
thinking: resolveEffectiveThinking(this.thinkingConfig, options?.thinkingLevel),
|
|
358
|
+
cache: this.stateless
|
|
359
|
+
? { sessionId: options?.sessionId || this.sessionId }
|
|
360
|
+
: { ...this.cacheConfig, sessionId: options?.sessionId || this.sessionId },
|
|
361
|
+
serviceTier: this.serviceTier,
|
|
362
|
+
sessionId: options?.sessionId || this.sessionId,
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const loopConfig = {
|
|
366
|
+
agentName: this.name,
|
|
367
|
+
provider: resolved.provider,
|
|
368
|
+
modelId: resolved.modelId,
|
|
369
|
+
context: this.context,
|
|
370
|
+
tools: this.tools,
|
|
371
|
+
options: providerOptions,
|
|
372
|
+
runOptions: options,
|
|
373
|
+
maxTurns: this.maxTurns,
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const promise = runAgentLoop(loopConfig).then((res) => {
|
|
377
|
+
if (this.stateless) {
|
|
378
|
+
this.context.messages = [];
|
|
379
|
+
this.context.thoughtSignatures = [];
|
|
380
|
+
}
|
|
381
|
+
return res;
|
|
382
|
+
});
|
|
383
|
+
return promise as any;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Convenience API: non-streaming alias for run, or a text-delta async iterable when streaming.
|
|
388
|
+
* @example `const response = await agent.ask("What is the answer?");`
|
|
389
|
+
*/
|
|
390
|
+
ask(
|
|
391
|
+
prompt: string | ContentPart[],
|
|
392
|
+
optionsOrStream?: boolean | AgentRunOptions
|
|
393
|
+
): Promise<AgentResponse> & AsyncIterable<string | StreamEvent> {
|
|
394
|
+
const isStream =
|
|
395
|
+
typeof optionsOrStream === "boolean"
|
|
396
|
+
? optionsOrStream
|
|
397
|
+
: optionsOrStream?.stream === true;
|
|
398
|
+
|
|
399
|
+
const runOpts: AgentRunOptions =
|
|
400
|
+
typeof optionsOrStream === "object" ? optionsOrStream : { stream: isStream };
|
|
401
|
+
|
|
402
|
+
if (isStream) {
|
|
403
|
+
const stream = this.stream(prompt, runOpts);
|
|
404
|
+
// If callbacks are used, proxy them through string stream as well
|
|
405
|
+
if (runOpts.onDelta || runOpts.onThinkingDelta || runOpts.onEvent) {
|
|
406
|
+
if (runOpts.onEvent) stream.on("*", runOpts.onEvent as any);
|
|
407
|
+
if (runOpts.onDelta) stream.on("text_delta", (e: any) => runOpts.onDelta!(e.delta!, e));
|
|
408
|
+
if (runOpts.onThinkingDelta) stream.on("thinking_delta", (e: any) => runOpts.onThinkingDelta!(e.thinkingDelta!, e));
|
|
409
|
+
}
|
|
410
|
+
const stringStream: any = {
|
|
411
|
+
[Symbol.asyncIterator]: async function* () {
|
|
412
|
+
for await (const chunk of stream) {
|
|
413
|
+
if (chunk.type === "text_delta" && chunk.delta) {
|
|
414
|
+
yield chunk.delta;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
result: () => stream.result(),
|
|
419
|
+
on: (evt: string, fn: any) => stream.on(evt, fn),
|
|
420
|
+
cancel: () => stream.cancel(),
|
|
421
|
+
onCancel: (fn: any) => stream.onCancel(fn),
|
|
422
|
+
isCancelled: () => stream.isCancelled(),
|
|
423
|
+
};
|
|
424
|
+
// Make await work (resolves to AgentResponse)
|
|
425
|
+
const resultPromise = stream.result();
|
|
426
|
+
(stringStream as any).then = (res: any, rej: any) => resultPromise.then(res, rej);
|
|
427
|
+
(stringStream as any).catch = (rej: any) => resultPromise.catch(rej);
|
|
428
|
+
return stringStream;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return this.run(prompt, runOpts);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Starts a stream of text, thinking, tool, sub-agent, usage, and completion events.
|
|
436
|
+
* @example
|
|
437
|
+
* ```ts
|
|
438
|
+
* for await (const event of agent.stream("Explain this")) {
|
|
439
|
+
* if (event.type === "text_delta") process.stdout.write(event.delta ?? "");
|
|
440
|
+
* }
|
|
441
|
+
* ```
|
|
442
|
+
*/
|
|
443
|
+
stream(
|
|
444
|
+
prompt: string | ContentPart[],
|
|
445
|
+
options?: AgentRunOptions
|
|
446
|
+
): AssistantMessageEventStream {
|
|
447
|
+
const resolved = resolveModel(this.modelStringOrSpec);
|
|
448
|
+
const effectiveLevel = options?.thinkingLevel || this.thinkingConfig?.level;
|
|
449
|
+
if (effectiveLevel) {
|
|
450
|
+
validateModelThinking(resolved.provider.id, resolved.modelId, effectiveLevel);
|
|
451
|
+
}
|
|
452
|
+
this.prepareTurn(prompt, options);
|
|
453
|
+
|
|
454
|
+
const providerOptions = {
|
|
455
|
+
apiKey: this.apiKey,
|
|
456
|
+
baseUrl: this.baseUrl,
|
|
457
|
+
headers: { ...(this.customHeaders ?? {}), ...(options?.headers ?? {}) },
|
|
458
|
+
thinking: resolveEffectiveThinking(this.thinkingConfig, options?.thinkingLevel),
|
|
459
|
+
cache: this.stateless
|
|
460
|
+
? { sessionId: options?.sessionId || this.sessionId }
|
|
461
|
+
: { ...this.cacheConfig, sessionId: options?.sessionId || this.sessionId },
|
|
462
|
+
serviceTier: this.serviceTier,
|
|
463
|
+
sessionId: options?.sessionId || this.sessionId,
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const loopConfig = {
|
|
467
|
+
agentName: this.name,
|
|
468
|
+
provider: resolved.provider,
|
|
469
|
+
modelId: resolved.modelId,
|
|
470
|
+
context: this.context,
|
|
471
|
+
tools: this.tools,
|
|
472
|
+
options: providerOptions,
|
|
473
|
+
runOptions: options,
|
|
474
|
+
maxTurns: this.maxTurns,
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const s = streamAgentLoop(loopConfig);
|
|
478
|
+
if (this.stateless) {
|
|
479
|
+
s.result().then(() => {
|
|
480
|
+
this.context.messages = [];
|
|
481
|
+
this.context.thoughtSignatures = [];
|
|
482
|
+
}).catch(() => {});
|
|
483
|
+
}
|
|
484
|
+
// Wire one-liner callbacks so `stream:true` + onDelta is enough — no manual for-await needed
|
|
485
|
+
if (options?.wrapThinking) {
|
|
486
|
+
// Auto-wrap reasoning as <think>\n...\n</think>\n\n per-turn — clean boundaries across multi-turn agent runs
|
|
487
|
+
let isThinking = false;
|
|
488
|
+
const userOnThinking = options.onThinkingDelta;
|
|
489
|
+
const userOnDelta = options.onDelta;
|
|
490
|
+
const userOnEvent = options.onEvent;
|
|
491
|
+
if (userOnEvent) s.on("*", userOnEvent as any);
|
|
492
|
+
|
|
493
|
+
const openThink = (e?: any) => {
|
|
494
|
+
if (!isThinking) {
|
|
495
|
+
isThinking = true;
|
|
496
|
+
const tag = "<think>\n";
|
|
497
|
+
if (userOnThinking) userOnThinking(tag, e);
|
|
498
|
+
else if (userOnDelta) userOnDelta(tag, e as any);
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const closeThink = (e?: any) => {
|
|
503
|
+
if (isThinking) {
|
|
504
|
+
isThinking = false;
|
|
505
|
+
const close = "\n</think>\n\n";
|
|
506
|
+
if (userOnThinking) userOnThinking(close, e);
|
|
507
|
+
else if (userOnDelta) userOnDelta(close, e as any);
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
if (userOnThinking || userOnDelta) {
|
|
512
|
+
s.on("thinking_delta", (e: any) => {
|
|
513
|
+
openThink(e);
|
|
514
|
+
if (userOnThinking) userOnThinking(e.thinkingDelta!, e);
|
|
515
|
+
else if (userOnDelta) userOnDelta(e.thinkingDelta!, e as any);
|
|
516
|
+
});
|
|
517
|
+
s.on("text_delta", (e: any) => {
|
|
518
|
+
closeThink(e);
|
|
519
|
+
if (userOnDelta) userOnDelta(e.delta!, e);
|
|
520
|
+
else if (userOnThinking) userOnThinking(e.delta!, e as any);
|
|
521
|
+
});
|
|
522
|
+
// Close thinking tag and reset turn state before tool/subagent execution or results
|
|
523
|
+
s.on("tool_call_start" as any, (e: any) => closeThink(e));
|
|
524
|
+
s.on("tool_call_complete" as any, (e: any) => closeThink(e));
|
|
525
|
+
s.on("tool_result" as any, (e: any) => closeThink(e));
|
|
526
|
+
s.on("subagent_complete" as any, (e: any) => closeThink(e));
|
|
527
|
+
s.on("done", (e: any) => {
|
|
528
|
+
closeThink(e || ({ type: "done" } as any));
|
|
529
|
+
});
|
|
530
|
+
} else {
|
|
531
|
+
// No callbacks but wrapThinking true — still emit tags as events for manual iteration
|
|
532
|
+
s.on("thinking_delta", (e: any) => {
|
|
533
|
+
if (!isThinking) {
|
|
534
|
+
isThinking = true;
|
|
535
|
+
s.push({ type: "thinking_delta", thinkingDelta: "<think>\n" } as any);
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
s.on("text_delta", (e: any) => {
|
|
539
|
+
if (isThinking) {
|
|
540
|
+
isThinking = false;
|
|
541
|
+
s.push({ type: "thinking_delta", thinkingDelta: "\n</think>\n\n" } as any);
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
s.on("tool_call_start" as any, () => {
|
|
545
|
+
if (isThinking) {
|
|
546
|
+
isThinking = false;
|
|
547
|
+
s.push({ type: "thinking_delta", thinkingDelta: "\n</think>\n\n" } as any);
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
s.on("tool_call_complete" as any, () => {
|
|
551
|
+
if (isThinking) {
|
|
552
|
+
isThinking = false;
|
|
553
|
+
s.push({ type: "thinking_delta", thinkingDelta: "\n</think>\n\n" } as any);
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
s.on("tool_result" as any, () => {
|
|
557
|
+
if (isThinking) {
|
|
558
|
+
isThinking = false;
|
|
559
|
+
s.push({ type: "thinking_delta", thinkingDelta: "\n</think>\n\n" } as any);
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
s.on("subagent_complete" as any, () => {
|
|
563
|
+
if (isThinking) {
|
|
564
|
+
isThinking = false;
|
|
565
|
+
s.push({ type: "thinking_delta", thinkingDelta: "\n</think>\n\n" } as any);
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
s.on("done", () => {
|
|
569
|
+
if (isThinking) {
|
|
570
|
+
isThinking = false;
|
|
571
|
+
s.push({ type: "thinking_delta", thinkingDelta: "\n</think>\n\n" } as any);
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
if (userOnEvent) s.on("*", userOnEvent as any);
|
|
575
|
+
}
|
|
576
|
+
} else {
|
|
577
|
+
if (options?.onEvent) s.on("*", options.onEvent as any);
|
|
578
|
+
if (options?.onDelta) s.on("text_delta", (e: any) => options.onDelta!(e.delta!, e));
|
|
579
|
+
if (options?.onThinkingDelta) s.on("thinking_delta", (e: any) => options.onThinkingDelta!(e.thinkingDelta!, e));
|
|
580
|
+
}
|
|
581
|
+
return s;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** Normalizes the public `thinkingLevel` flag into provider-neutral thinking settings. */
|
|
586
|
+
export function normalizeThinking(config: AgentConfig, fallbackLevel?: string): ThinkingConfig | undefined {
|
|
587
|
+
// DX4: only thinkingLevel, values: none, dynamic, minimal, low, medium, high, xhigh
|
|
588
|
+
const rawLevel = config.thinkingLevel ?? fallbackLevel;
|
|
589
|
+
const level = rawLevel as ThinkingLevel | undefined;
|
|
590
|
+
|
|
591
|
+
if (!level) return undefined;
|
|
592
|
+
|
|
593
|
+
// Validate model thinking from catalog if model is configured
|
|
594
|
+
if (config.model) {
|
|
595
|
+
try {
|
|
596
|
+
const rawModel: any = (config.model as any)?.model ? (config.model as any).model : config.model;
|
|
597
|
+
const modelStr = typeof rawModel === "string" ? rawModel : rawModel?.id || "";
|
|
598
|
+
const provStr = typeof rawModel === "object" && rawModel?.provider ? rawModel.provider : modelStr.includes("/") ? modelStr.split("/")[0] : "";
|
|
599
|
+
const actualModelId = modelStr.includes("/") ? modelStr.split("/").slice(1).join("/") : modelStr;
|
|
600
|
+
if (actualModelId) {
|
|
601
|
+
validateModelThinking(provStr || "opencode", actualModelId, level);
|
|
602
|
+
}
|
|
603
|
+
} catch (e) {
|
|
604
|
+
throw e;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (level === "none") {
|
|
609
|
+
return { enabled: false, level: "none", budgetTokens: 0 };
|
|
610
|
+
}
|
|
611
|
+
if (level === "dynamic") {
|
|
612
|
+
return { enabled: true, level: "dynamic", budgetTokens: -1 };
|
|
613
|
+
}
|
|
614
|
+
return { enabled: true, level };
|
|
615
|
+
}
|