@lunora/agent 1.0.0-alpha.10 → 1.0.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channels.mjs +1 -181
- package/dist/component.mjs +2 -418
- package/dist/inbound.mjs +1 -32
- package/dist/index.mjs +1 -18
- package/dist/naming.mjs +1 -8
- package/dist/packem_shared/AGENT_MODULE-BXx-tSCe.mjs +1 -0
- package/dist/packem_shared/VoiceSessionDO-CTaBSdXk.mjs +1 -0
- package/dist/packem_shared/adaptMcpResult-18G7zpFo.mjs +2 -0
- package/dist/packem_shared/agentAsTool-CJCTyAQG.mjs +1 -0
- package/dist/packem_shared/base64-jU-Ci6Tu.mjs +1 -0
- package/dist/packem_shared/braintrustTelemetry-CnOI5WAw.mjs +1 -0
- package/dist/packem_shared/buildModelMessages-Dm2wVNQy.mjs +5 -0
- package/dist/packem_shared/codeTool-DYJVy-bH.mjs +1 -0
- package/dist/packem_shared/collectAgenticMemoryTools-DBcP8dUa.mjs +1 -0
- package/dist/packem_shared/combineTelemetry-oLCV_EPV.mjs +1 -0
- package/dist/packem_shared/common-Dck97z3Y.mjs +1 -0
- package/dist/packem_shared/compileAgentWorkflow-Dwp-xNNO.mjs +1 -0
- package/dist/packem_shared/consoleTelemetry-CaoI-NGg.mjs +1 -0
- package/dist/packem_shared/createAgentContext-B1IfI7ro.mjs +1 -0
- package/dist/packem_shared/createAgentGenerate-BUVImo85.mjs +3 -0
- package/dist/packem_shared/createDispatchRunner-CQ8HXscn-BrqT0bIG.mjs +1 -0
- package/dist/packem_shared/defineAgent-CKhzzmys.mjs +3 -0
- package/dist/packem_shared/defineSkill-DfJxCwfZ.mjs +1 -0
- package/dist/packem_shared/functionTool-CWHhkYqd.mjs +1 -0
- package/dist/packem_shared/graph-component-B2YmC2eL.mjs +2 -0
- package/dist/packem_shared/memory-DNRyyp1b.mjs +1 -0
- package/dist/packem_shared/normalizeEntityName-BQpyssvL.mjs +1 -0
- package/dist/packem_shared/otlpTelemetry-B829MdU-.mjs +1 -0
- package/dist/packem_shared/runAgentLoop-DNAt-pGz.mjs +3 -0
- package/dist/packem_shared/runVoiceTurn-PkMsfG_v.mjs +1 -0
- package/dist/packem_shared/sandboxComponent-BhaGHsQZ.mjs +1 -0
- package/dist/packem_shared/sentryTelemetry-Dr2aPuvl.mjs +1 -0
- package/dist/sandbox.mjs +1 -113
- package/dist/telemetry/index.mjs +1 -5
- package/package.json +2 -2
- package/dist/packem_shared/AGENT_MODULE-Dnt_-AAT.mjs +0 -24
- package/dist/packem_shared/VoiceSessionDO-BdwlLaXC.mjs +0 -297
- package/dist/packem_shared/adaptMcpResult-wtNMvLoP.mjs +0 -65
- package/dist/packem_shared/agentAsTool-CUHlWsmt.mjs +0 -98
- package/dist/packem_shared/base64-BVwtgRJV.mjs +0 -18
- package/dist/packem_shared/braintrustTelemetry-TP7Kwuuj.mjs +0 -47
- package/dist/packem_shared/buildModelMessages-BWFigaoo.mjs +0 -69
- package/dist/packem_shared/codeTool-CjgJOC9t.mjs +0 -122
- package/dist/packem_shared/collectAgenticMemoryTools-QrzpV-WX.mjs +0 -97
- package/dist/packem_shared/combineTelemetry-DCyaaWAI.mjs +0 -43
- package/dist/packem_shared/common-DQXayow6.mjs +0 -89
- package/dist/packem_shared/compileAgentWorkflow-DYFFyx7i.mjs +0 -78
- package/dist/packem_shared/consoleTelemetry--3sWfu1R.mjs +0 -93
- package/dist/packem_shared/createAgentContext-4xJGXNR4.mjs +0 -50
- package/dist/packem_shared/createAgentGenerate-DO7Z96zX.mjs +0 -192
- package/dist/packem_shared/createDispatchRunner-DSbp_dph-ZHTtxy3f.mjs +0 -69
- package/dist/packem_shared/defineAgent-DAwAZC9P.mjs +0 -148
- package/dist/packem_shared/defineSkill-Ctf_S-rz.mjs +0 -22
- package/dist/packem_shared/functionTool-D6lCa2jB.mjs +0 -20
- package/dist/packem_shared/graph-component-Bbaxxymp.mjs +0 -217
- package/dist/packem_shared/memory-D4FPcBsX.mjs +0 -12
- package/dist/packem_shared/normalizeEntityName-BouctxLC.mjs +0 -3
- package/dist/packem_shared/otlpTelemetry-DU5ZmoEV.mjs +0 -177
- package/dist/packem_shared/runAgentLoop-M8PKbtWT.mjs +0 -493
- package/dist/packem_shared/runVoiceTurn-LnqLvCRR.mjs +0 -211
- package/dist/packem_shared/sandboxComponent-DR3pTwBL.mjs +0 -194
- package/dist/packem_shared/sentryTelemetry-A4F5ndh9.mjs +0 -36
package/dist/channels.mjs
CHANGED
|
@@ -1,181 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
const DEFAULT_TIMESTAMP_TOLERANCE = 300;
|
|
4
|
-
const NON_HEX = /[^0-9a-f]/iu;
|
|
5
|
-
const encoder = new TextEncoder();
|
|
6
|
-
const utf8 = (value) => new Uint8Array(encoder.encode(value));
|
|
7
|
-
const hexToBytes = (hex) => {
|
|
8
|
-
if (hex.length % 2 !== 0 || NON_HEX.test(hex)) {
|
|
9
|
-
throw new LunoraError("BAD_REQUEST", "@lunora/agent: malformed hex signature");
|
|
10
|
-
}
|
|
11
|
-
const bytes = new Uint8Array(hex.length / 2);
|
|
12
|
-
for (let index = 0; index < bytes.length; index += 1) {
|
|
13
|
-
bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
|
|
14
|
-
}
|
|
15
|
-
return bytes;
|
|
16
|
-
};
|
|
17
|
-
const verifyHmacSha256 = async (secret, message, signatureHex) => {
|
|
18
|
-
try {
|
|
19
|
-
const key = await crypto.subtle.importKey("raw", utf8(secret), { hash: "SHA-256", name: "HMAC" }, false, ["verify"]);
|
|
20
|
-
return await crypto.subtle.verify("HMAC", key, hexToBytes(signatureHex), utf8(message));
|
|
21
|
-
} catch {
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
const isFreshTimestamp = (timestampSeconds, nowSeconds, tolerance) => Number.isFinite(timestampSeconds) && Math.abs(nowSeconds - timestampSeconds) <= tolerance;
|
|
26
|
-
const verifySlack = async (options) => {
|
|
27
|
-
const { body, now = Date.now() / 1e3, signature, signingSecret, timestamp, tolerance = DEFAULT_TIMESTAMP_TOLERANCE } = options;
|
|
28
|
-
if (!signature || !timestamp || !signature.startsWith("v0=")) {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
if (!isFreshTimestamp(Number(timestamp), now, tolerance)) {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
return verifyHmacSha256(signingSecret, `v0:${timestamp}:${body}`, signature.slice("v0=".length));
|
|
35
|
-
};
|
|
36
|
-
const verifyGithub = async (options) => {
|
|
37
|
-
const { body, secret, signature } = options;
|
|
38
|
-
if (!signature?.startsWith("sha256=")) {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
return verifyHmacSha256(secret, body, signature.slice("sha256=".length));
|
|
42
|
-
};
|
|
43
|
-
const verifyDiscord = async (options) => {
|
|
44
|
-
const { body, publicKey, signature, timestamp } = options;
|
|
45
|
-
if (!signature || !timestamp) {
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
try {
|
|
49
|
-
const key = await crypto.subtle.importKey("raw", hexToBytes(publicKey), { name: "Ed25519" }, false, ["verify"]);
|
|
50
|
-
return await crypto.subtle.verify({ name: "Ed25519" }, key, hexToBytes(signature), utf8(timestamp + body));
|
|
51
|
-
} catch {
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
const detectChannel = (headers) => {
|
|
56
|
-
if (headers.has("x-slack-signature")) {
|
|
57
|
-
return "slack";
|
|
58
|
-
}
|
|
59
|
-
if (headers.has("x-hub-signature-256")) {
|
|
60
|
-
return "github";
|
|
61
|
-
}
|
|
62
|
-
if (headers.has("x-signature-ed25519")) {
|
|
63
|
-
return "discord";
|
|
64
|
-
}
|
|
65
|
-
return void 0;
|
|
66
|
-
};
|
|
67
|
-
const resolveSecret = (secret, env) => {
|
|
68
|
-
if (typeof secret === "function") {
|
|
69
|
-
return secret(env);
|
|
70
|
-
}
|
|
71
|
-
const value = env[secret];
|
|
72
|
-
return typeof value === "string" ? value : void 0;
|
|
73
|
-
};
|
|
74
|
-
const verifyChannel = async (channel, secret, headers, body) => {
|
|
75
|
-
switch (channel) {
|
|
76
|
-
case "discord": {
|
|
77
|
-
return verifyDiscord({
|
|
78
|
-
body,
|
|
79
|
-
publicKey: secret,
|
|
80
|
-
signature: headers.get("x-signature-ed25519") ?? void 0,
|
|
81
|
-
timestamp: headers.get("x-signature-timestamp") ?? void 0
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
case "github": {
|
|
85
|
-
return verifyGithub({ body, secret, signature: headers.get("x-hub-signature-256") ?? void 0 });
|
|
86
|
-
}
|
|
87
|
-
case "slack": {
|
|
88
|
-
return verifySlack({
|
|
89
|
-
body,
|
|
90
|
-
signature: headers.get("x-slack-signature") ?? void 0,
|
|
91
|
-
signingSecret: secret,
|
|
92
|
-
timestamp: headers.get("x-slack-request-timestamp") ?? void 0
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
default: {
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
const discordPongResponse = (channel, body) => {
|
|
101
|
-
if (channel !== "discord") {
|
|
102
|
-
return void 0;
|
|
103
|
-
}
|
|
104
|
-
try {
|
|
105
|
-
if (JSON.parse(body).type === 1) {
|
|
106
|
-
return Response.json({ type: 1 });
|
|
107
|
-
}
|
|
108
|
-
} catch {
|
|
109
|
-
}
|
|
110
|
-
return void 0;
|
|
111
|
-
};
|
|
112
|
-
const UNSAFE_INSTANCE_ID = /[^\w-]/gu;
|
|
113
|
-
const DUPLICATE_INSTANCE = /already[\s-]?exists/iu;
|
|
114
|
-
const ack = () => new Response(void 0, { status: 200 });
|
|
115
|
-
const isDuplicateInstanceError = (error) => DUPLICATE_INSTANCE.test(error instanceof Error ? error.message : String(error));
|
|
116
|
-
const deliveryId = (channel, headers, body) => {
|
|
117
|
-
if (channel === "github") {
|
|
118
|
-
return headers.get("x-github-delivery") ?? void 0;
|
|
119
|
-
}
|
|
120
|
-
try {
|
|
121
|
-
const parsed = JSON.parse(body);
|
|
122
|
-
const raw = channel === "slack" ? parsed.event_id : parsed.id;
|
|
123
|
-
return typeof raw === "string" ? raw : void 0;
|
|
124
|
-
} catch {
|
|
125
|
-
return void 0;
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
const startChannelRun = async (workflow, run, channel, id) => {
|
|
129
|
-
const sanitizedId = id === void 0 ? "" : id.replaceAll(UNSAFE_INSTANCE_ID, "");
|
|
130
|
-
if (sanitizedId === "") {
|
|
131
|
-
await workflow.create({ params: run });
|
|
132
|
-
return ack();
|
|
133
|
-
}
|
|
134
|
-
try {
|
|
135
|
-
await workflow.create({ id: `${channel}-${sanitizedId}`.slice(0, 60), params: run });
|
|
136
|
-
} catch (error) {
|
|
137
|
-
if (isDuplicateInstanceError(error)) {
|
|
138
|
-
return ack();
|
|
139
|
-
}
|
|
140
|
-
throw error;
|
|
141
|
-
}
|
|
142
|
-
return ack();
|
|
143
|
-
};
|
|
144
|
-
const dispatchAgentChannel = (targets) => async (request, env) => {
|
|
145
|
-
const body = await request.text();
|
|
146
|
-
const channel = detectChannel(request.headers);
|
|
147
|
-
if (channel === void 0) {
|
|
148
|
-
return new Response("Unrecognized webhook", { status: 400 });
|
|
149
|
-
}
|
|
150
|
-
const eligible = targets.map((target) => {
|
|
151
|
-
return { config: target.agent.onInbound, target };
|
|
152
|
-
}).filter((entry) => entry.config?.channel === channel);
|
|
153
|
-
const event = { channel, headers: request.headers, json: () => JSON.parse(body), rawBody: body };
|
|
154
|
-
let verifiedAny = false;
|
|
155
|
-
for (const { config, target } of eligible) {
|
|
156
|
-
const secret = resolveSecret(config.secret, env);
|
|
157
|
-
if (secret === void 0 || !await verifyChannel(channel, secret, request.headers, body)) {
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
verifiedAny = true;
|
|
161
|
-
const pong = discordPongResponse(channel, body);
|
|
162
|
-
if (pong) {
|
|
163
|
-
return pong;
|
|
164
|
-
}
|
|
165
|
-
const run = await config.map(event);
|
|
166
|
-
if (!run) {
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
const workflow = env[target.binding];
|
|
170
|
-
if (!workflow || typeof workflow.create !== "function") {
|
|
171
|
-
throw new LunoraError(
|
|
172
|
-
"INTERNAL",
|
|
173
|
-
`@lunora/agent: no Workflow binding "${target.binding}" on env for an inbound-channel agent — run codegen/dev so wrangler.jsonc declares it`
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
return await startChannelRun(workflow, run, channel, deliveryId(channel, request.headers, body));
|
|
177
|
-
}
|
|
178
|
-
return verifiedAny ? new Response(void 0, { status: 204 }) : new Response("Invalid signature", { status: 401 });
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
export { dispatchAgentChannel, isDuplicateInstanceError, verifyDiscord, verifyGithub, verifySlack };
|
|
1
|
+
import{LunoraError as w}from"@lunora/errors";const m=300,v=/[^0-9a-f]/iu,x=new TextEncoder,d=e=>new Uint8Array(x.encode(e)),g=e=>{if(e.length%2!==0||v.test(e))throw new w("BAD_REQUEST","@lunora/agent: malformed hex signature");const t=new Uint8Array(e.length/2);for(let r=0;r<t.length;r+=1)t[r]=Number.parseInt(e.slice(r*2,r*2+2),16);return t},b=async(e,t,r)=>{try{const s=await crypto.subtle.importKey("raw",d(e),{hash:"SHA-256",name:"HMAC"},!1,["verify"]);return await crypto.subtle.verify("HMAC",s,g(r),d(t))}catch{return!1}},k=(e,t,r)=>Number.isFinite(e)&&Math.abs(t-e)<=r,A=async e=>{const{body:t,now:r=Date.now()/1e3,signature:s,signingSecret:n,timestamp:a,tolerance:o=m}=e;return!s||!a||!s.startsWith("v0=")||!k(Number(a),r,o)?!1:b(n,`v0:${a}:${t}`,s.slice(3))},E=async e=>{const{body:t,secret:r,signature:s}=e;return s?.startsWith("sha256=")?b(r,t,s.slice(7)):!1},S=async e=>{const{body:t,publicKey:r,signature:s,timestamp:n}=e;if(!s||!n)return!1;try{const a=await crypto.subtle.importKey("raw",g(r),{name:"Ed25519"},!1,["verify"]);return await crypto.subtle.verify({name:"Ed25519"},a,g(s),d(n+t))}catch{return!1}},N=e=>{if(e.has("x-slack-signature"))return"slack";if(e.has("x-hub-signature-256"))return"github";if(e.has("x-signature-ed25519"))return"discord"},R=(e,t)=>{if(typeof e=="function")return e(t);const r=t[e];return typeof r=="string"?r:void 0},I=async(e,t,r,s)=>{switch(e){case"discord":return S({body:s,publicKey:t,signature:r.get("x-signature-ed25519")??void 0,timestamp:r.get("x-signature-timestamp")??void 0});case"github":return E({body:s,secret:t,signature:r.get("x-hub-signature-256")??void 0});case"slack":return A({body:s,signature:r.get("x-slack-signature")??void 0,signingSecret:t,timestamp:r.get("x-slack-request-timestamp")??void 0});default:return!1}},D=(e,t)=>{if(e==="discord")try{if(JSON.parse(t).type===1)return Response.json({type:1})}catch{}},K=/[^\w-]/gu,U=/already[\s-]?exists/iu,u=()=>new Response(void 0,{status:200}),$=e=>U.test(e instanceof Error?e.message:String(e)),C=(e,t,r)=>{if(e==="github")return t.get("x-github-delivery")??void 0;try{const s=JSON.parse(r),n=e==="slack"?s.event_id:s.id;return typeof n=="string"?n:void 0}catch{return}},H=async(e,t,r,s)=>{const n=s===void 0?"":s.replaceAll(K,"");if(n==="")return await e.create({params:t}),u();try{await e.create({id:`${r}-${n}`.slice(0,60),params:t})}catch(a){if($(a))return u();throw a}return u()},j=e=>async(t,r)=>{const s=await t.text(),n=N(t.headers);if(n===void 0)return new Response("Unrecognized webhook",{status:400});const a=e.map(i=>({config:i.agent.onInbound,target:i})).filter(i=>i.config?.channel===n),o={channel:n,headers:t.headers,json:()=>JSON.parse(s),rawBody:s};let f=!1;for(const{config:i,target:l}of a){const y=R(i.secret,r);if(y===void 0||!await I(n,y,t.headers,s))continue;f=!0;const h=D(n,s);if(h)return h;const p=await i.map(o);if(!p)continue;const c=r[l.binding];if(!c||typeof c.create!="function")throw new w("INTERNAL",`@lunora/agent: no Workflow binding "${l.binding}" on env for an inbound-channel agent — run codegen/dev so wrangler.jsonc declares it`);return await H(c,p,n,C(n,t.headers,s))}return f?new Response(void 0,{status:204}):new Response("Invalid signature",{status:401})};export{j as dispatchAgentChannel,$ as isDuplicateInstanceError,S as verifyDiscord,E as verifyGithub,A as verifySlack};
|
package/dist/component.mjs
CHANGED
|
@@ -1,418 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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-Bbaxxymp.mjs';
|
|
5
|
-
export { n as normalizeEntityName } from './packem_shared/graph-component-Bbaxxymp.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 || args.instanceId !== priorInstanceId);
|
|
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"], {
|
|
192
|
-
error: void 0,
|
|
193
|
-
status: "running",
|
|
194
|
-
updatedAt: now,
|
|
195
|
-
...args.instanceId === void 0 ? {} : { instanceId: args.instanceId }
|
|
196
|
-
});
|
|
197
|
-
return { created: false, priorInstanceId, replaced: true };
|
|
198
|
-
}
|
|
199
|
-
await context.db.patch(existing["_id"], {
|
|
200
|
-
error: void 0,
|
|
201
|
-
status: "running",
|
|
202
|
-
updatedAt: now,
|
|
203
|
-
...args.instanceId === void 0 ? {} : { instanceId: args.instanceId }
|
|
204
|
-
});
|
|
205
|
-
return { created: false };
|
|
206
|
-
}
|
|
207
|
-
await context.db.insert(THREADS_TABLE, {
|
|
208
|
-
agent: args.agent,
|
|
209
|
-
createdAt: now,
|
|
210
|
-
key: args.key,
|
|
211
|
-
messageCount: 0,
|
|
212
|
-
status: "running",
|
|
213
|
-
updatedAt: now,
|
|
214
|
-
...definedColumns({ instanceId: args.instanceId, owner: args.owner, state: args.initialState, title: args.title })
|
|
215
|
-
});
|
|
216
|
-
return { created: true };
|
|
217
|
-
});
|
|
218
|
-
const agentAppendMessage = mutation.input({
|
|
219
|
-
content: v.string(),
|
|
220
|
-
messageKey: v.string(),
|
|
221
|
-
role: v.union(v.literal("user"), v.literal("assistant"), v.literal("tool"), v.literal("system")),
|
|
222
|
-
status: v.optional(v.union(v.literal("awaiting_approval"), v.literal("approved"), v.literal("rejected"))),
|
|
223
|
-
stepName: v.optional(v.string()),
|
|
224
|
-
threadKey: v.string(),
|
|
225
|
-
toolCallId: v.optional(v.string()),
|
|
226
|
-
toolCalls: v.optional(v.array(v.object({ id: v.string(), input: v.any(), name: v.string() }))),
|
|
227
|
-
toolName: v.optional(v.string())
|
|
228
|
-
}).mutation(async ({ args, ctx: context }) => {
|
|
229
|
-
const existing = await context.db.query(MESSAGES_TABLE).withIndex("byMessageKey", (q) => q.eq("threadKey", args.threadKey).eq("messageKey", args.messageKey)).first();
|
|
230
|
-
if (existing) {
|
|
231
|
-
return { seq: existing["seq"] };
|
|
232
|
-
}
|
|
233
|
-
const thread = await context.db.query(THREADS_TABLE).withIndex("byKey", (q) => q.eq("key", args.threadKey)).first();
|
|
234
|
-
if (!thread) {
|
|
235
|
-
throw new Error(`@lunora/agent: cannot append to unknown thread "${args.threadKey}" — run agentEnsureThread first`);
|
|
236
|
-
}
|
|
237
|
-
const seq = thread["messageCount"];
|
|
238
|
-
const now = Date.now();
|
|
239
|
-
await context.db.insert(MESSAGES_TABLE, {
|
|
240
|
-
content: args.content,
|
|
241
|
-
createdAt: now,
|
|
242
|
-
messageKey: args.messageKey,
|
|
243
|
-
role: args.role,
|
|
244
|
-
seq,
|
|
245
|
-
threadKey: args.threadKey,
|
|
246
|
-
...args.status === void 0 ? {} : { status: args.status },
|
|
247
|
-
...args.stepName === void 0 ? {} : { stepName: args.stepName },
|
|
248
|
-
...args.toolCallId === void 0 ? {} : { toolCallId: args.toolCallId },
|
|
249
|
-
...args.toolCalls === void 0 ? {} : { toolCalls: args.toolCalls },
|
|
250
|
-
...args.toolName === void 0 ? {} : { toolName: args.toolName }
|
|
251
|
-
});
|
|
252
|
-
await context.db.patch(thread["_id"], { messageCount: seq + 1, updatedAt: now });
|
|
253
|
-
return { seq };
|
|
254
|
-
});
|
|
255
|
-
const agentPatchThread = mutation.input({
|
|
256
|
-
error: v.optional(v.string()),
|
|
257
|
-
// Target by thread key (the loop) OR by workflow instance id (cancel,
|
|
258
|
-
// which only knows the instance it terminated). Exactly one is set.
|
|
259
|
-
instanceId: v.optional(v.string()),
|
|
260
|
-
key: v.optional(v.string()),
|
|
261
|
-
status: v.optional(v.union(v.literal("idle"), v.literal("running"), v.literal("error"), v.literal("cancelled"), v.literal("awaiting_input"))),
|
|
262
|
-
title: v.optional(v.string()),
|
|
263
|
-
usage: v.optional(v.object({ inputTokens: v.optional(v.number()), outputTokens: v.optional(v.number()), totalTokens: v.optional(v.number()) }))
|
|
264
|
-
}).mutation(async ({ args, ctx: context }) => {
|
|
265
|
-
const { instanceId, key } = args;
|
|
266
|
-
let thread;
|
|
267
|
-
if (key !== void 0) {
|
|
268
|
-
thread = await context.db.query(THREADS_TABLE).withIndex("byKey", (q) => q.eq("key", key)).first();
|
|
269
|
-
} else if (instanceId !== void 0) {
|
|
270
|
-
thread = await context.db.query(THREADS_TABLE).withIndex("byInstance", (q) => q.eq("instanceId", instanceId)).first();
|
|
271
|
-
}
|
|
272
|
-
if (!thread) {
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
await context.db.patch(thread["_id"], {
|
|
276
|
-
updatedAt: Date.now(),
|
|
277
|
-
...args.error === void 0 ? {} : { error: args.error },
|
|
278
|
-
...args.status === void 0 ? {} : { status: args.status },
|
|
279
|
-
...args.title === void 0 ? {} : { title: args.title },
|
|
280
|
-
// The loop patches a per-run cumulative total; setting it (rather
|
|
281
|
-
// than adding) keeps the write idempotent under workflow replay.
|
|
282
|
-
...args.usage === void 0 ? {} : { usage: args.usage }
|
|
283
|
-
});
|
|
284
|
-
});
|
|
285
|
-
const agentSetState = mutation.input({
|
|
286
|
-
key: v.string(),
|
|
287
|
-
state: v.any()
|
|
288
|
-
}).mutation(async ({ args, ctx: context }) => {
|
|
289
|
-
const thread = await context.db.query(THREADS_TABLE).withIndex("byKey", (q) => q.eq("key", args.key)).first();
|
|
290
|
-
if (!thread) {
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
await context.db.patch(thread["_id"], { state: args.state, updatedAt: Date.now() });
|
|
294
|
-
});
|
|
295
|
-
const readableThread = (thread, auth) => {
|
|
296
|
-
if (!thread) {
|
|
297
|
-
return void 0;
|
|
298
|
-
}
|
|
299
|
-
const { owner } = thread;
|
|
300
|
-
if (owner !== void 0 && owner !== (auth.userId ?? void 0)) {
|
|
301
|
-
return void 0;
|
|
302
|
-
}
|
|
303
|
-
return thread;
|
|
304
|
-
};
|
|
305
|
-
const agentThread = query.input({ key: v.string() }).query(async ({ args, ctx: context }) => {
|
|
306
|
-
const thread = await context.db.query(THREADS_TABLE).withIndex("byKey", (q) => q.eq("key", args.key)).first();
|
|
307
|
-
return readableThread(thread, context.auth);
|
|
308
|
-
});
|
|
309
|
-
const agentState = query.input({ key: v.string() }).query(async ({ args, ctx: context }) => {
|
|
310
|
-
const thread = await context.db.query(THREADS_TABLE).withIndex("byKey", (q) => q.eq("key", args.key)).first();
|
|
311
|
-
return readableThread(thread, context.auth)?.["state"];
|
|
312
|
-
});
|
|
313
|
-
const agentMessages = query.input({ key: v.string(), limit: v.optional(v.number()) }).query(async ({ args, ctx: context }) => {
|
|
314
|
-
const thread = await context.db.query(THREADS_TABLE).withIndex("byKey", (q) => q.eq("key", args.key)).first();
|
|
315
|
-
if (readableThread(thread, context.auth) === void 0) {
|
|
316
|
-
return [];
|
|
317
|
-
}
|
|
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();
|
|
323
|
-
});
|
|
324
|
-
const agentResolveApproval = mutation.input({
|
|
325
|
-
decision: v.union(v.literal("approve"), v.literal("reject")),
|
|
326
|
-
instanceId: v.string(),
|
|
327
|
-
note: v.optional(v.string()),
|
|
328
|
-
threadKey: v.string(),
|
|
329
|
-
toolCallId: v.string()
|
|
330
|
-
}).mutation(async ({ args, ctx: context }) => {
|
|
331
|
-
const thread = await context.db.query(THREADS_TABLE).withIndex("byKey", (q) => q.eq("key", args.threadKey)).first();
|
|
332
|
-
const readable = readableThread(thread, context.auth);
|
|
333
|
-
if (readable === void 0) {
|
|
334
|
-
throw new LunoraError("FORBIDDEN", `@lunora/agent: not allowed to resolve approvals on thread "${args.threadKey}"`);
|
|
335
|
-
}
|
|
336
|
-
if (readable["instanceId"] !== args.instanceId) {
|
|
337
|
-
throw new LunoraError("FORBIDDEN", `@lunora/agent: instance "${args.instanceId}" does not own thread "${args.threadKey}"`);
|
|
338
|
-
}
|
|
339
|
-
const agentName = readable["agent"];
|
|
340
|
-
const { agents } = context;
|
|
341
|
-
const handle = agents?.[agentName];
|
|
342
|
-
if (typeof handle?.sendEvent !== "function") {
|
|
343
|
-
throw new LunoraError(
|
|
344
|
-
"INTERNAL",
|
|
345
|
-
`@lunora/agent: no ctx.agents["${agentName}"] producer to resolve the approval — run codegen/dev so the agent binding is wired`
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
await handle.sendEvent(args.instanceId, {
|
|
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}`
|
|
352
|
-
});
|
|
353
|
-
return { resolved: true };
|
|
354
|
-
});
|
|
355
|
-
const agentRun = mutation.input({
|
|
356
|
-
agent: v.string(),
|
|
357
|
-
input: v.string(),
|
|
358
|
-
threadKey: v.string(),
|
|
359
|
-
title: v.optional(v.string())
|
|
360
|
-
}).mutation(async ({ args, ctx: context }) => {
|
|
361
|
-
const { agents } = context;
|
|
362
|
-
const handle = agents?.[args.agent];
|
|
363
|
-
if (typeof handle?.run !== "function") {
|
|
364
|
-
throw new LunoraError(
|
|
365
|
-
"INTERNAL",
|
|
366
|
-
`@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`
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
if (handle.publicRun !== true) {
|
|
370
|
-
throw new LunoraError(
|
|
371
|
-
"FORBIDDEN",
|
|
372
|
-
`@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`
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
const owner = context.auth.userId ?? void 0;
|
|
376
|
-
const inflight = await context.db.query(THREADS_TABLE).withIndex("byKey", (q) => q.eq("key", args.threadKey)).first();
|
|
377
|
-
if (inflight) {
|
|
378
|
-
const status = inflight["status"];
|
|
379
|
-
const inflightInstanceId = inflight["instanceId"];
|
|
380
|
-
const inflightOwner = inflight["owner"];
|
|
381
|
-
if ((status === "running" || status === "awaiting_input") && inflightInstanceId !== void 0 && (inflightOwner === void 0 || inflightOwner === owner)) {
|
|
382
|
-
return { id: inflightInstanceId, threadKey: args.threadKey };
|
|
383
|
-
}
|
|
384
|
-
if (inflightOwner !== owner && owner !== void 0) {
|
|
385
|
-
throw new LunoraError("FORBIDDEN", `@lunora/agent: thread "${args.threadKey}" belongs to another owner`);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
const { id } = await handle.run({
|
|
389
|
-
input: args.input,
|
|
390
|
-
threadKey: args.threadKey,
|
|
391
|
-
...owner === void 0 ? {} : { owner },
|
|
392
|
-
...args.title === void 0 ? {} : { title: args.title }
|
|
393
|
-
});
|
|
394
|
-
return { id, threadKey: args.threadKey };
|
|
395
|
-
});
|
|
396
|
-
const graph = graphComponent();
|
|
397
|
-
const episodic = episodicComponent();
|
|
398
|
-
return {
|
|
399
|
-
extension: agentExtension,
|
|
400
|
-
functions: {
|
|
401
|
-
agentAppendMessage: asInternal(agentAppendMessage),
|
|
402
|
-
agentEnsureThread: asInternal(agentEnsureThread),
|
|
403
|
-
agentEpisodeRecall: episodic.agentEpisodeRecall,
|
|
404
|
-
agentEpisodeUpsert: episodic.agentEpisodeUpsert,
|
|
405
|
-
agentGraphTraverse: graph.agentGraphTraverse,
|
|
406
|
-
agentGraphUpsert: graph.agentGraphUpsert,
|
|
407
|
-
agentMessages,
|
|
408
|
-
agentPatchThread: asInternal(agentPatchThread),
|
|
409
|
-
agentResolveApproval,
|
|
410
|
-
agentRun,
|
|
411
|
-
agentSetState: asInternal(agentSetState),
|
|
412
|
-
agentState,
|
|
413
|
-
agentThread
|
|
414
|
-
}
|
|
415
|
-
};
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
export { agentComponent, agentExtension };
|
|
1
|
+
import{LunoraError as w}from"@lunora/errors";import{defineTable as x,initLunora as $,defineSchemaExtension as j}from"@lunora/server";import{v as e}from"@lunora/values";import{i as b,o as k,q as O,s as S,j as F}from"./packem_shared/graph-component-B2YmC2eL.mjs";import{p as ne}from"./packem_shared/graph-component-B2YmC2eL.mjs";import{sandboxComponent as ie}from"./packem_shared/sandboxComponent-BhaGHsQZ.mjs";const I="agent_episodes",L=5,U=20,B=500,N=200,G=/\s+/gu,z=c=>c===void 0||!Number.isFinite(c)?L:Math.min(U,Math.max(1,Math.trunc(c))),P={episodes:x({createdAt:e.number(),messageKey:e.string(),owner:e.string(),summary:e.string(),threadKey:e.optional(e.string())}).index("byOwnerCreatedAt",["owner","createdAt"]).index("byOwnerMessageKey",["owner","messageKey"],{unique:!0}).public()},{mutation:H,query:J}=$.dataModel().create(),Q=()=>{const c=H.input({createdAt:e.optional(e.number()),messageKey:e.string(),owner:e.string(),summary:e.string(),threadKey:e.optional(e.string())}).mutation(async({args:s,ctx:u})=>{const l=s.summary.replaceAll(G," ").trim().slice(0,B);if(l.length===0)return{recorded:!1};if(await u.db.query(I).withIndex("byOwnerMessageKey",p=>p.eq("owner",s.owner).eq("messageKey",s.messageKey)).first())return{recorded:!1};await u.db.insert(I,{createdAt:s.createdAt??Date.now(),messageKey:s.messageKey,owner:s.owner,summary:l,...s.threadKey===void 0?{}:{threadKey:s.threadKey}});const g=await u.db.query(I).withIndex("byOwnerCreatedAt",p=>p.eq("owner",s.owner)).order("asc").take(N+1);for(const p of g.slice(0,Math.max(0,g.length-N)))await u.db.delete(p._id);return{recorded:!0}}),v=J.input({limit:e.optional(e.number()),owner:e.string()}).query(async({args:s,ctx:u})=>{const l=await u.db.query(I).withIndex("byOwnerCreatedAt",g=>g.eq("owner",s.owner)).order("desc").take(z(s.limit));return l.length===0?{context:""}:{context:l.toReversed().map(g=>`- ${g.summary}`).join(`
|
|
2
|
+
`)}});return{agentEpisodeRecall:b(v),agentEpisodeUpsert:b(c)}},T="threads",R="messages",d=`${k}_${T}`,K=`${k}_${R}`,V=j(k,{tables:{[R]:x({content:e.string(),createdAt:e.number(),messageKey:e.string(),role:e.union(e.literal("user"),e.literal("assistant"),e.literal("tool"),e.literal("system")),seq:e.number(),status:e.optional(e.union(e.literal("awaiting_approval"),e.literal("approved"),e.literal("rejected"))),stepName:e.optional(e.string()),threadKey:e.string(),toolCallId:e.optional(e.string()),toolCalls:e.optional(e.array(e.object({id:e.string(),input:e.any(),name:e.string()}))),toolName:e.optional(e.string())}).index("byThread",["threadKey","seq"]).index("byMessageKey",["threadKey","messageKey"],{unique:!0}).public(),[T]:x({agent:e.string(),createdAt:e.number(),error:e.optional(e.string()),instanceId:e.optional(e.string()),key:e.string(),messageCount:e.number(),owner:e.optional(e.string()),state:e.optional(e.any()),status:e.union(e.literal("idle"),e.literal("running"),e.literal("error"),e.literal("cancelled"),e.literal("awaiting_input")),title:e.optional(e.string()),updatedAt:e.number(),usage:e.optional(e.object({inputTokens:e.optional(e.number()),outputTokens:e.optional(e.number()),totalTokens:e.optional(e.number())}))}).index("byKey",["key"],{unique:!0}).index("byAgent",["agent"]).index("byInstance",["instanceId"]).public(),...O,...P}}),{mutation:m,query:q}=$.dataModel().create(),ee=()=>{const c=m.input({agent:e.string(),initialState:e.optional(e.any()),instanceId:e.optional(e.string()),key:e.string(),onConcurrentRun:e.optional(e.union(e.literal("reject"),e.literal("queue"),e.literal("replace"))),owner:e.optional(e.string()),title:e.optional(e.string())}).mutation(async({args:t,ctx:a})=>{const r=Date.now(),n=await a.db.query(d).withIndex("byKey",i=>i.eq("key",t.key)).first();if(n){if(n.owner!==t.owner&&t.owner!==void 0)throw new Error(`@lunora/agent: thread "${t.key}" belongs to another owner`);const i=n.instanceId;if((n.status==="running"||n.status==="awaiting_input")&&i!==void 0&&(t.instanceId===void 0||t.instanceId!==i)){const o=t.onConcurrentRun??"reject";if(o!=="replace")throw new w("CONFLICT",`@lunora/agent: thread "${t.key}" already has a run in flight (instance "${i}") — onConcurrentRun="${o}"`);return await a.db.patch(n._id,{error:void 0,status:"running",updatedAt:r,...t.instanceId===void 0?{}:{instanceId:t.instanceId}}),{created:!1,priorInstanceId:i,replaced:!0}}return await a.db.patch(n._id,{error:void 0,status:"running",updatedAt:r,...t.instanceId===void 0?{}:{instanceId:t.instanceId}}),{created:!1}}return await a.db.insert(d,{agent:t.agent,createdAt:r,key:t.key,messageCount:0,status:"running",updatedAt:r,...S({instanceId:t.instanceId,owner:t.owner,state:t.initialState,title:t.title})}),{created:!0}}),v=m.input({content:e.string(),messageKey:e.string(),role:e.union(e.literal("user"),e.literal("assistant"),e.literal("tool"),e.literal("system")),status:e.optional(e.union(e.literal("awaiting_approval"),e.literal("approved"),e.literal("rejected"))),stepName:e.optional(e.string()),threadKey:e.string(),toolCallId:e.optional(e.string()),toolCalls:e.optional(e.array(e.object({id:e.string(),input:e.any(),name:e.string()}))),toolName:e.optional(e.string())}).mutation(async({args:t,ctx:a})=>{const r=await a.db.query(K).withIndex("byMessageKey",y=>y.eq("threadKey",t.threadKey).eq("messageKey",t.messageKey)).first();if(r)return{seq:r.seq};const n=await a.db.query(d).withIndex("byKey",y=>y.eq("key",t.threadKey)).first();if(!n)throw new Error(`@lunora/agent: cannot append to unknown thread "${t.threadKey}" — run agentEnsureThread first`);const i=n.messageCount,o=Date.now();return await a.db.insert(K,{content:t.content,createdAt:o,messageKey:t.messageKey,role:t.role,seq:i,threadKey:t.threadKey,...t.status===void 0?{}:{status:t.status},...t.stepName===void 0?{}:{stepName:t.stepName},...t.toolCallId===void 0?{}:{toolCallId:t.toolCallId},...t.toolCalls===void 0?{}:{toolCalls:t.toolCalls},...t.toolName===void 0?{}:{toolName:t.toolName}}),await a.db.patch(n._id,{messageCount:i+1,updatedAt:o}),{seq:i}}),s=m.input({error:e.optional(e.string()),instanceId:e.optional(e.string()),key:e.optional(e.string()),status:e.optional(e.union(e.literal("idle"),e.literal("running"),e.literal("error"),e.literal("cancelled"),e.literal("awaiting_input"))),title:e.optional(e.string()),usage:e.optional(e.object({inputTokens:e.optional(e.number()),outputTokens:e.optional(e.number()),totalTokens:e.optional(e.number())}))}).mutation(async({args:t,ctx:a})=>{const{instanceId:r,key:n}=t;let i;n!==void 0?i=await a.db.query(d).withIndex("byKey",o=>o.eq("key",n)).first():r!==void 0&&(i=await a.db.query(d).withIndex("byInstance",o=>o.eq("instanceId",r)).first()),i&&await a.db.patch(i._id,{updatedAt:Date.now(),...t.error===void 0?{}:{error:t.error},...t.status===void 0?{}:{status:t.status},...t.title===void 0?{}:{title:t.title},...t.usage===void 0?{}:{usage:t.usage}})}),u=m.input({key:e.string(),state:e.any()}).mutation(async({args:t,ctx:a})=>{const r=await a.db.query(d).withIndex("byKey",n=>n.eq("key",t.key)).first();r&&await a.db.patch(r._id,{state:t.state,updatedAt:Date.now()})}),l=(t,a)=>{if(!t)return;const{owner:r}=t;if(!(r!==void 0&&r!==(a.userId??void 0)))return t},g=q.input({key:e.string()}).query(async({args:t,ctx:a})=>{const r=await a.db.query(d).withIndex("byKey",n=>n.eq("key",t.key)).first();return l(r,a.auth)}),p=q.input({key:e.string()}).query(async({args:t,ctx:a})=>{const r=await a.db.query(d).withIndex("byKey",n=>n.eq("key",t.key)).first();return l(r,a.auth)?.state}),_=q.input({key:e.string(),limit:e.optional(e.number())}).query(async({args:t,ctx:a})=>{const r=await a.db.query(d).withIndex("byKey",n=>n.eq("key",t.key)).first();return l(r,a.auth)===void 0?[]:t.limit!==void 0?(await a.db.query(K).withIndex("byThread",n=>n.eq("threadKey",t.key)).order("desc").take(t.limit)).toReversed():a.db.query(K).withIndex("byThread",n=>n.eq("threadKey",t.key)).collect()}),D=m.input({decision:e.union(e.literal("approve"),e.literal("reject")),instanceId:e.string(),note:e.optional(e.string()),threadKey:e.string(),toolCallId:e.string()}).mutation(async({args:t,ctx:a})=>{const r=await a.db.query(d).withIndex("byKey",h=>h.eq("key",t.threadKey)).first(),n=l(r,a.auth);if(n===void 0)throw new w("FORBIDDEN",`@lunora/agent: not allowed to resolve approvals on thread "${t.threadKey}"`);if(n.instanceId!==t.instanceId)throw new w("FORBIDDEN",`@lunora/agent: instance "${t.instanceId}" does not own thread "${t.threadKey}"`);const i=n.agent,{agents:o}=a,y=o?.[i];if(typeof y?.sendEvent!="function")throw new w("INTERNAL",`@lunora/agent: no ctx.agents["${i}"] producer to resolve the approval — run codegen/dev so the agent binding is wired`);return await y.sendEvent(t.instanceId,{payload:{decision:t.decision,toolCallId:t.toolCallId,...t.note===void 0?{}:{note:t.note}},type:`agent-approval:${t.toolCallId}`}),{resolved:!0}}),M=m.input({agent:e.string(),input:e.string(),threadKey:e.string(),title:e.optional(e.string())}).mutation(async({args:t,ctx:a})=>{const{agents:r}=a,n=r?.[t.agent];if(typeof n?.run!="function")throw new w("INTERNAL",`@lunora/agent: no ctx.agents["${t.agent}"] producer to start a run — run codegen/dev so the agent binding is wired, and check the agent name`);if(n.publicRun!==!0)throw new w("FORBIDDEN",`@lunora/agent: agent "${t.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`);const i=a.auth.userId??void 0,o=await a.db.query(d).withIndex("byKey",h=>h.eq("key",t.threadKey)).first();if(o){const h=o.status,E=o.instanceId,f=o.owner;if((h==="running"||h==="awaiting_input")&&E!==void 0&&(f===void 0||f===i))return{id:E,threadKey:t.threadKey};if(f!==i&&i!==void 0)throw new w("FORBIDDEN",`@lunora/agent: thread "${t.threadKey}" belongs to another owner`)}const{id:y}=await n.run({input:t.input,threadKey:t.threadKey,...i===void 0?{}:{owner:i},...t.title===void 0?{}:{title:t.title}});return{id:y,threadKey:t.threadKey}}),C=F(),A=Q();return{extension:V,functions:{agentAppendMessage:b(v),agentEnsureThread:b(c),agentEpisodeRecall:A.agentEpisodeRecall,agentEpisodeUpsert:A.agentEpisodeUpsert,agentGraphTraverse:C.agentGraphTraverse,agentGraphUpsert:C.agentGraphUpsert,agentMessages:_,agentPatchThread:b(s),agentResolveApproval:D,agentRun:M,agentSetState:b(u),agentState:p,agentThread:g}}};export{ee as agentComponent,V as agentExtension,ne as normalizeEntityName,ie as sandboxComponent};
|
package/dist/inbound.mjs
CHANGED
|
@@ -1,32 +1 @@
|
|
|
1
|
-
import
|
|
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 };
|
|
1
|
+
import{LunoraError as s}from"@lunora/errors";import{createInboundEmailHandler as d,parseInboundEmail as u}from"@lunora/mail/inbound";const m=i=>{const c=d({dispatch:async(o,a)=>{for(const n of i){const t=n.agent.onEmail;if(!t)continue;const e=await t(o);if(e==null)continue;const r=a.env[n.binding];if(!r||typeof r.create!="function")throw new s("INTERNAL",`@lunora/agent: no Workflow binding "${n.binding}" on env for an inbound agent — run codegen/dev so wrangler.jsonc declares it`);await r.create({params:e});return}},parse:u});return async(o,a,n)=>c(o,a,n)};export{m as dispatchAgentEmail};
|