@lunora/agent 0.0.0 → 1.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +105 -0
- package/README.md +48 -43
- package/dist/channels.d.mts +54 -0
- package/dist/channels.d.ts +54 -0
- package/dist/channels.mjs +181 -0
- package/dist/component.d.mts +101 -0
- package/dist/component.d.ts +101 -0
- package/dist/component.mjs +407 -0
- package/dist/inbound.d.mts +29 -0
- package/dist/inbound.d.ts +29 -0
- package/dist/inbound.mjs +32 -0
- package/dist/index.d.mts +740 -0
- package/dist/index.d.ts +740 -0
- package/dist/index.mjs +18 -0
- package/dist/naming.d.mts +17 -0
- package/dist/naming.d.ts +17 -0
- package/dist/naming.mjs +8 -0
- package/dist/packem_shared/AGENT_MODULE-Dnt_-AAT.mjs +24 -0
- package/dist/packem_shared/VoiceSessionDO-DLoXsHGF.mjs +297 -0
- package/dist/packem_shared/adaptMcpResult-wtNMvLoP.mjs +65 -0
- package/dist/packem_shared/agentAsTool-Dt8NlU6k.mjs +94 -0
- package/dist/packem_shared/base64-BVwtgRJV.mjs +18 -0
- package/dist/packem_shared/braintrustTelemetry-wuGDErob.mjs +47 -0
- package/dist/packem_shared/buildModelMessages-BWFigaoo.mjs +69 -0
- package/dist/packem_shared/codeTool-CjgJOC9t.mjs +122 -0
- package/dist/packem_shared/collectAgenticMemoryTools-QrzpV-WX.mjs +97 -0
- package/dist/packem_shared/combineTelemetry-DCyaaWAI.mjs +43 -0
- package/dist/packem_shared/common-DAeFCot5.mjs +61 -0
- package/dist/packem_shared/compileAgentWorkflow-BxJjHgtD.mjs +55 -0
- package/dist/packem_shared/consoleTelemetry-z2MiP1jt.mjs +93 -0
- package/dist/packem_shared/createAgentContext-4xJGXNR4.mjs +50 -0
- package/dist/packem_shared/createAgentGenerate-BQv9YJ01.mjs +192 -0
- package/dist/packem_shared/createDispatchRunner-DSbp_dph-ZHTtxy3f.mjs +69 -0
- package/dist/packem_shared/defineAgent-D6maSbVc.mjs +148 -0
- package/dist/packem_shared/defineSkill-Ctf_S-rz.mjs +22 -0
- package/dist/packem_shared/functionTool-D6lCa2jB.mjs +20 -0
- package/dist/packem_shared/graph-component-aoUwO-f0.mjs +216 -0
- package/dist/packem_shared/memory-D4FPcBsX.mjs +12 -0
- package/dist/packem_shared/normalizeEntityName-CyEEWFkR.mjs +3 -0
- package/dist/packem_shared/runAgentLoop-Dhg4ZNvw.mjs +493 -0
- package/dist/packem_shared/runVoiceTurn-LnqLvCRR.mjs +211 -0
- package/dist/packem_shared/sandboxComponent-DR3pTwBL.mjs +194 -0
- package/dist/packem_shared/sentryTelemetry-CgqFJyLO.mjs +36 -0
- package/dist/packem_shared/types.d-BWG0uUtX.d.mts +1015 -0
- package/dist/packem_shared/types.d-BWG0uUtX.d.ts +1015 -0
- package/dist/sandbox.d.mts +185 -0
- package/dist/sandbox.d.ts +185 -0
- package/dist/sandbox.mjs +109 -0
- package/dist/telemetry/index.d.mts +150 -0
- package/dist/telemetry/index.d.ts +150 -0
- package/dist/telemetry/index.mjs +4 -0
- package/package.json +88 -7
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { createAi } from '@lunora/ai';
|
|
2
|
+
import { LunoraError } from '@lunora/errors';
|
|
3
|
+
import { jsonSchema, generateText, streamText, Output, tool } from 'ai';
|
|
4
|
+
|
|
5
|
+
const toAgentUsage = (usage) => {
|
|
6
|
+
if (!usage) {
|
|
7
|
+
return void 0;
|
|
8
|
+
}
|
|
9
|
+
const result = {};
|
|
10
|
+
if (usage.inputTokens !== void 0) {
|
|
11
|
+
result.inputTokens = usage.inputTokens;
|
|
12
|
+
}
|
|
13
|
+
if (usage.outputTokens !== void 0) {
|
|
14
|
+
result.outputTokens = usage.outputTokens;
|
|
15
|
+
}
|
|
16
|
+
if (usage.totalTokens !== void 0) {
|
|
17
|
+
result.totalTokens = usage.totalTokens;
|
|
18
|
+
}
|
|
19
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
20
|
+
};
|
|
21
|
+
const mapToolCalls = (calls) => calls.map((call) => {
|
|
22
|
+
return { id: call.toolCallId, input: call.input, name: call.toolName };
|
|
23
|
+
});
|
|
24
|
+
const resolveAgentModel = (model, env) => {
|
|
25
|
+
if (typeof model === "function") {
|
|
26
|
+
return model(env);
|
|
27
|
+
}
|
|
28
|
+
if (typeof model === "string") {
|
|
29
|
+
const binding = env["AI"];
|
|
30
|
+
if (!binding) {
|
|
31
|
+
throw new LunoraError(
|
|
32
|
+
"INTERNAL",
|
|
33
|
+
`@lunora/agent: the agent model "${model}" is a Workers AI id but there is no \`AI\` binding on the workflow env — declare the ai binding in wrangler.jsonc`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return createAi({ binding }).model(model);
|
|
37
|
+
}
|
|
38
|
+
return model;
|
|
39
|
+
};
|
|
40
|
+
const prepareAgentTurn = (agent, env) => {
|
|
41
|
+
const defaultModel = resolveAgentModel(agent.model, env);
|
|
42
|
+
const tools = {};
|
|
43
|
+
for (const [name, definition] of Object.entries(agent.tools ?? {})) {
|
|
44
|
+
tools[name] = tool({ description: definition.description, inputSchema: definition.inputSchema });
|
|
45
|
+
}
|
|
46
|
+
const output = agent.output === void 0 ? void 0 : Output.object({ schema: agent.output });
|
|
47
|
+
const staticSettings = {
|
|
48
|
+
...agent.temperature === void 0 ? {} : { temperature: agent.temperature },
|
|
49
|
+
...agent.maxOutputTokens === void 0 ? {} : { maxOutputTokens: agent.maxOutputTokens },
|
|
50
|
+
...agent.telemetry === void 0 ? {} : { telemetry: agent.telemetry },
|
|
51
|
+
...agent.repairToolCall === void 0 ? {} : { experimental_repairToolCall: agent.repairToolCall }
|
|
52
|
+
};
|
|
53
|
+
return { defaultModel, output, staticSettings, tools };
|
|
54
|
+
};
|
|
55
|
+
const buildTurnRequest = (base, { activeTools, messages, model, signal, toolChoice }) => {
|
|
56
|
+
const { defaultModel, output, staticSettings, tools } = base;
|
|
57
|
+
const exposed = activeTools === void 0 ? tools : Object.fromEntries(Object.entries(tools).filter(([name]) => activeTools.includes(name)));
|
|
58
|
+
return {
|
|
59
|
+
messages,
|
|
60
|
+
model: model ?? defaultModel,
|
|
61
|
+
...staticSettings,
|
|
62
|
+
...Object.keys(exposed).length > 0 ? { tools: exposed } : {},
|
|
63
|
+
...toolChoice === void 0 ? {} : { toolChoice },
|
|
64
|
+
...output === void 0 ? {} : { output },
|
|
65
|
+
// A voice barge-in aborts the turn mid-stream; the durable loop never
|
|
66
|
+
// sets it. Harmless on `generateText` (which accepts the same option).
|
|
67
|
+
...signal === void 0 ? {} : { abortSignal: signal }
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
const createAgentGenerate = (agent, env) => {
|
|
71
|
+
const base = prepareAgentTurn(agent, env);
|
|
72
|
+
return async (options) => {
|
|
73
|
+
const result = await generateText(buildTurnRequest(base, options));
|
|
74
|
+
const usage = toAgentUsage(result.usage);
|
|
75
|
+
return {
|
|
76
|
+
text: result.text,
|
|
77
|
+
toolCalls: mapToolCalls(result.toolCalls),
|
|
78
|
+
...usage === void 0 ? {} : { usage },
|
|
79
|
+
...base.output === void 0 ? {} : { output: result.output }
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
const createStreamGenerate = (agent, env) => {
|
|
84
|
+
const base = prepareAgentTurn(agent, env);
|
|
85
|
+
return async (options, onDelta) => {
|
|
86
|
+
const result = streamText(buildTurnRequest(base, options));
|
|
87
|
+
let streamed = "";
|
|
88
|
+
try {
|
|
89
|
+
for await (const delta of result.textStream) {
|
|
90
|
+
streamed += delta;
|
|
91
|
+
onDelta(delta);
|
|
92
|
+
}
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (options.signal?.aborted) {
|
|
95
|
+
return { text: streamed, toolCalls: [] };
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
const [text, toolCalls, usage, output] = await Promise.all([
|
|
100
|
+
result.text,
|
|
101
|
+
result.toolCalls,
|
|
102
|
+
result.usage,
|
|
103
|
+
base.output === void 0 ? Promise.resolve(void 0) : result.output
|
|
104
|
+
]);
|
|
105
|
+
const agentUsage = toAgentUsage(usage);
|
|
106
|
+
return {
|
|
107
|
+
text,
|
|
108
|
+
toolCalls: mapToolCalls(toolCalls),
|
|
109
|
+
...agentUsage === void 0 ? {} : { usage: agentUsage },
|
|
110
|
+
...base.output === void 0 ? {} : { output }
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
const GRAPH_EXTRACTION_SCHEMA = jsonSchema({
|
|
115
|
+
additionalProperties: false,
|
|
116
|
+
properties: {
|
|
117
|
+
entities: {
|
|
118
|
+
description: "The distinct entities (people, orgs, places, things, concepts) named in the exchange.",
|
|
119
|
+
items: {
|
|
120
|
+
additionalProperties: false,
|
|
121
|
+
properties: {
|
|
122
|
+
name: { description: "The entity's canonical name.", type: "string" },
|
|
123
|
+
type: { description: "An optional coarse type, e.g. person, org, place, product.", type: "string" }
|
|
124
|
+
},
|
|
125
|
+
required: ["name"],
|
|
126
|
+
type: "object"
|
|
127
|
+
},
|
|
128
|
+
type: "array"
|
|
129
|
+
},
|
|
130
|
+
relations: {
|
|
131
|
+
description: "The directed relationships between the entities, as (src)-[label]->(dst) triples.",
|
|
132
|
+
items: {
|
|
133
|
+
additionalProperties: false,
|
|
134
|
+
properties: {
|
|
135
|
+
confidence: { description: "Confidence in the relation, 0..1.", type: "number" },
|
|
136
|
+
dst: { description: "The destination entity name (must appear in `entities`).", type: "string" },
|
|
137
|
+
label: { description: "The relationship, a short snake_case verb phrase, e.g. works_at.", type: "string" },
|
|
138
|
+
src: { description: "The source entity name (must appear in `entities`).", type: "string" }
|
|
139
|
+
},
|
|
140
|
+
required: ["src", "dst", "label"],
|
|
141
|
+
type: "object"
|
|
142
|
+
},
|
|
143
|
+
type: "array"
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
required: ["entities", "relations"],
|
|
147
|
+
type: "object"
|
|
148
|
+
});
|
|
149
|
+
const buildExtractionPrompt = (userInput, assistantText) => [
|
|
150
|
+
"Extract a knowledge graph of the durable facts stated in the following exchange.",
|
|
151
|
+
"Return the distinct entities and the directed relationships between them.",
|
|
152
|
+
"Only include facts actually stated — do not invent entities or relations. Prefer few, high-signal triples.",
|
|
153
|
+
"",
|
|
154
|
+
`User: ${userInput}`,
|
|
155
|
+
"",
|
|
156
|
+
`Assistant: ${assistantText}`
|
|
157
|
+
].join("\n");
|
|
158
|
+
const createGraphExtract = () => async ({ assistantText, env, model, userInput }) => {
|
|
159
|
+
const { output } = await generateText({
|
|
160
|
+
model: resolveAgentModel(model, env),
|
|
161
|
+
output: Output.object({ schema: GRAPH_EXTRACTION_SCHEMA }),
|
|
162
|
+
prompt: buildExtractionPrompt(userInput, assistantText)
|
|
163
|
+
});
|
|
164
|
+
return output;
|
|
165
|
+
};
|
|
166
|
+
const buildEpisodePrompt = (userInput, assistantText) => [
|
|
167
|
+
"Summarize the following exchange as ONE concise past-tense sentence for a long-term memory log.",
|
|
168
|
+
"Capture what the user wanted and what was done or decided — a durable fact worth recalling in a later conversation.",
|
|
169
|
+
"No preamble and no quotes: return only the sentence.",
|
|
170
|
+
"",
|
|
171
|
+
`User: ${userInput}`,
|
|
172
|
+
"",
|
|
173
|
+
`Assistant: ${assistantText}`
|
|
174
|
+
].join("\n");
|
|
175
|
+
const createEpisodeExtract = () => async ({ assistantText, env, model, userInput }) => {
|
|
176
|
+
const { text } = await generateText({
|
|
177
|
+
model: resolveAgentModel(model, env),
|
|
178
|
+
prompt: buildEpisodePrompt(userInput, assistantText)
|
|
179
|
+
});
|
|
180
|
+
return { summary: text.trim() };
|
|
181
|
+
};
|
|
182
|
+
const COMPACTION_SYSTEM = "You are compacting an ongoing conversation. Summarize the messages so far into a concise brief that preserves decisions made, facts established, open questions, and current task state — everything the assistant needs to continue coherently. Write it as notes for the assistant, not a reply to the user. No preamble.";
|
|
183
|
+
const createCompact = () => async ({ env, messages, model }) => {
|
|
184
|
+
const { text } = await generateText({
|
|
185
|
+
messages,
|
|
186
|
+
model: resolveAgentModel(model, env),
|
|
187
|
+
system: COMPACTION_SYSTEM
|
|
188
|
+
});
|
|
189
|
+
return text.trim();
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export { createAgentGenerate, createCompact, createEpisodeExtract, createGraphExtract, createStreamGenerate, resolveAgentModel };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
2
|
+
|
|
3
|
+
const SCHEDULER_DISPATCH_PATH = "/_lunora/scheduler/dispatch";
|
|
4
|
+
const trimTrailingSlashes = (value) => {
|
|
5
|
+
let end = value.length;
|
|
6
|
+
while (end > 0 && value[end - 1] === "/") {
|
|
7
|
+
end -= 1;
|
|
8
|
+
}
|
|
9
|
+
return value.slice(0, end);
|
|
10
|
+
};
|
|
11
|
+
const toDispatchError = (label, status, rawBody) => {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(rawBody);
|
|
14
|
+
const errorBody = parsed?.error;
|
|
15
|
+
if (typeof errorBody === "object" && errorBody !== null && typeof errorBody.code === "string") {
|
|
16
|
+
const { code, data, message } = errorBody;
|
|
17
|
+
return new LunoraError(code, typeof message === "string" ? message : void 0, { data, status });
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
}
|
|
21
|
+
return new LunoraError("INTERNAL", `${label}: function dispatch failed (${String(status)}): ${rawBody}`, { status });
|
|
22
|
+
};
|
|
23
|
+
const createDispatchRunner = (options) => {
|
|
24
|
+
const { label } = options;
|
|
25
|
+
const globalFetch = globalThis.fetch;
|
|
26
|
+
const fetchImpl = options.fetchImpl ?? (typeof globalFetch === "function" ? globalFetch.bind(globalThis) : void 0);
|
|
27
|
+
return async (function_, args, runOptions = {}) => {
|
|
28
|
+
if (typeof fetchImpl !== "function") {
|
|
29
|
+
throw new TypeError(`${label}: no fetch implementation available — pass fetchImpl or run on a platform with global fetch`);
|
|
30
|
+
}
|
|
31
|
+
const origin = options.env.LUNORA_ORIGIN_URL;
|
|
32
|
+
if (typeof origin !== "string" || origin.length === 0) {
|
|
33
|
+
throw new LunoraError("INTERNAL", `${label}: \`LUNORA_ORIGIN_URL\` must be set on the Worker env so a handler can call back into Lunora functions`);
|
|
34
|
+
}
|
|
35
|
+
const token = options.env.LUNORA_ADMIN_TOKEN;
|
|
36
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
37
|
+
throw new LunoraError("INTERNAL", `${label}: \`LUNORA_ADMIN_TOKEN\` must be set on the Worker env to authenticate function dispatch`);
|
|
38
|
+
}
|
|
39
|
+
const url = `${trimTrailingSlashes(origin)}${SCHEDULER_DISPATCH_PATH}`;
|
|
40
|
+
const headers = { authorization: `Bearer ${token}`, "content-type": "application/json" };
|
|
41
|
+
if (options.identity?.userId !== void 0) {
|
|
42
|
+
headers["x-lunora-userid"] = options.identity.userId;
|
|
43
|
+
}
|
|
44
|
+
if (options.identity?.claims !== void 0) {
|
|
45
|
+
headers["x-lunora-identity"] = JSON.stringify(options.identity.claims);
|
|
46
|
+
}
|
|
47
|
+
const response = await fetchImpl(url, {
|
|
48
|
+
body: JSON.stringify({ args: args ?? {}, functionPath: function_.__lunoraRef, shardKey: runOptions.shardKey }),
|
|
49
|
+
headers,
|
|
50
|
+
method: "POST"
|
|
51
|
+
});
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
throw toDispatchError(label, response.status, await response.text());
|
|
54
|
+
}
|
|
55
|
+
const text = await response.text();
|
|
56
|
+
if (text.length === 0) {
|
|
57
|
+
return void 0;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(text);
|
|
61
|
+
} catch {
|
|
62
|
+
throw new LunoraError("INTERNAL", `${label}: function dispatch returned a non-JSON body (${String(response.status)}): ${text}`, {
|
|
63
|
+
status: response.status
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export { createDispatchRunner as c };
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
2
|
+
import { collectAgenticMemoryTools } from './collectAgenticMemoryTools-QrzpV-WX.mjs';
|
|
3
|
+
import { agentAsTool } from './agentAsTool-Dt8NlU6k.mjs';
|
|
4
|
+
import { i as isInjectedMemorySource } from './memory-D4FPcBsX.mjs';
|
|
5
|
+
import { SKILL_NAME_PATTERN, RESERVED_SKILL_NAME } from './defineSkill-Ctf_S-rz.mjs';
|
|
6
|
+
|
|
7
|
+
const TOOL_NAME_PATTERN = /^[a-zA-Z][\w-]*$/u;
|
|
8
|
+
const mergeSkillTools = (config, skills) => {
|
|
9
|
+
const tools = { ...config.tools };
|
|
10
|
+
for (const skill of skills) {
|
|
11
|
+
for (const [toolName, tool] of Object.entries(skill.tools ?? {})) {
|
|
12
|
+
if (Object.hasOwn(tools, toolName)) {
|
|
13
|
+
throw new LunoraError(
|
|
14
|
+
"INTERNAL",
|
|
15
|
+
`@lunora/agent: skill "${skill.name}" tool "${toolName}" collides with an existing tool — rename one (the agent's tool namespace is flat)`
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
tools[toolName] = tool;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return tools;
|
|
22
|
+
};
|
|
23
|
+
const composeInstructions = (config, skills) => {
|
|
24
|
+
const fragments = [];
|
|
25
|
+
for (const fragment of [config.instructions, ...skills.map((skill) => skill.instructions)]) {
|
|
26
|
+
if (fragment) {
|
|
27
|
+
fragments.push(fragment);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (fragments.length <= 1) {
|
|
31
|
+
return fragments[0];
|
|
32
|
+
}
|
|
33
|
+
return (context) => fragments.map((fragment) => typeof fragment === "function" ? fragment(context) : fragment).filter((text) => text.length > 0).join("\n\n");
|
|
34
|
+
};
|
|
35
|
+
const collectMemorySources = (config, skills) => {
|
|
36
|
+
const sources = [];
|
|
37
|
+
if (config.memory && isInjectedMemorySource(config.memory)) {
|
|
38
|
+
sources.push({ key: "default", ...config.memory });
|
|
39
|
+
}
|
|
40
|
+
for (const skill of skills) {
|
|
41
|
+
if (skill.knowledge && isInjectedMemorySource(skill.knowledge)) {
|
|
42
|
+
sources.push({ key: skill.name, ...skill.knowledge });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return sources;
|
|
46
|
+
};
|
|
47
|
+
const assertValidSkillNames = (skills) => {
|
|
48
|
+
const seen = /* @__PURE__ */ new Set();
|
|
49
|
+
for (const skill of skills) {
|
|
50
|
+
if (typeof skill.name !== "string" || !SKILL_NAME_PATTERN.test(skill.name)) {
|
|
51
|
+
throw new LunoraError(
|
|
52
|
+
"INTERNAL",
|
|
53
|
+
`@lunora/agent: skill \`name\` must be a valid identifier (letters, digits, _ or -, starting with a letter), got ${JSON.stringify(skill.name)}`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (skill.name === RESERVED_SKILL_NAME) {
|
|
57
|
+
throw new LunoraError(
|
|
58
|
+
"INTERNAL",
|
|
59
|
+
`@lunora/agent: skill name "${RESERVED_SKILL_NAME}" is reserved (it keys the agent's own \`memory\` source / the \`memory:retrieve\` step) — choose another name`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (seen.has(skill.name)) {
|
|
63
|
+
throw new LunoraError(
|
|
64
|
+
"INTERNAL",
|
|
65
|
+
`@lunora/agent: skill name "${skill.name}" is used by more than one skill — rename one (a skill name keys its knowledge memory source, which must be unique)`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
seen.add(skill.name);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const assertActiveToolsExposeMemory = (activeTools, agenticMemoryTools) => {
|
|
72
|
+
if (activeTools === void 0) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const unreachable = Object.keys(agenticMemoryTools).filter((name) => !activeTools.includes(name));
|
|
76
|
+
if (unreachable.length > 0) {
|
|
77
|
+
throw new LunoraError(
|
|
78
|
+
"INTERNAL",
|
|
79
|
+
`@lunora/agent: \`activeTools\` omits the agentic-memory tool(s) ${unreachable.map((name) => `"${name}"`).join(", ")} — the model could never call them; add them to \`activeTools\` (or drop \`activeTools\` to expose every tool)`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const assertMemorySourcesConfigured = (config, skills) => {
|
|
84
|
+
const requireSource = (memory, label) => {
|
|
85
|
+
if (memory && memory.kind !== "graph" && memory.kind !== "episodic" && memory.source === void 0) {
|
|
86
|
+
throw new LunoraError("INTERNAL", `@lunora/agent: ${label} requires a \`source\` action unless \`kind: "graph"\` or \`kind: "episodic"\``);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
requireSource(config.memory, "`memory`");
|
|
90
|
+
for (const skill of skills) {
|
|
91
|
+
requireSource(skill.knowledge, `skill "${skill.name}" \`knowledge\``);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const defineAgent = (config) => {
|
|
95
|
+
const model = config.model;
|
|
96
|
+
if (model === void 0 || model === null || typeof model === "string" && model.length === 0) {
|
|
97
|
+
throw new LunoraError(
|
|
98
|
+
"INTERNAL",
|
|
99
|
+
"@lunora/agent: defineAgent requires a `model` (a Workers AI id, an AI SDK LanguageModel, or an (env) => model thunk)"
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (config.maxTurns !== void 0 && (!Number.isInteger(config.maxTurns) || config.maxTurns < 1)) {
|
|
103
|
+
throw new LunoraError("INTERNAL", "@lunora/agent: `maxTurns` must be a positive integer");
|
|
104
|
+
}
|
|
105
|
+
const skills = config.skills ?? [];
|
|
106
|
+
assertValidSkillNames(skills);
|
|
107
|
+
assertMemorySourcesConfigured(config, skills);
|
|
108
|
+
const tools = mergeSkillTools(config, skills);
|
|
109
|
+
const agenticMemoryTools = collectAgenticMemoryTools(config, skills);
|
|
110
|
+
for (const [toolName, tool] of Object.entries(agenticMemoryTools)) {
|
|
111
|
+
if (Object.hasOwn(tools, toolName)) {
|
|
112
|
+
throw new LunoraError(
|
|
113
|
+
"INTERNAL",
|
|
114
|
+
`@lunora/agent: agentic-memory tool "${toolName}" collides with an existing tool — rename one (the agent's tool namespace is flat)`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
tools[toolName] = tool;
|
|
118
|
+
}
|
|
119
|
+
const hasAgenticTools = Object.keys(agenticMemoryTools).length > 0;
|
|
120
|
+
assertActiveToolsExposeMemory(config.activeTools, agenticMemoryTools);
|
|
121
|
+
for (const name of Object.keys(tools)) {
|
|
122
|
+
if (!TOOL_NAME_PATTERN.test(name)) {
|
|
123
|
+
throw new LunoraError("INTERNAL", `@lunora/agent: tool name "${name}" is not a valid identifier (letters, digits, _ or -, starting with a letter)`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const memorySources = collectMemorySources(config, skills);
|
|
127
|
+
const composedInstructions = composeInstructions(config, skills);
|
|
128
|
+
return {
|
|
129
|
+
...config,
|
|
130
|
+
asTool: agentAsTool,
|
|
131
|
+
isLunoraAgent: true,
|
|
132
|
+
...skills.some((skill) => Boolean(skill.instructions)) ? { instructions: composedInstructions } : {},
|
|
133
|
+
...memorySources.length > 0 ? { memorySources } : {},
|
|
134
|
+
...skills.length > 0 || hasAgenticTools ? { tools } : {}
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
const isAgentDefinition = (value) => typeof value === "object" && value !== null && value.isLunoraAgent === true;
|
|
138
|
+
const defineAgentTool = (config) => {
|
|
139
|
+
if (typeof config.execute !== "function") {
|
|
140
|
+
throw new LunoraError("INTERNAL", "@lunora/agent: defineAgentTool requires an `execute` function");
|
|
141
|
+
}
|
|
142
|
+
if (typeof config.description !== "string" || config.description.length === 0) {
|
|
143
|
+
throw new LunoraError("INTERNAL", "@lunora/agent: defineAgentTool requires a non-empty `description` (the model decides from it)");
|
|
144
|
+
}
|
|
145
|
+
return { ...config, isLunoraAgentTool: true };
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export { defineAgent, defineAgentTool, isAgentDefinition };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
2
|
+
|
|
3
|
+
const SKILL_NAME_PATTERN = /^[a-zA-Z][\w-]*$/u;
|
|
4
|
+
const RESERVED_SKILL_NAME = "default";
|
|
5
|
+
const defineSkill = (config) => {
|
|
6
|
+
if (typeof config.name !== "string" || !SKILL_NAME_PATTERN.test(config.name)) {
|
|
7
|
+
throw new LunoraError(
|
|
8
|
+
"INTERNAL",
|
|
9
|
+
`@lunora/agent: defineSkill requires a \`name\` that is a valid identifier (letters, digits, _ or -, starting with a letter), got ${JSON.stringify(config.name)}`
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
if (config.name === RESERVED_SKILL_NAME) {
|
|
13
|
+
throw new LunoraError(
|
|
14
|
+
"INTERNAL",
|
|
15
|
+
`@lunora/agent: skill name "${RESERVED_SKILL_NAME}" is reserved (it keys the agent's own \`memory\` source / the historic \`memory:retrieve\` step) — choose another name`
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return { ...config, isLunoraSkill: true };
|
|
19
|
+
};
|
|
20
|
+
const isSkillDefinition = (value) => typeof value === "object" && value !== null && value.isLunoraSkill === true;
|
|
21
|
+
|
|
22
|
+
export { RESERVED_SKILL_NAME, SKILL_NAME_PATTERN, defineSkill, isSkillDefinition };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
2
|
+
import { toFunctionReference } from './AGENT_MODULE-Dnt_-AAT.mjs';
|
|
3
|
+
|
|
4
|
+
const functionTool = (reference, options) => {
|
|
5
|
+
if (typeof options.description !== "string" || options.description.length === 0) {
|
|
6
|
+
throw new LunoraError("INTERNAL", "@lunora/agent: functionTool requires a non-empty `description` (the model decides from it)");
|
|
7
|
+
}
|
|
8
|
+
if (options.inputSchema === void 0) {
|
|
9
|
+
throw new LunoraError("INTERNAL", "@lunora/agent: functionTool requires an `inputSchema` (a zod schema or `jsonSchema(...)`)");
|
|
10
|
+
}
|
|
11
|
+
const target = toFunctionReference(reference);
|
|
12
|
+
return {
|
|
13
|
+
description: options.description,
|
|
14
|
+
execute: (input, context) => context.run(target, input),
|
|
15
|
+
inputSchema: options.inputSchema,
|
|
16
|
+
isLunoraAgentTool: true
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export { functionTool };
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { defineTable, initLunora } from '@lunora/server';
|
|
2
|
+
import { v } from '@lunora/values';
|
|
3
|
+
|
|
4
|
+
const AGENT_EXTENSION_KEY = "agent";
|
|
5
|
+
const asInternal = (function_) => {
|
|
6
|
+
return { ...function_, visibility: "internal" };
|
|
7
|
+
};
|
|
8
|
+
const definedColumns = (columns) => {
|
|
9
|
+
const result = {};
|
|
10
|
+
for (const [key, value] of Object.entries(columns)) {
|
|
11
|
+
if (value !== void 0) {
|
|
12
|
+
result[key] = value;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const ENTITIES_BARE_TABLE = "entities";
|
|
19
|
+
const EDGES_BARE_TABLE = "edges";
|
|
20
|
+
const ENTITIES_TABLE = `${AGENT_EXTENSION_KEY}_${ENTITIES_BARE_TABLE}`;
|
|
21
|
+
const EDGES_TABLE = `${AGENT_EXTENSION_KEY}_${EDGES_BARE_TABLE}`;
|
|
22
|
+
const graphTables = {
|
|
23
|
+
/**
|
|
24
|
+
* Graph-memory nodes — one per normalized entity name per owner. `weight` is
|
|
25
|
+
* salience (last-write-wins, absolute set → replay-idempotent),
|
|
26
|
+
* `firstMessageKey` is provenance.
|
|
27
|
+
*/
|
|
28
|
+
[ENTITIES_BARE_TABLE]: defineTable({
|
|
29
|
+
createdAt: v.number(),
|
|
30
|
+
firstMessageKey: v.optional(v.string()),
|
|
31
|
+
/** Normalized (trim/collapse/lowercase) — the per-owner dedup key. */
|
|
32
|
+
name: v.string(),
|
|
33
|
+
owner: v.string(),
|
|
34
|
+
type: v.optional(v.string()),
|
|
35
|
+
updatedAt: v.number(),
|
|
36
|
+
weight: v.optional(v.number())
|
|
37
|
+
}).index("byOwnerName", ["owner", "name"], { unique: true }).public(),
|
|
38
|
+
/**
|
|
39
|
+
* Graph-memory edges — directed triples storing the normalized endpoint
|
|
40
|
+
* NAMES (no join on write), owner-scoped like the nodes. `weight` is
|
|
41
|
+
* confidence (last-write-wins), `messageKey` is provenance. Traversal is
|
|
42
|
+
* bidirectional, so both endpoints are indexed.
|
|
43
|
+
*/
|
|
44
|
+
[EDGES_BARE_TABLE]: defineTable({
|
|
45
|
+
createdAt: v.number(),
|
|
46
|
+
dstName: v.string(),
|
|
47
|
+
label: v.string(),
|
|
48
|
+
messageKey: v.string(),
|
|
49
|
+
owner: v.string(),
|
|
50
|
+
srcName: v.string(),
|
|
51
|
+
updatedAt: v.number(),
|
|
52
|
+
weight: v.optional(v.number())
|
|
53
|
+
}).index("byOwnerSrc", ["owner", "srcName"]).index("byOwnerDst", ["owner", "dstName"]).index("byTriple", ["owner", "srcName", "label", "dstName"], { unique: true }).public()
|
|
54
|
+
};
|
|
55
|
+
const GRAPH_ARRAY_CAP = 64;
|
|
56
|
+
const DEFAULT_GRAPH_DEPTH = 2;
|
|
57
|
+
const DEFAULT_GRAPH_MAX_SEEDS = 4;
|
|
58
|
+
const DEFAULT_GRAPH_FAN_OUT = 8;
|
|
59
|
+
const DEFAULT_GRAPH_MAX_NODES = 32;
|
|
60
|
+
const WHITESPACE_RUN = /\s+/gu;
|
|
61
|
+
const NON_WORD = /[^\p{L}\p{N}]+/u;
|
|
62
|
+
const normalizeEntityName = (name) => name.trim().replaceAll(WHITESPACE_RUN, " ").toLowerCase();
|
|
63
|
+
const tokenizeQuery = (text) => text.toLowerCase().split(NON_WORD).filter((token) => token.length >= 2);
|
|
64
|
+
const toGraphEdge = (row) => {
|
|
65
|
+
return {
|
|
66
|
+
dstName: row["dstName"],
|
|
67
|
+
label: row["label"],
|
|
68
|
+
srcName: row["srcName"],
|
|
69
|
+
weight: row["weight"] ?? 1
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
const compareEdgesByWeight = (a, b) => b.weight - a.weight || a.label.localeCompare(b.label) || a.srcName.localeCompare(b.srcName) || a.dstName.localeCompare(b.dstName);
|
|
73
|
+
const matchesSeed = (entityName, tokens) => tokens.some((token) => entityName.includes(token) || token.includes(entityName));
|
|
74
|
+
const renderTriples = (edges) => edges.map((edge) => `- ${edge.srcName} —[${edge.label}]→ ${edge.dstName}`).toSorted((a, b) => a.localeCompare(b)).join("\n");
|
|
75
|
+
const edgeKey = (edge) => `${edge.srcName}\0${edge.label}\0${edge.dstName}`;
|
|
76
|
+
const addEdge = (edge, from, bounds, accumulator) => {
|
|
77
|
+
const key = edgeKey(edge);
|
|
78
|
+
if (!accumulator.collectedKeys.has(key)) {
|
|
79
|
+
accumulator.collectedKeys.add(key);
|
|
80
|
+
accumulator.collected.push(edge);
|
|
81
|
+
}
|
|
82
|
+
const neighbor = edge.srcName === from ? edge.dstName : edge.srcName;
|
|
83
|
+
if (!accumulator.seen.has(neighbor) && accumulator.seen.size < bounds.maxNodes) {
|
|
84
|
+
accumulator.seen.add(neighbor);
|
|
85
|
+
accumulator.next.push(neighbor);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const traverseGraph = async (edgesFrom, seeds, bounds) => {
|
|
89
|
+
const accumulator = { collected: [], collectedKeys: /* @__PURE__ */ new Set(), next: [], seen: new Set(seeds) };
|
|
90
|
+
let frontier = seeds.slice(0, bounds.maxNodes);
|
|
91
|
+
for (let depth = 0; depth < bounds.depth && frontier.length > 0; depth += 1) {
|
|
92
|
+
accumulator.next = [];
|
|
93
|
+
for (const node of frontier) {
|
|
94
|
+
const nodeEdges = await edgesFrom(node);
|
|
95
|
+
const incident = nodeEdges.toSorted(compareEdgesByWeight).slice(0, bounds.fanOut);
|
|
96
|
+
for (const edge of incident) {
|
|
97
|
+
addEdge(edge, node, bounds, accumulator);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
frontier = accumulator.next;
|
|
101
|
+
}
|
|
102
|
+
return accumulator.collected;
|
|
103
|
+
};
|
|
104
|
+
const { mutation, query } = initLunora.dataModel().create();
|
|
105
|
+
const graphComponent = () => {
|
|
106
|
+
const agentGraphUpsert = mutation.input({
|
|
107
|
+
entities: v.array(v.object({ name: v.string(), type: v.optional(v.string()) })),
|
|
108
|
+
messageKey: v.string(),
|
|
109
|
+
owner: v.string(),
|
|
110
|
+
relations: v.array(v.object({ confidence: v.optional(v.number()), dst: v.string(), label: v.string(), src: v.string() }))
|
|
111
|
+
}).mutation(async ({ args, ctx: context }) => {
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
const upsertEntity = async (rawName, type) => {
|
|
114
|
+
const name = normalizeEntityName(rawName);
|
|
115
|
+
if (name.length === 0) {
|
|
116
|
+
return void 0;
|
|
117
|
+
}
|
|
118
|
+
const existing = await context.db.query(ENTITIES_TABLE).withIndex("byOwnerName", (q) => q.eq("owner", args.owner).eq("name", name)).first();
|
|
119
|
+
if (existing) {
|
|
120
|
+
if (type !== void 0 && existing["type"] === void 0) {
|
|
121
|
+
await context.db.patch(existing["_id"], { type, updatedAt: now });
|
|
122
|
+
}
|
|
123
|
+
return name;
|
|
124
|
+
}
|
|
125
|
+
await context.db.insert(ENTITIES_TABLE, {
|
|
126
|
+
createdAt: now,
|
|
127
|
+
name,
|
|
128
|
+
owner: args.owner,
|
|
129
|
+
updatedAt: now,
|
|
130
|
+
weight: 1,
|
|
131
|
+
...definedColumns({ firstMessageKey: args.messageKey, type })
|
|
132
|
+
});
|
|
133
|
+
return name;
|
|
134
|
+
};
|
|
135
|
+
const upsertRelation = async (relation) => {
|
|
136
|
+
const sourceName = normalizeEntityName(relation.src);
|
|
137
|
+
const destinationName = normalizeEntityName(relation.dst);
|
|
138
|
+
const label = normalizeEntityName(relation.label);
|
|
139
|
+
if (sourceName.length === 0 || destinationName.length === 0 || label.length === 0 || sourceName === destinationName) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
await upsertEntity(sourceName);
|
|
143
|
+
await upsertEntity(destinationName);
|
|
144
|
+
const confidence = relation.confidence ?? 1;
|
|
145
|
+
const existing = await context.db.query(EDGES_TABLE).withIndex("byTriple", (q) => q.eq("owner", args.owner).eq("srcName", sourceName).eq("label", label).eq("dstName", destinationName)).first();
|
|
146
|
+
if (existing) {
|
|
147
|
+
const priorWeight = existing["weight"] ?? 1;
|
|
148
|
+
await context.db.patch(existing["_id"], { updatedAt: now, weight: Math.max(priorWeight, confidence) });
|
|
149
|
+
} else {
|
|
150
|
+
await context.db.insert(EDGES_TABLE, {
|
|
151
|
+
createdAt: now,
|
|
152
|
+
dstName: destinationName,
|
|
153
|
+
label,
|
|
154
|
+
messageKey: args.messageKey,
|
|
155
|
+
owner: args.owner,
|
|
156
|
+
srcName: sourceName,
|
|
157
|
+
updatedAt: now,
|
|
158
|
+
weight: confidence
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return true;
|
|
162
|
+
};
|
|
163
|
+
let entities = 0;
|
|
164
|
+
for (const entity of args.entities.slice(0, GRAPH_ARRAY_CAP)) {
|
|
165
|
+
if (await upsertEntity(entity.name, entity.type) !== void 0) {
|
|
166
|
+
entities += 1;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
let relations = 0;
|
|
170
|
+
for (const relation of args.relations.slice(0, GRAPH_ARRAY_CAP)) {
|
|
171
|
+
if (await upsertRelation(relation)) {
|
|
172
|
+
relations += 1;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { entities, relations };
|
|
176
|
+
});
|
|
177
|
+
const agentGraphTraverse = query.input({
|
|
178
|
+
depth: v.optional(v.number()),
|
|
179
|
+
fanOut: v.optional(v.number()),
|
|
180
|
+
maxNodes: v.optional(v.number()),
|
|
181
|
+
maxSeeds: v.optional(v.number()),
|
|
182
|
+
owner: v.string(),
|
|
183
|
+
query: v.string()
|
|
184
|
+
}).query(async ({ args, ctx: context }) => {
|
|
185
|
+
const tokens = tokenizeQuery(args.query);
|
|
186
|
+
if (tokens.length === 0) {
|
|
187
|
+
return { context: "" };
|
|
188
|
+
}
|
|
189
|
+
const ownerEntities = await context.db.query(ENTITIES_TABLE).withIndex("byOwnerName", (q) => q.eq("owner", args.owner)).collect();
|
|
190
|
+
const seeds = ownerEntities.filter((row) => matchesSeed(row["name"], tokens)).toSorted(
|
|
191
|
+
(a, b) => (b["weight"] ?? 1) - (a["weight"] ?? 1) || a["name"].localeCompare(b["name"])
|
|
192
|
+
).slice(0, args.maxSeeds ?? DEFAULT_GRAPH_MAX_SEEDS).map((row) => row["name"]);
|
|
193
|
+
if (seeds.length === 0) {
|
|
194
|
+
return { context: "" };
|
|
195
|
+
}
|
|
196
|
+
const edgesFrom = async (node) => {
|
|
197
|
+
const [outgoing, incoming] = await Promise.all([
|
|
198
|
+
context.db.query(EDGES_TABLE).withIndex("byOwnerSrc", (q) => q.eq("owner", args.owner).eq("srcName", node)).collect(),
|
|
199
|
+
context.db.query(EDGES_TABLE).withIndex("byOwnerDst", (q) => q.eq("owner", args.owner).eq("dstName", node)).collect()
|
|
200
|
+
]);
|
|
201
|
+
return [...outgoing, ...incoming].map((row) => toGraphEdge(row));
|
|
202
|
+
};
|
|
203
|
+
const edges = await traverseGraph(edgesFrom, seeds, {
|
|
204
|
+
depth: args.depth ?? DEFAULT_GRAPH_DEPTH,
|
|
205
|
+
fanOut: args.fanOut ?? DEFAULT_GRAPH_FAN_OUT,
|
|
206
|
+
maxNodes: args.maxNodes ?? DEFAULT_GRAPH_MAX_NODES
|
|
207
|
+
});
|
|
208
|
+
return { context: renderTriples(edges) };
|
|
209
|
+
});
|
|
210
|
+
return {
|
|
211
|
+
agentGraphTraverse: asInternal(agentGraphTraverse),
|
|
212
|
+
agentGraphUpsert: asInternal(agentGraphUpsert)
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export { AGENT_EXTENSION_KEY as A, asInternal as a, graphComponent as b, definedColumns as d, graphTables as g, normalizeEntityName as n };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const isInjectedMemorySource = (memory) => memory.kind === "graph" || memory.kind === "episodic" || memory.mode !== "agentic";
|
|
2
|
+
const resolveInjectedSources = (agent) => {
|
|
3
|
+
if (agent.memorySources) {
|
|
4
|
+
return agent.memorySources;
|
|
5
|
+
}
|
|
6
|
+
return agent.memory && isInjectedMemorySource(agent.memory) ? [{ key: "default", ...agent.memory }] : [];
|
|
7
|
+
};
|
|
8
|
+
const firstGraphSource = (agent) => resolveInjectedSources(agent).find((source) => source.kind === "graph");
|
|
9
|
+
const firstEpisodicSource = (agent) => resolveInjectedSources(agent).find((source) => source.kind === "episodic");
|
|
10
|
+
const memoryStepName = (base, key) => key === "default" ? base : `${base}:${key}`;
|
|
11
|
+
|
|
12
|
+
export { firstEpisodicSource as a, firstGraphSource as f, isInjectedMemorySource as i, memoryStepName as m, resolveInjectedSources as r };
|