@lunora/agent 0.0.0 → 1.0.0-alpha.1

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.
Files changed (52) hide show
  1. package/LICENSE.md +105 -0
  2. package/README.md +48 -43
  3. package/dist/channels.d.mts +54 -0
  4. package/dist/channels.d.ts +54 -0
  5. package/dist/channels.mjs +181 -0
  6. package/dist/component.d.mts +101 -0
  7. package/dist/component.d.ts +101 -0
  8. package/dist/component.mjs +407 -0
  9. package/dist/inbound.d.mts +29 -0
  10. package/dist/inbound.d.ts +29 -0
  11. package/dist/inbound.mjs +32 -0
  12. package/dist/index.d.mts +740 -0
  13. package/dist/index.d.ts +740 -0
  14. package/dist/index.mjs +18 -0
  15. package/dist/naming.d.mts +17 -0
  16. package/dist/naming.d.ts +17 -0
  17. package/dist/naming.mjs +8 -0
  18. package/dist/packem_shared/AGENT_MODULE-Dnt_-AAT.mjs +24 -0
  19. package/dist/packem_shared/VoiceSessionDO-DLoXsHGF.mjs +297 -0
  20. package/dist/packem_shared/adaptMcpResult-wtNMvLoP.mjs +65 -0
  21. package/dist/packem_shared/agentAsTool-Dt8NlU6k.mjs +94 -0
  22. package/dist/packem_shared/base64-BVwtgRJV.mjs +18 -0
  23. package/dist/packem_shared/braintrustTelemetry-wuGDErob.mjs +47 -0
  24. package/dist/packem_shared/buildModelMessages-BWFigaoo.mjs +69 -0
  25. package/dist/packem_shared/codeTool-CjgJOC9t.mjs +122 -0
  26. package/dist/packem_shared/collectAgenticMemoryTools-QrzpV-WX.mjs +97 -0
  27. package/dist/packem_shared/combineTelemetry-DCyaaWAI.mjs +43 -0
  28. package/dist/packem_shared/common-DAeFCot5.mjs +61 -0
  29. package/dist/packem_shared/compileAgentWorkflow-BxJjHgtD.mjs +55 -0
  30. package/dist/packem_shared/consoleTelemetry-z2MiP1jt.mjs +93 -0
  31. package/dist/packem_shared/createAgentContext-4xJGXNR4.mjs +50 -0
  32. package/dist/packem_shared/createAgentGenerate-BQv9YJ01.mjs +192 -0
  33. package/dist/packem_shared/createDispatchRunner-DSbp_dph-ZHTtxy3f.mjs +69 -0
  34. package/dist/packem_shared/defineAgent-D6maSbVc.mjs +148 -0
  35. package/dist/packem_shared/defineSkill-Ctf_S-rz.mjs +22 -0
  36. package/dist/packem_shared/functionTool-D6lCa2jB.mjs +20 -0
  37. package/dist/packem_shared/graph-component-aoUwO-f0.mjs +216 -0
  38. package/dist/packem_shared/memory-D4FPcBsX.mjs +12 -0
  39. package/dist/packem_shared/normalizeEntityName-CyEEWFkR.mjs +3 -0
  40. package/dist/packem_shared/runAgentLoop-Dhg4ZNvw.mjs +493 -0
  41. package/dist/packem_shared/runVoiceTurn-LnqLvCRR.mjs +211 -0
  42. package/dist/packem_shared/sandboxComponent-DR3pTwBL.mjs +194 -0
  43. package/dist/packem_shared/sentryTelemetry-CgqFJyLO.mjs +36 -0
  44. package/dist/packem_shared/types.d-BWG0uUtX.d.mts +1015 -0
  45. package/dist/packem_shared/types.d-BWG0uUtX.d.ts +1015 -0
  46. package/dist/sandbox.d.mts +185 -0
  47. package/dist/sandbox.d.ts +185 -0
  48. package/dist/sandbox.mjs +109 -0
  49. package/dist/telemetry/index.d.mts +150 -0
  50. package/dist/telemetry/index.d.ts +150 -0
  51. package/dist/telemetry/index.mjs +4 -0
  52. package/package.json +88 -7
