@radaros/core 0.3.5 → 0.3.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/dist/index.d.ts +1711 -0
- package/dist/index.js +6341 -0
- package/package.json +6 -2
- package/src/a2a/a2a-remote-agent.ts +0 -270
- package/src/a2a/types.ts +0 -142
- package/src/agent/agent.ts +0 -417
- package/src/agent/llm-loop.ts +0 -290
- package/src/agent/run-context.ts +0 -35
- package/src/agent/types.ts +0 -89
- package/src/events/event-bus.ts +0 -45
- package/src/events/types.ts +0 -16
- package/src/guardrails/types.ts +0 -5
- package/src/hooks/types.ts +0 -6
- package/src/index.ts +0 -157
- package/src/knowledge/knowledge-base.ts +0 -146
- package/src/logger/logger.ts +0 -249
- package/src/mcp/mcp-client.ts +0 -264
- package/src/memory/memory.ts +0 -87
- package/src/memory/types.ts +0 -13
- package/src/memory/user-memory.ts +0 -211
- package/src/models/provider.ts +0 -22
- package/src/models/providers/anthropic.ts +0 -360
- package/src/models/providers/google.ts +0 -386
- package/src/models/providers/ollama.ts +0 -211
- package/src/models/providers/openai.ts +0 -345
- package/src/models/providers/vertex.ts +0 -427
- package/src/models/registry.ts +0 -107
- package/src/models/types.ts +0 -124
- package/src/session/session-manager.ts +0 -75
- package/src/session/types.ts +0 -10
- package/src/storage/driver.ts +0 -10
- package/src/storage/in-memory.ts +0 -44
- package/src/storage/mongodb.ts +0 -70
- package/src/storage/postgres.ts +0 -81
- package/src/storage/sqlite.ts +0 -81
- package/src/team/modes.ts +0 -1
- package/src/team/team.ts +0 -323
- package/src/team/types.ts +0 -26
- package/src/toolkits/base.ts +0 -15
- package/src/toolkits/duckduckgo.ts +0 -256
- package/src/toolkits/gmail.ts +0 -226
- package/src/toolkits/hackernews.ts +0 -121
- package/src/toolkits/websearch.ts +0 -158
- package/src/toolkits/whatsapp.ts +0 -209
- package/src/tools/define-tool.ts +0 -22
- package/src/tools/tool-executor.ts +0 -221
- package/src/tools/types.ts +0 -36
- package/src/utils/retry.ts +0 -56
- package/src/vector/base.ts +0 -44
- package/src/vector/embeddings/google.ts +0 -64
- package/src/vector/embeddings/openai.ts +0 -66
- package/src/vector/in-memory.ts +0 -115
- package/src/vector/mongodb.ts +0 -241
- package/src/vector/pgvector.ts +0 -169
- package/src/vector/qdrant.ts +0 -203
- package/src/vector/types.ts +0 -55
- package/src/workflow/step-runner.ts +0 -303
- package/src/workflow/types.ts +0 -55
- package/src/workflow/workflow.ts +0 -68
- package/tsconfig.json +0 -8
package/src/agent/agent.ts
DELETED
|
@@ -1,417 +0,0 @@
|
|
|
1
|
-
import { v4 as uuidv4 } from "uuid";
|
|
2
|
-
import { EventBus } from "../events/event-bus.js";
|
|
3
|
-
import { InMemoryStorage } from "../storage/in-memory.js";
|
|
4
|
-
import { SessionManager } from "../session/session-manager.js";
|
|
5
|
-
import { ToolExecutor } from "../tools/tool-executor.js";
|
|
6
|
-
import { Logger } from "../logger/logger.js";
|
|
7
|
-
import { LLMLoop } from "./llm-loop.js";
|
|
8
|
-
import { RunContext } from "./run-context.js";
|
|
9
|
-
import { getTextContent, type ChatMessage, type MessageContent, type StreamChunk } from "../models/types.js";
|
|
10
|
-
import type { Session } from "../session/types.js";
|
|
11
|
-
import type {
|
|
12
|
-
AgentConfig,
|
|
13
|
-
RunOpts,
|
|
14
|
-
RunOutput,
|
|
15
|
-
} from "./types.js";
|
|
16
|
-
|
|
17
|
-
export class Agent {
|
|
18
|
-
readonly name: string;
|
|
19
|
-
readonly eventBus: EventBus;
|
|
20
|
-
readonly instructions?: string | ((ctx: RunContext) => string);
|
|
21
|
-
|
|
22
|
-
private config: AgentConfig;
|
|
23
|
-
private sessionManager: SessionManager;
|
|
24
|
-
private llmLoop: LLMLoop;
|
|
25
|
-
private logger: Logger;
|
|
26
|
-
private storageInitPromise: Promise<void> | null = null;
|
|
27
|
-
|
|
28
|
-
get tools() {
|
|
29
|
-
return this.config.tools ?? [];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
get modelId(): string {
|
|
33
|
-
return this.config.model.modelId;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
get providerId(): string {
|
|
37
|
-
return this.config.model.providerId;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
get hasStructuredOutput(): boolean {
|
|
41
|
-
return !!this.config.structuredOutput;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
get structuredOutputSchema(): import("zod").ZodSchema | undefined {
|
|
45
|
-
return this.config.structuredOutput;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
constructor(config: AgentConfig) {
|
|
49
|
-
this.config = config;
|
|
50
|
-
this.name = config.name;
|
|
51
|
-
this.instructions = config.instructions;
|
|
52
|
-
this.eventBus = config.eventBus ?? new EventBus();
|
|
53
|
-
|
|
54
|
-
const storage = config.storage ?? new InMemoryStorage();
|
|
55
|
-
if (typeof (storage as any).initialize === "function") {
|
|
56
|
-
this.storageInitPromise = (storage as any).initialize();
|
|
57
|
-
}
|
|
58
|
-
this.sessionManager = new SessionManager(storage);
|
|
59
|
-
|
|
60
|
-
this.logger = new Logger({
|
|
61
|
-
level: config.logLevel ?? "silent",
|
|
62
|
-
prefix: config.name,
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const toolExecutor =
|
|
66
|
-
config.tools && config.tools.length > 0
|
|
67
|
-
? new ToolExecutor(config.tools)
|
|
68
|
-
: null;
|
|
69
|
-
|
|
70
|
-
this.llmLoop = new LLMLoop(config.model, toolExecutor, {
|
|
71
|
-
maxToolRoundtrips: config.maxToolRoundtrips ?? 10,
|
|
72
|
-
temperature: config.temperature,
|
|
73
|
-
structuredOutput: config.structuredOutput,
|
|
74
|
-
logger: this.logger,
|
|
75
|
-
reasoning: config.reasoning,
|
|
76
|
-
retry: config.retry,
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async run(input: MessageContent, opts?: RunOpts): Promise<RunOutput> {
|
|
81
|
-
const startTime = Date.now();
|
|
82
|
-
const sessionId = opts?.sessionId ?? this.config.sessionId ?? uuidv4();
|
|
83
|
-
const userId = opts?.userId ?? this.config.userId;
|
|
84
|
-
const inputText = typeof input === "string" ? input : getTextContent(input);
|
|
85
|
-
|
|
86
|
-
if (this.storageInitPromise) await this.storageInitPromise;
|
|
87
|
-
const session = await this.sessionManager.getOrCreate(sessionId, userId);
|
|
88
|
-
|
|
89
|
-
const ctx = new RunContext({
|
|
90
|
-
sessionId,
|
|
91
|
-
userId,
|
|
92
|
-
metadata: opts?.metadata ?? {},
|
|
93
|
-
eventBus: this.eventBus,
|
|
94
|
-
sessionState: { ...session.state },
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
this.logger.agentStart(this.name, inputText);
|
|
98
|
-
|
|
99
|
-
this.eventBus.emit("run.start", {
|
|
100
|
-
runId: ctx.runId,
|
|
101
|
-
agentName: this.name,
|
|
102
|
-
input: inputText,
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
if (this.config.hooks?.beforeRun) {
|
|
107
|
-
await this.config.hooks.beforeRun(ctx);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (this.config.guardrails?.input) {
|
|
111
|
-
for (const guardrail of this.config.guardrails.input) {
|
|
112
|
-
const result = await guardrail.validate(input, ctx);
|
|
113
|
-
if (!result.pass) {
|
|
114
|
-
throw new Error(
|
|
115
|
-
`Input guardrail "${guardrail.name}" blocked: ${result.reason}`
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const messages = await this.buildMessages(input, session, ctx);
|
|
122
|
-
const output = await this.llmLoop.run(messages, ctx, opts?.apiKey);
|
|
123
|
-
|
|
124
|
-
output.durationMs = Date.now() - startTime;
|
|
125
|
-
|
|
126
|
-
if (this.config.guardrails?.output) {
|
|
127
|
-
for (const guardrail of this.config.guardrails.output) {
|
|
128
|
-
const result = await guardrail.validate(output, ctx);
|
|
129
|
-
if (!result.pass) {
|
|
130
|
-
throw new Error(
|
|
131
|
-
`Output guardrail "${guardrail.name}" blocked: ${result.reason}`
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
await this.sessionManager.appendMessages(sessionId, [
|
|
138
|
-
{ role: "user", content: inputText },
|
|
139
|
-
{ role: "assistant", content: output.text },
|
|
140
|
-
]);
|
|
141
|
-
await this.sessionManager.updateState(sessionId, ctx.sessionState);
|
|
142
|
-
|
|
143
|
-
if (this.config.memory) {
|
|
144
|
-
await this.config.memory.addMessages(sessionId, [
|
|
145
|
-
{ role: "user", content: inputText },
|
|
146
|
-
{ role: "assistant", content: output.text },
|
|
147
|
-
]);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (this.config.userMemory && userId) {
|
|
151
|
-
this.config.userMemory
|
|
152
|
-
.extractAndStore(
|
|
153
|
-
userId,
|
|
154
|
-
[
|
|
155
|
-
{ role: "user", content: inputText },
|
|
156
|
-
{ role: "assistant", content: output.text },
|
|
157
|
-
],
|
|
158
|
-
this.config.model
|
|
159
|
-
)
|
|
160
|
-
.catch((e: unknown) => this.logger.warn(`UserMemory extraction failed: ${e}`));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (this.config.hooks?.afterRun) {
|
|
164
|
-
await this.config.hooks.afterRun(ctx, output);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (output.thinking) {
|
|
168
|
-
this.logger.thinking(output.thinking);
|
|
169
|
-
}
|
|
170
|
-
this.logger.agentEnd(this.name, output.text, output.usage, output.durationMs);
|
|
171
|
-
|
|
172
|
-
this.eventBus.emit("run.complete", {
|
|
173
|
-
runId: ctx.runId,
|
|
174
|
-
output,
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
return output;
|
|
178
|
-
} catch (error) {
|
|
179
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
180
|
-
|
|
181
|
-
this.logger.error(`Run failed: ${err.message}`);
|
|
182
|
-
|
|
183
|
-
if (this.config.hooks?.onError) {
|
|
184
|
-
await this.config.hooks.onError(ctx, err);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
this.eventBus.emit("run.error", {
|
|
188
|
-
runId: ctx.runId,
|
|
189
|
-
error: err,
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
throw err;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async *stream(
|
|
197
|
-
input: MessageContent,
|
|
198
|
-
opts?: RunOpts
|
|
199
|
-
): AsyncGenerator<StreamChunk> {
|
|
200
|
-
const sessionId = opts?.sessionId ?? this.config.sessionId ?? uuidv4();
|
|
201
|
-
const userId = opts?.userId ?? this.config.userId;
|
|
202
|
-
const inputText = typeof input === "string" ? input : getTextContent(input);
|
|
203
|
-
|
|
204
|
-
if (this.storageInitPromise) await this.storageInitPromise;
|
|
205
|
-
const session = await this.sessionManager.getOrCreate(sessionId, userId);
|
|
206
|
-
|
|
207
|
-
const ctx = new RunContext({
|
|
208
|
-
sessionId,
|
|
209
|
-
userId,
|
|
210
|
-
metadata: opts?.metadata ?? {},
|
|
211
|
-
eventBus: this.eventBus,
|
|
212
|
-
sessionState: { ...session.state },
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
this.eventBus.emit("run.start", {
|
|
216
|
-
runId: ctx.runId,
|
|
217
|
-
agentName: this.name,
|
|
218
|
-
input: inputText,
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
let fullText = "";
|
|
222
|
-
let streamOk = false;
|
|
223
|
-
let streamUsage: import("../models/types.js").TokenUsage = {
|
|
224
|
-
promptTokens: 0,
|
|
225
|
-
completionTokens: 0,
|
|
226
|
-
totalTokens: 0,
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
try {
|
|
230
|
-
if (this.config.hooks?.beforeRun) {
|
|
231
|
-
await this.config.hooks.beforeRun(ctx);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (this.config.guardrails?.input) {
|
|
235
|
-
for (const guardrail of this.config.guardrails.input) {
|
|
236
|
-
const result = await guardrail.validate(input, ctx);
|
|
237
|
-
if (!result.pass) {
|
|
238
|
-
throw new Error(
|
|
239
|
-
`Input guardrail "${guardrail.name}" blocked: ${result.reason}`
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const messages = await this.buildMessages(input, session, ctx);
|
|
246
|
-
|
|
247
|
-
for await (const chunk of this.llmLoop.stream(messages, ctx, opts?.apiKey)) {
|
|
248
|
-
if (chunk.type === "text") {
|
|
249
|
-
fullText += chunk.text;
|
|
250
|
-
} else if (chunk.type === "finish" && chunk.usage) {
|
|
251
|
-
streamUsage = {
|
|
252
|
-
promptTokens: streamUsage.promptTokens + chunk.usage.promptTokens,
|
|
253
|
-
completionTokens: streamUsage.completionTokens + chunk.usage.completionTokens,
|
|
254
|
-
totalTokens: streamUsage.totalTokens + chunk.usage.totalTokens,
|
|
255
|
-
...(chunk.usage.reasoningTokens
|
|
256
|
-
? { reasoningTokens: (streamUsage.reasoningTokens ?? 0) + chunk.usage.reasoningTokens }
|
|
257
|
-
: {}),
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
yield chunk;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
streamOk = true;
|
|
264
|
-
} catch (error) {
|
|
265
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
266
|
-
|
|
267
|
-
if (this.config.hooks?.onError) {
|
|
268
|
-
await this.config.hooks.onError(ctx, err);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
this.eventBus.emit("run.error", {
|
|
272
|
-
runId: ctx.runId,
|
|
273
|
-
error: err,
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
throw err;
|
|
277
|
-
} finally {
|
|
278
|
-
if (streamOk) {
|
|
279
|
-
await this.sessionManager.appendMessages(sessionId, [
|
|
280
|
-
{ role: "user", content: inputText },
|
|
281
|
-
{ role: "assistant", content: fullText },
|
|
282
|
-
]);
|
|
283
|
-
await this.sessionManager.updateState(sessionId, ctx.sessionState);
|
|
284
|
-
|
|
285
|
-
if (this.config.memory) {
|
|
286
|
-
await this.config.memory.addMessages(sessionId, [
|
|
287
|
-
{ role: "user", content: inputText },
|
|
288
|
-
{ role: "assistant", content: fullText },
|
|
289
|
-
]);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (this.config.userMemory && userId) {
|
|
293
|
-
this.config.userMemory
|
|
294
|
-
.extractAndStore(
|
|
295
|
-
userId,
|
|
296
|
-
[
|
|
297
|
-
{ role: "user", content: inputText },
|
|
298
|
-
{ role: "assistant", content: fullText },
|
|
299
|
-
],
|
|
300
|
-
this.config.model
|
|
301
|
-
)
|
|
302
|
-
.catch((e: unknown) => this.logger.warn(`UserMemory extraction failed: ${e}`));
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
this.eventBus.emit("run.complete", {
|
|
306
|
-
runId: ctx.runId,
|
|
307
|
-
output: {
|
|
308
|
-
text: fullText,
|
|
309
|
-
toolCalls: [],
|
|
310
|
-
usage: streamUsage,
|
|
311
|
-
},
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
private async buildMessages(
|
|
318
|
-
input: MessageContent,
|
|
319
|
-
session: Session,
|
|
320
|
-
ctx: RunContext
|
|
321
|
-
): Promise<ChatMessage[]> {
|
|
322
|
-
const messages: ChatMessage[] = [];
|
|
323
|
-
|
|
324
|
-
let systemContent = "";
|
|
325
|
-
if (this.config.instructions) {
|
|
326
|
-
systemContent =
|
|
327
|
-
typeof this.config.instructions === "function"
|
|
328
|
-
? this.config.instructions(ctx)
|
|
329
|
-
: this.config.instructions;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (this.config.memory) {
|
|
333
|
-
const memoryContext = await this.config.memory.getContextString(
|
|
334
|
-
session.sessionId
|
|
335
|
-
);
|
|
336
|
-
if (memoryContext) {
|
|
337
|
-
systemContent = systemContent
|
|
338
|
-
? `${systemContent}\n\n${memoryContext}`
|
|
339
|
-
: memoryContext;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
if (this.config.userMemory && ctx.userId) {
|
|
344
|
-
const hasRecallTool = (this.config.tools ?? []).some(
|
|
345
|
-
(t) => t.name === "recall_user_facts"
|
|
346
|
-
);
|
|
347
|
-
if (!hasRecallTool) {
|
|
348
|
-
const userContext = await this.config.userMemory.getContextString(ctx.userId);
|
|
349
|
-
if (userContext) {
|
|
350
|
-
systemContent = systemContent
|
|
351
|
-
? `${systemContent}\n\n${userContext}`
|
|
352
|
-
: userContext;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (systemContent) {
|
|
358
|
-
messages.push({ role: "system", content: systemContent });
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
if (this.config.addHistoryToMessages !== false) {
|
|
362
|
-
const limit = this.config.numHistoryRuns
|
|
363
|
-
? this.config.numHistoryRuns * 2
|
|
364
|
-
: 20;
|
|
365
|
-
let history = session.messages ?? [];
|
|
366
|
-
if (limit > 0 && history.length > limit) {
|
|
367
|
-
history = history.slice(-limit);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
if (this.config.maxContextTokens) {
|
|
371
|
-
history = this.trimHistoryByTokens(history, systemContent, input);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (history.length > 0) {
|
|
375
|
-
this.logger.info(`Loaded ${history.length} history messages for session ${session.sessionId}`);
|
|
376
|
-
}
|
|
377
|
-
messages.push(...history);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
messages.push({ role: "user", content: input });
|
|
381
|
-
|
|
382
|
-
this.logger.info(`Sending ${messages.length} messages to LLM`);
|
|
383
|
-
|
|
384
|
-
return messages;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
private estimateTokens(text: string): number {
|
|
388
|
-
return Math.ceil(text.length / 3.5);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
private trimHistoryByTokens(
|
|
392
|
-
history: ChatMessage[],
|
|
393
|
-
systemContent: string,
|
|
394
|
-
currentInput: MessageContent
|
|
395
|
-
): ChatMessage[] {
|
|
396
|
-
const maxTokens = this.config.maxContextTokens!;
|
|
397
|
-
const inputText = typeof currentInput === "string" ? currentInput : "(multimodal)";
|
|
398
|
-
let reservedTokens = this.estimateTokens(systemContent) + this.estimateTokens(inputText) + 100;
|
|
399
|
-
|
|
400
|
-
const available = maxTokens - reservedTokens;
|
|
401
|
-
if (available <= 0) return [];
|
|
402
|
-
|
|
403
|
-
const result: ChatMessage[] = [];
|
|
404
|
-
let used = 0;
|
|
405
|
-
|
|
406
|
-
for (let i = history.length - 1; i >= 0; i--) {
|
|
407
|
-
const msg = history[i];
|
|
408
|
-
const text = typeof msg.content === "string" ? msg.content : "";
|
|
409
|
-
const tokens = this.estimateTokens(text);
|
|
410
|
-
if (used + tokens > available) break;
|
|
411
|
-
used += tokens;
|
|
412
|
-
result.unshift(msg);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
return result;
|
|
416
|
-
}
|
|
417
|
-
}
|
package/src/agent/llm-loop.ts
DELETED
|
@@ -1,290 +0,0 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
2
|
-
import type { z } from "zod";
|
|
3
|
-
import type { ModelProvider } from "../models/provider.js";
|
|
4
|
-
import {
|
|
5
|
-
getTextContent,
|
|
6
|
-
type ChatMessage,
|
|
7
|
-
type ModelConfig,
|
|
8
|
-
type ReasoningConfig,
|
|
9
|
-
type StreamChunk,
|
|
10
|
-
type ToolDefinition,
|
|
11
|
-
} from "../models/types.js";
|
|
12
|
-
import type { ToolExecutor } from "../tools/tool-executor.js";
|
|
13
|
-
import type { RunContext } from "./run-context.js";
|
|
14
|
-
import type { RunOutput } from "./types.js";
|
|
15
|
-
import type { ToolCallResult } from "../tools/types.js";
|
|
16
|
-
import type { Logger } from "../logger/logger.js";
|
|
17
|
-
import { withRetry, type RetryConfig } from "../utils/retry.js";
|
|
18
|
-
|
|
19
|
-
const _require = createRequire(import.meta.url);
|
|
20
|
-
|
|
21
|
-
export class LLMLoop {
|
|
22
|
-
private provider: ModelProvider;
|
|
23
|
-
private toolExecutor: ToolExecutor | null;
|
|
24
|
-
private maxToolRoundtrips: number;
|
|
25
|
-
private temperature?: number;
|
|
26
|
-
private maxTokens?: number;
|
|
27
|
-
private structuredOutput?: z.ZodSchema;
|
|
28
|
-
private logger?: Logger;
|
|
29
|
-
private reasoning?: ReasoningConfig;
|
|
30
|
-
private retry?: Partial<RetryConfig>;
|
|
31
|
-
|
|
32
|
-
constructor(
|
|
33
|
-
provider: ModelProvider,
|
|
34
|
-
toolExecutor: ToolExecutor | null,
|
|
35
|
-
options: {
|
|
36
|
-
maxToolRoundtrips: number;
|
|
37
|
-
temperature?: number;
|
|
38
|
-
maxTokens?: number;
|
|
39
|
-
structuredOutput?: z.ZodSchema;
|
|
40
|
-
logger?: Logger;
|
|
41
|
-
reasoning?: ReasoningConfig;
|
|
42
|
-
retry?: Partial<RetryConfig>;
|
|
43
|
-
}
|
|
44
|
-
) {
|
|
45
|
-
this.provider = provider;
|
|
46
|
-
this.toolExecutor = toolExecutor;
|
|
47
|
-
this.maxToolRoundtrips = options.maxToolRoundtrips;
|
|
48
|
-
this.temperature = options.temperature;
|
|
49
|
-
this.maxTokens = options.maxTokens;
|
|
50
|
-
this.structuredOutput = options.structuredOutput;
|
|
51
|
-
this.logger = options.logger;
|
|
52
|
-
this.reasoning = options.reasoning;
|
|
53
|
-
this.retry = options.retry;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async run(messages: ChatMessage[], ctx: RunContext, apiKey?: string): Promise<RunOutput> {
|
|
57
|
-
const allToolCalls: ToolCallResult[] = [];
|
|
58
|
-
let totalPromptTokens = 0;
|
|
59
|
-
let totalCompletionTokens = 0;
|
|
60
|
-
let totalReasoningTokens = 0;
|
|
61
|
-
let thinkingContent = "";
|
|
62
|
-
const currentMessages = [...messages];
|
|
63
|
-
const toolDefs = this.toolExecutor?.getToolDefinitions() ?? [];
|
|
64
|
-
|
|
65
|
-
for (let roundtrip = 0; roundtrip <= this.maxToolRoundtrips; roundtrip++) {
|
|
66
|
-
const modelConfig: ModelConfig & { tools?: ToolDefinition[] } = {};
|
|
67
|
-
if (apiKey) modelConfig.apiKey = apiKey;
|
|
68
|
-
if (this.temperature !== undefined)
|
|
69
|
-
modelConfig.temperature = this.temperature;
|
|
70
|
-
if (this.maxTokens !== undefined) modelConfig.maxTokens = this.maxTokens;
|
|
71
|
-
if (toolDefs.length > 0) modelConfig.tools = toolDefs;
|
|
72
|
-
if (this.reasoning) modelConfig.reasoning = this.reasoning;
|
|
73
|
-
|
|
74
|
-
if (this.structuredOutput) {
|
|
75
|
-
modelConfig.responseFormat = {
|
|
76
|
-
type: "json_schema",
|
|
77
|
-
schema: this.zodToJsonSchema(this.structuredOutput),
|
|
78
|
-
name: "structured_response",
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const response = await withRetry(
|
|
83
|
-
() => this.provider.generate(currentMessages, modelConfig),
|
|
84
|
-
this.retry
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
totalPromptTokens += response.usage.promptTokens;
|
|
88
|
-
totalCompletionTokens += response.usage.completionTokens;
|
|
89
|
-
if (response.usage.reasoningTokens) totalReasoningTokens += response.usage.reasoningTokens;
|
|
90
|
-
|
|
91
|
-
if ((response as any).thinking) {
|
|
92
|
-
thinkingContent += (thinkingContent ? "\n" : "") + (response as any).thinking;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
currentMessages.push(response.message);
|
|
96
|
-
|
|
97
|
-
if (
|
|
98
|
-
response.finishReason !== "tool_calls" ||
|
|
99
|
-
!response.message.toolCalls?.length ||
|
|
100
|
-
!this.toolExecutor
|
|
101
|
-
) {
|
|
102
|
-
const text = getTextContent(response.message.content);
|
|
103
|
-
|
|
104
|
-
const output: RunOutput = {
|
|
105
|
-
text,
|
|
106
|
-
toolCalls: allToolCalls,
|
|
107
|
-
usage: {
|
|
108
|
-
promptTokens: totalPromptTokens,
|
|
109
|
-
completionTokens: totalCompletionTokens,
|
|
110
|
-
totalTokens: totalPromptTokens + totalCompletionTokens,
|
|
111
|
-
...(totalReasoningTokens > 0 ? { reasoningTokens: totalReasoningTokens } : {}),
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
if (thinkingContent) output.thinking = thinkingContent;
|
|
116
|
-
|
|
117
|
-
if (this.structuredOutput && text) {
|
|
118
|
-
try {
|
|
119
|
-
const jsonStr = this.extractJson(text);
|
|
120
|
-
const parsed = JSON.parse(jsonStr);
|
|
121
|
-
output.structured = this.structuredOutput.parse(parsed);
|
|
122
|
-
} catch {
|
|
123
|
-
// structured parsing failed, raw text is still available
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return output;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const toolResults = await this.toolExecutor.executeAll(
|
|
131
|
-
response.message.toolCalls,
|
|
132
|
-
ctx
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
allToolCalls.push(...toolResults);
|
|
136
|
-
|
|
137
|
-
for (const result of toolResults) {
|
|
138
|
-
const content =
|
|
139
|
-
typeof result.result === "string"
|
|
140
|
-
? result.result
|
|
141
|
-
: result.result.content;
|
|
142
|
-
|
|
143
|
-
this.logger?.toolCall(result.toolName, {});
|
|
144
|
-
this.logger?.toolResult(result.toolName, typeof content === "string" ? content : JSON.stringify(content));
|
|
145
|
-
|
|
146
|
-
currentMessages.push({
|
|
147
|
-
role: "tool",
|
|
148
|
-
content,
|
|
149
|
-
toolCallId: result.toolCallId,
|
|
150
|
-
name: result.toolName,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const lastAssistantMsg = currentMessages
|
|
156
|
-
.reverse()
|
|
157
|
-
.find((m) => m.role === "assistant");
|
|
158
|
-
|
|
159
|
-
const text = getTextContent(lastAssistantMsg?.content ?? null);
|
|
160
|
-
|
|
161
|
-
return {
|
|
162
|
-
text,
|
|
163
|
-
toolCalls: allToolCalls,
|
|
164
|
-
usage: {
|
|
165
|
-
promptTokens: totalPromptTokens,
|
|
166
|
-
completionTokens: totalCompletionTokens,
|
|
167
|
-
totalTokens: totalPromptTokens + totalCompletionTokens,
|
|
168
|
-
...(totalReasoningTokens > 0 ? { reasoningTokens: totalReasoningTokens } : {}),
|
|
169
|
-
},
|
|
170
|
-
...(thinkingContent ? { thinking: thinkingContent } : {}),
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
async *stream(
|
|
175
|
-
messages: ChatMessage[],
|
|
176
|
-
ctx: RunContext,
|
|
177
|
-
apiKey?: string
|
|
178
|
-
): AsyncGenerator<StreamChunk> {
|
|
179
|
-
const currentMessages = [...messages];
|
|
180
|
-
const toolDefs = this.toolExecutor?.getToolDefinitions() ?? [];
|
|
181
|
-
|
|
182
|
-
for (let roundtrip = 0; roundtrip <= this.maxToolRoundtrips; roundtrip++) {
|
|
183
|
-
const modelConfig: ModelConfig & { tools?: ToolDefinition[] } = {};
|
|
184
|
-
if (apiKey) modelConfig.apiKey = apiKey;
|
|
185
|
-
if (this.temperature !== undefined)
|
|
186
|
-
modelConfig.temperature = this.temperature;
|
|
187
|
-
if (this.maxTokens !== undefined) modelConfig.maxTokens = this.maxTokens;
|
|
188
|
-
if (toolDefs.length > 0) modelConfig.tools = toolDefs;
|
|
189
|
-
if (this.reasoning) modelConfig.reasoning = this.reasoning;
|
|
190
|
-
|
|
191
|
-
let fullText = "";
|
|
192
|
-
const pendingToolCalls: Array<{
|
|
193
|
-
id: string;
|
|
194
|
-
name: string;
|
|
195
|
-
args: string;
|
|
196
|
-
}> = [];
|
|
197
|
-
let finishReason = "stop";
|
|
198
|
-
|
|
199
|
-
const streamGen = this.provider.stream(currentMessages, modelConfig);
|
|
200
|
-
|
|
201
|
-
for await (const chunk of streamGen) {
|
|
202
|
-
yield chunk;
|
|
203
|
-
|
|
204
|
-
if (chunk.type === "text") {
|
|
205
|
-
fullText += chunk.text;
|
|
206
|
-
ctx.eventBus.emit("run.stream.chunk", {
|
|
207
|
-
runId: ctx.runId,
|
|
208
|
-
chunk: chunk.text,
|
|
209
|
-
});
|
|
210
|
-
} else if (chunk.type === "tool_call_start") {
|
|
211
|
-
pendingToolCalls.push({
|
|
212
|
-
id: chunk.toolCall.id,
|
|
213
|
-
name: chunk.toolCall.name,
|
|
214
|
-
args: "",
|
|
215
|
-
});
|
|
216
|
-
} else if (chunk.type === "tool_call_delta") {
|
|
217
|
-
const tc = pendingToolCalls.find(
|
|
218
|
-
(t) => t.id === chunk.toolCallId
|
|
219
|
-
);
|
|
220
|
-
if (tc) {
|
|
221
|
-
tc.args += chunk.argumentsDelta;
|
|
222
|
-
}
|
|
223
|
-
} else if (chunk.type === "finish") {
|
|
224
|
-
finishReason = chunk.finishReason;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (finishReason !== "tool_calls" || pendingToolCalls.length === 0 || !this.toolExecutor) {
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const assistantMsg: ChatMessage = {
|
|
233
|
-
role: "assistant",
|
|
234
|
-
content: fullText || null,
|
|
235
|
-
toolCalls: pendingToolCalls.map((tc) => ({
|
|
236
|
-
id: tc.id,
|
|
237
|
-
name: tc.name,
|
|
238
|
-
arguments: JSON.parse(tc.args || "{}"),
|
|
239
|
-
})),
|
|
240
|
-
};
|
|
241
|
-
currentMessages.push(assistantMsg);
|
|
242
|
-
|
|
243
|
-
const toolResults = await this.toolExecutor.executeAll(
|
|
244
|
-
assistantMsg.toolCalls!,
|
|
245
|
-
ctx
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
for (const result of toolResults) {
|
|
249
|
-
const content =
|
|
250
|
-
typeof result.result === "string"
|
|
251
|
-
? result.result
|
|
252
|
-
: result.result.content;
|
|
253
|
-
|
|
254
|
-
currentMessages.push({
|
|
255
|
-
role: "tool",
|
|
256
|
-
content,
|
|
257
|
-
toolCallId: result.toolCallId,
|
|
258
|
-
name: result.toolName,
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
private extractJson(text: string): string {
|
|
265
|
-
const fenceMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
|
|
266
|
-
if (fenceMatch) return fenceMatch[1].trim();
|
|
267
|
-
|
|
268
|
-
const braceStart = text.indexOf("{");
|
|
269
|
-
const braceEnd = text.lastIndexOf("}");
|
|
270
|
-
if (braceStart !== -1 && braceEnd > braceStart) {
|
|
271
|
-
return text.slice(braceStart, braceEnd + 1);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return text.trim();
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
private zodToJsonSchema(schema: z.ZodSchema): Record<string, unknown> {
|
|
278
|
-
try {
|
|
279
|
-
const { zodToJsonSchema } = _require("zod-to-json-schema");
|
|
280
|
-
const result = zodToJsonSchema(schema, {
|
|
281
|
-
target: "jsonSchema7",
|
|
282
|
-
$refStrategy: "none",
|
|
283
|
-
}) as Record<string, unknown>;
|
|
284
|
-
delete result["$schema"];
|
|
285
|
-
return result;
|
|
286
|
-
} catch {
|
|
287
|
-
return {};
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|