@stratusagent/agents 0.11.4 → 0.11.6

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 (53) hide show
  1. package/dist/define.d.ts +22 -0
  2. package/dist/define.d.ts.map +1 -0
  3. package/dist/define.js +64 -0
  4. package/dist/define.js.map +1 -0
  5. package/dist/frontmatter.d.ts +65 -0
  6. package/dist/frontmatter.d.ts.map +1 -0
  7. package/dist/frontmatter.js +260 -0
  8. package/dist/frontmatter.js.map +1 -0
  9. package/dist/identity.d.ts +139 -0
  10. package/dist/identity.d.ts.map +1 -0
  11. package/dist/identity.js +259 -0
  12. package/dist/identity.js.map +1 -0
  13. package/dist/index.d.ts +11 -654
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +16 -1641
  16. package/dist/index.js.map +1 -1
  17. package/dist/router.d.ts +18 -0
  18. package/dist/router.d.ts.map +1 -0
  19. package/dist/router.js +44 -0
  20. package/dist/router.js.map +1 -0
  21. package/dist/schedules.d.ts +163 -0
  22. package/dist/schedules.d.ts.map +1 -0
  23. package/dist/schedules.js +215 -0
  24. package/dist/schedules.js.map +1 -0
  25. package/dist/skills.d.ts +135 -0
  26. package/dist/skills.d.ts.map +1 -0
  27. package/dist/skills.js +167 -0
  28. package/dist/skills.js.map +1 -0
  29. package/dist/soul.d.ts +44 -0
  30. package/dist/soul.d.ts.map +1 -0
  31. package/dist/soul.js +98 -0
  32. package/dist/soul.js.map +1 -0
  33. package/dist/tools/delegate.d.ts +93 -0
  34. package/dist/tools/delegate.d.ts.map +1 -0
  35. package/dist/tools/delegate.js +223 -0
  36. package/dist/tools/delegate.js.map +1 -0
  37. package/dist/tools/gateway-only.d.ts +19 -0
  38. package/dist/tools/gateway-only.d.ts.map +1 -0
  39. package/dist/tools/gateway-only.js +29 -0
  40. package/dist/tools/gateway-only.js.map +1 -0
  41. package/dist/tools/memory.d.ts +51 -0
  42. package/dist/tools/memory.d.ts.map +1 -0
  43. package/dist/tools/memory.js +306 -0
  44. package/dist/tools/memory.js.map +1 -0
  45. package/dist/tools/message.d.ts +26 -0
  46. package/dist/tools/message.d.ts.map +1 -0
  47. package/dist/tools/message.js +46 -0
  48. package/dist/tools/message.js.map +1 -0
  49. package/dist/tools/schedule.d.ts +20 -0
  50. package/dist/tools/schedule.d.ts.map +1 -0
  51. package/dist/tools/schedule.js +146 -0
  52. package/dist/tools/schedule.js.map +1 -0
  53. package/package.json +2 -2
