@lunora/agent 0.0.0 → 1.0.0-alpha.2

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 +38 -31
  3. package/dist/channels.d.mts +66 -0
  4. package/dist/channels.d.ts +66 -0
  5. package/dist/channels.mjs +181 -0
  6. package/dist/component.d.mts +115 -0
  7. package/dist/component.d.ts +115 -0
  8. package/dist/component.mjs +407 -0
  9. package/dist/inbound.d.mts +34 -0
  10. package/dist/inbound.d.ts +34 -0
  11. package/dist/inbound.mjs +32 -0
  12. package/dist/index.d.mts +848 -0
  13. package/dist/index.d.ts +848 -0
  14. package/dist/index.mjs +18 -0
  15. package/dist/naming.d.mts +30 -0
  16. package/dist/naming.d.ts +30 -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-BhJFTvz_.d.mts +1114 -0
  45. package/dist/packem_shared/types.d-BhJFTvz_.d.ts +1114 -0
  46. package/dist/sandbox.d.mts +200 -0
  47. package/dist/sandbox.d.ts +200 -0
  48. package/dist/sandbox.mjs +109 -0
  49. package/dist/telemetry/index.d.mts +173 -0
  50. package/dist/telemetry/index.d.ts +173 -0
  51. package/dist/telemetry/index.mjs +4 -0
  52. package/package.json +88 -7
