@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.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 allTools = agent.voices.flatMap((v) => v.tools);
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
- const request = {
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
- () => this.provider.chat(request)
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
- return {
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 executeTool(tools, block, context, timeoutMs) {
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
- const result = await this.executeWithTimeout(
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
- async add(entry) {
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
- async search(query, agent_name, limit = 5) {
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 scored.filter((s) => s.score > 0).sort((a, b) => b.score - a.score).slice(0, limit).map((s) => s.entry);
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
- async delete(id) {
1071
+ delete(id) {
729
1072
  this.entries = this.entries.filter((e) => e.id !== id);
1073
+ return Promise.resolve();
730
1074
  }
731
- async clear(agent_name) {
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 Error(
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 Error(
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 Error(
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
- const available = Object.keys(this._score.agents).join(", ");
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 = z.object({
929
- agent_id: z.enum(delegateIds).describe("Which specialist agent to delegate to"),
930
- task: z.string().describe("The specific task description to pass to the specialist")
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 z2 } from "zod";
972
- var PermissionSchema = z2.enum(["network", "filesystem", "shell", "browser"]);
973
- var VoiceSchema = z2.object({
974
- name: z2.string().min(1, "Voice name cannot be empty"),
975
- tools: z2.array(z2.any()),
976
- required_permissions: z2.array(PermissionSchema)
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 = z2.object({
979
- max_tokens: z2.number().positive().optional(),
980
- max_cost_usd: z2.number().positive().optional(),
981
- warn_at_percent: z2.number().min(1).max(100).optional()
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 = z2.object({
984
- name: z2.string().min(1, "Agent name cannot be empty"),
985
- system_prompt: z2.string().min(1, "Agent system_prompt cannot be empty"),
986
- voices: z2.array(VoiceSchema),
987
- model: z2.string().optional(),
988
- description: z2.string().optional(),
989
- permissions: z2.array(PermissionSchema).optional(),
990
- max_turns: z2.number().int().positive("max_turns must be a positive number").optional(),
991
- max_tool_calls: z2.number().int().positive("max_tool_calls must be a positive number").optional(),
992
- tool_timeout_ms: z2.number().int().positive("tool_timeout_ms must be a positive number").optional(),
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
- delegates: z2.array(z2.string()).optional(),
995
- role: z2.enum(["orchestrator", "specialist"]).optional()
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 = 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()
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 = z2.object({
1003
- provider: z2.object({ chat: z2.function() }).passthrough().refine((p) => typeof p.chat === "function", {
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: z2.record(z2.string(), AgentSchema).refine(
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: z2.string().optional(),
1011
- description: z2.string().optional(),
1012
- default_model: z2.string().optional(),
1013
- entry: z2.string().optional(),
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 Error(
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 Error(
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 Error(
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 Error(
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
- throw new Error(
1113
- `Anthropic API error: ${msg}
1114
- Check that ANTHROPIC_API_KEY is set correctly in your .env file.`
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 Error(
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
- throw new Error(
1226
- `OpenAI API error: ${msg}
1227
- Check that OPENAI_API_KEY is set correctly in your .env file.`
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 Error(
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 Error(
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,