@cosmonapse/sdk 0.1.2
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/index.cjs +4258 -0
- package/dist/index.d.cts +1915 -0
- package/dist/index.d.ts +1915 -0
- package/dist/index.js +4137 -0
- package/package.json +75 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,4258 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
AXON_TYPES: () => AXON_TYPES,
|
|
34
|
+
Axon: () => Axon,
|
|
35
|
+
Cortex: () => Cortex,
|
|
36
|
+
CortexProtocolError: () => DendriteProtocolError,
|
|
37
|
+
Dendrite: () => Dendrite,
|
|
38
|
+
DendriteProtocolError: () => DendriteProtocolError,
|
|
39
|
+
DevSynapse: () => DevSynapse,
|
|
40
|
+
DevSynapseServer: () => DevSynapseServer,
|
|
41
|
+
Engram: () => Engram,
|
|
42
|
+
EngramBinding: () => EngramBinding,
|
|
43
|
+
EngramCancelled: () => EngramCancelled,
|
|
44
|
+
EngramClient: () => EngramClient,
|
|
45
|
+
EngramError: () => EngramError,
|
|
46
|
+
EngramNotBound: () => EngramNotBound,
|
|
47
|
+
EngramOverloaded: () => EngramOverloaded,
|
|
48
|
+
EngramTimeout: () => EngramTimeout,
|
|
49
|
+
InMemoryEngram: () => InMemoryEngram,
|
|
50
|
+
KafkaSynapse: () => KafkaSynapse,
|
|
51
|
+
LifecycleHooks: () => LifecycleHooks,
|
|
52
|
+
MemoryRegistryStore: () => MemoryRegistryStore,
|
|
53
|
+
MemorySynapse: () => MemorySynapse,
|
|
54
|
+
NatsSynapse: () => NatsSynapse,
|
|
55
|
+
PostgresEngram: () => PostgresEngram,
|
|
56
|
+
PostgresRegistryStore: () => PostgresRegistryStore,
|
|
57
|
+
SYNAPSE_TYPES: () => SYNAPSE_TYPES,
|
|
58
|
+
SignalType: () => SignalType,
|
|
59
|
+
SqliteEngram: () => SqliteEngram,
|
|
60
|
+
SqliteRegistryStore: () => SqliteRegistryStore,
|
|
61
|
+
VERSION: () => VERSION,
|
|
62
|
+
agentOutputSignal: () => agentOutputSignal,
|
|
63
|
+
anthropicNeuron: () => anthropicNeuron,
|
|
64
|
+
bidSignal: () => bidSignal,
|
|
65
|
+
clarificationAnswerSignal: () => clarificationAnswerSignal,
|
|
66
|
+
clarificationSignal: () => clarificationSignal,
|
|
67
|
+
clarify: () => clarify,
|
|
68
|
+
connectSynapse: () => connectSynapse,
|
|
69
|
+
consensusSignal: () => consensusSignal,
|
|
70
|
+
contextSyncSignal: () => contextSyncSignal,
|
|
71
|
+
createSignal: () => createSignal,
|
|
72
|
+
critiqueSignal: () => critiqueSignal,
|
|
73
|
+
decode: () => decode,
|
|
74
|
+
deepMerge: () => deepMerge,
|
|
75
|
+
deregisterSignal: () => deregisterSignal,
|
|
76
|
+
directedTo: () => directedTo,
|
|
77
|
+
discoverSignal: () => discoverSignal,
|
|
78
|
+
encode: () => encode,
|
|
79
|
+
errorResult: () => errorResult,
|
|
80
|
+
errorSignal: () => errorSignal,
|
|
81
|
+
escalationSignal: () => escalationSignal,
|
|
82
|
+
finalSignal: () => finalSignal,
|
|
83
|
+
heartbeatSignal: () => heartbeatSignal,
|
|
84
|
+
huggingFaceNeuron: () => huggingFaceNeuron,
|
|
85
|
+
imprintSignal: () => imprintSignal,
|
|
86
|
+
imprintedSignal: () => imprintedSignal,
|
|
87
|
+
isClarification: () => isClarification,
|
|
88
|
+
isErrorOutput: () => isErrorOutput,
|
|
89
|
+
isPermissionRequest: () => isPermissionRequest,
|
|
90
|
+
mcpNeuron: () => mcpNeuron,
|
|
91
|
+
memoryAppendSignal: () => memoryAppendSignal,
|
|
92
|
+
neuron: () => neuron,
|
|
93
|
+
neuronRecord: () => neuronRecord,
|
|
94
|
+
newEngramId: () => newEngramId,
|
|
95
|
+
newEventId: () => newEventId,
|
|
96
|
+
newTraceId: () => newTraceId,
|
|
97
|
+
normalizeDirected: () => normalizeDirected,
|
|
98
|
+
ollamaNeuron: () => ollamaNeuron,
|
|
99
|
+
openaiNeuron: () => openaiNeuron,
|
|
100
|
+
parseLlmIntents: () => parseLlmIntents,
|
|
101
|
+
parseMcpIntents: () => parseMcpIntents,
|
|
102
|
+
permissionDecisionSignal: () => permissionDecisionSignal,
|
|
103
|
+
permissionRequest: () => permissionRequest,
|
|
104
|
+
permissionSignal: () => permissionSignal,
|
|
105
|
+
planSignal: () => planSignal,
|
|
106
|
+
recallSignal: () => recallSignal,
|
|
107
|
+
recalledSignal: () => recalledSignal,
|
|
108
|
+
registerSignal: () => registerSignal,
|
|
109
|
+
reply: () => reply,
|
|
110
|
+
standardMcpServers: () => standardMcpServers,
|
|
111
|
+
synapseFromUrl: () => synapseFromUrl,
|
|
112
|
+
taskOfferSignal: () => taskOfferSignal,
|
|
113
|
+
taskSignal: () => taskSignal,
|
|
114
|
+
thoughtDeltaSignal: () => thoughtDeltaSignal,
|
|
115
|
+
toolCallSignal: () => toolCallSignal,
|
|
116
|
+
toolResultSignal: () => toolResultSignal,
|
|
117
|
+
validateSignal: () => validateSignal
|
|
118
|
+
});
|
|
119
|
+
module.exports = __toCommonJS(index_exports);
|
|
120
|
+
|
|
121
|
+
// src/envelope.ts
|
|
122
|
+
var import_ulid = require("ulid");
|
|
123
|
+
function newEventId() {
|
|
124
|
+
return `evt_${(0, import_ulid.ulid)()}`;
|
|
125
|
+
}
|
|
126
|
+
function newTraceId() {
|
|
127
|
+
return `trc_${(0, import_ulid.ulid)()}`;
|
|
128
|
+
}
|
|
129
|
+
function newEngramId() {
|
|
130
|
+
return `eng_${(0, import_ulid.ulid)()}`;
|
|
131
|
+
}
|
|
132
|
+
function nowUtc() {
|
|
133
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
134
|
+
}
|
|
135
|
+
var SignalType = {
|
|
136
|
+
// Lifecycle [A] / [C]
|
|
137
|
+
TASK: "TASK",
|
|
138
|
+
AGENT_OUTPUT: "AGENT_OUTPUT",
|
|
139
|
+
FINAL: "FINAL",
|
|
140
|
+
ERROR: "ERROR",
|
|
141
|
+
// Routing [C]
|
|
142
|
+
TASK_OFFER: "TASK_OFFER",
|
|
143
|
+
BID: "BID",
|
|
144
|
+
TASK_AWARDED: "TASK_AWARDED",
|
|
145
|
+
TASK_DECLINED: "TASK_DECLINED",
|
|
146
|
+
// Cognition [C]
|
|
147
|
+
THOUGHT_DELTA: "THOUGHT_DELTA",
|
|
148
|
+
PLAN: "PLAN",
|
|
149
|
+
TOOL_CALL: "TOOL_CALL",
|
|
150
|
+
TOOL_RESULT: "TOOL_RESULT",
|
|
151
|
+
// Memory [C]
|
|
152
|
+
MEMORY_APPEND: "MEMORY_APPEND",
|
|
153
|
+
ESCALATION: "ESCALATION",
|
|
154
|
+
// Coordination [C] / [A]
|
|
155
|
+
CONSENSUS: "CONSENSUS",
|
|
156
|
+
CONTEXT_SYNC: "CONTEXT_SYNC",
|
|
157
|
+
CRITIQUE: "CRITIQUE",
|
|
158
|
+
CLARIFICATION: "CLARIFICATION",
|
|
159
|
+
// Interactive cognition [A] request / [C] response.
|
|
160
|
+
// CLARIFICATION (above) and PERMISSION are Axon-originated requests a Neuron
|
|
161
|
+
// returns as a marker; the matching *_ANSWER / *_DECISION are emitted by
|
|
162
|
+
// whichever Dendrite answers (a central Cortex or a peer). There is no
|
|
163
|
+
// built-in correlation client - the developer wires the loop (keyed by
|
|
164
|
+
// parent_id == the request's id where needed).
|
|
165
|
+
PERMISSION: "PERMISSION",
|
|
166
|
+
PERMISSION_DECISION: "PERMISSION_DECISION",
|
|
167
|
+
CLARIFICATION_ANSWER: "CLARIFICATION_ANSWER",
|
|
168
|
+
// Agent management [A]
|
|
169
|
+
REGISTER: "REGISTER",
|
|
170
|
+
DEREGISTER: "DEREGISTER",
|
|
171
|
+
HEARTBEAT: "HEARTBEAT",
|
|
172
|
+
// Engram [C] - see ENGRAM_DESIGN.md
|
|
173
|
+
RECALL: "RECALL",
|
|
174
|
+
RECALLED: "RECALLED",
|
|
175
|
+
IMPRINT: "IMPRINT",
|
|
176
|
+
IMPRINTED: "IMPRINTED",
|
|
177
|
+
// Discovery [C]
|
|
178
|
+
DISCOVER: "DISCOVER"
|
|
179
|
+
};
|
|
180
|
+
var AXON_TYPES = /* @__PURE__ */ new Set([
|
|
181
|
+
SignalType.AGENT_OUTPUT,
|
|
182
|
+
SignalType.CLARIFICATION,
|
|
183
|
+
SignalType.PERMISSION,
|
|
184
|
+
SignalType.ERROR,
|
|
185
|
+
SignalType.REGISTER,
|
|
186
|
+
SignalType.DEREGISTER,
|
|
187
|
+
SignalType.HEARTBEAT
|
|
188
|
+
]);
|
|
189
|
+
var SYNAPSE_TYPES = /* @__PURE__ */ new Set([
|
|
190
|
+
SignalType.TASK,
|
|
191
|
+
SignalType.FINAL,
|
|
192
|
+
SignalType.ERROR,
|
|
193
|
+
SignalType.TASK_OFFER,
|
|
194
|
+
SignalType.BID,
|
|
195
|
+
SignalType.TASK_AWARDED,
|
|
196
|
+
SignalType.TASK_DECLINED,
|
|
197
|
+
SignalType.THOUGHT_DELTA,
|
|
198
|
+
SignalType.PLAN,
|
|
199
|
+
SignalType.TOOL_CALL,
|
|
200
|
+
SignalType.TOOL_RESULT,
|
|
201
|
+
SignalType.MEMORY_APPEND,
|
|
202
|
+
SignalType.ESCALATION,
|
|
203
|
+
SignalType.CONSENSUS,
|
|
204
|
+
SignalType.CONTEXT_SYNC,
|
|
205
|
+
SignalType.CRITIQUE,
|
|
206
|
+
// Responses to Axon-originated CLARIFICATION / PERMISSION requests, emitted
|
|
207
|
+
// by the answering Dendrite (central Cortex or peer) and correlated by the
|
|
208
|
+
// requester's CognitionClient via parent_id.
|
|
209
|
+
SignalType.PERMISSION_DECISION,
|
|
210
|
+
SignalType.CLARIFICATION_ANSWER,
|
|
211
|
+
SignalType.DISCOVER,
|
|
212
|
+
SignalType.RECALL,
|
|
213
|
+
SignalType.RECALLED,
|
|
214
|
+
SignalType.IMPRINT,
|
|
215
|
+
SignalType.IMPRINTED
|
|
216
|
+
]);
|
|
217
|
+
function normalizeDirected(d) {
|
|
218
|
+
if (d === null || d === void 0) return null;
|
|
219
|
+
return {
|
|
220
|
+
id: d.id ?? null,
|
|
221
|
+
type: d.type ?? null,
|
|
222
|
+
capabilities: d.capabilities ? [...d.capabilities] : []
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function directedTo(id, opts = {}) {
|
|
226
|
+
return {
|
|
227
|
+
id: id ?? null,
|
|
228
|
+
type: opts.type ?? null,
|
|
229
|
+
capabilities: opts.capabilities ? [...opts.capabilities] : []
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function createSignal(input) {
|
|
233
|
+
const signal = {
|
|
234
|
+
v: input.v ?? "1",
|
|
235
|
+
id: input.id ?? newEventId(),
|
|
236
|
+
trace_id: input.trace_id ?? newTraceId(),
|
|
237
|
+
parent_id: input.parent_id ?? null,
|
|
238
|
+
type: input.type,
|
|
239
|
+
directed: normalizeDirected(input.directed),
|
|
240
|
+
ts: input.ts ?? nowUtc(),
|
|
241
|
+
payload: input.payload ?? {},
|
|
242
|
+
meta: input.meta ?? {}
|
|
243
|
+
};
|
|
244
|
+
validateSignal(signal);
|
|
245
|
+
return signal;
|
|
246
|
+
}
|
|
247
|
+
function validateSignal(signal) {
|
|
248
|
+
if (!signal.id.startsWith("evt_")) {
|
|
249
|
+
throw new Error(`Signal id must start with 'evt_', got: ${signal.id}`);
|
|
250
|
+
}
|
|
251
|
+
if (!signal.trace_id.startsWith("trc_")) {
|
|
252
|
+
throw new Error(`trace_id must start with 'trc_', got: ${signal.trace_id}`);
|
|
253
|
+
}
|
|
254
|
+
if (signal.parent_id !== null && !signal.parent_id.startsWith("evt_")) {
|
|
255
|
+
throw new Error(`parent_id must start with 'evt_', got: ${signal.parent_id}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function encode(signal) {
|
|
259
|
+
return new TextEncoder().encode(JSON.stringify(signal));
|
|
260
|
+
}
|
|
261
|
+
function decode(data) {
|
|
262
|
+
const text = typeof data === "string" ? data : new TextDecoder().decode(data);
|
|
263
|
+
const parsed = JSON.parse(text);
|
|
264
|
+
if (parsed.type === void 0) {
|
|
265
|
+
throw new Error("Signal is missing required field 'type'");
|
|
266
|
+
}
|
|
267
|
+
return createSignal(parsed);
|
|
268
|
+
}
|
|
269
|
+
function reply(source, opts) {
|
|
270
|
+
return createSignal({
|
|
271
|
+
type: opts.type,
|
|
272
|
+
trace_id: source.trace_id,
|
|
273
|
+
parent_id: source.id,
|
|
274
|
+
payload: opts.payload ?? {},
|
|
275
|
+
directed: opts.directed !== void 0 ? opts.directed : source.directed,
|
|
276
|
+
meta: opts.meta ?? {}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/signals.ts
|
|
281
|
+
function taskSignal(args) {
|
|
282
|
+
const payload = { input: args.input };
|
|
283
|
+
if (args.contextRef) payload["context_ref"] = args.contextRef;
|
|
284
|
+
if (args.capabilities) payload["capabilities"] = args.capabilities;
|
|
285
|
+
return createSignal({
|
|
286
|
+
type: SignalType.TASK,
|
|
287
|
+
trace_id: args.traceId ?? newTraceId(),
|
|
288
|
+
parent_id: args.parentId ?? null,
|
|
289
|
+
directed: args.directed ?? null,
|
|
290
|
+
payload,
|
|
291
|
+
meta: args.meta ?? {}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
function agentOutputSignal(args) {
|
|
295
|
+
return createSignal({
|
|
296
|
+
type: SignalType.AGENT_OUTPUT,
|
|
297
|
+
trace_id: args.traceId,
|
|
298
|
+
parent_id: args.parentId,
|
|
299
|
+
directed: args.directed ?? null,
|
|
300
|
+
payload: { output: args.output },
|
|
301
|
+
meta: args.meta ?? {}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
function clarificationSignal(args) {
|
|
305
|
+
const payload = { question: args.question };
|
|
306
|
+
if (args.context) payload["context"] = args.context;
|
|
307
|
+
return createSignal({
|
|
308
|
+
type: SignalType.CLARIFICATION,
|
|
309
|
+
trace_id: args.traceId,
|
|
310
|
+
parent_id: args.parentId,
|
|
311
|
+
directed: args.directed ?? null,
|
|
312
|
+
payload,
|
|
313
|
+
meta: args.meta ?? {}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
function permissionSignal(args) {
|
|
317
|
+
const payload = { action: args.action };
|
|
318
|
+
if (args.scope) payload["scope"] = args.scope;
|
|
319
|
+
if (args.reason !== void 0) payload["reason"] = args.reason;
|
|
320
|
+
if (args.context) payload["context"] = args.context;
|
|
321
|
+
return createSignal({
|
|
322
|
+
type: SignalType.PERMISSION,
|
|
323
|
+
trace_id: args.traceId,
|
|
324
|
+
parent_id: args.parentId,
|
|
325
|
+
directed: args.directed ?? null,
|
|
326
|
+
payload,
|
|
327
|
+
meta: args.meta ?? {}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
function permissionDecisionSignal(args) {
|
|
331
|
+
const payload = { granted: args.granted };
|
|
332
|
+
if (args.reason !== void 0) payload["reason"] = args.reason;
|
|
333
|
+
if (args.ttlMs !== void 0) payload["ttl_ms"] = args.ttlMs;
|
|
334
|
+
return createSignal({
|
|
335
|
+
type: SignalType.PERMISSION_DECISION,
|
|
336
|
+
trace_id: args.traceId,
|
|
337
|
+
parent_id: args.parentId,
|
|
338
|
+
directed: args.directed ?? null,
|
|
339
|
+
payload,
|
|
340
|
+
meta: args.meta ?? {}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
function clarificationAnswerSignal(args) {
|
|
344
|
+
return createSignal({
|
|
345
|
+
type: SignalType.CLARIFICATION_ANSWER,
|
|
346
|
+
trace_id: args.traceId,
|
|
347
|
+
parent_id: args.parentId,
|
|
348
|
+
directed: args.directed ?? null,
|
|
349
|
+
payload: { answer: args.answer },
|
|
350
|
+
meta: args.meta ?? {}
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
function finalSignal(args) {
|
|
354
|
+
const payload = { result: args.result };
|
|
355
|
+
if (args.cost) payload["cost"] = args.cost;
|
|
356
|
+
return createSignal({
|
|
357
|
+
type: SignalType.FINAL,
|
|
358
|
+
trace_id: args.traceId,
|
|
359
|
+
parent_id: args.parentId,
|
|
360
|
+
directed: args.directed ?? null,
|
|
361
|
+
payload,
|
|
362
|
+
meta: args.meta ?? {}
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
function errorSignal(args) {
|
|
366
|
+
return createSignal({
|
|
367
|
+
type: SignalType.ERROR,
|
|
368
|
+
trace_id: args.traceId,
|
|
369
|
+
parent_id: args.parentId ?? null,
|
|
370
|
+
directed: args.directed ?? null,
|
|
371
|
+
payload: {
|
|
372
|
+
code: args.code,
|
|
373
|
+
message: args.message,
|
|
374
|
+
recoverable: args.recoverable ?? false
|
|
375
|
+
},
|
|
376
|
+
meta: args.meta ?? {}
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
function registerSignal(args) {
|
|
380
|
+
const caps = args.capabilities ?? args.directed?.capabilities ?? [];
|
|
381
|
+
const payload = { capabilities: caps };
|
|
382
|
+
if (args.version) payload["version"] = args.version;
|
|
383
|
+
if (args.engram) payload["engram"] = true;
|
|
384
|
+
return createSignal({
|
|
385
|
+
type: SignalType.REGISTER,
|
|
386
|
+
trace_id: newTraceId(),
|
|
387
|
+
// management signals get their own trace
|
|
388
|
+
directed: args.directed ?? null,
|
|
389
|
+
payload,
|
|
390
|
+
meta: args.meta ?? {}
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
function deregisterSignal(args) {
|
|
394
|
+
const payload = {};
|
|
395
|
+
if (args.reason) payload["reason"] = args.reason;
|
|
396
|
+
return createSignal({
|
|
397
|
+
type: SignalType.DEREGISTER,
|
|
398
|
+
trace_id: newTraceId(),
|
|
399
|
+
directed: args.directed ?? null,
|
|
400
|
+
payload,
|
|
401
|
+
meta: args.meta ?? {}
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
function heartbeatSignal(args) {
|
|
405
|
+
return createSignal({
|
|
406
|
+
type: SignalType.HEARTBEAT,
|
|
407
|
+
trace_id: newTraceId(),
|
|
408
|
+
directed: args.directed ?? null,
|
|
409
|
+
payload: { status: args.status ?? "ok" },
|
|
410
|
+
meta: args.meta ?? {}
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
function memoryAppendSignal(args) {
|
|
414
|
+
return createSignal({
|
|
415
|
+
type: SignalType.MEMORY_APPEND,
|
|
416
|
+
trace_id: args.traceId,
|
|
417
|
+
parent_id: args.parentId,
|
|
418
|
+
directed: args.directed ?? null,
|
|
419
|
+
payload: { key: args.key, value: args.value },
|
|
420
|
+
meta: args.meta ?? {}
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
function taskOfferSignal(args) {
|
|
424
|
+
const payload = { input: args.input };
|
|
425
|
+
if (args.capabilities) payload["capabilities"] = args.capabilities;
|
|
426
|
+
if (args.deadlineMs !== void 0) payload["deadline_ms"] = args.deadlineMs;
|
|
427
|
+
return createSignal({
|
|
428
|
+
type: SignalType.TASK_OFFER,
|
|
429
|
+
trace_id: args.traceId,
|
|
430
|
+
parent_id: args.parentId ?? null,
|
|
431
|
+
payload,
|
|
432
|
+
meta: args.meta ?? {}
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
function bidSignal(args) {
|
|
436
|
+
const payload = { cost: args.cost };
|
|
437
|
+
if (args.etaMs !== void 0) payload["eta_ms"] = args.etaMs;
|
|
438
|
+
if (args.confidence !== void 0) payload["confidence"] = args.confidence;
|
|
439
|
+
return createSignal({
|
|
440
|
+
type: SignalType.BID,
|
|
441
|
+
trace_id: args.traceId,
|
|
442
|
+
parent_id: args.parentId,
|
|
443
|
+
directed: args.directed ?? null,
|
|
444
|
+
payload,
|
|
445
|
+
meta: args.meta ?? {}
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
function critiqueSignal(args) {
|
|
449
|
+
return createSignal({
|
|
450
|
+
type: SignalType.CRITIQUE,
|
|
451
|
+
trace_id: args.traceId,
|
|
452
|
+
parent_id: args.parentId,
|
|
453
|
+
directed: args.directed ?? null,
|
|
454
|
+
payload: {
|
|
455
|
+
target_event_id: args.targetEventId,
|
|
456
|
+
issues: args.issues,
|
|
457
|
+
verdict: args.verdict
|
|
458
|
+
},
|
|
459
|
+
meta: args.meta ?? {}
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
function planSignal(args) {
|
|
463
|
+
const payload = { steps: args.steps };
|
|
464
|
+
if (args.rationale !== void 0) payload["rationale"] = args.rationale;
|
|
465
|
+
return createSignal({
|
|
466
|
+
type: SignalType.PLAN,
|
|
467
|
+
trace_id: args.traceId,
|
|
468
|
+
parent_id: args.parentId,
|
|
469
|
+
directed: args.directed ?? null,
|
|
470
|
+
payload,
|
|
471
|
+
meta: args.meta ?? {}
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
function thoughtDeltaSignal(args) {
|
|
475
|
+
const payload = { delta: args.delta };
|
|
476
|
+
if (args.seq !== void 0) payload["seq"] = args.seq;
|
|
477
|
+
return createSignal({
|
|
478
|
+
type: SignalType.THOUGHT_DELTA,
|
|
479
|
+
trace_id: args.traceId,
|
|
480
|
+
parent_id: args.parentId,
|
|
481
|
+
directed: args.directed ?? null,
|
|
482
|
+
payload,
|
|
483
|
+
meta: args.meta ?? {}
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
function toolCallSignal(args) {
|
|
487
|
+
const payload = { tool: args.tool, args: args.args };
|
|
488
|
+
if (args.callId !== void 0) payload["call_id"] = args.callId;
|
|
489
|
+
return createSignal({
|
|
490
|
+
type: SignalType.TOOL_CALL,
|
|
491
|
+
trace_id: args.traceId,
|
|
492
|
+
parent_id: args.parentId,
|
|
493
|
+
directed: args.directed ?? null,
|
|
494
|
+
payload,
|
|
495
|
+
meta: args.meta ?? {}
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
function toolResultSignal(args) {
|
|
499
|
+
const payload = { tool: args.tool };
|
|
500
|
+
if (args.result !== void 0) payload["result"] = args.result;
|
|
501
|
+
if (args.error !== void 0) payload["error"] = args.error;
|
|
502
|
+
if (args.callId !== void 0) payload["call_id"] = args.callId;
|
|
503
|
+
return createSignal({
|
|
504
|
+
type: SignalType.TOOL_RESULT,
|
|
505
|
+
trace_id: args.traceId,
|
|
506
|
+
parent_id: args.parentId,
|
|
507
|
+
directed: args.directed ?? null,
|
|
508
|
+
payload,
|
|
509
|
+
meta: args.meta ?? {}
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
function escalationSignal(args) {
|
|
513
|
+
const payload = { reason: args.reason };
|
|
514
|
+
if (args.target !== void 0) payload["target"] = args.target;
|
|
515
|
+
if (args.context !== void 0) payload["context"] = args.context;
|
|
516
|
+
return createSignal({
|
|
517
|
+
type: SignalType.ESCALATION,
|
|
518
|
+
trace_id: args.traceId,
|
|
519
|
+
parent_id: args.parentId,
|
|
520
|
+
directed: args.directed ?? null,
|
|
521
|
+
payload,
|
|
522
|
+
meta: args.meta ?? {}
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
function consensusSignal(args) {
|
|
526
|
+
const payload = { members: args.members, verdict: args.verdict };
|
|
527
|
+
if (args.votes !== void 0) payload["votes"] = args.votes;
|
|
528
|
+
return createSignal({
|
|
529
|
+
type: SignalType.CONSENSUS,
|
|
530
|
+
trace_id: args.traceId,
|
|
531
|
+
parent_id: args.parentId,
|
|
532
|
+
directed: args.directed ?? null,
|
|
533
|
+
payload,
|
|
534
|
+
meta: args.meta ?? {}
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
function contextSyncSignal(args) {
|
|
538
|
+
const payload = { snapshot: args.snapshot };
|
|
539
|
+
if (args.version !== void 0) payload["version"] = args.version;
|
|
540
|
+
return createSignal({
|
|
541
|
+
type: SignalType.CONTEXT_SYNC,
|
|
542
|
+
trace_id: args.traceId,
|
|
543
|
+
parent_id: args.parentId,
|
|
544
|
+
directed: args.directed ?? null,
|
|
545
|
+
payload,
|
|
546
|
+
meta: args.meta ?? {}
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
function discoverSignal(args = {}) {
|
|
550
|
+
const payload = {};
|
|
551
|
+
if (args.neuron !== void 0) payload["neuron"] = args.neuron;
|
|
552
|
+
if (args.capabilities !== void 0) payload["capabilities"] = args.capabilities;
|
|
553
|
+
return createSignal({
|
|
554
|
+
type: SignalType.DISCOVER,
|
|
555
|
+
trace_id: args.traceId ?? newTraceId(),
|
|
556
|
+
parent_id: args.parentId ?? null,
|
|
557
|
+
payload,
|
|
558
|
+
meta: args.meta ?? {}
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
var RECALL_MODES = /* @__PURE__ */ new Set(["first", "merge", "all"]);
|
|
562
|
+
var IMPRINT_OPS = /* @__PURE__ */ new Set(["add", "append", "merge", "upsert", "delete"]);
|
|
563
|
+
function recallSignal(args) {
|
|
564
|
+
const directed = normalizeDirected(args.directed);
|
|
565
|
+
if (!directed || !directed.id && !directed.type) {
|
|
566
|
+
throw new Error(
|
|
567
|
+
"recallSignal requires directed.id (engram_id) or directed.type (engram_kind)"
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
const mode = args.recallMode ?? "first";
|
|
571
|
+
if (!RECALL_MODES.has(mode)) {
|
|
572
|
+
throw new Error(`recallMode must be 'first' | 'merge' | 'all', got '${mode}'`);
|
|
573
|
+
}
|
|
574
|
+
const payload = { query: args.query, recall_mode: mode };
|
|
575
|
+
if (args.filters !== void 0) payload["filters"] = args.filters;
|
|
576
|
+
if (args.contextRef !== void 0) payload["context_ref"] = args.contextRef;
|
|
577
|
+
if (args.deadlineMs !== void 0) payload["deadline_ms"] = args.deadlineMs;
|
|
578
|
+
if (args.minConfidence !== void 0) payload["min_confidence"] = args.minConfidence;
|
|
579
|
+
return createSignal({
|
|
580
|
+
type: SignalType.RECALL,
|
|
581
|
+
trace_id: args.traceId,
|
|
582
|
+
parent_id: args.parentId,
|
|
583
|
+
directed: args.directed,
|
|
584
|
+
payload,
|
|
585
|
+
meta: args.meta ?? {}
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
function recalledSignal(args) {
|
|
589
|
+
const payload = {
|
|
590
|
+
engram_id: args.engramId,
|
|
591
|
+
hits: args.hits,
|
|
592
|
+
truncated: args.truncated ?? false
|
|
593
|
+
};
|
|
594
|
+
if (args.tookMs !== void 0) payload["took_ms"] = args.tookMs;
|
|
595
|
+
return createSignal({
|
|
596
|
+
type: SignalType.RECALLED,
|
|
597
|
+
trace_id: args.traceId,
|
|
598
|
+
parent_id: args.parentId,
|
|
599
|
+
directed: args.directed ?? null,
|
|
600
|
+
payload,
|
|
601
|
+
meta: args.meta ?? {}
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
function imprintSignal(args) {
|
|
605
|
+
if (!IMPRINT_OPS.has(args.op)) {
|
|
606
|
+
throw new Error(`imprint op must be one of ${[...IMPRINT_OPS].join(" | ")}, got '${args.op}'`);
|
|
607
|
+
}
|
|
608
|
+
const directed = normalizeDirected(args.directed);
|
|
609
|
+
if (!directed || !directed.id && !directed.type) {
|
|
610
|
+
throw new Error(
|
|
611
|
+
"imprintSignal requires directed.id (engram_id) or directed.type (engram_kind)"
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
if ((args.op === "merge" || args.op === "upsert") && !args.mergeKey) {
|
|
615
|
+
throw new Error(`imprint op='${args.op}' requires mergeKey`);
|
|
616
|
+
}
|
|
617
|
+
const payload = { op: args.op, entry: args.entry };
|
|
618
|
+
if (args.mergeKey !== void 0) payload["merge_key"] = args.mergeKey;
|
|
619
|
+
return createSignal({
|
|
620
|
+
type: SignalType.IMPRINT,
|
|
621
|
+
trace_id: args.traceId,
|
|
622
|
+
parent_id: args.parentId,
|
|
623
|
+
directed: args.directed,
|
|
624
|
+
payload,
|
|
625
|
+
meta: args.meta ?? {}
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
function imprintedSignal(args) {
|
|
629
|
+
const payload = { engram_id: args.engramId, op: args.op };
|
|
630
|
+
if (args.id !== void 0) payload["id"] = args.id;
|
|
631
|
+
if (args.version !== void 0) payload["version"] = args.version;
|
|
632
|
+
if (args.tookMs !== void 0) payload["took_ms"] = args.tookMs;
|
|
633
|
+
if (args.error !== void 0) payload["error"] = args.error;
|
|
634
|
+
return createSignal({
|
|
635
|
+
type: SignalType.IMPRINTED,
|
|
636
|
+
trace_id: args.traceId,
|
|
637
|
+
parent_id: args.parentId,
|
|
638
|
+
directed: args.directed ?? null,
|
|
639
|
+
payload,
|
|
640
|
+
meta: args.meta ?? {}
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// src/synapse.ts
|
|
645
|
+
var MemorySubscription = class {
|
|
646
|
+
active = true;
|
|
647
|
+
synapse;
|
|
648
|
+
subject;
|
|
649
|
+
handlerId;
|
|
650
|
+
constructor(synapse, subject, handlerId) {
|
|
651
|
+
this.synapse = synapse;
|
|
652
|
+
this.subject = subject;
|
|
653
|
+
this.handlerId = handlerId;
|
|
654
|
+
}
|
|
655
|
+
async unsubscribe() {
|
|
656
|
+
if (this.active) {
|
|
657
|
+
this.synapse._removeHandler(this.subject, this.handlerId);
|
|
658
|
+
this.active = false;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
var MemorySynapse = class _MemorySynapse {
|
|
663
|
+
subs = /* @__PURE__ */ new Map();
|
|
664
|
+
counter = 0;
|
|
665
|
+
connected = false;
|
|
666
|
+
rrCounters = /* @__PURE__ */ new Map();
|
|
667
|
+
async connect() {
|
|
668
|
+
this.connected = true;
|
|
669
|
+
}
|
|
670
|
+
async close() {
|
|
671
|
+
this.subs.clear();
|
|
672
|
+
this.connected = false;
|
|
673
|
+
}
|
|
674
|
+
nextId() {
|
|
675
|
+
this.counter += 1;
|
|
676
|
+
return this.counter;
|
|
677
|
+
}
|
|
678
|
+
/** @internal */
|
|
679
|
+
_removeHandler(subject, handlerId) {
|
|
680
|
+
const entries = this.subs.get(subject);
|
|
681
|
+
if (!entries) return;
|
|
682
|
+
const kept = entries.filter((e) => e.id !== handlerId);
|
|
683
|
+
if (kept.length) this.subs.set(subject, kept);
|
|
684
|
+
else this.subs.delete(subject);
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Return true if `subject` matches `pattern`.
|
|
688
|
+
* * matches any single token (no dots)
|
|
689
|
+
* > matches any sequence of trailing tokens
|
|
690
|
+
*/
|
|
691
|
+
static matches(pattern, subject) {
|
|
692
|
+
if (pattern === subject) return true;
|
|
693
|
+
const p = pattern.split(".");
|
|
694
|
+
const s = subject.split(".");
|
|
695
|
+
let i = 0;
|
|
696
|
+
let j = 0;
|
|
697
|
+
while (i < p.length && j < s.length) {
|
|
698
|
+
if (p[i] === ">") return true;
|
|
699
|
+
if (p[i] === "*") {
|
|
700
|
+
i += 1;
|
|
701
|
+
j += 1;
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
if (p[i] !== s[j]) return false;
|
|
705
|
+
i += 1;
|
|
706
|
+
j += 1;
|
|
707
|
+
}
|
|
708
|
+
return i === p.length && j === s.length;
|
|
709
|
+
}
|
|
710
|
+
async publish(subject, signal) {
|
|
711
|
+
if (!this.connected) throw new Error("Synapse not connected");
|
|
712
|
+
await this.deliver(subject, signal);
|
|
713
|
+
}
|
|
714
|
+
async deliver(subject, signal) {
|
|
715
|
+
const queueGroups = /* @__PURE__ */ new Map();
|
|
716
|
+
const solo = [];
|
|
717
|
+
for (const [pattern, entries] of this.subs) {
|
|
718
|
+
if (!_MemorySynapse.matches(pattern, subject)) continue;
|
|
719
|
+
for (const e of entries) {
|
|
720
|
+
if (e.group === null) solo.push(e.handler);
|
|
721
|
+
else {
|
|
722
|
+
const list = queueGroups.get(e.group) ?? [];
|
|
723
|
+
list.push(e.handler);
|
|
724
|
+
queueGroups.set(e.group, list);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
const pending = [];
|
|
729
|
+
const invoke = (h) => {
|
|
730
|
+
try {
|
|
731
|
+
const r = h(structuredClone(signal));
|
|
732
|
+
if (r instanceof Promise) pending.push(r);
|
|
733
|
+
} catch (err) {
|
|
734
|
+
pending.push(Promise.reject(err));
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
for (const h of solo) invoke(h);
|
|
738
|
+
for (const [group, handlers] of queueGroups) {
|
|
739
|
+
if (!handlers.length) continue;
|
|
740
|
+
const n = this.rrCounters.get(group) ?? 0;
|
|
741
|
+
const idx = n % handlers.length;
|
|
742
|
+
this.rrCounters.set(group, n + 1);
|
|
743
|
+
invoke(handlers[idx]);
|
|
744
|
+
}
|
|
745
|
+
if (pending.length) await Promise.allSettled(pending);
|
|
746
|
+
}
|
|
747
|
+
async subscribe(subject, handler, opts = {}) {
|
|
748
|
+
if (!this.connected) throw new Error("Synapse not connected");
|
|
749
|
+
const id = this.nextId();
|
|
750
|
+
const entries = this.subs.get(subject) ?? [];
|
|
751
|
+
entries.push({ id, group: opts.queueGroup ?? null, handler });
|
|
752
|
+
this.subs.set(subject, entries);
|
|
753
|
+
return new MemorySubscription(this, subject, id);
|
|
754
|
+
}
|
|
755
|
+
async request(subject, signal, opts = {}) {
|
|
756
|
+
if (!this.connected) throw new Error("Synapse not connected");
|
|
757
|
+
const timeoutMs = opts.timeoutMs ?? 5e3;
|
|
758
|
+
const replySubject = `_INBOX.${signal.id}`;
|
|
759
|
+
let resolveFn;
|
|
760
|
+
let settled = false;
|
|
761
|
+
const fut = new Promise((resolve) => {
|
|
762
|
+
resolveFn = resolve;
|
|
763
|
+
});
|
|
764
|
+
const sub = await this.subscribe(replySubject, (reply2) => {
|
|
765
|
+
if (!settled) {
|
|
766
|
+
settled = true;
|
|
767
|
+
resolveFn(reply2);
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
try {
|
|
771
|
+
const enriched = {
|
|
772
|
+
...signal,
|
|
773
|
+
meta: { ...signal.meta, _reply_to: replySubject }
|
|
774
|
+
};
|
|
775
|
+
await this.publish(subject, enriched);
|
|
776
|
+
return await new Promise((resolve, reject) => {
|
|
777
|
+
const timer = setTimeout(() => {
|
|
778
|
+
if (!settled) {
|
|
779
|
+
settled = true;
|
|
780
|
+
reject(new Error(`No reply received on '${replySubject}' within ${timeoutMs}ms`));
|
|
781
|
+
}
|
|
782
|
+
}, timeoutMs);
|
|
783
|
+
fut.then((s) => {
|
|
784
|
+
clearTimeout(timer);
|
|
785
|
+
resolve(s);
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
} finally {
|
|
789
|
+
await sub.unsubscribe();
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Convenience: send `reply` to the `_reply_to` subject stored in
|
|
794
|
+
* `original.meta`. Used by request/reply responders.
|
|
795
|
+
*/
|
|
796
|
+
async replyTo(original, reply2) {
|
|
797
|
+
const replySubject = original.meta["_reply_to"];
|
|
798
|
+
if (typeof replySubject !== "string" || !replySubject) {
|
|
799
|
+
throw new Error("Signal has no _reply_to in meta - not a request signal");
|
|
800
|
+
}
|
|
801
|
+
await this.publish(replySubject, reply2);
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
// src/synapse-nats.ts
|
|
806
|
+
var NatsSubscription = class {
|
|
807
|
+
active = true;
|
|
808
|
+
sub;
|
|
809
|
+
constructor(sub) {
|
|
810
|
+
this.sub = sub;
|
|
811
|
+
}
|
|
812
|
+
async unsubscribe() {
|
|
813
|
+
if (this.active) {
|
|
814
|
+
this.sub.unsubscribe();
|
|
815
|
+
this.active = false;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
var NatsSynapse = class {
|
|
820
|
+
url;
|
|
821
|
+
nc = null;
|
|
822
|
+
connected = false;
|
|
823
|
+
constructor(opts = {}) {
|
|
824
|
+
this.url = opts.url ?? "nats://127.0.0.1:4222";
|
|
825
|
+
}
|
|
826
|
+
async connect() {
|
|
827
|
+
if (this.connected) return;
|
|
828
|
+
let mod;
|
|
829
|
+
try {
|
|
830
|
+
mod = await import("nats");
|
|
831
|
+
} catch (err) {
|
|
832
|
+
throw new Error(
|
|
833
|
+
"NatsSynapse requires the 'nats' package. Install it with: npm i nats" + (err instanceof Error ? ` (${err.message})` : "")
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
this.nc = await mod.connect({ servers: this.url });
|
|
837
|
+
this.connected = true;
|
|
838
|
+
}
|
|
839
|
+
async close() {
|
|
840
|
+
if (!this.connected || this.nc === null) return;
|
|
841
|
+
await this.nc.drain();
|
|
842
|
+
this.nc = null;
|
|
843
|
+
this.connected = false;
|
|
844
|
+
}
|
|
845
|
+
requireConn(method) {
|
|
846
|
+
if (!this.connected || this.nc === null) {
|
|
847
|
+
throw new Error(`NatsSynapse.${method} called before connect()`);
|
|
848
|
+
}
|
|
849
|
+
return this.nc;
|
|
850
|
+
}
|
|
851
|
+
async publish(subject, signal) {
|
|
852
|
+
this.requireConn("publish").publish(subject, encode(signal));
|
|
853
|
+
}
|
|
854
|
+
async subscribe(subject, handler, opts = {}) {
|
|
855
|
+
const nc = this.requireConn("subscribe");
|
|
856
|
+
const sub = nc.subscribe(subject, {
|
|
857
|
+
...opts.queueGroup !== void 0 ? { queue: opts.queueGroup } : {},
|
|
858
|
+
callback: (err, msg) => {
|
|
859
|
+
if (err) return;
|
|
860
|
+
let signal;
|
|
861
|
+
try {
|
|
862
|
+
signal = decode(msg.data);
|
|
863
|
+
} catch {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (msg.reply) {
|
|
867
|
+
signal.meta = { ...signal.meta, _reply_to: msg.reply };
|
|
868
|
+
}
|
|
869
|
+
void Promise.resolve(handler(signal)).catch(() => {
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
return new NatsSubscription(sub);
|
|
874
|
+
}
|
|
875
|
+
async request(subject, signal, opts = {}) {
|
|
876
|
+
const nc = this.requireConn("request");
|
|
877
|
+
const timeout = opts.timeoutMs ?? 5e3;
|
|
878
|
+
let msg;
|
|
879
|
+
try {
|
|
880
|
+
msg = await nc.request(subject, encode(signal), { timeout });
|
|
881
|
+
} catch (err) {
|
|
882
|
+
throw new Error(
|
|
883
|
+
`NatsSynapse: no reply on '${subject}' within ${timeout}ms` + (err instanceof Error ? ` (${err.message})` : "")
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
return decode(msg.data);
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Send `reply` to the `_reply_to` subject the inbound bridge stashed in
|
|
890
|
+
* `original.meta` (the native NATS inbox). Mirrors MemorySynapse.replyTo so
|
|
891
|
+
* request/reply responder code is adapter-agnostic.
|
|
892
|
+
*/
|
|
893
|
+
async replyTo(original, reply2) {
|
|
894
|
+
const replySubject = original.meta["_reply_to"];
|
|
895
|
+
if (typeof replySubject !== "string" || !replySubject) {
|
|
896
|
+
throw new Error("Signal has no _reply_to in meta - not a request signal");
|
|
897
|
+
}
|
|
898
|
+
await this.publish(replySubject, reply2);
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
// src/synapse-dev.ts
|
|
903
|
+
var import_node_net = require("net");
|
|
904
|
+
function matches(pattern, subject) {
|
|
905
|
+
return MemorySynapse.matches(pattern, subject);
|
|
906
|
+
}
|
|
907
|
+
var LineSplitter = class {
|
|
908
|
+
buf = "";
|
|
909
|
+
push(chunk) {
|
|
910
|
+
this.buf += chunk.toString("utf-8");
|
|
911
|
+
const lines = [];
|
|
912
|
+
let idx;
|
|
913
|
+
while ((idx = this.buf.indexOf("\n")) !== -1) {
|
|
914
|
+
lines.push(this.buf.slice(0, idx));
|
|
915
|
+
this.buf = this.buf.slice(idx + 1);
|
|
916
|
+
}
|
|
917
|
+
return lines;
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
var ClientSession = class {
|
|
921
|
+
constructor(socket, peer) {
|
|
922
|
+
this.socket = socket;
|
|
923
|
+
this.peer = peer;
|
|
924
|
+
}
|
|
925
|
+
socket;
|
|
926
|
+
peer;
|
|
927
|
+
subs = /* @__PURE__ */ new Map();
|
|
928
|
+
alive = true;
|
|
929
|
+
writeChain = Promise.resolve();
|
|
930
|
+
send(payload) {
|
|
931
|
+
if (!this.alive) return Promise.resolve();
|
|
932
|
+
const line = JSON.stringify(payload) + "\n";
|
|
933
|
+
this.writeChain = this.writeChain.then(
|
|
934
|
+
() => new Promise((resolve) => {
|
|
935
|
+
if (!this.alive) return resolve();
|
|
936
|
+
this.socket.write(line, () => resolve());
|
|
937
|
+
})
|
|
938
|
+
);
|
|
939
|
+
return this.writeChain;
|
|
940
|
+
}
|
|
941
|
+
close() {
|
|
942
|
+
this.alive = false;
|
|
943
|
+
try {
|
|
944
|
+
this.socket.destroy();
|
|
945
|
+
} catch {
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
var DevSynapseServer = class {
|
|
950
|
+
_host;
|
|
951
|
+
_port;
|
|
952
|
+
server = null;
|
|
953
|
+
sessions = /* @__PURE__ */ new Set();
|
|
954
|
+
rrCounters = /* @__PURE__ */ new Map();
|
|
955
|
+
namespaces = /* @__PURE__ */ new Map();
|
|
956
|
+
/**
|
|
957
|
+
* Optional observer hook: every published Signal is passed here as
|
|
958
|
+
* (subject, frame) before fan-out. Used by `cosmo synapse start` to stream
|
|
959
|
+
* to stdout.
|
|
960
|
+
*/
|
|
961
|
+
onSignal = null;
|
|
962
|
+
constructor(opts = {}) {
|
|
963
|
+
this._host = opts.host ?? "127.0.0.1";
|
|
964
|
+
this._port = opts.port ?? 7070;
|
|
965
|
+
}
|
|
966
|
+
get host() {
|
|
967
|
+
return this._host;
|
|
968
|
+
}
|
|
969
|
+
get port() {
|
|
970
|
+
return this._port;
|
|
971
|
+
}
|
|
972
|
+
get url() {
|
|
973
|
+
return `cosmo://${this._host}:${this._port}`;
|
|
974
|
+
}
|
|
975
|
+
get sessionCount() {
|
|
976
|
+
return this.sessions.size;
|
|
977
|
+
}
|
|
978
|
+
async start() {
|
|
979
|
+
if (this.server !== null) return;
|
|
980
|
+
await new Promise((resolve, reject) => {
|
|
981
|
+
const server = (0, import_node_net.createServer)((socket) => this.handleClient(socket));
|
|
982
|
+
server.once("error", reject);
|
|
983
|
+
server.listen(this._port, this._host, () => {
|
|
984
|
+
const addr = server.address();
|
|
985
|
+
if (addr && typeof addr === "object") this._port = addr.port;
|
|
986
|
+
server.off("error", reject);
|
|
987
|
+
this.server = server;
|
|
988
|
+
resolve();
|
|
989
|
+
});
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
async stop() {
|
|
993
|
+
if (this.server === null) return;
|
|
994
|
+
for (const s of [...this.sessions]) s.close();
|
|
995
|
+
this.sessions.clear();
|
|
996
|
+
const server = this.server;
|
|
997
|
+
this.server = null;
|
|
998
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
999
|
+
}
|
|
1000
|
+
handleClient(socket) {
|
|
1001
|
+
socket.setNoDelay(true);
|
|
1002
|
+
const peerAddr = socket.remoteAddress ?? "?";
|
|
1003
|
+
const peerPort = socket.remotePort ?? 0;
|
|
1004
|
+
const session = new ClientSession(socket, `${peerAddr}:${peerPort}`);
|
|
1005
|
+
this.sessions.add(session);
|
|
1006
|
+
const splitter = new LineSplitter();
|
|
1007
|
+
void session.send({ op: "welcome" });
|
|
1008
|
+
socket.on("data", (chunk) => {
|
|
1009
|
+
for (const line of splitter.push(chunk)) {
|
|
1010
|
+
if (!line.trim()) continue;
|
|
1011
|
+
let msg;
|
|
1012
|
+
try {
|
|
1013
|
+
msg = JSON.parse(line);
|
|
1014
|
+
} catch (err) {
|
|
1015
|
+
void session.send({ op: "err", message: `bad JSON: ${String(err)}` });
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
void this.handleOp(session, msg);
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
const cleanup = () => {
|
|
1022
|
+
this.sessions.delete(session);
|
|
1023
|
+
session.close();
|
|
1024
|
+
for (const [ns, info] of [...this.namespaces]) {
|
|
1025
|
+
if (info.owner === session) this.namespaces.delete(ns);
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
socket.on("close", cleanup);
|
|
1029
|
+
socket.on("error", cleanup);
|
|
1030
|
+
}
|
|
1031
|
+
async handleOp(session, msg) {
|
|
1032
|
+
const op = msg.op;
|
|
1033
|
+
switch (op) {
|
|
1034
|
+
case "pub": {
|
|
1035
|
+
const subject = msg["subject"];
|
|
1036
|
+
const frame = msg["frame"];
|
|
1037
|
+
if (!subject || frame === void 0) {
|
|
1038
|
+
await session.send({ op: "err", message: "pub: missing subject/frame" });
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
await this.deliver(subject, frame);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
case "sub": {
|
|
1045
|
+
const subId = msg["sub_id"];
|
|
1046
|
+
const subject = msg["subject"];
|
|
1047
|
+
const queueGroup = msg["queue_group"] ?? null;
|
|
1048
|
+
if (!subId || !subject) {
|
|
1049
|
+
await session.send({ op: "err", message: "sub: missing sub_id/subject" });
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
session.subs.set(subId, { subject, queueGroup });
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
case "unsub": {
|
|
1056
|
+
const subId = msg["sub_id"];
|
|
1057
|
+
if (subId) session.subs.delete(subId);
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
case "hello":
|
|
1061
|
+
await session.send({ op: "welcome" });
|
|
1062
|
+
return;
|
|
1063
|
+
case "ping":
|
|
1064
|
+
await session.send({ op: "pong" });
|
|
1065
|
+
return;
|
|
1066
|
+
case "ns_register": {
|
|
1067
|
+
const namespace = msg["namespace"];
|
|
1068
|
+
const transport = msg["transport"] ?? "memory";
|
|
1069
|
+
if (!namespace) {
|
|
1070
|
+
await session.send({ op: "err", message: "ns_register: missing namespace" });
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
this.namespaces.set(namespace, {
|
|
1074
|
+
transport,
|
|
1075
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1076
|
+
signal_count: 0,
|
|
1077
|
+
owner: session
|
|
1078
|
+
});
|
|
1079
|
+
await session.send({ op: "ns_registered", namespace });
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
case "mgmt_list": {
|
|
1083
|
+
const nsList = [...this.namespaces.entries()].map(([namespace, info]) => ({
|
|
1084
|
+
namespace,
|
|
1085
|
+
transport: info.transport,
|
|
1086
|
+
started_at: info.started_at,
|
|
1087
|
+
signal_count: info.signal_count
|
|
1088
|
+
}));
|
|
1089
|
+
await session.send({ op: "mgmt_ns_list", namespaces: nsList });
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
case "mgmt_info": {
|
|
1093
|
+
const namespace = msg["namespace"];
|
|
1094
|
+
const info = namespace ? this.namespaces.get(namespace) : void 0;
|
|
1095
|
+
if (!namespace || !info) {
|
|
1096
|
+
await session.send({ op: "err", message: `namespace '${String(namespace)}' not found` });
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
await session.send({
|
|
1100
|
+
op: "mgmt_ns_info",
|
|
1101
|
+
namespace,
|
|
1102
|
+
transport: info.transport,
|
|
1103
|
+
started_at: info.started_at,
|
|
1104
|
+
signal_count: info.signal_count,
|
|
1105
|
+
client_count: this.sessions.size
|
|
1106
|
+
});
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
case "mgmt_stop": {
|
|
1110
|
+
const namespace = msg["namespace"];
|
|
1111
|
+
const info = namespace ? this.namespaces.get(namespace) : void 0;
|
|
1112
|
+
if (!namespace || !info) {
|
|
1113
|
+
await session.send({ op: "err", message: `namespace '${String(namespace)}' not found` });
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
this.namespaces.delete(namespace);
|
|
1117
|
+
if (info.owner.alive) await info.owner.send({ op: "ns_stopping", namespace });
|
|
1118
|
+
await session.send({ op: "mgmt_stop_ack", namespace });
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
default:
|
|
1122
|
+
await session.send({ op: "err", message: `unknown op '${String(op)}'` });
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
async deliver(subject, frame) {
|
|
1126
|
+
const parts = subject.split(".");
|
|
1127
|
+
if (parts.length >= 2 && parts[0] === "cosmonapse") {
|
|
1128
|
+
const info = this.namespaces.get(parts[1]);
|
|
1129
|
+
if (info) info.signal_count += 1;
|
|
1130
|
+
}
|
|
1131
|
+
if (this.onSignal !== null) {
|
|
1132
|
+
try {
|
|
1133
|
+
this.onSignal(subject, frame);
|
|
1134
|
+
} catch {
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
const solo = [];
|
|
1138
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1139
|
+
for (const session of this.sessions) {
|
|
1140
|
+
for (const [subId, { subject: pat, queueGroup }] of session.subs) {
|
|
1141
|
+
if (!matches(pat, subject)) continue;
|
|
1142
|
+
if (queueGroup === null) solo.push([session, subId]);
|
|
1143
|
+
else {
|
|
1144
|
+
const list = groups.get(queueGroup) ?? [];
|
|
1145
|
+
list.push([session, subId]);
|
|
1146
|
+
groups.set(queueGroup, list);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
const base = { op: "msg", subject, frame };
|
|
1151
|
+
for (const [session, subId] of solo) await session.send({ ...base, sub_id: subId });
|
|
1152
|
+
for (const [group, members] of groups) {
|
|
1153
|
+
if (!members.length) continue;
|
|
1154
|
+
const n = this.rrCounters.get(group) ?? 0;
|
|
1155
|
+
const idx = n % members.length;
|
|
1156
|
+
this.rrCounters.set(group, n + 1);
|
|
1157
|
+
const [session, subId] = members[idx];
|
|
1158
|
+
await session.send({ ...base, sub_id: subId });
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
var DevSubscription = class {
|
|
1163
|
+
constructor(synapse, subId) {
|
|
1164
|
+
this.synapse = synapse;
|
|
1165
|
+
this.subId = subId;
|
|
1166
|
+
}
|
|
1167
|
+
synapse;
|
|
1168
|
+
subId;
|
|
1169
|
+
active = true;
|
|
1170
|
+
async unsubscribe() {
|
|
1171
|
+
if (!this.active) return;
|
|
1172
|
+
this.active = false;
|
|
1173
|
+
await this.synapse._sendUnsub(this.subId);
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
var DevSynapse = class {
|
|
1177
|
+
_host;
|
|
1178
|
+
_port;
|
|
1179
|
+
socket = null;
|
|
1180
|
+
connected = false;
|
|
1181
|
+
handlers = /* @__PURE__ */ new Map();
|
|
1182
|
+
subCounter = 0;
|
|
1183
|
+
splitter = new LineSplitter();
|
|
1184
|
+
constructor(opts = {}) {
|
|
1185
|
+
let host = opts.host;
|
|
1186
|
+
let port = opts.port;
|
|
1187
|
+
if (opts.url !== void 0) {
|
|
1188
|
+
const u = new URL(opts.url);
|
|
1189
|
+
if (u.protocol !== "cosmo:") {
|
|
1190
|
+
throw new Error(`DevSynapse expects scheme cosmo://, got '${u.protocol}'`);
|
|
1191
|
+
}
|
|
1192
|
+
host = host ?? (u.hostname || "127.0.0.1");
|
|
1193
|
+
if (port === void 0) port = u.port ? Number(u.port) : 7070;
|
|
1194
|
+
}
|
|
1195
|
+
this._host = host ?? "127.0.0.1";
|
|
1196
|
+
this._port = port ?? 7070;
|
|
1197
|
+
}
|
|
1198
|
+
get url() {
|
|
1199
|
+
return `cosmo://${this._host}:${this._port}`;
|
|
1200
|
+
}
|
|
1201
|
+
async connect() {
|
|
1202
|
+
if (this.connected) return;
|
|
1203
|
+
const socket = await new Promise((resolve, reject) => {
|
|
1204
|
+
const s = (0, import_node_net.createConnection)({ host: this._host, port: this._port }, () => {
|
|
1205
|
+
s.off("error", reject);
|
|
1206
|
+
resolve(s);
|
|
1207
|
+
});
|
|
1208
|
+
s.once("error", reject);
|
|
1209
|
+
});
|
|
1210
|
+
socket.setNoDelay(true);
|
|
1211
|
+
this.socket = socket;
|
|
1212
|
+
this.connected = true;
|
|
1213
|
+
socket.on("data", (chunk) => {
|
|
1214
|
+
for (const line of this.splitter.push(chunk)) {
|
|
1215
|
+
if (!line.trim()) continue;
|
|
1216
|
+
let msg;
|
|
1217
|
+
try {
|
|
1218
|
+
msg = JSON.parse(line);
|
|
1219
|
+
} catch {
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
this.onFrame(msg);
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
socket.on("close", () => {
|
|
1226
|
+
this.connected = false;
|
|
1227
|
+
});
|
|
1228
|
+
socket.on("error", () => {
|
|
1229
|
+
this.connected = false;
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
async close() {
|
|
1233
|
+
if (!this.connected) return;
|
|
1234
|
+
this.connected = false;
|
|
1235
|
+
const socket = this.socket;
|
|
1236
|
+
this.socket = null;
|
|
1237
|
+
this.handlers.clear();
|
|
1238
|
+
if (socket) {
|
|
1239
|
+
await new Promise((resolve) => {
|
|
1240
|
+
socket.end(() => resolve());
|
|
1241
|
+
socket.destroy();
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
onFrame(msg) {
|
|
1246
|
+
const op = msg.op;
|
|
1247
|
+
if (op === "msg") {
|
|
1248
|
+
const subId = msg["sub_id"];
|
|
1249
|
+
const frame = msg["frame"];
|
|
1250
|
+
if (!subId || frame === void 0) return;
|
|
1251
|
+
const handler = this.handlers.get(subId);
|
|
1252
|
+
if (!handler) return;
|
|
1253
|
+
let signal;
|
|
1254
|
+
try {
|
|
1255
|
+
signal = decode(frame);
|
|
1256
|
+
} catch {
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
void Promise.resolve(handler(signal)).catch(() => {
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
send(payload) {
|
|
1264
|
+
if (!this.connected || this.socket === null) {
|
|
1265
|
+
return Promise.reject(new Error("DevSynapse not connected"));
|
|
1266
|
+
}
|
|
1267
|
+
const line = JSON.stringify(payload) + "\n";
|
|
1268
|
+
return new Promise((resolve, reject) => {
|
|
1269
|
+
this.socket.write(line, (err) => err ? reject(err) : resolve());
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
/** @internal - used by DevSubscription. */
|
|
1273
|
+
async _sendUnsub(subId) {
|
|
1274
|
+
this.handlers.delete(subId);
|
|
1275
|
+
if (this.connected) {
|
|
1276
|
+
try {
|
|
1277
|
+
await this.send({ op: "unsub", sub_id: subId });
|
|
1278
|
+
} catch {
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
async publish(subject, signal) {
|
|
1283
|
+
const frame = Buffer.from(encode(signal)).toString("utf-8");
|
|
1284
|
+
await this.send({ op: "pub", subject, frame });
|
|
1285
|
+
}
|
|
1286
|
+
async subscribe(subject, handler, opts = {}) {
|
|
1287
|
+
this.subCounter += 1;
|
|
1288
|
+
const subId = `s${this.subCounter}-${Date.now().toString(36)}`;
|
|
1289
|
+
this.handlers.set(subId, handler);
|
|
1290
|
+
await this.send({
|
|
1291
|
+
op: "sub",
|
|
1292
|
+
sub_id: subId,
|
|
1293
|
+
subject,
|
|
1294
|
+
queue_group: opts.queueGroup ?? null
|
|
1295
|
+
});
|
|
1296
|
+
return new DevSubscription(this, subId);
|
|
1297
|
+
}
|
|
1298
|
+
async request(subject, signal, opts = {}) {
|
|
1299
|
+
const timeoutMs = opts.timeoutMs ?? 5e3;
|
|
1300
|
+
const replySubject = `_inbox.${signal.id}`;
|
|
1301
|
+
let settled = false;
|
|
1302
|
+
let resolveFn;
|
|
1303
|
+
const fut = new Promise((resolve) => {
|
|
1304
|
+
resolveFn = resolve;
|
|
1305
|
+
});
|
|
1306
|
+
const sub = await this.subscribe(replySubject, (reply2) => {
|
|
1307
|
+
if (!settled) {
|
|
1308
|
+
settled = true;
|
|
1309
|
+
resolveFn(reply2);
|
|
1310
|
+
}
|
|
1311
|
+
});
|
|
1312
|
+
try {
|
|
1313
|
+
const enriched = {
|
|
1314
|
+
...signal,
|
|
1315
|
+
meta: { ...signal.meta, _reply_to: replySubject }
|
|
1316
|
+
};
|
|
1317
|
+
await this.publish(subject, enriched);
|
|
1318
|
+
return await new Promise((resolve, reject) => {
|
|
1319
|
+
const timer = setTimeout(() => {
|
|
1320
|
+
if (!settled) {
|
|
1321
|
+
settled = true;
|
|
1322
|
+
reject(new Error(`DevSynapse: no reply on '${replySubject}' within ${timeoutMs}ms`));
|
|
1323
|
+
}
|
|
1324
|
+
}, timeoutMs);
|
|
1325
|
+
void fut.then((s) => {
|
|
1326
|
+
clearTimeout(timer);
|
|
1327
|
+
resolve(s);
|
|
1328
|
+
});
|
|
1329
|
+
});
|
|
1330
|
+
} finally {
|
|
1331
|
+
await sub.unsubscribe();
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
/** Send `reply` to the `_reply_to` subject stored in `original.meta`. */
|
|
1335
|
+
async replyTo(original, reply2) {
|
|
1336
|
+
const replySubject = original.meta["_reply_to"];
|
|
1337
|
+
if (typeof replySubject !== "string" || !replySubject) {
|
|
1338
|
+
throw new Error("Signal has no _reply_to in meta - not a request signal");
|
|
1339
|
+
}
|
|
1340
|
+
await this.publish(replySubject, reply2);
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
|
|
1344
|
+
// src/synapse-kafka.ts
|
|
1345
|
+
function subjectToTopicRegex(pattern) {
|
|
1346
|
+
if (!pattern.includes("*") && !pattern.includes(">")) return null;
|
|
1347
|
+
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1348
|
+
const parts = pattern.split(".").map((tok) => {
|
|
1349
|
+
if (tok === "*") return "[^.]+";
|
|
1350
|
+
if (tok === ">") return ".+";
|
|
1351
|
+
return escape(tok);
|
|
1352
|
+
});
|
|
1353
|
+
return new RegExp("^" + parts.join("\\.") + "$");
|
|
1354
|
+
}
|
|
1355
|
+
var KafkaSubscription = class {
|
|
1356
|
+
constructor(consumer) {
|
|
1357
|
+
this.consumer = consumer;
|
|
1358
|
+
}
|
|
1359
|
+
consumer;
|
|
1360
|
+
active = true;
|
|
1361
|
+
async unsubscribe() {
|
|
1362
|
+
if (!this.active) return;
|
|
1363
|
+
this.active = false;
|
|
1364
|
+
try {
|
|
1365
|
+
await this.consumer.disconnect();
|
|
1366
|
+
} catch {
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
};
|
|
1370
|
+
var KafkaSynapse = class {
|
|
1371
|
+
brokers;
|
|
1372
|
+
clientId;
|
|
1373
|
+
kafka = null;
|
|
1374
|
+
producer = null;
|
|
1375
|
+
consumers = [];
|
|
1376
|
+
connected = false;
|
|
1377
|
+
constructor(opts = {}) {
|
|
1378
|
+
const bs = opts.bootstrapServers ?? "localhost:9092";
|
|
1379
|
+
this.brokers = Array.isArray(bs) ? bs : [bs];
|
|
1380
|
+
this.clientId = opts.clientId;
|
|
1381
|
+
}
|
|
1382
|
+
async connect() {
|
|
1383
|
+
if (this.connected) return;
|
|
1384
|
+
let mod;
|
|
1385
|
+
try {
|
|
1386
|
+
mod = await import("kafkajs");
|
|
1387
|
+
} catch (err) {
|
|
1388
|
+
throw new Error(
|
|
1389
|
+
"KafkaSynapse requires the 'kafkajs' package. Install it with: npm i kafkajs" + (err instanceof Error ? ` (${err.message})` : "")
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
this.kafka = new mod.Kafka({
|
|
1393
|
+
brokers: this.brokers,
|
|
1394
|
+
...this.clientId !== void 0 ? { clientId: this.clientId } : {}
|
|
1395
|
+
});
|
|
1396
|
+
this.producer = this.kafka.producer();
|
|
1397
|
+
await this.producer.connect();
|
|
1398
|
+
this.connected = true;
|
|
1399
|
+
}
|
|
1400
|
+
async close() {
|
|
1401
|
+
if (!this.connected) return;
|
|
1402
|
+
for (const c of this.consumers) {
|
|
1403
|
+
try {
|
|
1404
|
+
await c.disconnect();
|
|
1405
|
+
} catch {
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
this.consumers.length = 0;
|
|
1409
|
+
if (this.producer !== null) {
|
|
1410
|
+
await this.producer.disconnect();
|
|
1411
|
+
this.producer = null;
|
|
1412
|
+
}
|
|
1413
|
+
this.kafka = null;
|
|
1414
|
+
this.connected = false;
|
|
1415
|
+
}
|
|
1416
|
+
async publish(subject, signal) {
|
|
1417
|
+
if (!this.connected || this.producer === null) {
|
|
1418
|
+
throw new Error("KafkaSynapse.publish called before connect()");
|
|
1419
|
+
}
|
|
1420
|
+
await this.producer.send({ topic: subject, messages: [{ value: encode(signal) }] });
|
|
1421
|
+
}
|
|
1422
|
+
async subscribe(subject, handler, opts = {}) {
|
|
1423
|
+
if (!this.connected || this.kafka === null) {
|
|
1424
|
+
throw new Error("KafkaSynapse.subscribe called before connect()");
|
|
1425
|
+
}
|
|
1426
|
+
const groupId = opts.queueGroup ?? `cosmonapse-solo-${Math.random().toString(36).slice(2, 14)}`;
|
|
1427
|
+
const consumer = this.kafka.consumer({ groupId });
|
|
1428
|
+
await consumer.connect();
|
|
1429
|
+
const topicRegex = subjectToTopicRegex(subject);
|
|
1430
|
+
await consumer.subscribe(
|
|
1431
|
+
topicRegex !== null ? { topic: topicRegex, fromBeginning: false } : { topic: subject, fromBeginning: false }
|
|
1432
|
+
);
|
|
1433
|
+
await consumer.run({
|
|
1434
|
+
eachMessage: async ({ message }) => {
|
|
1435
|
+
if (message.value === null) return;
|
|
1436
|
+
let signal;
|
|
1437
|
+
try {
|
|
1438
|
+
signal = decode(message.value);
|
|
1439
|
+
} catch {
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
try {
|
|
1443
|
+
await handler(signal);
|
|
1444
|
+
} catch {
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1448
|
+
this.consumers.push(consumer);
|
|
1449
|
+
return new KafkaSubscription(consumer);
|
|
1450
|
+
}
|
|
1451
|
+
async request(subject, signal, opts = {}) {
|
|
1452
|
+
if (!this.connected) throw new Error("KafkaSynapse.request called before connect()");
|
|
1453
|
+
const timeoutMs = opts.timeoutMs ?? 5e3;
|
|
1454
|
+
const replyTopic = `_inbox.${signal.id}`;
|
|
1455
|
+
let settled = false;
|
|
1456
|
+
let resolveFn;
|
|
1457
|
+
const fut = new Promise((resolve) => {
|
|
1458
|
+
resolveFn = resolve;
|
|
1459
|
+
});
|
|
1460
|
+
const sub = await this.subscribe(replyTopic, (reply2) => {
|
|
1461
|
+
if (!settled && reply2.parent_id === signal.id) {
|
|
1462
|
+
settled = true;
|
|
1463
|
+
resolveFn(reply2);
|
|
1464
|
+
}
|
|
1465
|
+
});
|
|
1466
|
+
try {
|
|
1467
|
+
const enriched = {
|
|
1468
|
+
...signal,
|
|
1469
|
+
meta: { ...signal.meta, _reply_to: replyTopic }
|
|
1470
|
+
};
|
|
1471
|
+
await this.publish(subject, enriched);
|
|
1472
|
+
return await new Promise((resolve, reject) => {
|
|
1473
|
+
const timer = setTimeout(() => {
|
|
1474
|
+
if (!settled) {
|
|
1475
|
+
settled = true;
|
|
1476
|
+
reject(new Error(`KafkaSynapse: no reply on '${replyTopic}' within ${timeoutMs}ms`));
|
|
1477
|
+
}
|
|
1478
|
+
}, timeoutMs);
|
|
1479
|
+
void fut.then((s) => {
|
|
1480
|
+
clearTimeout(timer);
|
|
1481
|
+
resolve(s);
|
|
1482
|
+
});
|
|
1483
|
+
});
|
|
1484
|
+
} finally {
|
|
1485
|
+
await sub.unsubscribe();
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
|
|
1490
|
+
// src/url.ts
|
|
1491
|
+
function synapseFromUrl(url) {
|
|
1492
|
+
const parsed = new URL(url);
|
|
1493
|
+
const scheme = parsed.protocol.replace(/:$/, "").toLowerCase();
|
|
1494
|
+
switch (scheme) {
|
|
1495
|
+
case "cosmo":
|
|
1496
|
+
return new DevSynapse({
|
|
1497
|
+
host: parsed.hostname || "127.0.0.1",
|
|
1498
|
+
port: parsed.port ? Number(parsed.port) : 7070
|
|
1499
|
+
});
|
|
1500
|
+
case "nats":
|
|
1501
|
+
return new NatsSynapse({ url });
|
|
1502
|
+
case "kafka": {
|
|
1503
|
+
const host = parsed.hostname || "localhost";
|
|
1504
|
+
const port = parsed.port || "9092";
|
|
1505
|
+
return new KafkaSynapse({ bootstrapServers: `${host}:${port}` });
|
|
1506
|
+
}
|
|
1507
|
+
default:
|
|
1508
|
+
throw new Error(
|
|
1509
|
+
`Unknown synapse URL scheme '${scheme}'. Expected one of: cosmo, nats, kafka. For in-process MemorySynapse, instantiate it directly.`
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
async function connectSynapse(url) {
|
|
1514
|
+
const synapse = synapseFromUrl(url);
|
|
1515
|
+
await synapse.connect();
|
|
1516
|
+
return synapse;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// src/storage.ts
|
|
1520
|
+
function neuronRecord(init) {
|
|
1521
|
+
return {
|
|
1522
|
+
neuron_id: init.neuron_id,
|
|
1523
|
+
capabilities: init.capabilities ?? [],
|
|
1524
|
+
version: init.version ?? null,
|
|
1525
|
+
status: init.status ?? "registered",
|
|
1526
|
+
last_heartbeat: init.last_heartbeat ?? null,
|
|
1527
|
+
registered_at: init.registered_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
var MemoryRegistryStore = class {
|
|
1531
|
+
records = /* @__PURE__ */ new Map();
|
|
1532
|
+
async connect() {
|
|
1533
|
+
}
|
|
1534
|
+
async close() {
|
|
1535
|
+
this.records.clear();
|
|
1536
|
+
}
|
|
1537
|
+
async upsert(record) {
|
|
1538
|
+
const existing = this.records.get(record.neuron_id);
|
|
1539
|
+
const merged = existing ? { ...record, registered_at: existing.registered_at } : { ...record };
|
|
1540
|
+
this.records.set(record.neuron_id, merged);
|
|
1541
|
+
}
|
|
1542
|
+
async markDeregistered(neuronId) {
|
|
1543
|
+
const rec = this.records.get(neuronId);
|
|
1544
|
+
if (rec) rec.status = "deregistered";
|
|
1545
|
+
}
|
|
1546
|
+
async touchHeartbeat(neuronId, ts, status) {
|
|
1547
|
+
const rec = this.records.get(neuronId);
|
|
1548
|
+
if (!rec) {
|
|
1549
|
+
this.records.set(
|
|
1550
|
+
neuronId,
|
|
1551
|
+
neuronRecord({
|
|
1552
|
+
neuron_id: neuronId,
|
|
1553
|
+
last_heartbeat: ts,
|
|
1554
|
+
...status ? { status } : {}
|
|
1555
|
+
})
|
|
1556
|
+
);
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
rec.last_heartbeat = ts;
|
|
1560
|
+
if (status) rec.status = status;
|
|
1561
|
+
}
|
|
1562
|
+
async get(neuronId) {
|
|
1563
|
+
return this.records.get(neuronId) ?? null;
|
|
1564
|
+
}
|
|
1565
|
+
async list(opts = {}) {
|
|
1566
|
+
const out = [];
|
|
1567
|
+
for (const rec of this.records.values()) {
|
|
1568
|
+
if (!opts.includeDeregistered && rec.status === "deregistered") continue;
|
|
1569
|
+
if (opts.capability !== void 0 && !rec.capabilities.includes(opts.capability)) continue;
|
|
1570
|
+
out.push(rec);
|
|
1571
|
+
}
|
|
1572
|
+
return out;
|
|
1573
|
+
}
|
|
1574
|
+
};
|
|
1575
|
+
|
|
1576
|
+
// src/storage-sqlite.ts
|
|
1577
|
+
var SCHEMA = `
|
|
1578
|
+
CREATE TABLE IF NOT EXISTS neurons (
|
|
1579
|
+
neuron_id TEXT PRIMARY KEY,
|
|
1580
|
+
capabilities TEXT NOT NULL DEFAULT '[]',
|
|
1581
|
+
version TEXT,
|
|
1582
|
+
status TEXT NOT NULL DEFAULT 'registered',
|
|
1583
|
+
last_heartbeat TEXT,
|
|
1584
|
+
registered_at TEXT NOT NULL
|
|
1585
|
+
);
|
|
1586
|
+
`;
|
|
1587
|
+
function recordFromRow(row) {
|
|
1588
|
+
return {
|
|
1589
|
+
neuron_id: row.neuron_id,
|
|
1590
|
+
capabilities: row.capabilities ? JSON.parse(row.capabilities) : [],
|
|
1591
|
+
version: row.version ?? null,
|
|
1592
|
+
status: row.status ?? "registered",
|
|
1593
|
+
last_heartbeat: row.last_heartbeat ?? null,
|
|
1594
|
+
registered_at: row.registered_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
var SqliteRegistryStore = class {
|
|
1598
|
+
constructor(path = ":memory:") {
|
|
1599
|
+
this.path = path;
|
|
1600
|
+
}
|
|
1601
|
+
path;
|
|
1602
|
+
db = null;
|
|
1603
|
+
async connect() {
|
|
1604
|
+
if (this.db !== null) return;
|
|
1605
|
+
let mod;
|
|
1606
|
+
const specifier = "better-sqlite3";
|
|
1607
|
+
try {
|
|
1608
|
+
mod = await import(specifier);
|
|
1609
|
+
} catch (err) {
|
|
1610
|
+
throw new Error(
|
|
1611
|
+
"SqliteRegistryStore requires the 'better-sqlite3' package. Install it with: npm i better-sqlite3" + (err instanceof Error ? ` (${err.message})` : "")
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
const Database = mod.default;
|
|
1615
|
+
this.db = new Database(this.path);
|
|
1616
|
+
this.db.exec(SCHEMA);
|
|
1617
|
+
}
|
|
1618
|
+
async close() {
|
|
1619
|
+
if (this.db === null) return;
|
|
1620
|
+
this.db.close();
|
|
1621
|
+
this.db = null;
|
|
1622
|
+
}
|
|
1623
|
+
require() {
|
|
1624
|
+
if (this.db === null) throw new Error("SqliteRegistryStore.connect() not called");
|
|
1625
|
+
return this.db;
|
|
1626
|
+
}
|
|
1627
|
+
async upsert(record) {
|
|
1628
|
+
const db = this.require();
|
|
1629
|
+
const existing = db.prepare("SELECT registered_at FROM neurons WHERE neuron_id = ?").get(record.neuron_id);
|
|
1630
|
+
const registeredAt = existing ? existing.registered_at : record.registered_at;
|
|
1631
|
+
db.prepare(
|
|
1632
|
+
`INSERT INTO neurons
|
|
1633
|
+
(neuron_id, capabilities, version, status, last_heartbeat, registered_at)
|
|
1634
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1635
|
+
ON CONFLICT(neuron_id) DO UPDATE SET
|
|
1636
|
+
capabilities = excluded.capabilities,
|
|
1637
|
+
version = excluded.version,
|
|
1638
|
+
status = excluded.status,
|
|
1639
|
+
last_heartbeat = excluded.last_heartbeat`
|
|
1640
|
+
).run(
|
|
1641
|
+
record.neuron_id,
|
|
1642
|
+
JSON.stringify(record.capabilities),
|
|
1643
|
+
record.version,
|
|
1644
|
+
record.status,
|
|
1645
|
+
record.last_heartbeat,
|
|
1646
|
+
registeredAt
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
async markDeregistered(neuronId) {
|
|
1650
|
+
this.require().prepare("UPDATE neurons SET status = 'deregistered' WHERE neuron_id = ?").run(neuronId);
|
|
1651
|
+
}
|
|
1652
|
+
async touchHeartbeat(neuronId, ts, status) {
|
|
1653
|
+
const db = this.require();
|
|
1654
|
+
const existing = db.prepare("SELECT neuron_id FROM neurons WHERE neuron_id = ?").get(neuronId);
|
|
1655
|
+
if (!existing) {
|
|
1656
|
+
db.prepare(
|
|
1657
|
+
`INSERT INTO neurons
|
|
1658
|
+
(neuron_id, capabilities, version, status, last_heartbeat, registered_at)
|
|
1659
|
+
VALUES (?, '[]', NULL, ?, ?, ?)`
|
|
1660
|
+
).run(neuronId, status ?? "registered", ts, (/* @__PURE__ */ new Date()).toISOString());
|
|
1661
|
+
} else if (status !== void 0) {
|
|
1662
|
+
db.prepare(
|
|
1663
|
+
"UPDATE neurons SET last_heartbeat = ?, status = ? WHERE neuron_id = ?"
|
|
1664
|
+
).run(ts, status, neuronId);
|
|
1665
|
+
} else {
|
|
1666
|
+
db.prepare("UPDATE neurons SET last_heartbeat = ? WHERE neuron_id = ?").run(ts, neuronId);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
async get(neuronId) {
|
|
1670
|
+
const row = this.require().prepare(
|
|
1671
|
+
"SELECT neuron_id, capabilities, version, status, last_heartbeat, registered_at FROM neurons WHERE neuron_id = ?"
|
|
1672
|
+
).get(neuronId);
|
|
1673
|
+
return row ? recordFromRow(row) : null;
|
|
1674
|
+
}
|
|
1675
|
+
async list(opts = {}) {
|
|
1676
|
+
let sql = "SELECT neuron_id, capabilities, version, status, last_heartbeat, registered_at FROM neurons";
|
|
1677
|
+
if (!opts.includeDeregistered) sql += " WHERE status != 'deregistered'";
|
|
1678
|
+
const rows = this.require().prepare(sql).all();
|
|
1679
|
+
let out = rows.map(recordFromRow);
|
|
1680
|
+
if (opts.capability !== void 0) {
|
|
1681
|
+
out = out.filter((r) => r.capabilities.includes(opts.capability));
|
|
1682
|
+
}
|
|
1683
|
+
return out;
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
|
|
1687
|
+
// src/storage-postgres.ts
|
|
1688
|
+
var SCHEMA2 = `
|
|
1689
|
+
CREATE TABLE IF NOT EXISTS cosmonapse_neurons (
|
|
1690
|
+
neuron_id TEXT PRIMARY KEY,
|
|
1691
|
+
capabilities JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
1692
|
+
version TEXT,
|
|
1693
|
+
status TEXT NOT NULL DEFAULT 'registered',
|
|
1694
|
+
last_heartbeat TIMESTAMPTZ,
|
|
1695
|
+
registered_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
1696
|
+
);
|
|
1697
|
+
CREATE INDEX IF NOT EXISTS cosmonapse_neurons_status_idx
|
|
1698
|
+
ON cosmonapse_neurons (status);
|
|
1699
|
+
`;
|
|
1700
|
+
function toIso(value) {
|
|
1701
|
+
if (value === null) return null;
|
|
1702
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
1703
|
+
}
|
|
1704
|
+
function recordFromRow2(row) {
|
|
1705
|
+
let caps = row.capabilities;
|
|
1706
|
+
if (typeof caps === "string") caps = JSON.parse(caps);
|
|
1707
|
+
return {
|
|
1708
|
+
neuron_id: row.neuron_id,
|
|
1709
|
+
capabilities: Array.isArray(caps) ? caps : [],
|
|
1710
|
+
version: row.version ?? null,
|
|
1711
|
+
status: row.status ?? "registered",
|
|
1712
|
+
last_heartbeat: toIso(row.last_heartbeat),
|
|
1713
|
+
registered_at: toIso(row.registered_at) ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
var PostgresRegistryStore = class {
|
|
1717
|
+
dsn;
|
|
1718
|
+
minSize;
|
|
1719
|
+
maxSize;
|
|
1720
|
+
pool = null;
|
|
1721
|
+
constructor(opts) {
|
|
1722
|
+
this.dsn = opts.dsn;
|
|
1723
|
+
this.minSize = opts.minSize ?? 1;
|
|
1724
|
+
this.maxSize = opts.maxSize ?? 5;
|
|
1725
|
+
}
|
|
1726
|
+
async connect() {
|
|
1727
|
+
if (this.pool !== null) return;
|
|
1728
|
+
let mod;
|
|
1729
|
+
const specifier = "pg";
|
|
1730
|
+
try {
|
|
1731
|
+
mod = await import(specifier);
|
|
1732
|
+
} catch (err) {
|
|
1733
|
+
throw new Error(
|
|
1734
|
+
"PostgresRegistryStore requires the 'pg' package. Install it with: npm i pg" + (err instanceof Error ? ` (${err.message})` : "")
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
this.pool = new mod.Pool({
|
|
1738
|
+
connectionString: this.dsn,
|
|
1739
|
+
min: this.minSize,
|
|
1740
|
+
max: this.maxSize
|
|
1741
|
+
});
|
|
1742
|
+
await this.pool.query(SCHEMA2);
|
|
1743
|
+
}
|
|
1744
|
+
async close() {
|
|
1745
|
+
if (this.pool === null) return;
|
|
1746
|
+
await this.pool.end();
|
|
1747
|
+
this.pool = null;
|
|
1748
|
+
}
|
|
1749
|
+
require() {
|
|
1750
|
+
if (this.pool === null) throw new Error("PostgresRegistryStore.connect() not called");
|
|
1751
|
+
return this.pool;
|
|
1752
|
+
}
|
|
1753
|
+
async upsert(record) {
|
|
1754
|
+
await this.require().query(
|
|
1755
|
+
`INSERT INTO cosmonapse_neurons
|
|
1756
|
+
(neuron_id, capabilities, version, status, last_heartbeat, registered_at)
|
|
1757
|
+
VALUES ($1, $2::jsonb, $3, $4, $5, $6)
|
|
1758
|
+
ON CONFLICT (neuron_id) DO UPDATE SET
|
|
1759
|
+
capabilities = EXCLUDED.capabilities,
|
|
1760
|
+
version = EXCLUDED.version,
|
|
1761
|
+
status = EXCLUDED.status,
|
|
1762
|
+
last_heartbeat = EXCLUDED.last_heartbeat`,
|
|
1763
|
+
[
|
|
1764
|
+
record.neuron_id,
|
|
1765
|
+
JSON.stringify(record.capabilities),
|
|
1766
|
+
record.version,
|
|
1767
|
+
record.status,
|
|
1768
|
+
record.last_heartbeat,
|
|
1769
|
+
record.registered_at
|
|
1770
|
+
]
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
async markDeregistered(neuronId) {
|
|
1774
|
+
await this.require().query(
|
|
1775
|
+
"UPDATE cosmonapse_neurons SET status = 'deregistered' WHERE neuron_id = $1",
|
|
1776
|
+
[neuronId]
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
async touchHeartbeat(neuronId, ts, status) {
|
|
1780
|
+
const pool = this.require();
|
|
1781
|
+
if (status !== void 0) {
|
|
1782
|
+
await pool.query(
|
|
1783
|
+
`INSERT INTO cosmonapse_neurons (neuron_id, last_heartbeat, status)
|
|
1784
|
+
VALUES ($1, $2, $3)
|
|
1785
|
+
ON CONFLICT (neuron_id) DO UPDATE SET
|
|
1786
|
+
last_heartbeat = EXCLUDED.last_heartbeat,
|
|
1787
|
+
status = EXCLUDED.status`,
|
|
1788
|
+
[neuronId, ts, status]
|
|
1789
|
+
);
|
|
1790
|
+
} else {
|
|
1791
|
+
await pool.query(
|
|
1792
|
+
`INSERT INTO cosmonapse_neurons (neuron_id, last_heartbeat)
|
|
1793
|
+
VALUES ($1, $2)
|
|
1794
|
+
ON CONFLICT (neuron_id) DO UPDATE SET
|
|
1795
|
+
last_heartbeat = EXCLUDED.last_heartbeat`,
|
|
1796
|
+
[neuronId, ts]
|
|
1797
|
+
);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
async get(neuronId) {
|
|
1801
|
+
const res = await this.require().query(
|
|
1802
|
+
"SELECT neuron_id, capabilities, version, status, last_heartbeat, registered_at FROM cosmonapse_neurons WHERE neuron_id = $1",
|
|
1803
|
+
[neuronId]
|
|
1804
|
+
);
|
|
1805
|
+
const row = res.rows[0];
|
|
1806
|
+
return row ? recordFromRow2(row) : null;
|
|
1807
|
+
}
|
|
1808
|
+
async list(opts = {}) {
|
|
1809
|
+
const clauses = [];
|
|
1810
|
+
const params = [];
|
|
1811
|
+
if (!opts.includeDeregistered) clauses.push("status <> 'deregistered'");
|
|
1812
|
+
if (opts.capability !== void 0) {
|
|
1813
|
+
params.push(opts.capability);
|
|
1814
|
+
clauses.push(`capabilities @> to_jsonb($${params.length}::text)`);
|
|
1815
|
+
}
|
|
1816
|
+
let sql = "SELECT neuron_id, capabilities, version, status, last_heartbeat, registered_at FROM cosmonapse_neurons";
|
|
1817
|
+
if (clauses.length) sql += " WHERE " + clauses.join(" AND ");
|
|
1818
|
+
const res = await this.require().query(sql, params);
|
|
1819
|
+
return res.rows.map(recordFromRow2);
|
|
1820
|
+
}
|
|
1821
|
+
};
|
|
1822
|
+
|
|
1823
|
+
// src/hooks.ts
|
|
1824
|
+
var LifecycleHooks = class {
|
|
1825
|
+
constructor(owner) {
|
|
1826
|
+
this.owner = owner;
|
|
1827
|
+
}
|
|
1828
|
+
owner;
|
|
1829
|
+
connectHooks = [];
|
|
1830
|
+
refreshHooks = [];
|
|
1831
|
+
scheduleHooks = [];
|
|
1832
|
+
timers = /* @__PURE__ */ new Set();
|
|
1833
|
+
started = false;
|
|
1834
|
+
// -- decorators / registration ------------------------------------
|
|
1835
|
+
/** Register a fire-once handler called after the host finishes start(). */
|
|
1836
|
+
onConnect(fn) {
|
|
1837
|
+
this.connectHooks.push(fn);
|
|
1838
|
+
return fn;
|
|
1839
|
+
}
|
|
1840
|
+
/** Register a handler called whenever the host's state refreshes. */
|
|
1841
|
+
onRefresh(fn) {
|
|
1842
|
+
this.refreshHooks.push(fn);
|
|
1843
|
+
return fn;
|
|
1844
|
+
}
|
|
1845
|
+
/**
|
|
1846
|
+
* Register a periodic handler. The background loop runs every `everyMs`
|
|
1847
|
+
* for the lifetime of the host. If the host is already running, the loop
|
|
1848
|
+
* starts immediately.
|
|
1849
|
+
*/
|
|
1850
|
+
onSchedule(everyMs, fn) {
|
|
1851
|
+
if (everyMs <= 0) throw new Error("onSchedule requires everyMs > 0");
|
|
1852
|
+
this.scheduleHooks.push([everyMs, fn]);
|
|
1853
|
+
if (this.started) this.spawnLoop(everyMs, fn);
|
|
1854
|
+
return fn;
|
|
1855
|
+
}
|
|
1856
|
+
// -- driven by the host component ---------------------------------
|
|
1857
|
+
/** @internal */
|
|
1858
|
+
async _fireConnect() {
|
|
1859
|
+
for (const h of [...this.connectHooks]) {
|
|
1860
|
+
try {
|
|
1861
|
+
await h(this.owner);
|
|
1862
|
+
} catch {
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
/** @internal */
|
|
1867
|
+
async _fireRefresh(event) {
|
|
1868
|
+
for (const h of [...this.refreshHooks]) {
|
|
1869
|
+
try {
|
|
1870
|
+
await h(this.owner, event);
|
|
1871
|
+
} catch {
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
/** @internal */
|
|
1876
|
+
_launchSchedule() {
|
|
1877
|
+
if (this.started) return;
|
|
1878
|
+
for (const [interval, fn] of this.scheduleHooks) this.spawnLoop(interval, fn);
|
|
1879
|
+
this.started = true;
|
|
1880
|
+
}
|
|
1881
|
+
/** @internal */
|
|
1882
|
+
_stopHooks() {
|
|
1883
|
+
for (const t of this.timers) clearTimeout(t);
|
|
1884
|
+
this.timers.clear();
|
|
1885
|
+
this.started = false;
|
|
1886
|
+
}
|
|
1887
|
+
/**
|
|
1888
|
+
* Manually fire a refresh event. Useful when a developer's own code knows
|
|
1889
|
+
* internal state has changed.
|
|
1890
|
+
*/
|
|
1891
|
+
async refresh(opts = {}) {
|
|
1892
|
+
await this._fireRefresh({
|
|
1893
|
+
reason: opts.reason ?? "manual",
|
|
1894
|
+
neuronId: opts.neuronId ?? null,
|
|
1895
|
+
extra: opts.extra ?? {}
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
// -- internal -----------------------------------------------------
|
|
1899
|
+
spawnLoop(interval, fn) {
|
|
1900
|
+
const schedule = () => {
|
|
1901
|
+
const timer = setTimeout(() => {
|
|
1902
|
+
this.timers.delete(timer);
|
|
1903
|
+
void tick();
|
|
1904
|
+
}, interval);
|
|
1905
|
+
timer.unref?.();
|
|
1906
|
+
this.timers.add(timer);
|
|
1907
|
+
};
|
|
1908
|
+
const tick = async () => {
|
|
1909
|
+
try {
|
|
1910
|
+
await fn(this.owner);
|
|
1911
|
+
} catch {
|
|
1912
|
+
}
|
|
1913
|
+
if (this.started) schedule();
|
|
1914
|
+
};
|
|
1915
|
+
schedule();
|
|
1916
|
+
}
|
|
1917
|
+
};
|
|
1918
|
+
|
|
1919
|
+
// src/neuron.ts
|
|
1920
|
+
function clarify(question, context) {
|
|
1921
|
+
return context === void 0 ? { __clarification__: true, question } : { __clarification__: true, question, context };
|
|
1922
|
+
}
|
|
1923
|
+
function isClarification(output) {
|
|
1924
|
+
return typeof output === "object" && output !== null && output["__clarification__"] === true;
|
|
1925
|
+
}
|
|
1926
|
+
function permissionRequest(action, opts = {}) {
|
|
1927
|
+
return {
|
|
1928
|
+
__permission__: true,
|
|
1929
|
+
action,
|
|
1930
|
+
...opts.scope !== void 0 ? { scope: opts.scope } : {},
|
|
1931
|
+
...opts.reason !== void 0 ? { reason: opts.reason } : {},
|
|
1932
|
+
...opts.context !== void 0 ? { context: opts.context } : {}
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
function isPermissionRequest(output) {
|
|
1936
|
+
return typeof output === "object" && output !== null && output["__permission__"] === true;
|
|
1937
|
+
}
|
|
1938
|
+
function errorResult(message, opts = {}) {
|
|
1939
|
+
return {
|
|
1940
|
+
__error__: true,
|
|
1941
|
+
message,
|
|
1942
|
+
code: opts.code ?? "NEURON_ERROR",
|
|
1943
|
+
recoverable: opts.recoverable ?? false
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
function isErrorOutput(output) {
|
|
1947
|
+
return typeof output === "object" && output !== null && output["__error__"] === true;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
// src/neuron-mcp.ts
|
|
1951
|
+
var standardMcpServers = {
|
|
1952
|
+
filesystem: {
|
|
1953
|
+
command: "npx",
|
|
1954
|
+
args: ["-y", "@modelcontextprotocol/server-filesystem"],
|
|
1955
|
+
note: "Append one or more allowed directories, e.g. args=['/data']."
|
|
1956
|
+
},
|
|
1957
|
+
memory: {
|
|
1958
|
+
command: "npx",
|
|
1959
|
+
args: ["-y", "@modelcontextprotocol/server-memory"],
|
|
1960
|
+
note: "Knowledge-graph memory store."
|
|
1961
|
+
},
|
|
1962
|
+
everything: {
|
|
1963
|
+
command: "npx",
|
|
1964
|
+
args: ["-y", "@modelcontextprotocol/server-everything"],
|
|
1965
|
+
note: "Reference server exercising every MCP feature; handy for tests."
|
|
1966
|
+
},
|
|
1967
|
+
sequentialthinking: {
|
|
1968
|
+
command: "npx",
|
|
1969
|
+
args: ["-y", "@modelcontextprotocol/server-sequential-thinking"],
|
|
1970
|
+
note: "Structured step-by-step reasoning tool."
|
|
1971
|
+
},
|
|
1972
|
+
fetch: {
|
|
1973
|
+
command: "uvx",
|
|
1974
|
+
args: ["mcp-server-fetch"],
|
|
1975
|
+
note: "Fetch a URL and return its content as markdown/text."
|
|
1976
|
+
},
|
|
1977
|
+
git: {
|
|
1978
|
+
command: "uvx",
|
|
1979
|
+
args: ["mcp-server-git"],
|
|
1980
|
+
note: "Read/inspect a git repo. Append --repository <path>."
|
|
1981
|
+
},
|
|
1982
|
+
time: {
|
|
1983
|
+
command: "uvx",
|
|
1984
|
+
args: ["mcp-server-time"],
|
|
1985
|
+
note: "Current time and timezone conversions."
|
|
1986
|
+
}
|
|
1987
|
+
};
|
|
1988
|
+
var CONTROL_KEYS = /* @__PURE__ */ new Set(["tool", "arguments", "args", "__list_tools__"]);
|
|
1989
|
+
function resolveLaunch(opts) {
|
|
1990
|
+
const extra = opts.args ?? [];
|
|
1991
|
+
if (opts.server != null) {
|
|
1992
|
+
const preset = standardMcpServers[opts.server];
|
|
1993
|
+
if (!preset) {
|
|
1994
|
+
const available = Object.keys(standardMcpServers).sort().join(", ");
|
|
1995
|
+
throw new Error(
|
|
1996
|
+
`Unknown MCP server preset '${opts.server}'. Available: ${available}. (Or pass command/args to wrap any other stdio MCP server.)`
|
|
1997
|
+
);
|
|
1998
|
+
}
|
|
1999
|
+
return { command: opts.command ?? preset.command, args: [...preset.args, ...extra] };
|
|
2000
|
+
}
|
|
2001
|
+
if (opts.command == null) {
|
|
2002
|
+
throw new Error("mcpNeuron(...) needs either `command` (+optional `args`) or a `server` preset name.");
|
|
2003
|
+
}
|
|
2004
|
+
return { command: opts.command, args: extra };
|
|
2005
|
+
}
|
|
2006
|
+
function mcpNeuron(opts) {
|
|
2007
|
+
const { command, args } = resolveLaunch(opts);
|
|
2008
|
+
let client = null;
|
|
2009
|
+
let connecting = null;
|
|
2010
|
+
async function ensure() {
|
|
2011
|
+
if (client) return client;
|
|
2012
|
+
if (connecting) return connecting;
|
|
2013
|
+
connecting = (async () => {
|
|
2014
|
+
const clientSpec = "@modelcontextprotocol/sdk/client/index.js";
|
|
2015
|
+
const stdioSpec = "@modelcontextprotocol/sdk/client/stdio.js";
|
|
2016
|
+
const clientMod = await import(clientSpec);
|
|
2017
|
+
const stdioMod = await import(stdioSpec);
|
|
2018
|
+
const Client = clientMod.Client;
|
|
2019
|
+
const StdioClientTransport = stdioMod.StdioClientTransport;
|
|
2020
|
+
const transport = new StdioClientTransport({
|
|
2021
|
+
command,
|
|
2022
|
+
args,
|
|
2023
|
+
...opts.env ? { env: opts.env } : {},
|
|
2024
|
+
...opts.cwd ? { cwd: opts.cwd } : {}
|
|
2025
|
+
});
|
|
2026
|
+
const c = new Client(
|
|
2027
|
+
{ name: opts.clientName ?? "cosmonapse", version: opts.clientVersion ?? "0.2.0" },
|
|
2028
|
+
{ capabilities: {} }
|
|
2029
|
+
);
|
|
2030
|
+
await c.connect(transport);
|
|
2031
|
+
client = c;
|
|
2032
|
+
return c;
|
|
2033
|
+
})();
|
|
2034
|
+
return connecting;
|
|
2035
|
+
}
|
|
2036
|
+
const fn = (async (input, _context) => {
|
|
2037
|
+
const c = await ensure();
|
|
2038
|
+
const inp = input ?? {};
|
|
2039
|
+
if (inp.__list_tools__) {
|
|
2040
|
+
const res2 = await c.listTools();
|
|
2041
|
+
return {
|
|
2042
|
+
tools: (res2.tools ?? []).map((t) => ({
|
|
2043
|
+
name: t.name,
|
|
2044
|
+
description: t.description ?? null,
|
|
2045
|
+
input_schema: t.inputSchema ?? null
|
|
2046
|
+
}))
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
let tool = inp.tool ?? opts.tool;
|
|
2050
|
+
let toolArgs = inp.arguments ?? inp.args;
|
|
2051
|
+
if (toolArgs == null) {
|
|
2052
|
+
toolArgs = {};
|
|
2053
|
+
for (const [k, v] of Object.entries(inp)) {
|
|
2054
|
+
if (!CONTROL_KEYS.has(k)) toolArgs[k] = v;
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
if (!tool) {
|
|
2058
|
+
const res2 = await c.listTools();
|
|
2059
|
+
const names = (res2.tools ?? []).map((t) => t.name);
|
|
2060
|
+
if (names.length === 1) {
|
|
2061
|
+
tool = names[0];
|
|
2062
|
+
} else {
|
|
2063
|
+
throw new Error(
|
|
2064
|
+
`MCP Neuron could not determine which tool to call. Pass tool=... (server exposes: ${JSON.stringify(names)}).`
|
|
2065
|
+
);
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
const res = await c.callTool({ name: tool, arguments: toolArgs });
|
|
2069
|
+
const content = res.content ?? [];
|
|
2070
|
+
const texts = content.filter((x) => x?.text != null).map((x) => x.text);
|
|
2071
|
+
return {
|
|
2072
|
+
response: texts.join("\n"),
|
|
2073
|
+
result: res.structuredContent ?? null,
|
|
2074
|
+
is_error: Boolean(res.isError),
|
|
2075
|
+
content,
|
|
2076
|
+
meta: { tool, server: opts.server ?? null, command }
|
|
2077
|
+
};
|
|
2078
|
+
});
|
|
2079
|
+
fn.close = async () => {
|
|
2080
|
+
const c = client;
|
|
2081
|
+
client = null;
|
|
2082
|
+
connecting = null;
|
|
2083
|
+
if (c) {
|
|
2084
|
+
try {
|
|
2085
|
+
await c.close();
|
|
2086
|
+
} catch {
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
};
|
|
2090
|
+
return fn;
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
// src/neuron-http.ts
|
|
2094
|
+
function readPrompt(input) {
|
|
2095
|
+
const v = input["prompt"] ?? input["text"] ?? input["query"] ?? input["content"];
|
|
2096
|
+
return typeof v === "string" && v ? v : null;
|
|
2097
|
+
}
|
|
2098
|
+
function readMessages(input) {
|
|
2099
|
+
const m = input["messages"];
|
|
2100
|
+
return Array.isArray(m) ? m : null;
|
|
2101
|
+
}
|
|
2102
|
+
function requireInput(input, provider) {
|
|
2103
|
+
const prompt = readPrompt(input);
|
|
2104
|
+
const messages = readMessages(input);
|
|
2105
|
+
if (!prompt && !messages) {
|
|
2106
|
+
throw new Error(
|
|
2107
|
+
`${provider} Neuron expects 'prompt' or 'messages' in the input dict. Got keys: ${Object.keys(input).join(", ")}`
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2110
|
+
return { prompt, messages };
|
|
2111
|
+
}
|
|
2112
|
+
async function postJson(url, body, headers, timeoutMs) {
|
|
2113
|
+
const ctrl = new AbortController();
|
|
2114
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
2115
|
+
try {
|
|
2116
|
+
const res = await fetch(url, {
|
|
2117
|
+
method: "POST",
|
|
2118
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
2119
|
+
body: JSON.stringify(body),
|
|
2120
|
+
signal: ctrl.signal
|
|
2121
|
+
});
|
|
2122
|
+
if (!res.ok) {
|
|
2123
|
+
const text = await res.text().catch(() => "");
|
|
2124
|
+
throw new Error(`HTTP ${res.status} from ${url}: ${text.slice(0, 200)}`);
|
|
2125
|
+
}
|
|
2126
|
+
return await res.json();
|
|
2127
|
+
} finally {
|
|
2128
|
+
clearTimeout(timer);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
function ollamaNeuron(opts) {
|
|
2132
|
+
const endpoint = (opts.endpoint ?? "http://localhost:11434").replace(/\/+$/, "");
|
|
2133
|
+
const timeoutMs = opts.timeoutMs ?? 12e4;
|
|
2134
|
+
const options = () => {
|
|
2135
|
+
const o = {};
|
|
2136
|
+
if (opts.temperature !== void 0) o["temperature"] = opts.temperature;
|
|
2137
|
+
if (opts.maxTokens !== void 0) o["num_predict"] = opts.maxTokens;
|
|
2138
|
+
return o;
|
|
2139
|
+
};
|
|
2140
|
+
return async (input) => {
|
|
2141
|
+
const inp = input ?? {};
|
|
2142
|
+
const { prompt, messages } = requireInput(inp, "Ollama");
|
|
2143
|
+
const opt = options();
|
|
2144
|
+
if (messages !== null) {
|
|
2145
|
+
const all = opts.system ? [{ role: "system", content: opts.system }, ...messages] : messages;
|
|
2146
|
+
const body2 = { model: opts.model, messages: all, stream: false };
|
|
2147
|
+
if (Object.keys(opt).length) body2["options"] = opt;
|
|
2148
|
+
const data2 = await postJson(`${endpoint}/api/chat`, body2, {}, timeoutMs);
|
|
2149
|
+
const message = data2["message"] ?? {};
|
|
2150
|
+
return { response: message["content"] ?? "", meta: data2 };
|
|
2151
|
+
}
|
|
2152
|
+
const body = { model: opts.model, prompt: prompt ?? "", stream: false };
|
|
2153
|
+
if (opts.system) body["system"] = opts.system;
|
|
2154
|
+
if (Object.keys(opt).length) body["options"] = opt;
|
|
2155
|
+
const data = await postJson(`${endpoint}/api/generate`, body, {}, timeoutMs);
|
|
2156
|
+
return { response: data["response"] ?? "", meta: data };
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
function huggingFaceNeuron(opts) {
|
|
2160
|
+
const endpoint = opts.endpoint.replace(/\/+$/, "");
|
|
2161
|
+
const maxNewTokens = opts.maxNewTokens ?? 512;
|
|
2162
|
+
const timeoutMs = opts.timeoutMs ?? 12e4;
|
|
2163
|
+
const headers = {};
|
|
2164
|
+
if (opts.apiKey) headers["Authorization"] = `Bearer ${opts.apiKey}`;
|
|
2165
|
+
return async (input) => {
|
|
2166
|
+
const inp = input ?? {};
|
|
2167
|
+
const { prompt, messages } = requireInput(inp, "HuggingFace");
|
|
2168
|
+
if (messages !== null || opts.useChatApi) {
|
|
2169
|
+
const msgs = messages ?? [{ role: "user", content: prompt ?? "" }];
|
|
2170
|
+
const body2 = { messages: msgs, max_tokens: maxNewTokens };
|
|
2171
|
+
if (opts.model) body2["model"] = opts.model;
|
|
2172
|
+
if (opts.temperature !== void 0) body2["temperature"] = opts.temperature;
|
|
2173
|
+
const data2 = await postJson(`${endpoint}/v1/chat/completions`, body2, headers, timeoutMs);
|
|
2174
|
+
const choices = data2["choices"] ?? [];
|
|
2175
|
+
const message = choices[0]?.["message"] ?? {};
|
|
2176
|
+
return { response: message["content"] ?? "", meta: data2 };
|
|
2177
|
+
}
|
|
2178
|
+
const params = { max_new_tokens: maxNewTokens };
|
|
2179
|
+
if (opts.temperature !== void 0) params["temperature"] = opts.temperature;
|
|
2180
|
+
const body = { inputs: prompt ?? "", parameters: params };
|
|
2181
|
+
const data = await postJson(`${endpoint}/generate`, body, headers, timeoutMs);
|
|
2182
|
+
let text = "";
|
|
2183
|
+
if (Array.isArray(data)) {
|
|
2184
|
+
text = data[0]?.["generated_text"] ?? "";
|
|
2185
|
+
} else {
|
|
2186
|
+
text = data["generated_text"] ?? "";
|
|
2187
|
+
}
|
|
2188
|
+
return { response: text, meta: data };
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
// src/neuron-openai.ts
|
|
2193
|
+
function openaiNeuron(opts) {
|
|
2194
|
+
const key = opts.apiKey ?? process.env["OPENAI_API_KEY"];
|
|
2195
|
+
if (!key) {
|
|
2196
|
+
throw new Error(
|
|
2197
|
+
"OpenAI Neuron requires an API key. Pass apiKey=... or set the OPENAI_API_KEY environment variable."
|
|
2198
|
+
);
|
|
2199
|
+
}
|
|
2200
|
+
const endpoint = (opts.endpoint ?? "https://api.openai.com/v1").replace(/\/+$/, "");
|
|
2201
|
+
const timeoutMs = opts.timeoutMs ?? 12e4;
|
|
2202
|
+
const headers = { Authorization: `Bearer ${key}` };
|
|
2203
|
+
return async (input) => {
|
|
2204
|
+
const inp = input ?? {};
|
|
2205
|
+
const { prompt, messages } = requireInput(inp, "OpenAI");
|
|
2206
|
+
let msgs = messages !== null ? [...messages] : [{ role: "user", content: prompt ?? "" }];
|
|
2207
|
+
if (opts.system) msgs = [{ role: "system", content: opts.system }, ...msgs];
|
|
2208
|
+
const body = { model: opts.model, messages: msgs };
|
|
2209
|
+
if (opts.temperature !== void 0) body["temperature"] = opts.temperature;
|
|
2210
|
+
if (opts.maxTokens !== void 0) body["max_tokens"] = opts.maxTokens;
|
|
2211
|
+
const data = await postJson(`${endpoint}/chat/completions`, body, headers, timeoutMs);
|
|
2212
|
+
const choices = data["choices"] ?? [];
|
|
2213
|
+
const message = choices[0]?.["message"] ?? {};
|
|
2214
|
+
return { response: message["content"] ?? "", meta: data };
|
|
2215
|
+
};
|
|
2216
|
+
}
|
|
2217
|
+
var ANTHROPIC_ENDPOINT = "https://api.anthropic.com/v1";
|
|
2218
|
+
var ANTHROPIC_VERSION = "2023-06-01";
|
|
2219
|
+
function anthropicNeuron(opts) {
|
|
2220
|
+
const key = opts.apiKey ?? process.env["ANTHROPIC_API_KEY"];
|
|
2221
|
+
if (!key) {
|
|
2222
|
+
throw new Error(
|
|
2223
|
+
"Anthropic Neuron requires an API key. Pass apiKey=... or set the ANTHROPIC_API_KEY environment variable."
|
|
2224
|
+
);
|
|
2225
|
+
}
|
|
2226
|
+
const maxTokens = opts.maxTokens ?? 1024;
|
|
2227
|
+
const timeoutMs = opts.timeoutMs ?? 12e4;
|
|
2228
|
+
const headers = {
|
|
2229
|
+
"anthropic-version": ANTHROPIC_VERSION,
|
|
2230
|
+
"x-api-key": key
|
|
2231
|
+
};
|
|
2232
|
+
return async (input) => {
|
|
2233
|
+
const inp = input ?? {};
|
|
2234
|
+
const { prompt, messages } = requireInput(inp, "Anthropic");
|
|
2235
|
+
let system = opts.system;
|
|
2236
|
+
let msgs;
|
|
2237
|
+
if (messages !== null) {
|
|
2238
|
+
const systemMsgs = messages.filter((m) => m["role"] === "system");
|
|
2239
|
+
if (systemMsgs.length > 1) {
|
|
2240
|
+
console.warn("Anthropic Neuron received multiple system messages; using the last one.");
|
|
2241
|
+
}
|
|
2242
|
+
const lastSystem = systemMsgs[systemMsgs.length - 1];
|
|
2243
|
+
if (lastSystem && typeof lastSystem["content"] === "string") {
|
|
2244
|
+
system = lastSystem["content"];
|
|
2245
|
+
}
|
|
2246
|
+
msgs = messages.filter((m) => m["role"] !== "system");
|
|
2247
|
+
} else {
|
|
2248
|
+
msgs = [{ role: "user", content: prompt ?? "" }];
|
|
2249
|
+
}
|
|
2250
|
+
const body = { model: opts.model, messages: msgs, max_tokens: maxTokens };
|
|
2251
|
+
if (system) body["system"] = system;
|
|
2252
|
+
if (opts.temperature !== void 0) body["temperature"] = opts.temperature;
|
|
2253
|
+
const data = await postJson(`${ANTHROPIC_ENDPOINT}/messages`, body, headers, timeoutMs);
|
|
2254
|
+
const blocks = data["content"] ?? [];
|
|
2255
|
+
const text = blocks.filter((b) => b["type"] === "text").map((b) => b["text"] ?? "").join("");
|
|
2256
|
+
return { response: text, meta: data };
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
// src/neuron-factory.ts
|
|
2261
|
+
var OPENAI_COMPAT = {
|
|
2262
|
+
groq: { endpoint: "https://api.groq.com/openai", apiKeyEnv: "GROQ_API_KEY" },
|
|
2263
|
+
openrouter: { endpoint: "https://openrouter.ai/api", apiKeyEnv: "OPENROUTER_API_KEY" },
|
|
2264
|
+
together: { endpoint: "https://api.together.xyz", apiKeyEnv: "TOGETHER_API_KEY" },
|
|
2265
|
+
mistral: { endpoint: "https://api.mistral.ai", apiKeyEnv: "MISTRAL_API_KEY" }
|
|
2266
|
+
};
|
|
2267
|
+
function openAICompatNeuron(alias, opts) {
|
|
2268
|
+
const { endpoint, apiKeyEnv } = OPENAI_COMPAT[alias];
|
|
2269
|
+
const apiKey = opts.apiKey ?? process.env[apiKeyEnv];
|
|
2270
|
+
const hfOpts = {
|
|
2271
|
+
endpoint: opts.endpoint ?? endpoint,
|
|
2272
|
+
useChatApi: opts.useChatApi ?? true
|
|
2273
|
+
};
|
|
2274
|
+
if (opts.model !== void 0) hfOpts.model = opts.model;
|
|
2275
|
+
if (opts.temperature !== void 0) hfOpts.temperature = opts.temperature;
|
|
2276
|
+
if (opts.maxNewTokens !== void 0) hfOpts.maxNewTokens = opts.maxNewTokens;
|
|
2277
|
+
if (opts.timeoutMs !== void 0) hfOpts.timeoutMs = opts.timeoutMs;
|
|
2278
|
+
if (apiKey !== void 0) hfOpts.apiKey = apiKey;
|
|
2279
|
+
return huggingFaceNeuron(hfOpts);
|
|
2280
|
+
}
|
|
2281
|
+
function neuron(source, opts) {
|
|
2282
|
+
switch (source) {
|
|
2283
|
+
case "mcp":
|
|
2284
|
+
return mcpNeuron(opts);
|
|
2285
|
+
case "ollama":
|
|
2286
|
+
return ollamaNeuron(opts);
|
|
2287
|
+
case "huggingface":
|
|
2288
|
+
case "hf":
|
|
2289
|
+
return huggingFaceNeuron(opts);
|
|
2290
|
+
case "openai":
|
|
2291
|
+
return openaiNeuron(opts);
|
|
2292
|
+
case "anthropic":
|
|
2293
|
+
return anthropicNeuron(opts);
|
|
2294
|
+
case "groq":
|
|
2295
|
+
case "openrouter":
|
|
2296
|
+
case "together":
|
|
2297
|
+
case "mistral":
|
|
2298
|
+
return openAICompatNeuron(source, opts ?? {});
|
|
2299
|
+
default: {
|
|
2300
|
+
const available = `"mcp", "ollama", "huggingface", "hf", "openai", "anthropic", "groq", "openrouter", "together", "mistral"`;
|
|
2301
|
+
throw new Error(`Unknown neuron source '${String(source)}'. Available: ${available}`);
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
// src/axon.ts
|
|
2307
|
+
var ATTACH = /* @__PURE__ */ Symbol("cosmonapse.axon.attach");
|
|
2308
|
+
var DETACH = /* @__PURE__ */ Symbol("cosmonapse.axon.detach");
|
|
2309
|
+
var noopContextFetcher = () => [];
|
|
2310
|
+
var Axon = class _Axon {
|
|
2311
|
+
neuronId;
|
|
2312
|
+
capabilities;
|
|
2313
|
+
version;
|
|
2314
|
+
fn;
|
|
2315
|
+
contextFetcher;
|
|
2316
|
+
outputParser;
|
|
2317
|
+
dendrite = null;
|
|
2318
|
+
/**
|
|
2319
|
+
* Decorator-registered recognisers, one bucket per capability (the asking
|
|
2320
|
+
* side; named `detects*` to stay distinct from the Dendrite's `on*` inbound
|
|
2321
|
+
* handlers). Applied in precedence error -> clarification -> permission ->
|
|
2322
|
+
* output by {@link applyRecognisers}.
|
|
2323
|
+
*/
|
|
2324
|
+
recognisers = { error: [], clarification: [], permission: [], output: [] };
|
|
2325
|
+
/** @internal - lifecycle hooks, driven by the hosting Dendrite. */
|
|
2326
|
+
hooks = new LifecycleHooks(this);
|
|
2327
|
+
constructor(opts) {
|
|
2328
|
+
this.neuronId = opts.neuronId;
|
|
2329
|
+
this.capabilities = opts.capabilities ?? [];
|
|
2330
|
+
this.version = opts.version;
|
|
2331
|
+
this.fn = opts.neuronFn;
|
|
2332
|
+
this.contextFetcher = opts.contextFetcher ?? noopContextFetcher;
|
|
2333
|
+
this.outputParser = opts.outputParser;
|
|
2334
|
+
}
|
|
2335
|
+
// -- source-paired factories --------------------------------------
|
|
2336
|
+
// Build an Axon already paired with one of the `neuron(source, ...)`
|
|
2337
|
+
// providers AND wired with the matching recogniser. No new class: the
|
|
2338
|
+
// result is a plain Axon.
|
|
2339
|
+
static build(neuronId, neuronFn, source, extra) {
|
|
2340
|
+
const recognize = extra.recognize ?? true;
|
|
2341
|
+
const o = { neuronId, neuronFn };
|
|
2342
|
+
if (extra.capabilities) o.capabilities = extra.capabilities;
|
|
2343
|
+
if (extra.version !== void 0) o.version = extra.version;
|
|
2344
|
+
if (extra.contextFetcher) o.contextFetcher = extra.contextFetcher;
|
|
2345
|
+
if (recognize) o.outputParser = source === "mcp" ? parseMcpIntents : parseLlmIntents;
|
|
2346
|
+
return new _Axon(o);
|
|
2347
|
+
}
|
|
2348
|
+
/** Axon paired with any registered Neuron source + its recogniser. */
|
|
2349
|
+
static fromSource(source, neuronId, opts, extra = {}) {
|
|
2350
|
+
return _Axon.build(neuronId, neuron(source, opts), source, extra);
|
|
2351
|
+
}
|
|
2352
|
+
/** Axon paired with the OpenAI Chat Completions API. */
|
|
2353
|
+
static openai(neuronId, opts, extra = {}) {
|
|
2354
|
+
return _Axon.build(neuronId, neuron("openai", opts), "openai", extra);
|
|
2355
|
+
}
|
|
2356
|
+
/** Axon paired with the Anthropic Messages API. */
|
|
2357
|
+
static anthropic(neuronId, opts, extra = {}) {
|
|
2358
|
+
return _Axon.build(neuronId, neuron("anthropic", opts), "anthropic", extra);
|
|
2359
|
+
}
|
|
2360
|
+
/** Axon paired with a local Ollama daemon. */
|
|
2361
|
+
static ollama(neuronId, opts, extra = {}) {
|
|
2362
|
+
return _Axon.build(neuronId, neuron("ollama", opts), "ollama", extra);
|
|
2363
|
+
}
|
|
2364
|
+
/** Axon paired with a HuggingFace TGI / OpenAI-compatible endpoint. */
|
|
2365
|
+
static huggingface(neuronId, opts, extra = {}) {
|
|
2366
|
+
return _Axon.build(neuronId, neuron("huggingface", opts), "huggingface", extra);
|
|
2367
|
+
}
|
|
2368
|
+
/** Axon paired with a stdio MCP server. */
|
|
2369
|
+
static mcp(neuronId, opts, extra = {}) {
|
|
2370
|
+
return _Axon.build(neuronId, neuron("mcp", opts), "mcp", extra);
|
|
2371
|
+
}
|
|
2372
|
+
// -- recognition decorators ---------------------------------------
|
|
2373
|
+
// The asking side: `detects*` registers a detector over the Neuron's raw
|
|
2374
|
+
// output, distinct from the Dendrite's `on*` handlers (which consume inbound
|
|
2375
|
+
// Signals). Return the intent's fields to match, or null/undefined to fall
|
|
2376
|
+
// through. Sync or async; multiple per capability tried in order. These run
|
|
2377
|
+
// after `outputParser` and before the literal `__marker__` checks.
|
|
2378
|
+
/** Detector returning the AGENT_OUTPUT payload, or null to wrap verbatim. */
|
|
2379
|
+
detectsOutput(fn) {
|
|
2380
|
+
this.recognisers.output.push(fn);
|
|
2381
|
+
return fn;
|
|
2382
|
+
}
|
|
2383
|
+
/** Detector returning `{ question, context? }` to emit CLARIFICATION, or null. */
|
|
2384
|
+
detectsClarification(fn) {
|
|
2385
|
+
this.recognisers.clarification.push(fn);
|
|
2386
|
+
return fn;
|
|
2387
|
+
}
|
|
2388
|
+
/** Detector returning `{ action, scope?, reason?, context? }` for PERMISSION, or null. */
|
|
2389
|
+
detectsPermission(fn) {
|
|
2390
|
+
this.recognisers.permission.push(fn);
|
|
2391
|
+
return fn;
|
|
2392
|
+
}
|
|
2393
|
+
/** Detector returning `{ code?, message?, recoverable? }` to emit ERROR, or null. */
|
|
2394
|
+
detectsError(fn) {
|
|
2395
|
+
this.recognisers.error.push(fn);
|
|
2396
|
+
return fn;
|
|
2397
|
+
}
|
|
2398
|
+
async applyRecognisers(raw) {
|
|
2399
|
+
const rec = this.recognisers;
|
|
2400
|
+
if (!rec.error.length && !rec.clarification.length && !rec.permission.length && !rec.output.length) {
|
|
2401
|
+
return raw;
|
|
2402
|
+
}
|
|
2403
|
+
const first = async (fns) => {
|
|
2404
|
+
for (const fn of fns) {
|
|
2405
|
+
const r = await fn(raw);
|
|
2406
|
+
if (r !== null && r !== void 0) return r;
|
|
2407
|
+
}
|
|
2408
|
+
return void 0;
|
|
2409
|
+
};
|
|
2410
|
+
let hit = await first(rec.error);
|
|
2411
|
+
if (hit !== void 0) return { __error__: true, ...hit };
|
|
2412
|
+
hit = await first(rec.clarification);
|
|
2413
|
+
if (hit !== void 0) return { __clarification__: true, ...hit };
|
|
2414
|
+
hit = await first(rec.permission);
|
|
2415
|
+
if (hit !== void 0) return { __permission__: true, ...hit };
|
|
2416
|
+
hit = await first(rec.output);
|
|
2417
|
+
if (hit !== void 0) return hit;
|
|
2418
|
+
return raw;
|
|
2419
|
+
}
|
|
2420
|
+
/** Register a fire-once handler called after this Axon connects (attaches + registers). */
|
|
2421
|
+
onConnect(fn) {
|
|
2422
|
+
return this.hooks.onConnect(fn);
|
|
2423
|
+
}
|
|
2424
|
+
/** Register a handler called whenever this Axon's observable state refreshes. */
|
|
2425
|
+
onRefresh(fn) {
|
|
2426
|
+
return this.hooks.onRefresh(fn);
|
|
2427
|
+
}
|
|
2428
|
+
/** Register a periodic handler that runs every `everyMs` while the host runs. */
|
|
2429
|
+
onSchedule(everyMs, fn) {
|
|
2430
|
+
return this.hooks.onSchedule(everyMs, fn);
|
|
2431
|
+
}
|
|
2432
|
+
/**
|
|
2433
|
+
* Package-internal: invoked by Dendrite.attachAxon via the {@link ATTACH}
|
|
2434
|
+
* symbol. Not callable by external consumers (the symbol is not exported from
|
|
2435
|
+
* index.ts), so this replaces the previous `@internal`-comment-only contract
|
|
2436
|
+
* with real, enforced encapsulation.
|
|
2437
|
+
*/
|
|
2438
|
+
[ATTACH](dendrite) {
|
|
2439
|
+
if (this.dendrite !== null && this.dendrite !== dendrite) {
|
|
2440
|
+
throw new Error(`Axon '${this.neuronId}' is already attached to a different Dendrite`);
|
|
2441
|
+
}
|
|
2442
|
+
this.dendrite = dendrite;
|
|
2443
|
+
}
|
|
2444
|
+
/** Package-internal: invoked via the {@link DETACH} symbol. */
|
|
2445
|
+
[DETACH]() {
|
|
2446
|
+
this.dendrite = null;
|
|
2447
|
+
}
|
|
2448
|
+
/** Run the Neuron and return AGENT_OUTPUT / CLARIFICATION / ERROR. */
|
|
2449
|
+
async handleTask(task) {
|
|
2450
|
+
const traceId = task.trace_id;
|
|
2451
|
+
const parentId = task.id;
|
|
2452
|
+
const input = task.payload["input"] ?? {};
|
|
2453
|
+
const contextRef = task.payload["context_ref"];
|
|
2454
|
+
let context = [];
|
|
2455
|
+
if (contextRef) {
|
|
2456
|
+
try {
|
|
2457
|
+
context = await this.contextFetcher(contextRef);
|
|
2458
|
+
} catch {
|
|
2459
|
+
context = [];
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
let rawOutput;
|
|
2463
|
+
try {
|
|
2464
|
+
rawOutput = await this.fn(input, context);
|
|
2465
|
+
if (this.outputParser) rawOutput = this.outputParser(rawOutput);
|
|
2466
|
+
rawOutput = await this.applyRecognisers(rawOutput);
|
|
2467
|
+
} catch (err) {
|
|
2468
|
+
return errorSignal({
|
|
2469
|
+
traceId,
|
|
2470
|
+
parentId,
|
|
2471
|
+
directed: { id: this.neuronId },
|
|
2472
|
+
code: "NEURON_EXCEPTION",
|
|
2473
|
+
message: err instanceof Error ? err.message : String(err),
|
|
2474
|
+
recoverable: false
|
|
2475
|
+
});
|
|
2476
|
+
}
|
|
2477
|
+
if (isErrorOutput(rawOutput)) {
|
|
2478
|
+
return errorSignal({
|
|
2479
|
+
traceId,
|
|
2480
|
+
parentId,
|
|
2481
|
+
directed: { id: this.neuronId },
|
|
2482
|
+
code: rawOutput.code ?? "NEURON_ERROR",
|
|
2483
|
+
message: rawOutput.message ?? "",
|
|
2484
|
+
recoverable: Boolean(rawOutput.recoverable)
|
|
2485
|
+
});
|
|
2486
|
+
}
|
|
2487
|
+
if (isClarification(rawOutput)) {
|
|
2488
|
+
return clarificationSignal({
|
|
2489
|
+
traceId,
|
|
2490
|
+
parentId,
|
|
2491
|
+
directed: { id: this.neuronId },
|
|
2492
|
+
question: rawOutput.question,
|
|
2493
|
+
...rawOutput.context !== void 0 ? { context: rawOutput.context } : {}
|
|
2494
|
+
});
|
|
2495
|
+
}
|
|
2496
|
+
if (isPermissionRequest(rawOutput)) {
|
|
2497
|
+
return permissionSignal({
|
|
2498
|
+
traceId,
|
|
2499
|
+
parentId,
|
|
2500
|
+
directed: { id: this.neuronId },
|
|
2501
|
+
action: rawOutput.action,
|
|
2502
|
+
...rawOutput.scope !== void 0 ? { scope: rawOutput.scope } : {},
|
|
2503
|
+
...rawOutput.reason !== void 0 ? { reason: rawOutput.reason } : {},
|
|
2504
|
+
...rawOutput.context !== void 0 ? { context: rawOutput.context } : {}
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
const output = typeof rawOutput === "object" && rawOutput !== null ? rawOutput : { value: rawOutput };
|
|
2508
|
+
return agentOutputSignal({ traceId, parentId, directed: { id: this.neuronId }, output });
|
|
2509
|
+
}
|
|
2510
|
+
};
|
|
2511
|
+
var INTENT_KEY = "cosmo";
|
|
2512
|
+
var FENCED_JSON = /```(?:json)?\s*(\{[\s\S]*?\})\s*```/g;
|
|
2513
|
+
function extractCosmoIntent(text) {
|
|
2514
|
+
if (!text) return null;
|
|
2515
|
+
const candidates = [text.trim()];
|
|
2516
|
+
FENCED_JSON.lastIndex = 0;
|
|
2517
|
+
let m;
|
|
2518
|
+
while ((m = FENCED_JSON.exec(text)) !== null) candidates.push(m[1]);
|
|
2519
|
+
for (const cand of candidates) {
|
|
2520
|
+
let obj;
|
|
2521
|
+
try {
|
|
2522
|
+
obj = JSON.parse(cand);
|
|
2523
|
+
} catch {
|
|
2524
|
+
continue;
|
|
2525
|
+
}
|
|
2526
|
+
if (obj !== null && typeof obj === "object" && typeof obj[INTENT_KEY] === "string") {
|
|
2527
|
+
return obj;
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
return null;
|
|
2531
|
+
}
|
|
2532
|
+
function intentToMarker(intent) {
|
|
2533
|
+
const kind = intent[INTENT_KEY];
|
|
2534
|
+
if (kind === "clarification") {
|
|
2535
|
+
return {
|
|
2536
|
+
__clarification__: true,
|
|
2537
|
+
question: intent["question"] ?? "",
|
|
2538
|
+
...intent["context"] !== void 0 ? { context: intent["context"] } : {}
|
|
2539
|
+
};
|
|
2540
|
+
}
|
|
2541
|
+
if (kind === "permission") {
|
|
2542
|
+
return {
|
|
2543
|
+
__permission__: true,
|
|
2544
|
+
action: intent["action"] ?? "",
|
|
2545
|
+
...intent["scope"] !== void 0 ? { scope: intent["scope"] } : {},
|
|
2546
|
+
...intent["reason"] !== void 0 ? { reason: intent["reason"] } : {},
|
|
2547
|
+
...intent["context"] !== void 0 ? { context: intent["context"] } : {}
|
|
2548
|
+
};
|
|
2549
|
+
}
|
|
2550
|
+
if (kind === "error") {
|
|
2551
|
+
return {
|
|
2552
|
+
__error__: true,
|
|
2553
|
+
code: intent["code"] ?? "NEURON_ERROR",
|
|
2554
|
+
message: intent["message"] ?? "",
|
|
2555
|
+
recoverable: Boolean(intent["recoverable"])
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
if (kind === "output") {
|
|
2559
|
+
const out = intent["output"];
|
|
2560
|
+
return out !== null && typeof out === "object" ? out : { value: out };
|
|
2561
|
+
}
|
|
2562
|
+
return null;
|
|
2563
|
+
}
|
|
2564
|
+
function parseLlmIntents(raw) {
|
|
2565
|
+
if (raw === null || typeof raw !== "object") return { value: raw };
|
|
2566
|
+
const text = raw["response"];
|
|
2567
|
+
if (typeof text === "string") {
|
|
2568
|
+
const intent = extractCosmoIntent(text);
|
|
2569
|
+
if (intent) {
|
|
2570
|
+
const marker = intentToMarker(intent);
|
|
2571
|
+
if (marker) return marker;
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
return raw;
|
|
2575
|
+
}
|
|
2576
|
+
function parseMcpIntents(raw) {
|
|
2577
|
+
if (raw === null || typeof raw !== "object") return { value: raw };
|
|
2578
|
+
const r = raw;
|
|
2579
|
+
if (r["is_error"]) {
|
|
2580
|
+
const msg = r["response"] ?? r["content"] ?? "MCP tool returned is_error";
|
|
2581
|
+
return { __error__: true, code: "MCP_TOOL_ERROR", message: String(msg) };
|
|
2582
|
+
}
|
|
2583
|
+
const text = r["response"];
|
|
2584
|
+
if (typeof text === "string") {
|
|
2585
|
+
const intent = extractCosmoIntent(text);
|
|
2586
|
+
if (intent) {
|
|
2587
|
+
const marker = intentToMarker(intent);
|
|
2588
|
+
if (marker) return marker;
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
return raw;
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
// src/dendrite.ts
|
|
2595
|
+
Symbol.asyncDispose ??= /* @__PURE__ */ Symbol.for("Symbol.asyncDispose");
|
|
2596
|
+
var DendriteProtocolError = class extends Error {
|
|
2597
|
+
constructor(message) {
|
|
2598
|
+
super(message);
|
|
2599
|
+
this.name = "DendriteProtocolError";
|
|
2600
|
+
}
|
|
2601
|
+
};
|
|
2602
|
+
var Dendrite = class {
|
|
2603
|
+
synapse;
|
|
2604
|
+
registryStore;
|
|
2605
|
+
namespace;
|
|
2606
|
+
dendriteId;
|
|
2607
|
+
heartbeatMs;
|
|
2608
|
+
reregisterOnHeartbeat;
|
|
2609
|
+
_axons = /* @__PURE__ */ new Map();
|
|
2610
|
+
handlers = /* @__PURE__ */ new Map();
|
|
2611
|
+
taskSub = null;
|
|
2612
|
+
inboundSubs = /* @__PURE__ */ new Map();
|
|
2613
|
+
// Self-scheduling setTimeout handle (not setInterval - see startHeartbeatLoop).
|
|
2614
|
+
heartbeatTimer = null;
|
|
2615
|
+
// Set true by stop() so an in-flight tick won't re-arm the loop.
|
|
2616
|
+
heartbeatStopped = true;
|
|
2617
|
+
running = false;
|
|
2618
|
+
/** @internal - lifecycle hooks for this Dendrite. */
|
|
2619
|
+
hooks = new LifecycleHooks(this);
|
|
2620
|
+
constructor(opts) {
|
|
2621
|
+
if (!opts.synapse) throw new TypeError("Dendrite requires a synapse");
|
|
2622
|
+
this.synapse = opts.synapse;
|
|
2623
|
+
this.registryStore = opts.registryStore ?? null;
|
|
2624
|
+
this.namespace = opts.namespace ?? "default";
|
|
2625
|
+
this.dendriteId = opts.dendriteId ?? "dendrite";
|
|
2626
|
+
this.heartbeatMs = opts.heartbeatMs ?? 3e4;
|
|
2627
|
+
this.reregisterOnHeartbeat = opts.reregisterOnHeartbeat ?? true;
|
|
2628
|
+
for (const t of AXON_TYPES) this.handlers.set(t, []);
|
|
2629
|
+
}
|
|
2630
|
+
// -- properties ----------------------------------------------------
|
|
2631
|
+
get axons() {
|
|
2632
|
+
return new Map(this._axons);
|
|
2633
|
+
}
|
|
2634
|
+
axon(neuronId) {
|
|
2635
|
+
return this._axons.get(neuronId);
|
|
2636
|
+
}
|
|
2637
|
+
// -- attachment ----------------------------------------------------
|
|
2638
|
+
attachAxon(axon) {
|
|
2639
|
+
if (this._axons.has(axon.neuronId)) {
|
|
2640
|
+
throw new Error(`Dendrite already has an Axon for neuronId='${axon.neuronId}'`);
|
|
2641
|
+
}
|
|
2642
|
+
this._axons.set(axon.neuronId, axon);
|
|
2643
|
+
axon[ATTACH](this);
|
|
2644
|
+
}
|
|
2645
|
+
// -- inbound handler registration ----------------------------------
|
|
2646
|
+
on(type, fn) {
|
|
2647
|
+
const list = this.handlers.get(type);
|
|
2648
|
+
if (!list) {
|
|
2649
|
+
throw new DendriteProtocolError(`Cannot handle non-Axon type '${type}'`);
|
|
2650
|
+
}
|
|
2651
|
+
list.push(fn);
|
|
2652
|
+
if (this.running && !this.inboundSubs.has(type)) {
|
|
2653
|
+
void this.ensureInboundSub(type);
|
|
2654
|
+
}
|
|
2655
|
+
return fn;
|
|
2656
|
+
}
|
|
2657
|
+
onAgentOutput(fn) {
|
|
2658
|
+
return this.on(SignalType.AGENT_OUTPUT, fn);
|
|
2659
|
+
}
|
|
2660
|
+
onClarification(fn) {
|
|
2661
|
+
return this.on(SignalType.CLARIFICATION, fn);
|
|
2662
|
+
}
|
|
2663
|
+
/**
|
|
2664
|
+
* Register a handler fired on inbound PERMISSION requests - the *answering*
|
|
2665
|
+
* side. A central Cortex or a peer Dendrite evaluates the request (often
|
|
2666
|
+
* consulting an Engram of standing grants, keyed per-neuron) and replies via
|
|
2667
|
+
* {@link respondToPermission} (re-dispatch a TASK with the verdict) or
|
|
2668
|
+
* {@link grantPermission} / {@link denyPermission} (emit a discrete
|
|
2669
|
+
* PERMISSION_DECISION). It may also imprint the decision into an Engram so
|
|
2670
|
+
* future recalls hit.
|
|
2671
|
+
*/
|
|
2672
|
+
onPermission(fn) {
|
|
2673
|
+
return this.on(SignalType.PERMISSION, fn);
|
|
2674
|
+
}
|
|
2675
|
+
onErrorSignal(fn) {
|
|
2676
|
+
return this.on(SignalType.ERROR, fn);
|
|
2677
|
+
}
|
|
2678
|
+
onRegister(fn) {
|
|
2679
|
+
return this.on(SignalType.REGISTER, fn);
|
|
2680
|
+
}
|
|
2681
|
+
onDeregister(fn) {
|
|
2682
|
+
return this.on(SignalType.DEREGISTER, fn);
|
|
2683
|
+
}
|
|
2684
|
+
onHeartbeat(fn) {
|
|
2685
|
+
return this.on(SignalType.HEARTBEAT, fn);
|
|
2686
|
+
}
|
|
2687
|
+
// -- lifecycle hooks ----------------------------------------------
|
|
2688
|
+
/** Register a fire-once handler called after start() completes. */
|
|
2689
|
+
onConnect(fn) {
|
|
2690
|
+
return this.hooks.onConnect(fn);
|
|
2691
|
+
}
|
|
2692
|
+
/** Register a handler called whenever this Dendrite's state refreshes. */
|
|
2693
|
+
onRefresh(fn) {
|
|
2694
|
+
return this.hooks.onRefresh(fn);
|
|
2695
|
+
}
|
|
2696
|
+
/** Register a periodic handler that runs every `everyMs` until stop(). */
|
|
2697
|
+
onSchedule(everyMs, fn) {
|
|
2698
|
+
return this.hooks.onSchedule(everyMs, fn);
|
|
2699
|
+
}
|
|
2700
|
+
/** Manually fire a refresh event (reason defaults to "manual"). */
|
|
2701
|
+
async refresh(opts = {}) {
|
|
2702
|
+
await this.hooks.refresh(opts);
|
|
2703
|
+
}
|
|
2704
|
+
// -- lifecycle -----------------------------------------------------
|
|
2705
|
+
async start() {
|
|
2706
|
+
if (this.running) return;
|
|
2707
|
+
if (this.registryStore !== null) await this.registryStore.connect();
|
|
2708
|
+
if (this._axons.size > 0) {
|
|
2709
|
+
this.taskSub = await this.synapse.subscribe(
|
|
2710
|
+
this.subject(SignalType.TASK),
|
|
2711
|
+
(s) => this.onTask(s)
|
|
2712
|
+
);
|
|
2713
|
+
for (const axon of this._axons.values()) {
|
|
2714
|
+
await this.mirrorToStore(axon, "registered");
|
|
2715
|
+
await this.emitRegister(axon);
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
for (const [type, hs] of this.handlers) {
|
|
2719
|
+
if (hs.length) await this.ensureInboundSub(type);
|
|
2720
|
+
}
|
|
2721
|
+
if (this.registryStore !== null) {
|
|
2722
|
+
for (const t of [SignalType.REGISTER, SignalType.DEREGISTER, SignalType.HEARTBEAT]) {
|
|
2723
|
+
await this.ensureInboundSub(t);
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
this.running = true;
|
|
2727
|
+
if (this._axons.size > 0 && this.heartbeatMs > 0) {
|
|
2728
|
+
this.startHeartbeatLoop();
|
|
2729
|
+
}
|
|
2730
|
+
await this.hooks._fireConnect();
|
|
2731
|
+
this.hooks._launchSchedule();
|
|
2732
|
+
for (const axon of this._axons.values()) {
|
|
2733
|
+
await axon.hooks._fireConnect();
|
|
2734
|
+
axon.hooks._launchSchedule();
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
/**
|
|
2738
|
+
* Heartbeat as a self-scheduling async loop rather than `setInterval`.
|
|
2739
|
+
*
|
|
2740
|
+
* Why not setInterval: it fires on a fixed wall-clock cadence regardless of
|
|
2741
|
+
* whether the previous tick finished, so under load ticks overlap and the
|
|
2742
|
+
* effective interval drifts; and because the callback is sync, any rejection
|
|
2743
|
+
* from the async work inside is an unhandled rejection that setInterval
|
|
2744
|
+
* silently drops. Here each tick is fully awaited, its errors are caught, and
|
|
2745
|
+
* only then is the next tick scheduled - matching the Python SDK's
|
|
2746
|
+
* asyncio.Task semantics (structured error handling + clean cancellation).
|
|
2747
|
+
*/
|
|
2748
|
+
startHeartbeatLoop() {
|
|
2749
|
+
this.heartbeatStopped = false;
|
|
2750
|
+
const schedule = () => {
|
|
2751
|
+
this.heartbeatTimer = setTimeout(() => {
|
|
2752
|
+
void tick();
|
|
2753
|
+
}, this.heartbeatMs);
|
|
2754
|
+
this.heartbeatTimer.unref?.();
|
|
2755
|
+
};
|
|
2756
|
+
const tick = async () => {
|
|
2757
|
+
if (this.heartbeatStopped || !this.running) return;
|
|
2758
|
+
try {
|
|
2759
|
+
await this.heartbeatTick();
|
|
2760
|
+
} catch {
|
|
2761
|
+
}
|
|
2762
|
+
if (!this.heartbeatStopped && this.running) schedule();
|
|
2763
|
+
};
|
|
2764
|
+
schedule();
|
|
2765
|
+
}
|
|
2766
|
+
async stop(reason) {
|
|
2767
|
+
if (!this.running) return;
|
|
2768
|
+
this.running = false;
|
|
2769
|
+
this.hooks._stopHooks();
|
|
2770
|
+
for (const axon of this._axons.values()) axon.hooks._stopHooks();
|
|
2771
|
+
this.heartbeatStopped = true;
|
|
2772
|
+
if (this.heartbeatTimer !== null) {
|
|
2773
|
+
clearTimeout(this.heartbeatTimer);
|
|
2774
|
+
this.heartbeatTimer = null;
|
|
2775
|
+
}
|
|
2776
|
+
if (this.taskSub !== null) {
|
|
2777
|
+
await this.taskSub.unsubscribe();
|
|
2778
|
+
this.taskSub = null;
|
|
2779
|
+
}
|
|
2780
|
+
for (const sub of this.inboundSubs.values()) {
|
|
2781
|
+
try {
|
|
2782
|
+
await sub.unsubscribe();
|
|
2783
|
+
} catch {
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
this.inboundSubs.clear();
|
|
2787
|
+
for (const axon of this._axons.values()) {
|
|
2788
|
+
if (this.registryStore !== null) {
|
|
2789
|
+
try {
|
|
2790
|
+
await this.registryStore.markDeregistered(axon.neuronId);
|
|
2791
|
+
} catch {
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
await this.emitDeregister(axon, reason);
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
/**
|
|
2798
|
+
* Explicit-resource-management hook so a Dendrite can be used with
|
|
2799
|
+
* `await using` - the TS equivalent of Python's `async with dendrite:`.
|
|
2800
|
+
*
|
|
2801
|
+
* ```ts
|
|
2802
|
+
* await using dendrite = new Dendrite({ synapse });
|
|
2803
|
+
* dendrite.attachAxon(axon);
|
|
2804
|
+
* await dendrite.start();
|
|
2805
|
+
* // ... stop() runs automatically when this scope exits, even on throw.
|
|
2806
|
+
* ```
|
|
2807
|
+
*
|
|
2808
|
+
* Idempotent: stop() is a no-op if the Dendrite was never started or already
|
|
2809
|
+
* stopped. As with stop(), the caller still owns the Synapse/registry store.
|
|
2810
|
+
*/
|
|
2811
|
+
async [Symbol.asyncDispose]() {
|
|
2812
|
+
await this.stop();
|
|
2813
|
+
}
|
|
2814
|
+
// -- registry helpers ----------------------------------------------
|
|
2815
|
+
requireStore() {
|
|
2816
|
+
if (this.registryStore === null) {
|
|
2817
|
+
throw new Error(
|
|
2818
|
+
"Dendrite has no registryStore - pass one at construction to use registry helpers (findNeurons / registrySnapshot)."
|
|
2819
|
+
);
|
|
2820
|
+
}
|
|
2821
|
+
return this.registryStore;
|
|
2822
|
+
}
|
|
2823
|
+
/** All known records, optionally filtered (live records only by default). */
|
|
2824
|
+
async registrySnapshot(opts = {}) {
|
|
2825
|
+
return this.requireStore().list(opts);
|
|
2826
|
+
}
|
|
2827
|
+
/** Live (non-deregistered) records, optionally filtered by capability. */
|
|
2828
|
+
async findNeurons(opts = {}) {
|
|
2829
|
+
return this.requireStore().list({
|
|
2830
|
+
...opts.capability !== void 0 ? { capability: opts.capability } : {},
|
|
2831
|
+
includeDeregistered: false
|
|
2832
|
+
});
|
|
2833
|
+
}
|
|
2834
|
+
// -- outbound primitives ------------------------------------------
|
|
2835
|
+
async dispatchTask(args) {
|
|
2836
|
+
const sig = taskSignal({
|
|
2837
|
+
directed: { id: args.neuron },
|
|
2838
|
+
input: args.input,
|
|
2839
|
+
...args.traceId !== void 0 ? { traceId: args.traceId } : {},
|
|
2840
|
+
...args.parentId !== void 0 ? { parentId: args.parentId } : {},
|
|
2841
|
+
...args.contextRef !== void 0 ? { contextRef: args.contextRef } : {},
|
|
2842
|
+
...args.capabilities !== void 0 ? { capabilities: args.capabilities } : {},
|
|
2843
|
+
...args.meta !== void 0 ? { meta: args.meta } : {}
|
|
2844
|
+
});
|
|
2845
|
+
await this.emit(sig);
|
|
2846
|
+
return sig;
|
|
2847
|
+
}
|
|
2848
|
+
async emitFinal(args) {
|
|
2849
|
+
const sig = finalSignal({
|
|
2850
|
+
traceId: args.traceId,
|
|
2851
|
+
parentId: args.parentId,
|
|
2852
|
+
result: args.result,
|
|
2853
|
+
directed: { id: this.dendriteId },
|
|
2854
|
+
...args.meta !== void 0 ? { meta: args.meta } : {}
|
|
2855
|
+
});
|
|
2856
|
+
await this.emit(sig);
|
|
2857
|
+
return sig;
|
|
2858
|
+
}
|
|
2859
|
+
async emitError(args) {
|
|
2860
|
+
const sig = errorSignal({
|
|
2861
|
+
traceId: args.traceId,
|
|
2862
|
+
code: args.code,
|
|
2863
|
+
message: args.message,
|
|
2864
|
+
directed: { id: this.dendriteId },
|
|
2865
|
+
...args.parentId !== void 0 ? { parentId: args.parentId } : {},
|
|
2866
|
+
...args.recoverable !== void 0 ? { recoverable: args.recoverable } : {},
|
|
2867
|
+
...args.meta !== void 0 ? { meta: args.meta } : {}
|
|
2868
|
+
});
|
|
2869
|
+
await this.emit(sig);
|
|
2870
|
+
return sig;
|
|
2871
|
+
}
|
|
2872
|
+
/** Emit a synapse-side Signal. Refuses Axon-owned types. */
|
|
2873
|
+
/**
|
|
2874
|
+
* Reply to a PERMISSION by re-dispatching a TASK carrying the verdict.
|
|
2875
|
+
*
|
|
2876
|
+
* The "send it back to the axon" path: the follow-up TASK is addressed by
|
|
2877
|
+
* default to the Neuron that asked (`signal.neuron`), with `parentId` = the
|
|
2878
|
+
* PERMISSION's id and the original `traceId` carried over, so the Neuron
|
|
2879
|
+
* resumes on the same thread and can imprint the decision into an Engram (or
|
|
2880
|
+
* recall it next time). New TASK input: `{ permission: { action, granted,
|
|
2881
|
+
* reason?, ttlMs?, ...extra } }`.
|
|
2882
|
+
*/
|
|
2883
|
+
async respondToPermission(request, opts) {
|
|
2884
|
+
if (request.type !== SignalType.PERMISSION) {
|
|
2885
|
+
throw new DendriteProtocolError(
|
|
2886
|
+
`respondToPermission expects a PERMISSION signal, got '${request.type}'`
|
|
2887
|
+
);
|
|
2888
|
+
}
|
|
2889
|
+
const target = opts.neuron ?? request.directed?.id ?? null;
|
|
2890
|
+
if (!target) {
|
|
2891
|
+
throw new DendriteProtocolError(
|
|
2892
|
+
"respondToPermission: signal has no neuron and no neuron override - nowhere to dispatch the follow-up TASK"
|
|
2893
|
+
);
|
|
2894
|
+
}
|
|
2895
|
+
const permission = {
|
|
2896
|
+
action: request.payload["action"] ?? null,
|
|
2897
|
+
granted: opts.granted
|
|
2898
|
+
};
|
|
2899
|
+
if (opts.reason !== void 0) permission["reason"] = opts.reason;
|
|
2900
|
+
if (opts.ttlMs !== void 0) permission["ttl_ms"] = opts.ttlMs;
|
|
2901
|
+
if (opts.extra !== void 0) Object.assign(permission, opts.extra);
|
|
2902
|
+
return this.dispatchTask({
|
|
2903
|
+
neuron: target,
|
|
2904
|
+
input: { permission },
|
|
2905
|
+
traceId: request.trace_id,
|
|
2906
|
+
parentId: request.id,
|
|
2907
|
+
...opts.meta !== void 0 ? { meta: opts.meta } : {}
|
|
2908
|
+
});
|
|
2909
|
+
}
|
|
2910
|
+
// -- cognition decision signals (discrete, decentralised option) -----
|
|
2911
|
+
// Thin, stateless emit helpers for the new response signal types - no
|
|
2912
|
+
// correlation client. Use these when you want the decision to travel as a
|
|
2913
|
+
// discrete PERMISSION_DECISION / CLARIFICATION_ANSWER signal (e.g. for a
|
|
2914
|
+
// peer/observer to imprint into an Engram) rather than as a re-dispatched
|
|
2915
|
+
// TASK. Published via `publish` so any Dendrite - including a peer - can
|
|
2916
|
+
// answer; correlation, if needed, is the developer's choice.
|
|
2917
|
+
/** Approve a PERMISSION request. `ttlMs` optionally advertises how long the
|
|
2918
|
+
* grant is valid so the requester can cache it (e.g. in an Engram). */
|
|
2919
|
+
async grantPermission(request, opts = {}) {
|
|
2920
|
+
return this.decidePermission(request, true, opts);
|
|
2921
|
+
}
|
|
2922
|
+
/** Reject a PERMISSION request. */
|
|
2923
|
+
async denyPermission(request, opts = {}) {
|
|
2924
|
+
return this.decidePermission(request, false, opts);
|
|
2925
|
+
}
|
|
2926
|
+
async decidePermission(request, granted, opts) {
|
|
2927
|
+
if (request.type !== SignalType.PERMISSION) {
|
|
2928
|
+
throw new DendriteProtocolError(
|
|
2929
|
+
`grant/denyPermission expects a PERMISSION signal, got '${request.type}'`
|
|
2930
|
+
);
|
|
2931
|
+
}
|
|
2932
|
+
const sig = permissionDecisionSignal({
|
|
2933
|
+
traceId: request.trace_id,
|
|
2934
|
+
parentId: request.id,
|
|
2935
|
+
granted,
|
|
2936
|
+
directed: { id: this.dendriteId },
|
|
2937
|
+
...opts.reason !== void 0 ? { reason: opts.reason } : {},
|
|
2938
|
+
...opts.ttlMs !== void 0 ? { ttlMs: opts.ttlMs } : {},
|
|
2939
|
+
...opts.meta !== void 0 ? { meta: opts.meta } : {}
|
|
2940
|
+
});
|
|
2941
|
+
await this.publish(sig);
|
|
2942
|
+
return sig;
|
|
2943
|
+
}
|
|
2944
|
+
/** Answer a *blocking* CLARIFICATION (the Neuron called ask(...) and is
|
|
2945
|
+
* awaiting). Distinct from the legacy return-marker flow. */
|
|
2946
|
+
async answerClarification(request, answer, opts = {}) {
|
|
2947
|
+
if (request.type !== SignalType.CLARIFICATION) {
|
|
2948
|
+
throw new DendriteProtocolError(
|
|
2949
|
+
`answerClarification expects a CLARIFICATION signal, got '${request.type}'`
|
|
2950
|
+
);
|
|
2951
|
+
}
|
|
2952
|
+
const sig = clarificationAnswerSignal({
|
|
2953
|
+
traceId: request.trace_id,
|
|
2954
|
+
parentId: request.id,
|
|
2955
|
+
answer,
|
|
2956
|
+
directed: { id: this.dendriteId },
|
|
2957
|
+
...opts.meta !== void 0 ? { meta: opts.meta } : {}
|
|
2958
|
+
});
|
|
2959
|
+
await this.publish(sig);
|
|
2960
|
+
return sig;
|
|
2961
|
+
}
|
|
2962
|
+
async emit(signal) {
|
|
2963
|
+
if (!SYNAPSE_TYPES.has(signal.type)) {
|
|
2964
|
+
throw new DendriteProtocolError(
|
|
2965
|
+
`Dendrite refuses to emit '${signal.type}': only synapse-side types may be emitted this way. '${signal.type}' is an Axon-owned type.`
|
|
2966
|
+
);
|
|
2967
|
+
}
|
|
2968
|
+
await this.publish(signal);
|
|
2969
|
+
}
|
|
2970
|
+
async publish(signal) {
|
|
2971
|
+
await this.synapse.publish(this.subject(signal.type), signal);
|
|
2972
|
+
}
|
|
2973
|
+
async subscribe(type, handler, opts) {
|
|
2974
|
+
return this.synapse.subscribe(this.subject(type), handler, opts);
|
|
2975
|
+
}
|
|
2976
|
+
// -- internal ------------------------------------------------------
|
|
2977
|
+
subject(type) {
|
|
2978
|
+
return `cosmonapse.${this.namespace}.${type}`;
|
|
2979
|
+
}
|
|
2980
|
+
async ensureInboundSub(type) {
|
|
2981
|
+
if (this.inboundSubs.has(type)) return;
|
|
2982
|
+
const sub = await this.subscribe(type, (s) => this.dispatchInbound(s));
|
|
2983
|
+
this.inboundSubs.set(type, sub);
|
|
2984
|
+
}
|
|
2985
|
+
async onTask(task) {
|
|
2986
|
+
const target = task.directed?.id ?? null;
|
|
2987
|
+
if (!target) return;
|
|
2988
|
+
const axon = this._axons.get(target);
|
|
2989
|
+
if (!axon) return;
|
|
2990
|
+
let reply2;
|
|
2991
|
+
try {
|
|
2992
|
+
reply2 = await axon.handleTask(task);
|
|
2993
|
+
} catch (err) {
|
|
2994
|
+
reply2 = errorSignal({
|
|
2995
|
+
traceId: task.trace_id,
|
|
2996
|
+
parentId: task.id,
|
|
2997
|
+
directed: { id: target },
|
|
2998
|
+
code: "AXON_EXCEPTION",
|
|
2999
|
+
message: err instanceof Error ? err.message : String(err),
|
|
3000
|
+
recoverable: false
|
|
3001
|
+
});
|
|
3002
|
+
}
|
|
3003
|
+
await this.publish(reply2);
|
|
3004
|
+
}
|
|
3005
|
+
async emitRegister(axon) {
|
|
3006
|
+
await this.publish(
|
|
3007
|
+
registerSignal({
|
|
3008
|
+
directed: { id: axon.neuronId, capabilities: [...axon.capabilities] },
|
|
3009
|
+
capabilities: axon.capabilities,
|
|
3010
|
+
...axon.version !== void 0 ? { version: axon.version } : {}
|
|
3011
|
+
})
|
|
3012
|
+
);
|
|
3013
|
+
}
|
|
3014
|
+
async emitDeregister(axon, reason) {
|
|
3015
|
+
await this.publish(
|
|
3016
|
+
deregisterSignal({
|
|
3017
|
+
directed: { id: axon.neuronId },
|
|
3018
|
+
...reason !== void 0 ? { reason } : {}
|
|
3019
|
+
})
|
|
3020
|
+
);
|
|
3021
|
+
}
|
|
3022
|
+
async heartbeatTick() {
|
|
3023
|
+
if (!this.running) return;
|
|
3024
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3025
|
+
for (const axon of this._axons.values()) {
|
|
3026
|
+
try {
|
|
3027
|
+
if (this.reregisterOnHeartbeat) await this.emitRegister(axon);
|
|
3028
|
+
await this.synapse.publish(
|
|
3029
|
+
this.subject(SignalType.HEARTBEAT),
|
|
3030
|
+
heartbeatSignal({ directed: { id: axon.neuronId } })
|
|
3031
|
+
);
|
|
3032
|
+
} catch {
|
|
3033
|
+
}
|
|
3034
|
+
if (this.registryStore !== null) {
|
|
3035
|
+
try {
|
|
3036
|
+
await this.registryStore.touchHeartbeat(axon.neuronId, now);
|
|
3037
|
+
} catch {
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
await this.hooks._fireRefresh({ reason: "heartbeat", neuronId: axon.neuronId, extra: {} });
|
|
3041
|
+
await axon.hooks._fireRefresh({ reason: "heartbeat", neuronId: axon.neuronId, extra: {} });
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
async mirrorToStore(axon, status) {
|
|
3045
|
+
if (this.registryStore === null) return;
|
|
3046
|
+
try {
|
|
3047
|
+
await this.registryStore.upsert(
|
|
3048
|
+
neuronRecord({
|
|
3049
|
+
neuron_id: axon.neuronId,
|
|
3050
|
+
capabilities: [...axon.capabilities],
|
|
3051
|
+
version: axon.version ?? null,
|
|
3052
|
+
status,
|
|
3053
|
+
last_heartbeat: (/* @__PURE__ */ new Date()).toISOString()
|
|
3054
|
+
})
|
|
3055
|
+
);
|
|
3056
|
+
} catch {
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
async dispatchInbound(signal) {
|
|
3060
|
+
if (!AXON_TYPES.has(signal.type)) return;
|
|
3061
|
+
if (this.registryStore !== null) {
|
|
3062
|
+
try {
|
|
3063
|
+
await this.updateRegistry(signal);
|
|
3064
|
+
} catch {
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
const handlers = this.handlers.get(signal.type) ?? [];
|
|
3068
|
+
if (!handlers.length) return;
|
|
3069
|
+
await Promise.allSettled(handlers.map((h) => h(signal)));
|
|
3070
|
+
}
|
|
3071
|
+
async updateRegistry(signal) {
|
|
3072
|
+
if (this.registryStore === null) return;
|
|
3073
|
+
if (signal.payload["engram"]) return;
|
|
3074
|
+
const neuronId = signal.directed?.id ?? null;
|
|
3075
|
+
if (!neuronId) return;
|
|
3076
|
+
let reason = null;
|
|
3077
|
+
if (signal.type === SignalType.REGISTER) {
|
|
3078
|
+
await this.registryStore.upsert(
|
|
3079
|
+
neuronRecord({
|
|
3080
|
+
neuron_id: neuronId,
|
|
3081
|
+
capabilities: signal.payload["capabilities"] ?? [],
|
|
3082
|
+
version: signal.payload["version"] ?? null,
|
|
3083
|
+
status: "registered",
|
|
3084
|
+
last_heartbeat: signal.ts
|
|
3085
|
+
})
|
|
3086
|
+
);
|
|
3087
|
+
reason = "register";
|
|
3088
|
+
} else if (signal.type === SignalType.DEREGISTER) {
|
|
3089
|
+
await this.registryStore.markDeregistered(neuronId);
|
|
3090
|
+
reason = "deregister";
|
|
3091
|
+
} else if (signal.type === SignalType.HEARTBEAT) {
|
|
3092
|
+
const status = signal.payload["status"];
|
|
3093
|
+
if (status) await this.registryStore.touchHeartbeat(neuronId, signal.ts, status);
|
|
3094
|
+
else await this.registryStore.touchHeartbeat(neuronId, signal.ts);
|
|
3095
|
+
reason = "heartbeat";
|
|
3096
|
+
}
|
|
3097
|
+
if (reason !== null) {
|
|
3098
|
+
await this.hooks._fireRefresh({ reason, neuronId, extra: {} });
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
};
|
|
3102
|
+
var Cortex = Dendrite;
|
|
3103
|
+
|
|
3104
|
+
// src/engram.ts
|
|
3105
|
+
var EngramBinding = class {
|
|
3106
|
+
name;
|
|
3107
|
+
directedId;
|
|
3108
|
+
directedType;
|
|
3109
|
+
defaultDeadlineMs;
|
|
3110
|
+
defaultRecallMode;
|
|
3111
|
+
constructor(init) {
|
|
3112
|
+
this.name = init.name;
|
|
3113
|
+
this.directedId = init.directedId ?? null;
|
|
3114
|
+
this.directedType = init.directedType ?? null;
|
|
3115
|
+
this.defaultDeadlineMs = init.defaultDeadlineMs ?? null;
|
|
3116
|
+
this.defaultRecallMode = init.defaultRecallMode ?? "first";
|
|
3117
|
+
if (!this.directedId && !this.directedType) {
|
|
3118
|
+
throw new Error(
|
|
3119
|
+
`EngramBinding '${this.name}' requires directedId (engram_id) or directedType (engram_kind), or both`
|
|
3120
|
+
);
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
/** Build the `Directed` addressing this Engram. */
|
|
3124
|
+
toDirected() {
|
|
3125
|
+
return { id: this.directedId, type: this.directedType, capabilities: [] };
|
|
3126
|
+
}
|
|
3127
|
+
};
|
|
3128
|
+
var EngramError = class extends Error {
|
|
3129
|
+
constructor(message) {
|
|
3130
|
+
super(message);
|
|
3131
|
+
this.name = new.target.name;
|
|
3132
|
+
}
|
|
3133
|
+
};
|
|
3134
|
+
var EngramTimeout = class extends EngramError {
|
|
3135
|
+
};
|
|
3136
|
+
var EngramCancelled = class extends EngramError {
|
|
3137
|
+
};
|
|
3138
|
+
var EngramNotBound = class extends EngramError {
|
|
3139
|
+
};
|
|
3140
|
+
var EngramOverloaded = class extends EngramError {
|
|
3141
|
+
};
|
|
3142
|
+
var Engram = class {
|
|
3143
|
+
version = null;
|
|
3144
|
+
/** Return false if this Engram cannot satisfy the query. Default: serve all. */
|
|
3145
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
3146
|
+
async canServe(_query) {
|
|
3147
|
+
return true;
|
|
3148
|
+
}
|
|
3149
|
+
};
|
|
3150
|
+
function receipt(engramId, op, fields = {}) {
|
|
3151
|
+
const error = fields.error ?? null;
|
|
3152
|
+
return {
|
|
3153
|
+
engramId,
|
|
3154
|
+
op,
|
|
3155
|
+
id: fields.id ?? null,
|
|
3156
|
+
version: fields.version ?? null,
|
|
3157
|
+
tookMs: fields.tookMs ?? null,
|
|
3158
|
+
error,
|
|
3159
|
+
ok: error === null
|
|
3160
|
+
};
|
|
3161
|
+
}
|
|
3162
|
+
function entryToDict(e) {
|
|
3163
|
+
const out = {
|
|
3164
|
+
id: e.id,
|
|
3165
|
+
content: e.content,
|
|
3166
|
+
tags: [...e.tags],
|
|
3167
|
+
version: e.version,
|
|
3168
|
+
created_at: e.createdAt,
|
|
3169
|
+
updated_at: e.updatedAt
|
|
3170
|
+
};
|
|
3171
|
+
if (e.mergeKey !== null) out["merge_key"] = e.mergeKey;
|
|
3172
|
+
if (Object.keys(e.extra).length > 0) out["meta"] = { ...e.extra };
|
|
3173
|
+
return out;
|
|
3174
|
+
}
|
|
3175
|
+
function asStringArray(v) {
|
|
3176
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
3177
|
+
}
|
|
3178
|
+
function asObject(v) {
|
|
3179
|
+
return v !== null && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
3180
|
+
}
|
|
3181
|
+
var InMemoryEngram = class extends Engram {
|
|
3182
|
+
engramId;
|
|
3183
|
+
engramKind;
|
|
3184
|
+
capabilities;
|
|
3185
|
+
entries = /* @__PURE__ */ new Map();
|
|
3186
|
+
byMergeKey = /* @__PURE__ */ new Map();
|
|
3187
|
+
imprintSeen = /* @__PURE__ */ new Map();
|
|
3188
|
+
constructor(init = {}) {
|
|
3189
|
+
super();
|
|
3190
|
+
this.engramId = init.engramId ?? "engram-memory";
|
|
3191
|
+
this.engramKind = init.engramKind ?? "keyvalue";
|
|
3192
|
+
this.capabilities = init.capabilities ?? ["substring", "tags", "merge_key"];
|
|
3193
|
+
this.version = init.version ?? "0.0.1";
|
|
3194
|
+
}
|
|
3195
|
+
async connect() {
|
|
3196
|
+
return;
|
|
3197
|
+
}
|
|
3198
|
+
async close() {
|
|
3199
|
+
this.entries.clear();
|
|
3200
|
+
this.byMergeKey.clear();
|
|
3201
|
+
this.imprintSeen.clear();
|
|
3202
|
+
}
|
|
3203
|
+
async recall(query, opts = {}) {
|
|
3204
|
+
const q = query ?? {};
|
|
3205
|
+
const text = typeof q["text"] === "string" ? q["text"].toLowerCase() : "";
|
|
3206
|
+
const tagQ = typeof q["tag"] === "string" ? q["tag"] : null;
|
|
3207
|
+
const mergeKey = typeof q["merge_key"] === "string" ? q["merge_key"] : null;
|
|
3208
|
+
const topK = typeof q["top_k"] === "number" ? q["top_k"] : 50;
|
|
3209
|
+
const filters = opts.filters ?? {};
|
|
3210
|
+
const requireTags = asStringArray(filters["tags"]);
|
|
3211
|
+
const since = typeof filters["since"] === "string" ? Date.parse(filters["since"]) : NaN;
|
|
3212
|
+
const until = typeof filters["until"] === "string" ? Date.parse(filters["until"]) : NaN;
|
|
3213
|
+
let candidates;
|
|
3214
|
+
if (mergeKey !== null) {
|
|
3215
|
+
const ids = this.byMergeKey.get(mergeKey) ?? [];
|
|
3216
|
+
candidates = ids.map((i) => this.entries.get(i)).filter((e) => e !== void 0);
|
|
3217
|
+
} else {
|
|
3218
|
+
candidates = [...this.entries.values()];
|
|
3219
|
+
}
|
|
3220
|
+
const hits = [];
|
|
3221
|
+
for (const ent of candidates) {
|
|
3222
|
+
if (requireTags.length > 0 && !requireTags.every((t) => ent.tags.includes(t))) continue;
|
|
3223
|
+
const updated = Date.parse(ent.updatedAt);
|
|
3224
|
+
if (!Number.isNaN(since) && updated < since) continue;
|
|
3225
|
+
if (!Number.isNaN(until) && updated > until) continue;
|
|
3226
|
+
if (tagQ !== null && !ent.tags.includes(tagQ)) continue;
|
|
3227
|
+
let score = 1;
|
|
3228
|
+
if (text) {
|
|
3229
|
+
const hay = String(ent.content).toLowerCase();
|
|
3230
|
+
if (!hay.includes(text)) continue;
|
|
3231
|
+
score = Math.min(1, text.length / Math.max(1, hay.length));
|
|
3232
|
+
}
|
|
3233
|
+
if (opts.minConfidence !== void 0 && score < opts.minConfidence) continue;
|
|
3234
|
+
hits.push({ id: ent.id, entry: entryToDict(ent), score });
|
|
3235
|
+
}
|
|
3236
|
+
hits.sort((a, b) => b.score - a.score);
|
|
3237
|
+
return hits.slice(0, topK);
|
|
3238
|
+
}
|
|
3239
|
+
async imprint(op, entry, opts = {}) {
|
|
3240
|
+
const t0 = Date.now();
|
|
3241
|
+
const mergeKey = opts.mergeKey ?? null;
|
|
3242
|
+
const tookMs = () => Date.now() - t0;
|
|
3243
|
+
if (opts.imprintId !== void 0) {
|
|
3244
|
+
const seen = this.imprintSeen.get(opts.imprintId);
|
|
3245
|
+
if (seen !== void 0) {
|
|
3246
|
+
const existing = this.entries.get(seen);
|
|
3247
|
+
return receipt(this.engramId, op, {
|
|
3248
|
+
id: seen,
|
|
3249
|
+
version: existing ? existing.version : null,
|
|
3250
|
+
tookMs: tookMs()
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
let resultingId = null;
|
|
3255
|
+
let version = null;
|
|
3256
|
+
if (op === "add") {
|
|
3257
|
+
const ent = this.makeEntry(entry, mergeKey);
|
|
3258
|
+
if (this.entries.has(ent.id)) {
|
|
3259
|
+
return receipt(this.engramId, op, { error: `entry id '${ent.id}' already exists`, tookMs: tookMs() });
|
|
3260
|
+
}
|
|
3261
|
+
this.store(ent);
|
|
3262
|
+
resultingId = ent.id;
|
|
3263
|
+
version = ent.version;
|
|
3264
|
+
} else if (op === "append") {
|
|
3265
|
+
let ent = this.makeEntry(entry, mergeKey);
|
|
3266
|
+
while (this.entries.has(ent.id)) {
|
|
3267
|
+
ent = this.makeEntry({ ...entry, id: newEngramId() }, mergeKey);
|
|
3268
|
+
}
|
|
3269
|
+
this.store(ent);
|
|
3270
|
+
resultingId = ent.id;
|
|
3271
|
+
version = ent.version;
|
|
3272
|
+
} else if (op === "upsert") {
|
|
3273
|
+
const existingIds = this.byMergeKey.get(mergeKey ?? "") ?? [];
|
|
3274
|
+
const targetId = existingIds[existingIds.length - 1];
|
|
3275
|
+
const old = targetId !== void 0 ? this.entries.get(targetId) : void 0;
|
|
3276
|
+
if (old !== void 0) {
|
|
3277
|
+
const next = this.makeEntry({ ...entry, id: old.id }, mergeKey);
|
|
3278
|
+
next.createdAt = old.createdAt;
|
|
3279
|
+
next.version = old.version + 1;
|
|
3280
|
+
this.store(next, true);
|
|
3281
|
+
resultingId = next.id;
|
|
3282
|
+
version = next.version;
|
|
3283
|
+
} else {
|
|
3284
|
+
const ent = this.makeEntry(entry, mergeKey);
|
|
3285
|
+
this.store(ent);
|
|
3286
|
+
resultingId = ent.id;
|
|
3287
|
+
version = ent.version;
|
|
3288
|
+
}
|
|
3289
|
+
} else if (op === "merge") {
|
|
3290
|
+
const existingIds = this.byMergeKey.get(mergeKey ?? "") ?? [];
|
|
3291
|
+
const targetId = existingIds[existingIds.length - 1];
|
|
3292
|
+
const old = targetId !== void 0 ? this.entries.get(targetId) : void 0;
|
|
3293
|
+
if (old === void 0) {
|
|
3294
|
+
return receipt(this.engramId, op, { error: `no entry for merge_key='${mergeKey}'`, tookMs: tookMs() });
|
|
3295
|
+
}
|
|
3296
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3297
|
+
const next = {
|
|
3298
|
+
id: old.id,
|
|
3299
|
+
content: deepMerge(old.content, entry["content"]),
|
|
3300
|
+
tags: [.../* @__PURE__ */ new Set([...old.tags, ...asStringArray(entry["tags"])])],
|
|
3301
|
+
mergeKey: old.mergeKey,
|
|
3302
|
+
version: old.version + 1,
|
|
3303
|
+
createdAt: old.createdAt,
|
|
3304
|
+
updatedAt: now,
|
|
3305
|
+
extra: asObject(deepMerge(old.extra, entry["meta"]))
|
|
3306
|
+
};
|
|
3307
|
+
this.store(next, true);
|
|
3308
|
+
resultingId = next.id;
|
|
3309
|
+
version = next.version;
|
|
3310
|
+
} else if (op === "delete") {
|
|
3311
|
+
let targetId = null;
|
|
3312
|
+
const entId = entry["id"];
|
|
3313
|
+
if (typeof entId === "string") {
|
|
3314
|
+
targetId = entId;
|
|
3315
|
+
} else if (mergeKey !== null) {
|
|
3316
|
+
const ids = this.byMergeKey.get(mergeKey) ?? [];
|
|
3317
|
+
targetId = ids[ids.length - 1] ?? null;
|
|
3318
|
+
}
|
|
3319
|
+
if (targetId === null || !this.entries.has(targetId)) {
|
|
3320
|
+
return receipt(this.engramId, op, { tookMs: tookMs() });
|
|
3321
|
+
}
|
|
3322
|
+
this.evict(targetId);
|
|
3323
|
+
resultingId = targetId;
|
|
3324
|
+
version = null;
|
|
3325
|
+
} else {
|
|
3326
|
+
return receipt(this.engramId, op, { error: `unknown op '${op}'`, tookMs: tookMs() });
|
|
3327
|
+
}
|
|
3328
|
+
if (opts.imprintId !== void 0 && resultingId !== null) {
|
|
3329
|
+
this.imprintSeen.set(opts.imprintId, resultingId);
|
|
3330
|
+
}
|
|
3331
|
+
return receipt(this.engramId, op, { id: resultingId, version, tookMs: tookMs() });
|
|
3332
|
+
}
|
|
3333
|
+
/** Test/debug helper - NOT part of the Engram contract. */
|
|
3334
|
+
snapshot() {
|
|
3335
|
+
return [...this.entries.values()].map(entryToDict);
|
|
3336
|
+
}
|
|
3337
|
+
makeEntry(entry, mergeKey) {
|
|
3338
|
+
const id = typeof entry["id"] === "string" ? entry["id"] : newEngramId();
|
|
3339
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3340
|
+
return {
|
|
3341
|
+
id,
|
|
3342
|
+
content: entry["content"],
|
|
3343
|
+
tags: asStringArray(entry["tags"]),
|
|
3344
|
+
mergeKey,
|
|
3345
|
+
version: 1,
|
|
3346
|
+
createdAt: now,
|
|
3347
|
+
updatedAt: now,
|
|
3348
|
+
extra: asObject(entry["meta"])
|
|
3349
|
+
};
|
|
3350
|
+
}
|
|
3351
|
+
store(ent, replace = false) {
|
|
3352
|
+
if (replace) {
|
|
3353
|
+
const old = this.entries.get(ent.id);
|
|
3354
|
+
if (old !== void 0 && old.mergeKey) {
|
|
3355
|
+
const bucket = this.byMergeKey.get(old.mergeKey);
|
|
3356
|
+
if (bucket) {
|
|
3357
|
+
const idx = bucket.indexOf(ent.id);
|
|
3358
|
+
if (idx >= 0) bucket.splice(idx, 1);
|
|
3359
|
+
if (bucket.length === 0) this.byMergeKey.delete(old.mergeKey);
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
this.entries.set(ent.id, ent);
|
|
3364
|
+
if (ent.mergeKey) {
|
|
3365
|
+
const bucket = this.byMergeKey.get(ent.mergeKey);
|
|
3366
|
+
if (bucket) bucket.push(ent.id);
|
|
3367
|
+
else this.byMergeKey.set(ent.mergeKey, [ent.id]);
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
evict(entryId) {
|
|
3371
|
+
const ent = this.entries.get(entryId);
|
|
3372
|
+
this.entries.delete(entryId);
|
|
3373
|
+
if (ent === void 0) return;
|
|
3374
|
+
if (ent.mergeKey) {
|
|
3375
|
+
const bucket = this.byMergeKey.get(ent.mergeKey);
|
|
3376
|
+
if (bucket) {
|
|
3377
|
+
const idx = bucket.indexOf(entryId);
|
|
3378
|
+
if (idx >= 0) bucket.splice(idx, 1);
|
|
3379
|
+
if (bucket.length === 0) this.byMergeKey.delete(ent.mergeKey);
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
};
|
|
3384
|
+
function deepMerge(base, incoming) {
|
|
3385
|
+
if (incoming === void 0 || incoming === null) return base;
|
|
3386
|
+
const bothObjects = base !== null && typeof base === "object" && !Array.isArray(base) && typeof incoming === "object" && !Array.isArray(incoming);
|
|
3387
|
+
if (bothObjects) {
|
|
3388
|
+
const out = { ...base };
|
|
3389
|
+
for (const [k, v] of Object.entries(incoming)) {
|
|
3390
|
+
out[k] = k in out ? deepMerge(out[k], v) : v;
|
|
3391
|
+
}
|
|
3392
|
+
return out;
|
|
3393
|
+
}
|
|
3394
|
+
if (Array.isArray(base) && Array.isArray(incoming)) {
|
|
3395
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3396
|
+
const out = [];
|
|
3397
|
+
for (const item of [...base, ...incoming]) {
|
|
3398
|
+
const key = JSON.stringify(item);
|
|
3399
|
+
if (seen.has(key)) continue;
|
|
3400
|
+
seen.add(key);
|
|
3401
|
+
out.push(item);
|
|
3402
|
+
}
|
|
3403
|
+
return out;
|
|
3404
|
+
}
|
|
3405
|
+
return incoming;
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3408
|
+
// src/engram-client.ts
|
|
3409
|
+
function deferred() {
|
|
3410
|
+
let resolve;
|
|
3411
|
+
let reject;
|
|
3412
|
+
const promise = new Promise((res, rej) => {
|
|
3413
|
+
resolve = res;
|
|
3414
|
+
reject = rej;
|
|
3415
|
+
});
|
|
3416
|
+
return { promise, resolve, reject };
|
|
3417
|
+
}
|
|
3418
|
+
var EngramClient = class {
|
|
3419
|
+
constructor(publisher) {
|
|
3420
|
+
this.publisher = publisher;
|
|
3421
|
+
}
|
|
3422
|
+
publisher;
|
|
3423
|
+
pendingRecalls = /* @__PURE__ */ new Map();
|
|
3424
|
+
pendingImprints = /* @__PURE__ */ new Map();
|
|
3425
|
+
byTrace = /* @__PURE__ */ new Map();
|
|
3426
|
+
async recall(args) {
|
|
3427
|
+
let engramId = args.engramId;
|
|
3428
|
+
let engramKind = args.engramKind;
|
|
3429
|
+
let deadlineMs = args.deadlineMs;
|
|
3430
|
+
let recallMode = args.recallMode;
|
|
3431
|
+
if (args.binding) {
|
|
3432
|
+
engramId = engramId ?? args.binding.directedId ?? void 0;
|
|
3433
|
+
engramKind = engramKind ?? args.binding.directedType ?? void 0;
|
|
3434
|
+
if (deadlineMs === void 0) deadlineMs = args.binding.defaultDeadlineMs ?? void 0;
|
|
3435
|
+
if (recallMode === void 0) recallMode = args.binding.defaultRecallMode;
|
|
3436
|
+
}
|
|
3437
|
+
const mode = recallMode ?? "first";
|
|
3438
|
+
const sig = recallSignal({
|
|
3439
|
+
traceId: args.traceId,
|
|
3440
|
+
parentId: args.parentId,
|
|
3441
|
+
directed: directedTo(engramId ?? null, { type: engramKind ?? null }),
|
|
3442
|
+
query: args.query,
|
|
3443
|
+
...args.filters !== void 0 ? { filters: args.filters } : {},
|
|
3444
|
+
...args.contextRef !== void 0 ? { contextRef: args.contextRef } : {},
|
|
3445
|
+
...deadlineMs !== void 0 ? { deadlineMs } : {},
|
|
3446
|
+
...args.minConfidence !== void 0 ? { minConfidence: args.minConfidence } : {},
|
|
3447
|
+
recallMode: mode,
|
|
3448
|
+
...args.meta !== void 0 ? { meta: args.meta } : {}
|
|
3449
|
+
});
|
|
3450
|
+
const d = deferred();
|
|
3451
|
+
const pending = { deferred: d, mode, timer: null, done: false, hitsSoFar: [], engrams: [] };
|
|
3452
|
+
this.pendingRecalls.set(sig.id, pending);
|
|
3453
|
+
this.track(args.traceId, sig.id);
|
|
3454
|
+
if (deadlineMs !== void 0 && deadlineMs > 0) {
|
|
3455
|
+
pending.timer = setTimeout(() => this.onRecallDeadline(sig.id), deadlineMs);
|
|
3456
|
+
}
|
|
3457
|
+
try {
|
|
3458
|
+
await this.publisher.publish(sig);
|
|
3459
|
+
} catch (err) {
|
|
3460
|
+
this.cleanupRecall(args.traceId, sig.id);
|
|
3461
|
+
throw err;
|
|
3462
|
+
}
|
|
3463
|
+
try {
|
|
3464
|
+
return await d.promise;
|
|
3465
|
+
} finally {
|
|
3466
|
+
this.cleanupRecall(args.traceId, sig.id);
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
async imprint(args) {
|
|
3470
|
+
let engramId = args.engramId;
|
|
3471
|
+
let engramKind = args.engramKind;
|
|
3472
|
+
if (args.binding) {
|
|
3473
|
+
engramId = engramId ?? args.binding.directedId ?? void 0;
|
|
3474
|
+
engramKind = engramKind ?? args.binding.directedType ?? void 0;
|
|
3475
|
+
}
|
|
3476
|
+
const sig = imprintSignal({
|
|
3477
|
+
traceId: args.traceId,
|
|
3478
|
+
parentId: args.parentId,
|
|
3479
|
+
directed: directedTo(engramId ?? null, { type: engramKind ?? null }),
|
|
3480
|
+
op: args.op,
|
|
3481
|
+
entry: args.entry,
|
|
3482
|
+
...args.mergeKey !== void 0 ? { mergeKey: args.mergeKey } : {},
|
|
3483
|
+
...args.meta !== void 0 ? { meta: args.meta } : {}
|
|
3484
|
+
});
|
|
3485
|
+
if (!args.awaitAck) {
|
|
3486
|
+
await this.publisher.publish(sig);
|
|
3487
|
+
return null;
|
|
3488
|
+
}
|
|
3489
|
+
const d = deferred();
|
|
3490
|
+
const pending = { deferred: d, timer: null, done: false };
|
|
3491
|
+
this.pendingImprints.set(sig.id, pending);
|
|
3492
|
+
this.track(args.traceId, sig.id);
|
|
3493
|
+
if (args.deadlineMs !== void 0 && args.deadlineMs > 0) {
|
|
3494
|
+
pending.timer = setTimeout(() => this.onImprintDeadline(sig.id), args.deadlineMs);
|
|
3495
|
+
}
|
|
3496
|
+
try {
|
|
3497
|
+
await this.publisher.publish(sig);
|
|
3498
|
+
} catch (err) {
|
|
3499
|
+
this.cleanupImprint(args.traceId, sig.id);
|
|
3500
|
+
throw err;
|
|
3501
|
+
}
|
|
3502
|
+
try {
|
|
3503
|
+
return await d.promise;
|
|
3504
|
+
} finally {
|
|
3505
|
+
this.cleanupImprint(args.traceId, sig.id);
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
/** Match RECALLED / IMPRINTED by parent_id and resolve pendings. */
|
|
3509
|
+
deliver(sig) {
|
|
3510
|
+
const pid = sig.parent_id;
|
|
3511
|
+
if (pid === null) return;
|
|
3512
|
+
if (sig.type === SignalType.RECALLED) {
|
|
3513
|
+
const pending = this.pendingRecalls.get(pid);
|
|
3514
|
+
if (pending === void 0) return;
|
|
3515
|
+
const hits = hitsFromPayload(sig.payload["hits"]);
|
|
3516
|
+
const engramId = typeof sig.payload["engram_id"] === "string" ? sig.payload["engram_id"] : "";
|
|
3517
|
+
const tookMs = typeof sig.payload["took_ms"] === "number" ? sig.payload["took_ms"] : null;
|
|
3518
|
+
const truncated = sig.payload["truncated"] === true;
|
|
3519
|
+
if (pending.mode === "first") {
|
|
3520
|
+
if (!pending.done) {
|
|
3521
|
+
pending.done = true;
|
|
3522
|
+
pending.deferred.resolve({
|
|
3523
|
+
hits,
|
|
3524
|
+
engramIds: engramId ? [engramId] : [],
|
|
3525
|
+
truncated,
|
|
3526
|
+
tookMs
|
|
3527
|
+
});
|
|
3528
|
+
}
|
|
3529
|
+
} else {
|
|
3530
|
+
pending.hitsSoFar.push(...hits);
|
|
3531
|
+
if (engramId) pending.engrams.push(engramId);
|
|
3532
|
+
}
|
|
3533
|
+
} else if (sig.type === SignalType.IMPRINTED) {
|
|
3534
|
+
const pending = this.pendingImprints.get(pid);
|
|
3535
|
+
if (pending === void 0 || pending.done) return;
|
|
3536
|
+
pending.done = true;
|
|
3537
|
+
pending.deferred.resolve({
|
|
3538
|
+
engramId: typeof sig.payload["engram_id"] === "string" ? sig.payload["engram_id"] : "",
|
|
3539
|
+
op: typeof sig.payload["op"] === "string" ? sig.payload["op"] : "",
|
|
3540
|
+
id: typeof sig.payload["id"] === "string" ? sig.payload["id"] : null,
|
|
3541
|
+
version: typeof sig.payload["version"] === "number" ? sig.payload["version"] : null,
|
|
3542
|
+
tookMs: typeof sig.payload["took_ms"] === "number" ? sig.payload["took_ms"] : null,
|
|
3543
|
+
error: typeof sig.payload["error"] === "string" ? sig.payload["error"] : null,
|
|
3544
|
+
ok: !(typeof sig.payload["error"] === "string")
|
|
3545
|
+
});
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
/** Cancel every in-flight recall/imprint on a trace (FINAL/ERROR or shutdown). */
|
|
3549
|
+
cancelTrace(traceId) {
|
|
3550
|
+
const ids = this.byTrace.get(traceId);
|
|
3551
|
+
this.byTrace.delete(traceId);
|
|
3552
|
+
if (ids === void 0) return;
|
|
3553
|
+
for (const id of ids) {
|
|
3554
|
+
const pr = this.pendingRecalls.get(id);
|
|
3555
|
+
if (pr !== void 0 && !pr.done) {
|
|
3556
|
+
pr.done = true;
|
|
3557
|
+
if (pr.timer !== null) clearTimeout(pr.timer);
|
|
3558
|
+
pr.deferred.reject(new EngramCancelled(`trace ${traceId} terminated while recall ${id} in flight`));
|
|
3559
|
+
this.pendingRecalls.delete(id);
|
|
3560
|
+
}
|
|
3561
|
+
const pi = this.pendingImprints.get(id);
|
|
3562
|
+
if (pi !== void 0 && !pi.done) {
|
|
3563
|
+
pi.done = true;
|
|
3564
|
+
if (pi.timer !== null) clearTimeout(pi.timer);
|
|
3565
|
+
pi.deferred.reject(new EngramCancelled(`trace ${traceId} terminated while imprint ${id} in flight`));
|
|
3566
|
+
this.pendingImprints.delete(id);
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
cancelAll() {
|
|
3571
|
+
for (const traceId of [...this.byTrace.keys()]) this.cancelTrace(traceId);
|
|
3572
|
+
}
|
|
3573
|
+
onRecallDeadline(id) {
|
|
3574
|
+
const pending = this.pendingRecalls.get(id);
|
|
3575
|
+
if (pending === void 0 || pending.done) return;
|
|
3576
|
+
pending.done = true;
|
|
3577
|
+
if (pending.mode === "first") {
|
|
3578
|
+
pending.deferred.reject(new EngramTimeout(`RECALL ${id} elapsed deadline without any responder`));
|
|
3579
|
+
} else {
|
|
3580
|
+
pending.deferred.resolve({
|
|
3581
|
+
hits: [...pending.hitsSoFar].sort((a, b) => b.score - a.score),
|
|
3582
|
+
engramIds: [...pending.engrams],
|
|
3583
|
+
truncated: false,
|
|
3584
|
+
tookMs: null
|
|
3585
|
+
});
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
onImprintDeadline(id) {
|
|
3589
|
+
const pending = this.pendingImprints.get(id);
|
|
3590
|
+
if (pending === void 0 || pending.done) return;
|
|
3591
|
+
pending.done = true;
|
|
3592
|
+
pending.deferred.reject(new EngramTimeout(`IMPRINT ${id} elapsed deadline without IMPRINTED`));
|
|
3593
|
+
}
|
|
3594
|
+
track(traceId, id) {
|
|
3595
|
+
const bucket = this.byTrace.get(traceId);
|
|
3596
|
+
if (bucket) bucket.add(id);
|
|
3597
|
+
else this.byTrace.set(traceId, /* @__PURE__ */ new Set([id]));
|
|
3598
|
+
}
|
|
3599
|
+
cleanupRecall(traceId, id) {
|
|
3600
|
+
const p = this.pendingRecalls.get(id);
|
|
3601
|
+
if (p?.timer != null) clearTimeout(p.timer);
|
|
3602
|
+
this.pendingRecalls.delete(id);
|
|
3603
|
+
this.discardTrace(traceId, id);
|
|
3604
|
+
}
|
|
3605
|
+
cleanupImprint(traceId, id) {
|
|
3606
|
+
const p = this.pendingImprints.get(id);
|
|
3607
|
+
if (p?.timer != null) clearTimeout(p.timer);
|
|
3608
|
+
this.pendingImprints.delete(id);
|
|
3609
|
+
this.discardTrace(traceId, id);
|
|
3610
|
+
}
|
|
3611
|
+
discardTrace(traceId, id) {
|
|
3612
|
+
const bucket = this.byTrace.get(traceId);
|
|
3613
|
+
if (bucket === void 0) return;
|
|
3614
|
+
bucket.delete(id);
|
|
3615
|
+
if (bucket.size === 0) this.byTrace.delete(traceId);
|
|
3616
|
+
}
|
|
3617
|
+
};
|
|
3618
|
+
function hitsFromPayload(raw) {
|
|
3619
|
+
if (!Array.isArray(raw)) return [];
|
|
3620
|
+
const out = [];
|
|
3621
|
+
for (const h of raw) {
|
|
3622
|
+
if (h === null || typeof h !== "object") continue;
|
|
3623
|
+
const obj = h;
|
|
3624
|
+
const entryVal = obj["entry"];
|
|
3625
|
+
out.push({
|
|
3626
|
+
id: typeof obj["id"] === "string" ? obj["id"] : "",
|
|
3627
|
+
entry: entryVal !== null && typeof entryVal === "object" && !Array.isArray(entryVal) ? entryVal : { value: entryVal },
|
|
3628
|
+
score: typeof obj["score"] === "number" ? obj["score"] : 1
|
|
3629
|
+
});
|
|
3630
|
+
}
|
|
3631
|
+
return out;
|
|
3632
|
+
}
|
|
3633
|
+
|
|
3634
|
+
// src/engram-sqlite.ts
|
|
3635
|
+
var SCHEMA3 = `
|
|
3636
|
+
CREATE TABLE IF NOT EXISTS engram_entries (
|
|
3637
|
+
id TEXT PRIMARY KEY,
|
|
3638
|
+
engram_kind TEXT NOT NULL,
|
|
3639
|
+
merge_key TEXT,
|
|
3640
|
+
content TEXT NOT NULL,
|
|
3641
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
3642
|
+
meta TEXT NOT NULL DEFAULT '{}',
|
|
3643
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
3644
|
+
created_at TEXT NOT NULL,
|
|
3645
|
+
updated_at TEXT NOT NULL,
|
|
3646
|
+
deleted_at TEXT
|
|
3647
|
+
);
|
|
3648
|
+
CREATE INDEX IF NOT EXISTS engram_entries_kind_idx ON engram_entries (engram_kind);
|
|
3649
|
+
CREATE INDEX IF NOT EXISTS engram_entries_merge_key_idx ON engram_entries (merge_key) WHERE merge_key IS NOT NULL;
|
|
3650
|
+
CREATE INDEX IF NOT EXISTS engram_entries_updated_idx ON engram_entries (updated_at);
|
|
3651
|
+
CREATE TABLE IF NOT EXISTS engram_imprint_seen (
|
|
3652
|
+
imprint_id TEXT PRIMARY KEY,
|
|
3653
|
+
entry_id TEXT NOT NULL,
|
|
3654
|
+
seen_at TEXT NOT NULL
|
|
3655
|
+
);
|
|
3656
|
+
`;
|
|
3657
|
+
function nowIso() {
|
|
3658
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
3659
|
+
}
|
|
3660
|
+
function parseJson(s, fallback) {
|
|
3661
|
+
if (!s) return fallback;
|
|
3662
|
+
try {
|
|
3663
|
+
return JSON.parse(s);
|
|
3664
|
+
} catch {
|
|
3665
|
+
return fallback;
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
function rowToEntryDict(row) {
|
|
3669
|
+
const out = {
|
|
3670
|
+
id: row.id,
|
|
3671
|
+
content: parseJson(row.content, null),
|
|
3672
|
+
tags: parseJson(row.tags, []),
|
|
3673
|
+
version: row.version,
|
|
3674
|
+
created_at: row.created_at,
|
|
3675
|
+
updated_at: row.updated_at
|
|
3676
|
+
};
|
|
3677
|
+
if (row.merge_key !== null) out["merge_key"] = row.merge_key;
|
|
3678
|
+
const meta = parseJson(row.meta, {});
|
|
3679
|
+
if (meta !== null && typeof meta === "object" && Object.keys(meta).length > 0) {
|
|
3680
|
+
out["meta"] = meta;
|
|
3681
|
+
}
|
|
3682
|
+
return out;
|
|
3683
|
+
}
|
|
3684
|
+
function asStringArray2(v) {
|
|
3685
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
3686
|
+
}
|
|
3687
|
+
var SqliteEngram = class extends Engram {
|
|
3688
|
+
engramId;
|
|
3689
|
+
engramKind;
|
|
3690
|
+
capabilities;
|
|
3691
|
+
path;
|
|
3692
|
+
db = null;
|
|
3693
|
+
constructor(init = {}) {
|
|
3694
|
+
super();
|
|
3695
|
+
this.path = init.path ?? ":memory:";
|
|
3696
|
+
this.engramId = init.engramId ?? "engram-sqlite";
|
|
3697
|
+
this.engramKind = init.engramKind ?? "relational";
|
|
3698
|
+
this.capabilities = init.capabilities ?? ["substring", "tags", "merge_key", "time_range"];
|
|
3699
|
+
this.version = init.version ?? "0.0.1";
|
|
3700
|
+
}
|
|
3701
|
+
async connect() {
|
|
3702
|
+
if (this.db !== null) return;
|
|
3703
|
+
const specifier = "better-sqlite3";
|
|
3704
|
+
let mod;
|
|
3705
|
+
try {
|
|
3706
|
+
mod = await import(specifier);
|
|
3707
|
+
} catch (err) {
|
|
3708
|
+
throw new Error(
|
|
3709
|
+
"SqliteEngram requires the 'better-sqlite3' package. Install it with: npm i better-sqlite3" + (err instanceof Error ? ` (${err.message})` : "")
|
|
3710
|
+
);
|
|
3711
|
+
}
|
|
3712
|
+
const Database = mod.default;
|
|
3713
|
+
this.db = new Database(this.path);
|
|
3714
|
+
this.db.exec(SCHEMA3);
|
|
3715
|
+
}
|
|
3716
|
+
async close() {
|
|
3717
|
+
if (this.db === null) return;
|
|
3718
|
+
this.db.close();
|
|
3719
|
+
this.db = null;
|
|
3720
|
+
}
|
|
3721
|
+
require() {
|
|
3722
|
+
if (this.db === null) throw new Error("SqliteEngram.connect() not called");
|
|
3723
|
+
return this.db;
|
|
3724
|
+
}
|
|
3725
|
+
async recall(query, opts = {}) {
|
|
3726
|
+
const db = this.require();
|
|
3727
|
+
const q = query ?? {};
|
|
3728
|
+
const text = typeof q["text"] === "string" ? q["text"].toLowerCase() : "";
|
|
3729
|
+
const tagQ = typeof q["tag"] === "string" ? q["tag"] : null;
|
|
3730
|
+
const mergeKey = typeof q["merge_key"] === "string" ? q["merge_key"] : null;
|
|
3731
|
+
const topK = typeof q["top_k"] === "number" ? q["top_k"] : 50;
|
|
3732
|
+
const filters = opts.filters ?? {};
|
|
3733
|
+
const requireTags = asStringArray2(filters["tags"]);
|
|
3734
|
+
const since = typeof filters["since"] === "string" ? filters["since"] : null;
|
|
3735
|
+
const until = typeof filters["until"] === "string" ? filters["until"] : null;
|
|
3736
|
+
const clauses = ["deleted_at IS NULL"];
|
|
3737
|
+
const params = [];
|
|
3738
|
+
if (mergeKey !== null) {
|
|
3739
|
+
clauses.push("merge_key = ?");
|
|
3740
|
+
params.push(mergeKey);
|
|
3741
|
+
}
|
|
3742
|
+
if (since !== null) {
|
|
3743
|
+
clauses.push("updated_at >= ?");
|
|
3744
|
+
params.push(since);
|
|
3745
|
+
}
|
|
3746
|
+
if (until !== null) {
|
|
3747
|
+
clauses.push("updated_at <= ?");
|
|
3748
|
+
params.push(until);
|
|
3749
|
+
}
|
|
3750
|
+
const sql = `SELECT id, engram_kind, merge_key, content, tags, meta, version, created_at, updated_at, deleted_at FROM engram_entries WHERE ${clauses.join(" AND ")} ORDER BY updated_at DESC`;
|
|
3751
|
+
const rows = db.prepare(sql).all(...params);
|
|
3752
|
+
const hits = [];
|
|
3753
|
+
for (const row of rows) {
|
|
3754
|
+
const ent = rowToEntryDict(row);
|
|
3755
|
+
const tags = asStringArray2(ent["tags"]);
|
|
3756
|
+
if (requireTags.length > 0 && !requireTags.every((t) => tags.includes(t))) continue;
|
|
3757
|
+
if (tagQ !== null && !tags.includes(tagQ)) continue;
|
|
3758
|
+
let score = 1;
|
|
3759
|
+
if (text) {
|
|
3760
|
+
const hay = JSON.stringify(ent["content"]).toLowerCase();
|
|
3761
|
+
if (!hay.includes(text)) continue;
|
|
3762
|
+
score = Math.min(1, text.length / Math.max(1, hay.length));
|
|
3763
|
+
}
|
|
3764
|
+
if (opts.minConfidence !== void 0 && score < opts.minConfidence) continue;
|
|
3765
|
+
hits.push({ id: row.id, entry: ent, score });
|
|
3766
|
+
if (hits.length >= topK) break;
|
|
3767
|
+
}
|
|
3768
|
+
return hits;
|
|
3769
|
+
}
|
|
3770
|
+
async imprint(op, entry, opts = {}) {
|
|
3771
|
+
const db = this.require();
|
|
3772
|
+
const t0 = Date.now();
|
|
3773
|
+
const mergeKey = opts.mergeKey ?? null;
|
|
3774
|
+
const tookMs = () => Date.now() - t0;
|
|
3775
|
+
if (opts.imprintId !== void 0) {
|
|
3776
|
+
const seen = db.prepare("SELECT entry_id FROM engram_imprint_seen WHERE imprint_id = ?").get(opts.imprintId);
|
|
3777
|
+
if (seen !== void 0) {
|
|
3778
|
+
const verRow = db.prepare("SELECT version FROM engram_entries WHERE id = ?").get(seen.entry_id);
|
|
3779
|
+
return receipt(this.engramId, op, {
|
|
3780
|
+
id: seen.entry_id,
|
|
3781
|
+
version: verRow ? verRow.version : null,
|
|
3782
|
+
tookMs: tookMs()
|
|
3783
|
+
});
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
const insert = (id) => {
|
|
3787
|
+
db.prepare(
|
|
3788
|
+
"INSERT INTO engram_entries (id, engram_kind, merge_key, content, tags, meta, version, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)"
|
|
3789
|
+
).run(
|
|
3790
|
+
id,
|
|
3791
|
+
this.engramKind,
|
|
3792
|
+
mergeKey,
|
|
3793
|
+
JSON.stringify(entry["content"] ?? null),
|
|
3794
|
+
JSON.stringify(asStringArray2(entry["tags"])),
|
|
3795
|
+
JSON.stringify(entry["meta"] ?? {}),
|
|
3796
|
+
1,
|
|
3797
|
+
nowIso(),
|
|
3798
|
+
nowIso()
|
|
3799
|
+
);
|
|
3800
|
+
};
|
|
3801
|
+
let resultingId = null;
|
|
3802
|
+
let version = null;
|
|
3803
|
+
let error = null;
|
|
3804
|
+
if (op === "add") {
|
|
3805
|
+
const id = typeof entry["id"] === "string" ? entry["id"] : newEngramId();
|
|
3806
|
+
try {
|
|
3807
|
+
insert(id);
|
|
3808
|
+
resultingId = id;
|
|
3809
|
+
version = 1;
|
|
3810
|
+
} catch (err) {
|
|
3811
|
+
error = `add: id collision (${err instanceof Error ? err.message : String(err)})`;
|
|
3812
|
+
}
|
|
3813
|
+
} else if (op === "append") {
|
|
3814
|
+
const id = newEngramId();
|
|
3815
|
+
insert(id);
|
|
3816
|
+
resultingId = id;
|
|
3817
|
+
version = 1;
|
|
3818
|
+
} else if (op === "upsert") {
|
|
3819
|
+
if (mergeKey === null) {
|
|
3820
|
+
error = "upsert requires merge_key";
|
|
3821
|
+
} else {
|
|
3822
|
+
const existing = db.prepare(
|
|
3823
|
+
"SELECT id, version FROM engram_entries WHERE merge_key = ? AND deleted_at IS NULL ORDER BY updated_at DESC LIMIT 1"
|
|
3824
|
+
).get(mergeKey);
|
|
3825
|
+
if (existing === void 0) {
|
|
3826
|
+
const id = typeof entry["id"] === "string" ? entry["id"] : newEngramId();
|
|
3827
|
+
insert(id);
|
|
3828
|
+
resultingId = id;
|
|
3829
|
+
version = 1;
|
|
3830
|
+
} else {
|
|
3831
|
+
version = existing.version + 1;
|
|
3832
|
+
db.prepare(
|
|
3833
|
+
"UPDATE engram_entries SET content = ?, tags = ?, meta = ?, version = ?, updated_at = ? WHERE id = ?"
|
|
3834
|
+
).run(
|
|
3835
|
+
JSON.stringify(entry["content"] ?? null),
|
|
3836
|
+
JSON.stringify(asStringArray2(entry["tags"])),
|
|
3837
|
+
JSON.stringify(entry["meta"] ?? {}),
|
|
3838
|
+
version,
|
|
3839
|
+
nowIso(),
|
|
3840
|
+
existing.id
|
|
3841
|
+
);
|
|
3842
|
+
resultingId = existing.id;
|
|
3843
|
+
}
|
|
3844
|
+
}
|
|
3845
|
+
} else if (op === "merge") {
|
|
3846
|
+
if (mergeKey === null) {
|
|
3847
|
+
error = "merge requires merge_key";
|
|
3848
|
+
} else {
|
|
3849
|
+
const existing = db.prepare(
|
|
3850
|
+
"SELECT id, content, tags, meta, version FROM engram_entries WHERE merge_key = ? AND deleted_at IS NULL ORDER BY updated_at DESC LIMIT 1"
|
|
3851
|
+
).get(mergeKey);
|
|
3852
|
+
if (existing === void 0) {
|
|
3853
|
+
error = `no entry for merge_key='${mergeKey}'`;
|
|
3854
|
+
} else {
|
|
3855
|
+
const newContent = deepMerge(parseJson(existing.content, null), entry["content"]);
|
|
3856
|
+
const newTags = [.../* @__PURE__ */ new Set([...asStringArray2(parseJson(existing.tags, [])), ...asStringArray2(entry["tags"])])];
|
|
3857
|
+
const mergedMeta = deepMerge(parseJson(existing.meta, {}), entry["meta"]);
|
|
3858
|
+
version = existing.version + 1;
|
|
3859
|
+
db.prepare(
|
|
3860
|
+
"UPDATE engram_entries SET content = ?, tags = ?, meta = ?, version = ?, updated_at = ? WHERE id = ?"
|
|
3861
|
+
).run(
|
|
3862
|
+
JSON.stringify(newContent),
|
|
3863
|
+
JSON.stringify(newTags),
|
|
3864
|
+
JSON.stringify(mergedMeta ?? {}),
|
|
3865
|
+
version,
|
|
3866
|
+
nowIso(),
|
|
3867
|
+
existing.id
|
|
3868
|
+
);
|
|
3869
|
+
resultingId = existing.id;
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
} else if (op === "delete") {
|
|
3873
|
+
let targetId = typeof entry["id"] === "string" ? entry["id"] : null;
|
|
3874
|
+
if (targetId === null && mergeKey !== null) {
|
|
3875
|
+
const row = db.prepare(
|
|
3876
|
+
"SELECT id FROM engram_entries WHERE merge_key = ? AND deleted_at IS NULL ORDER BY updated_at DESC LIMIT 1"
|
|
3877
|
+
).get(mergeKey);
|
|
3878
|
+
if (row !== void 0) targetId = row.id;
|
|
3879
|
+
}
|
|
3880
|
+
if (targetId !== null) {
|
|
3881
|
+
db.prepare("UPDATE engram_entries SET deleted_at = ? WHERE id = ?").run(nowIso(), targetId);
|
|
3882
|
+
resultingId = targetId;
|
|
3883
|
+
}
|
|
3884
|
+
} else {
|
|
3885
|
+
error = `unknown op '${op}'`;
|
|
3886
|
+
}
|
|
3887
|
+
if (opts.imprintId !== void 0 && resultingId !== null && error === null) {
|
|
3888
|
+
db.prepare(
|
|
3889
|
+
"INSERT OR IGNORE INTO engram_imprint_seen (imprint_id, entry_id, seen_at) VALUES (?,?,?)"
|
|
3890
|
+
).run(opts.imprintId, resultingId, nowIso());
|
|
3891
|
+
}
|
|
3892
|
+
return receipt(this.engramId, op, { id: resultingId, version, error, tookMs: tookMs() });
|
|
3893
|
+
}
|
|
3894
|
+
};
|
|
3895
|
+
|
|
3896
|
+
// src/engram-postgres.ts
|
|
3897
|
+
var SCHEMA4 = `
|
|
3898
|
+
CREATE TABLE IF NOT EXISTS cosmonapse_engram_entries (
|
|
3899
|
+
id TEXT PRIMARY KEY,
|
|
3900
|
+
engram_kind TEXT NOT NULL,
|
|
3901
|
+
merge_key TEXT,
|
|
3902
|
+
content JSONB NOT NULL,
|
|
3903
|
+
tags TEXT[] NOT NULL DEFAULT '{}',
|
|
3904
|
+
meta JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
3905
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
3906
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
3907
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
3908
|
+
deleted_at TIMESTAMPTZ
|
|
3909
|
+
);
|
|
3910
|
+
CREATE INDEX IF NOT EXISTS cosmonapse_engram_kind_idx ON cosmonapse_engram_entries (engram_kind);
|
|
3911
|
+
CREATE INDEX IF NOT EXISTS cosmonapse_engram_merge_key_idx ON cosmonapse_engram_entries (merge_key) WHERE merge_key IS NOT NULL;
|
|
3912
|
+
CREATE INDEX IF NOT EXISTS cosmonapse_engram_updated_idx ON cosmonapse_engram_entries (updated_at DESC);
|
|
3913
|
+
CREATE INDEX IF NOT EXISTS cosmonapse_engram_tags_gin ON cosmonapse_engram_entries USING gin (tags);
|
|
3914
|
+
CREATE TABLE IF NOT EXISTS cosmonapse_engram_imprint_seen (
|
|
3915
|
+
imprint_id TEXT PRIMARY KEY,
|
|
3916
|
+
entry_id TEXT NOT NULL,
|
|
3917
|
+
seen_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
3918
|
+
);
|
|
3919
|
+
`;
|
|
3920
|
+
function asStringArray3(v) {
|
|
3921
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
3922
|
+
}
|
|
3923
|
+
function asJson(v, fallback) {
|
|
3924
|
+
if (typeof v === "string") {
|
|
3925
|
+
try {
|
|
3926
|
+
return JSON.parse(v);
|
|
3927
|
+
} catch {
|
|
3928
|
+
return fallback;
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
return v ?? fallback;
|
|
3932
|
+
}
|
|
3933
|
+
function toIso2(v) {
|
|
3934
|
+
if (v === null || v === void 0) return null;
|
|
3935
|
+
if (v instanceof Date) return v.toISOString();
|
|
3936
|
+
return String(v);
|
|
3937
|
+
}
|
|
3938
|
+
function rowToEntryDict2(row) {
|
|
3939
|
+
const out = {
|
|
3940
|
+
id: String(row["id"]),
|
|
3941
|
+
content: asJson(row["content"], null),
|
|
3942
|
+
tags: asStringArray3(row["tags"]),
|
|
3943
|
+
version: typeof row["version"] === "number" ? row["version"] : Number(row["version"]),
|
|
3944
|
+
created_at: toIso2(row["created_at"]),
|
|
3945
|
+
updated_at: toIso2(row["updated_at"])
|
|
3946
|
+
};
|
|
3947
|
+
if (row["merge_key"] !== null && row["merge_key"] !== void 0) out["merge_key"] = row["merge_key"];
|
|
3948
|
+
const meta = asJson(row["meta"], {});
|
|
3949
|
+
if (meta !== null && typeof meta === "object" && Object.keys(meta).length > 0) {
|
|
3950
|
+
out["meta"] = meta;
|
|
3951
|
+
}
|
|
3952
|
+
return out;
|
|
3953
|
+
}
|
|
3954
|
+
var PostgresEngram = class extends Engram {
|
|
3955
|
+
engramId;
|
|
3956
|
+
engramKind;
|
|
3957
|
+
capabilities;
|
|
3958
|
+
dsn;
|
|
3959
|
+
minSize;
|
|
3960
|
+
maxSize;
|
|
3961
|
+
pool = null;
|
|
3962
|
+
constructor(init) {
|
|
3963
|
+
super();
|
|
3964
|
+
this.dsn = init.dsn;
|
|
3965
|
+
this.engramId = init.engramId ?? "engram-postgres";
|
|
3966
|
+
this.engramKind = init.engramKind ?? "relational";
|
|
3967
|
+
this.capabilities = init.capabilities ?? ["substring", "tags", "merge_key", "time_range", "jsonb"];
|
|
3968
|
+
this.version = init.version ?? "0.0.1";
|
|
3969
|
+
this.minSize = init.minSize ?? 1;
|
|
3970
|
+
this.maxSize = init.maxSize ?? 5;
|
|
3971
|
+
}
|
|
3972
|
+
async connect() {
|
|
3973
|
+
if (this.pool !== null) return;
|
|
3974
|
+
const specifier = "pg";
|
|
3975
|
+
let mod;
|
|
3976
|
+
try {
|
|
3977
|
+
mod = await import(specifier);
|
|
3978
|
+
} catch (err) {
|
|
3979
|
+
throw new Error(
|
|
3980
|
+
"PostgresEngram requires the 'pg' package. Install it with: npm i pg" + (err instanceof Error ? ` (${err.message})` : "")
|
|
3981
|
+
);
|
|
3982
|
+
}
|
|
3983
|
+
const Pool = mod.Pool ?? mod.default?.Pool;
|
|
3984
|
+
if (Pool === void 0) throw new Error("PostgresEngram: could not load Pool from 'pg'");
|
|
3985
|
+
this.pool = new Pool({ connectionString: this.dsn, min: this.minSize, max: this.maxSize });
|
|
3986
|
+
await this.pool.query(SCHEMA4);
|
|
3987
|
+
}
|
|
3988
|
+
async close() {
|
|
3989
|
+
if (this.pool === null) return;
|
|
3990
|
+
const pool = this.pool;
|
|
3991
|
+
this.pool = null;
|
|
3992
|
+
await pool.end();
|
|
3993
|
+
}
|
|
3994
|
+
require() {
|
|
3995
|
+
if (this.pool === null) throw new Error("PostgresEngram.connect() not called");
|
|
3996
|
+
return this.pool;
|
|
3997
|
+
}
|
|
3998
|
+
async recall(query, opts = {}) {
|
|
3999
|
+
const pool = this.require();
|
|
4000
|
+
const q = query ?? {};
|
|
4001
|
+
const text = typeof q["text"] === "string" ? q["text"].toLowerCase() : "";
|
|
4002
|
+
const tagQ = typeof q["tag"] === "string" ? q["tag"] : null;
|
|
4003
|
+
const mergeKey = typeof q["merge_key"] === "string" ? q["merge_key"] : null;
|
|
4004
|
+
const topK = typeof q["top_k"] === "number" ? q["top_k"] : 50;
|
|
4005
|
+
const filters = opts.filters ?? {};
|
|
4006
|
+
const requireTags = asStringArray3(filters["tags"]);
|
|
4007
|
+
const since = typeof filters["since"] === "string" ? filters["since"] : null;
|
|
4008
|
+
const until = typeof filters["until"] === "string" ? filters["until"] : null;
|
|
4009
|
+
const clauses = ["deleted_at IS NULL"];
|
|
4010
|
+
const params = [];
|
|
4011
|
+
const p = (value) => {
|
|
4012
|
+
params.push(value);
|
|
4013
|
+
return `$${params.length}`;
|
|
4014
|
+
};
|
|
4015
|
+
if (mergeKey !== null) clauses.push(`merge_key = ${p(mergeKey)}`);
|
|
4016
|
+
if (requireTags.length > 0) clauses.push(`tags @> ${p(requireTags)}`);
|
|
4017
|
+
if (tagQ !== null) clauses.push(`${p(tagQ)} = ANY(tags)`);
|
|
4018
|
+
if (since !== null) clauses.push(`updated_at >= ${p(since)}`);
|
|
4019
|
+
if (until !== null) clauses.push(`updated_at <= ${p(until)}`);
|
|
4020
|
+
if (text) clauses.push(`content::text ILIKE ${p(`%${text}%`)}`);
|
|
4021
|
+
const sql = `SELECT id, engram_kind, merge_key, content, tags, meta, version, created_at, updated_at, deleted_at FROM cosmonapse_engram_entries WHERE ${clauses.join(" AND ")} ORDER BY updated_at DESC LIMIT ${p(topK)}`;
|
|
4022
|
+
const res = await pool.query(sql, params);
|
|
4023
|
+
const hits = [];
|
|
4024
|
+
for (const row of res.rows) {
|
|
4025
|
+
const ent = rowToEntryDict2(row);
|
|
4026
|
+
let score = 1;
|
|
4027
|
+
if (text) {
|
|
4028
|
+
const hay = JSON.stringify(ent["content"]).toLowerCase();
|
|
4029
|
+
score = Math.min(1, text.length / Math.max(1, hay.length));
|
|
4030
|
+
}
|
|
4031
|
+
if (opts.minConfidence !== void 0 && score < opts.minConfidence) continue;
|
|
4032
|
+
hits.push({ id: String(row["id"]), entry: ent, score });
|
|
4033
|
+
}
|
|
4034
|
+
return hits;
|
|
4035
|
+
}
|
|
4036
|
+
async imprint(op, entry, opts = {}) {
|
|
4037
|
+
const pool = this.require();
|
|
4038
|
+
const t0 = Date.now();
|
|
4039
|
+
const mergeKey = opts.mergeKey ?? null;
|
|
4040
|
+
const tookMs = () => Date.now() - t0;
|
|
4041
|
+
const contentJson = JSON.stringify(entry["content"] ?? null);
|
|
4042
|
+
const tags = asStringArray3(entry["tags"]);
|
|
4043
|
+
const metaJson = JSON.stringify(entry["meta"] ?? {});
|
|
4044
|
+
let resultingId = null;
|
|
4045
|
+
let version = null;
|
|
4046
|
+
let error = null;
|
|
4047
|
+
const client = await pool.connect();
|
|
4048
|
+
try {
|
|
4049
|
+
await client.query("BEGIN");
|
|
4050
|
+
if (opts.imprintId !== void 0) {
|
|
4051
|
+
const seen = await client.query(
|
|
4052
|
+
"SELECT entry_id FROM cosmonapse_engram_imprint_seen WHERE imprint_id = $1",
|
|
4053
|
+
[opts.imprintId]
|
|
4054
|
+
);
|
|
4055
|
+
const seenRow = seen.rows[0];
|
|
4056
|
+
if (seenRow !== void 0) {
|
|
4057
|
+
const seenId = String(seenRow["entry_id"]);
|
|
4058
|
+
const ver = await client.query("SELECT version FROM cosmonapse_engram_entries WHERE id = $1", [seenId]);
|
|
4059
|
+
const verRow = ver.rows[0];
|
|
4060
|
+
await client.query("COMMIT");
|
|
4061
|
+
return receipt(this.engramId, op, {
|
|
4062
|
+
id: seenId,
|
|
4063
|
+
version: verRow !== void 0 ? Number(verRow["version"]) : null,
|
|
4064
|
+
tookMs: tookMs()
|
|
4065
|
+
});
|
|
4066
|
+
}
|
|
4067
|
+
}
|
|
4068
|
+
const insert = async (id) => {
|
|
4069
|
+
await client.query(
|
|
4070
|
+
"INSERT INTO cosmonapse_engram_entries (id, engram_kind, merge_key, content, tags, meta) VALUES ($1,$2,$3,$4::jsonb,$5,$6::jsonb)",
|
|
4071
|
+
[id, this.engramKind, mergeKey, contentJson, tags, metaJson]
|
|
4072
|
+
);
|
|
4073
|
+
};
|
|
4074
|
+
if (op === "add") {
|
|
4075
|
+
const id = typeof entry["id"] === "string" ? entry["id"] : newEngramId();
|
|
4076
|
+
try {
|
|
4077
|
+
await insert(id);
|
|
4078
|
+
resultingId = id;
|
|
4079
|
+
version = 1;
|
|
4080
|
+
} catch (err) {
|
|
4081
|
+
error = `add: ${err instanceof Error ? err.message : String(err)}`;
|
|
4082
|
+
}
|
|
4083
|
+
} else if (op === "append") {
|
|
4084
|
+
const id = newEngramId();
|
|
4085
|
+
await insert(id);
|
|
4086
|
+
resultingId = id;
|
|
4087
|
+
version = 1;
|
|
4088
|
+
} else if (op === "upsert") {
|
|
4089
|
+
if (mergeKey === null) {
|
|
4090
|
+
error = "upsert requires merge_key";
|
|
4091
|
+
} else {
|
|
4092
|
+
const existing = await client.query(
|
|
4093
|
+
"SELECT id, version FROM cosmonapse_engram_entries WHERE merge_key = $1 AND deleted_at IS NULL ORDER BY updated_at DESC LIMIT 1",
|
|
4094
|
+
[mergeKey]
|
|
4095
|
+
);
|
|
4096
|
+
const row = existing.rows[0];
|
|
4097
|
+
if (row === void 0) {
|
|
4098
|
+
const id = typeof entry["id"] === "string" ? entry["id"] : newEngramId();
|
|
4099
|
+
await insert(id);
|
|
4100
|
+
resultingId = id;
|
|
4101
|
+
version = 1;
|
|
4102
|
+
} else {
|
|
4103
|
+
const id = String(row["id"]);
|
|
4104
|
+
version = Number(row["version"]) + 1;
|
|
4105
|
+
await client.query(
|
|
4106
|
+
"UPDATE cosmonapse_engram_entries SET content=$1::jsonb, tags=$2, meta=$3::jsonb, version=$4, updated_at=now() WHERE id=$5",
|
|
4107
|
+
[contentJson, tags, metaJson, version, id]
|
|
4108
|
+
);
|
|
4109
|
+
resultingId = id;
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
} else if (op === "merge") {
|
|
4113
|
+
if (mergeKey === null) {
|
|
4114
|
+
error = "merge requires merge_key";
|
|
4115
|
+
} else {
|
|
4116
|
+
const existing = await client.query(
|
|
4117
|
+
"SELECT id, content, tags, meta, version FROM cosmonapse_engram_entries WHERE merge_key = $1 AND deleted_at IS NULL ORDER BY updated_at DESC LIMIT 1",
|
|
4118
|
+
[mergeKey]
|
|
4119
|
+
);
|
|
4120
|
+
const row = existing.rows[0];
|
|
4121
|
+
if (row === void 0) {
|
|
4122
|
+
error = `no entry for merge_key='${mergeKey}'`;
|
|
4123
|
+
} else {
|
|
4124
|
+
const newContent = deepMerge(asJson(row["content"], null), entry["content"]);
|
|
4125
|
+
const newTags = [.../* @__PURE__ */ new Set([...asStringArray3(row["tags"]), ...tags])];
|
|
4126
|
+
const newMeta = deepMerge(asJson(row["meta"], {}), entry["meta"]);
|
|
4127
|
+
version = Number(row["version"]) + 1;
|
|
4128
|
+
await client.query(
|
|
4129
|
+
"UPDATE cosmonapse_engram_entries SET content=$1::jsonb, tags=$2, meta=$3::jsonb, version=$4, updated_at=now() WHERE id=$5",
|
|
4130
|
+
[JSON.stringify(newContent), newTags, JSON.stringify(newMeta ?? {}), version, String(row["id"])]
|
|
4131
|
+
);
|
|
4132
|
+
resultingId = String(row["id"]);
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
} else if (op === "delete") {
|
|
4136
|
+
let targetId = typeof entry["id"] === "string" ? entry["id"] : null;
|
|
4137
|
+
if (targetId === null && mergeKey !== null) {
|
|
4138
|
+
const row = await client.query(
|
|
4139
|
+
"SELECT id FROM cosmonapse_engram_entries WHERE merge_key = $1 AND deleted_at IS NULL ORDER BY updated_at DESC LIMIT 1",
|
|
4140
|
+
[mergeKey]
|
|
4141
|
+
);
|
|
4142
|
+
const r = row.rows[0];
|
|
4143
|
+
if (r !== void 0) targetId = String(r["id"]);
|
|
4144
|
+
}
|
|
4145
|
+
if (targetId !== null) {
|
|
4146
|
+
await client.query("UPDATE cosmonapse_engram_entries SET deleted_at = now() WHERE id = $1", [targetId]);
|
|
4147
|
+
resultingId = targetId;
|
|
4148
|
+
}
|
|
4149
|
+
} else {
|
|
4150
|
+
error = `unknown op '${op}'`;
|
|
4151
|
+
}
|
|
4152
|
+
if (opts.imprintId !== void 0 && resultingId !== null && error === null) {
|
|
4153
|
+
await client.query(
|
|
4154
|
+
"INSERT INTO cosmonapse_engram_imprint_seen (imprint_id, entry_id) VALUES ($1,$2) ON CONFLICT (imprint_id) DO NOTHING",
|
|
4155
|
+
[opts.imprintId, resultingId]
|
|
4156
|
+
);
|
|
4157
|
+
}
|
|
4158
|
+
await client.query("COMMIT");
|
|
4159
|
+
} catch (err) {
|
|
4160
|
+
await client.query("ROLLBACK");
|
|
4161
|
+
error = error ?? (err instanceof Error ? err.message : String(err));
|
|
4162
|
+
} finally {
|
|
4163
|
+
client.release();
|
|
4164
|
+
}
|
|
4165
|
+
return receipt(this.engramId, op, { id: resultingId, version, error, tookMs: tookMs() });
|
|
4166
|
+
}
|
|
4167
|
+
};
|
|
4168
|
+
|
|
4169
|
+
// src/index.ts
|
|
4170
|
+
var VERSION = true ? "0.1.2" : "0.0.0-dev";
|
|
4171
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
4172
|
+
0 && (module.exports = {
|
|
4173
|
+
AXON_TYPES,
|
|
4174
|
+
Axon,
|
|
4175
|
+
Cortex,
|
|
4176
|
+
CortexProtocolError,
|
|
4177
|
+
Dendrite,
|
|
4178
|
+
DendriteProtocolError,
|
|
4179
|
+
DevSynapse,
|
|
4180
|
+
DevSynapseServer,
|
|
4181
|
+
Engram,
|
|
4182
|
+
EngramBinding,
|
|
4183
|
+
EngramCancelled,
|
|
4184
|
+
EngramClient,
|
|
4185
|
+
EngramError,
|
|
4186
|
+
EngramNotBound,
|
|
4187
|
+
EngramOverloaded,
|
|
4188
|
+
EngramTimeout,
|
|
4189
|
+
InMemoryEngram,
|
|
4190
|
+
KafkaSynapse,
|
|
4191
|
+
LifecycleHooks,
|
|
4192
|
+
MemoryRegistryStore,
|
|
4193
|
+
MemorySynapse,
|
|
4194
|
+
NatsSynapse,
|
|
4195
|
+
PostgresEngram,
|
|
4196
|
+
PostgresRegistryStore,
|
|
4197
|
+
SYNAPSE_TYPES,
|
|
4198
|
+
SignalType,
|
|
4199
|
+
SqliteEngram,
|
|
4200
|
+
SqliteRegistryStore,
|
|
4201
|
+
VERSION,
|
|
4202
|
+
agentOutputSignal,
|
|
4203
|
+
anthropicNeuron,
|
|
4204
|
+
bidSignal,
|
|
4205
|
+
clarificationAnswerSignal,
|
|
4206
|
+
clarificationSignal,
|
|
4207
|
+
clarify,
|
|
4208
|
+
connectSynapse,
|
|
4209
|
+
consensusSignal,
|
|
4210
|
+
contextSyncSignal,
|
|
4211
|
+
createSignal,
|
|
4212
|
+
critiqueSignal,
|
|
4213
|
+
decode,
|
|
4214
|
+
deepMerge,
|
|
4215
|
+
deregisterSignal,
|
|
4216
|
+
directedTo,
|
|
4217
|
+
discoverSignal,
|
|
4218
|
+
encode,
|
|
4219
|
+
errorResult,
|
|
4220
|
+
errorSignal,
|
|
4221
|
+
escalationSignal,
|
|
4222
|
+
finalSignal,
|
|
4223
|
+
heartbeatSignal,
|
|
4224
|
+
huggingFaceNeuron,
|
|
4225
|
+
imprintSignal,
|
|
4226
|
+
imprintedSignal,
|
|
4227
|
+
isClarification,
|
|
4228
|
+
isErrorOutput,
|
|
4229
|
+
isPermissionRequest,
|
|
4230
|
+
mcpNeuron,
|
|
4231
|
+
memoryAppendSignal,
|
|
4232
|
+
neuron,
|
|
4233
|
+
neuronRecord,
|
|
4234
|
+
newEngramId,
|
|
4235
|
+
newEventId,
|
|
4236
|
+
newTraceId,
|
|
4237
|
+
normalizeDirected,
|
|
4238
|
+
ollamaNeuron,
|
|
4239
|
+
openaiNeuron,
|
|
4240
|
+
parseLlmIntents,
|
|
4241
|
+
parseMcpIntents,
|
|
4242
|
+
permissionDecisionSignal,
|
|
4243
|
+
permissionRequest,
|
|
4244
|
+
permissionSignal,
|
|
4245
|
+
planSignal,
|
|
4246
|
+
recallSignal,
|
|
4247
|
+
recalledSignal,
|
|
4248
|
+
registerSignal,
|
|
4249
|
+
reply,
|
|
4250
|
+
standardMcpServers,
|
|
4251
|
+
synapseFromUrl,
|
|
4252
|
+
taskOfferSignal,
|
|
4253
|
+
taskSignal,
|
|
4254
|
+
thoughtDeltaSignal,
|
|
4255
|
+
toolCallSignal,
|
|
4256
|
+
toolResultSignal,
|
|
4257
|
+
validateSignal
|
|
4258
|
+
});
|