@voicelayer/sdk 0.1.11 → 0.3.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.
@@ -1,5 +1,9 @@
1
1
  import { z } from 'zod';
2
2
  import OpenAI from 'openai';
3
+ import { AsyncLocalStorage } from 'async_hooks';
4
+ import 'dns';
5
+ import 'net';
6
+ import '@opentelemetry/api';
3
7
  import 'quickjs-emscripten';
4
8
  import { setup, createActor } from 'xstate';
5
9
  import { randomUUID } from 'crypto';
@@ -14,6 +18,18 @@ var __export = (target, all) => {
14
18
  __defProp(target, name, { get: all[name], enumerable: true });
15
19
  };
16
20
 
21
+ // src/runtime/graph/log.ts
22
+ function graphLog(event, fields) {
23
+ console.log(`[graph] ${event}`, { ...fields, t: Date.now() });
24
+ }
25
+ function graphWarn(event, fields) {
26
+ console.warn(`[graph] ${event}`, { ...fields, t: Date.now() });
27
+ }
28
+ var init_log = __esm({
29
+ "src/runtime/graph/log.ts"() {
30
+ }
31
+ });
32
+
17
33
  // src/runtime/graph/graph-model.ts
18
34
  function buildGraphModel(program) {
19
35
  return {
@@ -43,11 +59,100 @@ var init_graph_model = __esm({
43
59
  }
44
60
  });
45
61
 
62
+ // ../contracts/src/effect-stack.ts
63
+ var EffectStack;
64
+ var init_effect_stack = __esm({
65
+ "../contracts/src/effect-stack.ts"() {
66
+ EffectStack = class {
67
+ #disposers = [];
68
+ #disposed = false;
69
+ #draining = false;
70
+ #drain = null;
71
+ #onError;
72
+ constructor(options = {}) {
73
+ this.#onError = options.onDisposeError ?? ((error) => {
74
+ console.error("[EffectStack] disposer threw during teardown", error);
75
+ });
76
+ }
77
+ /**
78
+ * Register an acquire's inverse. Returns a handle that reverts THIS effect
79
+ * early (at most once). The handle is a no-op after it has fired and after
80
+ * dispose() has already drained the disposer.
81
+ */
82
+ defer(disposer) {
83
+ if (this.#disposed && !this.#draining) {
84
+ const ran = this.#run(disposer);
85
+ return async () => {
86
+ await ran;
87
+ };
88
+ }
89
+ this.#disposers.push(disposer);
90
+ let fired = false;
91
+ return async () => {
92
+ if (fired) return;
93
+ fired = true;
94
+ const i = this.#disposers.lastIndexOf(disposer);
95
+ if (i < 0) return;
96
+ this.#disposers.splice(i, 1);
97
+ await this.#run(disposer);
98
+ };
99
+ }
100
+ /** Number of pending disposers. */
101
+ get size() {
102
+ return this.#disposers.length;
103
+ }
104
+ get disposed() {
105
+ return this.#disposed;
106
+ }
107
+ /**
108
+ * Run every pending disposer in LIFO order, each awaited before the next.
109
+ * Idempotent AND concurrency-safe (contract 3): overlapping calls all await
110
+ * the one in-flight drain. A disposer that registers another disposer during
111
+ * teardown is drained too, in LIFO position.
112
+ */
113
+ async dispose() {
114
+ return this.#drain ??= this.#runDrain();
115
+ }
116
+ async #runDrain() {
117
+ this.#disposed = true;
118
+ this.#draining = true;
119
+ try {
120
+ for (let next = this.#disposers.pop(); next !== void 0; next = this.#disposers.pop()) {
121
+ await this.#run(next);
122
+ }
123
+ } finally {
124
+ this.#draining = false;
125
+ }
126
+ }
127
+ /** Enables `await using stack = new EffectStack()` on hosts that support it. */
128
+ async [Symbol.asyncDispose]() {
129
+ await this.dispose();
130
+ }
131
+ async #run(disposer) {
132
+ try {
133
+ await disposer();
134
+ } catch (error) {
135
+ this.#onError(error);
136
+ }
137
+ }
138
+ };
139
+ }
140
+ });
141
+
46
142
  // ../contracts/src/primitive.ts
47
143
  var BasePrimitive;
