@tuttiai/core 0.7.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
- () => agent.streaming ? this.streamToResponse(agent.name, request) : 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,12 +696,7 @@ 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
  )
@@ -476,7 +734,42 @@ Increase tool_timeout_ms in your agent config, or check if the tool is hanging.`
476
734
  }
477
735
  return { id: "", content, stop_reason: stopReason, usage };
478
736
  }
479
- async executeTool(tools, block, context, timeoutMs) {
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) {
480
773
  const tool = tools.find((t) => t.name === block.name);
481
774
  if (!tool) {
482
775
  const available = tools.map((t) => t.name).join(", ") || "(none)";
@@ -488,6 +781,16 @@ Increase tool_timeout_ms in your agent config, or check if the tool is hanging.`
488
781
  };
489
782
  }
490
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
+ }
491
794
  logger.debug({ tool: block.name, input: block.input }, "Tool called");
492
795
  this.events.emit({
493
796
  type: "tool:start",
@@ -497,11 +800,17 @@ Increase tool_timeout_ms in your agent config, or check if the tool is hanging.`
497
800
  });
498
801
  try {
499
802
  const parsed = tool.parameters.parse(block.input);
500
- const result = await this.executeWithTimeout(
803
+ let result = await this.executeWithTimeout(
501
804
  () => tool.execute(parsed, context),
502
805
  timeoutMs,
503
806
  block.name
504
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
+ }
505
814
  logger.debug({ tool: block.name, result: result.content }, "Tool completed");
506
815
  this.events.emit({
507
816
  type: "tool:end",
@@ -731,18 +1040,18 @@ var PostgresSessionStore = class {
731
1040
  import { randomUUID as randomUUID3 } from "crypto";
732
1041
  var InMemorySemanticStore = class {
733
1042
  entries = [];
734
- async add(entry) {
1043
+ add(entry) {
735
1044
  const full = {
736
1045
  ...entry,
737
1046
  id: randomUUID3(),
738
1047
  created_at: /* @__PURE__ */ new Date()
739
1048
  };
740
1049
  this.entries.push(full);
741
- return full;
1050
+ return Promise.resolve(full);
742
1051
  }
743
- async search(query, agent_name, limit = 5) {
1052
+ search(query, agent_name, limit = 5) {
744
1053
  const queryTokens = tokenize(query);
745
- if (queryTokens.size === 0) return [];
1054
+ if (queryTokens.size === 0) return Promise.resolve([]);
746
1055
  const agentEntries = this.entries.filter(
747
1056
  (e) => e.agent_name === agent_name
748
1057
  );
@@ -755,13 +1064,17 @@ var InMemorySemanticStore = class {
755
1064
  const score = overlap / queryTokens.size;
756
1065
  return { entry, score };
757
1066
  });
758
- 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
+ );
759
1070
  }
760
- async delete(id) {
1071
+ delete(id) {
761
1072
  this.entries = this.entries.filter((e) => e.id !== id);
1073
+ return Promise.resolve();
762
1074
  }
763
- async clear(agent_name) {
1075
+ clear(agent_name) {
764
1076
  this.entries = this.entries.filter((e) => e.agent_name !== agent_name);
1077
+ return Promise.resolve();
765
1078
  }
766
1079
  };
767
1080
  function tokenize(text) {
@@ -777,9 +1090,7 @@ var PermissionGuard = class {
777
1090
  (p) => !granted.includes(p)
778
1091
  );
779
1092
  if (missing.length > 0) {
780
- throw new Error(
781
- "Voice " + voice.name + " requires permissions not granted: " + missing.join(", ") + "\n\nGrant them in your score file:\n permissions: [" + missing.map((p) => "'" + p + "'").join(", ") + "]"
782
- );
1093
+ throw new PermissionError(voice.name, voice.required_permissions, granted);
783
1094
  }
784
1095
  }
785
1096
  static warn(voice) {
@@ -811,7 +1122,8 @@ var TuttiRuntime = class _TuttiRuntime {
811
1122
  score.provider,
812
1123
  this.events,
813
1124
  this._sessions,
814
- this.semanticMemory
1125
+ this.semanticMemory,
1126
+ score.hooks
815
1127
  );
816
1128
  if (score.telemetry) {
817
1129
  initTelemetry(score.telemetry);
@@ -837,15 +1149,17 @@ var TuttiRuntime = class _TuttiRuntime {
837
1149
  if (memory.provider === "postgres") {
838
1150
  const url = memory.url ?? process.env.DATABASE_URL;
839
1151
  if (!url) {
840
- throw new Error(
841
- "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" }
842
1155
  );
843
1156
  }
844
1157
  return new PostgresSessionStore(url);
845
1158
  }
846
- throw new Error(
1159
+ throw new ScoreValidationError(
847
1160
  `Unsupported memory provider: "${memory.provider}".
848
- Supported: "in-memory", "postgres"`
1161
+ Supported: "in-memory", "postgres"`,
1162
+ { field: "memory.provider", value: memory.provider }
849
1163
  );
850
1164
  }
