@dbx-tools/appkit-memory 0.0.0-stage → 0.9.132
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 +202 -0
- package/README.md +116 -2
- package/index.ts +17 -0
- package/lib/index.d.ts +13 -0
- package/lib/index.js +14 -0
- package/lib/smoke/mastra-tool.d.ts +1 -0
- package/lib/smoke/mastra-tool.js +46 -0
- package/lib/src/_contracts.d.ts +71 -0
- package/lib/src/_contracts.js +63 -0
- package/lib/src/_mastra.d.ts +11 -0
- package/lib/src/_mastra.js +66 -0
- package/lib/src/config.d.ts +70 -0
- package/lib/src/config.js +87 -0
- package/lib/src/databricks/_runtime.d.ts +18 -0
- package/lib/src/databricks/_runtime.js +79 -0
- package/lib/src/databricks/client.d.ts +48 -0
- package/lib/src/databricks/client.js +174 -0
- package/lib/src/databricks/index.d.ts +3 -0
- package/lib/src/databricks/index.js +6 -0
- package/lib/src/graphiti/_backend.d.ts +9 -0
- package/lib/src/graphiti/_backend.js +214 -0
- package/lib/src/graphiti/index.d.ts +3 -0
- package/lib/src/graphiti/index.js +6 -0
- package/lib/src/graphiti/options.d.ts +7 -0
- package/lib/src/graphiti/options.js +7 -0
- package/lib/src/graphiti/runtime.d.ts +49 -0
- package/lib/src/graphiti/runtime.js +170 -0
- package/lib/src/plugin.d.ts +38 -0
- package/lib/src/plugin.js +196 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/package.json +104 -4
- package/src/_contracts.ts +107 -0
- package/src/_mastra.ts +80 -0
- package/src/config.ts +134 -0
- package/src/databricks/_runtime.ts +103 -0
- package/src/databricks/client.ts +251 -0
- package/src/databricks/index.ts +7 -0
- package/src/graphiti/_backend.ts +282 -0
- package/src/graphiti/index.ts +7 -0
- package/src/graphiti/options.ts +17 -0
- package/src/graphiti/runtime.ts +261 -0
- package/src/plugin.ts +254 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node Graphiti runtime and process supervision.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { delimiter, dirname } from "node:path";
|
|
8
|
+
import { exec } from "@dbx-tools/core";
|
|
9
|
+
import type { ChildProcessResult } from "@dbx-tools/core/exec";
|
|
10
|
+
import {
|
|
11
|
+
asyncUtils,
|
|
12
|
+
environmentUtils,
|
|
13
|
+
functionUtils,
|
|
14
|
+
options as sharedOptions,
|
|
15
|
+
} from "@dbx-tools/shared-core";
|
|
16
|
+
import { coerce, rcompare, type SemVer } from "semver";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
graphitiOptionsEnvironment,
|
|
20
|
+
resolveGraphitiOptions,
|
|
21
|
+
type GraphitiOptions,
|
|
22
|
+
type ResolvedGraphitiOptions,
|
|
23
|
+
} from "./options.ts";
|
|
24
|
+
|
|
25
|
+
/** Context passed to one Graphiti sidecar readiness probe. */
|
|
26
|
+
export interface GraphitiHealthCheckContext {
|
|
27
|
+
readonly process: ChildProcessResult;
|
|
28
|
+
readonly signal: AbortSignal;
|
|
29
|
+
readonly attempt: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** HTTP probe used to decide that the Graphiti sidecar has started listening. */
|
|
33
|
+
export type GraphitiHealthCheck = (
|
|
34
|
+
context: GraphitiHealthCheckContext,
|
|
35
|
+
) => boolean | PromiseLike<boolean>;
|
|
36
|
+
|
|
37
|
+
/** Async creation result that preserves the awaitable child handle. */
|
|
38
|
+
export interface GraphitiChildProcess {
|
|
39
|
+
child: ChildProcessResult;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Shared Graphiti options plus the Node-only sidecar readiness probe. */
|
|
43
|
+
export type GraphitiRuntimeOptions = GraphitiOptions & {
|
|
44
|
+
/** Readiness probe for {@link createGraphitiChildProcess}. Defaults to `/healthcheck`. */
|
|
45
|
+
healthCheck?: GraphitiHealthCheck;
|
|
46
|
+
/** Cancels sidecar startup without changing the serialized Graphiti configuration. */
|
|
47
|
+
signal?: AbortSignal;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const HEALTH_CHECK_INTERVAL_MS = 200;
|
|
51
|
+
const PYTHON_CANDIDATES = ["python", "python3"] as const;
|
|
52
|
+
const GRAPHITI_IMPORT_PROBE = "import dbx_tools.graphiti";
|
|
53
|
+
|
|
54
|
+
interface PythonCandidate {
|
|
55
|
+
command: string;
|
|
56
|
+
version: SemVer;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const graphitiPython = functionUtils.memoize(async (): Promise<string | undefined> => {
|
|
60
|
+
const available = (
|
|
61
|
+
await Promise.all(PYTHON_CANDIDATES.map((command) => _probePython(command)))
|
|
62
|
+
).filter((candidate): candidate is PythonCandidate => candidate !== undefined);
|
|
63
|
+
available.sort((left, right) => rcompare(left.version, right.version));
|
|
64
|
+
return available[0]?.command;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
function graphitiOptions(options: GraphitiRuntimeOptions): GraphitiOptions {
|
|
68
|
+
const { healthCheck: _healthCheck, signal: _signal, ...graphiti } = options;
|
|
69
|
+
return graphiti;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Milliseconds left in a startup budget after `startedAt`. */
|
|
73
|
+
export function remainingTimeoutMs(startedAt: number, timeoutMs: number, now = Date.now()): number {
|
|
74
|
+
return Math.max(0, timeoutMs - (now - startedAt));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function _runtimeEnvironment(options: ResolvedGraphitiOptions): NodeJS.ProcessEnv {
|
|
78
|
+
if (options.listen.scheme !== "tcp") throw new Error("Graphiti requires a TCP listener");
|
|
79
|
+
const nodeBin = process.env.DBX_TOOLS_NODE_BIN?.trim();
|
|
80
|
+
const path = nodeBin
|
|
81
|
+
? [dirname(nodeBin), process.env.PATH].filter(Boolean).join(delimiter)
|
|
82
|
+
: process.env.PATH;
|
|
83
|
+
return {
|
|
84
|
+
...process.env,
|
|
85
|
+
...(path ? { PATH: path } : {}),
|
|
86
|
+
...graphitiOptionsEnvironment(options),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Return whether the Graphiti `/healthcheck` endpoint responds successfully. */
|
|
91
|
+
export async function graphitiHealthCheck(
|
|
92
|
+
options: Pick<ResolvedGraphitiOptions, "listen" | "bearer">,
|
|
93
|
+
signal: AbortSignal,
|
|
94
|
+
): Promise<boolean> {
|
|
95
|
+
try {
|
|
96
|
+
return (
|
|
97
|
+
await fetch(graphitiHttpUrl(options, "/healthcheck"), {
|
|
98
|
+
signal,
|
|
99
|
+
headers: graphitiRequestHeaders(options),
|
|
100
|
+
})
|
|
101
|
+
).ok;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (signal.aborted) throw error;
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Poll `/healthcheck` until it succeeds or `timeoutMs` elapses. */
|
|
109
|
+
export async function waitForGraphitiHealth(
|
|
110
|
+
options: Pick<ResolvedGraphitiOptions, "listen" | "bearer">,
|
|
111
|
+
timeoutMs: number,
|
|
112
|
+
signal?: AbortSignal,
|
|
113
|
+
): Promise<void> {
|
|
114
|
+
const budget = Math.max(1, timeoutMs);
|
|
115
|
+
for await (const healthy of asyncUtils.poll(
|
|
116
|
+
({ signal: pollSignal }) => graphitiHealthCheck(options, pollSignal),
|
|
117
|
+
{
|
|
118
|
+
intervalMs: HEALTH_CHECK_INTERVAL_MS,
|
|
119
|
+
timeoutMs: budget,
|
|
120
|
+
signal,
|
|
121
|
+
predicate: (ready) => !ready,
|
|
122
|
+
},
|
|
123
|
+
)) {
|
|
124
|
+
if (healthy) return;
|
|
125
|
+
}
|
|
126
|
+
throw new Error("Graphiti sidecar healthcheck did not succeed");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Build authorization headers for one optionally secured Graphiti runtime. */
|
|
130
|
+
export function graphitiRequestHeaders(
|
|
131
|
+
options: Pick<ResolvedGraphitiOptions, "bearer">,
|
|
132
|
+
): Readonly<Record<string, string>> {
|
|
133
|
+
return options.bearer ? { authorization: `Bearer ${options.bearer}` } : {};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Executable and arguments used to launch Graphiti. */
|
|
137
|
+
export interface GraphitiPythonCommand {
|
|
138
|
+
command: string;
|
|
139
|
+
args: string[];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Resolve configured or app-installed Python, otherwise a uv-provisioned runtime. */
|
|
143
|
+
export async function resolveGraphitiPythonCommand(
|
|
144
|
+
options: { dev?: boolean } = {},
|
|
145
|
+
...args: string[]
|
|
146
|
+
): Promise<GraphitiPythonCommand> {
|
|
147
|
+
const python = process.env.PYTHON?.trim();
|
|
148
|
+
if (python) return { command: python, args };
|
|
149
|
+
if (environmentUtils.isDatabricksAppEnv()) {
|
|
150
|
+
const installed = await graphitiPython();
|
|
151
|
+
if (installed) return { command: installed, args };
|
|
152
|
+
}
|
|
153
|
+
const extra = options.dev ? "[dev]" : "";
|
|
154
|
+
const packageArgs = [
|
|
155
|
+
"--with",
|
|
156
|
+
`dbx-tools-graphiti${extra}==${(await import("../../index.ts")).PACKAGE_VERSION}`,
|
|
157
|
+
];
|
|
158
|
+
return {
|
|
159
|
+
command: "uv",
|
|
160
|
+
args: ["run", "--no-project", ...packageArgs, "--python", "python3", "python", ...args],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function _probePython(command: string): Promise<PythonCandidate | undefined> {
|
|
165
|
+
const versionResult = await exec.spawn(command, ["-V"], {
|
|
166
|
+
check: false,
|
|
167
|
+
stdin: "ignore",
|
|
168
|
+
stdout: "capture",
|
|
169
|
+
stderr: "capture",
|
|
170
|
+
trim: true,
|
|
171
|
+
});
|
|
172
|
+
if (versionResult.exitCode !== 0) return undefined;
|
|
173
|
+
const version = coerce(`${versionResult.stdout}\n${versionResult.stderr}`);
|
|
174
|
+
if (!version) return undefined;
|
|
175
|
+
|
|
176
|
+
const importResult = await exec.spawn(command, ["-c", GRAPHITI_IMPORT_PROBE], {
|
|
177
|
+
check: false,
|
|
178
|
+
stdin: "ignore",
|
|
179
|
+
stdout: "ignore",
|
|
180
|
+
stderr: "ignore",
|
|
181
|
+
});
|
|
182
|
+
return importResult.exitCode === 0 ? { command, version } : undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Build one HTTP URL for a resolved Graphiti listener. */
|
|
186
|
+
export function graphitiHttpUrl(
|
|
187
|
+
options: Pick<ResolvedGraphitiOptions, "listen">,
|
|
188
|
+
path: string,
|
|
189
|
+
): string {
|
|
190
|
+
return `${sharedOptions
|
|
191
|
+
.formatListenAddress(options.listen)
|
|
192
|
+
.replace(/^tcp:/, "http:")}${path.startsWith("/") ? path : `/${path}`}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Build one readiness-gated Graphiti sidecar from the shared typed options. */
|
|
196
|
+
export async function createGraphitiChildProcess(
|
|
197
|
+
options: GraphitiRuntimeOptions = {},
|
|
198
|
+
): Promise<GraphitiChildProcess> {
|
|
199
|
+
const healthCheck = options.healthCheck;
|
|
200
|
+
const resolved = resolveGraphitiOptions(graphitiOptions(options));
|
|
201
|
+
const python = await resolveGraphitiPythonCommand(
|
|
202
|
+
{ dev: !resolved.databaseUrl },
|
|
203
|
+
"-m",
|
|
204
|
+
"dbx_tools.graphiti",
|
|
205
|
+
);
|
|
206
|
+
const child = exec.spawn(python.command, python.args, {
|
|
207
|
+
env: _runtimeEnvironment(resolved),
|
|
208
|
+
forwardTerminationSignals: false,
|
|
209
|
+
});
|
|
210
|
+
const controller = new AbortController();
|
|
211
|
+
const startupSignal = options.signal
|
|
212
|
+
? AbortSignal.any([controller.signal, options.signal])
|
|
213
|
+
: controller.signal;
|
|
214
|
+
const probe = healthCheck ?? (({ signal }) => graphitiHealthCheck(resolved, signal));
|
|
215
|
+
try {
|
|
216
|
+
await Promise.race([
|
|
217
|
+
waitForGraphitiChild(child, probe, resolved.startupTimeoutMs, startupSignal),
|
|
218
|
+
child.then((result) => {
|
|
219
|
+
throw new Error(
|
|
220
|
+
`Graphiti child ${child.pid ?? "<unknown>"} exited before becoming ready ` +
|
|
221
|
+
`(exit ${result.exitCode})`,
|
|
222
|
+
);
|
|
223
|
+
}),
|
|
224
|
+
]);
|
|
225
|
+
return { child };
|
|
226
|
+
} catch (error) {
|
|
227
|
+
await child.shutdown();
|
|
228
|
+
throw error;
|
|
229
|
+
} finally {
|
|
230
|
+
controller.abort();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function waitForGraphitiChild(
|
|
235
|
+
child: ChildProcessResult,
|
|
236
|
+
healthCheck: GraphitiHealthCheck,
|
|
237
|
+
timeoutMs: number,
|
|
238
|
+
signal: AbortSignal,
|
|
239
|
+
): Promise<void> {
|
|
240
|
+
for await (const healthy of asyncUtils.poll(
|
|
241
|
+
({ attempt, signal: pollSignal }) =>
|
|
242
|
+
healthCheck({ process: child, signal: pollSignal, attempt }),
|
|
243
|
+
{
|
|
244
|
+
intervalMs: HEALTH_CHECK_INTERVAL_MS,
|
|
245
|
+
timeoutMs,
|
|
246
|
+
signal,
|
|
247
|
+
predicate: (ready) => !ready,
|
|
248
|
+
},
|
|
249
|
+
)) {
|
|
250
|
+
if (healthy) return;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Run the supervised stack until the child exits. */
|
|
255
|
+
export async function runGraphiti(options: GraphitiRuntimeOptions = {}): Promise<void> {
|
|
256
|
+
const { child } = await createGraphitiChildProcess(options);
|
|
257
|
+
const outcome = await child;
|
|
258
|
+
if (outcome.exitCode !== 0) {
|
|
259
|
+
throw new Error(`Graphiti child exited with status ${outcome.exitCode}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AppKit memory plugin with Databricks-first tools and lazy Graphiti fallback.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getExecutionContext, toPlugin, type PluginManifest } from "@databricks/appkit";
|
|
8
|
+
import {
|
|
9
|
+
defineTool,
|
|
10
|
+
executeFromRegistry,
|
|
11
|
+
type ToolkitEntry,
|
|
12
|
+
type ToolkitOptions,
|
|
13
|
+
type ToolRegistry,
|
|
14
|
+
} from "@databricks/appkit/beta";
|
|
15
|
+
import {
|
|
16
|
+
appkit as dbxAppkit,
|
|
17
|
+
toolkitEntries,
|
|
18
|
+
ToolRegistryPlugin,
|
|
19
|
+
type AgentToolExecutionContext,
|
|
20
|
+
} from "@dbx-tools/appkit";
|
|
21
|
+
import { errorUtils, log } from "@dbx-tools/shared-core";
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
RecallMemoryInputSchema,
|
|
25
|
+
RememberMemoryInputSchema,
|
|
26
|
+
type GraphitiMemoryBackend,
|
|
27
|
+
type MemoryTurn,
|
|
28
|
+
type RecallMemoryInput,
|
|
29
|
+
type RecallMemoryResult,
|
|
30
|
+
type RememberMemoryInput,
|
|
31
|
+
type RememberMemoryResult,
|
|
32
|
+
} from "./_contracts.ts";
|
|
33
|
+
import {
|
|
34
|
+
MEMORY_CONFIG_SCHEMA,
|
|
35
|
+
resolveMemoryPluginConfig,
|
|
36
|
+
type MemoryPluginConfig,
|
|
37
|
+
type ResolvedMemoryPluginConfig,
|
|
38
|
+
} from "./config.ts";
|
|
39
|
+
import { DatabricksMemoryRuntime } from "./databricks/_runtime.ts";
|
|
40
|
+
|
|
41
|
+
/** AppKit memory plugin with Databricks managed memory and Graphiti fallback. */
|
|
42
|
+
export class MemoryPlugin extends ToolRegistryPlugin<MemoryPluginConfig> {
|
|
43
|
+
static phase = "deferred" as const;
|
|
44
|
+
static manifest: PluginManifest<"memory"> = {
|
|
45
|
+
name: "memory",
|
|
46
|
+
displayName: "Memory",
|
|
47
|
+
description: "Publishes Databricks-first durable memory tools with a lazy Graphiti fallback.",
|
|
48
|
+
stability: "beta",
|
|
49
|
+
resources: { required: [], optional: [] },
|
|
50
|
+
config: { schema: MEMORY_CONFIG_SCHEMA },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
private readonly logger = log.logger(this);
|
|
54
|
+
private resolved?: ResolvedMemoryPluginConfig;
|
|
55
|
+
private databricks?: DatabricksMemoryRuntime;
|
|
56
|
+
private graphiti?: GraphitiMemoryBackend;
|
|
57
|
+
private graphitiStarting?: Promise<GraphitiMemoryBackend>;
|
|
58
|
+
private readonly graphitiStartupAbort = new AbortController();
|
|
59
|
+
private closed = false;
|
|
60
|
+
|
|
61
|
+
protected get toolRegistry(): ToolRegistry {
|
|
62
|
+
return this.registry();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
override async setup(): Promise<void> {
|
|
66
|
+
const resolved = resolveMemoryPluginConfig(this.config);
|
|
67
|
+
if (resolved.saveTurns === true && resolved.graphiti === false) {
|
|
68
|
+
throw new Error("AppKit memory saveTurns requires the Graphiti fallback");
|
|
69
|
+
}
|
|
70
|
+
this.resolved = resolved;
|
|
71
|
+
if (resolved.databricks) {
|
|
72
|
+
this.databricks = new DatabricksMemoryRuntime(
|
|
73
|
+
getExecutionContext().client,
|
|
74
|
+
resolved.databricks,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let mastra = false;
|
|
79
|
+
const context = this.context;
|
|
80
|
+
const shouldAttachTurnSaver =
|
|
81
|
+
resolved.saveTurns !== false &&
|
|
82
|
+
resolved.graphiti !== false &&
|
|
83
|
+
context?.hasPlugin("mastra") === true;
|
|
84
|
+
if (shouldAttachTurnSaver && context) {
|
|
85
|
+
const { attachMastraTurnSaver } = await import("./_mastra.ts");
|
|
86
|
+
mastra = await attachMastraTurnSaver(context, (turn, signal) => this.saveTurn(turn, signal));
|
|
87
|
+
}
|
|
88
|
+
if (resolved.saveTurns === true && !mastra) {
|
|
89
|
+
throw new Error("AppKit memory saveTurns requires a registered Mastra plugin");
|
|
90
|
+
}
|
|
91
|
+
this.logger.info("ready", {
|
|
92
|
+
databricks: resolved.databricks !== false,
|
|
93
|
+
graphiti: resolved.graphiti !== false ? "lazy" : false,
|
|
94
|
+
mastraTurnSaver: mastra,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async toolkit(options?: ToolkitOptions): Promise<Record<string, ToolkitEntry>> {
|
|
99
|
+
return toolkitEntries.entries("memory", this.getAgentTools(), {
|
|
100
|
+
...options,
|
|
101
|
+
prefix: options?.prefix ?? "",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
override executeAgentTool(
|
|
106
|
+
name: string,
|
|
107
|
+
args: unknown,
|
|
108
|
+
signal?: AbortSignal,
|
|
109
|
+
context?: AgentToolExecutionContext,
|
|
110
|
+
): Promise<unknown> {
|
|
111
|
+
return executeFromRegistry(this.registry(context?.resourceId), name, args, signal);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
override abortActiveOperations(): void {
|
|
115
|
+
super.abortActiveOperations();
|
|
116
|
+
this.graphitiStartupAbort.abort(new Error("AppKit memory is shutting down"));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async shutdown(): Promise<void> {
|
|
120
|
+
this.closed = true;
|
|
121
|
+
this.graphitiStartupAbort.abort(new Error("AppKit memory is shutting down"));
|
|
122
|
+
const starting = this.graphitiStarting;
|
|
123
|
+
const backend = this.graphiti;
|
|
124
|
+
this.graphitiStarting = undefined;
|
|
125
|
+
this.graphiti = undefined;
|
|
126
|
+
await starting?.catch(() => undefined);
|
|
127
|
+
await backend?.shutdown();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private registry(resourceId?: string): ToolRegistry {
|
|
131
|
+
return {
|
|
132
|
+
remember: defineTool({
|
|
133
|
+
description:
|
|
134
|
+
"Save a durable user preference, fact, decision, or instruction for future conversations.",
|
|
135
|
+
schema: RememberMemoryInputSchema,
|
|
136
|
+
annotations: { effect: "write", requiresUserContext: true },
|
|
137
|
+
autoInheritable: false,
|
|
138
|
+
execute: (input, signal) => this.remember(this.actorId(resourceId), input, signal),
|
|
139
|
+
}),
|
|
140
|
+
recall_memory: defineTool({
|
|
141
|
+
description:
|
|
142
|
+
"Search durable actor-scoped memory for facts and preferences relevant to the current task.",
|
|
143
|
+
schema: RecallMemoryInputSchema,
|
|
144
|
+
annotations: { effect: "read", requiresUserContext: true },
|
|
145
|
+
autoInheritable: false,
|
|
146
|
+
execute: (input, signal) => this.recall(this.actorId(resourceId), input, signal),
|
|
147
|
+
}),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private async remember(
|
|
152
|
+
actorId: string,
|
|
153
|
+
input: RememberMemoryInput,
|
|
154
|
+
signal?: AbortSignal,
|
|
155
|
+
): Promise<RememberMemoryResult> {
|
|
156
|
+
const databricks = await this.databricksBackend(actorId, signal);
|
|
157
|
+
if (databricks) {
|
|
158
|
+
try {
|
|
159
|
+
return await databricks.remember(actorId, input, signal);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (!this.shouldFallbackFromDatabricks(error)) throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return (await this.graphitiBackend()).remember(actorId, input, signal);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private async recall(
|
|
168
|
+
actorId: string,
|
|
169
|
+
input: RecallMemoryInput,
|
|
170
|
+
signal?: AbortSignal,
|
|
171
|
+
): Promise<RecallMemoryResult> {
|
|
172
|
+
const parsed = RecallMemoryInputSchema.parse(input);
|
|
173
|
+
const databricks = await this.databricksBackend(actorId, signal);
|
|
174
|
+
if (databricks) {
|
|
175
|
+
try {
|
|
176
|
+
return await databricks.recall(actorId, parsed, signal);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (!this.shouldFallbackFromDatabricks(error)) throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return (await this.graphitiBackend()).recall(actorId, parsed, signal);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private async saveTurn(turn: MemoryTurn, signal?: AbortSignal): Promise<void> {
|
|
185
|
+
if (await this.databricksBackend(turn.actorId, signal)) return;
|
|
186
|
+
await (await this.graphitiBackend()).saveTurn(turn, signal);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private async databricksBackend(
|
|
190
|
+
actorId: string,
|
|
191
|
+
signal?: AbortSignal,
|
|
192
|
+
): Promise<DatabricksMemoryRuntime | undefined> {
|
|
193
|
+
const backend = this.databricks;
|
|
194
|
+
if (!backend) return undefined;
|
|
195
|
+
if (await backend.available(actorId, signal)) return backend;
|
|
196
|
+
const configured = this.requireConfig().databricks;
|
|
197
|
+
if (configured && configured.mode === "required") {
|
|
198
|
+
throw new Error("Required Databricks managed memory is unavailable for the current actor");
|
|
199
|
+
}
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private shouldFallbackFromDatabricks(error: unknown): boolean {
|
|
204
|
+
const configured = this.requireConfig().databricks;
|
|
205
|
+
return (
|
|
206
|
+
configured !== false &&
|
|
207
|
+
configured.mode === "auto" &&
|
|
208
|
+
errorUtils.errorContext(error).hasStatusCode(403, 404)
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private async graphitiBackend(): Promise<GraphitiMemoryBackend> {
|
|
213
|
+
if (this.closed) throw new Error("AppKit memory is closed");
|
|
214
|
+
if (this.graphiti?.running) return this.graphiti;
|
|
215
|
+
if (this.graphiti) {
|
|
216
|
+
await this.graphiti.shutdown();
|
|
217
|
+
this.graphiti = undefined;
|
|
218
|
+
}
|
|
219
|
+
const options = this.requireConfig().graphiti;
|
|
220
|
+
if (!options) throw new Error("Graphiti fallback is disabled");
|
|
221
|
+
this.graphitiStarting ??= import("./graphiti/_backend.ts")
|
|
222
|
+
.then(({ startGraphitiBackend }) =>
|
|
223
|
+
startGraphitiBackend(options, this.graphitiStartupAbort.signal),
|
|
224
|
+
)
|
|
225
|
+
.then((backend) => {
|
|
226
|
+
if (this.closed) {
|
|
227
|
+
return backend.shutdown().then(() => {
|
|
228
|
+
throw new Error("AppKit memory closed during Graphiti startup");
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
this.graphiti = backend;
|
|
232
|
+
return backend;
|
|
233
|
+
})
|
|
234
|
+
.finally(() => {
|
|
235
|
+
this.graphitiStarting = undefined;
|
|
236
|
+
});
|
|
237
|
+
return this.graphitiStarting;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private actorId(resourceId?: string): string {
|
|
241
|
+
if (resourceId) return resourceId;
|
|
242
|
+
const context = dbxAppkit.tryGetExecutionContext();
|
|
243
|
+
if (!context) throw new Error("Memory tools require an AppKit execution context");
|
|
244
|
+
return "userId" in context ? context.userId : context.serviceUserId;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private requireConfig(): ResolvedMemoryPluginConfig {
|
|
248
|
+
if (!this.resolved) throw new Error("AppKit memory is not initialized");
|
|
249
|
+
return this.resolved;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** AppKit registration factory for {@link MemoryPlugin}. */
|
|
254
|
+
export const memory = toPlugin(MemoryPlugin);
|