@tuttiai/core 0.5.0 → 0.7.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
 
@@ -148,157 +252,181 @@ The session may have expired or the ID is incorrect.
148
252
  Omit session_id to start a new conversation.`
149
253
  );
150
254
  }
151
- this.events.emit({
152
- type: "agent:start",
153
- agent_name: agent.name,
154
- session_id: session.id
155
- });
156
- const allTools = agent.voices.flatMap((v) => v.tools);
157
- const toolDefs = allTools.map(toolToDefinition);
158
- const messages = [
159
- ...session.messages,
160
- { role: "user", content: input }
161
- ];
162
- const maxTurns = agent.max_turns ?? DEFAULT_MAX_TURNS;
163
- const maxToolCalls = agent.max_tool_calls ?? DEFAULT_MAX_TOOL_CALLS;
164
- const budget = agent.budget ? new TokenBudget(agent.budget, agent.model ?? "") : void 0;
165
- const totalUsage = { input_tokens: 0, output_tokens: 0 };
166
- let turns = 0;
167
- let totalToolCalls = 0;
168
- while (turns < maxTurns) {
169
- turns++;
255
+ return TuttiTracer.agentRun(agent.name, session.id, async () => {
256
+ logger.info({ agent: agent.name, session: session.id }, "Agent started");
170
257
  this.events.emit({
171
- type: "turn:start",
258
+ type: "agent:start",
172
259
  agent_name: agent.name,
173
- session_id: session.id,
174
- turn: turns
260
+ session_id: session.id
175
261
  });
176
- let systemPrompt = agent.system_prompt;
177
- const memCfg = agent.semantic_memory;
178
- if (memCfg?.enabled && this.semanticMemory) {
179
- const maxMemories = memCfg.max_memories ?? 5;
180
- const injectSystem = memCfg.inject_system !== false;
181
- if (injectSystem) {
182
- const memories = await this.semanticMemory.search(
183
- input,
184
- agent.name,
185
- maxMemories
186
- );
187
- if (memories.length > 0) {
188
- const memoryBlock = memories.map((m) => `- ${m.content}`).join("\n");
189
- systemPrompt += "\n\nRelevant context from previous sessions:\n" + memoryBlock;
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
+ }
190
298
  }
191
299
  }
192
- }
193
- const request = {
194
- model: agent.model,
195
- system: systemPrompt,
196
- messages,
197
- tools: toolDefs.length > 0 ? toolDefs : void 0
198
- };
199
- this.events.emit({
200
- type: "llm:request",
201
- agent_name: agent.name,
202
- request
203
- });
204
- const response = await this.provider.chat(request);
205
- this.events.emit({
206
- type: "llm:response",
207
- agent_name: agent.name,
208
- response
209
- });
210
- totalUsage.input_tokens += response.usage.input_tokens;
211
- totalUsage.output_tokens += response.usage.output_tokens;
212
- if (budget) {
213
- budget.add(response.usage.input_tokens, response.usage.output_tokens);
214
- const status = budget.check();
215
- if (status === "warning") {
216
- this.events.emit({
217
- type: "budget:warning",
218
- agent_name: agent.name,
219
- tokens: budget.total_tokens,
220
- cost_usd: budget.estimated_cost_usd
221
- });
222
- } else if (status === "exceeded") {
223
- this.events.emit({
224
- type: "budget:exceeded",
225
- agent_name: agent.name,
226
- tokens: budget.total_tokens,
227
- cost_usd: budget.estimated_cost_usd
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
+ () => agent.streaming ? this.streamToResponse(agent.name, request) : 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
+ }))
228
379
  });
229
- messages.push({ role: "assistant", content: response.content });
230
380
  break;
231
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 });
232
409
  }
233
- 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
+ );
234
417
  this.events.emit({
235
- type: "turn:end",
418
+ type: "agent:end",
236
419
  agent_name: agent.name,
237
- session_id: session.id,
238
- turn: turns
420
+ session_id: session.id
239
421
  });
240
- if (response.stop_reason !== "tool_use") {
241
- break;
242
- }
243
- const toolUseBlocks = response.content.filter(
244
- (b) => b.type === "tool_use"
245
- );
246
- totalToolCalls += toolUseBlocks.length;
247
- if (totalToolCalls > maxToolCalls) {
248
- messages.push({
249
- role: "user",
250
- content: toolUseBlocks.map((block) => ({
251
- type: "tool_result",
252
- tool_use_id: block.id,
253
- content: `Tool call rate limit exceeded: ${totalToolCalls} calls (max: ${maxToolCalls})`,
254
- is_error: true
255
- }))
256
- });
257
- break;
258
- }
259
- const toolTimeoutMs = agent.tool_timeout_ms ?? DEFAULT_TOOL_TIMEOUT_MS;
260
- const toolContext = {
422
+ return {
261
423
  session_id: session.id,
262
- agent_name: agent.name
424
+ output,
425
+ messages,
426
+ turns,
427
+ usage: totalUsage
263
428
  };
264
- if (memCfg?.enabled && this.semanticMemory) {
265
- const sm = this.semanticMemory;
266
- const agentName = agent.name;
267
- toolContext.memory = {
268
- remember: async (content, metadata = {}) => {
269
- await sm.add({ agent_name: agentName, content, metadata });
270
- },
271
- recall: async (query, limit) => {
272
- const entries = await sm.search(query, agentName, limit);
273
- return entries.map((e) => ({ id: e.id, content: e.content }));
274
- },
275
- forget: async (id) => {
276
- await sm.delete(id);
277
- }
278
- };
279
- }
280
- const toolResults = await Promise.all(
281
- toolUseBlocks.map(
282
- (block) => this.executeTool(allTools, block, toolContext, toolTimeoutMs)
283
- )
284
- );
285
- messages.push({ role: "user", content: toolResults });
286
- }
287
- this.sessions.update(session.id, messages);
288
- const lastAssistant = messages.filter((m) => m.role === "assistant").at(-1);
289
- const output = extractText(lastAssistant?.content);
290
- this.events.emit({
291
- type: "agent:end",
292
- agent_name: agent.name,
293
- session_id: session.id
294
429
  });
295
- return {
296
- session_id: session.id,
297
- output,
298
- messages,
299
- turns,
300
- usage: totalUsage
301
- };
302
430
  }
303
431
  async executeWithTimeout(fn, timeoutMs, toolName) {
304
432
  return Promise.race([
@@ -316,6 +444,38 @@ Increase tool_timeout_ms in your agent config, or check if the tool is hanging.`
316
444
  )