@@ -0,0 +1,115 @@
1
+ import { SchemaExtension } from '@lunora/server';
2
+ /**
3
+ * Loose structural view of a registered Lunora function — wide enough for any
4
+ * concrete `RegisteredMutation`/`RegisteredQuery` (whose precise validator-map
5
+ * generics make them invariant), narrow enough for re-export, dispatch, and
6
+ * tests. Codegen registers the runtime value; it never needs the generics.
7
+ */
8
+ interface AgentRegisteredFunction {
9
+ readonly args: unknown;
10
+ readonly handler: (context: unknown, args: never) => unknown;
11
+ readonly kind: "mutation" | "query";
12
+ readonly visibility?: "internal" | "public";
13
+ }
14
+ /** Stamp a registered function internal — server-side callable only. */
15
+
16
+ /**
17
+ * The per-owner dedup key for an entity name: trim, collapse internal
18
+ * whitespace, lowercase. Deterministic (no locale) so a workflow replay or
19
+ * retry writes the exact same key — the graph upsert stays idempotent.
20
+ * @experimental
21
+ */
22
+ declare const normalizeEntityName: (name: string) => string;
23
+ /** The two internal graph-memory functions the durable loop dispatches to. */
24
+
25
+ /**
26
+ * Loose structural view of the registered sandbox action — wide enough for the
27
+ * concrete `RegisteredAction`, narrow enough for re-export + dispatch. Codegen
28
+ * registers the runtime value; it never needs the precise generics.
29
+ * @experimental
30
+ */
31
+ interface SandboxRegisteredFunction {
32
+ readonly args: unknown;
33
+ readonly handler: (context: unknown, args: never) => unknown;
34
+ readonly kind: "action";
35
+ readonly visibility?: "internal" | "public";
36
+ }
37
+ /**
38
+ * `SandboxComponent` is part of the experimental `@lunora/agent` API and may change without a major version bump.
39
+ * @experimental
40
+ */
41
+ interface SandboxComponent {
42
+ invoke: SandboxRegisteredFunction;
43
+ }
44
+ /**
45
+ * Resolve a model-supplied `path` to an absolute R2 key UNDER `root`, rejecting
46
+ * any `..` that would escape the sandbox root. Every returned key is guaranteed
47
+ * to start with the normalized `root`, so the model can never read or write
48
+ * outside its own prefix — the fs sandbox's core isolation guarantee.
49
+ */
50
+
51
+ /**
52
+ * Build the sandbox runtime component: the single internal action the
53
+ * batteries-included `browserTool`/`containerTool` dispatch to. Codegen
54
+ * auto-registers it at `sandbox:invoke` whenever `lunora/` imports a sandbox
55
+ * tool. It runs as an **action** because that is the only ctx codegen attaches
56
+ * `ctx.browser` to (and `ctx.containers` rides every ctx once a container is
57
+ * declared) — the durable tool step itself has neither.
58
+ * @experimental
59
+ */
60
+ declare const sandboxComponent: () => SandboxComponent;
61
+ /**
62
+ * The agent thread tables, shipped as a schema extension so an app merges
63
+ * them with one call and they can never collide with app tables:
64
+ *
65
+ * ```ts
66
+ * // lunora/schema.ts
67
+ * export default defineSchema({ ... }).extend(agentExtension);
68
+ * ```
69
+ *
70
+ * Message ordering follows the Convex-agent model: `seq` is the monotonic
71
+ * per-thread position (allocated from the thread's `messageCount` counter, so
72
+ * allocation is O(1) inside the serialized mutation), and `messageKey` is the
73
+ * deterministic idempotency key — a workflow replay that re-persists the same
74
+ * message is a no-op instead of a duplicate.
75
+ * @experimental
76
+ */
77
+ declare const agentExtension: SchemaExtension;
78
+ /**
79
+ * `AgentComponent` is part of the experimental `@lunora/agent` API and may change without a major version bump.
80
+ * @experimental
81
+ */
82
+ interface AgentComponent {
83
+ extension: SchemaExtension;
84
+ functions: {
85
+ agentAppendMessage: AgentRegisteredFunction;
86
+ agentEnsureThread: AgentRegisteredFunction;
87
+ agentEpisodeRecall: AgentRegisteredFunction;
88
+ agentEpisodeUpsert: AgentRegisteredFunction;
89
+ agentGraphTraverse: AgentRegisteredFunction;
90
+ agentGraphUpsert: AgentRegisteredFunction;
91
+ agentMessages: AgentRegisteredFunction;
92
+ agentPatchThread: AgentRegisteredFunction;
93
+ agentResolveApproval: AgentRegisteredFunction;
94
+ agentRun: AgentRegisteredFunction;
95
+ agentSetState: AgentRegisteredFunction;
96
+ agentState: AgentRegisteredFunction;
97
+ agentThread: AgentRegisteredFunction;
98
+ };
99
+ }
100
+ /**
101
+ * Build the agent runtime component: the thread schema extension plus the
102
+ * functions the durable loop dispatches to (and the client subscribes to).
103
+ * Codegen auto-registers them under the `agents:*` namespace whenever
104
+ * `lunora/agents.ts` declares an agent — the loop's dispatch paths assume
105
+ * that namespace, and apps never re-export these by hand.
106
+ *
107
+ * Most mutations are **internal** (only the workflow's admin-authenticated
108
+ * dispatch may call them); the queries are public so a client can subscribe
109
+ * to `agents:agentMessages` for a live thread view. Two mutations are public:
110
+ * `agentResolveApproval` (a client resolves a HITL approval) and `agentRun`
111
+ * (an HTTP client starts a durable run) — both owner-gated.
112
+ * @experimental
113
+ */
114
+ declare const agentComponent: () => AgentComponent;
115
+ export { AgentComponent, type SandboxComponent, type SandboxRegisteredFunction, agentComponent, agentExtension, normalizeEntityName, sandboxComponent };
@@ -0,0 +1,407 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+ import { defineTable, initLunora, defineSchemaExtension } from '@lunora/server';
3
+ import { v } from '@lunora/values';
4
+ import { a as asInternal, A as AGENT_EXTENSION_KEY, g as graphTables, d as definedColumns, b as graphComponent } from './packem_shared/graph-component-aoUwO-f0.mjs';
5
+ export { n as normalizeEntityName } from './packem_shared/graph-component-aoUwO-f0.mjs';
6
+ export { sandboxComponent } from './packem_shared/sandboxComponent-DR3pTwBL.mjs';
7
+
8
+ const EPISODES_TABLE = "agent_episodes";
9
+ const DEFAULT_EPISODE_RECALL = 5;
10
+ const MAX_EPISODE_RECALL = 20;
11
+ const MAX_EPISODE_SUMMARY_CHARS = 500;
12
+ const MAX_EPISODE_RETENTION = 200;
13
+ const WHITESPACE_RUN = /\s+/gu;
14
+ const clampRecall = (value) => {
15
+ if (value === void 0 || !Number.isFinite(value)) {
16
+ return DEFAULT_EPISODE_RECALL;
17
+ }
18
+ return Math.min(MAX_EPISODE_RECALL, Math.max(1, Math.trunc(value)));
19
+ };
20
+ const episodeTables = {
21
+ /**
22
+ * One episode per completed run — a short natural-language summary of the
23
+ * exchange, time-ordered for recency recall. `messageKey` (the extract
24
+ * step's instance-scoped key) is the idempotency key so a workflow replay
25
+ * never double-records the same run.
26
+ */
27
+ episodes: defineTable({
28
+ createdAt: v.number(),
29
+ messageKey: v.string(),
30
+ owner: v.string(),
31
+ /** A one/two-sentence summary of the run, injected as a memory-log line. */
32
+ summary: v.string(),
33
+ /** The thread the episode came from (provenance; not used for recall scope). */
34
+ threadKey: v.optional(v.string())
35
+ }).index("byOwnerCreatedAt", ["owner", "createdAt"]).index("byOwnerMessageKey", ["owner", "messageKey"], { unique: true }).public()
36
+ };
37
+ const { mutation: mutation$1, query: query$1 } = initLunora.dataModel().create();
38
+ const episodicComponent = () => {
39
+ const agentEpisodeUpsert = mutation$1.input({
40
+ createdAt: v.optional(v.number()),
41
+ messageKey: v.string(),
42
+ owner: v.string(),
43
+ summary: v.string(),
44
+ threadKey: v.optional(v.string())
45
+ }).mutation(async ({ args, ctx: context }) => {
46
+ const summary = args.summary.replaceAll(WHITESPACE_RUN, " ").trim().slice(0, MAX_EPISODE_SUMMARY_CHARS);
47
+ if (summary.length === 0) {
48
+ return { recorded: false };
49
+ }
50
+ const existing = await context.db.query(EPISODES_TABLE).withIndex("byOwnerMessageKey", (q) => q.eq("owner", args.owner).eq("messageKey", args.messageKey)).first();
51
+ if (existing) {
52
+ return { recorded: false };
53
+ }
54
+ await context.db.insert(EPISODES_TABLE, {
55
+ createdAt: args.createdAt ?? Date.now(),
56
+ messageKey: args.messageKey,
57
+ owner: args.owner,
58
+ summary,
59
+ ...args.threadKey === void 0 ? {} : { threadKey: args.threadKey }
60
+ });
61
+ const overflow = await context.db.query(EPISODES_TABLE).withIndex("byOwnerCreatedAt", (q) => q.eq("owner", args.owner)).order("asc").take(MAX_EPISODE_RETENTION + 1);
62
+ for (const stale of overflow.slice(0, Math.max(0, overflow.length - MAX_EPISODE_RETENTION))) {
63
+ await context.db.delete(stale["_id"]);
64
+ }
65
+ return { recorded: true };
66
+ });
67
+ const agentEpisodeRecall = query$1.input({
68
+ limit: v.optional(v.number()),
69
+ owner: v.string()
70
+ }).query(async ({ args, ctx: context }) => {
71
+ const recent = await context.db.query(EPISODES_TABLE).withIndex("byOwnerCreatedAt", (q) => q.eq("owner", args.owner)).order("desc").take(clampRecall(args.limit));
72
+ if (recent.length === 0) {
73
+ return { context: "" };
74
+ }
75
+ return {
76
+ context: recent.toReversed().map((row) => `- ${row["summary"]}`).join("\n")
77
+ };
78
+ });
79
+ return {
80
+ agentEpisodeRecall: asInternal(agentEpisodeRecall),
81
+ agentEpisodeUpsert: asInternal(agentEpisodeUpsert)
82
+ };
83
+ };
84
+
85
+ const THREADS_BARE_TABLE = "threads";
86
+ const MESSAGES_BARE_TABLE = "messages";
87
+ const THREADS_TABLE = `${AGENT_EXTENSION_KEY}_${THREADS_BARE_TABLE}`;
88
+ const MESSAGES_TABLE = `${AGENT_EXTENSION_KEY}_${MESSAGES_BARE_TABLE}`;
89
+ const agentExtension = defineSchemaExtension(AGENT_EXTENSION_KEY, {
90
+ tables: {
91
+ [MESSAGES_BARE_TABLE]: defineTable({
92
+ content: v.string(),
93
+ createdAt: v.number(),
94
+ messageKey: v.string(),
95
+ role: v.union(v.literal("user"), v.literal("assistant"), v.literal("tool"), v.literal("system")),
96
+ seq: v.number(),
97
+ /**
98
+ * Human-in-the-loop approval marker: `"awaiting_approval"` on the
99
+ * placeholder written while a run pauses on a gated tool, then
100
+ * `"approved"`/`"rejected"` on the tool result once resolved. Optional
101
+ * so ordinary messages (and pre-existing rows) are unaffected.
102
+ */
103
+ status: v.optional(v.union(v.literal("awaiting_approval"), v.literal("approved"), v.literal("rejected"))),
104
+ stepName: v.optional(v.string()),
105
+ threadKey: v.string(),
106
+ toolCallId: v.optional(v.string()),
107
+ toolCalls: v.optional(v.array(v.object({ id: v.string(), input: v.any(), name: v.string() }))),
108
+ toolName: v.optional(v.string())
109
+ }).index("byThread", ["threadKey", "seq"]).index("byMessageKey", ["threadKey", "messageKey"], { unique: true }).public(),
110
+ [THREADS_BARE_TABLE]: defineTable({
111
+ agent: v.string(),
112
+ createdAt: v.number(),
113
+ error: v.optional(v.string()),
114
+ /**
115
+ * The workflow instance id of the run that currently owns this
116
+ * thread. The concurrency guard compares it to a starting run's own
117
+ * instance id — a match is a replay (allow), a mismatch while
118
+ * `status === "running"` is a genuine second run (apply
119
+ * `onConcurrentRun`). Also the target for `cancel`/`replace`. Optional
120
+ * so pre-existing threads (written before this column) are unaffected.
121
+ */
122
+ instanceId: v.optional(v.string()),
123
+ key: v.string(),
124
+ // Next message seq — incremented on every append (see above).
125
+ messageCount: v.number(),
126
+ /**
127
+ * Verified identity of the thread owner (pass `ctx.auth.userId`
128
+ * when starting a run). When set, the public queries only answer
129
+ * for a caller with that identity; when absent the thread is
130
+ * readable by anyone who knows its key (single-tenant/anonymous
131
+ * apps). First writer wins — a later run may not change it.
132
+ */
133
+ owner: v.optional(v.string()),
134
+ /**
135
+ * The thread's synced agent state — a JSON object written by the
136
+ * internal `agentSetState` mutation (absolute REPLACE) and read by the
137
+ * public `agentState` query (`ctx.getState` / `useAgentState`). Seeded
138
+ * from `defineAgent({ initialState })` on thread creation. Optional so
139
+ * agent-free apps and pre-existing threads (written before this column)
140
+ * are unaffected.
141
+ */
142
+ state: v.optional(v.any()),
143
+ status: v.union(v.literal("idle"), v.literal("running"), v.literal("error"), v.literal("cancelled"), v.literal("awaiting_input")),
144
+ title: v.optional(v.string()),
145
+ updatedAt: v.number(),
146
+ /**
147
+ * Cumulative token usage for the latest run on this thread, patched
148
+ * at run end. Optional so agent-free apps and pre-existing threads
149
+ * (written before this column existed) are unaffected.
150
+ */
151
+ usage: v.optional(v.object({ inputTokens: v.optional(v.number()), outputTokens: v.optional(v.number()), totalTokens: v.optional(v.number()) }))
152
+ }).index("byKey", ["key"], { unique: true }).index("byAgent", ["agent"]).index("byInstance", ["instanceId"]).public(),
153
+ // The owner-scoped graph-memory tables (`agent_entities`/`agent_edges`)
154
+ // live in graph-component.ts alongside the functions that read/write
155
+ // them; spread in here so the app still merges one `agentExtension`.
156
+ ...graphTables,
157
+ // The owner-scoped episodic-memory table (`agent_episodes`) lives in
158
+ // episodic-component.ts alongside its functions; spread in here too.
159
+ ...episodeTables
160
+ }
161
+ });
162
+ const { mutation, query } = initLunora.dataModel().create();
163
+ const agentComponent = () => {
164
+ const agentEnsureThread = mutation.input({
165
+ agent: v.string(),
166
+ // Seed the thread's synced state — set on the INSERT branch only
167
+ // (first writer wins, like owner/title), so a replay never re-seeds.
168
+ initialState: v.optional(v.any()),
169
+ instanceId: v.optional(v.string()),
170
+ key: v.string(),
171
+ onConcurrentRun: v.optional(v.union(v.literal("reject"), v.literal("queue"), v.literal("replace"))),
172
+ owner: v.optional(v.string()),
173
+ title: v.optional(v.string())
174
+ }).mutation(async ({ args, ctx: context }) => {
175
+ const now = Date.now();
176
+ const existing = await context.db.query(THREADS_TABLE).withIndex("byKey", (q) => q.eq("key", args.key)).first();
177
+ if (existing) {
178
+ if (existing["owner"] !== args.owner && args.owner !== void 0) {
179
+ throw new Error(`@lunora/agent: thread "${args.key}" belongs to another owner`);
180
+ }
181
+ const priorInstanceId = existing["instanceId"];
182
+ const isConcurrentRun = (existing["status"] === "running" || existing["status"] === "awaiting_input") && priorInstanceId !== void 0 && args.instanceId !== void 0 && priorInstanceId !== args.instanceId;
183
+ if (isConcurrentRun) {
184
+ const policy = args.onConcurrentRun ?? "reject";
185
+ if (policy !== "replace") {
186
+ throw new LunoraError(
187
+ "CONFLICT",
188
+ `@lunora/agent: thread "${args.key}" already has a run in flight (instance "${priorInstanceId}") — onConcurrentRun="${policy}"`
189
+ );
190
+ }
191
+ await context.db.patch(existing["_id"], { error: void 0, instanceId: args.instanceId, status: "running", updatedAt: now });
192
+ return { created: false, priorInstanceId, replaced: true };
193
+ }
194
+ await context.db.patch(existing["_id"], {
195
+ error: void 0,
196
+ status: "running",
197
+ updatedAt: now,
198
+ ...args.instanceId === void 0 ? {} : { instanceId: args.instanceId }
199
+ });
200
+ return { created: false };
201
+ }
202
+ await context.db.insert(THREADS_TABLE, {
203
+ agent: args.agent,
204
+ createdAt: now,
205
+ key: args.key,
206
+ messageCount: 0,
207
+ status: "running",
208
+ updatedAt: now,
209
+ ...definedColumns({ instanceId: args.instanceId, owner: args.owner, state: args.initialState, title: args.title })
210
+ });
211
+ return { created: true };
212
+ });
213
+ const agentAppendMessage = mutation.input({
214
+ content: v.string(),
215
+ messageKey: v.string(),
216
+ role: v.union(v.literal("user"), v.literal("assistant"), v.literal("tool"), v.literal("system")),
217
+ status: v.optional(v.union(v.literal("awaiting_approval"), v.literal("approved"), v.literal("rejected"))),
218
+ stepName: v.optional(v.string()),
219
+ threadKey: v.string(),
220
+ toolCallId: v.optional(v.string()),
221
+ toolCalls: v.optional(v.array(v.object({ id: v.string(), input: v.any(), name: v.string() }))),
222
+ toolName: v.optional(v.string())
223
+ }).mutation(async ({ args, ctx: context }) => {
224
+ const existing = await context.db.query(MESSAGES_TABLE).withIndex("byMessageKey", (q) => q.eq("threadKey", args.threadKey).eq("messageKey", args.messageKey)).first();
225
+ if (existing) {
226
+ return { seq: existing["seq"] };
227
+ }
228
+ const thread = await context.db.query(THREADS_TABLE).withIndex("byKey", (q) => q.eq("key", args.threadKey)).first();
229
+ if (!thread) {
230
+ throw new Error(`@lunora/agent: cannot append to unknown thread "${args.threadKey}" — run agentEnsureThread first`);
231
+ }
232
+ const seq = thread["messageCount"];
233
+ const now = Date.now();
234
+ await context.db.insert(MESSAGES_TABLE, {
235
+ content: args.content,
236
+ createdAt: now,
237
+ messageKey: args.messageKey,
238
+ role: args.role,
239
+ seq,
240
+ threadKey: args.threadKey,
241
+ ...args.status === void 0 ? {} : { status: args.status },
242
+ ...args.stepName === void 0 ? {} : { stepName: args.stepName },
243
+ ...args.toolCallId === void 0 ? {} : { toolCallId: args.toolCallId },
244
+ ...args.toolCalls === void 0 ? {} : { toolCalls: args.toolCalls },
245
+ ...args.toolName === void 0 ? {} : { toolName: args.toolName }
246
+ });
247
+ await context.db.patch(thread["_id"], { messageCount: seq + 1, updatedAt: now });
248
+ return { seq };
249
+ });
250
+ const agentPatchThread = mutation.input({
251
+ error: v.optional(v.string()),
252
+ // Target by thread key (the loop) OR by workflow instance id (cancel,
253
+ // which only knows the instance it terminated). Exactly one is set.
254
+ instanceId: v.optional(v.string()),
255
+ key: v.optional(v.string()),
256
+ status: v.optional(v.union(v.literal("idle"), v.literal("running"), v.literal("error"), v.literal("cancelled"), v.literal("awaiting_input"))),
257
+ title: v.optional(v.string()),
258
+ usage: v.optional(v.object({ inputTokens: v.optional(v.number()), outputTokens: v.optional(v.number()), totalTokens: v.optional(v.number()) }))
259
+ }).mutation(async ({ args, ctx: context }) => {
260
+ const { instanceId, key } = args;
261
+ let thread;
262
+ if (key !== void 0) {
263
+ thread = await context.db.query(THREADS_TABLE).withIndex("byKey", (q) => q.eq("key", key)).first();
264
+ } else if (instanceId !== void 0) {
265
+ thread = await context.db.query(THREADS_TABLE).withIndex("byInstance", (q) => q.eq("instanceId", instanceId)).first();
266
+ }
267
+ if (!thread) {
268
+ return;
269
+ }
270
+ await context.db.patch(thread["_id"], {
271
+ updatedAt: Date.now(),
272
+ ...args.error === void 0 ? {} : { error: args.error },
273
+ ...args.status === void 0 ? {} : { status: args.status },
274
+ ...args.title === void 0 ? {} : { title: args.title },
275
+ // The loop patches a per-run cumulative total; setting it (rather
276
+ // than adding) keeps the write idempotent under workflow replay.
277
+ ...args.usage === void 0 ? {} : { usage: args.usage }
278
+ });
279
+ });
280
+ const agentSetState = mutation.input({
281
+ key: v.string(),
282
+ state: v.any()
283
+ }).mutation(async ({ args, ctx: context }) => {
284
+ const thread = await context.db.query(THREADS_TABLE).withIndex("byKey", (q) => q.eq("key", args.key)).first();
285
+ if (!thread) {
286
+ return;
287
+ }
288
+ await context.db.patch(thread["_id"], { state: args.state, updatedAt: Date.now() });
289
+ });
290
+ const readableThread = (thread, auth) => {
291
+ if (!thread) {
292
+ return void 0;
293
+ }
294
+ const { owner } = thread;
295
+ if (owner !== void 0 && owner !== (auth.userId ?? void 0)) {
296
+ return void 0;
297
+ }
298
+ return thread;
299
+ };
300
+ const agentThread = query.input({ key: v.string() }).query(async ({ args, ctx: context }) => {
301
+ const thread = await context.db.query(THREADS_TABLE).withIndex("byKey", (q) => q.eq("key", args.key)).first();
302
+ return readableThread(thread, context.auth);
303
+ });
304
+ const agentState = query.input({ key: v.string() }).query(async ({ args, ctx: context }) => {
305
+ const thread = await context.db.query(THREADS_TABLE).withIndex("byKey", (q) => q.eq("key", args.key)).first();
306
+ return readableThread(thread, context.auth)?.["state"];
307
+ });
308
+ const agentMessages = query.input({ key: v.string(), limit: v.optional(v.number()) }).query(async ({ args, ctx: context }) => {
309
+ const thread = await context.db.query(THREADS_TABLE).withIndex("byKey", (q) => q.eq("key", args.key)).first();
310
+ if (readableThread(thread, context.auth) === void 0) {
311
+ return [];
312
+ }
313
+ const rows = await context.db.query(MESSAGES_TABLE).withIndex("byThread", (q) => q.eq("threadKey", args.key)).collect();
314
+ const ordered = rows.toSorted((a, b) => a["seq"] - b["seq"]);
315
+ return args.limit === void 0 ? ordered : ordered.slice(Math.max(0, ordered.length - args.limit));
316
+ });
317
+ const agentResolveApproval = mutation.input({
318
+ decision: v.union(v.literal("approve"), v.literal("reject")),
319
+ instanceId: v.string(),
320
+ note: v.optional(v.string()),
321
+ threadKey: v.string(),
322
+ toolCallId: v.string()
323
+ }).mutation(async ({ args, ctx: context }) => {
324
+ const thread = await context.db.query(THREADS_TABLE).withIndex("byKey", (q) => q.eq("key", args.threadKey)).first();
325
+ const readable = readableThread(thread, context.auth);
326
+ if (readable === void 0) {
327
+ throw new LunoraError("FORBIDDEN", `@lunora/agent: not allowed to resolve approvals on thread "${args.threadKey}"`);
328
+ }
329
+ const agentName = readable["agent"];
330
+ const { agents } = context;
331
+ const handle = agents?.[agentName];
332
+ if (typeof handle?.sendEvent !== "function") {
333
+ throw new LunoraError(
334
+ "INTERNAL",
335
+ `@lunora/agent: no ctx.agents["${agentName}"] producer to resolve the approval — run codegen/dev so the agent binding is wired`
336
+ );
337
+ }
338
+ await handle.sendEvent(args.instanceId, {
339
+ payload: { decision: args.decision, ...args.note === void 0 ? {} : { note: args.note } },
340
+ type: "agent-approval"
341
+ });
342
+ return { resolved: true };
343
+ });
344
+ const agentRun = mutation.input({
345
+ agent: v.string(),
346
+ input: v.string(),
347
+ threadKey: v.string(),
348
+ title: v.optional(v.string())
349
+ }).mutation(async ({ args, ctx: context }) => {
350
+ const { agents } = context;
351
+ const handle = agents?.[args.agent];
352
+ if (typeof handle?.run !== "function") {
353
+ throw new LunoraError(
354
+ "INTERNAL",
355
+ `@lunora/agent: no ctx.agents["${args.agent}"] producer to start a run — run codegen/dev so the agent binding is wired, and check the agent name`
356
+ );
357
+ }
358
+ if (handle.publicRun !== true) {
359
+ throw new LunoraError(
360
+ "FORBIDDEN",
361
+ `@lunora/agent: agent "${args.agent}" is not enabled for public runs — set defineAgent({ publicRun: true }) to allow an external client (e.g. the @lunora/mcp server) to start it`
362
+ );
363
+ }
364
+ const owner = context.auth.userId ?? void 0;
365
+ const inflight = await context.db.query(THREADS_TABLE).withIndex("byKey", (q) => q.eq("key", args.threadKey)).first();
366
+ if (inflight) {
367
+ const status = inflight["status"];
368
+ const inflightInstanceId = inflight["instanceId"];
369
+ const inflightOwner = inflight["owner"];
370
+ if ((status === "running" || status === "awaiting_input") && inflightInstanceId !== void 0 && (inflightOwner === void 0 || inflightOwner === owner)) {
371
+ return { id: inflightInstanceId, threadKey: args.threadKey };
372
+ }
373
+ if (inflightOwner !== owner && owner !== void 0) {
374
+ throw new LunoraError("FORBIDDEN", `@lunora/agent: thread "${args.threadKey}" belongs to another owner`);
375
+ }
376
+ }
377
+ const { id } = await handle.run({
378
+ input: args.input,
379
+ threadKey: args.threadKey,
380
+ ...owner === void 0 ? {} : { owner },
381
+ ...args.title === void 0 ? {} : { title: args.title }
382
+ });
383
+ return { id, threadKey: args.threadKey };
384
+ });
385
+ const graph = graphComponent();
386
+ const episodic = episodicComponent();
387
+ return {
388
+ extension: agentExtension,
389
+ functions: {
390
+ agentAppendMessage: asInternal(agentAppendMessage),
391
+ agentEnsureThread: asInternal(agentEnsureThread),
392
+ agentEpisodeRecall: episodic.agentEpisodeRecall,
393
+ agentEpisodeUpsert: episodic.agentEpisodeUpsert,
394
+ agentGraphTraverse: graph.agentGraphTraverse,
395
+ agentGraphUpsert: graph.agentGraphUpsert,
396
+ agentMessages,
397
+ agentPatchThread: asInternal(agentPatchThread),
398
+ agentResolveApproval,
399
+ agentRun,
400
+ agentSetState: asInternal(agentSetState),
401
+ agentState,
402
+ agentThread
403
+ }
404
+ };
405
+ };
406
+
407
+ export { agentComponent, agentExtension };
@@ -0,0 +1,34 @@
1
+ import { A as AgentDefinition } from "./packem_shared/types.d-BhJFTvz_.mjs";
2
+ import '@lunora/mail/inbound';
3
+ import 'ai';
4
+ /**
5
+ * One agent wired into the inbound `email()` handler.
6
+ * @experimental
7
+ */
8
+ interface AgentEmailTarget {
9
+ /**
10
+ * The agent definition — only its `onEmail` mapper is read, deciding whether
11
+ * this agent claims the message and, if so, the run to start.
12
+ */
13
+ agent: Pick<AgentDefinition, "onEmail">;
14
+ /** The `AGENT_*` Workflow binding name (off `env`) that starts a run. */
15
+ binding: string;
16
+ }
17
+ /**
18
+ * The worker `email(message, env, ctx)` callback. Typed with `unknown`
19
+ * parameters so it drops straight onto the generated `composed.email` slot
20
+ * without a cast.
21
+ * @experimental
22
+ */
23
+ type InboundAgentEmailHandler = (message: unknown, env: unknown, context: unknown) => Promise<void>;
24
+ /**
25
+ * Build the inbound `email()` handler for one or more `onEmail` agents. The
26
+ * returned callback parses the message, then walks `targets` in order and starts
27
+ * a durable run for the first agent whose `onEmail` mapper returns a run. If the
28
+ * matched agent's Workflow binding is missing from `env` (run codegen/dev so
29
+ * `wrangler.jsonc` declares it) the dispatch throws, and the inbound handler's
30
+ * `onError` rejects (bounces) the message.
31
+ * @experimental
32
+ */
33
+ declare const dispatchAgentEmail: (targets: ReadonlyArray<AgentEmailTarget>) => InboundAgentEmailHandler;
34
+ export { type AgentEmailTarget, type InboundAgentEmailHandler, dispatchAgentEmail };
@@ -0,0 +1,34 @@
1
+ import { A as AgentDefinition } from "./packem_shared/types.d-BhJFTvz_.js";
2
+ import '@lunora/mail/inbound';
3
+ import 'ai';
4
+ /**
5
+ * One agent wired into the inbound `email()` handler.
6
+ * @experimental
7
+ */
8
+ interface AgentEmailTarget {
9
+ /**
10
+ * The agent definition — only its `onEmail` mapper is read, deciding whether
11
+ * this agent claims the message and, if so, the run to start.
12
+ */
13
+ agent: Pick<AgentDefinition, "onEmail">;
14
+ /** The `AGENT_*` Workflow binding name (off `env`) that starts a run. */
15
+ binding: string;
16
+ }
17
+ /**
18
+ * The worker `email(message, env, ctx)` callback. Typed with `unknown`
19
+ * parameters so it drops straight onto the generated `composed.email` slot
20
+ * without a cast.
21
+ * @experimental
22
+ */
23
+ type InboundAgentEmailHandler = (message: unknown, env: unknown, context: unknown) => Promise<void>;
24
+ /**
25
+ * Build the inbound `email()` handler for one or more `onEmail` agents. The
26
+ * returned callback parses the message, then walks `targets` in order and starts
27
+ * a durable run for the first agent whose `onEmail` mapper returns a run. If the
28
+ * matched agent's Workflow binding is missing from `env` (run codegen/dev so
29
+ * `wrangler.jsonc` declares it) the dispatch throws, and the inbound handler's
30
+ * `onError` rejects (bounces) the message.
31
+ * @experimental
32
+ */
33
+ declare const dispatchAgentEmail: (targets: ReadonlyArray<AgentEmailTarget>) => InboundAgentEmailHandler;
34
+ export { type AgentEmailTarget, type InboundAgentEmailHandler, dispatchAgentEmail };
@@ -0,0 +1,32 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+ import { createInboundEmailHandler, parseInboundEmail } from '@lunora/mail/inbound';
3
+
4
+ const dispatchAgentEmail = (targets) => {
5
+ const handler = createInboundEmailHandler({
6
+ dispatch: async (email, context) => {
7
+ for (const target of targets) {
8
+ const mapper = target.agent.onEmail;
9
+ if (!mapper) {
10
+ continue;
11
+ }
12
+ const run = await mapper(email);
13
+ if (run === null || run === void 0) {
14
+ continue;
15
+ }
16
+ const binding = context.env[target.binding];
17
+ if (!binding || typeof binding.create !== "function") {
18
+ throw new LunoraError(
19
+ "INTERNAL",
20
+ `@lunora/agent: no Workflow binding "${target.binding}" on env for an inbound agent — run codegen/dev so wrangler.jsonc declares it`
21
+ );
22
+ }
23
+ await binding.create({ params: run });
24
+ return;
25
+ }
26
+ },
27
+ parse: parseInboundEmail
28
+ });
29
+ return async (message, env, context) => handler(message, env, context);
30
+ };
31
+
32
+ export { dispatchAgentEmail };