@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.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
- this.events.emit({
150
- type: "agent:start",
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: "turn:start",
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 request = {
175
- model: agent.model,
176
- system: agent.system_prompt,
177
- messages,
178
- tools: toolDefs.length > 0 ? toolDefs : void 0
179
- };
180
- this.events.emit({
181
- type: "llm:request",
182
- agent_name: agent.name,
183
- request
184
- });
185
- const response = await this.provider.chat(request);
186
- this.events.emit({
187
- type: "llm:response",
188
- agent_name: agent.name,
189
- response
190
- });
191
- totalUsage.input_tokens += response.usage.input_tokens;
192
- totalUsage.output_tokens += response.usage.output_tokens;
193
- if (budget) {
194
- budget.add(response.usage.input_tokens, response.usage.output_tokens);
195
- const status = budget.check();
196
- if (status === "warning") {
197
- this.events.emit({
198
- type: "budget:warning",
199
- agent_name: agent.name,
200
- tokens: budget.total_tokens,
201
- cost_usd: budget.estimated_cost_usd
202
- });
203
- } else if (status === "exceeded") {
204
- this.events.emit({
205
- type: "budget:exceeded",
206
- agent_name: agent.name,
207
- tokens: budget.total_tokens,
208
- cost_usd: budget.estimated_cost_usd
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
- messages.push({ role: "assistant", content: response.content });
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: "turn:end",
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
- if (response.stop_reason !== "tool_use") {
222
- break;
223
- }
224
- const toolUseBlocks = response.content.filter(
225
- (b) => b.type === "tool_use"
226
- );
227
- totalToolCalls += toolUseBlocks.length;
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
- this.events.emit({
295
- type: "tool:start",
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:end",
461
+ type: "tool:start",
309
462
  agent_name: context.agent_name,
310
463
  tool_name: block.name,
311
- result
464
+ input: block.input
312
465
  });
313
- const scan = PromptGuard.scan(result.content);
314
- if (!scan.safe) {
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: "security:injection_detected",
475
+ type: "tool:end",
317
476
  agent_name: context.agent_name,
318
477
  tool_name: block.name,
319
- patterns: scan.found
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
- return {
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
- console.warn(
451
- "[tutti] Warning: voice " + voice.name + " has elevated permissions: " + dangerous.join(", ")
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 = new InMemorySessionStore();
467
- this._runner = new AgentRunner(score.provider, this.events, this._sessions);
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