48
144
  var init_primitive = __esm({
49
145
  "../contracts/src/primitive.ts"() {
146
+ init_effect_stack();
50
147
  BasePrimitive = class {
148
+ /**
149
+ * Instance-lifetime effects. Subclasses that acquire a connection, queue, or
150
+ * timer in their constructor register its inverse here via
151
+ * `this.effects.defer(...)`; `[Symbol.asyncDispose]()` replays them LIFO. This
152
+ * is the structural fix for "a primitive that opens a resource cannot close
153
+ * it" (GROUNDING F1) — per-call resources still use onCallEnd.
154
+ */
155
+ effects = new EffectStack();
51
156
  async onError(err, _ctx) {
52
157
  return {
53
158
  kind: "fail",
@@ -81,6 +186,10 @@ var init_primitive = __esm({
81
186
  return () => {
82
187
  };
83
188
  }
189
+ /** Release instance-lifetime resources. Idempotent. */
190
+ async [Symbol.asyncDispose]() {
191
+ await this.effects.dispose();
192
+ }
84
193
  };
85
194
  }
86
195
  });
@@ -255,7 +364,13 @@ var init_flow_program = __esm({
255
364
  droppedEdges: z.array(z.string().max(128)).max(256),
256
365
  // Node ids needing external plumbing not wired yet (rag/classify/code) — the
257
366
  // interpreter passes through them.
258
- unsupportedNodes: z.array(z.string().max(128)).max(256)
367
+ unsupportedNodes: z.array(z.string().max(128)).max(256),
368
+ // Port-less edges out of a conversational node (llm/ask/playbook) that carry
369
+ // a display `label` but no `condition` — compiled as the UNCONDITIONAL
370
+ // default (the label never routes). Mirrors validateFlowGraph's
371
+ // `edge_label_not_condition` warning. Optional: programs stored before this
372
+ // field existed must still pass the read-side parse.
373
+ labelOnlyEdges: z.array(z.string().max(128)).max(256).optional()
259
374
  });
260
375
  FlowProgram = z.object({
261
376
  v: z.literal(1),
@@ -372,6 +487,16 @@ var init_agents = __esm({
372
487
  readiness: z.array(ReadinessReport).max(64).optional(),
373
488
  metadata: AgentMetadata.optional(),
374
489
  processSchema: ProcessSchemaDTO.nullable().optional(),
490
+ // A code-first agent's base prompt (its instructions live in code, not the DB).
491
+ // Synced here so a generic worker can run it over text (P2). Omit for flow agents.
492
+ // Cap matches AgentConfig.systemPrompt (64k) so a large-but-valid prompt doesn't
493
+ // fail registration outright (which agent.ts swallows → agent runs but never
494
+ // registers/heartbeats).
495
+ prompt: z.string().max(64e3).nullable().optional(),
496
+ // The text-turn protocol this worker speaks (burn-down G-4): it registers `<name>::text` and runs text turns as the
497
+ // agent. Absent = it can't (an older SDK, a puppet, a flow pool) — stored as null, so a downgrade stops text turns.
498
+ // Any version is accepted (a newer SDK never fails registration); the API decides which versions it dispatches to.
499
+ textTurns: z.number().int().min(1).max(1e3).optional(),
375
500
  // Opt into MCP exposure. When true the API upserts a matching agent_config
376
501
  // row keyed by (project_id, name, environment) so MCP / dashboard / REST
377
502
  // can list and inspect this agent's identity. When false / omitted, the
@@ -391,6 +516,13 @@ var init_agents = __esm({
391
516
  lastSeenAt: z.string().datetime().nullable(),
392
517
  metadata: AgentMetadata,
393
518
  processSchema: ProcessSchemaDTO.nullable().optional(),
519
+ // Whether this agent can run through the engine over text: a flow agent (its
520
+ // compiled graph rides in processSchema.program), a brain-connector agent, OR a
521
+ // code-first agent whose worker takes text turns (G-4). Drives the dashboard's chat gate so it
522
+ // no longer hard-codes "flow only". Computed server-side; not stored. OPTIONAL
523
+ // on the wire so a new worker validating a response from an OLD api pod during
524
+ // a rolling deploy doesn't fail register/heartbeat on a missing field.
525
+ textRunnable: z.boolean().optional(),
394
526
  createdAt: z.string().datetime(),
395
527
  updatedAt: z.string().datetime()
396
528
  });
@@ -400,6 +532,9 @@ var init_agents = __esm({
400
532
  z.object({
401
533
  processSchema: ProcessSchemaDTO.nullable()
402
534
  });
535
+ z.object({
536
+ prompt: z.string().nullable()
537
+ });
403
538
  z.object({
404
539
  agent: AgentDTO,
405
540
  // Server-controlled cadence so we can tune without an SDK release.
@@ -486,7 +621,10 @@ var init_consultation = __esm({
486
621
  consultationId: z.string().uuid(),
487
622
  guidance: z.string().min(1).max(4e3),
488
623
  // Optional bridge phrase the agent may speak before delivering guidance.
489
- resumePhrase: z.string().max(500).optional()
624
+ resumePhrase: z.string().max(500).optional(),
625
+ // G-5: the API's id for this command; the agent confirms it (CallControlAck). Optional so a command from an older
626
+ // API is still acted on (just not confirmed).
627
+ commandId: z.string().uuid().optional()
490
628
  });
491
629
  ConsultationPolicy = z.object({
492
630
  enabled: z.boolean().default(true),
@@ -511,7 +649,7 @@ var init_consultation = __esm({
511
649
  });
512
650
  }
513
651
  });
514
- var CallEventKind, OP_CALL_EVENT_KIND, OpCallEventKind, SayCommand, HangupCommand, DtmfCommand, InstructCommand;
652
+ var CallEventKind, OP_CALL_EVENT_KIND, OpCallEventKind, commandId, SayCommand, HangupCommand, DtmfCommand, InstructCommand;
515
653
  var init_call_events = __esm({
516
654
  "../contracts/src/call-events.ts"() {
517
655
  init_consultation();
@@ -536,7 +674,7 @@ var init_call_events = __esm({
536
674
  "dtmf.received",
537
675
  "dtmf.sent"
538
676
  ]);
539
- OP_CALL_EVENT_KIND = /^(tool|handoff|lookup|record|notify)\.[a-z0-9_]{1,48}(\.[a-z0-9_]{1,48})?$/;
677
+ OP_CALL_EVENT_KIND = /^(tool|handoff|lookup|record|notify|engine)\.[a-z0-9_]{1,48}(\.[a-z0-9_]{1,48})?$/;
540
678
  OpCallEventKind = z.string().regex(OP_CALL_EVENT_KIND);
541
679
  z.object({
542
680
  reason: z.string().optional(),
@@ -581,8 +719,10 @@ var init_call_events = __esm({
581
719
  ts: z.string().datetime()
582
720
  });
583
721
  z.enum(["say", "hangup", "dtmf", "instruct", "guidance"]);
722
+ commandId = z.string().uuid().optional();
584
723
  SayCommand = z.object({
585
724
  kind: z.literal("say"),
725
+ commandId,
586
726
  text: z.string().min(1),
587
727
  // 'interrupt' replaces any in-flight TTS; 'queue' waits for current to end.
588
728
  mode: z.enum(["interrupt", "queue"]).default("interrupt"),
@@ -590,14 +730,17 @@ var init_call_events = __esm({
590
730
  });
591
731
  HangupCommand = z.object({
592
732
  kind: z.literal("hangup"),
733
+ commandId,
593
734
  reason: z.string().optional()
594
735
  });
595
736
  DtmfCommand = z.object({
596
737
  kind: z.literal("dtmf"),
738
+ commandId,
597
739
  digits: z.string().regex(/^[0-9*#]+$/)
598
740
  });
599
741
  InstructCommand = z.object({
600
742
  kind: z.literal("instruct"),
743
+ commandId,
601
744
  text: z.string().min(1)
602
745
  });
603
746
  z.discriminatedUnion("kind", [
@@ -607,6 +750,63 @@ var init_call_events = __esm({
607
750
  InstructCommand,
608
751
  GuidanceCommand
609
752
  ]);
753
+ z.object({
754
+ commandId: z.string().uuid(),
755
+ outcome: z.enum(["applied", "refused", "failed"]),
756
+ // machine code: output_blocked · not_supported · unknown_consultation · no_consultation · invalid_command ·
757
+ // say_unavailable · dtmf_unavailable
758
+ code: z.string().max(64).optional(),
759
+ detail: z.string().max(500).optional()
760
+ });
761
+ }
762
+ });
763
+ var TextTurnHistoryEntry, TextTurnUsage, TextTurnErrorCode;
764
+ var init_text_turns = __esm({
765
+ "../contracts/src/text-turns.ts"() {
766
+ z.object({
767
+ mode: z.literal("text"),
768
+ turnId: z.string().uuid(),
769
+ projectId: z.string().min(1),
770
+ agentId: z.string().uuid()
771
+ });
772
+ TextTurnHistoryEntry = z.object({
773
+ role: z.enum(["user", "assistant"]),
774
+ content: z.string().max(16e3)
775
+ });
776
+ z.object({
777
+ turnId: z.string().uuid(),
778
+ agentId: z.string().uuid(),
779
+ /** The Line the message came in on (`sms`, `discord`, `web`, `playground`, …). */
780
+ channel: z.string().min(1).max(40),
781
+ /** The sender on that Line (phone number, Discord user, widget visitor) — the agent's caller-scoped memory key. */
782
+ from: z.string().min(1).max(200),
783
+ /** The conversation this turn belongs to (stable across turns) — the tool context's call id. */
784
+ conversationKey: z.string().min(1).max(300),
785
+ history: z.array(TextTurnHistoryEntry).max(80),
786
+ text: z.string().min(1).max(8e3),
787
+ /** When the API stops waiting; the worker stops the run before it. */
788
+ deadlineAt: z.string().datetime()
789
+ });
790
+ TextTurnUsage = z.object({
791
+ provider: z.string().min(1).max(60),
792
+ model: z.string().min(1).max(200),
793
+ inputTokens: z.number().int().nonnegative(),
794
+ outputTokens: z.number().int().nonnegative(),
795
+ /** Only a first-party worker's label is used; a customer's worker runs on its own key (the API decides). */
796
+ byok: z.boolean()
797
+ });
798
+ TextTurnErrorCode = z.enum(["agent_unavailable", "agent_timeout"]);
799
+ z.union([
800
+ z.object({
801
+ agentId: z.string().uuid(),
802
+ replies: z.array(z.string().max(16e3)).max(20),
803
+ usage: z.array(TextTurnUsage).max(50).default([])
804
+ }),
805
+ z.object({
806
+ agentId: z.string().uuid(),
807
+ error: z.object({ code: TextTurnErrorCode, detail: z.string().max(500).optional() })
808
+ })
809
+ ]);
610
810
  }
611
811
  });
612
812
  var VoiceConfig, ModelConfig, SttConfig, RealtimeConfig, PipelineMode, ToolBinding, ProcessFieldType, ProcessFieldSpec, ProcessCompleteWhen, ProcessBackendAckSpec, ProcessSpec, TriggerConditionSpec, TriggerActionSpec, TriggerSpec, RequiredInfoField, AgentConfig;
@@ -1022,6 +1222,113 @@ var init_forms = __esm({
1022
1222
  "../contracts/src/forms.ts"() {
1023
1223
  }
1024
1224
  });
1225
+ var AgentConfigVersionDTO, AgentConfigDiffField, AgentConfigChange;
1226
+ var init_agent_versions = __esm({
1227
+ "../contracts/src/agent-versions.ts"() {
1228
+ init_agent_config();
1229
+ init_agents();
1230
+ init_consultation();
1231
+ z.object({
1232
+ systemPrompt: z.string().max(64e3),
1233
+ routingInstructions: z.string().max(32e3).nullable(),
1234
+ voice: VoiceConfig,
1235
+ model: ModelConfig,
1236
+ stt: SttConfig.nullable(),
1237
+ mode: PipelineMode,
1238
+ realtime: RealtimeConfig.nullable(),
1239
+ language: z.string().min(2).max(16),
1240
+ tools: z.array(ToolBinding).max(64),
1241
+ consultation: ConsultationPolicy,
1242
+ tags: z.array(z.string().max(64)).max(32).nullable(),
1243
+ brainConnectorId: z.string().uuid().nullable(),
1244
+ flowVersion: z.number().int().nullable(),
1245
+ processSchema: ProcessSchemaDTO.nullable()
1246
+ });
1247
+ z.enum(["published", "draft"]);
1248
+ AgentConfigVersionDTO = z.object({
1249
+ id: z.string().uuid(),
1250
+ version: z.number().int().min(1),
1251
+ createdAt: z.string().datetime(),
1252
+ // 'user:<clerkId>' · 'api_key:<id>' · 'system:backfill' · 'system:deploy' · 'system:register'
1253
+ createdBy: z.string().nullable(),
1254
+ note: z.string().nullable(),
1255
+ flowVersion: z.number().int().nullable(),
1256
+ isPublished: z.boolean()
1257
+ });
1258
+ z.object({
1259
+ versions: z.array(AgentConfigVersionDTO),
1260
+ publishedVersion: z.number().int().min(1)
1261
+ });
1262
+ z.object({
1263
+ note: z.string().trim().max(500).optional()
1264
+ });
1265
+ z.object({
1266
+ version: AgentConfigVersionDTO
1267
+ });
1268
+ AgentConfigDiffField = z.enum([
1269
+ "systemPrompt",
1270
+ "routingInstructions",
1271
+ "mode",
1272
+ "model",
1273
+ "voice",
1274
+ "stt",
1275
+ "realtime",
1276
+ "language",
1277
+ "tools",
1278
+ "brainConnectorId",
1279
+ "consultation",
1280
+ "tags",
1281
+ "flowVersion",
1282
+ "processSchema"
1283
+ ]);
1284
+ AgentConfigChange = z.object({
1285
+ field: AgentConfigDiffField,
1286
+ label: z.string(),
1287
+ /** The published value in plain words. */
1288
+ from: z.string(),
1289
+ /** The draft value in plain words. */
1290
+ to: z.string(),
1291
+ /** True for long text (prompts) — the sheet renders it in a scrollable block. */
1292
+ longText: z.boolean()
1293
+ });
1294
+ z.object({
1295
+ published: z.object({
1296
+ version: z.number().int().min(1),
1297
+ createdAt: z.string().datetime(),
1298
+ note: z.string().nullable()
1299
+ }),
1300
+ hasChanges: z.boolean(),
1301
+ changes: z.array(AgentConfigChange)
1302
+ });
1303
+ }
1304
+ });
1305
+
1306
+ // ../contracts/src/providers.ts
1307
+ function providerIdentity(id) {
1308
+ if (!id) return void 0;
1309
+ return PROVIDERS[PROVIDER_ALIASES[id] ?? id];
1310
+ }
1311
+ function byokProviders() {
1312
+ return Object.values(PROVIDERS).filter((p) => p.byok);
1313
+ }
1314
+ var PROVIDERS, PROVIDER_ALIASES;
1315
+ var init_providers = __esm({
1316
+ "../contracts/src/providers.ts"() {
1317
+ PROVIDERS = {
1318
+ openai: { id: "openai", label: "OpenAI", capabilities: ["llm", "tts", "realtime"], byok: true, supportsBaseUrl: true, keyPlaceholder: "sk-..." },
1319
+ anthropic: { id: "anthropic", label: "Anthropic", capabilities: ["llm"], byok: true, supportsBaseUrl: true, keyPlaceholder: "sk-ant-..." },
1320
+ deepgram: { id: "deepgram", label: "Deepgram", capabilities: ["stt", "tts"], byok: true, supportsBaseUrl: false, keyPlaceholder: "Deepgram API key" },
1321
+ elevenlabs: { id: "elevenlabs", label: "ElevenLabs", capabilities: ["tts"], byok: true, supportsBaseUrl: false, keyPlaceholder: "ElevenLabs API key" },
1322
+ cartesia: { id: "cartesia", label: "Cartesia", capabilities: ["stt", "tts"], byok: true, supportsBaseUrl: false, keyPlaceholder: "Cartesia API key" },
1323
+ assemblyai: { id: "assemblyai", label: "AssemblyAI", capabilities: ["stt"], byok: true, supportsBaseUrl: false, keyPlaceholder: "AssemblyAI API key" },
1324
+ google: { id: "google", label: "Google (Gemini)", capabilities: ["llm", "realtime"], byok: true, supportsBaseUrl: false, keyPlaceholder: "AIza..." },
1325
+ groq: { id: "groq", label: "Groq", capabilities: ["llm"], byok: true, supportsBaseUrl: true, keyPlaceholder: "gsk_..." },
1326
+ // Runtime-only providers (no external key): local VAD.
1327
+ silero: { id: "silero", label: "Silero", capabilities: ["vad"], byok: false, supportsBaseUrl: false }
1328
+ };
1329
+ PROVIDER_ALIASES = { gemini: "google" };
1330
+ }
1331
+ });
1025
1332
  var ProviderKind, CatalogVoice, CatalogModel, CatalogProvider, CostEstimateAssumptions, CostEstimatePerMinute, CostEstimateBilledPerMinute;
1026
1333
  var init_model_catalog = __esm({
1027
1334
  "../contracts/src/model-catalog.ts"() {
@@ -1419,6 +1726,8 @@ var init_numbers = __esm({
1419
1726
  agentId: z.string().uuid().nullable(),
1420
1727
  // Environments pin (draft/live). null = no pin → live (today's behaviour).
1421
1728
  agentConfigEnv: z.enum(["draft", "live"]).nullable(),
1729
+ // "Agent answers texts": the bound agent replies to inbound SMS (burn-down G-1). Off by default.
1730
+ smsAgentReplies: z.boolean(),
1422
1731
  status: PhoneNumberStatus,
1423
1732
  friendlyName: z.string().nullable(),
1424
1733
  region: z.string().nullable(),
@@ -1442,6 +1751,7 @@ var init_numbers = __esm({
1442
1751
  // null detaches.
1443
1752
  agentId: z.string().uuid().nullable()
1444
1753
  });
1754
+ z.object({ enabled: z.boolean() });
1445
1755
  }
1446
1756
  });
1447
1757
  var init_project_limits = __esm({
@@ -1548,6 +1858,12 @@ var init_compliance = __esm({
1548
1858
  ComplianceConfig.partial();
1549
1859
  }
1550
1860
  });
1861
+
1862
+ // ../contracts/src/sms-keywords.ts
1863
+ var init_sms_keywords = __esm({
1864
+ "../contracts/src/sms-keywords.ts"() {
1865
+ }
1866
+ });
1551
1867
  var init_call_recording = __esm({
1552
1868
  "../contracts/src/call-recording.ts"() {
1553
1869
  z.object({
@@ -1584,10 +1900,16 @@ var init_outbound = __esm({
1584
1900
  var ConnectionAuthType, ConnectionStatus;
1585
1901
  var init_connection = __esm({
1586
1902
  "../contracts/src/connection.ts"() {
1903
+ init_providers();
1587
1904
  ConnectionAuthType = z.enum(["oauth2", "api_key", "bearer", "basic", "none"]);
1588
1905
  ConnectionStatus = z.enum(["active", "revoked", "error"]);
1589
1906
  z.object({
1590
1907
  v: z.number().int(),
1908
+ // Key id (short fingerprint) of the key that encrypted this envelope — lets the
1909
+ // cipher pick the right key from its ring so CREDENTIAL_ENCRYPTION_KEY can rotate
1910
+ // in-band (Tier-1 item 4). Optional/absent on legacy v1 envelopes, which decrypt
1911
+ // with the designated legacy key.
1912
+ kid: z.string().optional(),
1591
1913
  iv: z.string(),
1592
1914
  ct: z.string(),
1593
1915
  tag: z.string()
@@ -1625,6 +1947,12 @@ var init_connection = __esm({
1625
1947
  }).refine((v) => v.name !== void 0 || v.secret !== void 0 || v.metadata !== void 0, {
1626
1948
  message: "nothing to update"
1627
1949
  });
1950
+ byokProviders().map((p) => ({
1951
+ id: p.id,
1952
+ label: p.label,
1953
+ keyPlaceholder: p.keyPlaceholder ?? "",
1954
+ supportsBaseUrl: p.supportsBaseUrl
1955
+ }));
1628
1956
  }
1629
1957
  });
1630
1958
  var E164, RoomPrefix, RecordingPrefix, Cidr, TelephonySettings, ProfileSource, EffectiveCallProfile;
@@ -1958,6 +2286,7 @@ var init_flow_enrichment = __esm({
1958
2286
  var init_flow_compile = __esm({
1959
2287
  "../contracts/src/flow-compile.ts"() {
1960
2288
  init_flow_enrichment();
2289
+ init_flow_validate();
1961
2290
  }
1962
2291
  });
1963
2292
  var EnvironmentSpec;
@@ -2102,6 +2431,1057 @@ var init_playbook_compile = __esm({
2102
2431
  "../contracts/src/playbook-compile.ts"() {
2103
2432
  }
2104
2433
  });
2434
+ var EngineSeamBinding, EnginePluginDTO;
2435
+ var init_engine = __esm({
2436
+ "../contracts/src/engine.ts"() {
2437
+ EngineSeamBinding = z.object({
2438
+ key: z.string(),
2439
+ bound: z.boolean()
2440
+ });
2441
+ EnginePluginDTO = z.object({
2442
+ name: z.string(),
2443
+ provides: z.array(EngineSeamBinding),
2444
+ injects: z.array(z.string())
2445
+ });
2446
+ z.object({
2447
+ plugins: z.array(EnginePluginDTO)
2448
+ });
2449
+ }
2450
+ });
2451
+
2452
+ // ../contracts/src/identity.ts
2453
+ var init_identity = __esm({
2454
+ "../contracts/src/identity.ts"() {
2455
+ }
2456
+ });
2457
+
2458
+ // ../contracts/src/interaction.ts
2459
+ var init_interaction = __esm({
2460
+ "../contracts/src/interaction.ts"() {
2461
+ }
2462
+ });
2463
+
2464
+ // ../contracts/src/channel-connector/capability-state.ts
2465
+ var CONDITION_CLASSES, CONDITION_TYPES, SURFACES, TRANSPORT_KINDS, AGENT_INITIATION, TEMPLATE_REQUIREMENT, WINDOW_OUTSIDE, PEER_ID_SCOPES, RECEIPT_KINDS;
2466
+ var init_capability_state = __esm({
2467
+ "../contracts/src/channel-connector/capability-state.ts"() {
2468
+ CONDITION_CLASSES = ["authority", "operational", "observability"];
2469
+ CONDITION_TYPES = [
2470
+ { id: "CredentialsValid", defaultClass: "authority", summary: "credential lease resolves and is not expired/revoked" },
2471
+ { id: "ProviderAccountGood", defaultClass: "authority", summary: "provider account / sender standing is not suspended or restricted" },
2472
+ { id: "RegistrationApproved", defaultClass: "authority", summary: "required registration / approval is current" },
2473
+ { id: "ResourceOwnershipConfirmed", defaultClass: "authority", summary: "the native object is confirmed to belong to this principal" },
2474
+ { id: "InstallationPresent", defaultClass: "authority", summary: "the container installation exists and is not revoked" },
2475
+ { id: "PermissionsHeld", defaultClass: "authority", summary: "required provider permissions/scopes/intents are held" },
2476
+ { id: "ConsentResolved", defaultClass: "authority", summary: "consent verdict available (ch. 02; not_evaluated until it ships)" },
2477
+ { id: "WindowOpen", defaultClass: "authority", summary: "the reply window for the peer is open" },
2478
+ { id: "TemplateAvailable", defaultClass: "authority", summary: "an approved template exists for the operation" },
2479
+ { id: "PeerReachable", defaultClass: "authority", summary: "the peer can be reached on this channel (e.g. RCS capability, not blocked)" },
2480
+ { id: "WebhookRegistered", defaultClass: "operational", summary: "the provider has our ingress registered" },
2481
+ { id: "InboundVerificationReady", defaultClass: "authority", summary: "the material the declared inbound-auth strategy needs (for the selected authorization method) resolves \u2014 REST auth alone never satisfies it (#306 L4)" },
2482
+ { id: "WebhookHealthy", defaultClass: "operational", summary: "recent verified inbound / delivery health" },
2483
+ { id: "TransportConnected", defaultClass: "operational", summary: "a transport lease is live (gateway / long-poll / pub-sub)" },
2484
+ { id: "ReconcileFresh", defaultClass: "operational", summary: "the last reconcile is within its window" },
2485
+ { id: "ProviderLatency", defaultClass: "observability", summary: "provider latency / error-rate telemetry" }
2486
+ ];
2487
+ new Set(CONDITION_TYPES.map((c) => c.id));
2488
+ SURFACES = ["direct", "group", "channel", "thread", "broadcast"];
2489
+ TRANSPORT_KINDS = ["webhook", "long_poll", "gateway", "pubsub", "http_interactions"];
2490
+ AGENT_INITIATION = ["always", "installed_only", "opt_in_required", "template_only", "never"];
2491
+ TEMPLATE_REQUIREMENT = ["never", "outside_window", "always"];
2492
+ WINDOW_OUTSIDE = ["tagged_only", "template_only", "blocked"];
2493
+ PEER_ID_SCOPES = ["global", "app_scoped", "workspace_scoped", "installation_scoped"];
2494
+ RECEIPT_KINDS = ["sent", "delivered", "read", "failed"];
2495
+ }
2496
+ });
2497
+
2498
+ // ../contracts/src/channel-connector/channels.ts
2499
+ function baseline(channel, partial) {
2500
+ return {
2501
+ channel,
2502
+ connector: null,
2503
+ version: "1.0",
2504
+ requiredPermissions: [],
2505
+ registration: [],
2506
+ readinessGates: [
2507
+ { condition: "CredentialsValid", class: "authority", blocks: "*", ttlSec: 300 },
2508
+ { condition: "ProviderAccountGood", class: "authority", blocks: "*", ttlSec: 3600 },
2509
+ { condition: "WebhookHealthy", class: "operational", blocks: ["receive.message", "call.receive"], ttlSec: 900, evidence: "runtime" }
2510
+ ],
2511
+ ...partial
2512
+ };
2513
+ }
2514
+ var TEXT_ONLY, RICH_MESSAGING, MESSAGE_CORE, CONTAINER_GATES, CHANNELS, CHANNEL_IDS;
2515
+ var init_channels = __esm({
2516
+ "../contracts/src/channel-connector/channels.ts"() {
2517
+ init_capability_state();
2518
+ TEXT_ONLY = ["text"];
2519
+ RICH_MESSAGING = ["text", "image", "audio", "video", "document", "location", "contact", "sticker", "reaction", "interactive", "template"];
2520
+ MESSAGE_CORE = ["receive.message", "send.text", "send.media", "initiate.conversation", "reply.in_window"];
2521
+ CONTAINER_GATES = [
2522
+ { condition: "CredentialsValid", class: "authority", blocks: "*", ttlSec: 300 },
2523
+ { condition: "InstallationPresent", class: "authority", blocks: "*", ttlSec: 3600 },
2524
+ { condition: "PermissionsHeld", class: "authority", blocks: ["send.text", "send.media", "channel.post", "thread.reply", "group.post"], ttlSec: 3600 },
2525
+ { condition: "WebhookHealthy", class: "operational", blocks: ["receive.message"], ttlSec: 900, evidence: "runtime" }
2526
+ ];
2527
+ CHANNELS = [
2528
+ {
2529
+ id: "sms",
2530
+ displayName: "SMS",
2531
+ addressSpace: "phone",
2532
+ surfaces: ["direct"],
2533
+ bindingCapabilities: ["sms"],
2534
+ baseline: baseline("sms", {
2535
+ operations: [...MESSAGE_CORE, "read.receipt"],
2536
+ direction: { inbound: true, outbound: true },
2537
+ initiation: { agentInitiated: "always", userInitiated: true, firstContactRule: "agent-initiated with registration (10DLC / TFV per country); STOP/HELP keywords mandatory" },
2538
+ windows: [],
2539
+ templates: { required: "never", reviewed: false },
2540
+ surfaces: ["direct"],
2541
+ messageTypes: TEXT_ONLY,
2542
+ // GSM-7 160/153, UCS-2 70/67 per segment (doc 01 §3); no body cap is a channel fact.
2543
+ limits: { segments: true, note: "GSM-7 160/153 \xB7 UCS-2 70/67 per segment" },
2544
+ receipts: ["sent", "delivered", "failed"],
2545
+ edits: { edit: false, delete: false, reactions: false, commands: false },
2546
+ transport: ["webhook"],
2547
+ identity: { peerIdKind: "phone", peerIdScope: "global" },
2548
+ registration: [{ kind: "campaign_vetting", blocks: ["send.text", "send.media", "initiate.conversation"] }]
2549
+ })
2550
+ },
2551
+ {
2552
+ id: "mms",
2553
+ displayName: "MMS",
2554
+ addressSpace: "phone",
2555
+ surfaces: ["direct", "group"],
2556
+ bindingCapabilities: ["mms"],
2557
+ baseline: baseline("mms", {
2558
+ operations: [...MESSAGE_CORE, "group.post"],
2559
+ direction: { inbound: true, outbound: true },
2560
+ initiation: { agentInitiated: "always", userInitiated: true, firstContactRule: "as SMS; carrier MMS size caps apply" },
2561
+ windows: [],
2562
+ templates: { required: "never", reviewed: false },
2563
+ surfaces: ["direct", "group"],
2564
+ messageTypes: ["text", "image", "audio", "video", "document"],
2565
+ limits: { mediaBytes: 1e6, note: "carrier MMS \u22480.6\u20131 MB (doc 01 \xA73)" },
2566
+ receipts: ["sent", "delivered", "failed"],
2567
+ edits: { edit: false, delete: false, reactions: false, commands: false },
2568
+ transport: ["webhook"],
2569
+ identity: { peerIdKind: "phone", peerIdScope: "global" },
2570
+ registration: [{ kind: "campaign_vetting", blocks: ["send.text", "send.media", "initiate.conversation"] }]
2571
+ })
2572
+ },
2573
+ {
2574
+ id: "voice",
2575
+ displayName: "Voice (PSTN)",
2576
+ addressSpace: "phone",
2577
+ surfaces: ["direct"],
2578
+ bindingCapabilities: ["voice"],
2579
+ baseline: baseline("voice", {
2580
+ operations: ["call.receive", "call.place"],
2581
+ direction: { inbound: true, outbound: true },
2582
+ initiation: { agentInitiated: "always", userInitiated: true, firstContactRule: "agent-initiated; STIR/SHAKEN attestation; emergency addressing; trunk config" },
2583
+ windows: [],
2584
+ templates: { required: "never", reviewed: false },
2585
+ surfaces: ["direct"],
2586
+ messageTypes: [],
2587
+ limits: {},
2588
+ receipts: [],
2589
+ edits: { edit: false, delete: false, reactions: false, commands: false },
2590
+ transport: ["webhook"],
2591
+ identity: { peerIdKind: "phone", peerIdScope: "global" }
2592
+ })
2593
+ },
2594
+ {
2595
+ id: "whatsapp",
2596
+ displayName: "WhatsApp",
2597
+ addressSpace: "whatsapp",
2598
+ surfaces: ["direct", "group"],
2599
+ bindingCapabilities: ["whatsapp.message"],
2600
+ baseline: baseline("whatsapp", {
2601
+ operations: [...MESSAGE_CORE, "send.template", "send.interactive", "send.reaction", "read.receipt", "typing.indicate"],
2602
+ direction: { inbound: true, outbound: true },
2603
+ initiation: { agentInitiated: "template_only", userInitiated: true, firstContactRule: "user-initiated or a reviewed template outside the 24-h window; opt-in mandatory" },
2604
+ windows: [{ kind: "reply", durationSec: 86400, opensOn: "inbound_message", outside: "template_only" }],
2605
+ templates: { required: "outside_window", reviewed: true },
2606
+ surfaces: ["direct", "group"],
2607
+ messageTypes: RICH_MESSAGING,
2608
+ limits: {
2609
+ textChars: 4096,
2610
+ mediaBytes: 1e8,
2611
+ perSecond: 80,
2612
+ formats: { audio: { mime: ["audio/ogg", "audio/mpeg", "audio/mp4", "audio/aac", "audio/amr"], variants: ["voice_note", "file"] } }
2613
+ },
2614
+ receipts: ["sent", "delivered", "read", "failed"],
2615
+ edits: { edit: false, delete: false, reactions: true, commands: false },
2616
+ transport: ["webhook"],
2617
+ identity: { peerIdKind: "whatsapp", peerIdScope: "global" },
2618
+ registration: [{ kind: "business_verification", blocks: ["initiate.conversation", "send.template"] }]
2619
+ })
2620
+ },
2621
+ {
2622
+ id: "instagram",
2623
+ displayName: "Instagram DM",
2624
+ addressSpace: "app",
2625
+ surfaces: ["direct"],
2626
+ bindingCapabilities: ["instagram.message"],
2627
+ baseline: baseline("instagram", {
2628
+ operations: ["receive.message", "send.text", "send.media", "send.reaction", "reply.in_window", "delete.message", "read.receipt"],
2629
+ direction: { inbound: true, outbound: true },
2630
+ initiation: { agentInitiated: "never", userInitiated: true, firstContactRule: "user-initiated only \u2014 the peer identifier exists only after the user messages or clicks" },
2631
+ windows: [{ kind: "reply", durationSec: 86400, opensOn: "inbound_message", outside: "tagged_only" }],
2632
+ templates: { required: "never", reviewed: false },
2633
+ surfaces: ["direct"],
2634
+ messageTypes: ["text", "image", "audio", "video", "document", "sticker", "reaction", "interactive", "template"],
2635
+ limits: { textBytes: 1e3, mediaBytes: 25e6 },
2636
+ receipts: ["read"],
2637
+ edits: { edit: false, delete: true, reactions: true, commands: false },
2638
+ transport: ["webhook"],
2639
+ // doc 07 §3: per person × professional account, stable across apps — the
2640
+ // account scope is embedded in the normalized value, so the id is global.
2641
+ identity: { peerIdKind: "meta_account_user", peerIdScope: "global" },
2642
+ registration: [{ kind: "app_review", blocks: ["send.text", "send.media"] }]
2643
+ })
2644
+ },
2645
+ {
2646
+ id: "messenger",
2647
+ displayName: "Messenger",
2648
+ addressSpace: "app",
2649
+ surfaces: ["direct"],
2650
+ bindingCapabilities: ["messenger.message"],
2651
+ baseline: baseline("messenger", {
2652
+ operations: ["receive.message", "send.text", "send.media", "send.reaction", "reply.in_window", "edit.message", "read.receipt", "typing.indicate"],
2653
+ direction: { inbound: true, outbound: true },
2654
+ initiation: { agentInitiated: "never", userInitiated: true, firstContactRule: "as Instagram plus Get Started / link / ads entry points" },
2655
+ windows: [{ kind: "reply", durationSec: 86400, opensOn: "inbound_message", outside: "tagged_only" }],
2656
+ templates: { required: "never", reviewed: false },
2657
+ surfaces: ["direct"],
2658
+ messageTypes: RICH_MESSAGING,
2659
+ limits: { mediaBytes: 25e6 },
2660
+ receipts: ["delivered", "read"],
2661
+ edits: { edit: true, delete: false, reactions: true, commands: false },
2662
+ transport: ["webhook"],
2663
+ identity: { peerIdKind: "meta_account_user", peerIdScope: "global" },
2664
+ registration: [{ kind: "app_review", blocks: ["send.text", "send.media"] }]
2665
+ })
2666
+ },
2667
+ {
2668
+ id: "telegram",
2669
+ displayName: "Telegram",
2670
+ addressSpace: "app",
2671
+ surfaces: ["direct", "group", "channel", "thread"],
2672
+ bindingCapabilities: ["telegram.message"],
2673
+ baseline: baseline("telegram", {
2674
+ operations: [...MESSAGE_CORE, "send.interactive", "send.reaction", "edit.message", "delete.message", "command.register", "group.post", "channel.post", "thread.reply"],
2675
+ direction: { inbound: true, outbound: true },
2676
+ // Plain bot: the user must start; no reply window. The business-account
2677
+ // 24-h rule is an installation-level narrowing, not the channel ceiling.
2678
+ initiation: { agentInitiated: "never", userInitiated: true, firstContactRule: "the user must start the bot (/start, deep link, group add) before it may message them" },
2679
+ windows: [],
2680
+ templates: { required: "never", reviewed: false },
2681
+ surfaces: ["direct", "group", "channel", "thread"],
2682
+ messageTypes: ["text", "image", "audio", "video", "document", "location", "contact", "sticker", "reaction", "interactive", "poll"],
2683
+ limits: { textChars: 4096, mediaBytes: 5e7, formats: { audio: { mime: ["audio/ogg", "audio/mpeg"], variants: ["voice_note", "file"] } } },
2684
+ receipts: [],
2685
+ edits: { edit: true, delete: true, reactions: true, commands: true },
2686
+ transport: ["webhook", "long_poll"],
2687
+ identity: { peerIdKind: "telegram_user", peerIdScope: "global" }
2688
+ })
2689
+ },
2690
+ {
2691
+ id: "discord",
2692
+ displayName: "Discord",
2693
+ addressSpace: "app",
2694
+ surfaces: ["direct", "group", "channel", "thread"],
2695
+ bindingCapabilities: ["discord.message", "discord.command"],
2696
+ baseline: baseline("discord", {
2697
+ // The channel exposes BOTH modes (doc 04 §1): text via `send.*`, realtime
2698
+ // audio via `call.*`. Mode is per OPERATION — the channel id never implies it.
2699
+ operations: [...MESSAGE_CORE, "send.interactive", "send.reaction", "edit.message", "delete.message", "command.register", "channel.post", "thread.reply", "call.receive", "call.place"],
2700
+ direction: { inbound: true, outbound: true },
2701
+ initiation: { agentInitiated: "installed_only", userInitiated: true, firstContactRule: "guild channels by permission; DMs need a mutual guild or a user install" },
2702
+ windows: [],
2703
+ templates: { required: "never", reviewed: false },
2704
+ surfaces: ["direct", "group", "channel", "thread"],
2705
+ messageTypes: ["text", "image", "audio", "video", "document", "reaction", "interactive", "card", "poll"],
2706
+ limits: { textChars: 2e3, mediaBytes: 10485760 },
2707
+ receipts: [],
2708
+ edits: { edit: true, delete: true, reactions: true, commands: true },
2709
+ transport: ["gateway", "http_interactions"],
2710
+ identity: { peerIdKind: "discord_user", peerIdScope: "global" },
2711
+ readinessGates: [
2712
+ { condition: "CredentialsValid", class: "authority", blocks: "*", ttlSec: 300 },
2713
+ { condition: "InstallationPresent", class: "authority", blocks: "*", ttlSec: 3600 },
2714
+ { condition: "PermissionsHeld", class: "authority", blocks: ["send.text", "send.media", "channel.post", "thread.reply", "call.receive", "call.place"], ttlSec: 3600 },
2715
+ { condition: "TransportConnected", class: "operational", blocks: ["receive.message", "call.receive"], ttlSec: 120 }
2716
+ ]
2717
+ })
2718
+ },
2719
+ {
2720
+ // G-3: the embeddable website chat widget. The widget key IS the identity and its evidence (a `web`-space Binding
2721
+ // at `widget:<keyId>`, no provider connection), so no provider readiness gate applies. The agent only ever answers
2722
+ // inside the visitor's own request — it never starts or pushes a message.
2723
+ id: "web",
2724
+ displayName: "Website chat",
2725
+ addressSpace: "web",
2726
+ surfaces: ["direct"],
2727
+ bindingCapabilities: ["web.message"],
2728
+ baseline: baseline("web", {
2729
+ operations: ["receive.message", "reply.in_window"],
2730
+ direction: { inbound: true, outbound: true },
2731
+ initiation: { agentInitiated: "never", userInitiated: true, firstContactRule: "the visitor opens the widget and writes first" },
2732
+ windows: [],
2733
+ templates: { required: "never", reviewed: false },
2734
+ surfaces: ["direct"],
2735
+ messageTypes: TEXT_ONLY,
2736
+ limits: { textChars: 8e3 },
2737
+ receipts: [],
2738
+ edits: { edit: false, delete: false, reactions: false, commands: false },
2739
+ transport: ["http_interactions"],
2740
+ identity: { peerIdKind: "web_visitor", peerIdScope: "app_scoped" },
2741
+ readinessGates: []
2742
+ })
2743
+ },
2744
+ {
2745
+ id: "slack",
2746
+ displayName: "Slack",
2747
+ addressSpace: "app",
2748
+ surfaces: ["direct", "group", "channel", "thread"],
2749
+ bindingCapabilities: ["slack.message"],
2750
+ baseline: baseline("slack", {
2751
+ operations: [...MESSAGE_CORE, "send.interactive", "send.reaction", "edit.message", "delete.message", "command.register", "channel.post", "thread.reply"],
2752
+ direction: { inbound: true, outbound: true },
2753
+ initiation: { agentInitiated: "installed_only", userInitiated: true, firstContactRule: "any channel the bot is in; DMs" },
2754
+ windows: [],
2755
+ templates: { required: "never", reviewed: false },
2756
+ surfaces: ["direct", "group", "channel", "thread"],
2757
+ messageTypes: ["text", "image", "audio", "video", "document", "reaction", "interactive", "card"],
2758
+ limits: { textChars: 4e3, note: "truncated at 40,000 (doc 01 \xA73)" },
2759
+ receipts: [],
2760
+ edits: { edit: true, delete: true, reactions: true, commands: true },
2761
+ transport: ["webhook", "gateway"],
2762
+ identity: { peerIdKind: "slack_user", peerIdScope: "workspace_scoped" },
2763
+ readinessGates: CONTAINER_GATES
2764
+ })
2765
+ },
2766
+ {
2767
+ id: "teams",
2768
+ displayName: "Microsoft Teams",
2769
+ addressSpace: "app",
2770
+ surfaces: ["direct", "group", "channel", "thread"],
2771
+ bindingCapabilities: ["teams.message"],
2772
+ baseline: baseline("teams", {
2773
+ operations: [...MESSAGE_CORE, "send.interactive", "edit.message", "delete.message", "channel.post", "thread.reply", "read.receipt"],
2774
+ direction: { inbound: true, outbound: true },
2775
+ initiation: { agentInitiated: "installed_only", userInitiated: true, firstContactRule: "proactive send needs an installation in scope plus a conversation reference" },
2776
+ windows: [],
2777
+ templates: { required: "never", reviewed: false },
2778
+ surfaces: ["direct", "group", "channel", "thread"],
2779
+ messageTypes: ["text", "image", "document", "card", "interactive", "reaction"],
2780
+ limits: { textBytes: 102400, note: "message payload \u2264100 KB (doc 01 \xA73)" },
2781
+ receipts: ["read"],
2782
+ edits: { edit: true, delete: true, reactions: false, commands: false },
2783
+ transport: ["webhook"],
2784
+ identity: { peerIdKind: "teams_user", peerIdScope: "installation_scoped" },
2785
+ readinessGates: CONTAINER_GATES
2786
+ })
2787
+ },
2788
+ {
2789
+ id: "rcs",
2790
+ displayName: "RCS",
2791
+ addressSpace: "rcs",
2792
+ surfaces: ["direct"],
2793
+ bindingCapabilities: ["rcs.message"],
2794
+ baseline: baseline("rcs", {
2795
+ operations: [...MESSAGE_CORE, "send.interactive", "read.receipt", "typing.indicate", "delete.message"],
2796
+ direction: { inbound: true, outbound: true },
2797
+ initiation: { agentInitiated: "opt_in_required", userInitiated: true, firstContactRule: "agent-initiated only with consent per AUP; STOP / UNSUBSCRIBE brand-wide" },
2798
+ windows: [],
2799
+ templates: { required: "never", reviewed: false },
2800
+ surfaces: ["direct"],
2801
+ messageTypes: ["text", "image", "audio", "video", "document", "card", "carousel", "interactive"],
2802
+ limits: { textChars: 3072, mediaBytes: 104857600 },
2803
+ receipts: ["delivered", "read"],
2804
+ edits: { edit: false, delete: true, reactions: false, commands: false },
2805
+ transport: ["webhook"],
2806
+ identity: { peerIdKind: "phone", peerIdScope: "global" },
2807
+ registration: [{ kind: "agent_launch", blocks: ["send.text", "send.media", "send.interactive", "initiate.conversation"] }],
2808
+ readinessGates: [
2809
+ { condition: "CredentialsValid", class: "authority", blocks: "*", ttlSec: 300 },
2810
+ { condition: "RegistrationApproved", class: "authority", blocks: ["send.text", "send.media", "send.interactive", "initiate.conversation"], ttlSec: 86400 },
2811
+ { condition: "PeerReachable", class: "authority", blocks: ["send.text", "send.media", "send.interactive", "initiate.conversation"], ttlSec: 86400 },
2812
+ { condition: "WebhookHealthy", class: "operational", blocks: ["receive.message"], ttlSec: 900, evidence: "runtime" }
2813
+ ]
2814
+ })
2815
+ }
2816
+ ];
2817
+ new Map(CHANNELS.map((c) => [c.id, c]));
2818
+ CHANNEL_IDS = new Set(CHANNELS.map((c) => c.id));
2819
+ }
2820
+ });
2821
+
2822
+ // ../contracts/src/channel-connector/operations.ts
2823
+ var OPERATION_MODES, OPERATIONS, OPERATION_IDS;
2824
+ var init_operations = __esm({
2825
+ "../contracts/src/channel-connector/operations.ts"() {
2826
+ OPERATION_MODES = ["message", "realtime"];
2827
+ OPERATIONS = [
2828
+ { id: "receive.message", mode: "message", summary: "accept an inbound message" },
2829
+ { id: "send.text", mode: "message", summary: "send a text message" },
2830
+ { id: "send.media", mode: "message", summary: "send a message carrying a content item (image, audio, video, document)" },
2831
+ { id: "send.template", mode: "message", summary: "send a reviewed template" },
2832
+ { id: "send.interactive", mode: "message", summary: "send structured/interactive content" },
2833
+ { id: "send.reaction", mode: "message", summary: "react to a message" },
2834
+ { id: "edit.message", mode: "message", summary: "edit a sent message" },
2835
+ { id: "delete.message", mode: "message", summary: "delete a sent message" },
2836
+ { id: "initiate.conversation", mode: "message", summary: "first contact / outside any window" },
2837
+ { id: "reply.in_window", mode: "message", summary: "reply inside an open window" },
2838
+ { id: "call.receive", mode: "realtime", summary: "accept an inbound realtime session" },
2839
+ { id: "call.place", mode: "realtime", summary: "place an outbound realtime session" },
2840
+ { id: "read.receipt", mode: "message", summary: "mark a message as read" },
2841
+ { id: "typing.indicate", mode: "message", summary: "typing indicator" },
2842
+ { id: "command.register", mode: "message", summary: "register slash/bot commands" },
2843
+ { id: "thread.reply", mode: "message", summary: "reply inside a thread" },
2844
+ { id: "group.post", mode: "message", summary: "post to a group surface" },
2845
+ { id: "channel.post", mode: "message", summary: "post to a channel surface" }
2846
+ ];
2847
+ new Map(
2848
+ OPERATIONS.map((op) => [op.id, op])
2849
+ );
2850
+ OPERATION_IDS = new Set(OPERATIONS.map((op) => op.id));
2851
+ }
2852
+ });
2853
+
2854
+ // ../contracts/src/channel-connector/ids.ts
2855
+ var A2_SHIPPED_ROOTS, A2_PLATFORM_ROOTS, NAMESPACE_RE, SEMVER_RE;
2856
+ var init_ids2 = __esm({
2857
+ "../contracts/src/channel-connector/ids.ts"() {
2858
+ init_channels();
2859
+ init_operations();
2860
+ A2_SHIPPED_ROOTS = [
2861
+ "llm",
2862
+ "conversation",
2863
+ "channels",
2864
+ "tools",
2865
+ "guard",
2866
+ "store",
2867
+ "compliance",
2868
+ "carriers",
2869
+ "agents",
2870
+ "agentRun",
2871
+ "telemetry"
2872
+ ];
2873
+ A2_PLATFORM_ROOTS = [
2874
+ ...A2_SHIPPED_ROOTS,
2875
+ "policy",
2876
+ "identity",
2877
+ "binding",
2878
+ "provider",
2879
+ "workflow",
2880
+ "session",
2881
+ "engine",
2882
+ "message",
2883
+ "connector",
2884
+ "communication",
2885
+ "event",
2886
+ "reach",
2887
+ "handler",
2888
+ "plugin",
2889
+ "scope",
2890
+ "snapshot"
2891
+ ];
2892
+ /* @__PURE__ */ new Set([
2893
+ ...A2_PLATFORM_ROOTS,
2894
+ ...[...OPERATION_IDS].map((id) => id.split(".")[0]),
2895
+ ...CHANNEL_IDS
2896
+ ]);
2897
+ NAMESPACE_RE = /^[a-z0-9]+\.[a-z0-9]+$/;
2898
+ SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
2899
+ }
2900
+ });
2901
+ function parseContentRef(ref) {
2902
+ const m = CONTENT_REF_RE.exec(ref);
2903
+ return m ? { principalId: m[1], contentId: m[2] } : null;
2904
+ }
2905
+ function isContentRef(value) {
2906
+ return typeof value === "string" && CONTENT_REF_RE.test(value);
2907
+ }
2908
+ function looksLikeProviderLocator(value) {
2909
+ return /^(https?:\/\/|s3:\/\/|gs:\/\/)/i.test(value) || value.includes("?") || value.includes("://");
2910
+ }
2911
+ var MODALITY_KINDS, STRUCTURED_KINDS, CONTENT_KINDS, LOSSINESS, CONTENT_REF_RE, MessageContentProvenance, noLocator, MessageContentMetadata, contentRefSchema, OriginalMessageContent, DerivedMessageContent;
2912
+ var init_content_kinds = __esm({
2913
+ "../contracts/src/channel-connector/content-kinds.ts"() {
2914
+ MODALITY_KINDS = ["text", "image", "audio", "video", "document"];
2915
+ STRUCTURED_KINDS = [
2916
+ "location",
2917
+ "contact",
2918
+ "sticker",
2919
+ "reaction",
2920
+ "interactive",
2921
+ "card",
2922
+ "carousel",
2923
+ "poll",
2924
+ "template"
2925
+ ];
2926
+ CONTENT_KINDS = [...MODALITY_KINDS, ...STRUCTURED_KINDS];
2927
+ new Set(CONTENT_KINDS);
2928
+ LOSSINESS = ["lossless", "lossy", "semantic"];
2929
+ CONTENT_REF_RE = /^content:([A-Za-z0-9_-]+)\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/;
2930
+ MessageContentProvenance = z.object({
2931
+ /** The connector that produced the item (A7 id). */
2932
+ connector: z.string(),
2933
+ /** The provider-native identifier — reconciliation only; never agent-facing. */
2934
+ providerRef: z.string().optional(),
2935
+ receivedAt: z.string().datetime().optional()
2936
+ }).strict();
2937
+ noLocator = (label) => z.string().refine((v) => !looksLikeProviderLocator(v), { message: `${label} must not carry a provider locator \u2014 provider ids live in provenance.providerRef only` });
2938
+ MessageContentMetadata = z.object({
2939
+ bytes: z.number().int().nonnegative().optional(),
2940
+ durationMs: z.number().int().nonnegative().optional(),
2941
+ width: z.number().int().positive().optional(),
2942
+ height: z.number().int().positive().optional(),
2943
+ filename: noLocator("metadata.filename").optional(),
2944
+ caption: noLocator("metadata.caption").optional(),
2945
+ /** e.g. `voice_note` for audio. */
2946
+ variant: z.string().regex(/^[a-z0-9_]+$/).optional(),
2947
+ language: z.string().max(16).optional()
2948
+ }).strict();
2949
+ contentRefSchema = z.string().refine(isContentRef, {
2950
+ message: "ref must be an opaque platform content reference `content:<principal>/<uuid>`"
2951
+ });
2952
+ OriginalMessageContent = z.object({
2953
+ kind: z.enum(CONTENT_KINDS),
2954
+ /** MIME type or registry format id. */
2955
+ format: z.string().min(1),
2956
+ ref: contentRefSchema,
2957
+ metadata: MessageContentMetadata.default({}),
2958
+ provenance: MessageContentProvenance,
2959
+ /** Inline text for `text` kind only (the ref still names the stored item). */
2960
+ text: z.string().optional()
2961
+ }).strict().refine((item) => item.text === void 0 || item.kind === "text", { message: "inline text is only valid on kind=text" });
2962
+ DerivedMessageContent = OriginalMessageContent.innerType().extend({
2963
+ derivedFrom: contentRefSchema,
2964
+ /** The transform capability id (registry or extension id). */
2965
+ transform: z.string().min(1),
2966
+ /** A7 actor ref of the producer. */
2967
+ producer: z.string().min(1),
2968
+ lossiness: z.enum(LOSSINESS)
2969
+ }).strict().refine((item) => item.text === void 0 || item.kind === "text", { message: "inline text is only valid on kind=text" }).refine((item) => parseContentRef(item.derivedFrom)?.principalId === parseContentRef(item.ref)?.principalId, {
2970
+ message: "a derived item must derive from content of the same principal"
2971
+ });
2972
+ z.union([DerivedMessageContent, OriginalMessageContent]);
2973
+ }
2974
+ });
2975
+ var STEP_KINDS, OWNERSHIP_MODES, WorkflowStep, ConnectorWorkflow;
2976
+ var init_onboarding = __esm({
2977
+ "../contracts/src/channel-connector/onboarding.ts"() {
2978
+ STEP_KINDS = [
2979
+ "FORM",
2980
+ "SECRET",
2981
+ "OAUTH",
2982
+ "APP_INSTALL",
2983
+ "RESOURCE_SELECT",
2984
+ "RESOURCE_CREATE",
2985
+ "REGISTER",
2986
+ "SUBMIT_APPROVAL",
2987
+ "WAIT",
2988
+ "ACTION_REQUIRED",
2989
+ "GATEWAY_CONNECT",
2990
+ "PROBE",
2991
+ "ACTIVATE"
2992
+ ];
2993
+ OWNERSHIP_MODES = ["platform_managed", "customer_owned"];
2994
+ new Set(OWNERSHIP_MODES);
2995
+ new Set(STEP_KINDS);
2996
+ WorkflowStep = z.object({
2997
+ id: z.string().min(1),
2998
+ kind: z.enum(STEP_KINDS),
2999
+ title: z.string().min(1),
3000
+ /** JSON Schema for FORM/SECRET inputs — schema-driven, no connector UI code. */
3001
+ schema: z.record(z.unknown()).optional(),
3002
+ /** Credential role for SECRET/OAUTH outputs. */
3003
+ role: z.string().optional(),
3004
+ approvalKind: z.string().optional(),
3005
+ resourceKind: z.string().optional(),
3006
+ mode: z.string().optional(),
3007
+ dependsOn: z.array(z.string()).default([]),
3008
+ optional: z.boolean().default(false),
3009
+ /**
3010
+ * Ownership modes this step applies to (absent = all). A REQUIRED step out of the run's mode is ownership-skipped
3011
+ * (`OUT_OF_MODE`), which is distinct from `optional` — the engine must skip it regardless of `optional` (#306 §9).
3012
+ */
3013
+ ownership: z.array(z.enum(OWNERSHIP_MODES)).optional(),
3014
+ repeatable: z.boolean().default(false),
3015
+ producesChannels: z.array(z.string()).default([])
3016
+ }).strict();
3017
+ ConnectorWorkflow = z.object({
3018
+ version: z.string().min(1),
3019
+ steps: z.array(WorkflowStep).min(1),
3020
+ /** What ACTIVATE requires (condition type ids). */
3021
+ activationGates: z.array(z.string()),
3022
+ /** The single step that re-establishes credentials. */
3023
+ reauth: z.object({ stepId: z.string() }).strict(),
3024
+ /** Steps that adopt existing resources on transfer / reconnect. */
3025
+ relink: z.object({ stepIds: z.array(z.string()) }).strict()
3026
+ }).strict();
3027
+ }
3028
+ });
3029
+ var EXCLUSIVITY, TIMEOUT_EFFECTS, RECONCILE_BY, ABSENCE_PROOF_METHODS, DUPLICATE_COSTS, RESOURCE_KIND_RE, ResourceKindDeclaration;
3030
+ var init_external_resources = __esm({
3031
+ "../contracts/src/channel-connector/external-resources.ts"() {
3032
+ init_ids2();
3033
+ EXCLUSIVITY = ["exclusive", "per_connection", "shared"];
3034
+ TIMEOUT_EFFECTS = ["UNKNOWN", "NO_EFFECT", "PROVIDER_ACCEPTS", "PROVIDER_REJECTS"];
3035
+ RECONCILE_BY = ["provider_ref", "correlation_tag", "listing", "none"];
3036
+ ABSENCE_PROOF_METHODS = ["provider_ref", "correlation_tag", "listing"];
3037
+ DUPLICATE_COSTS = ["low", "high"];
3038
+ RESOURCE_KIND_RE = /^[a-z0-9]+(\.[a-z0-9_]+)+$/;
3039
+ ResourceKindDeclaration = z.object({
3040
+ kind: z.string().regex(RESOURCE_KIND_RE, "resource kind must be a namespaced dotted id"),
3041
+ /** Customer-facing name of the kind (presentation only; the connector owns its provider vocabulary). */
3042
+ label: z.string().min(1).optional(),
3043
+ parentKinds: z.array(z.string()).default([]),
3044
+ /** Channels this kind enables (registry ids), if any. */
3045
+ channels: z.array(z.string()).default([]),
3046
+ /** An installation target (guild, workspace, tenant, business account). */
3047
+ isContainer: z.boolean().default(false),
3048
+ /** A channel identity (sender). */
3049
+ isSender: z.boolean().default(false),
3050
+ isRegulatory: z.boolean().default(false),
3051
+ exclusivity: z.enum(EXCLUSIVITY),
3052
+ /** Provider-side sharing cap, manifest DATA never a core constant (e.g. Meta WABA = 2). */
3053
+ maxHolders: z.number().int().positive().optional(),
3054
+ /** doc 06 `reconcile { by, absenceProof?, duplicateCost }`. */
3055
+ reconcile: z.object({
3056
+ by: z.enum(RECONCILE_BY),
3057
+ absenceProof: z.object({ minAgeSec: z.number().int().nonnegative(), method: z.enum(ABSENCE_PROOF_METHODS) }).strict().optional(),
3058
+ duplicateCost: z.enum(DUPLICATE_COSTS)
3059
+ }).strict(),
3060
+ /** doc 06 `release { destructive }`. */
3061
+ release: z.object({ destructive: z.boolean() }).strict().default({ destructive: false }),
3062
+ /** Timeout semantics for the effect that creates this kind (D-23). */
3063
+ createTimeoutEffect: z.enum(TIMEOUT_EFFECTS).default("UNKNOWN")
3064
+ }).strict().superRefine((decl, ctx) => {
3065
+ if (decl.isContainer && decl.exclusivity === "exclusive") {
3066
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: `container kind ${decl.kind} cannot be exclusive` });
3067
+ }
3068
+ if (decl.maxHolders !== void 0 && decl.exclusivity === "exclusive" && decl.maxHolders !== 1) {
3069
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: `exclusive kind ${decl.kind} cannot declare maxHolders > 1` });
3070
+ }
3071
+ if (decl.exclusivity === "shared" && (decl.isSender || decl.isContainer)) {
3072
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: `shared kind ${decl.kind} is referenced by value and cannot be a sender or container` });
3073
+ }
3074
+ if (decl.reconcile.absenceProof && decl.reconcile.by === "none") {
3075
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: `kind ${decl.kind}: absenceProof needs a reconcile method other than none` });
3076
+ }
3077
+ });
3078
+ }
3079
+ });
3080
+
3081
+ // ../contracts/src/channel-connector/approvals.ts
3082
+ var APPROVAL_KINDS;
3083
+ var init_approvals = __esm({
3084
+ "../contracts/src/channel-connector/approvals.ts"() {
3085
+ APPROVAL_KINDS = [
3086
+ "business_verification",
3087
+ "campaign_vetting",
3088
+ "toll_free_verification",
3089
+ "app_review",
3090
+ "privileged_permission_review",
3091
+ "admin_consent",
3092
+ "display_name_review",
3093
+ "template_review",
3094
+ "agent_launch",
3095
+ "brand_verification"
3096
+ ];
3097
+ new Set(APPROVAL_KINDS);
3098
+ }
3099
+ });
3100
+ var DEPLOYMENT_MODELS, CREDENTIAL_STRENGTHS, CREDENTIAL_ROLES, INSTALLATION_KINDS, AUTHORIZATION_METHODS, AUTHORIZATION_CAPABILITIES, AuthorizationMethodDeclaration, INBOUND_AUTH_AVAILABILITY, InboundAuthMethodDeclaration, MutationApprovalDeclaration, ReadinessGateSchema, FormatsSchema, LimitsSchema, DescriptorOverride, DecisionRequestDeclaration;
3101
+ var init_manifest = __esm({
3102
+ "../contracts/src/channel-connector/manifest.ts"() {
3103
+ init_identity();
3104
+ init_approvals();
3105
+ init_capability_state();
3106
+ init_channels();
3107
+ init_content_kinds();
3108
+ init_external_resources();
3109
+ init_ids2();
3110
+ init_onboarding();
3111
+ init_operations();
3112
+ init_onboarding();
3113
+ DEPLOYMENT_MODELS = ["per_tenant_app", "platform_app", "both"];
3114
+ CREDENTIAL_STRENGTHS = ["ephemeral", "brokered", "exported_static"];
3115
+ CREDENTIAL_ROLES = ["primary", "signing", "webhook", "refresh", "app_signing", "oauth_client", "partner"];
3116
+ INSTALLATION_KINDS = ["guild", "user", "workspace", "org", "tenant", "team", "business_account", "chat"];
3117
+ new Set(INSTALLATION_KINDS);
3118
+ z.string().regex(/^cred:[a-z0-9_-]+\/[^\s]+$/, "A5 credential ref `cred:<store>/<path>`");
3119
+ AUTHORIZATION_METHODS = ["oauth", "api_key", "api_secret", "account_token", "bot_token", "delegated_app", "installation_token", "certificate", "other"];
3120
+ AUTHORIZATION_CAPABILITIES = ["discover", "provision", "send", "receive", "manage_resources", "manage_compliance", "manage_webhooks", "manage_installations", "manage_accounts"];
3121
+ AuthorizationMethodDeclaration = z.object({
3122
+ id: z.string().min(1),
3123
+ method: z.enum(AUTHORIZATION_METHODS),
3124
+ credentialRoles: z.array(z.enum(CREDENTIAL_ROLES)).min(1),
3125
+ capabilities: z.array(z.enum(AUTHORIZATION_CAPABILITIES)).default([]),
3126
+ scope: z.enum(["account", "container"]).optional()
3127
+ }).strict();
3128
+ INBOUND_AUTH_AVAILABILITY = ["ga", "beta", "private_beta", "unavailable"];
3129
+ InboundAuthMethodDeclaration = z.object({
3130
+ id: z.string().min(1),
3131
+ verifier: z.string().min(1),
3132
+ credentialRole: z.enum(CREDENTIAL_ROLES),
3133
+ compatibleWith: z.array(z.object({ authMethodId: z.string().min(1), reusesAuthCredential: z.boolean() }).strict()).default([]),
3134
+ availability: z.enum(INBOUND_AUTH_AVAILABILITY).default("ga")
3135
+ }).strict();
3136
+ MutationApprovalDeclaration = z.object({
3137
+ effect: z.string().min(1),
3138
+ approval: z.enum(["explicit", "implicit"]),
3139
+ capturePriorState: z.boolean().default(false)
3140
+ }).strict();
3141
+ ReadinessGateSchema = z.object({
3142
+ condition: z.string().min(1),
3143
+ class: z.enum(CONDITION_CLASSES),
3144
+ blocks: z.union([z.literal("*"), z.array(z.string())]),
3145
+ ttlSec: z.number().int().nonnegative(),
3146
+ evidence: z.enum(["configuration", "runtime"]).optional()
3147
+ }).strict();
3148
+ FormatsSchema = z.record(
3149
+ z.enum(CONTENT_KINDS),
3150
+ z.object({ mime: z.array(z.string()).optional(), variants: z.array(z.string()).optional() }).strict()
3151
+ );
3152
+ LimitsSchema = z.object({
3153
+ textChars: z.number().int().positive().optional(),
3154
+ textBytes: z.number().int().positive().optional(),
3155
+ mediaBytes: z.number().int().positive().optional(),
3156
+ segments: z.boolean().optional(),
3157
+ perSecond: z.number().positive().optional(),
3158
+ perRecipientPerSecond: z.number().positive().optional(),
3159
+ formats: FormatsSchema.optional(),
3160
+ note: z.string().optional()
3161
+ }).strict();
3162
+ DescriptorOverride = z.object({
3163
+ version: z.string().min(1).optional(),
3164
+ operations: z.array(z.string()).optional(),
3165
+ direction: z.object({ inbound: z.boolean(), outbound: z.boolean() }).strict().optional(),
3166
+ initiation: z.object({ agentInitiated: z.enum(AGENT_INITIATION), userInitiated: z.boolean(), firstContactRule: z.string() }).strict().optional(),
3167
+ windows: z.array(
3168
+ z.object({
3169
+ kind: z.literal("reply"),
3170
+ durationSec: z.number().int().positive(),
3171
+ opensOn: z.enum(["inbound_message", "user_action"]),
3172
+ outside: z.enum(WINDOW_OUTSIDE)
3173
+ }).strict()
3174
+ ).optional(),
3175
+ templates: z.object({ required: z.enum(TEMPLATE_REQUIREMENT), reviewed: z.boolean(), categories: z.array(z.string()).optional() }).strict().optional(),
3176
+ surfaces: z.array(z.enum(SURFACES)).optional(),
3177
+ messageTypes: z.array(z.enum(CONTENT_KINDS)).optional(),
3178
+ limits: LimitsSchema.optional(),
3179
+ receipts: z.array(z.enum(RECEIPT_KINDS)).optional(),
3180
+ edits: z.object({ edit: z.boolean(), delete: z.boolean(), reactions: z.boolean(), commands: z.boolean() }).strict().optional(),
3181
+ transport: z.array(z.enum(TRANSPORT_KINDS)).optional(),
3182
+ identity: z.object({ peerIdKind: z.string(), peerIdScope: z.enum(PEER_ID_SCOPES) }).strict().optional(),
3183
+ requiredPermissions: z.array(z.string()).optional(),
3184
+ registration: z.array(z.object({ kind: z.string(), blocks: z.array(z.string()) }).strict()).optional(),
3185
+ readinessGates: z.array(ReadinessGateSchema).optional()
3186
+ }).strict();
3187
+ DecisionRequestDeclaration = z.object({
3188
+ kind: z.string().min(1),
3189
+ responseDeadlineSec: z.number().int().positive(),
3190
+ /** The provider's law when we stay silent. */
3191
+ defaultOutcome: z.enum(TIMEOUT_EFFECTS)
3192
+ }).strict();
3193
+ z.object({
3194
+ /** A7 `<publisher>/<name>`. */
3195
+ id: z.string().min(3),
3196
+ /** A7: semver. */
3197
+ version: z.string().regex(SEMVER_RE, "A7: plugin versions are semver"),
3198
+ displayName: z.string().min(1),
3199
+ publisher: z.string().min(1),
3200
+ /** A STABLE https URL of the provider's own console (the `provider_console` remediation carries it); absent ⇒ remediation has no link. */
3201
+ consoleUrl: z.string().url().optional(),
3202
+ /** The publisher's reverse-domain namespace (`com.acme`) under which every contributed extension id must sit; verified in ch. 05 (D-9). Absent for first-party connectors, which use registry ids only. */
3203
+ namespace: z.string().regex(NAMESPACE_RE, "namespace must be <root>.<label>").optional(),
3204
+ deployment: z.enum(DEPLOYMENT_MODELS),
3205
+ credentialStrength: z.enum(CREDENTIAL_STRENGTHS),
3206
+ /** Roles this connector's connections store refs under. */
3207
+ credentialRoles: z.array(z.enum(CREDENTIAL_ROLES)).default(["primary"]),
3208
+ /** Channels served, each with its descriptor diff over the baseline. */
3209
+ channels: z.array(z.object({ channel: z.string().min(1), descriptor: DescriptorOverride.default({}) }).strict()).min(1),
3210
+ /** Operations the connector implements (registry ids or its own extension ops). */
3211
+ operations: z.array(z.string()).min(1),
3212
+ /** Extension operations this connector defines — each MUST declare a mode. */
3213
+ extensionOperations: z.array(z.object({ id: z.string(), mode: z.enum(OPERATION_MODES), summary: z.string() }).strict()).default([]),
3214
+ onboarding: ConnectorWorkflow,
3215
+ resourceKinds: z.array(ResourceKindDeclaration).default([]),
3216
+ installationKinds: z.array(z.string()).default([]),
3217
+ transports: z.array(z.enum(TRANSPORT_KINDS)).min(1),
3218
+ /** doc 05 §2: transports the provider makes mutually exclusive (e.g. webhook vs long_poll). */
3219
+ transportsExclusive: z.array(z.array(z.enum(TRANSPORT_KINDS)).min(2)).default([]),
3220
+ /** Verifier id the ingress runs before parsing; required with the `webhook` transport. */
3221
+ webhookVerifier: z.string().min(1).optional(),
3222
+ /** Reconcile handler ids by resource kind (doc 02). */
3223
+ reconcilers: z.array(z.object({ resourceKind: z.string(), handler: z.string().min(1) }).strict()).default([]),
3224
+ /** Inbound decision requests with provider-defined defaults (D-23). */
3225
+ decisionRequests: z.array(DecisionRequestDeclaration).default([]),
3226
+ /** Identifier kinds contributed to the ch. 00 B-1 vocabulary. */
3227
+ identifierKinds: z.array(z.string()).default([]),
3228
+ /** Approval kinds used (registry or extension ids). */
3229
+ approvalKinds: z.array(z.string()).default([]),
3230
+ /** Attribution key names used under `platform_app` (doc 08 §4a step 3). */
3231
+ attributionKeys: z.array(z.string()).default([]),
3232
+ /** Quarantine window for PENDING_ATTRIBUTION (D-21), seconds. */
3233
+ pendingAttributionTtlSec: z.number().int().positive().default(7 * 86400),
3234
+ metadata: z.record(z.unknown()).default({}),
3235
+ /** Which relationship ownerships this connector serves (#306 §A/C1). A managed-capable connector MUST declare it explicitly; the default serves only the common OAuth-BYO connector. */
3236
+ ownershipModes: z.array(z.enum(OWNERSHIP_MODES)).min(1).default(["customer_owned"]),
3237
+ /** Authorization methods the connector supports (#306 §4/C3). */
3238
+ authorizationMethods: z.array(AuthorizationMethodDeclaration).default([]),
3239
+ /** Inbound-event authentication strategies (#306 §4/C4); `webhookVerifier` is the single-strategy shorthand, normalized by `resolveInboundAuthMethods`. */
3240
+ inboundAuthMethods: z.array(InboundAuthMethodDeclaration).default([]),
3241
+ /** Provider effects that require an explicit decision, keyed on stable effect ids (#306 L5/C5). */
3242
+ mutationApprovals: z.array(MutationApprovalDeclaration).default([])
3243
+ }).strict();
3244
+ }
3245
+ });
3246
+
3247
+ // ../contracts/src/channel-connector/index.ts
3248
+ var init_channel_connector = __esm({
3249
+ "../contracts/src/channel-connector/index.ts"() {
3250
+ init_ids2();
3251
+ init_operations();
3252
+ init_content_kinds();
3253
+ init_capability_state();
3254
+ init_onboarding();
3255
+ init_external_resources();
3256
+ init_approvals();
3257
+ init_channels();
3258
+ init_manifest();
3259
+ }
3260
+ });
3261
+ function isSafeMonitoringHref(href) {
3262
+ if (href !== href.trim() || UNSAFE_CHARS.test(href) || href.includes("\\") || /%2e|%2f|%5c|%25/i.test(href)) return false;
3263
+ if (href.startsWith("/")) {
3264
+ if (href.startsWith("//")) return false;
3265
+ try {
3266
+ const u = new URL(href, "https://dashboard.invalid");
3267
+ return u.origin === "https://dashboard.invalid" && u.pathname.startsWith("/dashboard/") && u.pathname + u.search + u.hash === href;
3268
+ } catch {
3269
+ return false;
3270
+ }
3271
+ }
3272
+ if (!href.startsWith("https://")) return false;
3273
+ try {
3274
+ const u = new URL(href);
3275
+ return u.protocol === "https:" && !u.username && !u.password && Boolean(u.hostname);
3276
+ } catch {
3277
+ return false;
3278
+ }
3279
+ }
3280
+ var Label, Short, Body, Iso, MonitoringAuthor, MonitoringTone, UNSAFE_CHARS, MonitoringBlock, PresentedCommunication, MonitoringChannelId, MonitoringChannelRef, MonitoredPeer, MonitoredConversationSummary, MonitoredCommunication, MonitoringUnlinkedHistory, MONITORING_DIRECTIONS, MONITORING_STATES, MONITORING_MAX_ID_FILTERS, MONITORING_MAX_CHANNEL_FILTERS;
3281
+ var init_monitoring = __esm({
3282
+ "../contracts/src/monitoring.ts"() {
3283
+ Label = z.string().trim().min(1).max(60);
3284
+ Short = z.string().max(300);
3285
+ Body = z.string().max(4e3);
3286
+ Iso = z.string().datetime();
3287
+ MonitoringAuthor = z.enum(["peer", "agent", "system"]);
3288
+ MonitoringTone = z.enum(["ok", "warn", "bad", "info", "neutral"]);
3289
+ UNSAFE_CHARS = /[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u180e\u200b-\u200f\u202a-\u202e\u2060-\u2064\u2066-\u2069\ufeff\u{e0000}-\u{e007f}]/u;
3290
+ new RegExp(UNSAFE_CHARS.source, "gu");
3291
+ MonitoringBlock = z.discriminatedUnion("type", [
3292
+ /** One line of the conversation: what was said, by whom. */
3293
+ z.object({ type: z.literal("message"), author: MonitoringAuthor, text: Body, at: Iso.optional() }).strict(),
3294
+ /** Channel facts worth seeing at a glance (Discord server/channel, SMS from/to, call duration). */
3295
+ z.object({ type: z.literal("facts"), items: z.array(z.object({ label: Label, value: Short }).strict()).min(1).max(12) }).strict(),
3296
+ /** The communication's state in words, with a tone. */
3297
+ z.object({ type: z.literal("status"), label: Label, tone: MonitoringTone }).strict(),
3298
+ /** Somewhere to go: the provider's own view of it (https) or a deeper dashboard page. */
3299
+ z.object({ type: z.literal("link"), label: Label, href: z.string().max(2e3).refine(isSafeMonitoringHref, "href must be https or a /dashboard/ path") }).strict(),
3300
+ /** A dated lifecycle fact (sent, delivered, answered, ended). */
3301
+ z.object({ type: z.literal("event"), label: z.string().trim().min(1).max(120), at: Iso }).strict(),
3302
+ /** A spoken exchange, turn by turn. */
3303
+ z.object({
3304
+ type: z.literal("transcript"),
3305
+ turns: z.array(z.object({ author: MonitoringAuthor, text: Body, offsetMs: z.number().int().nonnegative().optional() }).strict()).max(500)
3306
+ }).strict()
3307
+ ]);
3308
+ PresentedCommunication = z.object({
3309
+ /** One line for lists ("Asked: What is 2 + 2?"). */
3310
+ summary: z.string().max(140).optional(),
3311
+ /**
3312
+ * Something new starts at this communication — e.g. the agent began a fresh session with no memory of what came
3313
+ * before. The page draws a labelled divider before it. Additive to v1: a reader that ignores it loses nothing else.
3314
+ */
3315
+ boundary: z.object({ label: z.string().trim().min(1).max(80) }).strict().optional(),
3316
+ blocks: z.array(MonitoringBlock).max(50)
3317
+ }).strict();
3318
+ MonitoringChannelId = z.string().regex(/^[a-z][a-z0-9_.-]{0,39}$/);
3319
+ MonitoringChannelRef = z.object({ id: z.string(), label: z.string() }).strict();
3320
+ MonitoredPeer = z.object({
3321
+ kind: z.string().nullable(),
3322
+ address: z.string().nullable(),
3323
+ /** From the channel's presenter (`describePeer`); null when it has no better name than the address. */
3324
+ displayName: z.string().nullable()
3325
+ }).strict();
3326
+ MonitoredConversationSummary = z.object({
3327
+ id: z.string().uuid(),
3328
+ status: z.string(),
3329
+ openedAt: z.string(),
3330
+ lastActivityAt: z.string(),
3331
+ agent: z.object({ id: z.string(), name: z.string() }).strict().nullable(),
3332
+ peer: MonitoredPeer,
3333
+ channels: z.array(MonitoringChannelRef),
3334
+ communications: z.number().int().nonnegative(),
3335
+ /** The latest INBOUND communication's one-liner (else the latest), as its channel presents it. */
3336
+ summary: z.string().nullable(),
3337
+ /** Something in it failed (a message not delivered, a call that failed). */
3338
+ failed: z.boolean(),
3339
+ /** A call in it is still in progress. */
3340
+ live: z.boolean()
3341
+ }).strict();
3342
+ MonitoredCommunication = z.object({
3343
+ id: z.string().uuid(),
3344
+ type: z.string(),
3345
+ channel: MonitoringChannelRef,
3346
+ direction: z.string(),
3347
+ status: z.string(),
3348
+ startedAt: z.string(),
3349
+ endedAt: z.string().nullable(),
3350
+ /** Which presenter shaped it (`vl/discord`, `vl/sms`, `generic`), for support. */
3351
+ presenter: z.string(),
3352
+ presented: PresentedCommunication
3353
+ }).strict();
3354
+ MonitoringUnlinkedHistory = z.object({ calls: z.boolean(), messages: z.boolean() }).strict();
3355
+ z.object({
3356
+ blocksVersion: z.number().int(),
3357
+ scope: z.string(),
3358
+ conversations: z.array(MonitoredConversationSummary),
3359
+ /** Opaque; pass back as `cursor` for the next (older) page. Null on the last page. */
3360
+ nextCursor: z.string().nullable(),
3361
+ /** History from before a channel was linked to the ledger (not shown here, 🔒 control-plane F8/F12). */
3362
+ unlinkedHistory: MonitoringUnlinkedHistory
3363
+ }).strict();
3364
+ z.object({
3365
+ blocksVersion: z.number().int(),
3366
+ scope: z.string(),
3367
+ conversation: MonitoredConversationSummary,
3368
+ /** Oldest first. The page holds the LATEST communications; `earlierCursor` fetches the ones before them. */
3369
+ communications: z.array(MonitoredCommunication),
3370
+ earlierCursor: z.string().nullable()
3371
+ }).strict();
3372
+ z.object({
3373
+ scope: z.string(),
3374
+ /** Channels a presenter currently shows, named by that presenter. */
3375
+ channels: z.array(MonitoringChannelRef),
3376
+ /** The Lines (numbers, bots) conversations in this scope arrived on. */
3377
+ lines: z.array(z.object({ id: z.string().uuid(), label: z.string() }).strict())
3378
+ }).strict();
3379
+ MONITORING_DIRECTIONS = ["inbound", "outbound"];
3380
+ MONITORING_STATES = ["live", "failed", "ok"];
3381
+ MONITORING_MAX_ID_FILTERS = 50;
3382
+ MONITORING_MAX_CHANNEL_FILTERS = 20;
3383
+ z.object({
3384
+ scope: z.enum(["env:dev", "env:stg", "env:prd"]).optional(),
3385
+ limit: z.coerce.number().int().min(1).max(100).optional(),
3386
+ cursor: z.string().max(200).optional(),
3387
+ from: z.string().datetime().optional(),
3388
+ to: z.string().datetime().optional(),
3389
+ channel: z.array(MonitoringChannelId).max(MONITORING_MAX_CHANNEL_FILTERS).optional(),
3390
+ direction: z.array(z.enum(MONITORING_DIRECTIONS)).max(2).optional(),
3391
+ line: z.array(z.string().uuid()).max(MONITORING_MAX_ID_FILTERS).optional(),
3392
+ agent: z.array(z.string().uuid()).max(MONITORING_MAX_ID_FILTERS).optional(),
3393
+ status: z.array(z.enum(MONITORING_STATES)).max(3).optional(),
3394
+ customer: z.string().trim().max(100).optional()
3395
+ }).strict();
3396
+ }
3397
+ });
3398
+ var WalletRefusalCode, WalletMode, WalletInvoiceStatus, WalletInvoice, WalletEntryKind, WalletEntry, TOPUP_MIN_USD, TOPUP_MAX_USD;
3399
+ var init_wallet = __esm({
3400
+ "../contracts/src/wallet.ts"() {
3401
+ WalletRefusalCode = z.enum([
3402
+ // Prepaid balance (or an invoiced credit limit) cannot cover the action.
3403
+ "insufficient_balance",
3404
+ // The wallet is frozen after a card dispute, until an operator unfreezes it.
3405
+ "account_frozen",
3406
+ // An invoice is unpaid past its 7 days' grace (B-19): new calls, messages and answers are refused; inbound calls are
3407
+ // still answered. Paying the invoice restores the workspace.
3408
+ "account_suspended",
3409
+ // The action's reservation already closed (e.g. a call ended or expired) — it cannot be re-admitted.
3410
+ "hold_closed"
3411
+ ]);
3412
+ z.object({
3413
+ error: WalletRefusalCode,
3414
+ code: WalletRefusalCode,
3415
+ availableUsd: z.string(),
3416
+ needUsd: z.string(),
3417
+ message: z.string()
3418
+ });
3419
+ WalletMode = z.enum(["prepaid", "invoiced"]);
3420
+ z.object({
3421
+ mode: WalletMode,
3422
+ status: z.enum(["active", "frozen"]),
3423
+ // Decimal strings, six places (the ledger's micro-dollar precision).
3424
+ balanceUsd: z.string(),
3425
+ // Reserved for calls in progress.
3426
+ heldUsd: z.string(),
3427
+ // balance − held (+ credit limit when invoiced); null = unlimited.
3428
+ availableUsd: z.string().nullable(),
3429
+ creditLimitUsd: z.string().nullable(),
3430
+ // A card saved by the first top-up — what auto top-up charges.
3431
+ cardOnFile: z.boolean(),
3432
+ autoTopup: z.object({ enabled: z.boolean(), thresholdUsd: z.string(), amountUsd: z.string() }),
3433
+ // B-19: 'past_due' = an invoice is overdue (a pay-now banner); 'suspended' = paused for it (or by an operator).
3434
+ standing: z.enum(["good", "past_due", "suspended"]),
3435
+ // Why it is suspended: an invoice unpaid past grace (paying restores it) or an operator (only support lifts it).
3436
+ suspension: z.enum(["unpaid_invoice", "operator"]).nullable(),
3437
+ // The oldest overdue invoice, when there is one.
3438
+ overdueInvoice: z.object({ amountUsd: z.string(), dueAt: z.string(), payUrl: z.string().nullable() }).nullable(),
3439
+ // Invoiced wallets: what the next invoice holds so far (charges since the last invoice); null when prepaid.
3440
+ uninvoicedUsd: z.string().nullable()
3441
+ });
3442
+ WalletInvoiceStatus = z.enum(["draft", "open", "paid", "void", "uncollectible"]);
3443
+ WalletInvoice = z.object({
3444
+ id: z.string(),
3445
+ // 'final' = issued when the workspace moved to prepaid.
3446
+ kind: z.enum(["monthly", "final"]),
3447
+ periodStart: z.string(),
3448
+ periodEnd: z.string(),
3449
+ amountUsd: z.string(),
3450
+ status: WalletInvoiceStatus,
3451
+ issuedAt: z.string().nullable(),
3452
+ dueAt: z.string().nullable(),
3453
+ paidAt: z.string().nullable(),
3454
+ // Stripe's hosted page to view and pay it (null until issued).
3455
+ payUrl: z.string().nullable()
3456
+ });
3457
+ z.object({ invoices: z.array(WalletInvoice) });
3458
+ WalletEntryKind = z.enum(["grant", "topup", "usage", "refund", "refund_reversal", "dispute", "dispute_reinstated", "adjustment", "invoice_payment"]);
3459
+ WalletEntry = z.object({
3460
+ id: z.string(),
3461
+ kind: WalletEntryKind,
3462
+ // Signed: a credit is positive, a debit negative.
3463
+ amountUsd: z.string(),
3464
+ balanceAfterUsd: z.string(),
3465
+ // The usage row a debit charged (usage entries only).
3466
+ usageEventId: z.string().nullable(),
3467
+ note: z.string().nullable(),
3468
+ createdAt: z.string()
3469
+ });
3470
+ z.object({
3471
+ entries: z.array(WalletEntry),
3472
+ // Pass as `before` for the next (older) page; null when there is none.
3473
+ nextBefore: z.string().nullable()
3474
+ });
3475
+ TOPUP_MIN_USD = 10;
3476
+ TOPUP_MAX_USD = 500;
3477
+ z.object({ amountUsd: z.number().int().min(TOPUP_MIN_USD).max(TOPUP_MAX_USD) });
3478
+ z.object({
3479
+ enabled: z.boolean(),
3480
+ thresholdUsd: z.number().min(1).max(TOPUP_MAX_USD),
3481
+ amountUsd: z.number().int().min(TOPUP_MIN_USD).max(TOPUP_MAX_USD)
3482
+ });
3483
+ }
3484
+ });
2105
3485
 
2106
3486
  // ../contracts/src/index.ts
2107
3487
  var init_src = __esm({
@@ -2114,12 +3494,15 @@ var init_src = __esm({
2114
3494
  init_agents();
2115
3495
  init_process_state();
2116
3496
  init_call_events();
3497
+ init_text_turns();
2117
3498
  init_call_plan();
2118
3499
  init_memory();
2119
3500
  init_participants();
2120
3501
  init_forms();
2121
3502
  init_consultation();
2122
3503
  init_agent_config();
3504
+ init_agent_versions();
3505
+ init_providers();
2123
3506
  init_model_catalog();
2124
3507
  init_evaluations();
2125
3508
  init_tests();
@@ -2134,6 +3517,7 @@ var init_src = __esm({
2134
3517
  init_flow_validate();
2135
3518
  init_flow_builtins();
2136
3519
  init_compliance();
3520
+ init_sms_keywords();
2137
3521
  init_call_recording();
2138
3522
  init_outbound();
2139
3523
  init_connection();
@@ -2148,20 +3532,27 @@ var init_src = __esm({
2148
3532
  init_playbooks();
2149
3533
  init_playbook_versions();
2150
3534
  init_playbook_compile();
3535
+ init_effect_stack();
3536
+ init_engine();
3537
+ init_identity();
3538
+ init_interaction();
3539
+ init_channel_connector();
3540
+ init_monitoring();
3541
+ init_wallet();
2151
3542
  }
2152
3543
  });
2153
3544
 
2154
3545
  // src/runtime/graph/interpolate.ts
2155
- function interpolate(template, scope) {
3546
+ function interpolate(template, scope2) {
2156
3547
  if (!template || template.indexOf("{{") === -1) return template;
2157
3548
  return template.replace(TOKEN, (_match, expr) => {
2158
- const value = resolvePath(scope, expr.trim());
3549
+ const value = resolvePath(scope2, expr.trim());
2159
3550
  return value === void 0 || value === null ? "" : String(value);
2160
3551
  });
2161
3552
  }
2162
- function resolvePath(scope, path) {
3553
+ function resolvePath(scope2, path) {
2163
3554
  const parts = path.split(".").map((p) => p.trim()).filter((p) => p.length > 0);
2164
- let current = scope;
3555
+ let current = scope2;
2165
3556
  for (const part of parts) {
2166
3557
  if (current === null || typeof current !== "object") return void 0;
2167
3558
  const obj = current;
@@ -2211,6 +3602,20 @@ var init_builtins = __esm({
2211
3602
  init_src();
2212
3603
  }
2213
3604
  });
3605
+ function createChatClient(config = {}) {
3606
+ return new OpenAI({
3607
+ ...config.apiKey ? { apiKey: config.apiKey } : {},
3608
+ ...config.baseURL ? { baseURL: config.baseURL } : {},
3609
+ ...config.timeoutMs ? { timeout: config.timeoutMs } : {}
3610
+ });
3611
+ }
3612
+ var init_dist = __esm({
3613
+ "../llm-client/dist/index.js"() {
3614
+ }
3615
+ });
3616
+ function runWithOpenAIScope(client, fn) {
3617
+ return scope.run(client, fn);
3618
+ }
2214
3619
  function defaultOpenAIOptions() {
2215
3620
  if (!credential) return {};
2216
3621
  return {
@@ -2218,17 +3623,283 @@ function defaultOpenAIOptions() {
2218
3623
  ...credential.baseURL ? { baseURL: credential.baseURL } : {}
2219
3624
  };
2220
3625
  }
3626
+ function internalChatClient(opts) {
3627
+ const cred = defaultOpenAIOptions();
3628
+ return createChatClient({
3629
+ ...cred.apiKey ? { apiKey: cred.apiKey } : {},
3630
+ ...cred.baseURL ? { baseURL: cred.baseURL } : {},
3631
+ ...{}
3632
+ });
3633
+ }
2221
3634
  function defaultOpenAI() {
2222
- return cached ??= new OpenAI(defaultOpenAIOptions());
3635
+ return scope.getStore() ?? (cached ??= internalChatClient());
2223
3636
  }
2224
3637
  function hasOpenAiKey() {
2225
- return credential !== null || Boolean(process.env["OPENAI_API_KEY"]);
3638
+ return scope.getStore() !== void 0 || credential !== null || Boolean(process.env["OPENAI_API_KEY"]);
2226
3639
  }
2227
- var credential, cached;
3640
+ var credential, cached, scope;
2228
3641
  var init_openai_default = __esm({
2229
3642
  "src/runtime/openai-default.ts"() {
3643
+ init_dist();
2230
3644
  credential = null;
2231
3645
  cached = null;
3646
+ scope = new AsyncLocalStorage();
3647
+ }
3648
+ });
3649
+ var init_ssrf = __esm({
3650
+ "src/brain/ssrf.ts"() {
3651
+ }
3652
+ });
3653
+
3654
+ // src/brain/serialize.ts
3655
+ var init_serialize = __esm({
3656
+ "src/brain/serialize.ts"() {
3657
+ }
3658
+ });
3659
+
3660
+ // src/brain/transport-callback.ts
3661
+ var init_transport_callback = __esm({
3662
+ "src/brain/transport-callback.ts"() {
3663
+ init_serialize();
3664
+ }
3665
+ });
3666
+
3667
+ // src/brain/connector-llm.ts
3668
+ var init_connector_llm = __esm({
3669
+ "src/brain/connector-llm.ts"() {
3670
+ init_serialize();
3671
+ }
3672
+ });
3673
+ var init_wrap = __esm({
3674
+ "src/providers/wrap.ts"() {
3675
+ }
3676
+ });
3677
+
3678
+ // src/providers/index.ts
3679
+ async function importOptional(spec, hint) {
3680
+ try {
3681
+ return await import(spec);
3682
+ } catch {
3683
+ throw new Error(`${hint}: "${spec}" is not installed. Add it with: pnpm add ${spec}`);
3684
+ }
3685
+ }
3686
+ function withCreds(opts, creds2) {
3687
+ if (creds2?.apiKey) opts["apiKey"] = creds2.apiKey;
3688
+ if (creds2?.baseURL) opts["baseURL"] = creds2.baseURL;
3689
+ return opts;
3690
+ }
3691
+ var deepgram, openai, anthropic, cartesia, elevenlabs, assemblyai, google, silero;
3692
+ var init_providers2 = __esm({
3693
+ "src/providers/index.ts"() {
3694
+ init_ssrf();
3695
+ init_transport_callback();
3696
+ init_connector_llm();
3697
+ init_wrap();
3698
+ deepgram = {
3699
+ stt(options = {}) {
3700
+ return async () => {
3701
+ const dg = await importOptional("@voicelayer/agents-plugin-deepgram", "deepgram.stt");
3702
+ return new dg.STT(
3703
+ withCreds(
3704
+ { model: options.model ?? "nova-3", language: options.language ?? "en-US" },
3705
+ options
3706
+ )
3707
+ );
3708
+ };
3709
+ },
3710
+ tts(options = {}) {
3711
+ return async () => {
3712
+ const dg = await importOptional("@voicelayer/agents-plugin-deepgram", "deepgram.tts");
3713
+ return new dg.TTS(
3714
+ withCreds({ model: options.model ?? "aura-2-harmonia-en" }, options)
3715
+ );
3716
+ };
3717
+ }
3718
+ };
3719
+ openai = {
3720
+ llm(options = {}) {
3721
+ return async () => {
3722
+ const oa = await importOptional("@voicelayer/agents-plugin-openai", "openai.llm");
3723
+ return new oa.LLM(
3724
+ withCreds({ model: options.model ?? "gpt-4o-mini" }, options)
3725
+ );
3726
+ };
3727
+ },
3728
+ tts(options = {}) {
3729
+ return async () => {
3730
+ const oa = await importOptional("@voicelayer/agents-plugin-openai", "openai.tts");
3731
+ const opts = { model: options.model ?? "tts-1" };
3732
+ if (options.voice) opts["voice"] = options.voice;
3733
+ if (options.instructions) opts["instructions"] = options.instructions;
3734
+ return new oa.TTS(withCreds(opts, options));
3735
+ };
3736
+ },
3737
+ // Speech-to-speech via the OpenAI Realtime API. Returns a RealtimeProvider
3738
+ // that the SDK uses in place of the stt/llm/tts pipeline.
3739
+ realtime(options = {}) {
3740
+ return async () => {
3741
+ const oa = await importOptional("@voicelayer/agents-plugin-openai", "openai.realtime");
3742
+ const opts = {};
3743
+ if (options.model) opts["model"] = options.model;
3744
+ if (options.voice) opts["voice"] = options.voice;
3745
+ return new oa.realtime.RealtimeModel(withCreds(opts, options));
3746
+ };
3747
+ }
3748
+ };
3749
+ anthropic = {
3750
+ llm(options = {}) {
3751
+ return async () => {
3752
+ const an = await importOptional("@livekit/agents-plugin-anthropic", "anthropic.llm");
3753
+ return new an.LLM(
3754
+ withCreds({ model: options.model ?? "claude-sonnet-4" }, options)
3755
+ );
3756
+ };
3757
+ }
3758
+ };
3759
+ cartesia = {
3760
+ tts(options = {}) {
3761
+ return async () => {
3762
+ const ct = await importOptional("@voicelayer/agents-plugin-cartesia", "cartesia.tts");
3763
+ return new ct.TTS(options);
3764
+ };
3765
+ },
3766
+ stt(options = {}) {
3767
+ return async () => {
3768
+ const ct = await importOptional("@voicelayer/agents-plugin-cartesia", "cartesia.stt");
3769
+ const opts = {};
3770
+ if (options.model) opts["model"] = options.model;
3771
+ if (options.language) opts["language"] = options.language;
3772
+ return new ct.STT(withCreds(opts, options));
3773
+ };
3774
+ }
3775
+ };
3776
+ elevenlabs = {
3777
+ tts(options = {}) {
3778
+ return async () => {
3779
+ const el = await importOptional("@voicelayer/agents-plugin-elevenlabs", "elevenlabs.tts");
3780
+ const opts = {};
3781
+ if (options.voice) opts["voiceId"] = options.voice;
3782
+ if (options.model) opts["model"] = options.model;
3783
+ return new el.TTS(withCreds(opts, options));
3784
+ };
3785
+ }
3786
+ };
3787
+ assemblyai = {
3788
+ stt(options = {}) {
3789
+ return async () => {
3790
+ const aai = await importOptional("@voicelayer/agents-plugin-assemblyai", "assemblyai.stt");
3791
+ const opts = {};
3792
+ if (options.language) opts["language"] = options.language;
3793
+ return new aai.STT(withCreds(opts, options));
3794
+ };
3795
+ }
3796
+ };
3797
+ google = {
3798
+ llm(options = {}) {
3799
+ return async () => {
3800
+ const g = await importOptional("@voicelayer/agents-plugin-google", "google.llm");
3801
+ return new g.LLM(
3802
+ withCreds({ model: options.model ?? "gemini-2.0-flash" }, options)
3803
+ );
3804
+ };
3805
+ },
3806
+ realtime(options = {}) {
3807
+ return async () => {
3808
+ const g = await importOptional("@voicelayer/agents-plugin-google", "google.realtime");
3809
+ const opts = {};
3810
+ if (options.model) opts["model"] = options.model;
3811
+ if (options.voice) opts["voice"] = options.voice;
3812
+ return new g.beta.realtime.RealtimeModel(withCreds(opts, options));
3813
+ };
3814
+ }
3815
+ };
3816
+ silero = {
3817
+ vad() {
3818
+ return async () => {
3819
+ const sil = await importOptional("@voicelayer/agents-plugin-silero", "silero.vad");
3820
+ return await sil.VAD.load();
3821
+ };
3822
+ }
3823
+ };
3824
+ }
3825
+ });
3826
+
3827
+ // src/providers/registry.ts
3828
+ function entry(id, bits) {
3829
+ const identity = providerIdentity(id);
3830
+ if (!identity) throw new Error(`PROVIDER_REGISTRY: no provider identity in contracts for '${id}'`);
3831
+ return { name: identity.id, capabilities: identity.capabilities, ...bits };
3832
+ }
3833
+ var model, lang, voice, creds;
3834
+ var init_registry = __esm({
3835
+ "src/providers/registry.ts"() {
3836
+ init_providers2();
3837
+ init_src();
3838
+ model = (o) => o.model ? { model: o.model } : {};
3839
+ lang = (o) => o.language ? { language: o.language } : {};
3840
+ voice = (o) => o.voice ? { voice: o.voice } : {};
3841
+ creds = (o) => o.creds ?? {};
3842
+ ({
3843
+ deepgram: entry("deepgram", {
3844
+ failureClass: "vendor-api",
3845
+ engines: ["livekit"],
3846
+ stt: (o) => deepgram.stt({ ...model(o), ...lang(o), ...creds(o) }),
3847
+ // Deepgram folds the voice into the model id (aura-2-<name>-en): wire config
3848
+ // passes a voice, code config passes a model — both land as `model`.
3849
+ tts: (o) => {
3850
+ const m = o.voice ?? o.model;
3851
+ return deepgram.tts({ ...m ? { model: m } : {}, ...creds(o) });
3852
+ }
3853
+ }),
3854
+ openai: entry("openai", {
3855
+ failureClass: "vendor-api",
3856
+ engines: ["livekit", "text"],
3857
+ llm: (o) => openai.llm({ ...model(o), ...creds(o) }),
3858
+ tts: (o) => openai.tts({ ...voice(o), ...model(o), ...creds(o) }),
3859
+ realtime: (o) => openai.realtime({ ...model(o), ...voice(o), ...creds(o) })
3860
+ }),
3861
+ anthropic: entry("anthropic", {
3862
+ failureClass: "vendor-api",
3863
+ engines: ["livekit", "text"],
3864
+ llm: (o) => anthropic.llm({ ...model(o), ...creds(o) })
3865
+ }),
3866
+ google: entry("google", {
3867
+ failureClass: "vendor-api",
3868
+ engines: ["livekit", "text"],
3869
+ llm: (o) => google.llm({ ...model(o), ...creds(o) }),
3870
+ realtime: (o) => google.realtime({ ...model(o), ...voice(o), ...creds(o) })
3871
+ }),
3872
+ cartesia: entry("cartesia", {
3873
+ failureClass: "vendor-api",
3874
+ engines: ["livekit"],
3875
+ stt: (o) => cartesia.stt({ ...model(o), ...lang(o), ...creds(o) }),
3876
+ tts: (o) => cartesia.tts({ ...voice(o), ...model(o), ...creds(o) })
3877
+ }),
3878
+ elevenlabs: entry("elevenlabs", {
3879
+ failureClass: "vendor-api",
3880
+ engines: ["livekit"],
3881
+ tts: (o) => elevenlabs.tts({ ...voice(o), ...model(o), ...creds(o) })
3882
+ }),
3883
+ assemblyai: entry("assemblyai", {
3884
+ failureClass: "vendor-api",
3885
+ engines: ["livekit"],
3886
+ stt: (o) => assemblyai.stt({ ...lang(o), ...creds(o) })
3887
+ // no model knob
3888
+ }),
3889
+ silero: entry("silero", {
3890
+ failureClass: "local",
3891
+ engines: ["livekit"],
3892
+ vad: () => silero.vad()
3893
+ })
3894
+ });
3895
+ }
3896
+ });
3897
+
3898
+ // src/providers/llm.ts
3899
+ var init_llm = __esm({
3900
+ "src/providers/llm.ts"() {
3901
+ init_openai_default();
3902
+ init_registry();
2232
3903
  }
2233
3904
  });
2234
3905
 
@@ -2241,6 +3912,7 @@ function dotProduct(a, b) {
2241
3912
  }
2242
3913
  function createOpenAIEmbedder() {
2243
3914
  const cache = /* @__PURE__ */ new Map();
3915
+ const inflight = /* @__PURE__ */ new Map();
2244
3916
  const available = () => hasOpenAiKey();
2245
3917
  function remember(key, vec) {
2246
3918
  if (cache.size >= CACHE_MAX) {
@@ -2259,24 +3931,31 @@ function createOpenAIEmbedder() {
2259
3931
  if (!available() || !key) return null;
2260
3932
  const hit = cache.get(key);
2261
3933
  if (hit) return hit;
2262
- const controller = new AbortController();
2263
- const timer = setTimeout(() => controller.abort(), EMBED_TIMEOUT_MS);
2264
- try {
2265
- const res = await defaultOpenAI().embeddings.create(
2266
- { model: EMBED_MODEL, input: key },
2267
- { signal: controller.signal }
2268
- );
2269
- const vec = res.data[0]?.embedding;
2270
- if (!Array.isArray(vec)) return null;
2271
- return remember(key, vec);
2272
- } catch (err) {
2273
- console.warn("[graph] embed failed; router will fall back to judge", {
2274
- err: err instanceof Error ? err.message : String(err)
2275
- });
2276
- return null;
2277
- } finally {
2278
- clearTimeout(timer);
2279
- }
3934
+ const pending = inflight.get(key);
3935
+ if (pending) return pending;
3936
+ const request = (async () => {
3937
+ const controller = new AbortController();
3938
+ const timer = setTimeout(() => controller.abort(), EMBED_TIMEOUT_MS);
3939
+ try {
3940
+ const res = await defaultOpenAI().embeddings.create(
3941
+ { model: EMBED_MODEL, input: key },
3942
+ { signal: controller.signal }
3943
+ );
3944
+ const vec = res.data[0]?.embedding;
3945
+ if (!Array.isArray(vec)) return null;
3946
+ return remember(key, vec);
3947
+ } catch (err) {
3948
+ graphWarn("embed failed; router will fall back to judge", {
3949
+ err: err instanceof Error ? err.message : String(err)
3950
+ });
3951
+ return null;
3952
+ } finally {
3953
+ clearTimeout(timer);
3954
+ inflight.delete(key);
3955
+ }
3956
+ })();
3957
+ inflight.set(key, request);
3958
+ return request;
2280
3959
  },
2281
3960
  prewarm(text) {
2282
3961
  void this.embed(text).catch(() => {
@@ -2292,7 +3971,8 @@ function getEmbedder() {
2292
3971
  var EMBED_MODEL, ROUTE_THRESHOLD, EMBED_TIMEOUT_MS, CACHE_MAX, singleton, override;
2293
3972
  var init_embedder = __esm({
2294
3973
  "src/runtime/graph/embedder.ts"() {
2295
- init_openai_default();
3974
+ init_log();
3975
+ init_llm();
2296
3976
  EMBED_MODEL = process.env["VOICELAYER_EMBED_MODEL"] ?? "text-embedding-3-small";
2297
3977
  ROUTE_THRESHOLD = Number(process.env["VL_FLOW_ROUTE_THRESHOLD"] ?? "0.62");
2298
3978
  EMBED_TIMEOUT_MS = Number(process.env["VL_FLOW_EMBED_TIMEOUT_MS"] ?? "2500");
@@ -2354,7 +4034,7 @@ async function routeByEmbedding(nl, ctx) {
2354
4034
  if (!best || score > best.score) best = { to: edge.to, score };
2355
4035
  }
2356
4036
  if (best && best.score >= ROUTE_THRESHOLD) {
2357
- console.log("[graph] route: embedding", { to: best.to, score: Number(best.score.toFixed(3)) });
4037
+ graphLog("route: embedding", { to: best.to, score: Number(best.score.toFixed(3)) });
2358
4038
  return best.to;
2359
4039
  }
2360
4040
  return null;
@@ -2380,11 +4060,21 @@ async function evaluateCondition(condition, ctx) {
2380
4060
  function splitTop(text, op) {
2381
4061
  return text.split(op);
2382
4062
  }
4063
+ function conditionReadsSlots(condition) {
4064
+ const text = condition.trim();
4065
+ if (!text) return false;
4066
+ return splitTop(text, "||").flatMap((part) => splitTop(part, "&&")).map((atom) => atom.trim()).some((atom) => {
4067
+ const lower = atom.toLowerCase();
4068
+ if (lower === "true" || lower === "always" || lower === "false" || lower === "never") return false;
4069
+ if (TOOL_ATOM.test(atom) || NODE_ATOM.test(atom) || DTMF_ATOM.test(atom)) return false;
4070
+ return OP_ATOM.test(atom) || SLOT_PRESENCE_ATOM.test(atom) || BARE_ATOM.test(atom);
4071
+ });
4072
+ }
2383
4073
  function evaluateDeterministic(atom, ctx) {
2384
4074
  const lower = atom.toLowerCase();
2385
4075
  if (lower === "true" || lower === "always") return true;
2386
4076
  if (lower === "false" || lower === "never") return false;
2387
- const toolMatch = atom.match(/^tool[.:]([A-Za-z0-9_-]+)(?:\.([\w.]+))?\s*(==|!=)?\s*(.*)$/);
4077
+ const toolMatch = atom.match(TOOL_ATOM);
2388
4078
  if (toolMatch) {
2389
4079
  const [, id, path, op, rawVal] = toolMatch;
2390
4080
  const result = ctx.tools[id];
@@ -2398,7 +4088,7 @@ function evaluateDeterministic(atom, ctx) {
2398
4088
  const eq = String(actual ?? "").toLowerCase() === (rawVal ?? "").trim().toLowerCase();
2399
4089
  return op === "!=" ? !eq : eq;
2400
4090
  }
2401
- const nodeMatch = atom.match(/^node:([A-Za-z0-9_-]+)(?:\.([\w.]+))?\s*(==|!=)?\s*(.*)$/);
4091
+ const nodeMatch = atom.match(NODE_ATOM);
2402
4092
  if (nodeMatch) {
2403
4093
  if (!ctx.nodeResults) return void 0;
2404
4094
  const [, id, path, op, rawVal] = nodeMatch;
@@ -2410,14 +4100,12 @@ function evaluateDeterministic(atom, ctx) {
2410
4100
  const eq = String(actual ?? "").toLowerCase() === (rawVal ?? "").trim().toLowerCase();
2411
4101
  return op === "!=" ? !eq : eq;
2412
4102
  }
2413
- const dtmfMatch = atom.match(/^dtmf:\s*(.+)$/i);
4103
+ const dtmfMatch = atom.match(DTMF_ATOM);
2414
4104
  if (dtmfMatch) {
2415
4105
  if (ctx.lastDtmf === void 0) return void 0;
2416
4106
  return ctx.lastDtmf === dtmfMatch[1].trim();
2417
4107
  }
2418
- const opMatch = atom.match(
2419
- /^(!?)\s*(?:slot:)?(.+?)\s*(==|!=|=|contains|starts_with|greater_than|less_than|>=|<=|>|<)\s*(.+)$/i
2420
- );
4108
+ const opMatch = atom.match(OP_ATOM);
2421
4109
  if (opMatch) {
2422
4110
  const [, neg, nameRaw, op, valRaw] = opMatch;
2423
4111
  const raw = resolvePath(ctx.slots, nameRaw.trim());
@@ -2439,14 +4127,14 @@ function evaluateDeterministic(atom, ctx) {
2439
4127
  else res = value === target;
2440
4128
  return neg ? !res : res;
2441
4129
  }
2442
- const slotPres = atom.match(/^(!?)\s*slot:(.+?)\s*(present|empty)?$/i);
4130
+ const slotPres = atom.match(SLOT_PRESENCE_ATOM);
2443
4131
  if (slotPres) {
2444
4132
  const [, neg, nameRaw, kw] = slotPres;
2445
4133
  const present = hasSlot(ctx.slots, nameRaw.trim());
2446
4134
  const res = (kw ?? "").toLowerCase() === "empty" ? !present : present;
2447
4135
  return neg ? !res : res;
2448
4136
  }
2449
- const bare = atom.match(/^(!?)\s*([\w.-]+)\s*(present|empty)?$/i);
4137
+ const bare = atom.match(BARE_ATOM);
2450
4138
  if (bare) {
2451
4139
  const [, neg, name, kw] = bare;
2452
4140
  const present = hasSlot(ctx.slots, name);
@@ -2457,13 +4145,27 @@ function evaluateDeterministic(atom, ctx) {
2457
4145
  }
2458
4146
  async function pickOption(options, ctx, hint) {
2459
4147
  if (options.length === 0) return null;
2460
- const collected = Object.entries(ctx.slots).filter(([, v]) => v !== void 0 && v !== null && v !== "").map(([k, v]) => `${k.trim()}: ${String(v)}`).join("; ");
4148
+ return judgeSettled(ctx, (collected) => pickWith(options, collected, ctx.lastUtterance, hint));
4149
+ }
4150
+ function collectedOf(slots) {
4151
+ return Object.entries(slots).filter(([, v]) => v !== void 0 && v !== null && v !== "").map(([k, v]) => `${k.trim()}: ${String(v)}`).join("; ");
4152
+ }
4153
+ async function judgeSettled(ctx, run) {
4154
+ const before = collectedOf(ctx.slots);
4155
+ if (!ctx.settle) return run(before);
4156
+ const [speculative, fresh] = await Promise.all([run(before), ctx.settle()]);
4157
+ const after = collectedOf(fresh);
4158
+ if (after === before) return speculative;
4159
+ graphLog("judge: capture changed context; re-judging");
4160
+ return run(after);
4161
+ }
4162
+ async function pickWith(options, collected, lastUtterance, hint) {
2461
4163
  const list = options.map((o, i) => `${i + 1}. ${o.text}`).join("\n");
2462
4164
  const prompt = [
2463
4165
  "Pick which outcome best matches the current state of this voice conversation.",
2464
4166
  ...hint && hint.trim() ? [`What decides the path: ${hint.trim()}`] : [],
2465
4167
  `Information collected: ${collected || "(none)"}`,
2466
- `The caller most recently said: "${ctx.lastUtterance ?? ""}"`,
4168
+ `The caller most recently said: "${lastUtterance ?? ""}"`,
2467
4169
  "Outcomes:",
2468
4170
  list,
2469
4171
  'Answer with ONLY the outcome number, or "none" if nothing clearly matches.'
@@ -2488,7 +4190,7 @@ async function pickOption(options, ctx, hint) {
2488
4190
  if (!Number.isInteger(n) || n < 1 || n > options.length) return null;
2489
4191
  return options[n - 1]?.id ?? null;
2490
4192
  } catch (err) {
2491
- console.warn("[graph] option pick failed; treating as undecided", {
4193
+ graphWarn("option pick failed; treating as undecided", {
2492
4194
  err: err instanceof Error ? err.message : String(err)
2493
4195
  });
2494
4196
  return null;
@@ -2497,12 +4199,18 @@ async function pickOption(options, ctx, hint) {
2497
4199
  }
2498
4200
  }
2499
4201
  async function judge(condition, ctx) {
2500
- const collected = Object.entries(ctx.slots).filter(([, v]) => v !== void 0 && v !== null && v !== "").map(([k, v]) => `${k.trim()}: ${String(v)}`).join("; ");
4202
+ return judgeSettled(ctx, (collected) => judgeWith(condition, collected, ctx.lastUtterance));
4203
+ }
4204
+ async function judgeWith(condition, collected, lastUtterance) {
2501
4205
  const prompt = [
2502
4206
  "You judge whether a condition about an ongoing voice conversation is currently TRUE.",
2503
4207
  `Information collected: ${collected || "(none)"}`,
2504
- `The caller most recently said: "${ctx.lastUtterance ?? ""}"`,
4208
+ `The caller most recently said: "${lastUtterance ?? ""}"`,
2505
4209
  `Condition: ${condition}`,
4210
+ // Observed live: "Keystone Arena." (a site name) was judged to satisfy
4211
+ // "caller described a network problem". The judge must not fill in what
4212
+ // the caller probably meant — only what they actually said counts.
4213
+ 'Judge only from what the caller actually said and the information collected \u2014 never infer or assume what they probably meant. If what they said does not itself establish the condition, answer "no".',
2506
4214
  'Answer strictly "yes" or "no".'
2507
4215
  ].join("\n");
2508
4216
  const controller = new AbortController();
@@ -2522,7 +4230,7 @@ async function judge(condition, ctx) {
2522
4230
  );
2523
4231
  return (res.choices[0]?.message?.content ?? "").trim().toLowerCase().startsWith("y");
2524
4232
  } catch (err) {
2525
- console.warn("[graph] condition judge failed; treating as false", {
4233
+ graphWarn("condition judge failed; treating as false", {
2526
4234
  condition,
2527
4235
  err: err instanceof Error ? err.message : String(err)
2528
4236
  });
@@ -2531,13 +4239,20 @@ async function judge(condition, ctx) {
2531
4239
  clearTimeout(timer);
2532
4240
  }
2533
4241
  }
2534
- var JUDGE_TIMEOUT_MS;
4242
+ var JUDGE_TIMEOUT_MS, TOOL_ATOM, NODE_ATOM, DTMF_ATOM, OP_ATOM, SLOT_PRESENCE_ATOM, BARE_ATOM;
2535
4243
  var init_conditions = __esm({
2536
4244
  "src/runtime/graph/conditions.ts"() {
2537
- init_openai_default();
4245
+ init_log();
4246
+ init_llm();
2538
4247
  init_interpolate();
2539
4248
  init_embedder();
2540
4249
  JUDGE_TIMEOUT_MS = 4e3;
4250
+ TOOL_ATOM = /^tool[.:]([A-Za-z0-9_-]+)(?:\.([\w.]+))?\s*(==|!=)?\s*(.*)$/;
4251
+ NODE_ATOM = /^node:([A-Za-z0-9_-]+)(?:\.([\w.]+))?\s*(==|!=)?\s*(.*)$/;
4252
+ DTMF_ATOM = /^dtmf:\s*(.+)$/i;
4253
+ OP_ATOM = /^(!?)\s*(?:slot:)?(.+?)\s*(==|!=|=|contains|starts_with|greater_than|less_than|>=|<=|>|<)\s*(.+)$/i;
4254
+ SLOT_PRESENCE_ATOM = /^(!?)\s*slot:(.+?)\s*(present|empty)?$/i;
4255
+ BARE_ATOM = /^(!?)\s*([\w.-]+)\s*(present|empty)?$/i;
2541
4256
  }
2542
4257
  });
2543
4258
 
@@ -2580,7 +4295,7 @@ async function resolveBranch(branches, ctx, opts) {
2580
4295
  if (!best || score > best.score) best = { id: b.id, score };
2581
4296
  }
2582
4297
  if (best && best.score >= ROUTE_THRESHOLD) {
2583
- console.log("[graph] branch: embedding", { id: best.id, score: Number(best.score.toFixed(3)) });
4298
+ graphLog("branch: embedding", { id: best.id, score: Number(best.score.toFixed(3)) });
2584
4299
  return best.id;
2585
4300
  }
2586
4301
  }
@@ -2595,6 +4310,7 @@ async function resolveBranch(branches, ctx, opts) {
2595
4310
  var ELSE_BRANCH_ID;
2596
4311
  var init_branch_resolve = __esm({
2597
4312
  "src/runtime/graph/branch-resolve.ts"() {
4313
+ init_log();
2598
4314
  init_conditions();
2599
4315
  init_embedder();
2600
4316
  ELSE_BRANCH_ID = "else";
@@ -2694,13 +4410,18 @@ function scopeFor(state, deps) {
2694
4410
  function str(value) {
2695
4411
  return typeof value === "string" ? value : "";
2696
4412
  }
4413
+ async function settledSlots(deps) {
4414
+ await deps.utterances.settle?.();
4415
+ return deps.processRt.getData();
4416
+ }
2697
4417
  function evalCtxOf(state, deps) {
2698
4418
  return {
2699
4419
  slots: deps.processRt.getData(),
2700
4420
  tools: state.toolResults,
2701
4421
  nodeResults: state.nodeResults,
2702
4422
  ...state.lastUtterance !== void 0 ? { lastUtterance: state.lastUtterance } : {},
2703
- ...state.lastDtmf !== void 0 ? { lastDtmf: state.lastDtmf } : {}
4423
+ ...state.lastDtmf !== void 0 ? { lastDtmf: state.lastDtmf } : {},
4424
+ settle: () => settledSlots(deps)
2704
4425
  };
2705
4426
  }
2706
4427
  function deterministicEdgeTarget(edges, ctx) {
@@ -2715,7 +4436,7 @@ async function speak(text, deps, opts) {
2715
4436
  const allowInterruptions = opts?.allowInterruptions === true;
2716
4437
  if (deps.ctx.adapter) {
2717
4438
  const r = await deps.ctx.adapter.say(text, { allowInterruptions });
2718
- console.log("[graph] said", { chars: text.length, interrupted: r.interrupted });
4439
+ graphLog("said", { chars: text.length, interrupted: r.interrupted });
2719
4440
  return;
2720
4441
  }
2721
4442
  const session = deps.ctx.livekit.session;
@@ -2725,12 +4446,12 @@ async function speak(text, deps, opts) {
2725
4446
 
2726
4447
  ${text}`
2727
4448
  }).waitForPlayout();
2728
- console.log("[graph] said (realtime)", { chars: text.length });
4449
+ graphLog("said (realtime)", { chars: text.length });
2729
4450
  return;
2730
4451
  }
2731
4452
  const handle = session.say(text, { allowInterruptions });
2732
4453
  await handle.waitForPlayout();
2733
- console.log("[graph] said", { chars: text.length, interrupted: handle.interrupted });
4454
+ graphLog("said", { chars: text.length, interrupted: handle.interrupted });
2734
4455
  }
2735
4456
  async function genReply(deps, instructions) {
2736
4457
  if (deps.ctx.adapter) {
@@ -2739,10 +4460,13 @@ async function genReply(deps, instructions) {
2739
4460
  }
2740
4461
  await deps.ctx.livekit.session.generateReply({ instructions }).waitForPlayout();
2741
4462
  }
2742
- async function nextTurn(deps, reprompt) {
4463
+ async function nextTurn(deps, reprompt, opts) {
2743
4464
  let reprompted = false;
2744
4465
  for (; ; ) {
2745
- const evt = await deps.utterances.next({ timeoutMs: NO_INPUT_MS });
4466
+ const evt = await deps.utterances.next({
4467
+ timeoutMs: NO_INPUT_MS,
4468
+ ...opts?.deferCapture ? { deferCapture: true } : {}
4469
+ });
2746
4470
  if (evt.kind === "hangup") return { result: { kind: "end" } };
2747
4471
  if (evt.kind === "control") return { result: evt.result };
2748
4472
  if (evt.kind === "timeout") {
@@ -2816,6 +4540,7 @@ function menuLabelOf(label) {
2816
4540
  var ASK_MAX_CLARIFY, LLM_MAX_TURNS, HUB_MAX_TURNS, NO_INPUT_MS, sayExecutor, YES_WORDS, NO_WORDS, confirmExecutor, askExecutor, gateExecutor, endExecutor, handoffExecutor, llmExecutor, toolExecutor, ragExecutor, codeExecutor, decisionExecutor, MENU_MAX_INPUTS, menuExecutor, classifyExecutor, routerExecutor, playbookExecutor, setExecutor, MAX_SUBFLOW_DEPTH, subflowExecutor, passthroughExecutor, EXECUTORS;
2817
4541
  var init_executors = __esm({
2818
4542
  "src/runtime/graph/executors.ts"() {
4543
+ init_log();
2819
4544
  init_src();
2820
4545
  init_interpolate();
2821
4546
  init_builtins();
@@ -2948,6 +4673,7 @@ var init_executors = __esm({
2948
4673
  const policy = historyPolicyOf(node);
2949
4674
  const maxTurns = hub ? HUB_MAX_TURNS : LLM_MAX_TURNS;
2950
4675
  const base = interpolate(str(node.config.instructions), scopeFor(state, deps)) || "Help the caller using everything collected so far. Keep replies short and natural.";
4676
+ const deferCapture = !conditional.some((e) => conditionReadsSlots(e.condition));
2951
4677
  for (let turn = 0; turn < maxTurns; turn++) {
2952
4678
  const collected = formatCollected(deps.processRt.getData());
2953
4679
  const parts = [base];
@@ -2960,25 +4686,19 @@ ${collected}`);
2960
4686
  () => guardedGenerate(node, state, deps, parts.join("\n\n"), base)
2961
4687
  );
2962
4688
  if (!hub && !terminal && conditional.length === 0) return { kind: "advance" };
2963
- const t = await nextTurn(deps);
4689
+ const t = await nextTurn(deps, void 0, { deferCapture });
2964
4690
  if ("result" in t) return t.result;
2965
4691
  state.lastUtterance = t.text;
2966
4692
  state.lastDtmf = t.dtmf;
2967
4693
  if (conditional.length > 0) {
2968
- const target = await selectEdge(out, {
2969
- slots: deps.processRt.getData(),
2970
- tools: state.toolResults,
2971
- nodeResults: state.nodeResults,
2972
- lastDtmf: state.lastDtmf,
2973
- lastUtterance: state.lastUtterance
2974
- });
4694
+ const target = await selectEdge(out, evalCtxOf(state, deps));
2975
4695
  if (target) {
2976
- console.log("[graph] llm: transition", { node: node.id, to: target, hub });
4696
+ graphLog("llm: transition", { node: node.id, to: target, hub });
2977
4697
  return { kind: "goto", to: target };
2978
4698
  }
2979
4699
  }
2980
4700
  }
2981
- console.log("[graph] llm: turn cap reached", { node: node.id, hub });
4701
+ graphLog("llm: turn cap reached", { node: node.id, hub });
2982
4702
  return terminal ? { kind: "end" } : { kind: "advance" };
2983
4703
  };
2984
4704
  toolExecutor = async (node, state, deps) => {
@@ -3337,7 +5057,7 @@ ${collected}`);
3337
5057
  const goal = str(cfg.goal);
3338
5058
  let instr = str(cfg.instructions);
3339
5059
  const toolNames = Array.isArray(cfg.tools) ? cfg.tools.filter((x) => typeof x === "string" && x.length > 0) : [];
3340
- let model = str(cfg.model) || void 0;
5060
+ let model2 = str(cfg.model) || void 0;
3341
5061
  let temperature = typeof cfg.temperature === "number" ? cfg.temperature : void 0;
3342
5062
  let libraryExits = [];
3343
5063
  const ref = str(cfg.playbookRef);
@@ -3347,7 +5067,7 @@ ${collected}`);
3347
5067
  if (!instr) instr = resolved.instructions;
3348
5068
  for (const f of resolved.requiredFields) if (!required.includes(f)) required.push(f);
3349
5069
  for (const t of resolved.tools ?? []) if (!toolNames.includes(t)) toolNames.push(t);
3350
- if (!model && resolved.model) model = resolved.model;
5070
+ if (!model2 && resolved.model) model2 = resolved.model;
3351
5071
  if (temperature === void 0 && resolved.temperature !== void 0) temperature = resolved.temperature;
3352
5072
  libraryExits = resolved.exits ?? [];
3353
5073
  }
@@ -3401,7 +5121,7 @@ ${collected}`);
3401
5121
  exits: exitOptions,
3402
5122
  captureVars: required,
3403
5123
  runTool: (name, args) => registry ? registry.run(name, args) : Promise.resolve({ ok: false, status: 0, data: { error: "no tool registry" } }),
3404
- ...model ? { model } : {},
5124
+ ...model2 ? { model: model2 } : {},
3405
5125
  ...temperature !== void 0 ? { temperature } : {}
3406
5126
  });
3407
5127
  } finally {
@@ -3594,18 +5314,18 @@ var interpreter_exports = {};
3594
5314
  __export(interpreter_exports, {
3595
5315
  runFlowGraph: () => runFlowGraph
3596
5316
  });
3597
- function pickEntry(model) {
3598
- return model.entry.length > 0 ? model.entry[0] : null;
5317
+ function pickEntry(model2) {
5318
+ return model2.entry.length > 0 ? model2.entry[0] : null;
3599
5319
  }
3600
- async function selectNext(model, from, result, state, deps) {
5320
+ async function selectNext(model2, from, result, state, deps) {
3601
5321
  if (result.kind === "goto") return result.to;
3602
5322
  if (result.kind === "take") {
3603
- const target = model.selectByPort(from, result.port);
5323
+ const target = model2.selectByPort(from, result.port);
3604
5324
  if (target !== null) return target;
3605
- const def = model.out(from).find((e) => (!e.condition || !e.condition.trim()) && !e.sourceHandle);
5325
+ const def = model2.out(from).find((e) => (!e.condition || !e.condition.trim()) && !e.sourceHandle);
3606
5326
  return def ? def.to : null;
3607
5327
  }
3608
- return selectEdge(model.out(from), {
5328
+ return selectEdge(model2.out(from), {
3609
5329
  slots: deps.processRt.getData(),
3610
5330
  tools: state.toolResults,
3611
5331
  nodeResults: state.nodeResults,
@@ -3614,10 +5334,10 @@ async function selectNext(model, from, result, state, deps) {
3614
5334
  });
3615
5335
  }
3616
5336
  async function runFlowGraph(program, deps) {
3617
- const model = buildGraphModel(program);
5337
+ const model2 = buildGraphModel(program);
3618
5338
  const maxNodeVisits = deps.maxNodeVisits ?? DEFAULT_MAX_NODE_VISITS;
3619
5339
  const maxSteps = deps.maxSteps ?? DEFAULT_MAX_STEPS;
3620
- let cursor = pickEntry(model);
5340
+ let cursor = pickEntry(model2);
3621
5341
  if (cursor === null) return { kind: "completed" };
3622
5342
  const state = { toolResults: {}, nodeResults: {}, cursor };
3623
5343
  const visits = /* @__PURE__ */ new Map();
@@ -3627,7 +5347,7 @@ async function runFlowGraph(program, deps) {
3627
5347
  deps.events.note("flow.loop_guard_tripped", { reason: "max_steps", steps });
3628
5348
  break;
3629
5349
  }
3630
- const node = model.node(cursor);
5350
+ const node = model2.node(cursor);
3631
5351
  if (!node) {
3632
5352
  deps.events.note("flow.missing_node", { nodeId: cursor });
3633
5353
  break;
@@ -3641,7 +5361,7 @@ async function runFlowGraph(program, deps) {
3641
5361
  }
3642
5362
  state.cursor = cursor;
3643
5363
  deps.events.enter(node.id, node.type);
3644
- console.log("[graph] enter", { node: node.id, type: node.type, step: steps });
5364
+ graphLog("enter", { node: node.id, type: node.type, step: steps });
3645
5365
  const executor = EXECUTORS[node.type] ?? passthroughExecutor;
3646
5366
  let result;
3647
5367
  try {
@@ -3650,7 +5370,7 @@ async function runFlowGraph(program, deps) {
3650
5370
  processRt: deps.processRt,
3651
5371
  utterances: deps.utterances,
3652
5372
  events: deps.events,
3653
- model,
5373
+ model: model2,
3654
5374
  ...deps.history ? { history: deps.history } : {},
3655
5375
  ...deps.realtime ? { realtime: true } : {},
3656
5376
  ...deps.resolvers ? { resolvers: deps.resolvers } : {},
@@ -3664,17 +5384,19 @@ async function runFlowGraph(program, deps) {
3664
5384
  return { kind: "ended", nodeId: node.id };
3665
5385
  }
3666
5386
  deps.events.note("node.errored", { nodeId: node.id, error: message });
3667
- const recovery = model.selectByPort(node.id, "error");
5387
+ const recovery = model2.selectByPort(node.id, "error");
3668
5388
  if (recovery !== null) {
3669
5389
  deps.events.traverse(node.id, recovery);
3670
5390
  cursor = recovery;
3671
5391
  continue;
3672
5392
  }
3673
5393
  return { kind: "error", nodeId: node.id, message };
5394
+ } finally {
5395
+ await deps.utterances.settle?.();
3674
5396
  }
3675
5397
  if (result.kind === "end") return { kind: "ended", nodeId: node.id };
3676
5398
  if (result.kind === "handoff") return { kind: "handoff", to: result.to };
3677
- const next = await selectNext(model, cursor, result, state, deps);
5399
+ const next = await selectNext(model2, cursor, result, state, deps);
3678
5400
  if (next !== null) deps.events.traverse(cursor, next);
3679
5401
  cursor = next;
3680
5402
  }
@@ -3683,6 +5405,7 @@ async function runFlowGraph(program, deps) {
3683
5405
  var DEFAULT_MAX_NODE_VISITS, DEFAULT_MAX_STEPS, HUB_MAX_NODE_VISITS;
3684
5406
  var init_interpreter = __esm({
3685
5407
  "src/runtime/graph/interpreter.ts"() {
5408
+ init_log();
3686
5409
  init_graph_model();
3687
5410
  init_executors();
3688
5411
  init_conditions();
@@ -3693,15 +5416,33 @@ var init_interpreter = __esm({
3693
5416
  });
3694
5417
 
3695
5418
  // src/runtime/graph/index.ts
5419
+ init_log();
3696
5420
  init_interpreter();
3697
5421
 
3698
5422
  // src/runtime/graph/utterances.ts
5423
+ init_log();
3699
5424
  var EVENT_UTTERANCE = "user_input_transcribed";
3700
5425
  var EVENT_CLOSE = "close";
3701
- function createUtteranceStream(session, processRt, ctx, rails, prewarm, wantsDtmf) {
5426
+ var DEFAULT_MIN_DELAY_MS = 600;
5427
+ var DEFAULT_MAX_DELAY_MS = 3e3;
5428
+ var VAD_SILENCE_MS = 550;
5429
+ var STT_LAG_MS = 400;
5430
+ function createUtteranceStream(session, processRt, ctx, rails, prewarm, wantsDtmf, onAwaitInput, endpointing) {
3702
5431
  const queue = [];
3703
5432
  let waiter = null;
3704
5433
  let hung = false;
5434
+ let pending = null;
5435
+ const capture = async (text, deferred) => {
5436
+ const t0 = Date.now();
5437
+ try {
5438
+ await processRt.ingest(text, ctx);
5439
+ } catch {
5440
+ }
5441
+ if (deferred) graphLog("capture", { deferred: true, ms: Date.now() - t0 });
5442
+ };
5443
+ const settle = async () => {
5444
+ if (pending) await pending;
5445
+ };
3705
5446
  const deliver = (e) => {
3706
5447
  if (hung && e.kind !== "hangup") return;
3707
5448
  if (waiter) {
@@ -3722,13 +5463,72 @@ function createUtteranceStream(session, processRt, ctx, rails, prewarm, wantsDtm
3722
5463
  };
3723
5464
  const unsubs = [];
3724
5465
  if (ctx.adapter) {
5466
+ let onSettle2 = function() {
5467
+ settle2 = null;
5468
+ settleDue = Number.POSITIVE_INFINITY;
5469
+ const idle = Date.now() - lastActivity;
5470
+ if (speaking && idle < minDelay + STT_LAG_MS) settleAt(minDelay + STT_LAG_MS - idle);
5471
+ else flush();
5472
+ };
3725
5473
  const a = ctx.adapter;
5474
+ const voice2 = a.capabilities?.channel === "voice";
5475
+ const minDelay = Math.max(0, endpointing?.minDelay ?? DEFAULT_MIN_DELAY_MS);
5476
+ const maxDelay = Math.max(minDelay, endpointing?.maxDelay ?? DEFAULT_MAX_DELAY_MS);
5477
+ const parts = [];
5478
+ let speaking = false;
5479
+ let lastActivity = 0;
5480
+ let settle2 = null;
5481
+ let settleDue = Number.POSITIVE_INFINITY;
5482
+ let cap = null;
5483
+ const clearTimers = () => {
5484
+ if (settle2) clearTimeout(settle2);
5485
+ if (cap) clearTimeout(cap);
5486
+ settle2 = null;
5487
+ cap = null;
5488
+ settleDue = Number.POSITIVE_INFINITY;
5489
+ };
5490
+ const flush = () => {
5491
+ clearTimers();
5492
+ const text = parts.splice(0).join(" ").trim();
5493
+ if (!text) return;
5494
+ graphLog("turn", { chars: text.length, quietMs: Date.now() - lastActivity, vadSpeaking: speaking });
5495
+ deliver({ kind: "utterance", text });
5496
+ };
5497
+ const settleAt = (ms) => {
5498
+ if (settle2) clearTimeout(settle2);
5499
+ const wait = Math.max(0, ms);
5500
+ settleDue = Date.now() + wait;
5501
+ settle2 = setTimeout(onSettle2, wait);
5502
+ };
3726
5503
  unsubs.push(
3727
5504
  a.on("user_input", (e) => {
3728
- if (!e.isFinal || hung) return;
3729
- deliver({ kind: "utterance", text: e.transcript });
5505
+ if (hung) return;
5506
+ lastActivity = Date.now();
5507
+ if (!e.isFinal) {
5508
+ if (parts.length > 0) settleAt(minDelay);
5509
+ return;
5510
+ }
5511
+ const text = e.transcript.trim();
5512
+ if (!text) return;
5513
+ if (!voice2 || minDelay === 0) {
5514
+ deliver({ kind: "utterance", text });
5515
+ return;
5516
+ }
5517
+ parts.push(text);
5518
+ if (cap) clearTimeout(cap);
5519
+ cap = setTimeout(flush, maxDelay);
5520
+ settleAt(minDelay);
5521
+ })
5522
+ );
5523
+ unsubs.push(
5524
+ a.on("user_state", (e) => {
5525
+ speaking = e.speaking;
5526
+ if (speaking || parts.length === 0) return;
5527
+ const due = e.at - VAD_SILENCE_MS + minDelay;
5528
+ if (due < settleDue) settleAt(due - Date.now());
3730
5529
  })
3731
5530
  );
5531
+ unsubs.push(clearTimers);
3732
5532
  unsubs.push(a.once("close", onClose));
3733
5533
  if (wantsDtmf) {
3734
5534
  if (typeof ctx.onDtmf === "function") {
@@ -3796,10 +5596,12 @@ function createUtteranceStream(session, processRt, ctx, rails, prewarm, wantsDtm
3796
5596
  } else if (hung) {
3797
5597
  return { kind: "hangup" };
3798
5598
  } else {
5599
+ if (onAwaitInput) onAwaitInput();
3799
5600
  raw = await awaitRaw(opts?.timeoutMs);
3800
5601
  }
3801
5602
  if (raw.kind === "hangup") return { kind: "hangup" };
3802
5603
  if (raw.kind === "timeout") return { kind: "timeout" };
5604
+ await settle();
3803
5605
  if (raw.kind === "dtmf") return { kind: "dtmf", digit: raw.digit };
3804
5606
  if (rails) {
3805
5607
  const outcome = await rails.intercept(raw.text, ctx);
@@ -3807,13 +5609,18 @@ function createUtteranceStream(session, processRt, ctx, rails, prewarm, wantsDtm
3807
5609
  if (outcome.kind === "control") return { kind: "control", result: outcome.result };
3808
5610
  }
3809
5611
  if (prewarm) prewarm(raw.text);
3810
- try {
3811
- await processRt.ingest(raw.text, ctx);
3812
- } catch {
5612
+ if (opts?.deferCapture) {
5613
+ const run = capture(raw.text, true).then(() => {
5614
+ if (pending === run) pending = null;
5615
+ });
5616
+ pending = run;
5617
+ return { kind: "utterance", text: raw.text };
3813
5618
  }
5619
+ await capture(raw.text, false);
3814
5620
  return { kind: "utterance", text: raw.text };
3815
5621
  }
3816
5622
  },
5623
+ settle,
3817
5624
  close() {
3818
5625
  hung = true;
3819
5626
  for (const u of unsubs) u();
@@ -3822,6 +5629,7 @@ function createUtteranceStream(session, processRt, ctx, rails, prewarm, wantsDtm
3822
5629
  }
3823
5630
 
3824
5631
  // src/runtime/graph/history.ts
5632
+ init_log();
3825
5633
  function isConversational(it) {
3826
5634
  return it.type === "message" && (it.role === "user" || it.role === "assistant");
3827
5635
  }
@@ -3861,7 +5669,7 @@ function createHistoryController(agent) {
3861
5669
  next.items = prune(next.items, policy);
3862
5670
  await agent.updateChatCtx(next);
3863
5671
  } catch (err) {
3864
- console.warn("[graph] historyPolicy prune failed; using full history", {
5672
+ graphWarn("historyPolicy prune failed; using full history", {
3865
5673
  policy,
3866
5674
  err: err instanceof Error ? err.message : String(err)
3867
5675
  });
@@ -3874,7 +5682,7 @@ function createHistoryController(agent) {
3874
5682
  try {
3875
5683
  await agent.updateChatCtx(saved);
3876
5684
  } catch (err) {
3877
- console.warn("[graph] historyPolicy restore failed", {
5685
+ graphWarn("historyPolicy restore failed", {
3878
5686
  err: err instanceof Error ? err.message : String(err)
3879
5687
  });
3880
5688
  }
@@ -3911,13 +5719,17 @@ function createGraphEvents(processRt) {
3911
5719
  };
3912
5720
  }
3913
5721
 
5722
+ // src/runtime/graph/rails.ts
5723
+ init_log();
5724
+
3914
5725
  // src/runtime/graph/index.ts
3915
5726
  init_conditions();
3916
5727
  init_branch_resolve();
3917
5728
  init_embedder();
3918
5729
 
3919
5730
  // src/runtime/graph/agent-runner.ts
3920
- init_openai_default();
5731
+ init_log();
5732
+ init_llm();
3921
5733
  process.env["VOICELAYER_LLM_MODEL"] ?? process.env["OPENAI_LLM_MODEL"] ?? "gpt-4o";
3922
5734
  Number(process.env["VL_FLOW_AGENT_TURN_TIMEOUT_MS"] ?? "30000");
3923
5735
  Number(process.env["VL_FLOW_AGENT_MAX_TOOL_CALLS"] ?? "5");
@@ -3930,7 +5742,7 @@ init_graph_model();
3930
5742
  // src/runtime/graph/resolvers.ts
3931
5743
  init_src();
3932
5744
  init_embedder();
3933
- init_openai_default();
5745
+ init_llm();
3934
5746
  Number(process.env["VL_FLOW_CODE_TIMEOUT_MS"] ?? "1000");
3935
5747
  Number(process.env["VL_FLOW_CODE_MEMORY_BYTES"] ?? String(16 * 1024 * 1024));
3936
5748
 
@@ -3951,7 +5763,9 @@ async function runFlowProgram(program, ctx, opts) {
3951
5763
  ctx,
3952
5764
  opts.rails,
3953
5765
  prewarm,
3954
- wantsDtmf
5766
+ wantsDtmf,
5767
+ opts.onAwaitInput,
5768
+ opts.speech?.endpointing
3955
5769
  );
3956
5770
  const history = createHistoryController(opts.agent ?? null);
3957
5771
  try {
@@ -3967,15 +5781,16 @@ async function runFlowProgram(program, ctx, opts) {
3967
5781
  ...opts.resolvers ? { resolvers: opts.resolvers } : {},
3968
5782
  ...opts.busy ? { busy: opts.busy } : {}
3969
5783
  });
3970
- console.log("[graph] outcome", outcome);
5784
+ graphLog("outcome", outcome);
3971
5785
  return outcome;
3972
5786
  } finally {
5787
+ await utterances.settle?.();
3973
5788
  utterances.close();
3974
5789
  }
3975
5790
  }
3976
5791
 
3977
5792
  // src/runtime/process.ts
3978
- init_openai_default();
5793
+ init_llm();
3979
5794
 
3980
5795
  // ../primitives/process-schema/src/index.ts
3981
5796
  init_src();
@@ -4871,6 +6686,10 @@ function createTextSessionAdapter(transport) {
4871
6686
  await transport.send(text);
4872
6687
  return { interrupted: false };
4873
6688
  },
6689
+ async enqueueSay(text) {
6690
+ fire("agent_turn", { text });
6691
+ await transport.send(text);
6692
+ },
4874
6693
  async generateReply(opts) {
4875
6694
  if (transport.generate) {
4876
6695
  const text = await transport.generate(opts?.instructions);
@@ -4949,6 +6768,7 @@ function processSchemaToDefinition(schema) {
4949
6768
  }
4950
6769
 
4951
6770
  // src/runtime/text-session.ts
6771
+ init_openai_default();
4952
6772
  function buildTextContext(adapter, call, processRt) {
4953
6773
  return {
4954
6774
  call: {
@@ -5010,7 +6830,8 @@ function runTextSession(opts) {
5010
6830
  // createUtteranceStream prefers ctx.adapter; the session arg is unused here.
5011
6831
  session: {},
5012
6832
  events,
5013
- ...opts.maxSteps !== void 0 ? { maxSteps: opts.maxSteps } : {}
6833
+ ...opts.maxSteps !== void 0 ? { maxSteps: opts.maxSteps } : {},
6834
+ ...opts.onAwaitInput ? { onAwaitInput: opts.onAwaitInput } : {}
5014
6835
  });
5015
6836
  return {
5016
6837
  sendUserMessage: (text) => handle.push(text),
@@ -5056,12 +6877,12 @@ function definitionFor(opts) {
5056
6877
  }
5057
6878
  async function runTextTranscript(opts) {
5058
6879
  const replies = [];
5059
- const trace = [];
6880
+ const trace2 = [];
5060
6881
  const processRt = opts.processRt ?? createProcessRuntime(definitionFor(opts));
5061
6882
  const session = runTextSession({
5062
6883
  program: opts.program,
5063
6884
  onAgentText: (t) => void replies.push(t),
5064
- onTrace: (e) => void trace.push(e),
6885
+ onTrace: (e) => void trace2.push(e),
5065
6886
  ...processRt ? { processRt } : {},
5066
6887
  ...opts.generate ? { generate: opts.generate } : {},
5067
6888
  ...opts.call ? { call: opts.call } : {},
@@ -5070,7 +6891,57 @@ async function runTextTranscript(opts) {
5070
6891
  for (const m of opts.messages) session.sendUserMessage(m);
5071
6892
  session.end();
5072
6893
  const outcome = await session.result;
5073
- return { replies, outcome, trace };
6894
+ return { replies, outcome, trace: trace2 };
6895
+ }
6896
+ function runLiveTextConversation(opts) {
6897
+ let buffer = [];
6898
+ let settle = null;
6899
+ let parkedPending = false;
6900
+ let ended = false;
6901
+ let outcome;
6902
+ const signal = () => {
6903
+ if (settle) {
6904
+ const s = settle;
6905
+ settle = null;
6906
+ s();
6907
+ } else {
6908
+ parkedPending = true;
6909
+ }
6910
+ };
6911
+ const processRt = opts.processRt ?? createProcessRuntime(definitionFor({ program: opts.program, ...opts.schema ? { schema: opts.schema } : {}, messages: [] }));
6912
+ const session = runTextSession({
6913
+ ...opts,
6914
+ processRt,
6915
+ onAgentText: (t) => {
6916
+ buffer.push(t);
6917
+ return opts.onAgentText?.(t);
6918
+ },
6919
+ onAwaitInput: signal
6920
+ });
6921
+ void session.result.then((o) => {
6922
+ outcome = o;
6923
+ ended = true;
6924
+ signal();
6925
+ });
6926
+ const collect = async () => {
6927
+ if (!parkedPending) await new Promise((res) => settle = res);
6928
+ parkedPending = false;
6929
+ const replies = buffer.slice();
6930
+ buffer = [];
6931
+ return { replies, done: ended, ...ended && outcome ? { outcome } : {} };
6932
+ };
6933
+ return {
6934
+ start: () => collect(),
6935
+ async turn(text) {
6936
+ if (ended) return { replies: [], done: true, ...outcome ? { outcome } : {} };
6937
+ session.sendUserMessage(text);
6938
+ return collect();
6939
+ },
6940
+ async end() {
6941
+ session.end();
6942
+ return session.result;
6943
+ }
6944
+ };
5074
6945
  }
5075
6946
 
5076
- export { runTextSession, runTextTranscript };
6947
+ export { runLiveTextConversation, runTextSession, runTextTranscript, runWithOpenAIScope };