@@ -0,0 +1,223 @@
1
+ import { SENDER_TRUST_METADATA_KEY, sessionTrustOf, sessionWriteTrust, } from '@stratusagent/core';
2
+ import { isDelegateAllowed, suggestedDelegatesEdit } from "../soul.js";
3
+ export const DELEGATE_TOOL_NAME = 'agent.delegate';
4
+ /**
5
+ * Metadata a delegated sub-session carries: which agent delegated, and the
6
+ * session at the root of the chain. Written by `agent.delegate` and read by
7
+ * the gateway's restart sweep, which is why the keys are named here rather
8
+ * than spelled twice — a sub-session's parent is a turn in some other
9
+ * process's memory, and after a restart nothing can consume its reply.
10
+ */
11
+ export const DELEGATED_BY_METADATA_KEY = 'delegatedBy';
12
+ export const ROOT_SESSION_ID_METADATA_KEY = 'rootSessionId';
13
+ export const DELEGATION_DEPTH_METADATA_KEY = 'delegationDepth';
14
+ /**
15
+ * The segment `agent.delegate` mints into every sub-session's id —
16
+ * `<parent>:delegate:<agent>:<depth>:<suffix>`. The second half of what
17
+ * identifies a sub-session, beside the metadata above: the gateway refuses
18
+ * the metadata keys at its public door, but rows written before it did
19
+ * cannot be told apart by their metadata alone, and a caller who wrote
20
+ * `delegatedBy` onto a session of its own would not also have minted an
21
+ * id in this shape.
22
+ */
23
+ export const DELEGATED_SESSION_ID_MARKER = ':delegate:';
24
+ /**
25
+ * Whether this session is a delegated sub-session — one whose reply is
26
+ * consumed by a parent turn's `agent.delegate` call and by nothing else.
27
+ * Both halves are required: the metadata `agent.delegate` writes, and the
28
+ * id shape it mints.
29
+ */
30
+ export const isDelegatedSession = (session) => typeof session.metadata?.[DELEGATED_BY_METADATA_KEY] === 'string'
31
+ && session.id.includes(DELEGATED_SESSION_ID_MARKER);
32
+ /**
33
+ * The id of the session whose `agent.delegate` call started this one, read
34
+ * off the id shape above — the text before the last marker, since a nested
35
+ * sub-session embeds its parent's id whole. Undefined for an id that was
36
+ * not minted that way.
37
+ */
38
+ export const delegatingSessionIdOf = (sessionId) => {
39
+ const at = sessionId.lastIndexOf(DELEGATED_SESSION_ID_MARKER);
40
+ return at > 0 ? sessionId.slice(0, at) : undefined;
41
+ };
42
+ /**
43
+ * The id of the `agent.delegate` call in `parent` that is still awaiting
44
+ * `child`, or undefined when none is.
45
+ *
46
+ * The one thing about a delegation nothing outside the runner can forge:
47
+ * a caller can name a session in the sub-session id shape and write the
48
+ * metadata keys, but only the runner writes assistant tool calls, and only
49
+ * a delegation in flight leaves one with no result behind it. Matched to
50
+ * the child and not merely counted, because a parent can be inside one
51
+ * delegation while an earlier sub-session of its — continued from outside
52
+ * under a version that kept the markers — is parked on something of its
53
+ * own. The call names its target and its prompt, and `agent.delegate`
54
+ * made the prompt the child's first user message. Read against the
55
+ * transcript order so a re-used call id cannot make a later call look
56
+ * answered.
57
+ */
58
+ export const outstandingDelegationFor = (parent, child) => {
59
+ const firstMessage = child.messages.find((message) => message.role === 'user')?.content;
60
+ if (firstMessage === undefined) {
61
+ return undefined;
62
+ }
63
+ const answered = new Map();
64
+ for (const message of parent.messages) {
65
+ if (message.role === 'tool' && message.toolResult) {
66
+ answered.set(message.toolResult.callId, (answered.get(message.toolResult.callId) ?? 0) + 1);
67
+ }
68
+ }
69
+ const seen = new Map();
70
+ for (const message of parent.messages) {
71
+ if (message.role !== 'assistant' || !message.toolCalls) {
72
+ continue;
73
+ }
74
+ for (const call of message.toolCalls) {
75
+ const occurrence = (seen.get(call.id) ?? 0) + 1;
76
+ seen.set(call.id, occurrence);
77
+ if (call.toolName !== DELEGATE_TOOL_NAME || occurrence <= (answered.get(call.id) ?? 0)) {
78
+ continue;
79
+ }
80
+ const target = typeof call.input.agent === 'string' ? call.input.agent : '';
81
+ const prompt = typeof call.input.prompt === 'string' ? call.input.prompt.trim() : '';
82
+ if ((target === child.agent.id || target === child.agent.name) && prompt === firstMessage) {
83
+ return call.id;
84
+ }
85
+ }
86
+ }
87
+ return undefined;
88
+ };
89
+ /**
90
+ * `metadata` with the delegation markers removed — what a sub-session
91
+ * carries once it has been addressed from outside. The delegation ended
92
+ * with the turn that awaited it; a conversation somebody continues on the
93
+ * same id afterwards is an ordinary one, and must not be closed as an
94
+ * orphan by a later restart.
95
+ */
96
+ export const withoutDelegation = (metadata) => {
97
+ const { [DELEGATED_BY_METADATA_KEY]: _delegatedBy, [ROOT_SESSION_ID_METADATA_KEY]: _root, [DELEGATION_DEPTH_METADATA_KEY]: _depth, ...rest } = metadata;
98
+ return rest;
99
+ };
100
+ const DEFAULT_MAX_DELEGATION_DEPTH = 3;
101
+ /**
102
+ * The orchestrator primitive: a tool that runs another agent in its own
103
+ * sub-session (with that agent's memory, tool allowlist, and credentials)
104
+ * and returns its final reply as the tool result.
105
+ */
106
+ export const createDelegateTool = ({ registry, runner, dispatch, maxDepth = DEFAULT_MAX_DELEGATION_DEPTH, }) => {
107
+ const runDelegated = dispatch ?? ((input) => runner.run(input));
108
+ // Sub-session ids must be unique even when one orchestrator delegates to
109
+ // the same target repeatedly, and must stay unique across tool rebuilds
110
+ // sharing one SessionStore — so a counter alone is not enough.
111
+ let delegationCount = 0;
112
+ const uniqueSuffix = () => `${delegationCount}-${Math.random().toString(36).slice(2, 10)}`;
113
+ return {
114
+ name: DELEGATE_TOOL_NAME,
115
+ description: 'Delegate a task to another agent by name and get their reply back.',
116
+ // Safe, and the call is arguable enough to record why. Delegation spends
117
+ // provider tokens and starts work as another agent, which reads like
118
+ // `gated` — but the money argument proves too much: the turn that decides
119
+ // to delegate was itself an unapproved provider call, so gating on spend
120
+ // gates the conversation. What delegation does NOT do is act outside
121
+ // Stratus. It stays in the fleet, the delegate's own tool calls face the
122
+ // policy again under the delegate's allowlist, and maxDepth bounds the
123
+ // chain.
124
+ //
125
+ // The practical half: `gated` here means a headless daemon refuses every
126
+ // delegation, and headless is what every installed service runs. That
127
+ // would remove the orchestrator pattern from the product until remote
128
+ // approval exists — a feature removal wearing a safety hat, with no way
129
+ // for an operator to say yes. Revisit when a human can actually be asked.
130
+ risk: 'safe',
131
+ parameters: {
132
+ type: 'object',
133
+ properties: {
134
+ agent: { type: 'string', description: 'Name or id of the agent to delegate to.' },
135
+ prompt: { type: 'string', description: 'The task or question for that agent.' },
136
+ },
137
+ required: ['agent', 'prompt'],
138
+ },
139
+ async execute(input, session, context) {
140
+ const targetRef = typeof input.agent === 'string' ? input.agent : '';
141
+ const prompt = typeof input.prompt === 'string' ? input.prompt.trim() : '';
142
+ if (!targetRef || !prompt) {
143
+ throw new Error('agent.delegate requires "agent" and "prompt" strings.');
144
+ }
145
+ const depth = typeof session.metadata?.[DELEGATION_DEPTH_METADATA_KEY] === 'number'
146
+ ? session.metadata[DELEGATION_DEPTH_METADATA_KEY]
147
+ : 0;
148
+ if (depth >= maxDepth) {
149
+ throw new Error(`Delegation depth limit reached (${maxDepth}).`);
150
+ }
151
+ const target = registry.get(targetRef) ?? registry.getByName(targetRef);
152
+ if (!target) {
153
+ throw new Error(`Agent not found: ${targetRef}`);
154
+ }
155
+ if (target.id === session.agent.id) {
156
+ throw new Error('An agent cannot delegate to itself.');
157
+ }
158
+ // Judged on the session's frozen copy of the agent, as the tool
159
+ // allowlist is: the target is model-chosen input, and without this
160
+ // list any agent holding this tool could run a turn as any other —
161
+ // under the other's tools, credentials, and memory — which is the
162
+ // lateral move a prompt-injected agent would take.
163
+ if (!isDelegateAllowed(session.agent, target.id)) {
164
+ throw new Error(`Agent ${session.agent.id} may not delegate to ${target.id}. `
165
+ + `Add ${suggestedDelegatesEdit(target.id)} to its soul, or delegates: ['*'] for any agent on the roster.`);
166
+ }
167
+ delegationCount += 1;
168
+ let result;
169
+ try {
170
+ result = await runDelegated({
171
+ sessionId: `${session.id}${DELEGATED_SESSION_ID_MARKER}${target.id}:${depth + 1}:${uniqueSuffix()}`,
172
+ agent: target,
173
+ userMessage: prompt,
174
+ metadata: {
175
+ [DELEGATION_DEPTH_METADATA_KEY]: depth + 1,
176
+ [DELEGATED_BY_METADATA_KEY]: session.agent.id,
177
+ [ROOT_SESSION_ID_METADATA_KEY]: typeof session.metadata?.[ROOT_SESSION_ID_METADATA_KEY] === 'string'
178
+ ? session.metadata[ROOT_SESSION_ID_METADATA_KEY]
179
+ : session.id,
180
+ // Outbound: the prompt is the parent's text, and the parent is its
181
+ // "sender" — a tainted parent must not hand attacker text to a
182
+ // teammate whose session has seen nothing. At most `agent`: the
183
+ // prompt is something an agent wrote, whoever it was talking to.
184
+ [SENDER_TRUST_METADATA_KEY]: sessionWriteTrust(session),
185
+ },
186
+ // A cancelled parent turn cancels the delegated run with it —
187
+ // otherwise the parent cannot settle until the target gives up.
188
+ ...(context?.signal ? { signal: context.signal } : {}),
189
+ });
190
+ }
191
+ catch (error) {
192
+ // A failed run has no session to read a label from here, and its
193
+ // error text is the target's — a provider quoting the request it
194
+ // refused, which is the target's prompt and injected memory, whatever
195
+ // label those carried. The parent sees that text as a tool error, so
196
+ // the call is marked at the label for provenance nobody can vouch
197
+ // for before it is rethrown. The parent's own cancellation is the
198
+ // exception: that error is this process's sentence, not the target's.
199
+ if (!context?.signal?.aborted) {
200
+ context?.markTrust?.('unknown');
201
+ }
202
+ throw error;
203
+ }
204
+ // Inbound: the reply carries the target session's label, whatever it
205
+ // is — not only `external`. A target whose own injected memory was
206
+ // `unknown` replies from content the parent never saw and cannot
207
+ // assess; the parent takes the lower of its own label and this one,
208
+ // by the same ordering as everything else.
209
+ const targetTrust = sessionTrustOf(result);
210
+ context?.markTrust?.(targetTrust);
211
+ const reply = [...result.messages]
212
+ .reverse()
213
+ .find((message) => message.role === 'assistant' && message.content.length > 0);
214
+ return {
215
+ agent: target.name,
216
+ reply: reply?.content ?? '(no reply)',
217
+ sessionId: result.id,
218
+ trust: targetTrust,
219
+ };
220
+ },
221
+ };
222
+ };
223
+ //# sourceMappingURL=delegate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delegate.js","sourceRoot":"","sources":["../../src/tools/delegate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The tools only a running gateway registers. Each needs the dispatcher,
3
+ * the store, or the channels, and nothing else in the tree has them — so a
4
+ * soul naming one is correct while a host without them cannot call it,
5
+ * which is a different thing from a name that does not exist. `stratus
6
+ * run` reads this to say which it is rather than reporting a daemon soul's
7
+ * `schedule.*` as a typo.
8
+ *
9
+ * It lives here, with the names, rather than in `@stratusagent/gateway`
10
+ * where the registrations are: the CLI imports the gateway only through
11
+ * `await import`, because `bin.ts` has to load the CLI to print the Node
12
+ * version message and the gateway pulls in `node:sqlite` at module scope.
13
+ * A static import for a list of strings would have made the floor guard
14
+ * fail with the exact builtin-module error it exists to replace. The
15
+ * gateway's own test pins this set against what it actually registers,
16
+ * which holds the two together better than proximity would.
17
+ */
18
+ export declare const GATEWAY_ONLY_TOOL_NAMES: readonly string[];
19
+ //# sourceMappingURL=gateway-only.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateway-only.d.ts","sourceRoot":"","sources":["../../src/tools/gateway-only.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,uBAAuB,EAAE,SAAS,MAAM,EAOpD,CAAC"}
@@ -0,0 +1,29 @@
1
+ import { DELEGATE_TOOL_NAME } from "./delegate.js";
2
+ import { MESSAGE_SEND_TOOL_NAME } from "./message.js";
3
+ import { SCHEDULE_EVERY_TOOL_NAME, SCHEDULE_AT_TOOL_NAME, SCHEDULE_LIST_TOOL_NAME, SCHEDULE_CANCEL_TOOL_NAME, } from "./schedule.js";
4
+ /**
5
+ * The tools only a running gateway registers. Each needs the dispatcher,
6
+ * the store, or the channels, and nothing else in the tree has them — so a
7
+ * soul naming one is correct while a host without them cannot call it,
8
+ * which is a different thing from a name that does not exist. `stratus
9
+ * run` reads this to say which it is rather than reporting a daemon soul's
10
+ * `schedule.*` as a typo.
11
+ *
12
+ * It lives here, with the names, rather than in `@stratusagent/gateway`
13
+ * where the registrations are: the CLI imports the gateway only through
14
+ * `await import`, because `bin.ts` has to load the CLI to print the Node
15
+ * version message and the gateway pulls in `node:sqlite` at module scope.
16
+ * A static import for a list of strings would have made the floor guard
17
+ * fail with the exact builtin-module error it exists to replace. The
18
+ * gateway's own test pins this set against what it actually registers,
19
+ * which holds the two together better than proximity would.
20
+ */
21
+ export const GATEWAY_ONLY_TOOL_NAMES = [
22
+ SCHEDULE_EVERY_TOOL_NAME,
23
+ SCHEDULE_AT_TOOL_NAME,
24
+ SCHEDULE_LIST_TOOL_NAME,
25
+ SCHEDULE_CANCEL_TOOL_NAME,
26
+ MESSAGE_SEND_TOOL_NAME,
27
+ DELEGATE_TOOL_NAME,
28
+ ];
29
+ //# sourceMappingURL=gateway-only.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateway-only.js","sourceRoot":"","sources":["../../src/tools/gateway-only.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EACL,wBAAwB,EACxB,qBAAqB,EACrB,uBAAuB,EACvB,yBAAyB,GAC1B,MAAM,eAAe,CAAC;AAEvB;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAsB;IACxD,wBAAwB;IACxB,qBAAqB;IACrB,uBAAuB;IACvB,yBAAyB;IACzB,sBAAsB;IACtB,kBAAkB;CACnB,CAAC"}
@@ -0,0 +1,51 @@
1
+ import { type AgentMemoryStore, type Tool } from '@stratusagent/core';
2
+ /** Test seam shared by every memory tool that has to decide what is true *now*. */
3
+ export interface MemoryToolOptions {
4
+ now?: () => Date;
5
+ }
6
+ export declare const MEMORY_TOOL_NAME = "memory.remember";
7
+ /**
8
+ * A tool that lets an agent write to its own long-term memory. Entries are
9
+ * keyed by the session's agent id, so what one agent learns stays exclusively
10
+ * that agent's knowledge — and follows it to every channel and thread.
11
+ *
12
+ * Everything past `fact` is optional, and the reason they are the model's to
13
+ * write rather than extracted afterwards is that the party writing the fact
14
+ * is the one that knows what it is about, when it stops being true, and
15
+ * which older belief it replaces. Extraction would be a knowledge graph
16
+ * arriving through a side door.
17
+ */
18
+ export declare const createRememberTool: (store: AgentMemoryStore) => Tool;
19
+ /** The kernel owns the name — the prompt's topic index points the agent at it. */
20
+ export declare const RECALL_TOOL_NAME = "memory.recall";
21
+ /**
22
+ * Searches the agent's own long-term memory. `safe` by the same rule as
23
+ * `memory.remember`: reading what this agent already knows. The query is
24
+ * literal text — the store does whatever escaping its backend needs, so no
25
+ * query a model writes is ever a syntax error; matching nothing is a normal
26
+ * result, because an agent that has learned nothing yet is the ordinary
27
+ * starting state, not a broken tool.
28
+ */
29
+ export declare const createRecallTool: (store: AgentMemoryStore, options?: MemoryToolOptions) => Tool;
30
+ export declare const FORGET_TOOL_NAME = "memory.forget";
31
+ /**
32
+ * Retires one of the agent's own memories, by id. Tombstoned rather than
33
+ * deleted — which is what makes `safe` defensible for it: the entry stops
34
+ * being live but does not stop existing, so an operator can still see what
35
+ * an agent chose to drop.
36
+ */
37
+ export declare const createForgetTool: (store: AgentMemoryStore) => Tool;
38
+ export declare const PIN_TOOL_NAME = "memory.pin";
39
+ /**
40
+ * Pins one of the agent's own facts into the core its prompt always
41
+ * carries. `safe` by the rule that already covers `remember` and `forget`:
42
+ * the agent's own notes, appended rather than destroyed, keyed to the
43
+ * agent — a pin is a record naming an entry, never a field written onto
44
+ * one, because a toggle on an append-only line is a rewrite.
45
+ *
46
+ * The cap refuses rather than evicts, and the refusal is a normal result
47
+ * rather than a thrown error: nothing went wrong, the budget is full, and
48
+ * the agent's next move is to unpin something.
49
+ */
50
+ export declare const createPinTool: (store: AgentMemoryStore) => Tool;
51
+ //# sourceMappingURL=memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/tools/memory.ts"],"names":[],"mappings":"AAAA,OAAO,EAUL,KAAK,gBAAgB,EAKrB,KAAK,IAAI,EACV,MAAM,oBAAoB,CAAC;AAE5B,mFAAmF;AACnF,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAiFD,eAAO,MAAM,gBAAgB,oBAAoB,CAAC;AAElD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,gBAAgB,KAAG,IA4E3D,CAAC;AAEH,kFAAkF;AAClF,eAAO,MAAM,gBAAgB,kBAA0B,CAAC;AAExD;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,gBAAgB,EAAE,UAAS,iBAAsB,KAAG,IAoD3F,CAAC;AAEF,eAAO,MAAM,gBAAgB,kBAAkB,CAAC;AAEhD;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,gBAAgB,KAAG,IAsBzD,CAAC;AAEH,eAAO,MAAM,aAAa,eAAe,CAAC;AAE1C;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,gBAAgB,KAAG,IAsCtD,CAAC"}
@@ -0,0 +1,306 @@
1
+ import { isMemoryEntryKind, isTaintedTrust, leastTrusted, MEMORY_ENTRY_KINDS, MEMORY_RECALL_TOOL_NAME, memoryEntryTrust, memoryValidityAt, sessionTaintedBy, sessionWriteTrust, } from '@stratusagent/core';
2
+ const clockOf = (options) => options.now ?? (() => new Date());
3
+ /** A trimmed string, or undefined — the shape every optional string parameter here reads as. */
4
+ const optionalString = (value) => {
5
+ if (typeof value !== 'string') {
6
+ return undefined;
7
+ }
8
+ const trimmed = value.trim();
9
+ return trimmed.length > 0 ? trimmed : undefined;
10
+ };
11
+ /**
12
+ * ISO-8601, and checked as ISO-8601 rather than as whatever `Date.parse`
13
+ * will take. `Date.parse` accepts `04/01/2026` and `1` and gives each an
14
+ * interpretation — one of them locale-shaped and one of them a year — so
15
+ * parsing alone would quietly store a different window than the model
16
+ * meant, and the fact would enter or leave the prompt on the wrong day.
17
+ * A bound that silently failed to parse is the same hazard running the
18
+ * other way: the fact reads as unbounded and outlives the thing it was
19
+ * about. So the shape is asserted first, then the value.
20
+ */
21
+ // A date alone is UTC midnight by specification, so it is unambiguous
22
+ // wherever the daemon runs. A *time* without an offset is not: it reads as
23
+ // the host's local zone, so the same requested bound would become a
24
+ // different instant in Los Angeles than in UTC, and a fact would activate
25
+ // or expire at a deployment-dependent hour. So the offset is required
26
+ // whenever a time is given.
27
+ const ISO_8601_INSTANT = /^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2}))?$/;
28
+ /**
29
+ * Whether the `YYYY-MM-DD` a bound opens with is a day that exists.
30
+ *
31
+ * `Date.parse` normalizes rather than refusing: `2026-02-30` parses fine and
32
+ * becomes March 2nd, and `2026-02-29` becomes March 1st in a non-leap year.
33
+ * The regex cannot see it either — both are ISO-*shaped*. A validity window
34
+ * is the field where that matters most and shows least: the fact starts or
35
+ * stops being true days from where the writer asked, and the record says
36
+ * only what it was normalized to.
37
+ *
38
+ * Checked on the calendar components as written, never by comparing the
39
+ * normalized instant back to the string — that was the first attempt at
40
+ * this and it rejected `2026-04-01T23:00:00-05:00`, whose UTC date is
41
+ * legitimately the 2nd. An offset is allowed to move the day; February 30th
42
+ * is not a day.
43
+ *
44
+ * `setUTCFullYear` rather than `Date.UTC`, which maps a two-digit year into
45
+ * the 1900s — the regex admits `0026-02-28`, and that is a real date.
46
+ */
47
+ const isRealCalendarDate = (raw) => {
48
+ const [year, month, day] = raw.slice(0, 10).split('-').map(Number);
49
+ const probe = new Date(0);
50
+ probe.setUTCFullYear(year, month - 1, day);
51
+ return probe.getUTCFullYear() === year && probe.getUTCMonth() === month - 1 && probe.getUTCDate() === day;
52
+ };
53
+ const validityBound = (value, field) => {
54
+ const raw = optionalString(value);
55
+ if (raw === undefined) {
56
+ return undefined;
57
+ }
58
+ const parsed = Date.parse(raw);
59
+ if (!ISO_8601_INSTANT.test(raw) || Number.isNaN(parsed) || !isRealCalendarDate(raw)) {
60
+ throw new Error(`${field} must be an ISO-8601 instant such as 2026-04-01 or 2026-04-01T00:00:00Z — a time needs Z or an offset, `
61
+ + `or it means something different on every machine. ${raw} is not one, and nothing was stored.`);
62
+ }
63
+ return new Date(parsed).toISOString();
64
+ };
65
+ const aboutKeys = (value) => {
66
+ if (value === undefined) {
67
+ return undefined;
68
+ }
69
+ if (!Array.isArray(value) || value.some((key) => typeof key !== 'string')) {
70
+ throw new Error('"about" must be an array of entity names, as plain strings. Nothing was stored.');
71
+ }
72
+ const keys = value.map((key) => key.trim()).filter((key) => key.length > 0);
73
+ return keys.length > 0 ? keys : undefined;
74
+ };
75
+ export const MEMORY_TOOL_NAME = 'memory.remember';
76
+ /**
77
+ * A tool that lets an agent write to its own long-term memory. Entries are
78
+ * keyed by the session's agent id, so what one agent learns stays exclusively
79
+ * that agent's knowledge — and follows it to every channel and thread.
80
+ *
81
+ * Everything past `fact` is optional, and the reason they are the model's to
82
+ * write rather than extracted afterwards is that the party writing the fact
83
+ * is the one that knows what it is about, when it stops being true, and
84
+ * which older belief it replaces. Extraction would be a knowledge graph
85
+ * arriving through a side door.
86
+ */
87
+ export const createRememberTool = (store) => ({
88
+ name: MEMORY_TOOL_NAME,
89
+ description: 'Save a fact to your long-term memory so you can recall it in future conversations on any channel.',
90
+ // The agent's own memory, keyed to the agent and read by nobody else.
91
+ risk: 'safe',
92
+ parameters: {
93
+ type: 'object',
94
+ properties: {
95
+ fact: { type: 'string', description: 'The fact to remember, phrased so it makes sense later without context.' },
96
+ kind: {
97
+ type: 'string',
98
+ enum: [...MEMORY_ENTRY_KINDS],
99
+ description: 'What sort of fact this is: semantic (about the world), episodic (something that happened), procedural (how something is done), or preference.',
100
+ },
101
+ about: {
102
+ type: 'array',
103
+ items: { type: 'string' },
104
+ description: 'The people, systems, or projects this fact concerns. These are how you will find it later, so include the names you would search for — aliases included.',
105
+ },
106
+ validFrom: { type: 'string', description: 'ISO-8601 instant this becomes true, if it is not true yet. Optional.' },
107
+ validUntil: { type: 'string', description: 'ISO-8601 instant this stops being true, if you know one. Optional.' },
108
+ supersedes: {
109
+ type: 'string',
110
+ description: 'The id of a fact of yours this one replaces (from memory.recall). The old fact stops informing you once this one is in force, and stays visible to your operator.',
111
+ },
112
+ },
113
+ required: ['fact'],
114
+ },
115
+ async execute(input, session) {
116
+ const fact = typeof input.fact === 'string' ? input.fact.trim() : '';
117
+ if (!fact) {
118
+ throw new Error('memory.remember requires a non-empty "fact" string.');
119
+ }
120
+ if (input.kind !== undefined && !isMemoryEntryKind(input.kind)) {
121
+ throw new Error(`"kind" must be one of ${MEMORY_ENTRY_KINDS.join(', ')}. Nothing was stored.`);
122
+ }
123
+ // Per session, not per fact: the runner knows this session has seen
124
+ // untrusted content and cannot know which words of the fact came from
125
+ // it, so the write carries the least trusted label the session holds.
126
+ // Coarse and over-marking — the safe direction.
127
+ const taintedBy = sessionTaintedBy(session);
128
+ const origin = { sessionId: session.id, ...(taintedBy !== undefined ? { taintedBy } : {}) };
129
+ const about = aboutKeys(input.about);
130
+ const validFrom = validityBound(input.validFrom, 'validFrom');
131
+ const validUntil = validityBound(input.validUntil, 'validUntil');
132
+ // A window that closes before it opens describes nothing: the entry is
133
+ // not-yet-valid before `validFrom` and expired after it, so it could
134
+ // never be true and would never reach a prompt — stored, findable, and
135
+ // silently inert. The record tolerates whatever a hand edit or an
136
+ // import puts there; what a model writes is checked here.
137
+ if (validFrom !== undefined && validUntil !== undefined && Date.parse(validFrom) >= Date.parse(validUntil)) {
138
+ throw new Error(`validUntil (${validUntil}) must be after validFrom (${validFrom}); a fact whose window closes before it opens is never true. Nothing was stored.`);
139
+ }
140
+ const supersedes = optionalString(input.supersedes);
141
+ const options = {
142
+ metadata: { sessionId: session.id },
143
+ provenance: { trust: sessionWriteTrust(session), origin },
144
+ ...(input.kind !== undefined ? { kind: input.kind } : {}),
145
+ ...(about !== undefined ? { about } : {}),
146
+ ...(validFrom !== undefined ? { validFrom } : {}),
147
+ ...(validUntil !== undefined ? { validUntil } : {}),
148
+ ...(supersedes !== undefined ? { supersedes } : {}),
149
+ };
150
+ // The store resolves `supersedes` against this agent's own live entries
151
+ // and throws before appending anything, so naming a stranger's id is a
152
+ // failed call rather than a retirement.
153
+ const entry = await store.append(session.agent.id, fact, options);
154
+ return {
155
+ remembered: true,
156
+ id: entry.id,
157
+ trust: memoryEntryTrust(entry),
158
+ ...(supersedes !== undefined ? { supersedes } : {}),
159
+ };
160
+ },
161
+ });
162
+ /** The kernel owns the name — the prompt's topic index points the agent at it. */
163
+ export const RECALL_TOOL_NAME = MEMORY_RECALL_TOOL_NAME;
164
+ /**
165
+ * Searches the agent's own long-term memory. `safe` by the same rule as
166
+ * `memory.remember`: reading what this agent already knows. The query is
167
+ * literal text — the store does whatever escaping its backend needs, so no
168
+ * query a model writes is ever a syntax error; matching nothing is a normal
169
+ * result, because an agent that has learned nothing yet is the ordinary
170
+ * starting state, not a broken tool.
171
+ */
172
+ export const createRecallTool = (store, options = {}) => {
173
+ const now = clockOf(options);
174
+ return {
175
+ name: RECALL_TOOL_NAME,
176
+ description: 'Search your long-term memory for facts you have remembered. Your prompt carries only the pinned facts and a short recent tail — use this to find everything else, including facts that have expired.',
177
+ risk: 'safe',
178
+ parameters: {
179
+ type: 'object',
180
+ properties: {
181
+ query: { type: 'string', description: 'What to look for, as plain words. Every word must appear in a fact — or in what the fact is about — for it to match.' },
182
+ limit: { type: 'number', description: 'Maximum facts to return. Optional; the store bounds it either way.' },
183
+ },
184
+ required: ['query'],
185
+ },
186
+ async execute(input, session, context) {
187
+ if (typeof input.query !== 'string') {
188
+ throw new Error('memory.recall requires a "query" string.');
189
+ }
190
+ const limit = typeof input.limit === 'number' ? input.limit : undefined;
191
+ const result = await store.search(session.agent.id, input.query, { ...(limit !== undefined ? { limit } : {}) });
192
+ // What enters the context taints the session, recalled as much as
193
+ // injected: an `external` entry surfacing here is a stranger's text
194
+ // in the prompt, and marking the call is what stops the store from
195
+ // laundering its own contents through a fresh session.
196
+ const lowest = leastTrusted(...result.entries.map(memoryEntryTrust));
197
+ if (isTaintedTrust(lowest)) {
198
+ context?.markTrust?.(lowest);
199
+ }
200
+ const at = now();
201
+ return {
202
+ results: result.entries.map((entry) => ({
203
+ id: entry.id,
204
+ content: entry.content,
205
+ createdAt: entry.createdAt,
206
+ trust: memoryEntryTrust(entry),
207
+ // Search keeps an out-of-window entry findable, which is right —
208
+ // returning it *unmarked* is not: an expired fact that reads
209
+ // exactly like a current one is the confusion `validFrom` and
210
+ // `validUntil` exist to prevent, arriving through the other door.
211
+ validity: memoryValidityAt(entry, at),
212
+ ...(entry.about ? { about: entry.about } : {}),
213
+ ...(entry.kind ? { kind: entry.kind } : {}),
214
+ ...(entry.validFrom ? { validFrom: entry.validFrom } : {}),
215
+ ...(entry.validUntil ? { validUntil: entry.validUntil } : {}),
216
+ })),
217
+ truncated: result.truncated,
218
+ // The ordering the store actually applied, which is not always the
219
+ // one asked for — see `MemoryRankingStrategy`.
220
+ ...(result.strategy !== undefined ? { strategy: result.strategy } : {}),
221
+ };
222
+ },
223
+ };
224
+ };
225
+ export const FORGET_TOOL_NAME = 'memory.forget';
226
+ /**
227
+ * Retires one of the agent's own memories, by id. Tombstoned rather than
228
+ * deleted — which is what makes `safe` defensible for it: the entry stops
229
+ * being live but does not stop existing, so an operator can still see what
230
+ * an agent chose to drop.
231
+ */
232
+ export const createForgetTool = (store) => ({
233
+ name: FORGET_TOOL_NAME,
234
+ description: 'Forget one of your remembered facts by its id (from memory.recall or memory.remember). It stops informing future conversations.',
235
+ risk: 'safe',
236
+ parameters: {
237
+ type: 'object',
238
+ properties: {
239
+ id: { type: 'string', description: 'The id of the memory entry to forget.' },
240
+ },
241
+ required: ['id'],
242
+ },
243
+ async execute(input, session) {
244
+ const id = typeof input.id === 'string' ? input.id.trim() : '';
245
+ if (!id) {
246
+ throw new Error('memory.forget requires a non-empty "id" string.');
247
+ }
248
+ const forgotten = await store.forget(session.agent.id, id);
249
+ if (!forgotten) {
250
+ throw new Error(`No live memory entry with id ${id} belongs to this agent — nothing was forgotten.`);
251
+ }
252
+ return { forgotten: true, id };
253
+ },
254
+ });
255
+ export const PIN_TOOL_NAME = 'memory.pin';
256
+ /**
257
+ * Pins one of the agent's own facts into the core its prompt always
258
+ * carries. `safe` by the rule that already covers `remember` and `forget`:
259
+ * the agent's own notes, appended rather than destroyed, keyed to the
260
+ * agent — a pin is a record naming an entry, never a field written onto
261
+ * one, because a toggle on an append-only line is a rewrite.
262
+ *
263
+ * The cap refuses rather than evicts, and the refusal is a normal result
264
+ * rather than a thrown error: nothing went wrong, the budget is full, and
265
+ * the agent's next move is to unpin something.
266
+ */
267
+ export const createPinTool = (store) => ({
268
+ name: PIN_TOOL_NAME,
269
+ description: 'Keep one of your remembered facts in front of you every turn. The pinned set is small and capped — pin only what you always need.',
270
+ risk: 'safe',
271
+ parameters: {
272
+ type: 'object',
273
+ properties: {
274
+ id: { type: 'string', description: 'The id of the memory entry to pin.' },
275
+ unpin: { type: 'boolean', description: 'Pass true to remove an existing pin instead of adding one.' },
276
+ },
277
+ required: ['id'],
278
+ },
279
+ async execute(input, session) {
280
+ const id = typeof input.id === 'string' ? input.id.trim() : '';
281
+ if (!id) {
282
+ throw new Error('memory.pin requires a non-empty "id" string.');
283
+ }
284
+ if (input.unpin === true) {
285
+ if (!store.unpin) {
286
+ throw new Error('This memory store does not support pinning, so there was nothing to unpin.');
287
+ }
288
+ const unpinned = await store.unpin(session.agent.id, id);
289
+ if (!unpinned) {
290
+ throw new Error(`No pin of yours names id ${id} — nothing was unpinned.`);
291
+ }
292
+ return { pinned: false, id };
293
+ }
294
+ if (!store.pin) {
295
+ throw new Error('This memory store does not support pinning.');
296
+ }
297
+ const outcome = await store.pin(session.agent.id, id);
298
+ return {
299
+ pinned: outcome.pinned,
300
+ id,
301
+ bytes: outcome.bytes,
302
+ ...(outcome.reason !== undefined ? { reason: outcome.reason } : {}),
303
+ };
304
+ },
305
+ });
306
+ //# sourceMappingURL=memory.js.map