851
1165
  /** The score configuration this runtime was created with. */
@@ -859,12 +1173,7 @@ Supported: "in-memory", "postgres"`
859
1173
  async run(agent_name, input, session_id) {
860
1174
  const agent = this._score.agents[agent_name];
861
1175
  if (!agent) {
862
- const available = Object.keys(this._score.agents).join(", ");
863
- throw new Error(
864
- `Agent "${agent_name}" not found in your score.
865
- Available agents: ${available}
866
- Check your tutti.score.ts \u2014 the agent ID must match the key in the agents object.`
867
- );
1176
+ throw new AgentNotFoundError(agent_name, Object.keys(this._score.agents));
868
1177
  }
869
1178
  const granted = agent.permissions ?? [];
870
1179
  for (const voice of agent.voices) {
@@ -874,6 +1183,13 @@ Check your tutti.score.ts \u2014 the agent ID must match the key in the agents o
874
1183
  const resolvedAgent = agent.model ? agent : { ...agent, model: this._score.default_model ?? "claude-sonnet-4-20250514" };
875
1184
  return this._runner.run(resolvedAgent, input, session_id);
876
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
+ }
877
1193
  /** Retrieve an existing session. */
878
1194
  getSession(id) {
879
1195
  return this._sessions.get(id);
@@ -881,7 +1197,7 @@ Check your tutti.score.ts \u2014 the agent ID must match the key in the agents o
881
1197
  };
882
1198
 
883
1199
  // src/agent-router.ts
884
- import { z } from "zod";
1200
+ import { z as z2 } from "zod";
885
1201
  var AgentRouter = class {
886
1202
  constructor(_score) {
887
1203
  this._score = _score;
@@ -957,9 +1273,9 @@ When the user's request matches a specialist's expertise, delegate to them with
957
1273
  const runtime = () => this.runtime;
958
1274
  const events = () => this.runtime.events;
959
1275
  const entryName = score.agents[score.entry ?? "orchestrator"]?.name ?? "orchestrator";
960
- const parameters = z.object({
961
- agent_id: z.enum(delegateIds).describe("Which specialist agent to delegate to"),
962
- 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")
963
1279
  });
964
1280
  return {
965
1281
  name: "delegate_to_agent",
@@ -1000,50 +1316,51 @@ import { pathToFileURL } from "url";
1000
1316
  import { resolve } from "path";
1001
1317
 
1002
1318
  // src/score-schema.ts
1003
- import { z as z2 } from "zod";
1004
- var PermissionSchema = z2.enum(["network", "filesystem", "shell", "browser"]);
1005
- var VoiceSchema = z2.object({
1006
- name: z2.string().min(1, "Voice name cannot be empty"),
1007
- tools: z2.array(z2.any()),
1008
- 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)
1009
1325
  }).passthrough();
1010
- var BudgetSchema = z2.object({
1011
- max_tokens: z2.number().positive().optional(),
1012
- max_cost_usd: z2.number().positive().optional(),
1013
- 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()
1014
1330
  }).strict();
1015
- var AgentSchema = z2.object({
1016
- name: z2.string().min(1, "Agent name cannot be empty"),
1017
- system_prompt: z2.string().min(1, "Agent system_prompt cannot be empty"),
1018
- voices: z2.array(VoiceSchema),
1019
- model: z2.string().optional(),
1020
- description: z2.string().optional(),
1021
- permissions: z2.array(PermissionSchema).optional(),
1022
- max_turns: z2.number().int().positive("max_turns must be a positive number").optional(),
1023
- max_tool_calls: z2.number().int().positive("max_tool_calls must be a positive number").optional(),
1024
- 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(),
1025
1341
  budget: BudgetSchema.optional(),
1026
- streaming: z2.boolean().optional(),
1027
- delegates: z2.array(z2.string()).optional(),
1028
- 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()
1029
1346
  }).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()
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()
1034
1351
  }).strict();
1035
- var ScoreSchema = z2.object({
1036
- 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", {
1037
1354
  message: "provider must have a chat() method \u2014 did you forget to pass a provider instance?"
1038
1355
  }),
1039
- agents: z2.record(z2.string(), AgentSchema).refine(
1356
+ agents: z3.record(z3.string(), AgentSchema).refine(
1040
1357
  (agents) => Object.keys(agents).length > 0,
1041
1358
  { message: "Score must define at least one agent" }
1042
1359
  ),
1043
- name: z2.string().optional(),
1044
- description: z2.string().optional(),
1045
- default_model: z2.string().optional(),
1046
- 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(),
1047
1364
  telemetry: TelemetrySchema.optional()
1048
1365
  }).passthrough();
1049
1366
  function validateScore(config) {
@@ -1053,7 +1370,7 @@ function validateScore(config) {
1053
1370
  const path = issue.path.length > 0 ? issue.path.join(".") : "(root)";
1054
1371
  return ` - ${path}: ${issue.message}`;
1055
1372
  });
1056
- throw new Error(
1373
+ throw new ScoreValidationError(
1057
1374
  "Invalid score file:\n" + issues.join("\n")
1058
1375
  );
1059
1376
  }
@@ -1063,18 +1380,20 @@ function validateScore(config) {
1063
1380
  if (agent.delegates) {
1064
1381
  for (const delegateId of agent.delegates) {
1065
1382
  if (!agentKeys.includes(delegateId)) {
1066
- throw new Error(
1383
+ throw new ScoreValidationError(
1067
1384
  `Invalid score file:
