@tuttiai/core 0.4.0 → 0.6.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 +87 -4
- package/dist/index.js +519 -153
- package/dist/index.js.map +1 -1
- package/package.json +29 -4
package/dist/index.js
CHANGED
|
@@ -1,3 +1,107 @@
|
|
|
1
|
+
// src/logger.ts
|
|
2
|
+
import pino from "pino";
|
|
3
|
+
var createLogger = (name) => pino({
|
|
4
|
+
name,
|
|
5
|
+
level: process.env.TUTTI_LOG_LEVEL ?? "info",
|
|
6
|
+
transport: process.env.NODE_ENV === "production" ? void 0 : {
|
|
7
|
+
target: "pino-pretty",
|
|
8
|
+
options: {
|
|
9
|
+
colorize: true,
|
|
10
|
+
translateTime: "HH:MM:ss",
|
|
11
|
+
ignore: "pid,hostname"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
var logger = createLogger("tutti");
|
|
16
|
+
|
|
17
|
+
// src/telemetry.ts
|
|
18
|
+
import { trace, SpanStatusCode } from "@opentelemetry/api";
|
|
19
|
+
var tracer = trace.getTracer("tutti", "1.0.0");
|
|
20
|
+
var TuttiTracer = {
|
|
21
|
+
agentRun(agentName, sessionId, fn) {
|
|
22
|
+
return tracer.startActiveSpan("agent.run", async (span) => {
|
|
23
|
+
span.setAttribute("agent.name", agentName);
|
|
24
|
+
span.setAttribute("session.id", sessionId);
|
|
25
|
+
try {
|
|
26
|
+
const result = await fn();
|
|
27
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
28
|
+
return result;
|
|
29
|
+
} catch (err) {
|
|
30
|
+
span.setStatus({
|
|
31
|
+
code: SpanStatusCode.ERROR,
|
|
32
|
+
message: err instanceof Error ? err.message : String(err)
|
|
33
|
+
});
|
|
34
|
+
throw err;
|
|
35
|
+
} finally {
|
|
36
|
+
span.end();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
llmCall(model, fn) {
|
|
41
|
+
return tracer.startActiveSpan("llm.call", async (span) => {
|
|
42
|
+
span.setAttribute("llm.model", model);
|
|
43
|
+
try {
|
|
44
|
+
const result = await fn();
|
|
45
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
46
|
+
return result;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
span.setStatus({
|
|
49
|
+
code: SpanStatusCode.ERROR,
|
|
50
|
+
message: err instanceof Error ? err.message : String(err)
|
|
51
|
+
});
|
|
52
|
+
throw err;
|
|
53
|
+
} finally {
|
|
54
|
+
span.end();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
toolCall(toolName, fn) {
|
|
59
|
+
return tracer.startActiveSpan("tool.call", async (span) => {
|
|
60
|
+
span.setAttribute("tool.name", toolName);
|
|
61
|
+
try {
|
|
62
|
+
const result = await fn();
|
|
63
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
64
|
+
return result;
|
|
65
|
+
} catch (err) {
|
|
66
|
+
span.setStatus({
|
|
67
|
+
code: SpanStatusCode.ERROR,
|
|
68
|
+
message: err instanceof Error ? err.message : String(err)
|
|
69
|
+
});
|
|
70
|
+
throw err;
|
|
71
|
+
} finally {
|
|
72
|
+
span.end();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/telemetry-setup.ts
|
|
79
|
+
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
80
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
81
|
+
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
82
|
+
var sdk;
|
|
83
|
+
function initTelemetry(config) {
|
|
84
|
+
if (!config.enabled || sdk) return;
|
|
85
|
+
const endpoint = config.endpoint ?? "http://localhost:4318";
|
|
86
|
+
const exporter = new OTLPTraceExporter({
|
|
87
|
+
url: `${endpoint}/v1/traces`,
|
|
88
|
+
headers: config.headers
|
|
89
|
+
});
|
|
90
|
+
sdk = new NodeSDK({
|
|
91
|
+
traceExporter: exporter,
|
|
92
|
+
instrumentations: [getNodeAutoInstrumentations({ "@opentelemetry/instrumentation-fs": { enabled: false } })],
|
|
93
|
+
serviceName: process.env.OTEL_SERVICE_NAME ?? "tutti"
|
|
94
|
+
});
|
|
95
|
+
sdk.start();
|
|
96
|
+
logger.info({ endpoint }, "OpenTelemetry tracing enabled");
|
|
97
|
+
}
|
|
98
|
+
async function shutdownTelemetry() {
|
|
99
|
+
if (sdk) {
|
|
100
|
+
await sdk.shutdown();
|
|
101
|
+
sdk = void 0;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
1
105
|
// src/agent-runner.ts
|
|
2
106
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
3
107
|
|
|
@@ -129,14 +233,16 @@ var DEFAULT_MAX_TURNS = 10;
|
|
|
129
233
|
var DEFAULT_MAX_TOOL_CALLS = 20;
|
|
130
234
|
var DEFAULT_TOOL_TIMEOUT_MS = 3e4;
|
|
131
235
|
var AgentRunner = class {
|
|
132
|
-
constructor(provider, events, sessions) {
|
|
236
|
+
constructor(provider, events, sessions, semanticMemory) {
|
|
133
237
|
this.provider = provider;
|
|
134
238
|
this.events = events;
|
|
135
239
|
this.sessions = sessions;
|
|
240
|
+
this.semanticMemory = semanticMemory;
|
|
136
241
|
}
|
|
137
242
|
provider;
|
|
138
243
|
events;
|
|
139
244
|
sessions;
|
|
245
|
+
semanticMemory;
|
|
140
246
|
async run(agent, input, session_id) {
|
|
141
247
|
const session = session_id ? this.sessions.get(session_id) : this.sessions.create(agent.name);
|
|
142
248
|
if (!session) {
|
|
@@ -146,123 +252,181 @@ The session may have expired or the ID is incorrect.
|
|
|
146
252
|
Omit session_id to start a new conversation.`
|
|
147
253
|
);
|
|
148
254
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
agent_name: agent.name,
|
|
152
|
-
session_id: session.id
|
|
153
|
-
});
|
|
154
|
-
const allTools = agent.voices.flatMap((v) => v.tools);
|
|
155
|
-
const toolDefs = allTools.map(toolToDefinition);
|
|
156
|
-
const messages = [
|
|
157
|
-
...session.messages,
|
|
158
|
-
{ role: "user", content: input }
|
|
159
|
-
];
|
|
160
|
-
const maxTurns = agent.max_turns ?? DEFAULT_MAX_TURNS;
|
|
161
|
-
const maxToolCalls = agent.max_tool_calls ?? DEFAULT_MAX_TOOL_CALLS;
|
|
162
|
-
const budget = agent.budget ? new TokenBudget(agent.budget, agent.model ?? "") : void 0;
|
|
163
|
-
const totalUsage = { input_tokens: 0, output_tokens: 0 };
|
|
164
|
-
let turns = 0;
|
|
165
|
-
let totalToolCalls = 0;
|
|
166
|
-
while (turns < maxTurns) {
|
|
167
|
-
turns++;
|
|
255
|
+
return TuttiTracer.agentRun(agent.name, session.id, async () => {
|
|
256
|
+
logger.info({ agent: agent.name, session: session.id }, "Agent started");
|
|
168
257
|
this.events.emit({
|
|
169
|
-
type: "
|
|
258
|
+
type: "agent:start",
|
|
170
259
|
agent_name: agent.name,
|
|
171
|
-
session_id: session.id
|
|
172
|
-
turn: turns
|
|
260
|
+
session_id: session.id
|
|
173
261
|
});
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
messages,
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
262
|
+
const allTools = agent.voices.flatMap((v) => v.tools);
|
|
263
|
+
const toolDefs = allTools.map(toolToDefinition);
|
|
264
|
+
const messages = [
|
|
265
|
+
...session.messages,
|
|
266
|
+
{ role: "user", content: input }
|
|
267
|
+
];
|
|
268
|
+
const maxTurns = agent.max_turns ?? DEFAULT_MAX_TURNS;
|
|
269
|
+
const maxToolCalls = agent.max_tool_calls ?? DEFAULT_MAX_TOOL_CALLS;
|
|
270
|
+
const budget = agent.budget ? new TokenBudget(agent.budget, agent.model ?? "") : void 0;
|
|
271
|
+
const totalUsage = { input_tokens: 0, output_tokens: 0 };
|
|
272
|
+
let turns = 0;
|
|
273
|
+
let totalToolCalls = 0;
|
|
274
|
+
while (turns < maxTurns) {
|
|
275
|
+
turns++;
|
|
276
|
+
logger.info({ agent: agent.name, session: session.id, turn: turns }, "Turn started");
|
|
277
|
+
this.events.emit({
|
|
278
|
+
type: "turn:start",
|
|
279
|
+
agent_name: agent.name,
|
|
280
|
+
session_id: session.id,
|
|
281
|
+
turn: turns
|
|
282
|
+
});
|
|
283
|
+
let systemPrompt = agent.system_prompt;
|
|
284
|
+
const memCfg = agent.semantic_memory;
|
|
285
|
+
if (memCfg?.enabled && this.semanticMemory) {
|
|
286
|
+
const maxMemories = memCfg.max_memories ?? 5;
|
|
287
|
+
const injectSystem = memCfg.inject_system !== false;
|
|
288
|
+
if (injectSystem) {
|
|
289
|
+
const memories = await this.semanticMemory.search(
|
|
290
|
+
input,
|
|
291
|
+
agent.name,
|
|
292
|
+
maxMemories
|
|
293
|
+
);
|
|
294
|
+
if (memories.length > 0) {
|
|
295
|
+
const memoryBlock = memories.map((m) => `- ${m.content}`).join("\n");
|
|
296
|
+
systemPrompt += "\n\nRelevant context from previous sessions:\n" + memoryBlock;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const request = {
|
|
301
|
+
model: agent.model,
|
|
302
|
+
system: systemPrompt,
|
|
303
|
+
messages,
|
|
304
|
+
tools: toolDefs.length > 0 ? toolDefs : void 0
|
|
305
|
+
};
|
|
306
|
+
logger.debug({ agent: agent.name, model: agent.model }, "LLM request");
|
|
307
|
+
this.events.emit({
|
|
308
|
+
type: "llm:request",
|
|
309
|
+
agent_name: agent.name,
|
|
310
|
+
request
|
|
311
|
+
});
|
|
312
|
+
const response = await TuttiTracer.llmCall(
|
|
313
|
+
agent.model ?? "unknown",
|
|
314
|
+
() => this.provider.chat(request)
|
|
315
|
+
);
|
|
316
|
+
logger.debug(
|
|
317
|
+
{ agent: agent.name, stopReason: response.stop_reason, usage: response.usage },
|
|
318
|
+
"LLM response"
|
|
319
|
+
);
|
|
320
|
+
this.events.emit({
|
|
321
|
+
type: "llm:response",
|
|
322
|
+
agent_name: agent.name,
|
|
323
|
+
response
|
|
324
|
+
});
|
|
325
|
+
totalUsage.input_tokens += response.usage.input_tokens;
|
|
326
|
+
totalUsage.output_tokens += response.usage.output_tokens;
|
|
327
|
+
if (budget) {
|
|
328
|
+
budget.add(response.usage.input_tokens, response.usage.output_tokens);
|
|
329
|
+
const status = budget.check();
|
|
330
|
+
if (status === "warning") {
|
|
331
|
+
logger.warn(
|
|
332
|
+
{ agent: agent.name, tokens: budget.total_tokens, cost_usd: budget.estimated_cost_usd },
|
|
333
|
+
"Approaching token budget limit"
|
|
334
|
+
);
|
|
335
|
+
this.events.emit({
|
|
336
|
+
type: "budget:warning",
|
|
337
|
+
agent_name: agent.name,
|
|
338
|
+
tokens: budget.total_tokens,
|
|
339
|
+
cost_usd: budget.estimated_cost_usd
|
|
340
|
+
});
|
|
341
|
+
} else if (status === "exceeded") {
|
|
342
|
+
logger.warn(
|
|
343
|
+
{ agent: agent.name, tokens: budget.total_tokens, cost_usd: budget.estimated_cost_usd },
|
|
344
|
+
"Token budget exceeded"
|
|
345
|
+
);
|
|
346
|
+
this.events.emit({
|
|
347
|
+
type: "budget:exceeded",
|
|
348
|
+
agent_name: agent.name,
|
|
349
|
+
tokens: budget.total_tokens,
|
|
350
|
+
cost_usd: budget.estimated_cost_usd
|
|
351
|
+
});
|
|
352
|
+
messages.push({ role: "assistant", content: response.content });
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
messages.push({ role: "assistant", content: response.content });
|
|
357
|
+
this.events.emit({
|
|
358
|
+
type: "turn:end",
|
|
359
|
+
agent_name: agent.name,
|
|
360
|
+
session_id: session.id,
|
|
361
|
+
turn: turns
|
|
362
|
+
});
|
|
363
|
+
if (response.stop_reason !== "tool_use") {
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
const toolUseBlocks = response.content.filter(
|
|
367
|
+
(b) => b.type === "tool_use"
|
|
368
|
+
);
|
|
369
|
+
totalToolCalls += toolUseBlocks.length;
|
|
370
|
+
if (totalToolCalls > maxToolCalls) {
|
|
371
|
+
messages.push({
|
|
372
|
+
role: "user",
|
|
373
|
+
content: toolUseBlocks.map((block) => ({
|
|
374
|
+
type: "tool_result",
|
|
375
|
+
tool_use_id: block.id,
|
|
376
|
+
content: `Tool call rate limit exceeded: ${totalToolCalls} calls (max: ${maxToolCalls})`,
|
|
377
|
+
is_error: true
|
|
378
|
+
}))
|
|
209
379
|
});
|
|
210
|
-
messages.push({ role: "assistant", content: response.content });
|
|
211
380
|
break;
|
|
212
381
|
}
|
|
382
|
+
const toolTimeoutMs = agent.tool_timeout_ms ?? DEFAULT_TOOL_TIMEOUT_MS;
|
|
383
|
+
const toolContext = {
|
|
384
|
+
session_id: session.id,
|
|
385
|
+
agent_name: agent.name
|
|
386
|
+
};
|
|
387
|
+
if (memCfg?.enabled && this.semanticMemory) {
|
|
388
|
+
const sm = this.semanticMemory;
|
|
389
|
+
const agentName = agent.name;
|
|
390
|
+
toolContext.memory = {
|
|
391
|
+
remember: async (content, metadata = {}) => {
|
|
392
|
+
await sm.add({ agent_name: agentName, content, metadata });
|
|
393
|
+
},
|
|
394
|
+
recall: async (query, limit) => {
|
|
395
|
+
const entries = await sm.search(query, agentName, limit);
|
|
396
|
+
return entries.map((e) => ({ id: e.id, content: e.content }));
|
|
397
|
+
},
|
|
398
|
+
forget: async (id) => {
|
|
399
|
+
await sm.delete(id);
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
const toolResults = await Promise.all(
|
|
404
|
+
toolUseBlocks.map(
|
|
405
|
+
(block) => this.executeTool(allTools, block, toolContext, toolTimeoutMs)
|
|
406
|
+
)
|
|
407
|
+
);
|
|
408
|
+
messages.push({ role: "user", content: toolResults });
|
|
213
409
|
}
|
|
214
|
-
|
|
410
|
+
this.sessions.update(session.id, messages);
|
|
411
|
+
const lastAssistant = messages.filter((m) => m.role === "assistant").at(-1);
|
|
412
|
+
const output = extractText(lastAssistant?.content);
|
|
413
|
+
logger.info(
|
|
414
|
+
{ agent: agent.name, session: session.id, turns, usage: totalUsage },
|
|
415
|
+
"Agent finished"
|
|
416
|
+
);
|
|
215
417
|
this.events.emit({
|
|
216
|
-
type: "
|
|
418
|
+
type: "agent:end",
|
|
217
419
|
agent_name: agent.name,
|
|
218
|
-
session_id: session.id
|
|
219
|
-
turn: turns
|
|
420
|
+
session_id: session.id
|
|
220
421
|
});
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (totalToolCalls > maxToolCalls) {
|
|
229
|
-
messages.push({
|
|
230
|
-
role: "user",
|
|
231
|
-
content: toolUseBlocks.map((block) => ({
|
|
232
|
-
type: "tool_result",
|
|
233
|
-
tool_use_id: block.id,
|
|
234
|
-
content: `Tool call rate limit exceeded: ${totalToolCalls} calls (max: ${maxToolCalls})`,
|
|
235
|
-
is_error: true
|
|
236
|
-
}))
|
|
237
|
-
});
|
|
238
|
-
break;
|
|
239
|
-
}
|
|
240
|
-
const toolTimeoutMs = agent.tool_timeout_ms ?? DEFAULT_TOOL_TIMEOUT_MS;
|
|
241
|
-
const toolResults = await Promise.all(
|
|
242
|
-
toolUseBlocks.map(
|
|
243
|
-
(block) => this.executeTool(allTools, block, {
|
|
244
|
-
session_id: session.id,
|
|
245
|
-
agent_name: agent.name
|
|
246
|
-
}, toolTimeoutMs)
|
|
247
|
-
)
|
|
248
|
-
);
|
|
249
|
-
messages.push({ role: "user", content: toolResults });
|
|
250
|
-
}
|
|
251
|
-
this.sessions.update(session.id, messages);
|
|
252
|
-
const lastAssistant = messages.filter((m) => m.role === "assistant").at(-1);
|
|
253
|
-
const output = extractText(lastAssistant?.content);
|
|
254
|
-
this.events.emit({
|
|
255
|
-
type: "agent:end",
|
|
256
|
-
agent_name: agent.name,
|
|
257
|
-
session_id: session.id
|
|
422
|
+
return {
|
|
423
|
+
session_id: session.id,
|
|
424
|
+
output,
|
|
425
|
+
messages,
|
|
426
|
+
turns,
|
|
427
|
+
usage: totalUsage
|
|
428
|
+
};
|
|
258
429
|
});
|
|
259
|
-
return {
|
|
260
|
-
session_id: session.id,
|
|
261
|
-
output,
|
|
262
|
-
messages,
|
|
263
|
-
turns,
|
|
264
|
-
usage: totalUsage
|
|
265
|
-
};
|
|
266
430
|
}
|
|
267
431
|
async executeWithTimeout(fn, timeoutMs, toolName) {
|
|
268
432
|
return Promise.race([
|
|
@@ -291,55 +455,64 @@ Increase tool_timeout_ms in your agent config, or check if the tool is hanging.`
|
|
|
291
455
|
is_error: true
|
|
292
456
|
};
|
|
293
457
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
agent_name: context.agent_name,
|
|
297
|
-
tool_name: block.name,
|
|
298
|
-
input: block.input
|
|
299
|
-
});
|
|
300
|
-
try {
|
|
301
|
-
const parsed = tool.parameters.parse(block.input);
|
|
302
|
-
const result = await this.executeWithTimeout(
|
|
303
|
-
() => tool.execute(parsed, context),
|
|
304
|
-
timeoutMs,
|
|
305
|
-
block.name
|
|
306
|
-
);
|
|
458
|
+
return TuttiTracer.toolCall(block.name, async () => {
|
|
459
|
+
logger.debug({ tool: block.name, input: block.input }, "Tool called");
|
|
307
460
|
this.events.emit({
|
|
308
|
-
type: "tool:
|
|
461
|
+
type: "tool:start",
|
|
309
462
|
agent_name: context.agent_name,
|
|
310
463
|
tool_name: block.name,
|
|
311
|
-
|
|
464
|
+
input: block.input
|
|
312
465
|
});
|
|
313
|
-
|
|
314
|
-
|
|
466
|
+
try {
|
|
467
|
+
const parsed = tool.parameters.parse(block.input);
|
|
468
|
+
const result = await this.executeWithTimeout(
|
|
469
|
+
() => tool.execute(parsed, context),
|
|
470
|
+
timeoutMs,
|
|
471
|
+
block.name
|
|
472
|
+
);
|
|
473
|
+
logger.debug({ tool: block.name, result: result.content }, "Tool completed");
|
|
315
474
|
this.events.emit({
|
|
316
|
-
type: "
|
|
475
|
+
type: "tool:end",
|
|
317
476
|
agent_name: context.agent_name,
|
|
318
477
|
tool_name: block.name,
|
|
319
|
-
|
|
478
|
+
result
|
|
320
479
|
});
|
|
480
|
+
const scan = PromptGuard.scan(result.content);
|
|
481
|
+
if (!scan.safe) {
|
|
482
|
+
logger.warn(
|
|
483
|
+
{ tool: block.name, patterns: scan.found },
|
|
484
|
+
"Potential prompt injection detected in tool output"
|
|
485
|
+
);
|
|
486
|
+
this.events.emit({
|
|
487
|
+
type: "security:injection_detected",
|
|
488
|
+
agent_name: context.agent_name,
|
|
489
|
+
tool_name: block.name,
|
|
490
|
+
patterns: scan.found
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
type: "tool_result",
|
|
495
|
+
tool_use_id: block.id,
|
|
496
|
+
content: PromptGuard.wrap(block.name, result.content),
|
|
497
|
+
is_error: result.is_error
|
|
498
|
+
};
|
|
499
|
+
} catch (error) {
|
|
500
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
501
|
+
logger.error({ error: message, tool: block.name }, "Tool failed");
|
|
502
|
+
this.events.emit({
|
|
503
|
+
type: "tool:error",
|
|
504
|
+
agent_name: context.agent_name,
|
|
505
|
+
tool_name: block.name,
|
|
506
|
+
error: error instanceof Error ? error : new Error(message)
|
|
507
|
+
});
|
|
508
|
+
return {
|
|
509
|
+
type: "tool_result",
|
|
510
|
+
tool_use_id: block.id,
|
|
511
|
+
content: SecretsManager.redact(`Tool execution error: ${message}`),
|
|
512
|
+
is_error: true
|
|
513
|
+
};
|
|
321
514
|
}
|
|
322
|
-
|
|
323
|
-
type: "tool_result",
|
|
324
|
-
tool_use_id: block.id,
|
|
325
|
-
content: PromptGuard.wrap(block.name, result.content),
|
|
326
|
-
is_error: result.is_error
|
|
327
|
-
};
|
|
328
|
-
} catch (error) {
|
|
329
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
330
|
-
this.events.emit({
|
|
331
|
-
type: "tool:error",
|
|
332
|
-
agent_name: context.agent_name,
|
|
333
|
-
tool_name: block.name,
|
|
334
|
-
error: error instanceof Error ? error : new Error(message)
|
|
335
|
-
});
|
|
336
|
-
return {
|
|
337
|
-
type: "tool_result",
|
|
338
|
-
tool_use_id: block.id,
|
|
339
|
-
content: SecretsManager.redact(`Tool execution error: ${message}`),
|
|
340
|
-
is_error: true
|
|
341
|
-
};
|
|
342
|
-
}
|
|
515
|
+
});
|
|
343
516
|
}
|
|
344
517
|
};
|
|
345
518
|
function toolToDefinition(tool) {
|
|
@@ -430,6 +603,141 @@ var InMemorySessionStore = class {
|
|
|
430
603
|
}
|
|
431
604
|
};
|
|
432
605
|
|
|
606
|
+
// src/memory/postgres.ts
|
|
607
|
+
import pg from "pg";
|
|
608
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
609
|
+
var { Pool } = pg;
|
|
610
|
+
var PostgresSessionStore = class {
|
|
611
|
+
pool;
|
|
612
|
+
constructor(connectionString) {
|
|
613
|
+
this.pool = new Pool({ connectionString });
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Create the tutti_sessions table if it doesn't exist.
|
|
617
|
+
* Call this once before using the store.
|
|
618
|
+
*/
|
|
619
|
+
async initialize() {
|
|
620
|
+
await this.pool.query(`
|
|
621
|
+
CREATE TABLE IF NOT EXISTS tutti_sessions (
|
|
622
|
+
id TEXT PRIMARY KEY,
|
|
623
|
+
agent_name TEXT NOT NULL,
|
|
624
|
+
messages JSONB NOT NULL DEFAULT '[]',
|
|
625
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
626
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
627
|
+
)
|
|
628
|
+
`);
|
|
629
|
+
}
|
|
630
|
+
create(agent_name) {
|
|
631
|
+
const session = {
|
|
632
|
+
id: randomUUID2(),
|
|
633
|
+
agent_name,
|
|
634
|
+
messages: [],
|
|
635
|
+
created_at: /* @__PURE__ */ new Date(),
|
|
636
|
+
updated_at: /* @__PURE__ */ new Date()
|
|
637
|
+
};
|
|
638
|
+
this.pool.query(
|
|
639
|
+
`INSERT INTO tutti_sessions (id, agent_name, messages, created_at, updated_at)
|
|
640
|
+
VALUES ($1, $2, $3, $4, $5)`,
|
|
641
|
+
[
|
|
642
|
+
session.id,
|
|
643
|
+
session.agent_name,
|
|
644
|
+
JSON.stringify(session.messages),
|
|
645
|
+
session.created_at,
|
|
646
|
+
session.updated_at
|
|
647
|
+
]
|
|
648
|
+
).catch((err) => {
|
|
649
|
+
logger.error(
|
|
650
|
+
{ error: err instanceof Error ? err.message : String(err), session: session.id },
|
|
651
|
+
"Failed to persist session to Postgres"
|
|
652
|
+
);
|
|
653
|
+
});
|
|
654
|
+
return session;
|
|
655
|
+
}
|
|
656
|
+
get(_id) {
|
|
657
|
+
return void 0;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Async version of get() that queries Postgres directly.
|
|
661
|
+
* Use this when you need to load a session from the database.
|
|
662
|
+
*/
|
|
663
|
+
async getAsync(id) {
|
|
664
|
+
const result = await this.pool.query(
|
|
665
|
+
`SELECT id, agent_name, messages, created_at, updated_at
|
|
666
|
+
FROM tutti_sessions WHERE id = $1`,
|
|
667
|
+
[id]
|
|
668
|
+
);
|
|
669
|
+
if (result.rows.length === 0) return void 0;
|
|
670
|
+
const row = result.rows[0];
|
|
671
|
+
return {
|
|
672
|
+
id: row.id,
|
|
673
|
+
agent_name: row.agent_name,
|
|
674
|
+
messages: row.messages,
|
|
675
|
+
created_at: new Date(row.created_at),
|
|
676
|
+
updated_at: new Date(row.updated_at)
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
update(id, messages) {
|
|
680
|
+
this.pool.query(
|
|
681
|
+
`UPDATE tutti_sessions
|
|
682
|
+
SET messages = $1, updated_at = NOW()
|
|
683
|
+
WHERE id = $2`,
|
|
684
|
+
[JSON.stringify(messages), id]
|
|
685
|
+
).catch((err) => {
|
|
686
|
+
logger.error(
|
|
687
|
+
{ error: err instanceof Error ? err.message : String(err), session: id },
|
|
688
|
+
"Failed to update session in Postgres"
|
|
689
|
+
);
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
/** Close the connection pool. Call on shutdown. */
|
|
693
|
+
async close() {
|
|
694
|
+
await this.pool.end();
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
// src/memory/in-memory-semantic.ts
|
|
699
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
700
|
+
var InMemorySemanticStore = class {
|
|
701
|
+
entries = [];
|
|
702
|
+
async add(entry) {
|
|
703
|
+
const full = {
|
|
704
|
+
...entry,
|
|
705
|
+
id: randomUUID3(),
|
|
706
|
+
created_at: /* @__PURE__ */ new Date()
|
|
707
|
+
};
|
|
708
|
+
this.entries.push(full);
|
|
709
|
+
return full;
|
|
710
|
+
}
|
|
711
|
+
async search(query, agent_name, limit = 5) {
|
|
712
|
+
const queryTokens = tokenize(query);
|
|
713
|
+
if (queryTokens.size === 0) return [];
|
|
714
|
+
const agentEntries = this.entries.filter(
|
|
715
|
+
(e) => e.agent_name === agent_name
|
|
716
|
+
);
|
|
717
|
+
const scored = agentEntries.map((entry) => {
|
|
718
|
+
const entryTokens = tokenize(entry.content);
|
|
719
|
+
let overlap = 0;
|
|
720
|
+
for (const token of queryTokens) {
|
|
721
|
+
if (entryTokens.has(token)) overlap++;
|
|
722
|
+
}
|
|
723
|
+
const score = overlap / queryTokens.size;
|
|
724
|
+
return { entry, score };
|
|
725
|
+
});
|
|
726
|
+
return scored.filter((s) => s.score > 0).sort((a, b) => b.score - a.score).slice(0, limit).map((s) => s.entry);
|
|
727
|
+
}
|
|
728
|
+
async delete(id) {
|
|
729
|
+
this.entries = this.entries.filter((e) => e.id !== id);
|
|
730
|
+
}
|
|
731
|
+
async clear(agent_name) {
|
|
732
|
+
this.entries = this.entries.filter((e) => e.agent_name !== agent_name);
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
function tokenize(text) {
|
|
736
|
+
return new Set(
|
|
737
|
+
text.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length > 1)
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
|
|
433
741
|
// src/permission-guard.ts
|
|
434
742
|
var PermissionGuard = class {
|
|
435
743
|
static check(voice, granted) {
|
|
@@ -447,24 +755,66 @@ var PermissionGuard = class {
|
|
|
447
755
|
(p) => p === "shell" || p === "filesystem"
|
|
448
756
|
);
|
|
449
757
|
if (dangerous.length > 0) {
|
|
450
|
-
|
|
451
|
-
|
|
758
|
+
logger.warn(
|
|
759
|
+
{ voice: voice.name, permissions: dangerous },
|
|
760
|
+
"Voice has elevated permissions"
|
|
452
761
|
);
|
|
453
762
|
}
|
|
454
763
|
}
|
|
455
764
|
};
|
|
456
765
|
|
|
457
766
|
// src/runtime.ts
|
|
458
|
-
var TuttiRuntime = class {
|
|
767
|
+
var TuttiRuntime = class _TuttiRuntime {
|
|
459
768
|
events;
|
|
769
|
+
semanticMemory;
|
|
460
770
|
_sessions;
|
|
461
771
|
_runner;
|
|
462
772
|
_score;
|
|
463
773
|
constructor(score) {
|
|
464
774
|
this._score = score;
|
|
465
775
|
this.events = new EventBus();
|
|
466
|
-
this._sessions =
|
|
467
|
-
this.
|
|
776
|
+
this._sessions = _TuttiRuntime.createStore(score);
|
|
777
|
+
this.semanticMemory = new InMemorySemanticStore();
|
|
778
|
+
this._runner = new AgentRunner(
|
|
779
|
+
score.provider,
|
|
780
|
+
this.events,
|
|
781
|
+
this._sessions,
|
|
782
|
+
this.semanticMemory
|
|
783
|
+
);
|
|
784
|
+
if (score.telemetry) {
|
|
785
|
+
initTelemetry(score.telemetry);
|
|
786
|
+
}
|
|
787
|
+
logger.info({ score: score.name, agents: Object.keys(score.agents) }, "Runtime initialized");
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Create a runtime with async initialization (required for Postgres).
|
|
791
|
+
* Prefer this over `new TuttiRuntime()` when using a database-backed store.
|
|
792
|
+
*/
|
|
793
|
+
static async create(score) {
|
|
794
|
+
const runtime = new _TuttiRuntime(score);
|
|
795
|
+
if (runtime._sessions instanceof PostgresSessionStore) {
|
|
796
|
+
await runtime._sessions.initialize();
|
|
797
|
+
}
|
|
798
|
+
return runtime;
|
|
799
|
+
}
|
|
800
|
+
static createStore(score) {
|
|
801
|
+
const memory = score.memory;
|
|
802
|
+
if (!memory || memory.provider === "in-memory") {
|
|
803
|
+
return new InMemorySessionStore();
|
|
804
|
+
}
|
|
805
|
+
if (memory.provider === "postgres") {
|
|
806
|
+
const url = memory.url ?? process.env.DATABASE_URL;
|
|
807
|
+
if (!url) {
|
|
808
|
+
throw new Error(
|
|
809
|
+
"PostgreSQL session store requires a connection URL.\nSet memory.url in your score, or DATABASE_URL in your .env file."
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
return new PostgresSessionStore(url);
|
|
813
|
+
}
|
|
814
|
+
throw new Error(
|
|
815
|
+
`Unsupported memory provider: "${memory.provider}".
|
|
816
|
+
Supported: "in-memory", "postgres"`
|
|
817
|
+
);
|
|
468
818
|
}
|
|
469
819
|
/** The score configuration this runtime was created with. */
|
|
470
820
|
get score() {
|
|
@@ -644,6 +994,11 @@ var AgentSchema = z2.object({
|
|
|
644
994
|
delegates: z2.array(z2.string()).optional(),
|
|
645
995
|
role: z2.enum(["orchestrator", "specialist"]).optional()
|
|
646
996
|
}).passthrough();
|
|
997
|
+
var TelemetrySchema = z2.object({
|
|
998
|
+
enabled: z2.boolean(),
|
|
999
|
+
endpoint: z2.string().url("telemetry.endpoint must be a valid URL").optional(),
|
|
1000
|
+
headers: z2.record(z2.string(), z2.string()).optional()
|
|
1001
|
+
}).strict();
|
|
647
1002
|
var ScoreSchema = z2.object({
|
|
648
1003
|
provider: z2.object({ chat: z2.function() }).passthrough().refine((p) => typeof p.chat === "function", {
|
|
649
1004
|
message: "provider must have a chat() method \u2014 did you forget to pass a provider instance?"
|
|
@@ -655,7 +1010,8 @@ var ScoreSchema = z2.object({
|
|
|
655
1010
|
name: z2.string().optional(),
|
|
656
1011
|
description: z2.string().optional(),
|
|
657
1012
|
default_model: z2.string().optional(),
|
|
658
|
-
entry: z2.string().optional()
|
|
1013
|
+
entry: z2.string().optional(),
|
|
1014
|
+
telemetry: TelemetrySchema.optional()
|
|
659
1015
|
}).passthrough();
|
|
660
1016
|
function validateScore(config) {
|
|
661
1017
|
const result = ScoreSchema.safeParse(config);
|
|
@@ -752,6 +1108,7 @@ var AnthropicProvider = class {
|
|
|
752
1108
|
});
|
|
753
1109
|
} catch (error) {
|
|
754
1110
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1111
|
+
logger.error({ error: msg, provider: "anthropic" }, "Provider request failed");
|
|
755
1112
|
throw new Error(
|
|
756
1113
|
`Anthropic API error: ${msg}
|
|
757
1114
|
Check that ANTHROPIC_API_KEY is set correctly in your .env file.`
|
|
@@ -864,6 +1221,7 @@ var OpenAIProvider = class {
|
|
|
864
1221
|
});
|
|
865
1222
|
} catch (error) {
|
|
866
1223
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1224
|
+
logger.error({ error: msg, provider: "openai" }, "Provider request failed");
|
|
867
1225
|
throw new Error(
|
|
868
1226
|
`OpenAI API error: ${msg}
|
|
869
1227
|
Check that OPENAI_API_KEY is set correctly in your .env file.`
|
|
@@ -1000,6 +1358,7 @@ var GeminiProvider = class {
|
|
|
1000
1358
|
});
|
|
1001
1359
|
} catch (error) {
|
|
1002
1360
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1361
|
+
logger.error({ error: msg, provider: "gemini" }, "Provider request failed");
|
|
1003
1362
|
throw new Error(
|
|
1004
1363
|
`Gemini API error: ${msg}
|
|
1005
1364
|
Check that GEMINI_API_KEY is set correctly in your .env file.`
|
|
@@ -1061,15 +1420,22 @@ export {
|
|
|
1061
1420
|
AnthropicProvider,
|
|
1062
1421
|
EventBus,
|
|
1063
1422
|
GeminiProvider,
|
|
1423
|
+
InMemorySemanticStore,
|
|
1064
1424
|
InMemorySessionStore,
|
|
1065
1425
|
OpenAIProvider,
|
|
1066
1426
|
PermissionGuard,
|
|
1427
|
+
PostgresSessionStore,
|
|
1067
1428
|
PromptGuard,
|
|
1068
1429
|
ScoreLoader,
|
|
1069
1430
|
SecretsManager,
|
|
1070
1431
|
TokenBudget,
|
|
1071
1432
|
TuttiRuntime,
|
|
1433
|
+
TuttiTracer,
|
|
1434
|
+
createLogger,
|
|
1072
1435
|
defineScore,
|
|
1436
|
+
initTelemetry,
|
|
1437
|
+
logger,
|
|
1438
|
+
shutdownTelemetry,
|
|
1073
1439
|
validateScore
|
|
1074
1440
|
};
|
|
1075
1441
|
//# sourceMappingURL=index.js.map
|