317
445
  ]);
318
446
  }
447
+ async streamToResponse(agentName, request) {
448
+ const content = [];
449
+ let textBuffer = "";
450
+ let usage = { input_tokens: 0, output_tokens: 0 };
451
+ let stopReason = "end_turn";
452
+ for await (const chunk of this.provider.stream(request)) {
453
+ if (chunk.type === "text" && chunk.text) {
454
+ textBuffer += chunk.text;
455
+ this.events.emit({
456
+ type: "token:stream",
457
+ agent_name: agentName,
458
+ text: chunk.text
459
+ });
460
+ }
461
+ if (chunk.type === "tool_use" && chunk.tool) {
462
+ content.push({
463
+ type: "tool_use",
464
+ id: chunk.tool.id,
465
+ name: chunk.tool.name,
466
+ input: chunk.tool.input
467
+ });
468
+ }
469
+ if (chunk.type === "usage") {
470
+ if (chunk.usage) usage = chunk.usage;
471
+ if (chunk.stop_reason) stopReason = chunk.stop_reason;
472
+ }
473
+ }
474
+ if (textBuffer) {
475
+ content.unshift({ type: "text", text: textBuffer });
476
+ }
477
+ return { id: "", content, stop_reason: stopReason, usage };
478
+ }
319
479
  async executeTool(tools, block, context, timeoutMs) {
320
480
  const tool = tools.find((t) => t.name === block.name);
321
481
  if (!tool) {
@@ -327,55 +487,64 @@ Increase tool_timeout_ms in your agent config, or check if the tool is hanging.`
327
487
  is_error: true
328
488
  };
329
489
  }
330
- this.events.emit({
331
- type: "tool:start",
332
- agent_name: context.agent_name,
333
- tool_name: block.name,
334
- input: block.input
335
- });
336
- try {
337
- const parsed = tool.parameters.parse(block.input);
338
- const result = await this.executeWithTimeout(
339
- () => tool.execute(parsed, context),
340
- timeoutMs,
341
- block.name
342
- );
490
+ return TuttiTracer.toolCall(block.name, async () => {
491
+ logger.debug({ tool: block.name, input: block.input }, "Tool called");
343
492
  this.events.emit({
344
- type: "tool:end",
493
+ type: "tool:start",
345
494
  agent_name: context.agent_name,
346
495
  tool_name: block.name,
347
- result
496
+ input: block.input
348
497
  });
349
- const scan = PromptGuard.scan(result.content);
350
- if (!scan.safe) {
498
+ try {
499
+ const parsed = tool.parameters.parse(block.input);
500
+ const result = await this.executeWithTimeout(
501
+ () => tool.execute(parsed, context),
502
+ timeoutMs,
503
+ block.name
504
+ );
505
+ logger.debug({ tool: block.name, result: result.content }, "Tool completed");
506
+ this.events.emit({
507
+ type: "tool:end",
508
+ agent_name: context.agent_name,
509
+ tool_name: block.name,
510
+ result
511
+ });
512
+ const scan = PromptGuard.scan(result.content);
513
+ if (!scan.safe) {
514
+ logger.warn(
515
+ { tool: block.name, patterns: scan.found },
516
+ "Potential prompt injection detected in tool output"
517
+ );
518
+ this.events.emit({
519
+ type: "security:injection_detected",
520
+ agent_name: context.agent_name,
521
+ tool_name: block.name,
522
+ patterns: scan.found
523
+ });
524
+ }
525
+ return {
526
+ type: "tool_result",
527
+ tool_use_id: block.id,
528
+ content: PromptGuard.wrap(block.name, result.content),
529
+ is_error: result.is_error
530
+ };
531
+ } catch (error) {
532
+ const message = error instanceof Error ? error.message : String(error);
533
+ logger.error({ error: message, tool: block.name }, "Tool failed");
351
534
  this.events.emit({
352
- type: "security:injection_detected",
535
+ type: "tool:error",
353
536
  agent_name: context.agent_name,
354
537
  tool_name: block.name,
355
- patterns: scan.found
538
+ error: error instanceof Error ? error : new Error(message)
356
539
  });
540
+ return {
541
+ type: "tool_result",
542
+ tool_use_id: block.id,
543
+ content: SecretsManager.redact(`Tool execution error: ${message}`),
544
+ is_error: true
545
+ };
357
546
  }
358
- return {
359
- type: "tool_result",
360
- tool_use_id: block.id,
361
- content: PromptGuard.wrap(block.name, result.content),
362
- is_error: result.is_error
363
- };
364
- } catch (error) {
365
- const message = error instanceof Error ? error.message : String(error);
366
- this.events.emit({
367
- type: "tool:error",
368
- agent_name: context.agent_name,
369
- tool_name: block.name,
370
- error: error instanceof Error ? error : new Error(message)
371
- });
372
- return {
373
- type: "tool_result",
374
- tool_use_id: block.id,
375
- content: SecretsManager.redact(`Tool execution error: ${message}`),
376
- is_error: true
377
- };
378
- }
547
+ });
379
548
  }
380
549
  };
381
550
  function toolToDefinition(tool) {
@@ -509,13 +678,14 @@ var PostgresSessionStore = class {
509
678
  session.updated_at
510
679
  ]
511
680
  ).catch((err) => {
512
- console.error(
513
- `[tutti] Failed to persist session ${session.id} to Postgres: ${err instanceof Error ? err.message : err}`
681
+ logger.error(
682
+ { error: err instanceof Error ? err.message : String(err), session: session.id },
683
+ "Failed to persist session to Postgres"
514
684
  );
515
685
  });
516
686
  return session;
517
687
  }
518
- get(id) {
688
+ get(_id) {
519
689
  return void 0;
520
690
  }
521
691
  /**
@@ -545,8 +715,9 @@ var PostgresSessionStore = class {
545
715
  WHERE id = $2`,
546
716
  [JSON.stringify(messages), id]
547
717
  ).catch((err) => {
548
- console.error(
549
- `[tutti] Failed to update session ${id} in Postgres: ${err instanceof Error ? err.message : err}`
718
+ logger.error(
719
+ { error: err instanceof Error ? err.message : String(err), session: id },
720
+ "Failed to update session in Postgres"
550
721
  );
551
722
  });
552
723
  }
@@ -616,8 +787,9 @@ var PermissionGuard = class {
616
787
  (p) => p === "shell" || p === "filesystem"
617
788
  );
618
789
  if (dangerous.length > 0) {
619
- console.warn(
620
- "[tutti] Warning: voice " + voice.name + " has elevated permissions: " + dangerous.join(", ")
790
+ logger.warn(
791
+ { voice: voice.name, permissions: dangerous },
792
+ "Voice has elevated permissions"
621
793
  );
622
794
  }
623
795
  }
@@ -641,6 +813,10 @@ var TuttiRuntime = class _TuttiRuntime {
641
813
  this._sessions,
642
814
  this.semanticMemory
643
815
  );
816
+ if (score.telemetry) {
817
+ initTelemetry(score.telemetry);
818
+ }
819
+ logger.info({ score: score.name, agents: Object.keys(score.agents) }, "Runtime initialized");
644
820
  }
