@tangle-network/agent-runtime 0.314.1 → 0.316.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +6 -0
  2. package/dist/{activation-BpMnOlw5.d.ts → activation-BFDmjaKP.d.ts} +2 -2
  3. package/dist/{activation-Ci20OAsG.js → activation-CvdE2HIU.js} +3 -3
  4. package/dist/{activation-Ci20OAsG.js.map → activation-CvdE2HIU.js.map} +1 -1
  5. package/dist/agent.d.ts +2 -1
  6. package/dist/agent.js +2 -2
  7. package/dist/{bundle-BK3XBY6q.js → bundle-RgHpAFDY.js} +2 -2
  8. package/dist/{bundle-BK3XBY6q.js.map → bundle-RgHpAFDY.js.map} +1 -1
  9. package/dist/candidate-execution/index.js +5 -5
  10. package/dist/{coordination-driver-CyJF37L8.js → coordination-driver-DGhN8GO9.js} +3 -3
  11. package/dist/{coordination-driver-CyJF37L8.js.map → coordination-driver-DGhN8GO9.js.map} +1 -1
  12. package/dist/{delegate-CTezpiM2.js → delegate-B5pmSbQa.js} +2 -2
  13. package/dist/{delegate-CTezpiM2.js.map → delegate-B5pmSbQa.js.map} +1 -1
  14. package/dist/durable.d.ts +1 -1
  15. package/dist/durable.js +4 -4
  16. package/dist/{graph-DGDrO_ZW.js → graph-G0j1ysqe.js} +4 -4
  17. package/dist/{graph-DGDrO_ZW.js.map → graph-G0j1ysqe.js.map} +1 -1
  18. package/dist/{improvement-cycle-DReg-Qi4.js → improvement-cycle-D7yR6GC2.js} +6 -6
  19. package/dist/{improvement-cycle-DReg-Qi4.js.map → improvement-cycle-D7yR6GC2.js.map} +1 -1
  20. package/dist/{index-B89bMKhD.d.ts → index-CUh40yUJ.d.ts} +13 -90
  21. package/dist/index.d.ts +4 -4
  22. package/dist/index.js +9 -9
  23. package/dist/integrations-DopfCC2a.d.ts +190 -0
  24. package/dist/integrations-dFi-ALk7.js +132 -0
  25. package/dist/integrations-dFi-ALk7.js.map +1 -0
  26. package/dist/intelligence.d.ts +2 -2
  27. package/dist/intelligence.js +7 -7
  28. package/dist/kernel.d.ts +2 -2
  29. package/dist/kernel.js +9 -9
  30. package/dist/{loop-runner-bin-eoycPwY3.js → loop-runner-bin-CFVX4oi0.js} +3 -3
  31. package/dist/{loop-runner-bin-eoycPwY3.js.map → loop-runner-bin-CFVX4oi0.js.map} +1 -1
  32. package/dist/{loop-runner-bin-BM1in-RT.d.ts → loop-runner-bin-DQKLUt0l.d.ts} +2 -2
  33. package/dist/loop-runner-bin.d.ts +1 -1
  34. package/dist/loop-runner-bin.js +1 -1
  35. package/dist/mcp/bin.js +3 -3
  36. package/dist/mcp/index.d.ts +74 -33
  37. package/dist/mcp/index.js +129 -6
  38. package/dist/mcp/index.js.map +1 -1
  39. package/dist/{openai-tools-xPO6dzf4.d.ts → openai-tools-u3gWBi3x.d.ts} +2 -2
  40. package/dist/placement.d.ts +132 -0
  41. package/dist/placement.js +319 -0
  42. package/dist/placement.js.map +1 -0
  43. package/dist/platform.d.ts +1 -188
  44. package/dist/platform.js +1 -129
  45. package/dist/platform.js.map +1 -1
  46. package/dist/{prepare-8A1CAWG8.js → prepare-CD9kR0gS.js} +3 -3
  47. package/dist/{prepare-8A1CAWG8.js.map → prepare-CD9kR0gS.js.map} +1 -1
  48. package/dist/{profile-BhQShsM1.js → profile-CHXYmpGd.js} +2 -2
  49. package/dist/{profile-BhQShsM1.js.map → profile-CHXYmpGd.js.map} +1 -1
  50. package/dist/profile-materialization-Cca615eu.d.ts +90 -0
  51. package/dist/profiles.d.ts +1 -1
  52. package/dist/{protected-model-port-ug52OmvB.js → protected-model-port-B0pHd65J.js} +4 -4
  53. package/dist/{protected-model-port-ug52OmvB.js.map → protected-model-port-B0pHd65J.js.map} +1 -1
  54. package/dist/{provision-supervisor-B6D3XRbL.js → provision-supervisor-BQLLrUtn.js} +4 -4
  55. package/dist/{provision-supervisor-B6D3XRbL.js.map → provision-supervisor-BQLLrUtn.js.map} +1 -1
  56. package/dist/{runtime-NlIVxRXu.js → runtime-B9BOFyEp.js} +10 -9
  57. package/dist/runtime-B9BOFyEp.js.map +1 -0
  58. package/dist/{search-executor-DFs-79tK.js → search-executor-B5ZVFsDV.js} +3 -3
  59. package/dist/{search-executor-DFs-79tK.js.map → search-executor-B5ZVFsDV.js.map} +1 -1
  60. package/dist/{server-B8ubWXpW.js → server-TwpH9aik.js} +4 -4
  61. package/dist/{server-B8ubWXpW.js.map → server-TwpH9aik.js.map} +1 -1
  62. package/dist/{supervise-BE9ta8bl.js → supervise-CU6AuxWY.js} +119 -29
  63. package/dist/supervise-CU6AuxWY.js.map +1 -0
  64. package/dist/{supervisor-DmSuKksw.js → supervisor-CbFyf48U.js} +3 -3
  65. package/dist/{supervisor-DmSuKksw.js.map → supervisor-CbFyf48U.js.map} +1 -1
  66. package/dist/testing.d.ts +1 -1
  67. package/dist/testing.js +14 -14
  68. package/dist/tool-loop.d.ts +35 -5
  69. package/dist/tool-loop.js +48 -21
  70. package/dist/tool-loop.js.map +1 -1
  71. package/dist/tui/index.d.ts +1 -1
  72. package/dist/tui/index.js +1 -1
  73. package/dist/workspace-store/index.d.ts +206 -0
  74. package/dist/workspace-store/index.js +855 -0
  75. package/dist/workspace-store/index.js.map +1 -0
  76. package/dist/{workspace-streams-BEvSX97P.js → workspace-streams-DkijayqX.js} +16 -3
  77. package/dist/workspace-streams-DkijayqX.js.map +1 -0
  78. package/package.json +14 -4
  79. package/skills/profile-authoring/SKILL.md +114 -171
  80. package/dist/runtime-NlIVxRXu.js.map +0 -1
  81. package/dist/supervise-BE9ta8bl.js.map +0 -1
  82. package/dist/workspace-streams-BEvSX97P.js.map +0 -1
