@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.
- package/dist/define.d.ts +22 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/define.js +64 -0
- package/dist/define.js.map +1 -0
- package/dist/frontmatter.d.ts +65 -0
- package/dist/frontmatter.d.ts.map +1 -0
- package/dist/frontmatter.js +260 -0
- package/dist/frontmatter.js.map +1 -0
- package/dist/identity.d.ts +139 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +259 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +11 -654
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -1641
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +18 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +44 -0
- package/dist/router.js.map +1 -0
- package/dist/schedules.d.ts +163 -0
- package/dist/schedules.d.ts.map +1 -0
- package/dist/schedules.js +215 -0
- package/dist/schedules.js.map +1 -0
- package/dist/skills.d.ts +135 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +167 -0
- package/dist/skills.js.map +1 -0
- package/dist/soul.d.ts +44 -0
- package/dist/soul.d.ts.map +1 -0
- package/dist/soul.js +98 -0
- package/dist/soul.js.map +1 -0
- package/dist/tools/delegate.d.ts +93 -0
- package/dist/tools/delegate.d.ts.map +1 -0
- package/dist/tools/delegate.js +223 -0
- package/dist/tools/delegate.js.map +1 -0
- package/dist/tools/gateway-only.d.ts +19 -0
- package/dist/tools/gateway-only.d.ts.map +1 -0
- package/dist/tools/gateway-only.js +29 -0
- package/dist/tools/gateway-only.js.map +1 -0
- package/dist/tools/memory.d.ts +51 -0
- package/dist/tools/memory.d.ts.map +1 -0
- package/dist/tools/memory.js +306 -0
- package/dist/tools/memory.js.map +1 -0
- package/dist/tools/message.d.ts +26 -0
- package/dist/tools/message.d.ts.map +1 -0
- package/dist/tools/message.js +46 -0
- package/dist/tools/message.js.map +1 -0
- package/dist/tools/schedule.d.ts +20 -0
- package/dist/tools/schedule.d.ts.map +1 -0
- package/dist/tools/schedule.js +146 -0
- package/dist/tools/schedule.js.map +1 -0
- 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
|