645
821
  /**
646
822
  * Create a runtime with async initialization (required for Postgres).
@@ -847,9 +1023,15 @@ var AgentSchema = z2.object({
847
1023
  max_tool_calls: z2.number().int().positive("max_tool_calls must be a positive number").optional(),
848
1024
  tool_timeout_ms: z2.number().int().positive("tool_timeout_ms must be a positive number").optional(),
849
1025
  budget: BudgetSchema.optional(),
1026
+ streaming: z2.boolean().optional(),
850
1027
  delegates: z2.array(z2.string()).optional(),
851
1028
  role: z2.enum(["orchestrator", "specialist"]).optional()
852
1029
  }).passthrough();
1030
+ var TelemetrySchema = z2.object({
1031
+ enabled: z2.boolean(),
1032
+ endpoint: z2.string().url("telemetry.endpoint must be a valid URL").optional(),
1033
+ headers: z2.record(z2.string(), z2.string()).optional()
1034
+ }).strict();
853
1035
  var ScoreSchema = z2.object({
854
1036
  provider: z2.object({ chat: z2.function() }).passthrough().refine((p) => typeof p.chat === "function", {
855
1037
  message: "provider must have a chat() method \u2014 did you forget to pass a provider instance?"
@@ -861,7 +1043,8 @@ var ScoreSchema = z2.object({
861
1043
  name: z2.string().optional(),
862
1044
  description: z2.string().optional(),
863
1045
  default_model: z2.string().optional(),
864
- entry: z2.string().optional()
1046
+ entry: z2.string().optional(),
1047
+ telemetry: TelemetrySchema.optional()
865
1048
  }).passthrough();
866
1049
  function validateScore(config) {
867
1050
  const result = ScoreSchema.safeParse(config);
@@ -958,6 +1141,7 @@ var AnthropicProvider = class {
958
1141
  });
959
1142
  } catch (error) {
960
1143
  const msg = error instanceof Error ? error.message : String(error);
1144
+ logger.error({ error: msg, provider: "anthropic" }, "Provider request failed");
961
1145
  throw new Error(
962
1146
  `Anthropic API error: ${msg}
963
1147
  Check that ANTHROPIC_API_KEY is set correctly in your .env file.`
@@ -987,6 +1171,90 @@ Check that ANTHROPIC_API_KEY is set correctly in your .env file.`
987
1171
  }
988
1172
  };
989
1173
  }
1174
+ async *stream(request) {
1175
+ if (!request.model) {
1176
+ throw new Error(
1177
+ "AnthropicProvider requires a model on ChatRequest.\nSet model on the agent or default_model on the score."
1178
+ );
1179
+ }
1180
+ let raw;
1181
+ try {
1182
+ raw = await this.client.messages.create({
1183
+ model: request.model,
1184
+ max_tokens: request.max_tokens ?? 4096,
1185
+ system: request.system ?? "",
1186
+ messages: request.messages.map((msg) => ({
1187
+ role: msg.role,
1188
+ content: msg.content
1189
+ })),
1190
+ tools: request.tools?.map((tool) => ({
1191
+ name: tool.name,
1192
+ description: tool.description,
1193
+ input_schema: tool.input_schema
1194
+ })),
1195
+ ...request.temperature != null && { temperature: request.temperature },
1196
+ ...request.stop_sequences && { stop_sequences: request.stop_sequences },
1197
+ stream: true
1198
+ });
1199
+ } catch (error) {
1200
+ const msg = error instanceof Error ? error.message : String(error);
1201
+ logger.error({ error: msg, provider: "anthropic" }, "Provider stream failed");
1202
+ throw new Error(
1203
+ `Anthropic API error: ${msg}
1204
+ Check that ANTHROPIC_API_KEY is set correctly in your .env file.`
1205
+ );
1206
+ }
1207
+ const toolBlocks = /* @__PURE__ */ new Map();
1208
+ let inputTokens = 0;
1209
+ let outputTokens = 0;
1210
+ let stopReason = "end_turn";
1211
+ for await (const event of raw) {
1212
+ if (event.type === "message_start") {
1213
+ inputTokens = event.message.usage.input_tokens;
1214
+ }
1215
+ if (event.type === "content_block_start") {
1216
+ if (event.content_block.type === "tool_use") {
1217
+ toolBlocks.set(event.index, {
1218
+ id: event.content_block.id,
1219
+ name: event.content_block.name,
1220
+ json: ""
1221
+ });
1222
+ }
1223
+ }
1224
+ if (event.type === "content_block_delta") {
1225
+ if (event.delta.type === "text_delta") {
1226
+ yield { type: "text", text: event.delta.text };
1227
+ }
1228
+ if (event.delta.type === "input_json_delta") {
1229
+ const block = toolBlocks.get(event.index);
1230
+ if (block) block.json += event.delta.partial_json;
1231
+ }
1232
+ }
1233
+ if (event.type === "content_block_stop") {
1234
+ const block = toolBlocks.get(event.index);
1235
+ if (block) {
1236
+ yield {
1237
+ type: "tool_use",
1238
+ tool: {
1239
+ id: block.id,
1240
+ name: block.name,
1241
+ input: block.json ? JSON.parse(block.json) : {}
1242
+ }
1243
+ };
1244
+ toolBlocks.delete(event.index);
1245
+ }
1246
+ }
1247
+ if (event.type === "message_delta") {
1248
+ outputTokens = event.usage.output_tokens;
1249
+ stopReason = event.delta.stop_reason ?? "end_turn";
1250
+ }
1251
+ }
1252
+ yield {
1253
+ type: "usage",
1254
+ usage: { input_tokens: inputTokens, output_tokens: outputTokens },
1255
+ stop_reason: stopReason
1256
+ };
1257
+ }
990
1258
  };
