@tuttiai/core 0.6.0 → 0.8.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/dist/index.d.ts +101 -4
- package/dist/index.js +741 -96
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,3 +1,189 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var TuttiError = class extends Error {
|
|
3
|
+
constructor(code, message, context = {}) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.context = context;
|
|
7
|
+
this.name = this.constructor.name;
|
|
8
|
+
Error.captureStackTrace(this, this.constructor);
|
|
9
|
+
}
|
|
10
|
+
code;
|
|
11
|
+
context;
|
|
12
|
+
};
|
|
13
|
+
var ScoreValidationError = class extends TuttiError {
|
|
14
|
+
constructor(message, context = {}) {
|
|
15
|
+
super("SCORE_INVALID", message, context);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var AgentNotFoundError = class extends TuttiError {
|
|
19
|
+
constructor(agentId, available) {
|
|
20
|
+
super(
|
|
21
|
+
"AGENT_NOT_FOUND",
|
|
22
|
+
`Agent "${agentId}" not found in your score.
|
|
23
|
+
Available agents: ${available.join(", ")}
|
|
24
|
+
Check your tutti.score.ts \u2014 the agent ID must match the key in the agents object.`,
|
|
25
|
+
{ agent_id: agentId, available }
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var PermissionError = class extends TuttiError {
|
|
30
|
+
constructor(voice, required, granted) {
|
|
31
|
+
const missing = required.filter((p) => !granted.includes(p));
|
|
32
|
+
super(
|
|
33
|
+
"PERMISSION_DENIED",
|
|
34
|
+
`Voice "${voice}" requires permissions not granted: ${missing.join(", ")}
|
|
35
|
+
Grant them in your score file:
|
|
36
|
+
permissions: [${missing.map((p) => "'" + p + "'").join(", ")}]`,
|
|
37
|
+
{ voice, required, granted }
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var BudgetExceededError = class extends TuttiError {
|
|
42
|
+
constructor(tokens, costUsd, limit) {
|
|
43
|
+
super(
|
|
44
|
+
"BUDGET_EXCEEDED",
|
|
45
|
+
`Token budget exceeded: ${tokens.toLocaleString()} tokens, $${costUsd.toFixed(4)} (limit: ${limit}).`,
|
|
46
|
+
{ tokens, cost_usd: costUsd, limit }
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var ToolTimeoutError = class extends TuttiError {
|
|
51
|
+
constructor(tool, timeoutMs) {
|
|
52
|
+
super(
|
|
53
|
+
"TOOL_TIMEOUT",
|
|
54
|
+
`Tool "${tool}" timed out after ${timeoutMs}ms.
|
|
55
|
+
Increase tool_timeout_ms in your agent config, or check if the tool is hanging.`,
|
|
56
|
+
{ tool, timeout_ms: timeoutMs }
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var ProviderError = class extends TuttiError {
|
|
61
|
+
constructor(message, context = { provider: "unknown" }) {
|
|
62
|
+
super("PROVIDER_ERROR", message, context);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var AuthenticationError = class extends ProviderError {
|
|
66
|
+
constructor(provider) {
|
|
67
|
+
super(
|
|
68
|
+
`Authentication failed for ${provider}.
|
|
69
|
+
Check that the API key is set correctly in your .env file.`,
|
|
70
|
+
{ provider }
|
|
71
|
+
);
|
|
72
|
+
Object.defineProperty(this, "code", { value: "AUTH_ERROR" });
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
var RateLimitError = class extends ProviderError {
|
|
76
|
+
retryAfter;
|
|
77
|
+
constructor(provider, retryAfter) {
|
|
78
|
+
const msg = retryAfter ? `Rate limited by ${provider}. Retry after ${retryAfter}s.` : `Rate limited by ${provider}.`;
|
|
79
|
+
super(msg, { provider, retryAfter });
|
|
80
|
+
Object.defineProperty(this, "code", { value: "RATE_LIMIT" });
|
|
81
|
+
this.retryAfter = retryAfter;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
var ContextWindowError = class extends ProviderError {
|
|
85
|
+
maxTokens;
|
|
86
|
+
constructor(provider, maxTokens) {
|
|
87
|
+
super(
|
|
88
|
+
`Context window exceeded for ${provider}.` + (maxTokens ? ` Max: ${maxTokens.toLocaleString()} tokens.` : "") + `
|
|
89
|
+
Reduce message history or use a model with a larger context window.`,
|
|
90
|
+
{ provider, max_tokens: maxTokens }
|
|
91
|
+
);
|
|
92
|
+
Object.defineProperty(this, "code", { value: "CONTEXT_WINDOW" });
|
|
93
|
+
this.maxTokens = maxTokens;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
var VoiceError = class extends TuttiError {
|
|
97
|
+
constructor(message, context) {
|
|
98
|
+
super("VOICE_ERROR", message, context);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
var PathTraversalError = class extends VoiceError {
|
|
102
|
+
constructor(path) {
|
|
103
|
+
super(
|
|
104
|
+
`Path traversal detected: "${path}" is not allowed.
|
|
105
|
+
All file paths must stay within the allowed directory.`,
|
|
106
|
+
{ voice: "filesystem", path }
|
|
107
|
+
);
|
|
108
|
+
Object.defineProperty(this, "code", { value: "PATH_TRAVERSAL" });
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
var UrlValidationError = class extends VoiceError {
|
|
112
|
+
constructor(url) {
|
|
113
|
+
super(
|
|
114
|
+
`URL blocked: "${url}".
|
|
115
|
+
Only http:// and https:// URLs to public hosts are allowed.`,
|
|
116
|
+
{ voice: "playwright", url }
|
|
117
|
+
);
|
|
118
|
+
Object.defineProperty(this, "code", { value: "URL_BLOCKED" });
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// src/hooks/index.ts
|
|
123
|
+
function createLoggingHook(log) {
|
|
124
|
+
return {
|
|
125
|
+
async beforeLLMCall(ctx, request) {
|
|
126
|
+
log.info({ agent: ctx.agent_name, turn: ctx.turn, model: request.model }, "LLM call");
|
|
127
|
+
return request;
|
|
128
|
+
},
|
|
129
|
+
async afterLLMCall(ctx, response) {
|
|
130
|
+
log.info({ agent: ctx.agent_name, turn: ctx.turn, usage: response.usage }, "LLM response");
|
|
131
|
+
},
|
|
132
|
+
async beforeToolCall(ctx, tool, input) {
|
|
133
|
+
log.info({ agent: ctx.agent_name, tool, input }, "Tool call");
|
|
134
|
+
return input;
|
|
135
|
+
},
|
|
136
|
+
async afterToolCall(ctx, tool, result) {
|
|
137
|
+
log.info({ agent: ctx.agent_name, tool, is_error: result.is_error }, "Tool result");
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function createCacheHook(store) {
|
|
143
|
+
function cacheKey(tool, input) {
|
|
144
|
+
return tool + ":" + JSON.stringify(input);
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
async beforeToolCall(_ctx, tool, input) {
|
|
148
|
+
const cached = store.get(cacheKey(tool, input));
|
|
149
|
+
if (cached) return cached;
|
|
150
|
+
return input;
|
|
151
|
+
},
|
|
152
|
+
async afterToolCall(_ctx, tool, result) {
|
|
153
|
+
if (!result.is_error) {
|
|
154
|
+
store.set(cacheKey(tool, result.content), result.content);
|
|
155
|
+
}
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function createBlocklistHook(blockedTools) {
|
|
161
|
+
const blocked = new Set(blockedTools);
|
|
162
|
+
return {
|
|
163
|
+
async beforeToolCall(_ctx, tool) {
|
|
164
|
+
return !blocked.has(tool);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function createMaxCostHook(maxUsd) {
|
|
169
|
+
let totalCost = 0;
|
|
170
|
+
const INPUT_PER_M = 3;
|
|
171
|
+
const OUTPUT_PER_M = 15;
|
|
172
|
+
return {
|
|
173
|
+
async afterLLMCall(_ctx, response) {
|
|
174
|
+
totalCost += response.usage.input_tokens / 1e6 * INPUT_PER_M + response.usage.output_tokens / 1e6 * OUTPUT_PER_M;
|
|
175
|
+
},
|
|
176
|
+
async beforeLLMCall(ctx, request) {
|
|
177
|
+
if (totalCost >= maxUsd) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
"Max cost hook: $" + totalCost.toFixed(4) + " exceeds limit $" + maxUsd.toFixed(2) + " for agent " + ctx.agent_name
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
return request;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
1
187
|
// src/logger.ts
|
|
2
188
|
import pino from "pino";
|
|
3
189
|
var createLogger = (name) => pino({
|
|
@@ -103,6 +289,7 @@ async function shutdownTelemetry() {
|
|
|
103
289
|
}
|
|
104
290
|
|
|
105
291
|
// src/agent-runner.ts
|
|
292
|
+
import { z } from "zod";
|
|
106
293
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
107
294
|
|
|
108
295
|
// src/secrets.ts
|
|
@@ -232,17 +419,63 @@ var TokenBudget = class {
|
|
|
232
419
|
var DEFAULT_MAX_TURNS = 10;
|
|
233
420
|
var DEFAULT_MAX_TOOL_CALLS = 20;
|
|
234
421
|
var DEFAULT_TOOL_TIMEOUT_MS = 3e4;
|
|
422
|
+
var DEFAULT_HITL_TIMEOUT_S = 300;
|
|
423
|
+
var MAX_PROVIDER_RETRIES = 3;
|
|
424
|
+
var hitlRequestSchema = z.object({
|
|
425
|
+
question: z.string().describe("The question to ask the human"),
|
|
426
|
+
options: z.array(z.string()).optional().describe("If provided, the human picks one of these"),
|
|
427
|
+
timeout_seconds: z.number().optional().describe("How long to wait before timing out (default 300)")
|
|
428
|
+
});
|
|
429
|
+
async function withRetry(fn) {
|
|
430
|
+
for (let attempt = 1; ; attempt++) {
|
|
431
|
+
try {
|
|
432
|
+
return await fn();
|
|
433
|
+
} catch (err) {
|
|
434
|
+
if (attempt >= MAX_PROVIDER_RETRIES || !(err instanceof ProviderError)) {
|
|
435
|
+
throw err;
|
|
436
|
+
}
|
|
437
|
+
if (err instanceof RateLimitError && err.retryAfter) {
|
|
438
|
+
logger.warn({ attempt, retryAfter: err.retryAfter }, "Rate limited, waiting before retry");
|
|
439
|
+
await new Promise((r) => setTimeout(r, err.retryAfter * 1e3));
|
|
440
|
+
} else {
|
|
441
|
+
const delayMs = Math.min(1e3 * 2 ** (attempt - 1), 8e3);
|
|
442
|
+
logger.warn({ attempt, delayMs }, "Provider error, retrying with backoff");
|
|
443
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
235
448
|
var AgentRunner = class {
|
|
236
|
-
constructor(provider, events, sessions, semanticMemory) {
|
|
449
|
+
constructor(provider, events, sessions, semanticMemory, globalHooks) {
|
|
237
450
|
this.provider = provider;
|
|
238
451
|
this.events = events;
|
|
239
452
|
this.sessions = sessions;
|
|
240
453
|
this.semanticMemory = semanticMemory;
|
|
454
|
+
this.globalHooks = globalHooks;
|
|
241
455
|
}
|
|
242
456
|
provider;
|
|
243
457
|
events;
|
|
244
458
|
sessions;
|
|
245
459
|
semanticMemory;
|
|
460
|
+
globalHooks;
|
|
461
|
+
pendingHitl = /* @__PURE__ */ new Map();
|
|
462
|
+
async safeHook(fn) {
|
|
463
|
+
if (!fn) return void 0;
|
|
464
|
+
try {
|
|
465
|
+
return await fn() ?? void 0;
|
|
466
|
+
} catch (err) {
|
|
467
|
+
logger.warn({ error: err instanceof Error ? err.message : String(err) }, "Hook error (non-fatal)");
|
|
468
|
+
return void 0;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
/** Resolve a pending human-in-the-loop request for a session. */
|
|
472
|
+
answer(sessionId, answer) {
|
|
473
|
+
const resolve2 = this.pendingHitl.get(sessionId);
|
|
474
|
+
if (resolve2) {
|
|
475
|
+
this.pendingHitl.delete(sessionId);
|
|
476
|
+
resolve2(answer);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
246
479
|
async run(agent, input, session_id) {
|
|
247
480
|
const session = session_id ? this.sessions.get(session_id) : this.sessions.create(agent.name);
|
|
248
481
|
if (!session) {
|
|
@@ -253,13 +486,31 @@ Omit session_id to start a new conversation.`
|
|
|
253
486
|
);
|
|
254
487
|
}
|
|
255
488
|
return TuttiTracer.agentRun(agent.name, session.id, async () => {
|
|
489
|
+
const agentHooks = agent.hooks;
|
|
490
|
+
const hookCtx = {
|
|
491
|
+
agent_name: agent.name,
|
|
492
|
+
session_id: session.id,
|
|
493
|
+
turn: 0,
|
|
494
|
+
metadata: {}
|
|
495
|
+
};
|
|
496
|
+
await this.safeHook(() => this.globalHooks?.beforeAgentRun?.(hookCtx));
|
|
497
|
+
await this.safeHook(() => agentHooks?.beforeAgentRun?.(hookCtx));
|
|
256
498
|
logger.info({ agent: agent.name, session: session.id }, "Agent started");
|
|
257
499
|
this.events.emit({
|
|
258
500
|
type: "agent:start",
|
|
259
501
|
agent_name: agent.name,
|
|
260
502
|
session_id: session.id
|
|
261
503
|
});
|
|
262
|
-
const
|
|
504
|
+
const voiceCtx = { session_id: session.id, agent_name: agent.name };
|
|
505
|
+
for (const voice of agent.voices) {
|
|
506
|
+
if (voice.setup) {
|
|
507
|
+
await voice.setup(voiceCtx);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const allTools = [...agent.voices.flatMap((v) => v.tools)];
|
|
511
|
+
if (agent.allow_human_input) {
|
|
512
|
+
allTools.push(this.createHitlTool(agent.name, session.id));
|
|
513
|
+
}
|
|
263
514
|
const toolDefs = allTools.map(toolToDefinition);
|
|
264
515
|
const messages = [
|
|
265
516
|
...session.messages,
|
|
@@ -297,12 +548,17 @@ Omit session_id to start a new conversation.`
|
|
|
297
548
|
}
|
|
298
549
|
}
|
|
299
550
|
}
|
|
300
|
-
|
|
551
|
+
let request = {
|
|
301
552
|
model: agent.model,
|
|
302
553
|
system: systemPrompt,
|
|
303
554
|
messages,
|
|
304
555
|
tools: toolDefs.length > 0 ? toolDefs : void 0
|
|
305
556
|
};
|
|
557
|
+
hookCtx.turn = turns;
|
|
558
|
+
const globalReq = await this.safeHook(() => this.globalHooks?.beforeLLMCall?.(hookCtx, request));
|
|
559
|
+
if (globalReq) request = globalReq;
|
|
560
|
+
const agentReq = await this.safeHook(() => agentHooks?.beforeLLMCall?.(hookCtx, request));
|
|
561
|
+
if (agentReq) request = agentReq;
|
|
306
562
|
logger.debug({ agent: agent.name, model: agent.model }, "LLM request");
|
|
307
563
|
this.events.emit({
|
|
308
564
|
type: "llm:request",
|
|
@@ -311,7 +567,9 @@ Omit session_id to start a new conversation.`
|
|
|
311
567
|
});
|
|
312
568
|
const response = await TuttiTracer.llmCall(
|
|
313
569
|
agent.model ?? "unknown",
|
|
314
|
-
() =>
|
|
570
|
+
() => withRetry(
|
|
571
|
+
() => agent.streaming ? this.streamToResponse(agent.name, request) : this.provider.chat(request)
|
|
572
|
+
)
|
|
315
573
|
);
|
|
316
574
|
logger.debug(
|
|
317
575
|
{ agent: agent.name, stopReason: response.stop_reason, usage: response.usage },
|
|
@@ -322,6 +580,8 @@ Omit session_id to start a new conversation.`
|
|
|
322
580
|
agent_name: agent.name,
|
|
323
581
|
response
|
|
324
582
|
});
|
|
583
|
+
await this.safeHook(() => this.globalHooks?.afterLLMCall?.(hookCtx, response));
|
|
584
|
+
await this.safeHook(() => agentHooks?.afterLLMCall?.(hookCtx, response));
|
|
325
585
|
totalUsage.input_tokens += response.usage.input_tokens;
|
|
326
586
|
totalUsage.output_tokens += response.usage.output_tokens;
|
|
327
587
|
if (budget) {
|
|
@@ -402,7 +662,7 @@ Omit session_id to start a new conversation.`
|
|
|
402
662
|
}
|
|
403
663
|
const toolResults = await Promise.all(
|
|
404
664
|
toolUseBlocks.map(
|
|
405
|
-
(block) => this.executeTool(allTools, block, toolContext, toolTimeoutMs)
|
|
665
|
+
(block) => this.executeTool(allTools, block, toolContext, toolTimeoutMs, hookCtx, agentHooks)
|
|
406
666
|
)
|
|
407
667
|
);
|
|
408
668
|
messages.push({ role: "user", content: toolResults });
|
|
@@ -419,13 +679,16 @@ Omit session_id to start a new conversation.`
|
|
|
419
679
|
agent_name: agent.name,
|
|
420
680
|
session_id: session.id
|
|
421
681
|
});
|
|
422
|
-
|
|
682
|
+
const agentResult = {
|
|
423
683
|
session_id: session.id,
|
|
424
684
|
output,
|
|
425
685
|
messages,
|
|
426
686
|
turns,
|
|
427
687
|
usage: totalUsage
|
|
428
688
|
};
|
|
689
|
+
await this.safeHook(() => this.globalHooks?.afterAgentRun?.(hookCtx, agentResult));
|
|
690
|
+
await this.safeHook(() => agentHooks?.afterAgentRun?.(hookCtx, agentResult));
|
|
691
|
+
return agentResult;
|
|
429
692
|
});
|
|
430
693
|
}
|
|
431
694
|
async executeWithTimeout(fn, timeoutMs, toolName) {
|
|
@@ -433,18 +696,80 @@ Omit session_id to start a new conversation.`
|
|
|
433
696
|
fn(),
|
|
434
697
|
new Promise(
|
|
435
698
|
(_, reject) => setTimeout(
|
|
436
|
-
() => reject(
|
|
437
|
-
new Error(
|
|
438
|
-
`Tool "${toolName}" timed out after ${timeoutMs}ms.
|
|
439
|
-
Increase tool_timeout_ms in your agent config, or check if the tool is hanging.`
|
|
440
|
-
)
|
|
441
|
-
),
|
|
699
|
+
() => reject(new ToolTimeoutError(toolName, timeoutMs)),
|
|
442
700
|
timeoutMs
|
|
443
701
|
)
|
|
444
702
|
)
|
|
445
703
|
]);
|
|
446
704
|
}
|
|
447
|
-
async
|
|
705
|
+
async streamToResponse(agentName, request) {
|
|
706
|
+
const content = [];
|
|
707
|
+
let textBuffer = "";
|
|
708
|
+
let usage = { input_tokens: 0, output_tokens: 0 };
|
|
709
|
+
let stopReason = "end_turn";
|
|
710
|
+
for await (const chunk of this.provider.stream(request)) {
|
|
711
|
+
if (chunk.type === "text" && chunk.text) {
|
|
712
|
+
textBuffer += chunk.text;
|
|
713
|
+
this.events.emit({
|
|
714
|
+
type: "token:stream",
|
|
715
|
+
agent_name: agentName,
|
|
716
|
+
text: chunk.text
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
if (chunk.type === "tool_use" && chunk.tool) {
|
|
720
|
+
content.push({
|
|
721
|
+
type: "tool_use",
|
|
722
|
+
id: chunk.tool.id,
|
|
723
|
+
name: chunk.tool.name,
|
|
724
|
+
input: chunk.tool.input
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
if (chunk.type === "usage") {
|
|
728
|
+
if (chunk.usage) usage = chunk.usage;
|
|
729
|
+
if (chunk.stop_reason) stopReason = chunk.stop_reason;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
if (textBuffer) {
|
|
733
|
+
content.unshift({ type: "text", text: textBuffer });
|
|
734
|
+
}
|
|
735
|
+
return { id: "", content, stop_reason: stopReason, usage };
|
|
736
|
+
}
|
|
737
|
+
createHitlTool(agentName, sessionId) {
|
|
738
|
+
return {
|
|
739
|
+
name: "request_human_input",
|
|
740
|
+
description: "Pause and ask the human for guidance or approval before proceeding.",
|
|
741
|
+
parameters: hitlRequestSchema,
|
|
742
|
+
execute: async (input) => {
|
|
743
|
+
const timeout = (input.timeout_seconds ?? DEFAULT_HITL_TIMEOUT_S) * 1e3;
|
|
744
|
+
logger.info({ agent: agentName, question: input.question }, "Waiting for human input");
|
|
745
|
+
const answer = await new Promise((resolve2) => {
|
|
746
|
+
this.pendingHitl.set(sessionId, resolve2);
|
|
747
|
+
this.events.emit({
|
|
748
|
+
type: "hitl:requested",
|
|
749
|
+
agent_name: agentName,
|
|
750
|
+
session_id: sessionId,
|
|
751
|
+
question: input.question,
|
|
752
|
+
options: input.options
|
|
753
|
+
});
|
|
754
|
+
setTimeout(() => {
|
|
755
|
+
if (this.pendingHitl.has(sessionId)) {
|
|
756
|
+
this.pendingHitl.delete(sessionId);
|
|
757
|
+
this.events.emit({ type: "hitl:timeout", agent_name: agentName, session_id: sessionId });
|
|
758
|
+
resolve2("[timeout: human did not respond within " + timeout / 1e3 + "s]");
|
|
759
|
+
}
|
|
760
|
+
}, timeout);
|
|
761
|
+
});
|
|
762
|
+
this.events.emit({
|
|
763
|
+
type: "hitl:answered",
|
|
764
|
+
agent_name: agentName,
|
|
765
|
+
session_id: sessionId,
|
|
766
|
+
answer
|
|
767
|
+
});
|
|
768
|
+
return { content: "Human responded: " + answer };
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
async executeTool(tools, block, context, timeoutMs, hookCtx, agentHooks) {
|
|
448
773
|
const tool = tools.find((t) => t.name === block.name);
|
|
449
774
|
if (!tool) {
|
|
450
775
|
const available = tools.map((t) => t.name).join(", ") || "(none)";
|
|
@@ -456,6 +781,16 @@ Increase tool_timeout_ms in your agent config, or check if the tool is hanging.`
|
|
|
456
781
|
};
|
|
457
782
|
}
|
|
458
783
|
return TuttiTracer.toolCall(block.name, async () => {
|
|
784
|
+
if (hookCtx) {
|
|
785
|
+
const globalResult = await this.safeHook(() => this.globalHooks?.beforeToolCall?.(hookCtx, block.name, block.input));
|
|
786
|
+
if (globalResult === false) {
|
|
787
|
+
return { type: "tool_result", tool_use_id: block.id, content: "Tool call blocked by hook", is_error: true };
|
|
788
|
+
}
|
|
789
|
+
const agentResult = await this.safeHook(() => agentHooks?.beforeToolCall?.(hookCtx, block.name, block.input));
|
|
790
|
+
if (agentResult === false) {
|
|
791
|
+
return { type: "tool_result", tool_use_id: block.id, content: "Tool call blocked by hook", is_error: true };
|
|
792
|
+
}
|
|
793
|
+
}
|
|
459
794
|
logger.debug({ tool: block.name, input: block.input }, "Tool called");
|
|
460
795
|
this.events.emit({
|
|
461
796
|
type: "tool:start",
|
|
@@ -465,11 +800,17 @@ Increase tool_timeout_ms in your agent config, or check if the tool is hanging.`
|
|
|
465
800
|
});
|
|
466
801
|
try {
|
|
467
802
|
const parsed = tool.parameters.parse(block.input);
|
|
468
|
-
|
|
803
|
+
let result = await this.executeWithTimeout(
|
|
469
804
|
() => tool.execute(parsed, context),
|
|
470
805
|
timeoutMs,
|
|
471
806
|
block.name
|
|
472
807
|
);
|
|
808
|
+
if (hookCtx) {
|
|
809
|
+
const globalMod = await this.safeHook(() => this.globalHooks?.afterToolCall?.(hookCtx, block.name, result));
|
|
810
|
+
if (globalMod) result = globalMod;
|
|
811
|
+
const agentMod = await this.safeHook(() => agentHooks?.afterToolCall?.(hookCtx, block.name, result));
|
|
812
|
+
if (agentMod) result = agentMod;
|
|
813
|
+
}
|
|
473
814
|
logger.debug({ tool: block.name, result: result.content }, "Tool completed");
|
|
474
815
|
this.events.emit({
|
|
475
816
|
type: "tool:end",
|
|
@@ -699,18 +1040,18 @@ var PostgresSessionStore = class {
|
|
|
699
1040
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
700
1041
|
var InMemorySemanticStore = class {
|
|
701
1042
|
entries = [];
|
|
702
|
-
|
|
1043
|
+
add(entry) {
|
|
703
1044
|
const full = {
|
|
704
1045
|
...entry,
|
|
705
1046
|
id: randomUUID3(),
|
|
706
1047
|
created_at: /* @__PURE__ */ new Date()
|
|
707
1048
|
};
|
|
708
1049
|
this.entries.push(full);
|
|
709
|
-
return full;
|
|
1050
|
+
return Promise.resolve(full);
|
|
710
1051
|
}
|
|
711
|
-
|
|
1052
|
+
search(query, agent_name, limit = 5) {
|
|
712
1053
|
const queryTokens = tokenize(query);
|
|
713
|
-
if (queryTokens.size === 0) return [];
|
|
1054
|
+
if (queryTokens.size === 0) return Promise.resolve([]);
|
|
714
1055
|
const agentEntries = this.entries.filter(
|
|
715
1056
|
(e) => e.agent_name === agent_name
|
|
716
1057
|
);
|
|
@@ -723,13 +1064,17 @@ var InMemorySemanticStore = class {
|
|
|
723
1064
|
const score = overlap / queryTokens.size;
|
|
724
1065
|
return { entry, score };
|
|
725
1066
|
});
|
|
726
|
-
return
|
|
1067
|
+
return Promise.resolve(
|
|
1068
|
+
scored.filter((s) => s.score > 0).sort((a, b) => b.score - a.score).slice(0, limit).map((s) => s.entry)
|
|
1069
|
+
);
|
|
727
1070
|
}
|
|
728
|
-
|
|
1071
|
+
delete(id) {
|
|
729
1072
|
this.entries = this.entries.filter((e) => e.id !== id);
|
|
1073
|
+
return Promise.resolve();
|
|
730
1074
|
}
|
|
731
|
-
|
|
1075
|
+
clear(agent_name) {
|
|
732
1076
|
this.entries = this.entries.filter((e) => e.agent_name !== agent_name);
|
|
1077
|
+
return Promise.resolve();
|
|
733
1078
|
}
|
|
734
1079
|
};
|
|
735
1080
|
function tokenize(text) {
|
|
@@ -745,9 +1090,7 @@ var PermissionGuard = class {
|
|
|
745
1090
|
(p) => !granted.includes(p)
|
|
746
1091
|
);
|
|
747
1092
|
if (missing.length > 0) {
|
|
748
|
-
throw new
|
|
749
|
-
"Voice " + voice.name + " requires permissions not granted: " + missing.join(", ") + "\n\nGrant them in your score file:\n permissions: [" + missing.map((p) => "'" + p + "'").join(", ") + "]"
|
|
750
|
-
);
|
|
1093
|
+
throw new PermissionError(voice.name, voice.required_permissions, granted);
|
|
751
1094
|
}
|
|
752
1095
|
}
|
|
753
1096
|
static warn(voice) {
|
|
@@ -779,7 +1122,8 @@ var TuttiRuntime = class _TuttiRuntime {
|
|
|
779
1122
|
score.provider,
|
|
780
1123
|
this.events,
|
|
781
1124
|
this._sessions,
|
|
782
|
-
this.semanticMemory
|
|
1125
|
+
this.semanticMemory,
|
|
1126
|
+
score.hooks
|
|
783
1127
|
);
|
|
784
1128
|
if (score.telemetry) {
|
|
785
1129
|
initTelemetry(score.telemetry);
|
|
@@ -805,15 +1149,17 @@ var TuttiRuntime = class _TuttiRuntime {
|
|
|
805
1149
|
if (memory.provider === "postgres") {
|
|
806
1150
|
const url = memory.url ?? process.env.DATABASE_URL;
|
|
807
1151
|
if (!url) {
|
|
808
|
-
throw new
|
|
809
|
-
"PostgreSQL session store requires a connection URL.\nSet memory.url in your score, or DATABASE_URL in your .env file."
|
|
1152
|
+
throw new ScoreValidationError(
|
|
1153
|
+
"PostgreSQL session store requires a connection URL.\nSet memory.url in your score, or DATABASE_URL in your .env file.",
|
|
1154
|
+
{ field: "memory.url" }
|
|
810
1155
|
);
|
|
811
1156
|
}
|
|
812
1157
|
return new PostgresSessionStore(url);
|
|
813
1158
|
}
|
|
814
|
-
throw new
|
|
1159
|
+
throw new ScoreValidationError(
|
|
815
1160
|
`Unsupported memory provider: "${memory.provider}".
|
|
816
|
-
Supported: "in-memory", "postgres"
|
|
1161
|
+
Supported: "in-memory", "postgres"`,
|
|
1162
|
+
{ field: "memory.provider", value: memory.provider }
|
|
817
1163
|
);
|
|
818
1164
|
}
|
|
819
1165
|
/** The score configuration this runtime was created with. */
|
|
@@ -827,12 +1173,7 @@ Supported: "in-memory", "postgres"`
|
|
|
827
1173
|
async run(agent_name, input, session_id) {
|
|
828
1174
|
const agent = this._score.agents[agent_name];
|
|
829
1175
|
if (!agent) {
|
|
830
|
-
|
|
831
|
-
throw new Error(
|
|
832
|
-
`Agent "${agent_name}" not found in your score.
|
|
833
|
-
Available agents: ${available}
|
|
834
|
-
Check your tutti.score.ts \u2014 the agent ID must match the key in the agents object.`
|
|
835
|
-
);
|
|
1176
|
+
throw new AgentNotFoundError(agent_name, Object.keys(this._score.agents));
|
|
836
1177
|
}
|
|
837
1178
|
const granted = agent.permissions ?? [];
|
|
838
1179
|
for (const voice of agent.voices) {
|
|
@@ -842,6 +1183,13 @@ Check your tutti.score.ts \u2014 the agent ID must match the key in the agents o
|
|
|
842
1183
|
const resolvedAgent = agent.model ? agent : { ...agent, model: this._score.default_model ?? "claude-sonnet-4-20250514" };
|
|
843
1184
|
return this._runner.run(resolvedAgent, input, session_id);
|
|
844
1185
|
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Provide an answer to a pending human-in-the-loop request.
|
|
1188
|
+
* Call this when a `hitl:requested` event fires to resume the agent.
|
|
1189
|
+
*/
|
|
1190
|
+
answer(sessionId, answer) {
|
|
1191
|
+
this._runner.answer(sessionId, answer);
|
|
1192
|
+
}
|
|
845
1193
|
/** Retrieve an existing session. */
|
|
846
1194
|
getSession(id) {
|
|
847
1195
|
return this._sessions.get(id);
|
|
@@ -849,7 +1197,7 @@ Check your tutti.score.ts \u2014 the agent ID must match the key in the agents o
|
|
|
849
1197
|
};
|
|
850
1198
|
|
|
851
1199
|
// src/agent-router.ts
|
|
852
|
-
import { z } from "zod";
|
|
1200
|
+
import { z as z2 } from "zod";
|
|
853
1201
|
var AgentRouter = class {
|
|
854
1202
|
constructor(_score) {
|
|
855
1203
|
this._score = _score;
|
|
@@ -925,9 +1273,9 @@ When the user's request matches a specialist's expertise, delegate to them with
|
|
|
925
1273
|
const runtime = () => this.runtime;
|
|
926
1274
|
const events = () => this.runtime.events;
|
|
927
1275
|
const entryName = score.agents[score.entry ?? "orchestrator"]?.name ?? "orchestrator";
|
|
928
|
-
const parameters =
|
|
929
|
-
agent_id:
|
|
930
|
-
task:
|
|
1276
|
+
const parameters = z2.object({
|
|
1277
|
+
agent_id: z2.enum(delegateIds).describe("Which specialist agent to delegate to"),
|
|
1278
|
+
task: z2.string().describe("The specific task description to pass to the specialist")
|
|
931
1279
|
});
|
|
932
1280
|
return {
|
|
933
1281
|
name: "delegate_to_agent",
|
|
@@ -968,49 +1316,51 @@ import { pathToFileURL } from "url";
|
|
|
968
1316
|
import { resolve } from "path";
|
|
969
1317
|
|
|
970
1318
|
// src/score-schema.ts
|
|
971
|
-
import { z as
|
|
972
|
-
var PermissionSchema =
|
|
973
|
-
var VoiceSchema =
|
|
974
|
-
name:
|
|
975
|
-
tools:
|
|
976
|
-
required_permissions:
|
|
1319
|
+
import { z as z3 } from "zod";
|
|
1320
|
+
var PermissionSchema = z3.enum(["network", "filesystem", "shell", "browser"]);
|
|
1321
|
+
var VoiceSchema = z3.object({
|
|
1322
|
+
name: z3.string().min(1, "Voice name cannot be empty"),
|
|
1323
|
+
tools: z3.array(z3.any()),
|
|
1324
|
+
required_permissions: z3.array(PermissionSchema)
|
|
977
1325
|
}).passthrough();
|
|
978
|
-
var BudgetSchema =
|
|
979
|
-
max_tokens:
|
|
980
|
-
max_cost_usd:
|
|
981
|
-
warn_at_percent:
|
|
1326
|
+
var BudgetSchema = z3.object({
|
|
1327
|
+
max_tokens: z3.number().positive().optional(),
|
|
1328
|
+
max_cost_usd: z3.number().positive().optional(),
|
|
1329
|
+
warn_at_percent: z3.number().min(1).max(100).optional()
|
|
982
1330
|
}).strict();
|
|
983
|
-
var AgentSchema =
|
|
984
|
-
name:
|
|
985
|
-
system_prompt:
|
|
986
|
-
voices:
|
|
987
|
-
model:
|
|
988
|
-
description:
|
|
989
|
-
permissions:
|
|
990
|
-
max_turns:
|
|
991
|
-
max_tool_calls:
|
|
992
|
-
tool_timeout_ms:
|
|
1331
|
+
var AgentSchema = z3.object({
|
|
1332
|
+
name: z3.string().min(1, "Agent name cannot be empty"),
|
|
1333
|
+
system_prompt: z3.string().min(1, "Agent system_prompt cannot be empty"),
|
|
1334
|
+
voices: z3.array(VoiceSchema),
|
|
1335
|
+
model: z3.string().optional(),
|
|
1336
|
+
description: z3.string().optional(),
|
|
1337
|
+
permissions: z3.array(PermissionSchema).optional(),
|
|
1338
|
+
max_turns: z3.number().int().positive("max_turns must be a positive number").optional(),
|
|
1339
|
+
max_tool_calls: z3.number().int().positive("max_tool_calls must be a positive number").optional(),
|
|
1340
|
+
tool_timeout_ms: z3.number().int().positive("tool_timeout_ms must be a positive number").optional(),
|
|
993
1341
|
budget: BudgetSchema.optional(),
|
|
994
|
-
|
|
995
|
-
|
|
1342
|
+
streaming: z3.boolean().optional(),
|
|
1343
|
+
allow_human_input: z3.boolean().optional(),
|
|
1344
|
+
delegates: z3.array(z3.string()).optional(),
|
|
1345
|
+
role: z3.enum(["orchestrator", "specialist"]).optional()
|
|
996
1346
|
}).passthrough();
|
|
997
|
-
var TelemetrySchema =
|
|
998
|
-
enabled:
|
|
999
|
-
endpoint:
|
|
1000
|
-
headers:
|
|
1347
|
+
var TelemetrySchema = z3.object({
|
|
1348
|
+
enabled: z3.boolean(),
|
|
1349
|
+
endpoint: z3.string().url("telemetry.endpoint must be a valid URL").optional(),
|
|
1350
|
+
headers: z3.record(z3.string(), z3.string()).optional()
|
|
1001
1351
|
}).strict();
|
|
1002
|
-
var ScoreSchema =
|
|
1003
|
-
provider:
|
|
1352
|
+
var ScoreSchema = z3.object({
|
|
1353
|
+
provider: z3.object({ chat: z3.function() }).passthrough().refine((p) => typeof p.chat === "function", {
|
|
1004
1354
|
message: "provider must have a chat() method \u2014 did you forget to pass a provider instance?"
|
|
1005
1355
|
}),
|
|
1006
|
-
agents:
|
|
1356
|
+
agents: z3.record(z3.string(), AgentSchema).refine(
|
|
1007
1357
|
(agents) => Object.keys(agents).length > 0,
|
|
1008
1358
|
{ message: "Score must define at least one agent" }
|
|
1009
1359
|
),
|
|
1010
|
-
name:
|
|
1011
|
-
description:
|
|
1012
|
-
default_model:
|
|
1013
|
-
entry:
|
|
1360
|
+
name: z3.string().optional(),
|
|
1361
|
+
description: z3.string().optional(),
|
|
1362
|
+
default_model: z3.string().optional(),
|
|
1363
|
+
entry: z3.string().optional(),
|
|
1014
1364
|
telemetry: TelemetrySchema.optional()
|
|
1015
1365
|
}).passthrough();
|
|
1016
1366
|
function validateScore(config) {
|
|
@@ -1020,7 +1370,7 @@ function validateScore(config) {
|
|
|
1020
1370
|
const path = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
1021
1371
|
return ` - ${path}: ${issue.message}`;
|
|
1022
1372
|
});
|
|
1023
|
-
throw new
|
|
1373
|
+
throw new ScoreValidationError(
|
|
1024
1374
|
"Invalid score file:\n" + issues.join("\n")
|
|
1025
1375
|
);
|
|
1026
1376
|
}
|
|
@@ -1030,18 +1380,20 @@ function validateScore(config) {
|
|
|
1030
1380
|
if (agent.delegates) {
|
|
1031
1381
|
for (const delegateId of agent.delegates) {
|
|
1032
1382
|
if (!agentKeys.includes(delegateId)) {
|
|
1033
|
-
throw new
|
|
1383
|
+
throw new ScoreValidationError(
|
|
1034
1384
|
`Invalid score file:
|
|
1035
|
-
- agents.${key}.delegates: references unknown agent "${delegateId}". Available: ${agentKeys.join(", ")}
|
|
1385
|
+
- agents.${key}.delegates: references unknown agent "${delegateId}". Available: ${agentKeys.join(", ")}`,
|
|
1386
|
+
{ field: `agents.${key}.delegates`, value: delegateId }
|
|
1036
1387
|
);
|
|
1037
1388
|
}
|
|
1038
1389
|
}
|
|
1039
1390
|
}
|
|
1040
1391
|
}
|
|
1041
1392
|
if (data.entry && !agentKeys.includes(data.entry)) {
|
|
1042
|
-
throw new
|
|
1393
|
+
throw new ScoreValidationError(
|
|
1043
1394
|
`Invalid score file:
|
|
1044
|
-
- entry: references unknown agent "${data.entry}". Available: ${agentKeys.join(", ")}
|
|
1395
|
+
- entry: references unknown agent "${data.entry}". Available: ${agentKeys.join(", ")}`,
|
|
1396
|
+
{ field: "entry", value: data.entry }
|
|
1045
1397
|
);
|
|
1046
1398
|
}
|
|
1047
1399
|
}
|
|
@@ -1084,8 +1436,9 @@ var AnthropicProvider = class {
|
|
|
1084
1436
|
}
|
|
1085
1437
|
async chat(request) {
|
|
1086
1438
|
if (!request.model) {
|
|
1087
|
-
throw new
|
|
1088
|
-
"AnthropicProvider requires a model on ChatRequest.\nSet model on the agent or default_model on the score."
|
|
1439
|
+
throw new ProviderError(
|
|
1440
|
+
"AnthropicProvider requires a model on ChatRequest.\nSet model on the agent or default_model on the score.",
|
|
1441
|
+
{ provider: "anthropic" }
|
|
1089
1442
|
);
|
|
1090
1443
|
}
|
|
1091
1444
|
let response;
|
|
@@ -1109,10 +1462,10 @@ var AnthropicProvider = class {
|
|
|
1109
1462
|
} catch (error) {
|
|
1110
1463
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1111
1464
|
logger.error({ error: msg, provider: "anthropic" }, "Provider request failed");
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
);
|
|
1465
|
+
if (msg.includes("authentication") || msg.includes("apiKey") || msg.includes("authToken")) {
|
|
1466
|
+
throw new AuthenticationError("anthropic");
|
|
1467
|
+
}
|
|
1468
|
+
throw new ProviderError(`Anthropic API error: ${msg}`, { provider: "anthropic" });
|
|
1116
1469
|
}
|
|
1117
1470
|
const content = response.content.map((block) => {
|
|
1118
1471
|
if (block.type === "text") {
|
|
@@ -1138,6 +1491,91 @@ Check that ANTHROPIC_API_KEY is set correctly in your .env file.`
|
|
|
1138
1491
|
}
|
|
1139
1492
|
};
|
|
1140
1493
|
}
|
|
1494
|
+
async *stream(request) {
|
|
1495
|
+
if (!request.model) {
|
|
1496
|
+
throw new ProviderError(
|
|
1497
|
+
"AnthropicProvider requires a model on ChatRequest.\nSet model on the agent or default_model on the score.",
|
|
1498
|
+
{ provider: "anthropic" }
|
|
1499
|
+
);
|
|
1500
|
+
}
|
|
1501
|
+
let raw;
|
|
1502
|
+
try {
|
|
1503
|
+
raw = await this.client.messages.create({
|
|
1504
|
+
model: request.model,
|
|
1505
|
+
max_tokens: request.max_tokens ?? 4096,
|
|
1506
|
+
system: request.system ?? "",
|
|
1507
|
+
messages: request.messages.map((msg) => ({
|
|
1508
|
+
role: msg.role,
|
|
1509
|
+
content: msg.content
|
|
1510
|
+
})),
|
|
1511
|
+
tools: request.tools?.map((tool) => ({
|
|
1512
|
+
name: tool.name,
|
|
1513
|
+
description: tool.description,
|
|
1514
|
+
input_schema: tool.input_schema
|
|
1515
|
+
})),
|
|
1516
|
+
...request.temperature != null && { temperature: request.temperature },
|
|
1517
|
+
...request.stop_sequences && { stop_sequences: request.stop_sequences },
|
|
1518
|
+
stream: true
|
|
1519
|
+
});
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1522
|
+
logger.error({ error: msg, provider: "anthropic" }, "Provider stream failed");
|
|
1523
|
+
if (msg.includes("authentication") || msg.includes("apiKey") || msg.includes("authToken")) {
|
|
1524
|
+
throw new AuthenticationError("anthropic");
|
|
1525
|
+
}
|
|
1526
|
+
throw new ProviderError(`Anthropic API error: ${msg}`, { provider: "anthropic" });
|
|
1527
|
+
}
|
|
1528
|
+
const toolBlocks = /* @__PURE__ */ new Map();
|
|
1529
|
+
let inputTokens = 0;
|
|
1530
|
+
let outputTokens = 0;
|
|
1531
|
+
let stopReason = "end_turn";
|
|
1532
|
+
for await (const event of raw) {
|
|
1533
|
+
if (event.type === "message_start") {
|
|
1534
|
+
inputTokens = event.message.usage.input_tokens;
|
|
1535
|
+
}
|
|
1536
|
+
if (event.type === "content_block_start") {
|
|
1537
|
+
if (event.content_block.type === "tool_use") {
|
|
1538
|
+
toolBlocks.set(event.index, {
|
|
1539
|
+
id: event.content_block.id,
|
|
1540
|
+
name: event.content_block.name,
|
|
1541
|
+
json: ""
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
if (event.type === "content_block_delta") {
|
|
1546
|
+
if (event.delta.type === "text_delta") {
|
|
1547
|
+
yield { type: "text", text: event.delta.text };
|
|
1548
|
+
}
|
|
1549
|
+
if (event.delta.type === "input_json_delta") {
|
|
1550
|
+
const block = toolBlocks.get(event.index);
|
|
1551
|
+
if (block) block.json += event.delta.partial_json;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
if (event.type === "content_block_stop") {
|
|
1555
|
+
const block = toolBlocks.get(event.index);
|
|
1556
|
+
if (block) {
|
|
1557
|
+
yield {
|
|
1558
|
+
type: "tool_use",
|
|
1559
|
+
tool: {
|
|
1560
|
+
id: block.id,
|
|
1561
|
+
name: block.name,
|
|
1562
|
+
input: block.json ? JSON.parse(block.json) : {}
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
toolBlocks.delete(event.index);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
if (event.type === "message_delta") {
|
|
1569
|
+
outputTokens = event.usage.output_tokens;
|
|
1570
|
+
stopReason = event.delta.stop_reason ?? "end_turn";
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
yield {
|
|
1574
|
+
type: "usage",
|
|
1575
|
+
usage: { input_tokens: inputTokens, output_tokens: outputTokens },
|
|
1576
|
+
stop_reason: stopReason
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1141
1579
|
};
|
|
1142
1580
|
|
|
1143
1581
|
// src/providers/openai.ts
|
|
@@ -1152,8 +1590,9 @@ var OpenAIProvider = class {
|
|
|
1152
1590
|
}
|
|
1153
1591
|
async chat(request) {
|
|
1154
1592
|
if (!request.model) {
|
|
1155
|
-
throw new
|
|
1156
|
-
"OpenAIProvider requires a model on ChatRequest.\nSet model on the agent or default_model on the score."
|
|
1593
|
+
throw new ProviderError(
|
|
1594
|
+
"OpenAIProvider requires a model on ChatRequest.\nSet model on the agent or default_model on the score.",
|
|
1595
|
+
{ provider: "openai" }
|
|
1157
1596
|
);
|
|
1158
1597
|
}
|
|
1159
1598
|
const messages = [];
|
|
@@ -1222,10 +1661,10 @@ var OpenAIProvider = class {
|
|
|
1222
1661
|
} catch (error) {
|
|
1223
1662
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1224
1663
|
logger.error({ error: msg, provider: "openai" }, "Provider request failed");
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
);
|
|
1664
|
+
if (msg.includes("Incorrect API key") || msg.includes("authentication")) {
|
|
1665
|
+
throw new AuthenticationError("openai");
|
|
1666
|
+
}
|
|
1667
|
+
throw new ProviderError(`OpenAI API error: ${msg}`, { provider: "openai" });
|
|
1229
1668
|
}
|
|
1230
1669
|
const choice = response.choices[0];
|
|
1231
1670
|
const content = [];
|
|
@@ -1266,6 +1705,113 @@ Check that OPENAI_API_KEY is set correctly in your .env file.`
|
|
|
1266
1705
|
}
|
|
1267
1706
|
};
|
|
1268
1707
|
}
|
|
1708
|
+
async *stream(request) {
|
|
1709
|
+
if (!request.model) {
|
|
1710
|
+
throw new ProviderError(
|
|
1711
|
+
"OpenAIProvider requires a model on ChatRequest.\nSet model on the agent or default_model on the score.",
|
|
1712
|
+
{ provider: "openai" }
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
const messages = [];
|
|
1716
|
+
if (request.system) {
|
|
1717
|
+
messages.push({ role: "system", content: request.system });
|
|
1718
|
+
}
|
|
1719
|
+
for (const msg of request.messages) {
|
|
1720
|
+
if (msg.role === "user") {
|
|
1721
|
+
if (typeof msg.content === "string") {
|
|
1722
|
+
messages.push({ role: "user", content: msg.content });
|
|
1723
|
+
} else {
|
|
1724
|
+
for (const block of msg.content) {
|
|
1725
|
+
if (block.type === "tool_result") {
|
|
1726
|
+
messages.push({ role: "tool", tool_call_id: block.tool_use_id, content: block.content });
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
} else if (msg.role === "assistant") {
|
|
1731
|
+
if (typeof msg.content === "string") {
|
|
1732
|
+
messages.push({ role: "assistant", content: msg.content });
|
|
1733
|
+
} else {
|
|
1734
|
+
const textParts = msg.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
|
|
1735
|
+
const toolCalls2 = msg.content.filter((b) => b.type === "tool_use").map((b) => {
|
|
1736
|
+
const block = b;
|
|
1737
|
+
return { id: block.id, type: "function", function: { name: block.name, arguments: JSON.stringify(block.input) } };
|
|
1738
|
+
});
|
|
1739
|
+
messages.push({ role: "assistant", content: textParts || null, ...toolCalls2.length > 0 && { tool_calls: toolCalls2 } });
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
const tools = request.tools?.map((tool) => ({
|
|
1744
|
+
type: "function",
|
|
1745
|
+
function: { name: tool.name, description: tool.description, parameters: tool.input_schema }
|
|
1746
|
+
}));
|
|
1747
|
+
let raw;
|
|
1748
|
+
try {
|
|
1749
|
+
raw = await this.client.chat.completions.create({
|
|
1750
|
+
model: request.model,
|
|
1751
|
+
messages,
|
|
1752
|
+
tools: tools && tools.length > 0 ? tools : void 0,
|
|
1753
|
+
max_tokens: request.max_tokens,
|
|
1754
|
+
temperature: request.temperature,
|
|
1755
|
+
stop: request.stop_sequences,
|
|
1756
|
+
stream: true,
|
|
1757
|
+
stream_options: { include_usage: true }
|
|
1758
|
+
});
|
|
1759
|
+
} catch (error) {
|
|
1760
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1761
|
+
logger.error({ error: msg, provider: "openai" }, "Provider stream failed");
|
|
1762
|
+
throw new Error(
|
|
1763
|
+
`OpenAI API error: ${msg}
|
|
1764
|
+
Check that OPENAI_API_KEY is set correctly in your .env file.`
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
const toolCalls = /* @__PURE__ */ new Map();
|
|
1768
|
+
let finishReason = "end_turn";
|
|
1769
|
+
for await (const chunk of raw) {
|
|
1770
|
+
const choice = chunk.choices[0];
|
|
1771
|
+
if (!choice) {
|
|
1772
|
+
if (chunk.usage) {
|
|
1773
|
+
yield {
|
|
1774
|
+
type: "usage",
|
|
1775
|
+
usage: { input_tokens: chunk.usage.prompt_tokens, output_tokens: chunk.usage.completion_tokens },
|
|
1776
|
+
stop_reason: finishReason
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
continue;
|
|
1780
|
+
}
|
|
1781
|
+
if (choice.delta.content) {
|
|
1782
|
+
yield { type: "text", text: choice.delta.content };
|
|
1783
|
+
}
|
|
1784
|
+
if (choice.delta.tool_calls) {
|
|
1785
|
+
for (const tc of choice.delta.tool_calls) {
|
|
1786
|
+
if (tc.id) {
|
|
1787
|
+
toolCalls.set(tc.index, { id: tc.id, name: tc.function?.name ?? "", args: "" });
|
|
1788
|
+
}
|
|
1789
|
+
const existing = toolCalls.get(tc.index);
|
|
1790
|
+
if (existing && tc.function?.arguments) {
|
|
1791
|
+
existing.args += tc.function.arguments;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
if (choice.finish_reason) {
|
|
1796
|
+
for (const tc of toolCalls.values()) {
|
|
1797
|
+
yield { type: "tool_use", tool: { id: tc.id, name: tc.name, input: JSON.parse(tc.args || "{}") } };
|
|
1798
|
+
}
|
|
1799
|
+
switch (choice.finish_reason) {
|
|
1800
|
+
case "tool_calls":
|
|
1801
|
+
finishReason = "tool_use";
|
|
1802
|
+
break;
|
|
1803
|
+
case "length":
|
|
1804
|
+
finishReason = "max_tokens";
|
|
1805
|
+
break;
|
|
1806
|
+
case "stop":
|
|
1807
|
+
finishReason = "end_turn";
|
|
1808
|
+
break;
|
|
1809
|
+
default:
|
|
1810
|
+
finishReason = "end_turn";
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1269
1815
|
};
|
|
1270
1816
|
|
|
1271
1817
|
// src/providers/gemini.ts
|
|
@@ -1278,9 +1824,7 @@ var GeminiProvider = class {
|
|
|
1278
1824
|
constructor(options = {}) {
|
|
1279
1825
|
const apiKey = options.api_key ?? SecretsManager.optional("GEMINI_API_KEY");
|
|
1280
1826
|
if (!apiKey) {
|
|
1281
|
-
throw new
|
|
1282
|
-
"GeminiProvider requires an API key.\nSet GEMINI_API_KEY in your .env file, or pass api_key to the constructor:\n new GeminiProvider({ api_key: 'your-key' })"
|
|
1283
|
-
);
|
|
1827
|
+
throw new AuthenticationError("gemini");
|
|
1284
1828
|
}
|
|
1285
1829
|
this.client = new GoogleGenerativeAI(apiKey);
|
|
1286
1830
|
}
|
|
@@ -1359,10 +1903,7 @@ var GeminiProvider = class {
|
|
|
1359
1903
|
} catch (error) {
|
|
1360
1904
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1361
1905
|
logger.error({ error: msg, provider: "gemini" }, "Provider request failed");
|
|
1362
|
-
throw new
|
|
1363
|
-
`Gemini API error: ${msg}
|
|
1364
|
-
Check that GEMINI_API_KEY is set correctly in your .env file.`
|
|
1365
|
-
);
|
|
1906
|
+
throw new ProviderError(`Gemini API error: ${msg}`, { provider: "gemini" });
|
|
1366
1907
|
}
|
|
1367
1908
|
const response = result.response;
|
|
1368
1909
|
const candidate = response.candidates?.[0];
|
|
@@ -1403,6 +1944,93 @@ Check that GEMINI_API_KEY is set correctly in your .env file.`
|
|
|
1403
1944
|
}
|
|
1404
1945
|
};
|
|
1405
1946
|
}
|
|
1947
|
+
async *stream(request) {
|
|
1948
|
+
const model = request.model ?? "gemini-2.0-flash";
|
|
1949
|
+
const tools = [];
|
|
1950
|
+
if (request.tools && request.tools.length > 0) {
|
|
1951
|
+
tools.push({
|
|
1952
|
+
functionDeclarations: request.tools.map((tool) => ({
|
|
1953
|
+
name: tool.name,
|
|
1954
|
+
description: tool.description,
|
|
1955
|
+
parameters: convertJsonSchemaToGemini(tool.input_schema)
|
|
1956
|
+
}))
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1959
|
+
const generativeModel = this.client.getGenerativeModel({
|
|
1960
|
+
model,
|
|
1961
|
+
systemInstruction: request.system,
|
|
1962
|
+
tools: tools.length > 0 ? tools : void 0
|
|
1963
|
+
});
|
|
1964
|
+
const contents = [];
|
|
1965
|
+
for (const msg of request.messages) {
|
|
1966
|
+
if (msg.role === "user") {
|
|
1967
|
+
if (typeof msg.content === "string") {
|
|
1968
|
+
contents.push({ role: "user", parts: [{ text: msg.content }] });
|
|
1969
|
+
} else {
|
|
1970
|
+
const parts = [];
|
|
1971
|
+
for (const block of msg.content) {
|
|
1972
|
+
if (block.type === "tool_result") {
|
|
1973
|
+
parts.push({ functionResponse: { name: block.tool_use_id, response: { content: block.content } } });
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
if (parts.length > 0) contents.push({ role: "user", parts });
|
|
1977
|
+
}
|
|
1978
|
+
} else if (msg.role === "assistant") {
|
|
1979
|
+
if (typeof msg.content === "string") {
|
|
1980
|
+
contents.push({ role: "model", parts: [{ text: msg.content }] });
|
|
1981
|
+
} else {
|
|
1982
|
+
const parts = [];
|
|
1983
|
+
for (const block of msg.content) {
|
|
1984
|
+
if (block.type === "text") parts.push({ text: block.text });
|
|
1985
|
+
else if (block.type === "tool_use") parts.push({ functionCall: { name: block.name, args: block.input } });
|
|
1986
|
+
}
|
|
1987
|
+
if (parts.length > 0) contents.push({ role: "model", parts });
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
let result;
|
|
1992
|
+
try {
|
|
1993
|
+
result = await generativeModel.generateContentStream({
|
|
1994
|
+
contents,
|
|
1995
|
+
generationConfig: {
|
|
1996
|
+
maxOutputTokens: request.max_tokens,
|
|
1997
|
+
temperature: request.temperature,
|
|
1998
|
+
stopSequences: request.stop_sequences
|
|
1999
|
+
}
|
|
2000
|
+
});
|
|
2001
|
+
} catch (error) {
|
|
2002
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2003
|
+
logger.error({ error: msg, provider: "gemini" }, "Provider stream failed");
|
|
2004
|
+
throw new Error(
|
|
2005
|
+
`Gemini API error: ${msg}
|
|
2006
|
+
Check that GEMINI_API_KEY is set correctly in your .env file.`
|
|
2007
|
+
);
|
|
2008
|
+
}
|
|
2009
|
+
let hasToolCalls = false;
|
|
2010
|
+
for await (const chunk of result.stream) {
|
|
2011
|
+
const candidate = chunk.candidates?.[0];
|
|
2012
|
+
if (!candidate) continue;
|
|
2013
|
+
for (const part of candidate.content.parts) {
|
|
2014
|
+
if ("text" in part && part.text) {
|
|
2015
|
+
yield { type: "text", text: part.text };
|
|
2016
|
+
}
|
|
2017
|
+
if ("functionCall" in part && part.functionCall) {
|
|
2018
|
+
hasToolCalls = true;
|
|
2019
|
+
yield {
|
|
2020
|
+
type: "tool_use",
|
|
2021
|
+
tool: { id: part.functionCall.name, name: part.functionCall.name, input: part.functionCall.args ?? {} }
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
const response = await result.response;
|
|
2027
|
+
const usage = response.usageMetadata;
|
|
2028
|
+
yield {
|
|
2029
|
+
type: "usage",
|
|
2030
|
+
usage: { input_tokens: usage?.promptTokenCount ?? 0, output_tokens: usage?.candidatesTokenCount ?? 0 },
|
|
2031
|
+
stop_reason: hasToolCalls ? "tool_use" : "end_turn"
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
1406
2034
|
};
|
|
1407
2035
|
function convertJsonSchemaToGemini(schema) {
|
|
1408
2036
|
const type = schema.type;
|
|
@@ -1415,23 +2043,40 @@ function convertJsonSchemaToGemini(schema) {
|
|
|
1415
2043
|
};
|
|
1416
2044
|
}
|
|
1417
2045
|
export {
|
|
2046
|
+
AgentNotFoundError,
|
|
1418
2047
|
AgentRouter,
|
|
1419
2048
|
AgentRunner,
|
|
1420
2049
|
AnthropicProvider,
|
|
2050
|
+
AuthenticationError,
|
|
2051
|
+
BudgetExceededError,
|
|
2052
|
+
ContextWindowError,
|
|
1421
2053
|
EventBus,
|
|
1422
2054
|
GeminiProvider,
|
|
1423
2055
|
InMemorySemanticStore,
|
|
1424
2056
|
InMemorySessionStore,
|
|
1425
2057
|
OpenAIProvider,
|
|
2058
|
+
PathTraversalError,
|
|
2059
|
+
PermissionError,
|
|
1426
2060
|
PermissionGuard,
|
|
1427
2061
|
PostgresSessionStore,
|
|
1428
2062
|
PromptGuard,
|
|
2063
|
+
ProviderError,
|
|
2064
|
+
RateLimitError,
|
|
1429
2065
|
ScoreLoader,
|
|
2066
|
+
ScoreValidationError,
|
|
1430
2067
|
SecretsManager,
|
|
1431
2068
|
TokenBudget,
|
|
2069
|
+
ToolTimeoutError,
|
|
2070
|
+
TuttiError,
|
|
1432
2071
|
TuttiRuntime,
|
|
1433
2072
|
TuttiTracer,
|
|
2073
|
+
UrlValidationError,
|
|
2074
|
+
VoiceError,
|
|
2075
|
+
createBlocklistHook,
|
|
2076
|
+
createCacheHook,
|
|
1434
2077
|
createLogger,
|
|
2078
|
+
createLoggingHook,
|
|
2079
|
+
createMaxCostHook,
|
|
1435
2080
|
defineScore,
|
|
1436
2081
|
initTelemetry,
|
|
1437
2082
|
logger,
|