1068
- - 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 }
1069
1387
  );
1070
1388
  }
1071
1389
  }
1072
1390
  }
1073
1391
  }
1074
1392
  if (data.entry && !agentKeys.includes(data.entry)) {
1075
- throw new Error(
1393
+ throw new ScoreValidationError(
1076
1394
  `Invalid score file:
1077
- - 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 }
1078
1397
  );
1079
1398
  }
1080
1399
  }
@@ -1117,8 +1436,9 @@ var AnthropicProvider = class {
1117
1436
  }
1118
1437
  async chat(request) {
1119
1438
  if (!request.model) {
1120
- throw new Error(
1121
- "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" }
1122
1442
  );
1123
1443
  }
1124
1444
  let response;
@@ -1142,10 +1462,10 @@ var AnthropicProvider = class {
1142
1462
  } catch (error) {
1143
1463
  const msg = error instanceof Error ? error.message : String(error);
1144
1464
  logger.error({ error: msg, provider: "anthropic" }, "Provider request failed");
1145
- throw new Error(
1146
- `Anthropic API error: ${msg}
1147
- Check that ANTHROPIC_API_KEY is set correctly in your .env file.`
1148
- );
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" });
1149
1469
  }
1150
1470
  const content = response.content.map((block) => {
1151
1471
  if (block.type === "text") {
@@ -1173,8 +1493,9 @@ Check that ANTHROPIC_API_KEY is set correctly in your .env file.`
1173
1493
  }
1174
1494
  async *stream(request) {
1175
1495
  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."
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" }
1178
1499
  );
1179
1500
  }
1180
1501
  let raw;
@@ -1199,10 +1520,10 @@ Check that ANTHROPIC_API_KEY is set correctly in your .env file.`
1199
1520
  } catch (error) {
1200
1521
  const msg = error instanceof Error ? error.message : String(error);
1201
1522
  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
- );
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" });
1206
1527
  }
1207
1528
  const toolBlocks = /* @__PURE__ */ new Map();
1208
1529
  let inputTokens = 0;
@@ -1269,8 +1590,9 @@ var OpenAIProvider = class {
1269
1590
  }
1270
1591
  async chat(request) {
1271
1592
  if (!request.model) {
1272
- throw new Error(
1273
- "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" }
1274
1596
  );
1275
1597
  }
1276
1598
  const messages = [];
@@ -1339,10 +1661,10 @@ var OpenAIProvider = class {
1339
1661
  } catch (error) {
1340
1662
  const msg = error instanceof Error ? error.message : String(error);
1341
1663
  logger.error({ error: msg, provider: "openai" }, "Provider request failed");
1342
- throw new Error(
1343
- `OpenAI API error: ${msg}
1344
- Check that OPENAI_API_KEY is set correctly in your .env file.`
1345
- );
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" });
1346
1668
  }
1347
1669
  const choice = response.choices[0];
1348
1670
  const content = [];
@@ -1385,8 +1707,9 @@ Check that OPENAI_API_KEY is set correctly in your .env file.`
1385
1707
  }