package/dist/index.mjs ADDED
@@ -0,0 +1,18 @@
1
+ export { runAgentLoop, splitForCompaction } from './packem_shared/runAgentLoop-Dhg4ZNvw.mjs';
2
+ export { collectAgenticMemoryTools, toSearchResults } from './packem_shared/collectAgenticMemoryTools-QrzpV-WX.mjs';
3
+ export { agentAsTool } from './packem_shared/agentAsTool-Dt8NlU6k.mjs';
4
+ export { codeTool } from './packem_shared/codeTool-CjgJOC9t.mjs';
5
+ export { agentComponent, agentExtension } from './component.mjs';
6
+ export { default as createAgentContext } from './packem_shared/createAgentContext-4xJGXNR4.mjs';
7
+ export { defineAgent, defineAgentTool, isAgentDefinition } from './packem_shared/defineAgent-D6maSbVc.mjs';
8
+ export { functionTool } from './packem_shared/functionTool-D6lCa2jB.mjs';
9
+ export { createAgentGenerate, createEpisodeExtract, createGraphExtract, createStreamGenerate, resolveAgentModel } from './packem_shared/createAgentGenerate-BQv9YJ01.mjs';
10
+ export { adaptMcpResult, mcpTools } from './packem_shared/adaptMcpResult-wtNMvLoP.mjs';
11
+ export { buildModelMessages } from './packem_shared/buildModelMessages-BWFigaoo.mjs';
12
+ export { agentBindingName, agentClassName, agentDefaultName, voiceBindingName, voiceClassName } from './naming.mjs';
13
+ export { AGENT_MODULE, DEFAULT_AGENT_FUNCTION_PATHS, SANDBOX_INVOKE_PATH, SANDBOX_MODULE, toFunctionReference } from './packem_shared/AGENT_MODULE-Dnt_-AAT.mjs';
14
+ export { browserTool, containerTool, fsTool } from './sandbox.mjs';
15
+ export { defineSkill, isSkillDefinition } from './packem_shared/defineSkill-Ctf_S-rz.mjs';
16
+ export { default as VoiceSessionDO } from './packem_shared/VoiceSessionDO-DLoXsHGF.mjs';
17
+ export { runVoiceTurn } from './packem_shared/runVoiceTurn-LnqLvCRR.mjs';
18
+ export { default as compileAgentWorkflow } from './packem_shared/compileAgentWorkflow-BxJjHgtD.mjs';
@@ -0,0 +1,17 @@
1
+ /** `support` → `SupportAgentWorkflow` — the generated WorkflowEntrypoint class name. */
2
+ declare const agentClassName: (exportName: string) => string;
3
+ /** `support` → `AGENT_SUPPORT` — the Cloudflare Workflows binding name. */
4
+ declare const agentBindingName: (exportName: string) => string;
5
+ /** `supportBot` → `agent-support-bot` — the default deployed workflow name. */
6
+ declare const agentDefaultName: (exportName: string) => string;
7
+ /**
8
+ * `support` → `SupportVoiceDO` — the generated voice-session Durable Object
9
+ * class name (a subclass of `VoiceSessionDO`). Distinct from the agent's
10
+ * `WorkflowEntrypoint` (`SupportAgentWorkflow`): the voice path is a
11
+ * hibernatable-WebSocket DO that runs the per-turn STT→LLM→TTS pipeline in-DO,
12
+ * NOT the replay-durable Workflow.
13
+ */
14
+ declare const voiceClassName: (exportName: string) => string;
15
+ /** `support` → `VOICE_SUPPORT` — the Cloudflare Durable Object binding name for the voice session. */
16
+ declare const voiceBindingName: (exportName: string) => string;
17
+ export { agentBindingName, agentClassName, agentDefaultName, voiceBindingName, voiceClassName };
@@ -0,0 +1,17 @@
1
+ /** `support` → `SupportAgentWorkflow` — the generated WorkflowEntrypoint class name. */
2
+ declare const agentClassName: (exportName: string) => string;
3
+ /** `support` → `AGENT_SUPPORT` — the Cloudflare Workflows binding name. */
4
+ declare const agentBindingName: (exportName: string) => string;
5
+ /** `supportBot` → `agent-support-bot` — the default deployed workflow name. */
6
+ declare const agentDefaultName: (exportName: string) => string;
7
+ /**
8
+ * `support` → `SupportVoiceDO` — the generated voice-session Durable Object
9
+ * class name (a subclass of `VoiceSessionDO`). Distinct from the agent's
10
+ * `WorkflowEntrypoint` (`SupportAgentWorkflow`): the voice path is a
11
+ * hibernatable-WebSocket DO that runs the per-turn STT→LLM→TTS pipeline in-DO,
12
+ * NOT the replay-durable Workflow.
13
+ */
14
+ declare const voiceClassName: (exportName: string) => string;
15
+ /** `support` → `VOICE_SUPPORT` — the Cloudflare Durable Object binding name for the voice session. */
16
+ declare const voiceBindingName: (exportName: string) => string;
17
+ export { agentBindingName, agentClassName, agentDefaultName, voiceBindingName, voiceClassName };
@@ -0,0 +1,8 @@
1
+ const CAMEL_BOUNDARY = /([a-z0-9])([A-Z])/gu;
2
+ const agentClassName = (exportName) => `${exportName.charAt(0).toUpperCase()}${exportName.slice(1)}AgentWorkflow`;
3
+ const agentBindingName = (exportName) => `AGENT_${exportName.replaceAll(CAMEL_BOUNDARY, "$1_$2").toUpperCase()}`;
4
+ const agentDefaultName = (exportName) => `agent-${exportName.replaceAll(CAMEL_BOUNDARY, "$1-$2").toLowerCase()}`;
5
+ const voiceClassName = (exportName) => `${exportName.charAt(0).toUpperCase()}${exportName.slice(1)}VoiceDO`;
6
+ const voiceBindingName = (exportName) => `VOICE_${exportName.replaceAll(CAMEL_BOUNDARY, "$1_$2").toUpperCase()}`;
7
+
8
+ export { agentBindingName, agentClassName, agentDefaultName, voiceBindingName, voiceClassName };
@@ -0,0 +1,24 @@
1
+ const AGENT_MODULE = "agents";
2
+ const SANDBOX_MODULE = "sandbox";
3
+ const SANDBOX_INVOKE_PATH = `${SANDBOX_MODULE}:invoke`;
4
+ const DEFAULT_AGENT_FUNCTION_PATHS = {
5
+ appendMessage: `${AGENT_MODULE}:agentAppendMessage`,
6
+ ensureThread: `${AGENT_MODULE}:agentEnsureThread`,
7
+ episodeRecall: `${AGENT_MODULE}:agentEpisodeRecall`,
8
+ episodeUpsert: `${AGENT_MODULE}:agentEpisodeUpsert`,
9
+ graphTraverse: `${AGENT_MODULE}:agentGraphTraverse`,
10
+ graphUpsert: `${AGENT_MODULE}:agentGraphUpsert`,
11
+ listMessages: `${AGENT_MODULE}:agentMessages`,
12
+ patchThread: `${AGENT_MODULE}:agentPatchThread`,
13
+ run: `${AGENT_MODULE}:agentRun`,
14
+ setState: `${AGENT_MODULE}:agentSetState`,
15
+ state: `${AGENT_MODULE}:agentState`
16
+ };
17
+ const toFunctionReference = (source) => {
18
+ if (typeof source === "string") {
19
+ return { __lunoraRef: source };
20
+ }
21
+ return source;
22
+ };
23
+
24
+ export { AGENT_MODULE, DEFAULT_AGENT_FUNCTION_PATHS, SANDBOX_INVOKE_PATH, SANDBOX_MODULE, toFunctionReference };
@@ -0,0 +1,297 @@
1
+ import { createAi } from '@lunora/ai';
2
+ import { c as createDispatchRunner } from './createDispatchRunner-DSbp_dph-ZHTtxy3f.mjs';
3
+ import { t as toBase64 } from './base64-BVwtgRJV.mjs';
4
+ import { createStreamGenerate } from './createAgentGenerate-BQv9YJ01.mjs';
5
+ import { DEFAULT_AGENT_FUNCTION_PATHS, toFunctionReference } from './AGENT_MODULE-Dnt_-AAT.mjs';
6
+ import { parseIdentity, pcmToWav, readTranscriptionText, readSynthesisAudio, runVoiceTurn, toByteIterable } from './runVoiceTurn-LnqLvCRR.mjs';
7
+
8
+ const DEFAULT_STT_MODEL = "@cf/openai/whisper-large-v3-turbo";
9
+ const DEFAULT_TTS_MODEL = "@cf/deepgram/aura-1";
10
+ const MAX_UTTERANCE_BYTES = 8 * 1024 * 1024;
11
+ const MAX_SOCKET_BUFFER_BYTES = 256 * 1024;
12
+ const DRAIN_POLL_MS = 15;
13
+ const MAX_DRAIN_WAIT_MS = 5e3;
14
+ class VoiceSessionDO {
15
+ agent;
16
+ ai;
17
+ env;
18
+ exportName;
19
+ paths;
20
+ streamGenerate;
21
+ sttModel;
22
+ ttsModel;
23
+ audioBuffers = /* @__PURE__ */ new Map();
24
+ bufferedBytes = /* @__PURE__ */ new Map();
25
+ controllers = /* @__PURE__ */ new Map();
26
+ state;
27
+ constructor(state, env, agent, exportName) {
28
+ this.state = state;
29
+ this.env = env;
30
+ this.agent = agent;
31
+ this.exportName = exportName;
32
+ this.paths = DEFAULT_AGENT_FUNCTION_PATHS;
33
+ this.ai = createAi({ binding: env["AI"] });
34
+ this.streamGenerate = createStreamGenerate(agent, env);
35
+ this.sttModel = agent.voice?.stt ?? DEFAULT_STT_MODEL;
36
+ this.ttsModel = agent.voice?.tts ?? DEFAULT_TTS_MODEL;
37
+ }
38
+ /** HTTP entry — only a WebSocket upgrade carrying a `threadKey` is accepted. */
39
+ fetch(request) {
40
+ if (request.headers.get("Upgrade") !== "websocket") {
41
+ return new Response("Expected a WebSocket upgrade", { status: 426 });
42
+ }
43
+ const url = new URL(request.url);
44
+ const threadKey = url.searchParams.get("threadKey");
45
+ if (!threadKey) {
46
+ return new Response("Missing threadKey", { status: 400 });
47
+ }
48
+ const WebSocketPairConstructor = globalThis.WebSocketPair;
49
+ const pair = new WebSocketPairConstructor();
50
+ const client = pair[0];
51
+ const server = pair[1];
52
+ this.state.acceptWebSocket(server);
53
+ const connectionId = crypto.randomUUID();
54
+ const identity = parseIdentity(request.headers.get("x-lunora-identity"));
55
+ const userId = request.headers.get("x-lunora-userid") ?? void 0;
56
+ server.serializeAttachment?.({
57
+ connectionId,
58
+ threadKey,
59
+ turn: 0,
60
+ ...identity === void 0 ? {} : { identity },
61
+ ...userId === void 0 ? {} : { userId }
62
+ });
63
+ this.send(server, { audioFormat: this.agent.voice?.audioFormat ?? "mp3", type: "ready" });
64
+ const greeting = this.agent.voice?.greeting;
65
+ if (greeting && greeting.length > 0) {
66
+ this.state.waitUntil?.(this.speakGreeting(server, connectionId, threadKey, userId, identity, greeting));
67
+ }
68
+ return new Response(null, { status: 101, webSocket: client });
69
+ }
70
+ /** Hibernation message handler — never throws (a thrown handler is fatal to the socket). */
71
+ async webSocketMessage(ws, message) {
72
+ const attachment = ws.deserializeAttachment?.();
73
+ if (!attachment) {
74
+ return;
75
+ }
76
+ try {
77
+ if (typeof message === "string") {
78
+ await this.handleControl(ws, attachment, message);
79
+ return;
80
+ }
81
+ this.bufferAudio(attachment.connectionId, new Uint8Array(message), ws);
82
+ } catch (error) {
83
+ this.send(ws, { message: error instanceof Error ? error.message : String(error), type: "error" });
84
+ }
85
+ }
86
+ /** Abort any in-flight turn + free the socket's buffers on close. Never throws. */
87
+ webSocketClose(ws) {
88
+ this.cleanupSocket(ws);
89
+ }
90
+ /** Abort any in-flight turn + free the socket's buffers on error. Never throws. */
91
+ webSocketError(ws) {
92
+ this.cleanupSocket(ws);
93
+ }
94
+ /**
95
+ * The runtime dispatch seam reaching the shared agent thread functions. When
96
+ * the socket carries a verified identity it is forwarded so the `agents:*`
97
+ * thread writes are attributed to the caller (RLS / row ownership) rather
98
+ * than the anonymous system dispatch.
99
+ */
100
+ resolveRun(userId, claims) {
101
+ const identity = userId === void 0 && claims === void 0 ? void 0 : { ...claims === void 0 ? {} : { claims }, ...userId === void 0 ? {} : { userId } };
102
+ return createDispatchRunner({ env: this.env, label: "@lunora/agent voice", ...identity === void 0 ? {} : { identity } });
103
+ }
104
+ /** Production STT seam: WAV-wrap the utterance and run the batch transcription model. */
105
+ async transcribe(pcm) {
106
+ const wav = pcmToWav(pcm);
107
+ return readTranscriptionText(await this.ai.run(this.sttModel, { audio: toBase64(wav) }));
108
+ }
109
+ /** Production TTS seam: synthesize one sentence to a normalized audio source; `signal` aborts an in-flight barge-in. */
110
+ async synthesize(text, signal) {
111
+ const speaker = this.agent.voice?.speaker;
112
+ return readSynthesisAudio(
113
+ await this.ai.run(this.ttsModel, { text, ...speaker === void 0 ? {} : { speaker } }, signal === void 0 ? void 0 : { signal })
114
+ );
115
+ }
116
+ /** Route a JSON control frame (`commit` / `interrupt` / `text`). */
117
+ async handleControl(ws, attachment, raw) {
118
+ let frame;
119
+ try {
120
+ frame = JSON.parse(raw);
121
+ } catch {
122
+ return;
123
+ }
124
+ if (frame.type === "interrupt") {
125
+ this.controllers.get(attachment.connectionId)?.abort();
126
+ return;
127
+ }
128
+ if (this.controllers.has(attachment.connectionId)) {
129
+ this.send(ws, { message: "a turn is already in progress — send an interrupt before the next utterance", type: "error" });
130
+ return;
131
+ }
132
+ if (frame.type === "commit") {
133
+ const pcm = this.drainAudio(attachment.connectionId);
134
+ await this.runTurn(ws, attachment, { pcm });
135
+ return;
136
+ }
137
+ await this.runTurn(ws, attachment, { text: frame.text });
138
+ }
139
+ /** Append a binary audio frame to the socket's utterance buffer (bounded). */
140
+ bufferAudio(connectionId, chunk, ws) {
141
+ const total = (this.bufferedBytes.get(connectionId) ?? 0) + chunk.byteLength;
142
+ if (total > MAX_UTTERANCE_BYTES) {
143
+ this.audioBuffers.delete(connectionId);
144
+ this.bufferedBytes.delete(connectionId);
145
+ this.send(ws, { message: "utterance exceeded the maximum buffer — send a commit sooner", type: "error" });
146
+ return;
147
+ }
148
+ const chunks = this.audioBuffers.get(connectionId) ?? [];
149
+ chunks.push(chunk);
150
+ this.audioBuffers.set(connectionId, chunks);
151
+ this.bufferedBytes.set(connectionId, total);
152
+ }
153
+ /** Concatenate + clear the socket's buffered utterance. */
154
+ drainAudio(connectionId) {
155
+ const chunks = this.audioBuffers.get(connectionId) ?? [];
156
+ this.audioBuffers.delete(connectionId);
157
+ this.bufferedBytes.delete(connectionId);
158
+ const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
159
+ const pcm = new Uint8Array(total);
160
+ let offset = 0;
161
+ for (const chunk of chunks) {
162
+ pcm.set(chunk, offset);
163
+ offset += chunk.byteLength;
164
+ }
165
+ return pcm;
166
+ }
167
+ /** Execute a turn under a fresh abort controller, advancing the socket's turn index. */
168
+ async runTurn(ws, attachment, input) {
169
+ const controller = new AbortController();
170
+ this.controllers.set(attachment.connectionId, controller);
171
+ try {
172
+ await runVoiceTurn({
173
+ agent: this.agent,
174
+ connectionId: attachment.connectionId,
175
+ env: this.env,
176
+ exportName: this.exportName,
177
+ paths: this.paths,
178
+ run: this.resolveRun(attachment.userId, attachment.identity),
179
+ send: (frame) => {
180
+ this.send(ws, frame);
181
+ },
182
+ sendAudio: (bytes) => {
183
+ this.sendAudio(ws, bytes);
184
+ },
185
+ signal: controller.signal,
186
+ streamGenerate: this.streamGenerate,
187
+ synthesize: async (text, signal) => this.synthesizeWithSignal(text, signal),
188
+ threadKey: attachment.threadKey,
189
+ transcribe: async (pcm) => this.transcribe(pcm),
190
+ turn: attachment.turn,
191
+ waitForDrain: async () => this.waitForSocketDrain(ws),
192
+ ...attachment.userId === void 0 ? {} : { owner: attachment.userId },
193
+ ...input.pcm === void 0 ? {} : { pcm: input.pcm },
194
+ ...input.text === void 0 ? {} : { text: input.text }
195
+ });
196
+ } finally {
197
+ if (this.controllers.get(attachment.connectionId) === controller) {
198
+ this.controllers.delete(attachment.connectionId);
199
+ }
200
+ ws.serializeAttachment?.({ ...attachment, turn: attachment.turn + 1 });
201
+ }
202
+ }
203
+ /** Synthesize a greeting on connect and persist it as the thread's opening assistant turn. */
204
+ async speakGreeting(ws, connectionId, threadKey, userId, identity, greeting) {
205
+ const run = this.resolveRun(userId, identity);
206
+ const controller = new AbortController();
207
+ const isAborted = () => controller.signal.aborted;
208
+ this.controllers.set(connectionId, controller);
209
+ try {
210
+ await run(toFunctionReference(this.paths.ensureThread), {
211
+ agent: this.exportName,
212
+ key: threadKey,
213
+ ...this.agent.initialState === void 0 ? {} : { initialState: this.agent.initialState },
214
+ ...userId === void 0 ? {} : { owner: userId }
215
+ });
216
+ for await (const chunk of toByteIterable(await this.synthesizeWithSignal(greeting, controller.signal))) {
217
+ if (isAborted()) {
218
+ break;
219
+ }
220
+ await this.waitForSocketDrain(ws);
221
+ if (isAborted()) {
222
+ break;
223
+ }
224
+ this.sendAudio(ws, chunk);
225
+ }
226
+ if (!isAborted()) {
227
+ await run(toFunctionReference(this.paths.appendMessage), {
228
+ content: greeting,
229
+ messageKey: "voice:greeting:assistant",
230
+ role: "assistant",
231
+ threadKey
232
+ });
233
+ this.send(ws, { text: greeting, type: "assistant_done" });
234
+ }
235
+ } catch (error) {
236
+ this.send(ws, { message: error instanceof Error ? error.message : String(error), type: "error" });
237
+ } finally {
238
+ if (this.controllers.get(connectionId) === controller) {
239
+ this.controllers.delete(connectionId);
240
+ }
241
+ }
242
+ }
243
+ /** Bridge the pipeline's `(text, signal)` synthesize seam onto the class TTS method, forwarding the barge-in signal. */
244
+ async synthesizeWithSignal(text, signal) {
245
+ if (signal.aborted) {
246
+ return new Uint8Array(0);
247
+ }
248
+ return this.synthesize(text, signal);
249
+ }
250
+ /** Abort an in-flight turn and free a socket's transient buffers. */
251
+ cleanupSocket(ws) {
252
+ const attachment = ws.deserializeAttachment?.();
253
+ if (!attachment) {
254
+ return;
255
+ }
256
+ this.controllers.get(attachment.connectionId)?.abort();
257
+ this.controllers.delete(attachment.connectionId);
258
+ this.audioBuffers.delete(attachment.connectionId);
259
+ this.bufferedBytes.delete(attachment.connectionId);
260
+ }
261
+ /** Send a JSON control frame, swallowing a closed-socket error (never throw from a handler). */
262
+ // eslint-disable-next-line class-methods-use-this -- instance method (kept non-static for subclass override symmetry); acts on the passed socket
263
+ send(ws, frame) {
264
+ try {
265
+ ws.send(JSON.stringify(frame));
266
+ } catch {
267
+ }
268
+ }
269
+ /** Send a binary audio frame, swallowing a closed-socket error. */
270
+ // eslint-disable-next-line class-methods-use-this -- instance method (kept non-static for subclass override symmetry); acts on the passed socket
271
+ sendAudio(ws, bytes) {
272
+ try {
273
+ ws.send(bytes);
274
+ } catch {
275
+ }
276
+ }
277
+ /**
278
+ * Outbound backpressure: if the socket exposes `bufferedAmount`, yield in
279
+ * short polls until the send buffer drains below the cap so a slow client
280
+ * can't balloon DO memory. Bounded by {@link MAX_DRAIN_WAIT_MS} so a stuck
281
+ * socket never blocks a turn forever, and never throws (a socket without
282
+ * `bufferedAmount` resolves immediately).
283
+ */
284
+ // eslint-disable-next-line class-methods-use-this -- instance method (kept non-static for subclass override symmetry); acts on the passed socket
285
+ async waitForSocketDrain(ws) {
286
+ const socket = ws;
287
+ let waited = 0;
288
+ while (typeof socket.bufferedAmount === "number" && socket.bufferedAmount > MAX_SOCKET_BUFFER_BYTES && waited < MAX_DRAIN_WAIT_MS) {
289
+ await new Promise((resolve) => {
290
+ setTimeout(resolve, DRAIN_POLL_MS);
291
+ });
292
+ waited += DRAIN_POLL_MS;
293
+ }
294
+ }
295
+ }
296
+
297
+ export { VoiceSessionDO as default };
@@ -0,0 +1,65 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+ import { jsonSchema } from 'ai';
3
+
4
+ const adaptMcpResult = (result) => {
5
+ if (result.structuredContent !== void 0) {
6
+ return result.structuredContent;
7
+ }
8
+ const text = (result.content ?? []).filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n");
9
+ if (result.isError === true) {
10
+ return `MCP tool error: ${text.length > 0 ? text : "unknown error"}`;
11
+ }
12
+ if (text.length > 0) {
13
+ return text;
14
+ }
15
+ return result.content;
16
+ };
17
+ const adaptMcpTool = (client, info) => {
18
+ return {
19
+ description: info.description ?? `MCP tool "${info.name}".`,
20
+ // Runs inside the loop's `tool:NAME:CALL_ID` durable step: a completed
21
+ // call is memoized on replay, and a failed step retries at-least-once —
22
+ // the same idempotency contract every agent tool carries.
23
+ execute: async (input) => adaptMcpResult(await client.callTool({ arguments: input, name: info.name })),
24
+ inputSchema: jsonSchema(info.inputSchema),
25
+ isLunoraAgentTool: true
26
+ };
27
+ };
28
+ const connectClient = async (options) => {
29
+ if (options.command !== void 0) {
30
+ throw new LunoraError(
31
+ "INTERNAL",
32
+ "@lunora/agent: mcpTools `command` (stdio transport) cannot run in the Workers runtime — pass a `url` for an HTTP/SSE MCP server, or inject a connected `client`"
33
+ );
34
+ }
35
+ if (options.url === void 0) {
36
+ throw new LunoraError("INTERNAL", "@lunora/agent: mcpTools requires a `url` (an HTTP/SSE MCP server) or an injected `client`");
37
+ }
38
+ const url = new URL(options.url);
39
+ const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
40
+ let transport;
41
+ if (options.transport === "sse") {
42
+ const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js');
43
+ transport = new SSEClientTransport(url);
44
+ } else {
45
+ const { StreamableHTTPClientTransport } = await import('@modelcontextprotocol/sdk/client/streamableHttp.js');
46
+ transport = new StreamableHTTPClientTransport(url);
47
+ }
48
+ const client = new Client({ name: options.name ?? "lunora-agent", version: options.version ?? "0.0.0" });
49
+ await client.connect(transport);
50
+ return client;
51
+ };
52
+ const mcpTools = async (options) => {
53
+ const client = options.client ?? await connectClient(options);
54
+ const { tools: listed } = await client.listTools();
55
+ const record = {};
56
+ for (const info of listed) {
57
+ if (options.only !== void 0 && !options.only.includes(info.name)) {
58
+ continue;
59
+ }
60
+ record[`${options.prefix ?? ""}${info.name}`] = adaptMcpTool(client, info);
61
+ }
62
+ return record;
63
+ };
64
+
65
+ export { adaptMcpResult, mcpTools };
@@ -0,0 +1,94 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+ import { jsonSchema } from 'ai';
3
+ import { agentBindingName } from '../naming.mjs';
4
+ import { toFunctionReference, DEFAULT_AGENT_FUNCTION_PATHS } from './AGENT_MODULE-Dnt_-AAT.mjs';
5
+
6
+ const TERMINAL_STATUSES = /* @__PURE__ */ new Set(["complete", "errored", "terminated"]);
7
+ const DEFAULT_MAX_POLLS = 120;
8
+ const DEFAULT_POLL_INTERVAL_MS = 500;
9
+ const readStatus = (raw) => {
10
+ if (raw !== null && typeof raw === "object" && typeof raw.status === "string") {
11
+ return raw.status;
12
+ }
13
+ return "unknown";
14
+ };
15
+ const defaultWait = async (ms) => {
16
+ await new Promise((resolve) => {
17
+ setTimeout(resolve, ms);
18
+ });
19
+ };
20
+ const pollUntilTerminal = async (instance, maxPolls, pollIntervalMs, wait) => {
21
+ for (let attempt = 0; attempt < maxPolls; attempt += 1) {
22
+ const status = readStatus(await instance.status());
23
+ if (TERMINAL_STATUSES.has(status)) {
24
+ return status;
25
+ }
26
+ await wait(pollIntervalMs);
27
+ }
28
+ return "timeout";
29
+ };
30
+ const finalAnswer = (history) => {
31
+ for (let index = history.length - 1; index >= 0; index -= 1) {
32
+ const row = history[index];
33
+ if (row?.role === "assistant" && (row.toolCalls === void 0 || row.toolCalls.length === 0)) {
34
+ return row.content;
35
+ }
36
+ }
37
+ return "";
38
+ };
39
+ const agentAsTool = (options) => {
40
+ if (typeof options.name !== "string" || options.name.length === 0) {
41
+ throw new LunoraError("INTERNAL", "@lunora/agent: agent.asTool requires a `name` (the child agent's export name, selecting its AGENT_* binding)");
42
+ }
43
+ if (typeof options.description !== "string" || options.description.length === 0) {
44
+ throw new LunoraError("INTERNAL", "@lunora/agent: agent.asTool requires a non-empty `description` (the parent model decides from it)");
45
+ }
46
+ const { name } = options;
47
+ const bindingName = agentBindingName(name);
48
+ const maxPolls = options.maxPolls ?? DEFAULT_MAX_POLLS;
49
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
50
+ const wait = options.wait ?? defaultWait;
51
+ const listMessages = toFunctionReference(DEFAULT_AGENT_FUNCTION_PATHS.listMessages);
52
+ const execute = async (input, context) => {
53
+ const binding = context.env[bindingName];
54
+ if (!binding || typeof binding.create !== "function" || typeof binding.get !== "function") {
55
+ throw new LunoraError(
56
+ "INTERNAL",
57
+ `@lunora/agent: agent.asTool("${name}") found no Workflow binding "${bindingName}" on env — declare the "${name}" agent so codegen wires its binding`
58
+ );
59
+ }
60
+ const childThreadKey = `${context.threadKey}::sub::${name}::${context.toolCallId}`;
61
+ const childInstanceId = `sub-${name}-${context.toolCallId}`;
62
+ const params = { input: input.prompt, threadKey: childThreadKey };
63
+ let instance;
64
+ try {
65
+ const handle = await binding.create({ id: childInstanceId, params });
66
+ instance = await binding.get(handle.id);
67
+ } catch {
68
+ instance = await binding.get(childInstanceId);
69
+ }
70
+ const terminal = await pollUntilTerminal(instance, maxPolls, pollIntervalMs, wait);
71
+ if (terminal === "errored" || terminal === "terminated") {
72
+ return `Sub-agent "${name}" ${terminal} before producing an answer.`;
73
+ }
74
+ if (terminal === "timeout") {
75
+ return `Sub-agent "${name}" did not finish within the allotted time.`;
76
+ }
77
+ const history = await context.run(listMessages, { key: childThreadKey });
78
+ return finalAnswer(history);
79
+ };
80
+ return {
81
+ description: options.description,
82
+ execute,
83
+ inputSchema: jsonSchema({
84
+ properties: {
85
+ prompt: { description: "The task or question to delegate to the sub-agent.", type: "string" }
86
+ },
87
+ required: ["prompt"],
88
+ type: "object"
89
+ }),
90
+ isLunoraAgentTool: true
91
+ };
92
+ };
93
+
94
+ export { agentAsTool };
@@ -0,0 +1,18 @@
1
+ const toBase64 = (bytes) => {
2
+ let binary = "";
3
+ const chunk = 32768;
4
+ for (let index = 0; index < bytes.length; index += chunk) {
5
+ binary += String.fromCharCode(...bytes.subarray(index, index + chunk));
6
+ }
7
+ return btoa(binary);
8
+ };
9
+ const fromBase64 = (base64) => {
10
+ const binary = atob(base64);
11
+ const bytes = new Uint8Array(binary.length);
12
+ for (let index = 0; index < binary.length; index += 1) {
13
+ bytes[index] = binary.codePointAt(index) ?? 0;
14
+ }
15
+ return bytes;
16
+ };
17
+
18
+ export { fromBase64 as f, toBase64 as t };
@@ -0,0 +1,47 @@
1
+ import { d as describeError, t as toolInputOf, r as readField } from './common-DAeFCot5.mjs';
2
+
3
+ const stringOr = (value, fallback) => typeof value === "string" && value.length > 0 ? value : fallback;
4
+ const braintrustTelemetry = (options) => {
5
+ const { functionId, logger, recordInputs = false, recordOutputs = false } = options;
6
+ return {
7
+ executeLanguageModelCall: (options_) => logger.traced(
8
+ async (span) => {
9
+ if (recordInputs) {
10
+ span.log({ input: readField(options_, "messages"), model: readField(options_, "modelId") });
11
+ }
12
+ const output = await options_.execute();
13
+ if (recordOutputs) {
14
+ span.log({ output });
15
+ }
16
+ return output;
17
+ },
18
+ { name: stringOr(functionId, "language_model_call"), type: "llm" }
19
+ ),
20
+ executeTool: (options_) => {
21
+ const toolName = readField(readField(options_, "toolCall"), "toolName");
22
+ return logger.traced(
23
+ async (span) => {
24
+ if (recordInputs) {
25
+ span.log({ input: toolInputOf(options_), tool: toolName });
26
+ }
27
+ const output = await options_.execute();
28
+ if (recordOutputs) {
29
+ span.log({ output });
30
+ }
31
+ return output;
32
+ },
33
+ { name: stringOr(toolName, "execute_tool"), type: "tool" }
34
+ );
35
+ },
36
+ onError: (error) => {
37
+ logger.traced(
38
+ (span) => {
39
+ span.log({ error: describeError(error) });
40
+ },
41
+ { name: "error", type: "error" }
42
+ );
43
+ }
44
+ };
45
+ };
46
+
47
+ export { braintrustTelemetry };