991
1259
 
992
1260
  // src/providers/openai.ts
@@ -1070,6 +1338,7 @@ var OpenAIProvider = class {
1070
1338
  });
1071
1339
  } catch (error) {
1072
1340
  const msg = error instanceof Error ? error.message : String(error);
1341
+ logger.error({ error: msg, provider: "openai" }, "Provider request failed");
1073
1342
  throw new Error(
1074
1343
  `OpenAI API error: ${msg}
1075
1344
  Check that OPENAI_API_KEY is set correctly in your .env file.`
@@ -1114,6 +1383,112 @@ Check that OPENAI_API_KEY is set correctly in your .env file.`
1114
1383
  }
1115
1384
  };
1116
1385
  }
1386
+ async *stream(request) {
1387
+ if (!request.model) {
1388
+ throw new Error(
1389
+ "OpenAIProvider requires a model on ChatRequest.\nSet model on the agent or default_model on the score."
1390
+ );
1391
+ }
1392
+ const messages = [];
1393
+ if (request.system) {
1394
+ messages.push({ role: "system", content: request.system });
1395
+ }
1396
+ for (const msg of request.messages) {
1397
+ if (msg.role === "user") {
1398
+ if (typeof msg.content === "string") {
1399
+ messages.push({ role: "user", content: msg.content });
1400
+ } else {
1401
+ for (const block of msg.content) {
1402
+ if (block.type === "tool_result") {
1403
+ messages.push({ role: "tool", tool_call_id: block.tool_use_id, content: block.content });
1404
+ }
1405
+ }
1406
+ }
1407
+ } else if (msg.role === "assistant") {
1408
+ if (typeof msg.content === "string") {
1409
+ messages.push({ role: "assistant", content: msg.content });
1410
+ } else {
1411
+ const textParts = msg.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
1412
+ const toolCalls2 = msg.content.filter((b) => b.type === "tool_use").map((b) => {
1413
+ const block = b;
1414
+ return { id: block.id, type: "function", function: { name: block.name, arguments: JSON.stringify(block.input) } };
1415
+ });
1416
+ messages.push({ role: "assistant", content: textParts || null, ...toolCalls2.length > 0 && { tool_calls: toolCalls2 } });
1417
+ }
1418
+ }
1419
+ }
1420
+ const tools = request.tools?.map((tool) => ({
1421
+ type: "function",
1422
+ function: { name: tool.name, description: tool.description, parameters: tool.input_schema }
1423
+ }));
1424
+ let raw;
1425
+ try {
1426
+ raw = await this.client.chat.completions.create({
1427
+ model: request.model,
1428
+ messages,
1429
+ tools: tools && tools.length > 0 ? tools : void 0,
1430
+ max_tokens: request.max_tokens,
1431
+ temperature: request.temperature,
1432
+ stop: request.stop_sequences,
1433
+ stream: true,
1434
+ stream_options: { include_usage: true }
1435
+ });
1436
+ } catch (error) {
1437
+ const msg = error instanceof Error ? error.message : String(error);
1438
+ logger.error({ error: msg, provider: "openai" }, "Provider stream failed");
1439
+ throw new Error(
1440
+ `OpenAI API error: ${msg}
1441
+ Check that OPENAI_API_KEY is set correctly in your .env file.`
1442
+ );
1443
+ }
1444
+ const toolCalls = /* @__PURE__ */ new Map();
1445
+ let finishReason = "end_turn";
1446
+ for await (const chunk of raw) {
1447
+ const choice = chunk.choices[0];
1448
+ if (!choice) {
1449
+ if (chunk.usage) {
1450
+ yield {
1451
+ type: "usage",
1452
+ usage: { input_tokens: chunk.usage.prompt_tokens, output_tokens: chunk.usage.completion_tokens },
1453
+ stop_reason: finishReason
1454
+ };
1455
+ }
1456
+ continue;
1457
+ }
1458
+ if (choice.delta.content) {
1459
+ yield { type: "text", text: choice.delta.content };
1460
+ }
1461
+ if (choice.delta.tool_calls) {
1462
+ for (const tc of choice.delta.tool_calls) {
1463
+ if (tc.id) {
1464
+ toolCalls.set(tc.index, { id: tc.id, name: tc.function?.name ?? "", args: "" });
1465
+ }
1466
+ const existing = toolCalls.get(tc.index);
1467
+ if (existing && tc.function?.arguments) {
1468
+ existing.args += tc.function.arguments;
1469
+ }
1470
+ }
1471
+ }
1472
+ if (choice.finish_reason) {
1473
+ for (const tc of toolCalls.values()) {
1474
+ yield { type: "tool_use", tool: { id: tc.id, name: tc.name, input: JSON.parse(tc.args || "{}") } };
1475
+ }
1476
+ switch (choice.finish_reason) {
1477
+ case "tool_calls":
1478
+ finishReason = "tool_use";
1479
+ break;
1480
+ case "length":
1481
+ finishReason = "max_tokens";
1482
+ break;
1483
+ case "stop":
1484
+ finishReason = "end_turn";
1485
+ break;
1486
+ default:
1487
+ finishReason = "end_turn";
1488
+ }
1489
+ }
1490
+ }
1491
+ }
1117
1492
  };
1118
1493
 
1119
1494
  // src/providers/gemini.ts
@@ -1206,6 +1581,7 @@ var GeminiProvider = class {
1206
1581
  });
1207
1582
  } catch (error) {
1208
1583
  const msg = error instanceof Error ? error.message : String(error);
1584
+ logger.error({ error: msg, provider: "gemini" }, "Provider request failed");
1209
1585
  throw new Error(
1210
1586
  `Gemini API error: ${msg}
1211
1587
  Check that GEMINI_API_KEY is set correctly in your .env file.`
@@ -1250,6 +1626,93 @@ Check that GEMINI_API_KEY is set correctly in your .env file.`
1250
1626
  }
1251
1627
  };