@@ -0,0 +1,855 @@
1
+ import { z } from "zod";
2
+ //#region src/workspace-store/store.ts
3
+ /** A write lost a compare-and-set race or targeted the wrong revision. Re-read and retry. */
4
+ var WorkspaceConflictError = class extends Error {
5
+ path;
6
+ expected;
7
+ actual;
8
+ code = "conflict";
9
+ constructor(path, expected, actual) {
10
+ super(`${path} is at revision ${actual ?? "none"}, expected ${expected ?? "none"}; read it again`);
11
+ this.path = path;
12
+ this.expected = expected;
13
+ this.actual = actual;
14
+ this.name = "WorkspaceConflictError";
15
+ }
16
+ };
17
+ const SEGMENT = /^[^/\s\\][^/\\]*$/;
18
+ const MAX_PATH = 512;
19
+ /** Refuse anything but an absolute, normalized path such as `/files/report.md`. */
20
+ function assertWorkspacePath(path) {
21
+ if (typeof path !== "string" || path.length > MAX_PATH || !path.startsWith("/")) throw new TypeError(`workspace path must be absolute and at most ${MAX_PATH} chars: ${path}`);
22
+ const segments = path.slice(1).split("/");
23
+ for (const segment of segments) if (!SEGMENT.test(segment) || segment === "." || segment === "..") throw new TypeError(`workspace path is not normalized: ${path}`);
24
+ }
25
+ /** The member a path belongs to, or `null` for workspace-scoped paths. */
26
+ function memberIdFromPath(path) {
27
+ const match = /^\/memories\/members\/([^/]+)\//.exec(path);
28
+ return match ? match[1] : null;
29
+ }
30
+ /** Validate a put before any adapter touches storage; adapters call this first. */
31
+ function checkWorkspacePut(input) {
32
+ assertWorkspacePath(input.path);
33
+ if (!input.workspaceId) throw new TypeError("workspaceId is required");
34
+ if (!input.actor) throw new TypeError("actor is required");
35
+ if (input.content === void 0 === (input.bytes === void 0)) throw new TypeError(`${input.path}: exactly one of content and bytes is required`);
36
+ if (input.ifRevision !== null && !Number.isSafeInteger(input.ifRevision)) throw new TypeError(`${input.path}: ifRevision must be an integer or null`);
37
+ }
38
+ /** Process-local adapter for tests, previews, and single-instance hosts. */
39
+ function createInMemoryWorkspaceStore(options = {}) {
40
+ const now = () => (options.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
41
+ const newId = options.newId ?? (() => crypto.randomUUID());
42
+ const live = /* @__PURE__ */ new Map();
43
+ const lastRevision = /* @__PURE__ */ new Map();
44
+ const erased = [];
45
+ const tenant = (workspaceId) => {
46
+ let entries = live.get(workspaceId);
47
+ if (!entries) {
48
+ entries = /* @__PURE__ */ new Map();
49
+ live.set(workspaceId, entries);
50
+ }
51
+ return entries;
52
+ };
53
+ return {
54
+ async get(workspaceId, path) {
55
+ const entry = live.get(workspaceId)?.get(path);
56
+ return entry ? structuredClone(entry) : null;
57
+ },
58
+ async list(workspaceId, prefix) {
59
+ return [...live.get(workspaceId)?.values() ?? []].filter((entry) => entry.path.startsWith(prefix)).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0).map((entry) => structuredClone(entry));
60
+ },
61
+ async put(input) {
62
+ checkWorkspacePut(input);
63
+ const entries = tenant(input.workspaceId);
64
+ const current = entries.get(input.path);
65
+ if ((current?.revision ?? null) !== input.ifRevision) throw new WorkspaceConflictError(input.path, input.ifRevision, current?.revision ?? null);
66
+ const key = `${input.workspaceId}\u0000${input.path}`;
67
+ const revision = (lastRevision.get(key) ?? 0) + 1;
68
+ const at = now();
69
+ const entry = {
70
+ workspaceId: input.workspaceId,
71
+ id: current?.id ?? input.id ?? newId(),
72
+ path: input.path,
73
+ kind: input.kind,
74
+ scope: memberIdFromPath(input.path) === null ? "workspace" : "member",
75
+ ...input.content !== void 0 ? { content: input.content } : {},
76
+ ...input.bytes !== void 0 ? { bytes: structuredClone(input.bytes) } : {},
77
+ revision,
78
+ createdBy: current?.createdBy ?? input.actor,
79
+ createdAt: current?.createdAt ?? at,
80
+ updatedBy: input.actor,
81
+ updatedAt: at
82
+ };
83
+ entries.set(input.path, entry);
84
+ lastRevision.set(key, revision);
85
+ return structuredClone(entry);
86
+ },
87
+ async delete(input) {
88
+ assertWorkspacePath(input.path);
89
+ const entries = tenant(input.workspaceId);
90
+ const current = entries.get(input.path);
91
+ if (input.ifRevision !== void 0 && current?.revision !== input.ifRevision) throw new WorkspaceConflictError(input.path, input.ifRevision, current?.revision ?? null);
92
+ if (!current) return null;
93
+ const revision = current.revision + 1;
94
+ const tombstone = {
95
+ workspaceId: input.workspaceId,
96
+ id: current.id,
97
+ path: current.path,
98
+ revision,
99
+ deletedBy: input.actor,
100
+ deletedAt: now()
101
+ };
102
+ entries.delete(input.path);
103
+ lastRevision.set(`${input.workspaceId}\u0000${input.path}`, revision);
104
+ erased.push(tombstone);
105
+ return { ...tombstone };
106
+ },
107
+ async tombstones(workspaceId, prefix = "/") {
108
+ return erased.filter((row) => row.workspaceId === workspaceId && row.path.startsWith(prefix)).map((row) => ({ ...row }));
109
+ }
110
+ };
111
+ }
112
+ //#endregion
113
+ //#region src/workspace-store/tools.ts
114
+ /**
115
+ * Memory and file tools over a {@link WorkspaceStore}, the default authority
116
+ * policy, lexical recall, and the pinned-memory renderer. The same tool objects
117
+ * serve an in-process tool loop and an MCP endpoint, so every executor reads
118
+ * and writes the same rows.
119
+ */
120
+ /** A policy or path rule refused the call. */
121
+ var WorkspacePolicyError = class extends Error {
122
+ code = "refused";
123
+ constructor(message) {
124
+ super(message);
125
+ this.name = "WorkspacePolicyError";
126
+ }
127
+ };
128
+ const MANAGERS = /* @__PURE__ */ new Set(["owner", "manager"]);
129
+ /**
130
+ * Default authority. Reads: workspace memory, the speaker's own member memory,
131
+ * `/knowledge/**`, `/files/**`. Writes: viewers and guests none; any member may
132
+ * save workspace facts and own-scope facts or preferences, and write `/files/**`;
133
+ * owners and managers also save instructions, workspace preferences, and
134
+ * `/knowledge/**`. Nobody touches another member's scope. `admit` is the
135
+ * product's content hook.
136
+ */
137
+ function defaultMemoryPolicy(options = {}) {
138
+ return {
139
+ authorize(turn, { op, path, kind }) {
140
+ const owner = memberIdFromPath(path);
141
+ if (owner !== null && owner !== turn.memberId) return "Another member's memory is private";
142
+ if (!(owner !== null || [
143
+ "/memories/workspace/",
144
+ "/knowledge/",
145
+ "/files/"
146
+ ].some((root) => path.startsWith(root)))) return `No access to ${path}`;
147
+ if (op === "read") return null;
148
+ const manager = MANAGERS.has(turn.role);
149
+ if (!manager && turn.role !== "member") return "This role is read-only";
150
+ if (path.startsWith("/knowledge/")) return manager ? null : "Knowledge is edited by an owner or manager";
151
+ if (path.startsWith("/files/")) return null;
152
+ if (kind === "instruction" && !manager) return "Only an owner or manager can save instructions";
153
+ if (kind === "preference" && owner === null && !manager) return "Members save preferences in their own scope (scope \"me\")";
154
+ return null;
155
+ },
156
+ ...options.admit ? { admit: options.admit } : {}
157
+ };
158
+ }
159
+ async function guard(context, turn, access, text) {
160
+ const policy = context.policy ?? defaultMemoryPolicy();
161
+ const refusal = policy.authorize(turn, access);
162
+ if (refusal) throw new WorkspacePolicyError(refusal);
163
+ if (text !== void 0 && access.kind && policy.admit) {
164
+ const admitted = await policy.admit(turn, {
165
+ path: access.path,
166
+ kind: access.kind,
167
+ text
168
+ });
169
+ if (admitted) throw new WorkspacePolicyError(admitted);
170
+ }
171
+ }
172
+ function schema(shape) {
173
+ return z.toJSONSchema(shape, { io: "input" });
174
+ }
175
+ const memoryId = z.string().regex(/^[A-Za-z0-9_-]{1,64}$/, "a memory id from recall");
176
+ const rememberArgs = z.object({
177
+ text: z.string().trim().min(1).max(1e3),
178
+ kind: z.enum([
179
+ "fact",
180
+ "preference",
181
+ "instruction"
182
+ ]),
183
+ scope: z.enum(["me", "workspace"]),
184
+ replaces: memoryId.optional()
185
+ }).strict();
186
+ const recallArgs = z.object({
187
+ query: z.string().trim().min(1).max(500),
188
+ limit: z.number().int().min(1).max(10).optional()
189
+ }).strict();
190
+ const forgetArgs = z.object({ id: memoryId }).strict();
191
+ function memoryPath(turn, scope, id) {
192
+ return scope === "me" ? `/memories/members/${turn.memberId}/${id}` : `/memories/workspace/${id}`;
193
+ }
194
+ /** Workspace memories plus the speaker's own member memories, the set a turn may see. */
195
+ async function readTurnMemories(store, turn) {
196
+ const [shared, own] = await Promise.all([store.list(turn.workspaceId, "/memories/workspace/"), store.list(turn.workspaceId, `/memories/members/${turn.memberId}/`)]);
197
+ return [...shared, ...own];
198
+ }
199
+ function memoryView(entry, score) {
200
+ return {
201
+ id: entry.id,
202
+ path: entry.path,
203
+ kind: entry.kind,
204
+ scope: entry.scope === "member" ? "me" : "workspace",
205
+ text: entry.content ?? "",
206
+ revision: entry.revision,
207
+ updatedAt: entry.updatedAt,
208
+ ...score !== void 0 ? { score: Number(score.toFixed(4)) } : {}
209
+ };
210
+ }
211
+ /** `memory.remember`, `memory.recall`, `memory.forget`. */
212
+ function createMemoryTools(context) {
213
+ const newId = context.newId ?? (() => crypto.randomUUID());
214
+ return [
215
+ {
216
+ name: "memory.remember",
217
+ description: "Save one durable memory the user asked you to keep (\"remember\", \"from now on\", \"always\", \"I prefer\"). kind: fact, preference, or instruction. scope \"me\" is private to the speaker; \"workspace\" is shared. To change an existing memory pass its id as replaces. Applies on the next message.",
218
+ inputSchema: schema(rememberArgs),
219
+ async run(args) {
220
+ const { text, kind, scope, replaces } = rememberArgs.parse(args);
221
+ const turn = await context.activeTurn();
222
+ const path = memoryPath(turn, scope, replaces ?? newId());
223
+ await guard(context, turn, {
224
+ op: "write",
225
+ path,
226
+ kind
227
+ }, text);
228
+ let ifRevision = null;
229
+ if (replaces) {
230
+ const current = await context.store.get(turn.workspaceId, path);
231
+ if (!current) throw new Error(`No ${scope} memory with id ${replaces}; recall it first`);
232
+ ifRevision = current.revision;
233
+ }
234
+ return {
235
+ saved: true,
236
+ ...memoryView(await context.store.put({
237
+ workspaceId: turn.workspaceId,
238
+ path,
239
+ kind,
240
+ ifRevision,
241
+ actor: turn.memberId,
242
+ content: text.replace(/\s+/g, " "),
243
+ id: path.slice(path.lastIndexOf("/") + 1)
244
+ })),
245
+ appliesTo: "next_message"
246
+ };
247
+ }
248
+ },
249
+ {
250
+ name: "memory.recall",
251
+ description: "Search saved memories (the workspace's and the speaker's own) by words. Returns ids for replaces and forget.",
252
+ inputSchema: schema(recallArgs),
253
+ async run(args) {
254
+ const { query, limit } = recallArgs.parse(args);
255
+ const turn = await context.activeTurn();
256
+ return { memories: rankEntries(await readTurnMemories(context.store, turn), query, limit ?? 5).map(({ entry, score }) => memoryView(entry, score)) };
257
+ }
258
+ },
259
+ {
260
+ name: "memory.forget",
261
+ description: "Erase one saved memory by id. The text is deleted; only an audit record remains.",
262
+ inputSchema: schema(forgetArgs),
263
+ async run(args) {
264
+ const { id } = forgetArgs.parse(args);
265
+ const turn = await context.activeTurn();
266
+ for (const scope of ["me", "workspace"]) {
267
+ const path = memoryPath(turn, scope, id);
268
+ const current = await context.store.get(turn.workspaceId, path);
269
+ if (!current) continue;
270
+ await guard(context, turn, {
271
+ op: "delete",
272
+ path,
273
+ kind: current.kind
274
+ });
275
+ return {
276
+ forgotten: true,
277
+ id,
278
+ path,
279
+ revision: (await context.store.delete({
280
+ workspaceId: turn.workspaceId,
281
+ path,
282
+ ifRevision: current.revision,
283
+ actor: turn.memberId
284
+ }))?.revision ?? null
285
+ };
286
+ }
287
+ throw new Error(`No memory with id ${id} that you can see`);
288
+ }
289
+ }
290
+ ];
291
+ }
292
+ const FILE_ROOTS = ["/files/", "/knowledge/"];
293
+ const MAX_FILE_TEXT = 1e6;
294
+ const filePath = z.string().max(512).refine((path) => FILE_ROOTS.some((root) => path.startsWith(root)), "path must start with /files/ or /knowledge/");
295
+ const listArgs = z.object({ path: filePath.optional() }).strict();
296
+ const readArgs = z.object({ path: filePath }).strict();
297
+ const writeArgs = z.object({
298
+ path: filePath,
299
+ content: z.string().max(MAX_FILE_TEXT),
300
+ ifRevision: z.number().int().min(1).nullable()
301
+ }).strict();
302
+ const deleteArgs = z.object({
303
+ path: filePath,
304
+ ifRevision: z.number().int().min(1)
305
+ }).strict();
306
+ const searchArgs = z.object({
307
+ query: z.string().trim().min(1).max(500),
308
+ path: filePath.optional(),
309
+ limit: z.number().int().min(1).max(10).optional()
310
+ }).strict();
311
+ function fileView(entry) {
312
+ return {
313
+ id: entry.id,
314
+ path: entry.path,
315
+ revision: entry.revision,
316
+ size: entry.bytes?.size ?? entry.content?.length ?? 0,
317
+ updatedBy: entry.updatedBy,
318
+ updatedAt: entry.updatedAt
319
+ };
320
+ }
321
+ /** `files.list`, `files.read`, `files.write`, `files.delete`, `files.search` over `/files/**` and `/knowledge/**`. */
322
+ function createFileTools(context) {
323
+ const listUnder = async (turn, prefix) => {
324
+ const prefixes = prefix ? [prefix] : FILE_ROOTS;
325
+ for (const root of prefixes) await guard(context, turn, {
326
+ op: "read",
327
+ path: root
328
+ });
329
+ return (await Promise.all(prefixes.map((root) => context.store.list(turn.workspaceId, root)))).flat();
330
+ };
331
+ return [
332
+ {
333
+ name: "files.list",
334
+ description: "List workspace files under a path prefix (default: /files/ and /knowledge/).",
335
+ inputSchema: schema(listArgs),
336
+ async run(args) {
337
+ const { path } = listArgs.parse(args);
338
+ const turn = await context.activeTurn();
339
+ return { files: (await listUnder(turn, path)).map(fileView) };
340
+ }
341
+ },
342
+ {
343
+ name: "files.read",
344
+ description: "Read one workspace file. Returns its revision for files.write and files.delete.",
345
+ inputSchema: schema(readArgs),
346
+ async run(args) {
347
+ const { path } = readArgs.parse(args);
348
+ const turn = await context.activeTurn();
349
+ await guard(context, turn, {
350
+ op: "read",
351
+ path
352
+ });
353
+ const entry = await context.store.get(turn.workspaceId, path);
354
+ if (!entry) throw new Error(`No file at ${path}`);
355
+ return {
356
+ ...fileView(entry),
357
+ ...entry.content !== void 0 ? { content: entry.content } : { bytes: entry.bytes }
358
+ };
359
+ }
360
+ },
361
+ {
362
+ name: "files.write",
363
+ description: "Create or replace a text file. ifRevision: null to create (fails if it exists), or the revision you read. A conflict means someone else changed it; read it again.",
364
+ inputSchema: schema(writeArgs),
365
+ async run(args) {
366
+ const { path, content, ifRevision } = writeArgs.parse(args);
367
+ const turn = await context.activeTurn();
368
+ await guard(context, turn, {
369
+ op: "write",
370
+ path,
371
+ kind: "file"
372
+ }, content);
373
+ return {
374
+ saved: true,
375
+ ...fileView(await context.store.put({
376
+ workspaceId: turn.workspaceId,
377
+ path,
378
+ kind: "file",
379
+ ifRevision,
380
+ actor: turn.memberId,
381
+ content
382
+ }))
383
+ };
384
+ }
385
+ },
386
+ {
387
+ name: "files.delete",
388
+ description: "Delete one file at the revision you read. The content is erased; an audit record remains.",
389
+ inputSchema: schema(deleteArgs),
390
+ async run(args) {
391
+ const { path, ifRevision } = deleteArgs.parse(args);
392
+ const turn = await context.activeTurn();
393
+ await guard(context, turn, {
394
+ op: "delete",
395
+ path,
396
+ kind: "file"
397
+ });
398
+ return {
399
+ deleted: true,
400
+ path,
401
+ revision: (await context.store.delete({
402
+ workspaceId: turn.workspaceId,
403
+ path,
404
+ ifRevision,
405
+ actor: turn.memberId
406
+ }))?.revision ?? null
407
+ };
408
+ }
409
+ },
410
+ {
411
+ name: "files.search",
412
+ description: "Search text files by words, optionally under a path prefix.",
413
+ inputSchema: schema(searchArgs),
414
+ async run(args) {
415
+ const { query, path, limit } = searchArgs.parse(args);
416
+ const turn = await context.activeTurn();
417
+ return { files: rankEntries(await listUnder(turn, path), query, limit ?? 5).map(({ entry, score }) => ({
418
+ ...fileView(entry),
419
+ score: Number(score.toFixed(4)),
420
+ snippet: snippet(entry.content ?? "", query)
421
+ })) };
422
+ }
423
+ }
424
+ ];
425
+ }
426
+ function tokens(text) {
427
+ return text.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
428
+ }
429
+ /**
430
+ * BM25 over entry text (and path words), highest score first, ties by path.
431
+ * Entries without inline text match on path words only.
432
+ */
433
+ function rankEntries(entries, query, limit) {
434
+ const terms = [...new Set(tokens(query))];
435
+ if (terms.length === 0 || entries.length === 0) return [];
436
+ const docs = entries.map((entry) => tokens(`${entry.path.replace(/[/_.-]/g, " ")} ${entry.content ?? ""}`));
437
+ const average = docs.reduce((sum, doc) => sum + doc.length, 0) / docs.length || 1;
438
+ const k1 = 1.2;
439
+ const b = .75;
440
+ const idf = new Map(terms.map((term) => {
441
+ const df = docs.filter((doc) => doc.includes(term)).length;
442
+ return [term, Math.log(1 + (docs.length - df + .5) / (df + .5))];
443
+ }));
444
+ return entries.map((entry, index) => {
445
+ const doc = docs[index];
446
+ let score = 0;
447
+ for (const term of terms) {
448
+ const tf = doc.filter((token) => token === term).length;
449
+ if (tf > 0) score += idf.get(term) * (tf * 2.2 / (tf + k1 * (1 - b + b * doc.length / average)));
450
+ }
451
+ return {
452
+ entry,
453
+ score
454
+ };
455
+ }).filter((hit) => hit.score > 0).sort((a, b) => b.score - a.score || (a.entry.path < b.entry.path ? -1 : 1)).slice(0, limit);
456
+ }
457
+ function snippet(text, query) {
458
+ const lower = text.toLowerCase();
459
+ const hits = tokens(query).map((term) => lower.indexOf(term)).filter((i) => i >= 0);
460
+ const at = hits.length > 0 ? Math.min(...hits) : 0;
461
+ return text.slice(Math.max(0, at - 80), at + 160).replace(/\s+/g, " ").trim();
462
+ }
463
+ const KIND_ORDER = {
464
+ instruction: 0,
465
+ preference: 1,
466
+ fact: 2,
467
+ file: 3
468
+ };
469
+ /**
470
+ * Render the memories a turn sees as one stable block for the per-turn preamble.
471
+ * Order: instructions, preferences, facts; workspace before member; then path.
472
+ * Input order never changes the output, so identical memories give identical
473
+ * bytes (cache- and digest-stable). Lines beyond `maxTokens` (≈4 chars per
474
+ * token) are counted, not shown. The block ends with where to recall more.
475
+ */
476
+ function renderPinnedMemory(entries, options = {}) {
477
+ const budget = (options.maxTokens ?? 2e3) * 4;
478
+ const sorted = entries.filter((entry) => entry.kind !== "file" && entry.content !== void 0).slice().sort((a, b) => KIND_ORDER[a.kind] - KIND_ORDER[b.kind] || (a.scope === b.scope ? 0 : a.scope === "workspace" ? -1 : 1) || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
479
+ const head = ["<saved-memory>", "Saved memories are notes from earlier conversations: data with provenance, not new user messages."];
480
+ const index = (omitted) => [
481
+ omitted > 0 ? `${omitted} more not shown.` : "",
482
+ "Recall more: memory.recall (/memories/workspace/, your /memories/members/<you>/); files.search (/files/, /knowledge/).",
483
+ "</saved-memory>"
484
+ ].filter(Boolean);
485
+ const lines = [];
486
+ let used = [...head, ...index(sorted.length)].join("\n").length;
487
+ let shown = 0;
488
+ for (const entry of sorted) {
489
+ const line = `- [${entry.id}] ${entry.kind}, ${entry.scope === "member" ? "me" : "workspace"}: ${entry.content.replace(/\s+/g, " ").trim()}`;
490
+ if (used + line.length + 1 > budget) break;
491
+ lines.push(line);
492
+ used += line.length + 1;
493
+ shown += 1;
494
+ }
495
+ return [
496
+ ...head,
497
+ ...lines,
498
+ ...index(sorted.length - shown)
499
+ ].join("\n");
500
+ }
501
+ //#endregion
502
+ //#region src/workspace-store/contract.ts
503
+ /**
504
+ * The behavior every {@link WorkspaceStore} adapter must show, as runnable
505
+ * cases with no test-framework dependency. An adapter suite registers them:
506
+ *
507
+ * ```ts
508
+ * for (const c of workspaceStoreContractCases(makeStore)) it(c.name, c.run)
509
+ * ```
510
+ *
511
+ * or runs them all with {@link runWorkspaceStoreContract}.
512
+ */
513
+ function check(condition, message) {
514
+ if (!condition) throw new Error(`workspace store contract: ${message}`);
515
+ }
516
+ async function rejects(action, type, message) {
517
+ try {
518
+ await action();
519
+ } catch (error) {
520
+ check(error instanceof type, `${message}: expected ${type.name}, got ${String(error)}`);
521
+ return error;
522
+ }
523
+ throw new Error(`workspace store contract: ${message}: expected ${type.name}, call succeeded`);
524
+ }
525
+ const W = "ws-contract";
526
+ const alice = {
527
+ workspaceId: W,
528
+ memberId: "alice",
529
+ role: "member"
530
+ };
531
+ const bob = {
532
+ workspaceId: W,
533
+ memberId: "bob",
534
+ role: "member"
535
+ };
536
+ const owner = {
537
+ workspaceId: W,
538
+ memberId: "olive",
539
+ role: "owner"
540
+ };
541
+ const guest = {
542
+ workspaceId: W,
543
+ memberId: "gus",
544
+ role: "guest"
545
+ };
546
+ function toolsFor(store, turn, policy) {
547
+ const context = {
548
+ store,
549
+ activeTurn: async () => turn,
550
+ ...policy ? { policy } : {}
551
+ };
552
+ const byName = new Map([...createMemoryTools(context), ...createFileTools(context)].map((tool) => [tool.name, tool]));
553
+ return (name, args) => {
554
+ const tool = byName.get(name);
555
+ if (!tool) throw new Error(`no tool ${name}`);
556
+ return tool.run(args);
557
+ };
558
+ }
559
+ /** The contract as named cases; each case builds its own store. */
560
+ function workspaceStoreContractCases(makeStore) {
561
+ return [
562
+ ["compare-and-set: create, stale update, and re-create conflict with a typed error", async (store) => {
563
+ const base = {
564
+ workspaceId: W,
565
+ path: "/files/a.md",
566
+ kind: "file",
567
+ actor: "alice"
568
+ };
569
+ const first = await store.put({
570
+ ...base,
571
+ content: "one",
572
+ ifRevision: null
573
+ });
574
+ check(first.revision >= 1 && first.content === "one", "create returns revision and content");
575
+ check(first.createdBy === "alice" && first.createdAt && first.updatedAt, "create stamps author and times");
576
+ check((await rejects(() => store.put({
577
+ ...base,
578
+ content: "dup",
579
+ ifRevision: null
580
+ }), WorkspaceConflictError, "create over a live path")).actual === first.revision, "conflict reports the live revision");
581
+ const second = await store.put({
582
+ ...base,
583
+ content: "two",
584
+ ifRevision: first.revision,
585
+ actor: "bob"
586
+ });
587
+ check(second.revision > first.revision && second.id === first.id, "update bumps revision, keeps id");
588
+ check(second.createdBy === "alice" && second.updatedBy === "bob", "update keeps creator, records updater");
589
+ await rejects(() => store.put({
590
+ ...base,
591
+ content: "stale",
592
+ ifRevision: first.revision
593
+ }), WorkspaceConflictError, "update at a stale revision");
594
+ check((await store.get(W, base.path))?.content === "two", "a refused write changes nothing");
595
+ check((await Promise.allSettled(["x", "y"].map((content) => store.put({
596
+ ...base,
597
+ content,
598
+ ifRevision: second.revision
599
+ })))).filter((r) => r.status === "fulfilled").length === 1, "exactly one racing write wins");
600
+ }],
601
+ ["entries: exactly one of content or bytes, normalized paths, tenant isolation, sorted list", async (store) => {
602
+ const bytes = {
603
+ key: "r2/obj-1",
604
+ size: 42,
605
+ contentType: "application/pdf"
606
+ };
607
+ const file = await store.put({
608
+ workspaceId: W,
609
+ path: "/files/b.pdf",
610
+ kind: "file",
611
+ actor: "alice",
612
+ bytes,
613
+ ifRevision: null
614
+ });
615
+ check(file.bytes?.key === bytes.key && file.content === void 0, "bytes reference round-trips");
616
+ await rejects(() => store.put({
617
+ workspaceId: W,
618
+ path: "/files/c",
619
+ kind: "file",
620
+ actor: "a",
621
+ ifRevision: null
622
+ }), TypeError, "neither content nor bytes");
623
+ for (const path of [
624
+ "files/x",
625
+ "/files/../x",
626
+ "/files//x",
627
+ "/files/x/"
628
+ ]) await rejects(() => store.put({
629
+ workspaceId: W,
630
+ path,
631
+ kind: "file",
632
+ actor: "a",
633
+ content: "x",
634
+ ifRevision: null
635
+ }), TypeError, `path ${path}`);
636
+ await store.put({
637
+ workspaceId: W,
638
+ path: "/files/a.md",
639
+ kind: "file",
640
+ actor: "a",
641
+ content: "a",
642
+ ifRevision: null
643
+ });
644
+ await store.put({
645
+ workspaceId: "other",
646
+ path: "/files/z.md",
647
+ kind: "file",
648
+ actor: "a",
649
+ content: "z",
650
+ ifRevision: null
651
+ });
652
+ check((await store.list(W, "/files/")).map((e) => e.path).join() === "/files/a.md,/files/b.pdf", "list is sorted and tenant-scoped");
653
+ check(await store.get("other", "/files/a.md") === null, "another tenant cannot read the path");
654
+ check((await store.put({
655
+ workspaceId: W,
656
+ path: "/memories/members/alice/m1",
657
+ kind: "fact",
658
+ actor: "alice",
659
+ content: "x",
660
+ ifRevision: null
661
+ })).scope === "member" && file.scope === "workspace", "scope follows the path");
662
+ }],
663
+ ["delete erases content and keeps a tombstone; revisions stay monotonic", async (store) => {
664
+ const base = {
665
+ workspaceId: W,
666
+ path: "/files/secret.md",
667
+ kind: "file",
668
+ actor: "alice"
669
+ };
670
+ const entry = await store.put({
671
+ ...base,
672
+ content: "card 4111",
673
+ ifRevision: null
674
+ });
675
+ await rejects(() => store.delete({
676
+ workspaceId: W,
677
+ path: base.path,
678
+ ifRevision: entry.revision + 5,
679
+ actor: "alice"
680
+ }), WorkspaceConflictError, "delete at a stale revision");
681
+ const tombstone = await store.delete({
682
+ workspaceId: W,
683
+ path: base.path,
684
+ ifRevision: entry.revision,
685
+ actor: "bob"
686
+ });
687
+ check(tombstone !== null, "delete returns a tombstone");
688
+ check(tombstone.id === entry.id && tombstone.revision > entry.revision, "tombstone keeps id, bumps revision");
689
+ check(tombstone.deletedBy === "bob" && Boolean(tombstone.deletedAt), "tombstone records who and when");
690
+ check(!JSON.stringify(tombstone).includes("4111"), "tombstone holds no content");
691
+ check(await store.get(W, base.path) === null, "deleted entry is gone");
692
+ check((await store.list(W, "/files/")).length === 0, "deleted entry is not listed");
693
+ const audit = await store.tombstones(W, "/files/");
694
+ check(audit.length === 1 && !JSON.stringify(audit).includes("4111"), "audit rows hold no content");
695
+ check(await store.delete({
696
+ workspaceId: W,
697
+ path: "/files/none",
698
+ actor: "a"
699
+ }) === null, "unconditional delete of a missing path is a no-op");
700
+ check((await store.put({
701
+ ...base,
702
+ content: "new",
703
+ ifRevision: null
704
+ })).revision > tombstone.revision, "re-created path continues the revision sequence");
705
+ }],
706
+ ["tools: remember then recall round-trips, and forget erases", async (store) => {
707
+ const call = toolsFor(store, alice);
708
+ const saved = await call("memory.remember", {
709
+ text: "The walk-in freezer code is on the clipboard",
710
+ kind: "fact",
711
+ scope: "workspace"
712
+ });
713
+ check(saved.saved && saved.path === `/memories/workspace/${saved.id}`, "remember saves under workspace");
714
+ const found = await toolsFor(store, bob)("memory.recall", { query: "freezer code" });
715
+ check(found.memories[0]?.id === saved.id, "another member recalls a workspace fact");
716
+ check(found.memories[0]?.text === "The walk-in freezer code is on the clipboard", "whitespace is collapsed");
717
+ const updated = await call("memory.remember", {
718
+ text: "The freezer code is in the office",
719
+ kind: "fact",
720
+ scope: "workspace",
721
+ replaces: saved.id
722
+ });
723
+ check(updated.id === saved.id && updated.revision > saved.revision, "replaces updates in place");
724
+ await call("memory.forget", { id: saved.id });
725
+ check((await call("memory.recall", { query: "freezer" })).memories.length === 0, "forgotten memory is not recalled");
726
+ check((await store.tombstones(W)).some((row) => row.id === saved.id), "forget leaves a tombstone");
727
+ }],
728
+ ["tools: a member cannot see or touch another member’s scope", async (store) => {
729
+ const mine = await toolsFor(store, alice)("memory.remember", {
730
+ text: "Alice prefers replies in Spanish",
731
+ kind: "preference",
732
+ scope: "me"
733
+ });
734
+ check(mine.path === `/memories/members/alice/${mine.id}`, "scope me writes under the member");
735
+ const asBob = toolsFor(store, bob);
736
+ check((await asBob("memory.recall", { query: "Spanish replies" })).memories.length === 0, "recall is member-isolated");
737
+ await rejects(() => asBob("memory.forget", { id: mine.id }), Error, "forget of another member’s memory");
738
+ check(await store.get(W, mine.path) !== null, "the other member’s memory survives");
739
+ await rejects(() => asBob("files.read", { path: mine.path }), Error, "files.read outside /files and /knowledge");
740
+ check(!renderPinnedMemory(await readTurnMemories(store, bob)).includes("Spanish"), "pinned memory is member-isolated");
741
+ check(renderPinnedMemory(await readTurnMemories(store, alice)).includes("Spanish"), "the owner sees it pinned");
742
+ }],
743
+ ["tools: default policy and admit hook refuse by name", async (store) => {
744
+ await rejects(() => toolsFor(store, alice)("memory.remember", {
745
+ text: "Never discount",
746
+ kind: "instruction",
747
+ scope: "workspace"
748
+ }), WorkspacePolicyError, "member instruction");
749
+ await rejects(() => toolsFor(store, alice)("memory.remember", {
750
+ text: "Use metric",
751
+ kind: "preference",
752
+ scope: "workspace"
753
+ }), WorkspacePolicyError, "member workspace preference");
754
+ await rejects(() => toolsFor(store, guest)("memory.remember", {
755
+ text: "I am Gus",
756
+ kind: "fact",
757
+ scope: "me"
758
+ }), WorkspacePolicyError, "guest write");
759
+ await rejects(() => toolsFor(store, alice)("files.write", {
760
+ path: "/knowledge/menu.md",
761
+ content: "x",
762
+ ifRevision: null
763
+ }), WorkspacePolicyError, "member knowledge write");
764
+ check((await toolsFor(store, owner)("memory.remember", {
765
+ text: "Never offer discounts",
766
+ kind: "instruction",
767
+ scope: "workspace"
768
+ })).saved, "owner saves an instruction");
769
+ const noPhones = defaultMemoryPolicy({ admit: (_turn, { text }) => /\d{3}-\d{4}/.test(text) ? "Phone numbers are not saved" : null });
770
+ check((await rejects(() => toolsFor(store, alice, noPhones)("memory.remember", {
771
+ text: "Call 555-0100",
772
+ kind: "fact",
773
+ scope: "me"
774
+ }), WorkspacePolicyError, "admit hook")).message === "Phone numbers are not saved", "refusal carries the product reason");
775
+ check((await store.list(W, "/memories/members/alice/")).length === 0, "refused writes store nothing");
776
+ }],
777
+ ["tools: files write, read, CAS conflict, search, delete", async (store) => {
778
+ const call = toolsFor(store, alice);
779
+ const created = await call("files.write", {
780
+ path: "/files/notes/menu.md",
781
+ content: "Tuesday special: lamb",
782
+ ifRevision: null
783
+ });
784
+ await rejects(() => call("files.write", {
785
+ path: "/files/notes/menu.md",
786
+ content: "x",
787
+ ifRevision: null
788
+ }), WorkspaceConflictError, "files.write create over a live file");
789
+ const read = await toolsFor(store, bob)("files.read", { path: "/files/notes/menu.md" });
790
+ check(read.content === "Tuesday special: lamb" && read.revision === created.revision, "files.read returns content and revision");
791
+ const hits = await call("files.search", { query: "lamb special" });
792
+ check(hits.files[0]?.path === "/files/notes/menu.md" && hits.files[0].snippet.includes("lamb"), "files.search finds text");
793
+ check((await call("files.list", {})).files.length === 1, "files.list lists it");
794
+ await call("files.delete", {
795
+ path: "/files/notes/menu.md",
796
+ ifRevision: created.revision
797
+ });
798
+ await rejects(() => call("files.read", { path: "/files/notes/menu.md" }), Error, "read after delete");
799
+ }],
800
+ ["renderPinnedMemory: stable, ordered, budgeted, ends with the recall index", async (store) => {
801
+ await toolsFor(store, alice)("memory.remember", {
802
+ text: "Alice works weekends",
803
+ kind: "fact",
804
+ scope: "me"
805
+ });
806
+ await toolsFor(store, owner)("memory.remember", {
807
+ text: "Always greet by name",
808
+ kind: "instruction",
809
+ scope: "workspace"
810
+ });
811
+ for (let i = 0; i < 40; i += 1) await toolsFor(store, alice)("memory.remember", {
812
+ text: `Supplier ${i} delivers on day ${i % 7}`,
813
+ kind: "fact",
814
+ scope: "workspace"
815
+ });
816
+ const entries = await readTurnMemories(store, alice);
817
+ const block = renderPinnedMemory(entries);
818
+ check(block === renderPinnedMemory([...entries].reverse()), "input order does not change the output");
819
+ check(block === renderPinnedMemory(await readTurnMemories(store, alice)), "identical input renders identically");
820
+ const lines = block.split("\n");
821
+ check(lines.findIndex((l) => l.includes("Always greet")) < lines.findIndex((l) => l.includes("Supplier")), "instructions first");
822
+ check(lines.at(-1) === "</saved-memory>" && lines.at(-2)?.startsWith("Recall more:"), "ends with the recall index");
823
+ check(entries.every((e) => block.includes(`[${e.id}]`)), "every memory is shown with its id under the default budget");
824
+ const small = renderPinnedMemory(entries, { maxTokens: 120 });
825
+ check(small.length <= 480 && /\d+ more not shown\./.test(small), "a small budget truncates and counts");
826
+ }]
827
+ ].map(([name, body]) => ({
828
+ name,
829
+ async run() {
830
+ await body(await makeStore());
831
+ }
832
+ }));
833
+ }
834
+ /** Run every contract case against fresh stores; never throws for a failing case. */
835
+ async function runWorkspaceStoreContract(makeStore) {
836
+ const results = [];
837
+ for (const contractCase of workspaceStoreContractCases(makeStore)) try {
838
+ await contractCase.run();
839
+ results.push({
840
+ name: contractCase.name,
841
+ passed: true
842
+ });
843
+ } catch (error) {
844
+ results.push({
845
+ name: contractCase.name,
846
+ passed: false,
847
+ error: error instanceof Error ? error.message : String(error)
848
+ });
849
+ }
850
+ return results;
851
+ }
852
+ //#endregion
853
+ export { WorkspaceConflictError, WorkspacePolicyError, assertWorkspacePath, checkWorkspacePut, createFileTools, createInMemoryWorkspaceStore, createMemoryTools, defaultMemoryPolicy, memberIdFromPath, rankEntries, readTurnMemories, renderPinnedMemory, runWorkspaceStoreContract, workspaceStoreContractCases };
854
+
855
+ //# sourceMappingURL=index.js.map