1386
1708
  async *stream(request) {
1387
1709
  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."
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" }
1390
1713
  );
1391
1714
  }
1392
1715
  const messages = [];
@@ -1501,9 +1824,7 @@ var GeminiProvider = class {
1501
1824
  constructor(options = {}) {
1502
1825
  const apiKey = options.api_key ?? SecretsManager.optional("GEMINI_API_KEY");
1503
1826
  if (!apiKey) {
1504
- throw new Error(
1505
- "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' })"
1506
- );
1827
+ throw new AuthenticationError("gemini");
1507
1828
  }
1508
1829
  this.client = new GoogleGenerativeAI(apiKey);
1509
1830
  }
@@ -1582,10 +1903,7 @@ var GeminiProvider = class {
1582
1903
  } catch (error) {
1583
1904
  const msg = error instanceof Error ? error.message : String(error);
1584
1905
  logger.error({ error: msg, provider: "gemini" }, "Provider request failed");
1585
- throw new Error(
1586
- `Gemini API error: ${msg}
1587
- Check that GEMINI_API_KEY is set correctly in your .env file.`
1588
- );
1906
+ throw new ProviderError(`Gemini API error: ${msg}`, { provider: "gemini" });
1589
1907
  }
1590
1908
  const response = result.response;
1591
1909
  const candidate = response.candidates?.[0];
@@ -1725,23 +2043,40 @@ function convertJsonSchemaToGemini(schema) {
1725
2043
  };
1726
2044
  }
1727
2045
  export {
2046
+ AgentNotFoundError,
1728
2047
  AgentRouter,
1729
2048
  AgentRunner,
1730
2049
  AnthropicProvider,
2050
+ AuthenticationError,
2051
+ BudgetExceededError,
2052
+ ContextWindowError,
1731
2053
  EventBus,
1732
2054
  GeminiProvider,
1733
2055
  InMemorySemanticStore,
1734
2056
  InMemorySessionStore,
1735
2057
  OpenAIProvider,
2058
+ PathTraversalError,
2059
+ PermissionError,
1736
2060
  PermissionGuard,
1737
2061
  PostgresSessionStore,
1738
2062
  PromptGuard,
2063
+ ProviderError,
2064
+ RateLimitError,
1739
2065
  ScoreLoader,
2066
+ ScoreValidationError,
1740
2067
  SecretsManager,
1741
2068
  TokenBudget,
2069
+ ToolTimeoutError,
2070
+ TuttiError,
1742
2071
  TuttiRuntime,
1743
2072
  TuttiTracer,
2073
+ UrlValidationError,
2074
+ VoiceError,
2075
+ createBlocklistHook,
2076
+ createCacheHook,
1744
2077
  createLogger,
2078
+ createLoggingHook,
2079
+ createMaxCostHook,
1745
2080
  defineScore,
1746
2081
  initTelemetry,
1747
2082
  logger,