1252
1628
  }
1629
+ async *stream(request) {
1630
+ const model = request.model ?? "gemini-2.0-flash";
1631
+ const tools = [];
1632
+ if (request.tools && request.tools.length > 0) {
1633
+ tools.push({
1634
+ functionDeclarations: request.tools.map((tool) => ({
1635
+ name: tool.name,
1636
+ description: tool.description,
1637
+ parameters: convertJsonSchemaToGemini(tool.input_schema)
1638
+ }))
1639
+ });
1640
+ }
1641
+ const generativeModel = this.client.getGenerativeModel({
1642
+ model,
1643
+ systemInstruction: request.system,
1644
+ tools: tools.length > 0 ? tools : void 0
1645
+ });
1646
+ const contents = [];
1647
+ for (const msg of request.messages) {
1648
+ if (msg.role === "user") {
1649
+ if (typeof msg.content === "string") {
1650
+ contents.push({ role: "user", parts: [{ text: msg.content }] });
1651
+ } else {
1652
+ const parts = [];
1653
+ for (const block of msg.content) {
1654
+ if (block.type === "tool_result") {
1655
+ parts.push({ functionResponse: { name: block.tool_use_id, response: { content: block.content } } });
1656
+ }
1657
+ }
1658
+ if (parts.length > 0) contents.push({ role: "user", parts });
1659
+ }
1660
+ } else if (msg.role === "assistant") {
1661
+ if (typeof msg.content === "string") {
1662
+ contents.push({ role: "model", parts: [{ text: msg.content }] });
1663
+ } else {
1664
+ const parts = [];
1665
+ for (const block of msg.content) {
1666
+ if (block.type === "text") parts.push({ text: block.text });
1667
+ else if (block.type === "tool_use") parts.push({ functionCall: { name: block.name, args: block.input } });
1668
+ }
1669
+ if (parts.length > 0) contents.push({ role: "model", parts });
1670
+ }
1671
+ }
1672
+ }
1673
+ let result;
1674
+ try {
1675
+ result = await generativeModel.generateContentStream({
1676
+ contents,
1677
+ generationConfig: {
1678
+ maxOutputTokens: request.max_tokens,
1679
+ temperature: request.temperature,
1680
+ stopSequences: request.stop_sequences
1681
+ }
1682
+ });
1683
+ } catch (error) {
1684
+ const msg = error instanceof Error ? error.message : String(error);
1685
+ logger.error({ error: msg, provider: "gemini" }, "Provider stream failed");
1686
+ throw new Error(
1687
+ `Gemini API error: ${msg}
1688
+ Check that GEMINI_API_KEY is set correctly in your .env file.`
1689
+ );
1690
+ }
1691
+ let hasToolCalls = false;
1692
+ for await (const chunk of result.stream) {
1693
+ const candidate = chunk.candidates?.[0];
1694
+ if (!candidate) continue;
1695
+ for (const part of candidate.content.parts) {
1696
+ if ("text" in part && part.text) {
1697
+ yield { type: "text", text: part.text };
1698
+ }
1699
+ if ("functionCall" in part && part.functionCall) {
1700
+ hasToolCalls = true;
1701
+ yield {
1702
+ type: "tool_use",
1703
+ tool: { id: part.functionCall.name, name: part.functionCall.name, input: part.functionCall.args ?? {} }
1704
+ };
1705
+ }
1706
+ }
1707
+ }
1708
+ const response = await result.response;
1709
+ const usage = response.usageMetadata;
1710
+ yield {
1711
+ type: "usage",
1712
+ usage: { input_tokens: usage?.promptTokenCount ?? 0, output_tokens: usage?.candidatesTokenCount ?? 0 },
1713
+ stop_reason: hasToolCalls ? "tool_use" : "end_turn"
1714
+ };
1715
+ }
1253
1716
  };
1254
1717
  function convertJsonSchemaToGemini(schema) {
1255
1718
  const type = schema.type;
@@ -1277,7 +1740,12 @@ export {
1277
1740
  SecretsManager,
1278
1741
  TokenBudget,
1279
1742
  TuttiRuntime,
1743
+ TuttiTracer,
1744
+ createLogger,
1280
1745
  defineScore,
1746
+ initTelemetry,
1747
+ logger,
1748
+ shutdownTelemetry,
1281
1749
  validateScore
1282
1750
  };
1283
1751
  //# sourceMappingURL=index.js.map