@lunora/agent 1.0.0-alpha.2 → 1.0.0-alpha.4

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.
@@ -52,6 +52,14 @@ interface AgentChannelTarget {
52
52
  */
53
53
  type InboundChannelHandler = (request: Request, env: Record<string, unknown>) => Promise<Response>;
54
54
  /**
55
+ * Whether a `workflow.create()` rejection is a duplicate-instance-id error — the
56
+ * idempotency signal — as opposed to a transient/config failure (Workflows
57
+ * service error, instance-creation quota, bad params). Only the former may be
58
+ * acked; every other failure MUST surface so the handler returns non-2xx and the
59
+ * provider redelivers, rather than silently dropping the event.
60
+ */
61
+ declare const isDuplicateInstanceError: (error: unknown) => boolean;
62
+ /**
55
63
  * Build an HTTP handler that starts a durable agent run from a verified inbound
56
64
  * webhook. The channel is detected from the signature headers; EACH eligible
57
65
  * target is verified against ITS OWN secret before its mapper is offered the
@@ -63,4 +71,4 @@ type InboundChannelHandler = (request: Request, env: Record<string, unknown>) =>
63
71
  * @experimental
64
72
  */
65
73
  declare const dispatchAgentChannel: (targets: ReadonlyArray<AgentChannelTarget>) => InboundChannelHandler;
66
- export { type AgentChannelTarget, type InboundChannelHandler, dispatchAgentChannel, verifyDiscord, verifyGithub, verifySlack };
74
+ export { type AgentChannelTarget, type InboundChannelHandler, dispatchAgentChannel, isDuplicateInstanceError, verifyDiscord, verifyGithub, verifySlack };
@@ -52,6 +52,14 @@ interface AgentChannelTarget {
52
52
  */
53
53
  type InboundChannelHandler = (request: Request, env: Record<string, unknown>) => Promise<Response>;
54
54
  /**
55
+ * Whether a `workflow.create()` rejection is a duplicate-instance-id error — the
56
+ * idempotency signal — as opposed to a transient/config failure (Workflows
57
+ * service error, instance-creation quota, bad params). Only the former may be
58
+ * acked; every other failure MUST surface so the handler returns non-2xx and the
59
+ * provider redelivers, rather than silently dropping the event.
60
+ */
61
+ declare const isDuplicateInstanceError: (error: unknown) => boolean;
62
+ /**
55
63
  * Build an HTTP handler that starts a durable agent run from a verified inbound
56
64
  * webhook. The channel is detected from the signature headers; EACH eligible
57
65
  * target is verified against ITS OWN secret before its mapper is offered the
@@ -63,4 +71,4 @@ type InboundChannelHandler = (request: Request, env: Record<string, unknown>) =>
63
71
  * @experimental
64
72
  */
65
73
  declare const dispatchAgentChannel: (targets: ReadonlyArray<AgentChannelTarget>) => InboundChannelHandler;
66
- export { type AgentChannelTarget, type InboundChannelHandler, dispatchAgentChannel, verifyDiscord, verifyGithub, verifySlack };
74
+ export { type AgentChannelTarget, type InboundChannelHandler, dispatchAgentChannel, isDuplicateInstanceError, verifyDiscord, verifyGithub, verifySlack };
package/dist/channels.mjs CHANGED
@@ -178,4 +178,4 @@ const dispatchAgentChannel = (targets) => async (request, env) => {
178
178
  return verifiedAny ? new Response(void 0, { status: 204 }) : new Response("Invalid signature", { status: 401 });
179
179
  };
180
180
 
181
- export { dispatchAgentChannel, verifyDiscord, verifyGithub, verifySlack };
181
+ export { dispatchAgentChannel, isDuplicateInstanceError, verifyDiscord, verifyGithub, verifySlack };
@@ -1,8 +1,8 @@
1
1
  import { LunoraError } from '@lunora/errors';
2
2
  import { defineTable, initLunora, defineSchemaExtension } from '@lunora/server';
3
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';
4
+ import { a as asInternal, A as AGENT_EXTENSION_KEY, g as graphTables, d as definedColumns, b as graphComponent } from './packem_shared/graph-component-Bbaxxymp.mjs';
5
+ export { n as normalizeEntityName } from './packem_shared/graph-component-Bbaxxymp.mjs';
6
6
  export { sandboxComponent } from './packem_shared/sandboxComponent-DR3pTwBL.mjs';
7
7
 
8
8
  const EPISODES_TABLE = "agent_episodes";
@@ -179,7 +179,7 @@ const agentComponent = () => {
179
179
  throw new Error(`@lunora/agent: thread "${args.key}" belongs to another owner`);
180
180
  }
181
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;
182
+ const isConcurrentRun = (existing["status"] === "running" || existing["status"] === "awaiting_input") && priorInstanceId !== void 0 && (args.instanceId === void 0 || args.instanceId !== priorInstanceId);
183
183
  if (isConcurrentRun) {
184
184
  const policy = args.onConcurrentRun ?? "reject";
185
185
  if (policy !== "replace") {
@@ -188,7 +188,12 @@ const agentComponent = () => {
188
188
  `@lunora/agent: thread "${args.key}" already has a run in flight (instance "${priorInstanceId}") — onConcurrentRun="${policy}"`
189
189
  );
190
190
  }
191
- await context.db.patch(existing["_id"], { error: void 0, instanceId: args.instanceId, status: "running", updatedAt: now });
191
+ await context.db.patch(existing["_id"], {
192
+ error: void 0,
193
+ status: "running",
194
+ updatedAt: now,
195
+ ...args.instanceId === void 0 ? {} : { instanceId: args.instanceId }
196
+ });
192
197
  return { created: false, priorInstanceId, replaced: true };
193
198
  }
194
199
  await context.db.patch(existing["_id"], {
@@ -310,9 +315,11 @@ const agentComponent = () => {
310
315
  if (readableThread(thread, context.auth) === void 0) {
311
316
  return [];
312
317
  }
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));
318
+ if (args.limit !== void 0) {
319
+ const tail = await context.db.query(MESSAGES_TABLE).withIndex("byThread", (q) => q.eq("threadKey", args.key)).order("desc").take(args.limit);
320
+ return tail.toReversed();
321
+ }
322
+ return context.db.query(MESSAGES_TABLE).withIndex("byThread", (q) => q.eq("threadKey", args.key)).collect();
316
323
  });
317
324
  const agentResolveApproval = mutation.input({
318
325
  decision: v.union(v.literal("approve"), v.literal("reject")),
@@ -326,6 +333,9 @@ const agentComponent = () => {
326
333
  if (readable === void 0) {
327
334
  throw new LunoraError("FORBIDDEN", `@lunora/agent: not allowed to resolve approvals on thread "${args.threadKey}"`);
328
335
  }
336
+ if (readable["instanceId"] !== args.instanceId) {
337
+ throw new LunoraError("FORBIDDEN", `@lunora/agent: instance "${args.instanceId}" does not own thread "${args.threadKey}"`);
338
+ }
329
339
  const agentName = readable["agent"];
330
340
  const { agents } = context;
331
341
  const handle = agents?.[agentName];
@@ -336,8 +346,9 @@ const agentComponent = () => {
336
346
  );
337
347
  }
338
348
  await handle.sendEvent(args.instanceId, {
339
- payload: { decision: args.decision, ...args.note === void 0 ? {} : { note: args.note } },
340
- type: "agent-approval"
349
+ payload: { decision: args.decision, toolCallId: args.toolCallId, ...args.note === void 0 ? {} : { note: args.note } },
350
+ // Scoped per tool call — see the doc comment above.
351
+ type: `agent-approval:${args.toolCallId}`
341
352
  });
342
353
  return { resolved: true };
343
354
  });
package/dist/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
- export { runAgentLoop, splitForCompaction } from './packem_shared/runAgentLoop-Dhg4ZNvw.mjs';
1
+ export { runAgentLoop, splitForCompaction } from './packem_shared/runAgentLoop-B3Q2o-Uz.mjs';
2
2
  export { collectAgenticMemoryTools, toSearchResults } from './packem_shared/collectAgenticMemoryTools-QrzpV-WX.mjs';
3
- export { agentAsTool } from './packem_shared/agentAsTool-Dt8NlU6k.mjs';
3
+ export { agentAsTool } from './packem_shared/agentAsTool-CUHlWsmt.mjs';
4
4
  export { codeTool } from './packem_shared/codeTool-CjgJOC9t.mjs';
5
5
  export { agentComponent, agentExtension } from './component.mjs';
6
6
  export { default as createAgentContext } from './packem_shared/createAgentContext-4xJGXNR4.mjs';
7
- export { defineAgent, defineAgentTool, isAgentDefinition } from './packem_shared/defineAgent-D6maSbVc.mjs';
7
+ export { defineAgent, defineAgentTool, isAgentDefinition } from './packem_shared/defineAgent-DAwAZC9P.mjs';
8
8
  export { functionTool } from './packem_shared/functionTool-D6lCa2jB.mjs';
9
9
  export { createAgentGenerate, createEpisodeExtract, createGraphExtract, createStreamGenerate, resolveAgentModel } from './packem_shared/createAgentGenerate-BQv9YJ01.mjs';
10
10
  export { adaptMcpResult, mcpTools } from './packem_shared/adaptMcpResult-wtNMvLoP.mjs';
@@ -15,4 +15,4 @@ export { browserTool, containerTool, fsTool } from './sandbox.mjs';
15
15
  export { defineSkill, isSkillDefinition } from './packem_shared/defineSkill-Ctf_S-rz.mjs';
16
16
  export { default as VoiceSessionDO } from './packem_shared/VoiceSessionDO-DLoXsHGF.mjs';
17
17
  export { runVoiceTurn } from './packem_shared/runVoiceTurn-LnqLvCRR.mjs';
18
- export { default as compileAgentWorkflow } from './packem_shared/compileAgentWorkflow-BxJjHgtD.mjs';
18
+ export { default as compileAgentWorkflow } from './packem_shared/compileAgentWorkflow-DX90058f.mjs';
@@ -1,5 +1,6 @@
1
1
  import { LunoraError } from '@lunora/errors';
2
2
  import { jsonSchema } from 'ai';
3
+ import { isDuplicateInstanceError } from '../channels.mjs';
3
4
  import { agentBindingName } from '../naming.mjs';
4
5
  import { toFunctionReference, DEFAULT_AGENT_FUNCTION_PATHS } from './AGENT_MODULE-Dnt_-AAT.mjs';
5
6
 
@@ -64,7 +65,10 @@ const agentAsTool = (options) => {
64
65
  try {
65
66
  const handle = await binding.create({ id: childInstanceId, params });
66
67
  instance = await binding.get(handle.id);
67
- } catch {
68
+ } catch (error) {
69
+ if (!isDuplicateInstanceError(error)) {
70
+ throw error;
71
+ }
68
72
  instance = await binding.get(childInstanceId);
69
73
  }
70
74
  const terminal = await pollUntilTerminal(instance, maxPolls, pollIntervalMs, wait);
@@ -1,5 +1,5 @@
1
1
  import { defineWorkflow } from '@lunora/workflow';
2
- import { runAgentLoop } from './runAgentLoop-Dhg4ZNvw.mjs';
2
+ import { runAgentLoop } from './runAgentLoop-B3Q2o-Uz.mjs';
3
3
  import { createStreamGenerate, createAgentGenerate, createEpisodeExtract, createGraphExtract, createCompact } from './createAgentGenerate-BQv9YJ01.mjs';
4
4
  import { agentDefaultName } from '../naming.mjs';
5
5
  import { DEFAULT_AGENT_FUNCTION_PATHS } from './AGENT_MODULE-Dnt_-AAT.mjs';
@@ -1,6 +1,6 @@
1
1
  import { LunoraError } from '@lunora/errors';
2
2
  import { collectAgenticMemoryTools } from './collectAgenticMemoryTools-QrzpV-WX.mjs';
3
- import { agentAsTool } from './agentAsTool-Dt8NlU6k.mjs';
3
+ import { agentAsTool } from './agentAsTool-CUHlWsmt.mjs';
4
4
  import { i as isInjectedMemorySource } from './memory-D4FPcBsX.mjs';
5
5
  import { SKILL_NAME_PATTERN, RESERVED_SKILL_NAME } from './defineSkill-Ctf_S-rz.mjs';
6
6
 
@@ -34,7 +34,7 @@ const graphTables = {
34
34
  type: v.optional(v.string()),
35
35
  updatedAt: v.number(),
36
36
  weight: v.optional(v.number())
37
- }).index("byOwnerName", ["owner", "name"], { unique: true }).public(),
37
+ }).index("byOwnerName", ["owner", "name"], { unique: true }).index("byOwnerUpdatedAt", ["owner", "updatedAt"]).public(),
38
38
  /**
39
39
  * Graph-memory edges — directed triples storing the normalized endpoint
40
40
  * NAMES (no join on write), owner-scoped like the nodes. `weight` is
@@ -57,6 +57,7 @@ const DEFAULT_GRAPH_DEPTH = 2;
57
57
  const DEFAULT_GRAPH_MAX_SEEDS = 4;
58
58
  const DEFAULT_GRAPH_FAN_OUT = 8;
59
59
  const DEFAULT_GRAPH_MAX_NODES = 32;
60
+ const GRAPH_SEED_SCAN_CAP = 500;
60
61
  const WHITESPACE_RUN = /\s+/gu;
61
62
  const NON_WORD = /[^\p{L}\p{N}]+/u;
62
63
  const normalizeEntityName = (name) => name.trim().replaceAll(WHITESPACE_RUN, " ").toLowerCase();
@@ -186,7 +187,7 @@ const graphComponent = () => {
186
187
  if (tokens.length === 0) {
187
188
  return { context: "" };
188
189
  }
189
- const ownerEntities = await context.db.query(ENTITIES_TABLE).withIndex("byOwnerName", (q) => q.eq("owner", args.owner)).collect();
190
+ const ownerEntities = await context.db.query(ENTITIES_TABLE).withIndex("byOwnerUpdatedAt", (q) => q.eq("owner", args.owner)).order("desc").take(GRAPH_SEED_SCAN_CAP);
190
191
  const seeds = ownerEntities.filter((row) => matchesSeed(row["name"], tokens)).toSorted(
191
192
  (a, b) => (b["weight"] ?? 1) - (a["weight"] ?? 1) || a["name"].localeCompare(b["name"])
192
193
  ).slice(0, args.maxSeeds ?? DEFAULT_GRAPH_MAX_SEEDS).map((row) => row["name"]);
@@ -1,3 +1,3 @@
1
1
  import '@lunora/server';
2
2
  import '@lunora/values';
3
- export { b as graphComponent, g as graphTables, n as normalizeEntityName } from './graph-component-aoUwO-f0.mjs';
3
+ export { b as graphComponent, g as graphTables, n as normalizeEntityName } from './graph-component-Bbaxxymp.mjs';
@@ -63,7 +63,7 @@ const awaitApproval = async (turnContext, call) => {
63
63
  toolName: call.name
64
64
  });
65
65
  await patchThread({ status: "awaiting_input" });
66
- const event = await step.waitForEvent(`approval:${call.id}`, { type: "agent-approval" });
66
+ const event = await step.waitForEvent(`approval:${call.id}`, { type: `agent-approval:${call.id}` });
67
67
  await patchThread({ status: "running" });
68
68
  return { decision: event.payload.decision, ...event.payload.note === void 0 ? {} : { note: event.payload.note } };
69
69
  };
@@ -154,13 +154,17 @@ declare const browserTool: (options?: BrowserToolOptions) => AgentToolDefinition
154
154
  * the model picks via `op`. The call dispatches to the auto-registered
155
155
  * `sandbox:invoke` action, which carries `ctx.containers`.
156
156
  *
157
- * By default a `fetch` runs unattended while command execution is gated behind a
158
- * human approval an `exec`, AND a `fetch` whose path resolves to the privileged
159
- * `/exec` route (both reach the same command-execution path in the container, so
160
- * gating on the `op` name alone would let a `fetch` to `/exec` run a command
161
- * unattended). Note a `fetch` can still reach any *other* container route
162
- * unattended, so scope the container's routes accordingly. Pass
163
- * `opts.needsApproval` to widen or disable the gate.
157
+ * By default a read-only (GET/HEAD/OPTIONS, or method-omitted) `fetch` runs
158
+ * unattended while everything else is gated behind a human approval an
159
+ * `exec`; a `fetch` whose path resolves to the privileged `/exec` route (both
160
+ * reach the same command-execution path in the container, so gating on the
161
+ * `op` name alone would let a `fetch` to `/exec` run a command unattended);
162
+ * and a `fetch` using a non-idempotent method (POST/PUT/PATCH/DELETE) to ANY
163
+ * route, since a prompt-injected model could otherwise mutate container state
164
+ * through some other privileged route just by avoiding the literal `/exec`
165
+ * path. A `fetch` can still reach any *other read* route unattended, so scope
166
+ * the container's GET routes accordingly. Pass `opts.needsApproval` to widen
167
+ * or disable the gate.
164
168
  *
165
169
  * ```ts
166
170
  * import { containerTool, defineAgent } from "@lunora/agent/sandbox";
package/dist/sandbox.d.ts CHANGED
@@ -154,13 +154,17 @@ declare const browserTool: (options?: BrowserToolOptions) => AgentToolDefinition
154
154
  * the model picks via `op`. The call dispatches to the auto-registered
155
155
  * `sandbox:invoke` action, which carries `ctx.containers`.
156
156
  *
157
- * By default a `fetch` runs unattended while command execution is gated behind a
158
- * human approval an `exec`, AND a `fetch` whose path resolves to the privileged
159
- * `/exec` route (both reach the same command-execution path in the container, so
160
- * gating on the `op` name alone would let a `fetch` to `/exec` run a command
161
- * unattended). Note a `fetch` can still reach any *other* container route
162
- * unattended, so scope the container's routes accordingly. Pass
163
- * `opts.needsApproval` to widen or disable the gate.
157
+ * By default a read-only (GET/HEAD/OPTIONS, or method-omitted) `fetch` runs
158
+ * unattended while everything else is gated behind a human approval an
159
+ * `exec`; a `fetch` whose path resolves to the privileged `/exec` route (both
160
+ * reach the same command-execution path in the container, so gating on the
161
+ * `op` name alone would let a `fetch` to `/exec` run a command unattended);
162
+ * and a `fetch` using a non-idempotent method (POST/PUT/PATCH/DELETE) to ANY
163
+ * route, since a prompt-injected model could otherwise mutate container state
164
+ * through some other privileged route just by avoiding the literal `/exec`
165
+ * path. A `fetch` can still reach any *other read* route unattended, so scope
166
+ * the container's GET routes accordingly. Pass `opts.needsApproval` to widen
167
+ * or disable the gate.
164
168
  *
165
169
  * ```ts
166
170
  * import { containerTool, defineAgent } from "@lunora/agent/sandbox";
package/dist/sandbox.mjs CHANGED
@@ -44,11 +44,15 @@ const normalizeContainerPath = (path) => {
44
44
  }
45
45
  return `/${segments.join("/")}`;
46
46
  };
47
+ const NON_IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["DELETE", "PATCH", "POST", "PUT"]);
47
48
  const defaultContainerGate = (input) => {
48
49
  if (input.op === "exec") {
49
50
  return true;
50
51
  }
51
- return normalizeContainerPath(input.path) === CONTAINER_EXEC_ROUTE;
52
+ if (normalizeContainerPath(input.path) === CONTAINER_EXEC_ROUTE) {
53
+ return true;
54
+ }
55
+ return NON_IDEMPOTENT_METHODS.has((input.method ?? "GET").toUpperCase());
52
56
  };
53
57
  const CONTAINER_TOOL_SCHEMA = jsonSchema({
54
58
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/agent",
3
- "version": "1.0.0-alpha.2",
3
+ "version": "1.0.0-alpha.4",
4
4
  "description": "Durable AI agents for Lunora: defineAgent compiles a replay-safe tool-loop onto Cloudflare Workflows, with DO SQLite threads and live message subscriptions",
5
5
  "keywords": [
6
6
  "agent",
@@ -76,10 +76,10 @@
76
76
  "access": "public"
77
77
  },
78
78
  "dependencies": {
79
- "@lunora/ai": "1.0.0-alpha.15",
79
+ "@lunora/ai": "1.0.0-alpha.16",
80
80
  "@lunora/errors": "1.0.0-alpha.5",
81
81
  "@lunora/mail": "1.0.0-alpha.15",
82
- "@lunora/server": "1.0.0-alpha.25",
82
+ "@lunora/server": "1.0.0-alpha.27",
83
83
  "@lunora/values": "1.0.0-alpha.8",
84
84
  "@lunora/workflow": "1.0.0-alpha.10",
85
85
  "@modelcontextprotocol/sdk": "^1.29.0",