@cosmonapse/sdk 0.1.2 → 0.1.3
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 +2757 -1059
- package/dist/index.d.cts +917 -285
- package/dist/index.d.ts +917 -285
- package/dist/index.js +2747 -1059
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -32,6 +32,7 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
AXON_TYPES: () => AXON_TYPES,
|
|
34
34
|
Axon: () => Axon,
|
|
35
|
+
COSMO_INTENT_SYSTEM_PROMPT: () => COSMO_INTENT_SYSTEM_PROMPT,
|
|
35
36
|
Cortex: () => Cortex,
|
|
36
37
|
CortexProtocolError: () => DendriteProtocolError,
|
|
37
38
|
Dendrite: () => Dendrite,
|
|
@@ -52,14 +53,19 @@ __export(index_exports, {
|
|
|
52
53
|
MemoryRegistryStore: () => MemoryRegistryStore,
|
|
53
54
|
MemorySynapse: () => MemorySynapse,
|
|
54
55
|
NatsSynapse: () => NatsSynapse,
|
|
56
|
+
PATHWAY_TYPES: () => PATHWAY_TYPES,
|
|
57
|
+
Pathway: () => Pathway,
|
|
58
|
+
PathwayClosedError: () => PathwayClosedError,
|
|
55
59
|
PostgresEngram: () => PostgresEngram,
|
|
56
60
|
PostgresRegistryStore: () => PostgresRegistryStore,
|
|
57
61
|
SYNAPSE_TYPES: () => SYNAPSE_TYPES,
|
|
58
62
|
SignalType: () => SignalType,
|
|
59
63
|
SqliteEngram: () => SqliteEngram,
|
|
60
64
|
SqliteRegistryStore: () => SqliteRegistryStore,
|
|
65
|
+
TERMINAL_TYPES: () => TERMINAL_TYPES,
|
|
61
66
|
VERSION: () => VERSION,
|
|
62
67
|
agentOutputSignal: () => agentOutputSignal,
|
|
68
|
+
ambientTrace: () => ambientTrace,
|
|
63
69
|
anthropicNeuron: () => anthropicNeuron,
|
|
64
70
|
bidSignal: () => bidSignal,
|
|
65
71
|
clarificationAnswerSignal: () => clarificationAnswerSignal,
|
|
@@ -80,6 +86,7 @@ __export(index_exports, {
|
|
|
80
86
|
errorSignal: () => errorSignal,
|
|
81
87
|
escalationSignal: () => escalationSignal,
|
|
82
88
|
finalSignal: () => finalSignal,
|
|
89
|
+
followupPrompt: () => followupPrompt,
|
|
83
90
|
heartbeatSignal: () => heartbeatSignal,
|
|
84
91
|
huggingFaceNeuron: () => huggingFaceNeuron,
|
|
85
92
|
imprintSignal: () => imprintSignal,
|
|
@@ -107,8 +114,11 @@ __export(index_exports, {
|
|
|
107
114
|
recalledSignal: () => recalledSignal,
|
|
108
115
|
registerSignal: () => registerSignal,
|
|
109
116
|
reply: () => reply,
|
|
117
|
+
runWithTraceContext: () => runWithTraceContext,
|
|
110
118
|
standardMcpServers: () => standardMcpServers,
|
|
111
119
|
synapseFromUrl: () => synapseFromUrl,
|
|
120
|
+
taskAwardedSignal: () => taskAwardedSignal,
|
|
121
|
+
taskDeclinedSignal: () => taskDeclinedSignal,
|
|
112
122
|
taskOfferSignal: () => taskOfferSignal,
|
|
113
123
|
taskSignal: () => taskSignal,
|
|
114
124
|
thoughtDeltaSignal: () => thoughtDeltaSignal,
|
|
@@ -245,6 +255,12 @@ function createSignal(input) {
|
|
|
245
255
|
return signal;
|
|
246
256
|
}
|
|
247
257
|
function validateSignal(signal) {
|
|
258
|
+
const major = signal.v.split(".", 1)[0];
|
|
259
|
+
if (major !== "1") {
|
|
260
|
+
throw new Error(
|
|
261
|
+
`unsupported protocol version '${signal.v}': this SDK speaks major version 1 (accepts '1' or '1.x')`
|
|
262
|
+
);
|
|
263
|
+
}
|
|
248
264
|
if (!signal.id.startsWith("evt_")) {
|
|
249
265
|
throw new Error(`Signal id must start with 'evt_', got: ${signal.id}`);
|
|
250
266
|
}
|
|
@@ -282,6 +298,7 @@ function taskSignal(args) {
|
|
|
282
298
|
const payload = { input: args.input };
|
|
283
299
|
if (args.contextRef) payload["context_ref"] = args.contextRef;
|
|
284
300
|
if (args.capabilities) payload["capabilities"] = args.capabilities;
|
|
301
|
+
if (args.finalize) payload["finalize"] = true;
|
|
285
302
|
return createSignal({
|
|
286
303
|
type: SignalType.TASK,
|
|
287
304
|
trace_id: args.traceId ?? newTraceId(),
|
|
@@ -378,9 +395,10 @@ function errorSignal(args) {
|
|
|
378
395
|
}
|
|
379
396
|
function registerSignal(args) {
|
|
380
397
|
const caps = args.capabilities ?? args.directed?.capabilities ?? [];
|
|
381
|
-
const
|
|
398
|
+
const role = args.role ?? (args.engram ? "engram" : "neuron");
|
|
399
|
+
const payload = { role, capabilities: caps };
|
|
382
400
|
if (args.version) payload["version"] = args.version;
|
|
383
|
-
if (args.engram) payload["engram"] = true;
|
|
401
|
+
if (args.engram || role === "engram") payload["engram"] = true;
|
|
384
402
|
return createSignal({
|
|
385
403
|
type: SignalType.REGISTER,
|
|
386
404
|
trace_id: newTraceId(),
|
|
@@ -445,6 +463,32 @@ function bidSignal(args) {
|
|
|
445
463
|
meta: args.meta ?? {}
|
|
446
464
|
});
|
|
447
465
|
}
|
|
466
|
+
function taskAwardedSignal(args) {
|
|
467
|
+
const payload = { input: args.input };
|
|
468
|
+
if (args.winningBid !== void 0) payload["winning_bid"] = args.winningBid;
|
|
469
|
+
if (args.contextRef !== void 0) payload["context_ref"] = args.contextRef;
|
|
470
|
+
if (args.finalize) payload["finalize"] = true;
|
|
471
|
+
return createSignal({
|
|
472
|
+
type: SignalType.TASK_AWARDED,
|
|
473
|
+
trace_id: args.traceId,
|
|
474
|
+
parent_id: args.parentId,
|
|
475
|
+
directed: args.directed ?? null,
|
|
476
|
+
payload,
|
|
477
|
+
meta: args.meta ?? {}
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
function taskDeclinedSignal(args) {
|
|
481
|
+
const payload = {};
|
|
482
|
+
if (args.reason !== void 0) payload["reason"] = args.reason;
|
|
483
|
+
return createSignal({
|
|
484
|
+
type: SignalType.TASK_DECLINED,
|
|
485
|
+
trace_id: args.traceId,
|
|
486
|
+
parent_id: args.parentId,
|
|
487
|
+
directed: args.directed ?? null,
|
|
488
|
+
payload,
|
|
489
|
+
meta: args.meta ?? {}
|
|
490
|
+
});
|
|
491
|
+
}
|
|
448
492
|
function critiqueSignal(args) {
|
|
449
493
|
return createSignal({
|
|
450
494
|
type: SignalType.CRITIQUE,
|
|
@@ -1916,6 +1960,16 @@ var LifecycleHooks = class {
|
|
|
1916
1960
|
}
|
|
1917
1961
|
};
|
|
1918
1962
|
|
|
1963
|
+
// src/trace-context.ts
|
|
1964
|
+
var import_node_async_hooks = require("async_hooks");
|
|
1965
|
+
var storage = new import_node_async_hooks.AsyncLocalStorage();
|
|
1966
|
+
function ambientTrace() {
|
|
1967
|
+
return storage.getStore() ?? null;
|
|
1968
|
+
}
|
|
1969
|
+
function runWithTraceContext(traceId, parentId, fn) {
|
|
1970
|
+
return storage.run([traceId, parentId], fn);
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1919
1973
|
// src/neuron.ts
|
|
1920
1974
|
function clarify(question, context) {
|
|
1921
1975
|
return context === void 0 ? { __clarification__: true, question } : { __clarification__: true, question, context };
|
|
@@ -1947,182 +2001,524 @@ function isErrorOutput(output) {
|
|
|
1947
2001
|
return typeof output === "object" && output !== null && output["__error__"] === true;
|
|
1948
2002
|
}
|
|
1949
2003
|
|
|
1950
|
-
// src/
|
|
1951
|
-
var
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
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(", ");
|
|
2004
|
+
// src/engram.ts
|
|
2005
|
+
var EngramBinding = class {
|
|
2006
|
+
name;
|
|
2007
|
+
directedId;
|
|
2008
|
+
directedType;
|
|
2009
|
+
defaultDeadlineMs;
|
|
2010
|
+
defaultRecallMode;
|
|
2011
|
+
constructor(init) {
|
|
2012
|
+
this.name = init.name;
|
|
2013
|
+
this.directedId = init.directedId ?? null;
|
|
2014
|
+
this.directedType = init.directedType ?? null;
|
|
2015
|
+
this.defaultDeadlineMs = init.defaultDeadlineMs ?? null;
|
|
2016
|
+
this.defaultRecallMode = init.defaultRecallMode ?? "first";
|
|
2017
|
+
if (!this.directedId && !this.directedType) {
|
|
1995
2018
|
throw new Error(
|
|
1996
|
-
`
|
|
2019
|
+
`EngramBinding '${this.name}' requires directedId (engram_id) or directedType (engram_kind), or both`
|
|
1997
2020
|
);
|
|
1998
2021
|
}
|
|
1999
|
-
return { command: opts.command ?? preset.command, args: [...preset.args, ...extra] };
|
|
2000
2022
|
}
|
|
2001
|
-
|
|
2002
|
-
|
|
2023
|
+
/** Build the `Directed` addressing this Engram. */
|
|
2024
|
+
toDirected() {
|
|
2025
|
+
return { id: this.directedId, type: this.directedType, capabilities: [] };
|
|
2003
2026
|
}
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
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;
|
|
2027
|
+
};
|
|
2028
|
+
var EngramError = class extends Error {
|
|
2029
|
+
constructor(message) {
|
|
2030
|
+
super(message);
|
|
2031
|
+
this.name = new.target.name;
|
|
2035
2032
|
}
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
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
|
-
}
|
|
2033
|
+
};
|
|
2034
|
+
var EngramTimeout = class extends EngramError {
|
|
2035
|
+
};
|
|
2036
|
+
var EngramCancelled = class extends EngramError {
|
|
2037
|
+
};
|
|
2038
|
+
var EngramNotBound = class extends EngramError {
|
|
2039
|
+
};
|
|
2040
|
+
var EngramOverloaded = class extends EngramError {
|
|
2041
|
+
};
|
|
2042
|
+
var Engram = class {
|
|
2043
|
+
version = null;
|
|
2044
|
+
/** Return false if this Engram cannot satisfy the query. Default: serve all. */
|
|
2045
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
2046
|
+
async canServe(_query) {
|
|
2047
|
+
return true;
|
|
2048
|
+
}
|
|
2049
|
+
};
|
|
2050
|
+
function receipt(engramId, op, fields = {}) {
|
|
2051
|
+
const error = fields.error ?? null;
|
|
2052
|
+
return {
|
|
2053
|
+
engramId,
|
|
2054
|
+
op,
|
|
2055
|
+
id: fields.id ?? null,
|
|
2056
|
+
version: fields.version ?? null,
|
|
2057
|
+
tookMs: fields.tookMs ?? null,
|
|
2058
|
+
error,
|
|
2059
|
+
ok: error === null
|
|
2089
2060
|
};
|
|
2090
|
-
return fn;
|
|
2091
2061
|
}
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2062
|
+
function entryToDict(e) {
|
|
2063
|
+
const out = {
|
|
2064
|
+
id: e.id,
|
|
2065
|
+
content: e.content,
|
|
2066
|
+
tags: [...e.tags],
|
|
2067
|
+
version: e.version,
|
|
2068
|
+
created_at: e.createdAt,
|
|
2069
|
+
updated_at: e.updatedAt
|
|
2070
|
+
};
|
|
2071
|
+
if (e.mergeKey !== null) out["merge_key"] = e.mergeKey;
|
|
2072
|
+
if (Object.keys(e.extra).length > 0) out["meta"] = { ...e.extra };
|
|
2073
|
+
return out;
|
|
2097
2074
|
}
|
|
2098
|
-
function
|
|
2099
|
-
|
|
2100
|
-
return Array.isArray(m) ? m : null;
|
|
2075
|
+
function asStringArray(v) {
|
|
2076
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
2101
2077
|
}
|
|
2102
|
-
function
|
|
2103
|
-
|
|
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 };
|
|
2078
|
+
function asObject(v) {
|
|
2079
|
+
return v !== null && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
2111
2080
|
}
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2081
|
+
var InMemoryEngram = class extends Engram {
|
|
2082
|
+
engramId;
|
|
2083
|
+
engramKind;
|
|
2084
|
+
capabilities;
|
|
2085
|
+
entries = /* @__PURE__ */ new Map();
|
|
2086
|
+
byMergeKey = /* @__PURE__ */ new Map();
|
|
2087
|
+
imprintSeen = /* @__PURE__ */ new Map();
|
|
2088
|
+
constructor(init = {}) {
|
|
2089
|
+
super();
|
|
2090
|
+
this.engramId = init.engramId ?? "engram-memory";
|
|
2091
|
+
this.engramKind = init.engramKind ?? "keyvalue";
|
|
2092
|
+
this.capabilities = init.capabilities ?? ["substring", "tags", "merge_key"];
|
|
2093
|
+
this.version = init.version ?? "0.0.1";
|
|
2094
|
+
}
|
|
2095
|
+
async connect() {
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
async close() {
|
|
2099
|
+
this.entries.clear();
|
|
2100
|
+
this.byMergeKey.clear();
|
|
2101
|
+
this.imprintSeen.clear();
|
|
2102
|
+
}
|
|
2103
|
+
async recall(query, opts = {}) {
|
|
2104
|
+
const q = query ?? {};
|
|
2105
|
+
const text = typeof q["text"] === "string" ? q["text"].toLowerCase() : "";
|
|
2106
|
+
const tagQ = typeof q["tag"] === "string" ? q["tag"] : null;
|
|
2107
|
+
const mergeKey = typeof q["merge_key"] === "string" ? q["merge_key"] : null;
|
|
2108
|
+
const topK = typeof q["top_k"] === "number" ? q["top_k"] : 50;
|
|
2109
|
+
const filters = opts.filters ?? {};
|
|
2110
|
+
const requireTags = asStringArray(filters["tags"]);
|
|
2111
|
+
const since = typeof filters["since"] === "string" ? Date.parse(filters["since"]) : NaN;
|
|
2112
|
+
const until = typeof filters["until"] === "string" ? Date.parse(filters["until"]) : NaN;
|
|
2113
|
+
let candidates;
|
|
2114
|
+
if (mergeKey !== null) {
|
|
2115
|
+
const ids = this.byMergeKey.get(mergeKey) ?? [];
|
|
2116
|
+
candidates = ids.map((i) => this.entries.get(i)).filter((e) => e !== void 0);
|
|
2117
|
+
} else {
|
|
2118
|
+
candidates = [...this.entries.values()];
|
|
2119
|
+
}
|
|
2120
|
+
const hits = [];
|
|
2121
|
+
for (const ent of candidates) {
|
|
2122
|
+
if (requireTags.length > 0 && !requireTags.every((t) => ent.tags.includes(t))) continue;
|
|
2123
|
+
const updated = Date.parse(ent.updatedAt);
|
|
2124
|
+
if (!Number.isNaN(since) && updated < since) continue;
|
|
2125
|
+
if (!Number.isNaN(until) && updated > until) continue;
|
|
2126
|
+
if (tagQ !== null && !ent.tags.includes(tagQ)) continue;
|
|
2127
|
+
let score = 1;
|
|
2128
|
+
if (text) {
|
|
2129
|
+
const hay = String(ent.content).toLowerCase();
|
|
2130
|
+
if (!hay.includes(text)) continue;
|
|
2131
|
+
score = Math.min(1, text.length / Math.max(1, hay.length));
|
|
2132
|
+
}
|
|
2133
|
+
if (opts.minConfidence !== void 0 && score < opts.minConfidence) continue;
|
|
2134
|
+
hits.push({ id: ent.id, entry: entryToDict(ent), score });
|
|
2135
|
+
}
|
|
2136
|
+
hits.sort((a, b) => b.score - a.score);
|
|
2137
|
+
return hits.slice(0, topK);
|
|
2138
|
+
}
|
|
2139
|
+
async imprint(op, entry, opts = {}) {
|
|
2140
|
+
const t0 = Date.now();
|
|
2141
|
+
const mergeKey = opts.mergeKey ?? null;
|
|
2142
|
+
const tookMs = () => Date.now() - t0;
|
|
2143
|
+
if (opts.imprintId !== void 0) {
|
|
2144
|
+
const seen = this.imprintSeen.get(opts.imprintId);
|
|
2145
|
+
if (seen !== void 0) {
|
|
2146
|
+
const existing = this.entries.get(seen);
|
|
2147
|
+
return receipt(this.engramId, op, {
|
|
2148
|
+
id: seen,
|
|
2149
|
+
version: existing ? existing.version : null,
|
|
2150
|
+
tookMs: tookMs()
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
let resultingId = null;
|
|
2155
|
+
let version = null;
|
|
2156
|
+
if (op === "add") {
|
|
2157
|
+
const ent = this.makeEntry(entry, mergeKey);
|
|
2158
|
+
if (this.entries.has(ent.id)) {
|
|
2159
|
+
return receipt(this.engramId, op, { error: `entry id '${ent.id}' already exists`, tookMs: tookMs() });
|
|
2160
|
+
}
|
|
2161
|
+
this.store(ent);
|
|
2162
|
+
resultingId = ent.id;
|
|
2163
|
+
version = ent.version;
|
|
2164
|
+
} else if (op === "append") {
|
|
2165
|
+
let ent = this.makeEntry(entry, mergeKey);
|
|
2166
|
+
while (this.entries.has(ent.id)) {
|
|
2167
|
+
ent = this.makeEntry({ ...entry, id: newEngramId() }, mergeKey);
|
|
2168
|
+
}
|
|
2169
|
+
this.store(ent);
|
|
2170
|
+
resultingId = ent.id;
|
|
2171
|
+
version = ent.version;
|
|
2172
|
+
} else if (op === "upsert") {
|
|
2173
|
+
const existingIds = this.byMergeKey.get(mergeKey ?? "") ?? [];
|
|
2174
|
+
const targetId = existingIds[existingIds.length - 1];
|
|
2175
|
+
const old = targetId !== void 0 ? this.entries.get(targetId) : void 0;
|
|
2176
|
+
if (old !== void 0) {
|
|
2177
|
+
const next = this.makeEntry({ ...entry, id: old.id }, mergeKey);
|
|
2178
|
+
next.createdAt = old.createdAt;
|
|
2179
|
+
next.version = old.version + 1;
|
|
2180
|
+
this.store(next, true);
|
|
2181
|
+
resultingId = next.id;
|
|
2182
|
+
version = next.version;
|
|
2183
|
+
} else {
|
|
2184
|
+
const ent = this.makeEntry(entry, mergeKey);
|
|
2185
|
+
this.store(ent);
|
|
2186
|
+
resultingId = ent.id;
|
|
2187
|
+
version = ent.version;
|
|
2188
|
+
}
|
|
2189
|
+
} else if (op === "merge") {
|
|
2190
|
+
const existingIds = this.byMergeKey.get(mergeKey ?? "") ?? [];
|
|
2191
|
+
const targetId = existingIds[existingIds.length - 1];
|
|
2192
|
+
const old = targetId !== void 0 ? this.entries.get(targetId) : void 0;
|
|
2193
|
+
if (old === void 0) {
|
|
2194
|
+
return receipt(this.engramId, op, { error: `no entry for merge_key='${mergeKey}'`, tookMs: tookMs() });
|
|
2195
|
+
}
|
|
2196
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2197
|
+
const next = {
|
|
2198
|
+
id: old.id,
|
|
2199
|
+
content: deepMerge(old.content, entry["content"]),
|
|
2200
|
+
tags: [.../* @__PURE__ */ new Set([...old.tags, ...asStringArray(entry["tags"])])],
|
|
2201
|
+
mergeKey: old.mergeKey,
|
|
2202
|
+
version: old.version + 1,
|
|
2203
|
+
createdAt: old.createdAt,
|
|
2204
|
+
updatedAt: now,
|
|
2205
|
+
extra: asObject(deepMerge(old.extra, entry["meta"]))
|
|
2206
|
+
};
|
|
2207
|
+
this.store(next, true);
|
|
2208
|
+
resultingId = next.id;
|
|
2209
|
+
version = next.version;
|
|
2210
|
+
} else if (op === "delete") {
|
|
2211
|
+
let targetId = null;
|
|
2212
|
+
const entId = entry["id"];
|
|
2213
|
+
if (typeof entId === "string") {
|
|
2214
|
+
targetId = entId;
|
|
2215
|
+
} else if (mergeKey !== null) {
|
|
2216
|
+
const ids = this.byMergeKey.get(mergeKey) ?? [];
|
|
2217
|
+
targetId = ids[ids.length - 1] ?? null;
|
|
2218
|
+
}
|
|
2219
|
+
if (targetId === null || !this.entries.has(targetId)) {
|
|
2220
|
+
return receipt(this.engramId, op, { tookMs: tookMs() });
|
|
2221
|
+
}
|
|
2222
|
+
this.evict(targetId);
|
|
2223
|
+
resultingId = targetId;
|
|
2224
|
+
version = null;
|
|
2225
|
+
} else {
|
|
2226
|
+
return receipt(this.engramId, op, { error: `unknown op '${op}'`, tookMs: tookMs() });
|
|
2227
|
+
}
|
|
2228
|
+
if (opts.imprintId !== void 0 && resultingId !== null) {
|
|
2229
|
+
this.imprintSeen.set(opts.imprintId, resultingId);
|
|
2230
|
+
}
|
|
2231
|
+
return receipt(this.engramId, op, { id: resultingId, version, tookMs: tookMs() });
|
|
2232
|
+
}
|
|
2233
|
+
/** Test/debug helper - NOT part of the Engram contract. */
|
|
2234
|
+
snapshot() {
|
|
2235
|
+
return [...this.entries.values()].map(entryToDict);
|
|
2236
|
+
}
|
|
2237
|
+
makeEntry(entry, mergeKey) {
|
|
2238
|
+
const id = typeof entry["id"] === "string" ? entry["id"] : newEngramId();
|
|
2239
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2240
|
+
return {
|
|
2241
|
+
id,
|
|
2242
|
+
content: entry["content"],
|
|
2243
|
+
tags: asStringArray(entry["tags"]),
|
|
2244
|
+
mergeKey,
|
|
2245
|
+
version: 1,
|
|
2246
|
+
createdAt: now,
|
|
2247
|
+
updatedAt: now,
|
|
2248
|
+
extra: asObject(entry["meta"])
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
store(ent, replace = false) {
|
|
2252
|
+
if (replace) {
|
|
2253
|
+
const old = this.entries.get(ent.id);
|
|
2254
|
+
if (old !== void 0 && old.mergeKey) {
|
|
2255
|
+
const bucket = this.byMergeKey.get(old.mergeKey);
|
|
2256
|
+
if (bucket) {
|
|
2257
|
+
const idx = bucket.indexOf(ent.id);
|
|
2258
|
+
if (idx >= 0) bucket.splice(idx, 1);
|
|
2259
|
+
if (bucket.length === 0) this.byMergeKey.delete(old.mergeKey);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
this.entries.set(ent.id, ent);
|
|
2264
|
+
if (ent.mergeKey) {
|
|
2265
|
+
const bucket = this.byMergeKey.get(ent.mergeKey);
|
|
2266
|
+
if (bucket) bucket.push(ent.id);
|
|
2267
|
+
else this.byMergeKey.set(ent.mergeKey, [ent.id]);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
evict(entryId) {
|
|
2271
|
+
const ent = this.entries.get(entryId);
|
|
2272
|
+
this.entries.delete(entryId);
|
|
2273
|
+
if (ent === void 0) return;
|
|
2274
|
+
if (ent.mergeKey) {
|
|
2275
|
+
const bucket = this.byMergeKey.get(ent.mergeKey);
|
|
2276
|
+
if (bucket) {
|
|
2277
|
+
const idx = bucket.indexOf(entryId);
|
|
2278
|
+
if (idx >= 0) bucket.splice(idx, 1);
|
|
2279
|
+
if (bucket.length === 0) this.byMergeKey.delete(ent.mergeKey);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
};
|
|
2284
|
+
function deepMerge(base, incoming) {
|
|
2285
|
+
if (incoming === void 0 || incoming === null) return base;
|
|
2286
|
+
const bothObjects = base !== null && typeof base === "object" && !Array.isArray(base) && typeof incoming === "object" && !Array.isArray(incoming);
|
|
2287
|
+
if (bothObjects) {
|
|
2288
|
+
const out = { ...base };
|
|
2289
|
+
for (const [k, v] of Object.entries(incoming)) {
|
|
2290
|
+
out[k] = k in out ? deepMerge(out[k], v) : v;
|
|
2291
|
+
}
|
|
2292
|
+
return out;
|
|
2293
|
+
}
|
|
2294
|
+
if (Array.isArray(base) && Array.isArray(incoming)) {
|
|
2295
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2296
|
+
const out = [];
|
|
2297
|
+
for (const item of [...base, ...incoming]) {
|
|
2298
|
+
const key = JSON.stringify(item);
|
|
2299
|
+
if (seen.has(key)) continue;
|
|
2300
|
+
seen.add(key);
|
|
2301
|
+
out.push(item);
|
|
2302
|
+
}
|
|
2303
|
+
return out;
|
|
2304
|
+
}
|
|
2305
|
+
return incoming;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
// src/neuron-mcp.ts
|
|
2309
|
+
var standardMcpServers = {
|
|
2310
|
+
filesystem: {
|
|
2311
|
+
command: "npx",
|
|
2312
|
+
args: ["-y", "@modelcontextprotocol/server-filesystem"],
|
|
2313
|
+
note: "Append one or more allowed directories, e.g. args=['/data']."
|
|
2314
|
+
},
|
|
2315
|
+
memory: {
|
|
2316
|
+
command: "npx",
|
|
2317
|
+
args: ["-y", "@modelcontextprotocol/server-memory"],
|
|
2318
|
+
note: "Knowledge-graph memory store."
|
|
2319
|
+
},
|
|
2320
|
+
everything: {
|
|
2321
|
+
command: "npx",
|
|
2322
|
+
args: ["-y", "@modelcontextprotocol/server-everything"],
|
|
2323
|
+
note: "Reference server exercising every MCP feature; handy for tests."
|
|
2324
|
+
},
|
|
2325
|
+
sequentialthinking: {
|
|
2326
|
+
command: "npx",
|
|
2327
|
+
args: ["-y", "@modelcontextprotocol/server-sequential-thinking"],
|
|
2328
|
+
note: "Structured step-by-step reasoning tool."
|
|
2329
|
+
},
|
|
2330
|
+
fetch: {
|
|
2331
|
+
command: "uvx",
|
|
2332
|
+
args: ["mcp-server-fetch"],
|
|
2333
|
+
note: "Fetch a URL and return its content as markdown/text."
|
|
2334
|
+
},
|
|
2335
|
+
git: {
|
|
2336
|
+
command: "uvx",
|
|
2337
|
+
args: ["mcp-server-git"],
|
|
2338
|
+
note: "Read/inspect a git repo. Append --repository <path>."
|
|
2339
|
+
},
|
|
2340
|
+
time: {
|
|
2341
|
+
command: "uvx",
|
|
2342
|
+
args: ["mcp-server-time"],
|
|
2343
|
+
note: "Current time and timezone conversions."
|
|
2344
|
+
}
|
|
2345
|
+
};
|
|
2346
|
+
var CONTROL_KEYS = /* @__PURE__ */ new Set(["tool", "arguments", "args", "__list_tools__"]);
|
|
2347
|
+
function resolveLaunch(opts) {
|
|
2348
|
+
const extra = opts.args ?? [];
|
|
2349
|
+
if (opts.server != null) {
|
|
2350
|
+
const preset = standardMcpServers[opts.server];
|
|
2351
|
+
if (!preset) {
|
|
2352
|
+
const available = Object.keys(standardMcpServers).sort().join(", ");
|
|
2353
|
+
throw new Error(
|
|
2354
|
+
`Unknown MCP server preset '${opts.server}'. Available: ${available}. (Or pass command/args to wrap any other stdio MCP server.)`
|
|
2355
|
+
);
|
|
2356
|
+
}
|
|
2357
|
+
return { command: opts.command ?? preset.command, args: [...preset.args, ...extra] };
|
|
2358
|
+
}
|
|
2359
|
+
if (opts.command == null) {
|
|
2360
|
+
throw new Error("mcpNeuron(...) needs either `command` (+optional `args`) or a `server` preset name.");
|
|
2361
|
+
}
|
|
2362
|
+
return { command: opts.command, args: extra };
|
|
2363
|
+
}
|
|
2364
|
+
function mcpNeuron(opts) {
|
|
2365
|
+
const { command, args } = resolveLaunch(opts);
|
|
2366
|
+
let client = null;
|
|
2367
|
+
let connecting = null;
|
|
2368
|
+
async function ensure() {
|
|
2369
|
+
if (client) return client;
|
|
2370
|
+
if (connecting) return connecting;
|
|
2371
|
+
connecting = (async () => {
|
|
2372
|
+
const clientSpec = "@modelcontextprotocol/sdk/client/index.js";
|
|
2373
|
+
const stdioSpec = "@modelcontextprotocol/sdk/client/stdio.js";
|
|
2374
|
+
const clientMod = await import(clientSpec);
|
|
2375
|
+
const stdioMod = await import(stdioSpec);
|
|
2376
|
+
const Client = clientMod.Client;
|
|
2377
|
+
const StdioClientTransport = stdioMod.StdioClientTransport;
|
|
2378
|
+
const transport = new StdioClientTransport({
|
|
2379
|
+
command,
|
|
2380
|
+
args,
|
|
2381
|
+
...opts.env ? { env: opts.env } : {},
|
|
2382
|
+
...opts.cwd ? { cwd: opts.cwd } : {}
|
|
2383
|
+
});
|
|
2384
|
+
const c = new Client(
|
|
2385
|
+
{ name: opts.clientName ?? "cosmonapse", version: opts.clientVersion ?? "0.2.0" },
|
|
2386
|
+
{ capabilities: {} }
|
|
2387
|
+
);
|
|
2388
|
+
await c.connect(transport);
|
|
2389
|
+
client = c;
|
|
2390
|
+
return c;
|
|
2391
|
+
})();
|
|
2392
|
+
return connecting;
|
|
2393
|
+
}
|
|
2394
|
+
const fn = (async (input, _context) => {
|
|
2395
|
+
const c = await ensure();
|
|
2396
|
+
const inp = input ?? {};
|
|
2397
|
+
if (inp.__list_tools__) {
|
|
2398
|
+
const res2 = await c.listTools();
|
|
2399
|
+
return {
|
|
2400
|
+
tools: (res2.tools ?? []).map((t) => ({
|
|
2401
|
+
name: t.name,
|
|
2402
|
+
description: t.description ?? null,
|
|
2403
|
+
input_schema: t.inputSchema ?? null
|
|
2404
|
+
}))
|
|
2405
|
+
};
|
|
2406
|
+
}
|
|
2407
|
+
let tool = inp.tool ?? opts.tool;
|
|
2408
|
+
let toolArgs = inp.arguments ?? inp.args;
|
|
2409
|
+
if (toolArgs == null) {
|
|
2410
|
+
toolArgs = {};
|
|
2411
|
+
for (const [k, v] of Object.entries(inp)) {
|
|
2412
|
+
if (!CONTROL_KEYS.has(k)) toolArgs[k] = v;
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
if (!tool) {
|
|
2416
|
+
const res2 = await c.listTools();
|
|
2417
|
+
const names = (res2.tools ?? []).map((t) => t.name);
|
|
2418
|
+
if (names.length === 1) {
|
|
2419
|
+
tool = names[0];
|
|
2420
|
+
} else {
|
|
2421
|
+
throw new Error(
|
|
2422
|
+
`MCP Neuron could not determine which tool to call. Pass tool=... (server exposes: ${JSON.stringify(names)}).`
|
|
2423
|
+
);
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
const res = await c.callTool({ name: tool, arguments: toolArgs });
|
|
2427
|
+
const content = res.content ?? [];
|
|
2428
|
+
const texts = content.filter((x) => x?.text != null).map((x) => x.text);
|
|
2429
|
+
return {
|
|
2430
|
+
response: texts.join("\n"),
|
|
2431
|
+
result: res.structuredContent ?? null,
|
|
2432
|
+
is_error: Boolean(res.isError),
|
|
2433
|
+
content,
|
|
2434
|
+
meta: { tool, server: opts.server ?? null, command }
|
|
2435
|
+
};
|
|
2436
|
+
});
|
|
2437
|
+
fn.close = async () => {
|
|
2438
|
+
const c = client;
|
|
2439
|
+
client = null;
|
|
2440
|
+
connecting = null;
|
|
2441
|
+
if (c) {
|
|
2442
|
+
try {
|
|
2443
|
+
await c.close();
|
|
2444
|
+
} catch {
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
};
|
|
2448
|
+
return fn;
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
// src/neuron-http.ts
|
|
2452
|
+
function readPrompt(input) {
|
|
2453
|
+
const v = input["prompt"] ?? input["text"] ?? input["query"] ?? input["content"];
|
|
2454
|
+
return typeof v === "string" && v ? v : null;
|
|
2455
|
+
}
|
|
2456
|
+
function readMessages(input) {
|
|
2457
|
+
const m = input["messages"];
|
|
2458
|
+
return Array.isArray(m) ? m : null;
|
|
2459
|
+
}
|
|
2460
|
+
function followupPrompt(input) {
|
|
2461
|
+
const c = input["clarification"];
|
|
2462
|
+
if (c !== null && typeof c === "object" && !Array.isArray(c)) {
|
|
2463
|
+
const cd = c;
|
|
2464
|
+
const lines = ["You previously asked a clarifying question while working on a task."];
|
|
2465
|
+
if (cd["question"] !== void 0 && cd["question"] !== null) {
|
|
2466
|
+
lines.push(`Your question: ${String(cd["question"])}`);
|
|
2467
|
+
}
|
|
2468
|
+
if ("answer" in cd) lines.push(`The answer: ${JSON.stringify(cd["answer"])}`);
|
|
2469
|
+
const extra = Object.fromEntries(
|
|
2470
|
+
Object.entries(cd).filter(([k]) => k !== "question" && k !== "answer")
|
|
2471
|
+
);
|
|
2472
|
+
if (Object.keys(extra).length) lines.push(`Additional context: ${JSON.stringify(extra)}`);
|
|
2473
|
+
lines.push("Continue the original task using this answer.");
|
|
2474
|
+
return lines.join("\n");
|
|
2475
|
+
}
|
|
2476
|
+
const p = input["permission"];
|
|
2477
|
+
if (p !== null && typeof p === "object" && !Array.isArray(p)) {
|
|
2478
|
+
const pd = p;
|
|
2479
|
+
const granted = Boolean(pd["granted"]);
|
|
2480
|
+
const lines = ["You previously requested permission while working on a task."];
|
|
2481
|
+
if (pd["action"] !== void 0 && pd["action"] !== null) {
|
|
2482
|
+
lines.push(`Requested action: ${String(pd["action"])}`);
|
|
2483
|
+
}
|
|
2484
|
+
lines.push(`The decision: ${granted ? "GRANTED" : "DENIED"}.`);
|
|
2485
|
+
if (pd["reason"] !== void 0 && pd["reason"] !== null) {
|
|
2486
|
+
lines.push(`Reason: ${String(pd["reason"])}`);
|
|
2487
|
+
}
|
|
2488
|
+
if (pd["ttl_ms"] !== void 0 && pd["ttl_ms"] !== null) {
|
|
2489
|
+
lines.push(`The grant is valid for ${String(pd["ttl_ms"])} ms.`);
|
|
2490
|
+
}
|
|
2491
|
+
lines.push(
|
|
2492
|
+
granted ? "Proceed with the action and continue the original task." : "Do not perform the action. Continue the task another way, or explain why you cannot."
|
|
2493
|
+
);
|
|
2494
|
+
return lines.join("\n");
|
|
2495
|
+
}
|
|
2496
|
+
return null;
|
|
2497
|
+
}
|
|
2498
|
+
function requireInput(input, provider) {
|
|
2499
|
+
const prompt = readPrompt(input) ?? followupPrompt(input);
|
|
2500
|
+
const messages = readMessages(input);
|
|
2501
|
+
if (!prompt && !messages) {
|
|
2502
|
+
throw new Error(
|
|
2503
|
+
`${provider} Neuron expects 'prompt' or 'messages' in the input dict. Got keys: ${Object.keys(input).join(", ")}`
|
|
2504
|
+
);
|
|
2505
|
+
}
|
|
2506
|
+
return { prompt, messages };
|
|
2507
|
+
}
|
|
2508
|
+
async function postJson(url, body, headers, timeoutMs) {
|
|
2509
|
+
const ctrl = new AbortController();
|
|
2510
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
2511
|
+
try {
|
|
2512
|
+
const res = await fetch(url, {
|
|
2513
|
+
method: "POST",
|
|
2514
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
2515
|
+
body: JSON.stringify(body),
|
|
2516
|
+
signal: ctrl.signal
|
|
2517
|
+
});
|
|
2518
|
+
if (!res.ok) {
|
|
2519
|
+
const text = await res.text().catch(() => "");
|
|
2520
|
+
throw new Error(`HTTP ${res.status} from ${url}: ${text.slice(0, 200)}`);
|
|
2521
|
+
}
|
|
2126
2522
|
return await res.json();
|
|
2127
2523
|
} finally {
|
|
2128
2524
|
clearTimeout(timer);
|
|
@@ -2311,9 +2707,11 @@ var Axon = class _Axon {
|
|
|
2311
2707
|
neuronId;
|
|
2312
2708
|
capabilities;
|
|
2313
2709
|
version;
|
|
2710
|
+
neuronKind;
|
|
2314
2711
|
fn;
|
|
2315
2712
|
contextFetcher;
|
|
2316
2713
|
outputParser;
|
|
2714
|
+
engramBindings = /* @__PURE__ */ new Map();
|
|
2317
2715
|
dendrite = null;
|
|
2318
2716
|
/**
|
|
2319
2717
|
* Decorator-registered recognisers, one bucket per capability (the asking
|
|
@@ -2322,48 +2720,139 @@ var Axon = class _Axon {
|
|
|
2322
2720
|
* output by {@link applyRecognisers}.
|
|
2323
2721
|
*/
|
|
2324
2722
|
recognisers = { error: [], clarification: [], permission: [], output: [] };
|
|
2723
|
+
/** Pre-task hooks (beforeTask): transform/validate/reject the TASK input
|
|
2724
|
+
* before the Neuron runs. */
|
|
2725
|
+
beforeTaskHooks = [];
|
|
2325
2726
|
/** @internal - lifecycle hooks, driven by the hosting Dendrite. */
|
|
2326
2727
|
hooks = new LifecycleHooks(this);
|
|
2327
2728
|
constructor(opts) {
|
|
2328
2729
|
this.neuronId = opts.neuronId;
|
|
2329
2730
|
this.capabilities = opts.capabilities ?? [];
|
|
2330
2731
|
this.version = opts.version;
|
|
2732
|
+
this.neuronKind = opts.neuronKind ?? "neuron";
|
|
2331
2733
|
this.fn = opts.neuronFn;
|
|
2332
2734
|
this.contextFetcher = opts.contextFetcher ?? noopContextFetcher;
|
|
2333
2735
|
this.outputParser = opts.outputParser;
|
|
2736
|
+
for (const b of opts.engrams ?? []) {
|
|
2737
|
+
if (this.engramBindings.has(b.name)) {
|
|
2738
|
+
throw new Error(`Axon '${opts.neuronId}': duplicate EngramBinding name '${b.name}'`);
|
|
2739
|
+
}
|
|
2740
|
+
this.engramBindings.set(b.name, b);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
/** Declared Engram bindings, keyed by name. */
|
|
2744
|
+
get engrams() {
|
|
2745
|
+
return new Map(this.engramBindings);
|
|
2746
|
+
}
|
|
2747
|
+
resolveBinding(name) {
|
|
2748
|
+
const binding = this.engramBindings.get(name);
|
|
2749
|
+
if (!binding) {
|
|
2750
|
+
throw new EngramNotBound(
|
|
2751
|
+
`Axon '${this.neuronId}': no Engram binding named '${name}'; available: ${[...this.engramBindings.keys()].sort().join(", ")}`
|
|
2752
|
+
);
|
|
2753
|
+
}
|
|
2754
|
+
return binding;
|
|
2755
|
+
}
|
|
2756
|
+
/** Build the per-task helpers object handed to the Neuron as its third
|
|
2757
|
+
* argument. Helpers throw EngramNotBound for undeclared names and
|
|
2758
|
+
* require a hosting Dendrite (the only thing the Axon pulls from it). */
|
|
2759
|
+
buildHelpers(traceId, parentId) {
|
|
2760
|
+
const requireClient = () => {
|
|
2761
|
+
if (this.dendrite === null) {
|
|
2762
|
+
throw new Error(
|
|
2763
|
+
`Axon '${this.neuronId}': not attached to a Dendrite; engram helpers require a hosting Dendrite`
|
|
2764
|
+
);
|
|
2765
|
+
}
|
|
2766
|
+
return this.dendrite.engramClient;
|
|
2767
|
+
};
|
|
2768
|
+
return {
|
|
2769
|
+
recall: async (name, args) => {
|
|
2770
|
+
const binding = this.resolveBinding(name);
|
|
2771
|
+
return requireClient().recall({
|
|
2772
|
+
binding,
|
|
2773
|
+
query: args.query,
|
|
2774
|
+
traceId,
|
|
2775
|
+
parentId,
|
|
2776
|
+
...args.filters !== void 0 ? { filters: args.filters } : {},
|
|
2777
|
+
...args.contextRef !== void 0 ? { contextRef: args.contextRef } : {},
|
|
2778
|
+
...args.deadlineMs !== void 0 ? { deadlineMs: args.deadlineMs } : {},
|
|
2779
|
+
...args.recallMode !== void 0 ? { recallMode: args.recallMode } : {},
|
|
2780
|
+
...args.minConfidence !== void 0 ? { minConfidence: args.minConfidence } : {},
|
|
2781
|
+
...args.meta !== void 0 ? { meta: args.meta } : {}
|
|
2782
|
+
});
|
|
2783
|
+
},
|
|
2784
|
+
imprint: async (name, args) => {
|
|
2785
|
+
const binding = this.resolveBinding(name);
|
|
2786
|
+
return requireClient().imprint({
|
|
2787
|
+
binding,
|
|
2788
|
+
op: args.op,
|
|
2789
|
+
entry: args.entry,
|
|
2790
|
+
traceId,
|
|
2791
|
+
parentId,
|
|
2792
|
+
...args.mergeKey !== void 0 ? { mergeKey: args.mergeKey } : {},
|
|
2793
|
+
...args.awaitAck !== void 0 ? { awaitAck: args.awaitAck } : {},
|
|
2794
|
+
...args.deadlineMs !== void 0 ? { deadlineMs: args.deadlineMs } : {},
|
|
2795
|
+
...args.meta !== void 0 ? { meta: args.meta } : {}
|
|
2796
|
+
});
|
|
2797
|
+
}
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
// -- source-paired factories --------------------------------------
|
|
2801
|
+
// Build an Axon already paired with one of the `neuron(source, ...)`
|
|
2802
|
+
// providers AND wired with the matching recogniser. No new class: the
|
|
2803
|
+
// result is a plain Axon.
|
|
2804
|
+
/** Resolve the teach-intents decision and return (possibly augmented) source opts. */
|
|
2805
|
+
static applyTeachIntents(source, opts, extra) {
|
|
2806
|
+
const recognize = extra.recognize ?? true;
|
|
2807
|
+
const teach = extra.teachIntents ?? (recognize && SYSTEM_CAPABLE_SOURCES.has(source.toLowerCase()));
|
|
2808
|
+
if (!teach) return opts;
|
|
2809
|
+
if (!SYSTEM_CAPABLE_SOURCES.has(source.toLowerCase())) {
|
|
2810
|
+
throw new Error(
|
|
2811
|
+
`teachIntents: true is not supported for source '${source}': its Neuron wrapper accepts no system option. Embed the convention in the prompt yourself (COSMO_INTENT_SYSTEM_PROMPT).`
|
|
2812
|
+
);
|
|
2813
|
+
}
|
|
2814
|
+
const existing = opts?.system;
|
|
2815
|
+
return {
|
|
2816
|
+
...opts ?? {},
|
|
2817
|
+
system: existing ? `${existing}
|
|
2818
|
+
|
|
2819
|
+
${COSMO_INTENT_SYSTEM_PROMPT}` : COSMO_INTENT_SYSTEM_PROMPT
|
|
2820
|
+
};
|
|
2334
2821
|
}
|
|
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
2822
|
static build(neuronId, neuronFn, source, extra) {
|
|
2340
2823
|
const recognize = extra.recognize ?? true;
|
|
2341
2824
|
const o = { neuronId, neuronFn };
|
|
2342
2825
|
if (extra.capabilities) o.capabilities = extra.capabilities;
|
|
2343
2826
|
if (extra.version !== void 0) o.version = extra.version;
|
|
2827
|
+
if (extra.neuronKind !== void 0) o.neuronKind = extra.neuronKind;
|
|
2344
2828
|
if (extra.contextFetcher) o.contextFetcher = extra.contextFetcher;
|
|
2345
2829
|
if (recognize) o.outputParser = source === "mcp" ? parseMcpIntents : parseLlmIntents;
|
|
2346
2830
|
return new _Axon(o);
|
|
2347
2831
|
}
|
|
2348
2832
|
/** Axon paired with any registered Neuron source + its recogniser. */
|
|
2349
2833
|
static fromSource(source, neuronId, opts, extra = {}) {
|
|
2350
|
-
|
|
2834
|
+
const o = _Axon.applyTeachIntents(source, opts, extra);
|
|
2835
|
+
return _Axon.build(neuronId, neuron(source, o), source, extra);
|
|
2351
2836
|
}
|
|
2352
2837
|
/** Axon paired with the OpenAI Chat Completions API. */
|
|
2353
2838
|
static openai(neuronId, opts, extra = {}) {
|
|
2354
|
-
|
|
2839
|
+
const o = _Axon.applyTeachIntents("openai", opts, extra);
|
|
2840
|
+
return _Axon.build(neuronId, neuron("openai", o), "openai", extra);
|
|
2355
2841
|
}
|
|
2356
2842
|
/** Axon paired with the Anthropic Messages API. */
|
|
2357
2843
|
static anthropic(neuronId, opts, extra = {}) {
|
|
2358
|
-
|
|
2844
|
+
const o = _Axon.applyTeachIntents("anthropic", opts, extra);
|
|
2845
|
+
return _Axon.build(neuronId, neuron("anthropic", o), "anthropic", extra);
|
|
2359
2846
|
}
|
|
2360
2847
|
/** Axon paired with a local Ollama daemon. */
|
|
2361
2848
|
static ollama(neuronId, opts, extra = {}) {
|
|
2362
|
-
|
|
2849
|
+
const o = _Axon.applyTeachIntents("ollama", opts, extra);
|
|
2850
|
+
return _Axon.build(neuronId, neuron("ollama", o), "ollama", extra);
|
|
2363
2851
|
}
|
|
2364
2852
|
/** Axon paired with a HuggingFace TGI / OpenAI-compatible endpoint. */
|
|
2365
2853
|
static huggingface(neuronId, opts, extra = {}) {
|
|
2366
|
-
|
|
2854
|
+
const o = _Axon.applyTeachIntents("huggingface", opts, extra);
|
|
2855
|
+
return _Axon.build(neuronId, neuron("huggingface", o), "huggingface", extra);
|
|
2367
2856
|
}
|
|
2368
2857
|
/** Axon paired with a stdio MCP server. */
|
|
2369
2858
|
static mcp(neuronId, opts, extra = {}) {
|
|
@@ -2375,6 +2864,26 @@ var Axon = class _Axon {
|
|
|
2375
2864
|
// Signals). Return the intent's fields to match, or null/undefined to fall
|
|
2376
2865
|
// through. Sync or async; multiple per capability tried in order. These run
|
|
2377
2866
|
// after `outputParser` and before the literal `__marker__` checks.
|
|
2867
|
+
/**
|
|
2868
|
+
* Register a pre-task hook over the TASK's `input`. Runs before the Neuron.
|
|
2869
|
+
* Sync or async; multiple hooks run in registration order, each receiving
|
|
2870
|
+
* the previous one's result. Return a (new) object to replace the input,
|
|
2871
|
+
* return null/undefined to pass through unchanged, or throw to reject the
|
|
2872
|
+
* TASK (surfaces as an ERROR Signal, code NEURON_EXCEPTION). The natural
|
|
2873
|
+
* place for input normalisation or per-Axon policy checks.
|
|
2874
|
+
*/
|
|
2875
|
+
beforeTask(fn) {
|
|
2876
|
+
this.beforeTaskHooks.push(fn);
|
|
2877
|
+
return fn;
|
|
2878
|
+
}
|
|
2879
|
+
async applyBeforeTask(input) {
|
|
2880
|
+
let current = input;
|
|
2881
|
+
for (const fn of this.beforeTaskHooks) {
|
|
2882
|
+
const r = await fn(current);
|
|
2883
|
+
if (r !== null && r !== void 0) current = r;
|
|
2884
|
+
}
|
|
2885
|
+
return current;
|
|
2886
|
+
}
|
|
2378
2887
|
/** Detector returning the AGENT_OUTPUT payload, or null to wrap verbatim. */
|
|
2379
2888
|
detectsOutput(fn) {
|
|
2380
2889
|
this.recognisers.output.push(fn);
|
|
@@ -2445,8 +2954,21 @@ var Axon = class _Axon {
|
|
|
2445
2954
|
[DETACH]() {
|
|
2446
2955
|
this.dendrite = null;
|
|
2447
2956
|
}
|
|
2448
|
-
/** Run the Neuron and return AGENT_OUTPUT / CLARIFICATION / ERROR.
|
|
2957
|
+
/** Run the Neuron and return AGENT_OUTPUT / CLARIFICATION / ERROR.
|
|
2958
|
+
*
|
|
2959
|
+
* Binds the TASK's (traceId, parentId=task.id) as the ambient trace
|
|
2960
|
+
* context for the whole handling pass - neuronFn, detectors, and hooks
|
|
2961
|
+
* included - so engram calls made without explicit trace plumbing (e.g.
|
|
2962
|
+
* `dendrite.imprint` from a `detectsOutput` hook) are attributed to this
|
|
2963
|
+
* task's trace. */
|
|
2449
2964
|
async handleTask(task) {
|
|
2965
|
+
return runWithTraceContext(
|
|
2966
|
+
task.trace_id,
|
|
2967
|
+
task.id,
|
|
2968
|
+
() => this.handleTaskInner(task)
|
|
2969
|
+
);
|
|
2970
|
+
}
|
|
2971
|
+
async handleTaskInner(task) {
|
|
2450
2972
|
const traceId = task.trace_id;
|
|
2451
2973
|
const parentId = task.id;
|
|
2452
2974
|
const input = task.payload["input"] ?? {};
|
|
@@ -2459,136 +2981,569 @@ var Axon = class _Axon {
|
|
|
2459
2981
|
context = [];
|
|
2460
2982
|
}
|
|
2461
2983
|
}
|
|
2462
|
-
let rawOutput;
|
|
2984
|
+
let rawOutput;
|
|
2985
|
+
try {
|
|
2986
|
+
const effectiveInput = this.beforeTaskHooks.length ? await this.applyBeforeTask(input) : input;
|
|
2987
|
+
const helpers = this.engramBindings.size ? this.buildHelpers(traceId, parentId) : void 0;
|
|
2988
|
+
rawOutput = await this.fn(effectiveInput, context, helpers);
|
|
2989
|
+
if (this.outputParser) rawOutput = this.outputParser(rawOutput);
|
|
2990
|
+
rawOutput = await this.applyRecognisers(rawOutput);
|
|
2991
|
+
} catch (err) {
|
|
2992
|
+
return errorSignal({
|
|
2993
|
+
traceId,
|
|
2994
|
+
parentId,
|
|
2995
|
+
directed: { id: this.neuronId },
|
|
2996
|
+
code: "NEURON_EXCEPTION",
|
|
2997
|
+
message: err instanceof Error ? err.message : String(err),
|
|
2998
|
+
recoverable: false
|
|
2999
|
+
});
|
|
3000
|
+
}
|
|
3001
|
+
if (isErrorOutput(rawOutput)) {
|
|
3002
|
+
return errorSignal({
|
|
3003
|
+
traceId,
|
|
3004
|
+
parentId,
|
|
3005
|
+
directed: { id: this.neuronId },
|
|
3006
|
+
code: rawOutput.code ?? "NEURON_ERROR",
|
|
3007
|
+
message: rawOutput.message ?? "",
|
|
3008
|
+
recoverable: Boolean(rawOutput.recoverable)
|
|
3009
|
+
});
|
|
3010
|
+
}
|
|
3011
|
+
if (isClarification(rawOutput)) {
|
|
3012
|
+
return clarificationSignal({
|
|
3013
|
+
traceId,
|
|
3014
|
+
parentId,
|
|
3015
|
+
directed: { id: this.neuronId },
|
|
3016
|
+
question: rawOutput.question,
|
|
3017
|
+
...rawOutput.context !== void 0 ? { context: rawOutput.context } : {}
|
|
3018
|
+
});
|
|
3019
|
+
}
|
|
3020
|
+
if (isPermissionRequest(rawOutput)) {
|
|
3021
|
+
return permissionSignal({
|
|
3022
|
+
traceId,
|
|
3023
|
+
parentId,
|
|
3024
|
+
directed: { id: this.neuronId },
|
|
3025
|
+
action: rawOutput.action,
|
|
3026
|
+
...rawOutput.scope !== void 0 ? { scope: rawOutput.scope } : {},
|
|
3027
|
+
...rawOutput.reason !== void 0 ? { reason: rawOutput.reason } : {},
|
|
3028
|
+
...rawOutput.context !== void 0 ? { context: rawOutput.context } : {}
|
|
3029
|
+
});
|
|
3030
|
+
}
|
|
3031
|
+
const output = typeof rawOutput === "object" && rawOutput !== null ? rawOutput : { value: rawOutput };
|
|
3032
|
+
return agentOutputSignal({ traceId, parentId, directed: { id: this.neuronId }, output });
|
|
3033
|
+
}
|
|
3034
|
+
};
|
|
3035
|
+
var COSMO_INTENT_SYSTEM_PROMPT = 'You can control the surrounding agent protocol by replying with a single JSON object carrying a "cosmo" key (either as your whole reply or inside a ```json fenced block):\n{"cosmo": "clarification", "question": "<what you need to know>"} - ask the orchestrator a question when the task is ambiguous.\n{"cosmo": "permission", "action": "<action>", "scope": {...}, "reason": "<why>"} - request approval before a sensitive action.\n{"cosmo": "error", "code": "<CODE>", "message": "<details>"} - report a structured failure.\n{"cosmo": "output", "output": {...}} - return a structured result.\nFor a normal answer, just reply with plain text - do not wrap ordinary answers in a cosmo object.';
|
|
3036
|
+
var SYSTEM_CAPABLE_SOURCES = /* @__PURE__ */ new Set([
|
|
3037
|
+
"ollama",
|
|
3038
|
+
"openai",
|
|
3039
|
+
"anthropic",
|
|
3040
|
+
"groq",
|
|
3041
|
+
"openrouter",
|
|
3042
|
+
"together",
|
|
3043
|
+
"mistral"
|
|
3044
|
+
]);
|
|
3045
|
+
var INTENT_KEY = "cosmo";
|
|
3046
|
+
var FENCED_JSON = /```(?:json)?\s*(\{[\s\S]*?\})\s*```/g;
|
|
3047
|
+
function extractCosmoIntent(text) {
|
|
3048
|
+
if (!text) return null;
|
|
3049
|
+
const candidates = [text.trim()];
|
|
3050
|
+
FENCED_JSON.lastIndex = 0;
|
|
3051
|
+
let m;
|
|
3052
|
+
while ((m = FENCED_JSON.exec(text)) !== null) candidates.push(m[1]);
|
|
3053
|
+
for (const cand of candidates) {
|
|
3054
|
+
let obj;
|
|
3055
|
+
try {
|
|
3056
|
+
obj = JSON.parse(cand);
|
|
3057
|
+
} catch {
|
|
3058
|
+
continue;
|
|
3059
|
+
}
|
|
3060
|
+
if (obj !== null && typeof obj === "object" && typeof obj[INTENT_KEY] === "string") {
|
|
3061
|
+
return obj;
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
return null;
|
|
3065
|
+
}
|
|
3066
|
+
function intentToMarker(intent) {
|
|
3067
|
+
const kind = intent[INTENT_KEY];
|
|
3068
|
+
if (kind === "clarification") {
|
|
3069
|
+
return {
|
|
3070
|
+
__clarification__: true,
|
|
3071
|
+
question: intent["question"] ?? "",
|
|
3072
|
+
...intent["context"] !== void 0 ? { context: intent["context"] } : {}
|
|
3073
|
+
};
|
|
3074
|
+
}
|
|
3075
|
+
if (kind === "permission") {
|
|
3076
|
+
return {
|
|
3077
|
+
__permission__: true,
|
|
3078
|
+
action: intent["action"] ?? "",
|
|
3079
|
+
...intent["scope"] !== void 0 ? { scope: intent["scope"] } : {},
|
|
3080
|
+
...intent["reason"] !== void 0 ? { reason: intent["reason"] } : {},
|
|
3081
|
+
...intent["context"] !== void 0 ? { context: intent["context"] } : {}
|
|
3082
|
+
};
|
|
3083
|
+
}
|
|
3084
|
+
if (kind === "error") {
|
|
3085
|
+
return {
|
|
3086
|
+
__error__: true,
|
|
3087
|
+
code: intent["code"] ?? "NEURON_ERROR",
|
|
3088
|
+
message: intent["message"] ?? "",
|
|
3089
|
+
recoverable: Boolean(intent["recoverable"])
|
|
3090
|
+
};
|
|
3091
|
+
}
|
|
3092
|
+
if (kind === "output") {
|
|
3093
|
+
const out = intent["output"];
|
|
3094
|
+
return out !== null && typeof out === "object" ? out : { value: out };
|
|
3095
|
+
}
|
|
3096
|
+
return null;
|
|
3097
|
+
}
|
|
3098
|
+
function parseLlmIntents(raw) {
|
|
3099
|
+
if (raw === null || typeof raw !== "object") return { value: raw };
|
|
3100
|
+
const text = raw["response"];
|
|
3101
|
+
if (typeof text === "string") {
|
|
3102
|
+
const intent = extractCosmoIntent(text);
|
|
3103
|
+
if (intent) {
|
|
3104
|
+
const marker = intentToMarker(intent);
|
|
3105
|
+
if (marker) return marker;
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
return raw;
|
|
3109
|
+
}
|
|
3110
|
+
function parseMcpIntents(raw) {
|
|
3111
|
+
if (raw === null || typeof raw !== "object") return { value: raw };
|
|
3112
|
+
const r = raw;
|
|
3113
|
+
if (r["is_error"]) {
|
|
3114
|
+
const msg = r["response"] ?? r["content"] ?? "MCP tool returned is_error";
|
|
3115
|
+
return { __error__: true, code: "MCP_TOOL_ERROR", message: String(msg) };
|
|
3116
|
+
}
|
|
3117
|
+
const text = r["response"];
|
|
3118
|
+
if (typeof text === "string") {
|
|
3119
|
+
const intent = extractCosmoIntent(text);
|
|
3120
|
+
if (intent) {
|
|
3121
|
+
const marker = intentToMarker(intent);
|
|
3122
|
+
if (marker) return marker;
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
return raw;
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
// src/pathway.ts
|
|
3129
|
+
var TERMINAL_TYPES = /* @__PURE__ */ new Set([
|
|
3130
|
+
SignalType.FINAL,
|
|
3131
|
+
SignalType.ERROR
|
|
3132
|
+
]);
|
|
3133
|
+
var WAIT_TYPES = /* @__PURE__ */ new Set([
|
|
3134
|
+
SignalType.AGENT_OUTPUT,
|
|
3135
|
+
SignalType.CLARIFICATION,
|
|
3136
|
+
SignalType.PERMISSION,
|
|
3137
|
+
SignalType.ERROR,
|
|
3138
|
+
SignalType.FINAL
|
|
3139
|
+
]);
|
|
3140
|
+
var SCOPE_TERMINAL_TYPES = /* @__PURE__ */ new Set([
|
|
3141
|
+
SignalType.FINAL,
|
|
3142
|
+
SignalType.ERROR,
|
|
3143
|
+
SignalType.CLARIFICATION,
|
|
3144
|
+
SignalType.PERMISSION
|
|
3145
|
+
]);
|
|
3146
|
+
var PATHWAY_TYPES = new Set(
|
|
3147
|
+
Object.values(SignalType).filter(
|
|
3148
|
+
(t) => t !== SignalType.TASK && t !== SignalType.REGISTER && t !== SignalType.DEREGISTER && t !== SignalType.HEARTBEAT && t !== SignalType.DISCOVER
|
|
3149
|
+
)
|
|
3150
|
+
);
|
|
3151
|
+
var PathwayClosedError = class extends Error {
|
|
3152
|
+
constructor(message) {
|
|
3153
|
+
super(message);
|
|
3154
|
+
this.name = "PathwayClosedError";
|
|
3155
|
+
}
|
|
3156
|
+
};
|
|
3157
|
+
var Pathway = class {
|
|
3158
|
+
traceId;
|
|
3159
|
+
parentId;
|
|
3160
|
+
role;
|
|
3161
|
+
scope;
|
|
3162
|
+
scopeFilter;
|
|
3163
|
+
onCloseHook;
|
|
3164
|
+
handlers = /* @__PURE__ */ new Map();
|
|
3165
|
+
waiters = [];
|
|
3166
|
+
buffered = [];
|
|
3167
|
+
closed_ = false;
|
|
3168
|
+
// Async iteration: a pull queue of pending `next()` resolvers and a push
|
|
3169
|
+
// queue of undelivered values. `null` is the close sentinel.
|
|
3170
|
+
iterPush = [];
|
|
3171
|
+
iterPull = [];
|
|
3172
|
+
constructor(opts) {
|
|
3173
|
+
const scope = opts.scope ?? "all";
|
|
3174
|
+
if (scope !== "all" && scope !== "terminal") {
|
|
3175
|
+
throw new Error(`scope must be 'all' or 'terminal', got '${scope}'`);
|
|
3176
|
+
}
|
|
3177
|
+
this.traceId = opts.traceId;
|
|
3178
|
+
this.parentId = opts.parentId ?? null;
|
|
3179
|
+
this.role = opts.role ?? "originator";
|
|
3180
|
+
this.scope = scope;
|
|
3181
|
+
this.scopeFilter = scope === "terminal" ? SCOPE_TERMINAL_TYPES : null;
|
|
3182
|
+
this.onCloseHook = opts.onClose;
|
|
3183
|
+
}
|
|
3184
|
+
get closed() {
|
|
3185
|
+
return this.closed_;
|
|
3186
|
+
}
|
|
3187
|
+
// -- consumer shape #1: wait ---------------------------------------
|
|
3188
|
+
/** Resolve on the next AGENT_OUTPUT, CLARIFICATION, PERMISSION, ERROR or
|
|
3189
|
+
* FINAL. Rejects with PathwayClosedError if the Pathway closes first, and
|
|
3190
|
+
* with a TimeoutError-named Error if `timeoutMs` elapses. */
|
|
3191
|
+
async wait(timeoutMs) {
|
|
3192
|
+
return this.waitForTypes(WAIT_TYPES, timeoutMs);
|
|
3193
|
+
}
|
|
3194
|
+
/** Resolve on the next Signal of the given type. */
|
|
3195
|
+
async waitFor(type, timeoutMs) {
|
|
3196
|
+
return this.waitForTypes(/* @__PURE__ */ new Set([type]), timeoutMs);
|
|
3197
|
+
}
|
|
3198
|
+
async waitForTypes(types, timeoutMs) {
|
|
3199
|
+
for (let i = 0; i < this.buffered.length; i++) {
|
|
3200
|
+
const sig = this.buffered[i];
|
|
3201
|
+
if (types.has(sig.type)) {
|
|
3202
|
+
this.buffered.splice(i, 1);
|
|
3203
|
+
return sig;
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
if (this.closed_) {
|
|
3207
|
+
throw new PathwayClosedError(`Pathway for trace '${this.traceId}' is closed`);
|
|
3208
|
+
}
|
|
3209
|
+
return new Promise((resolve, reject) => {
|
|
3210
|
+
const waiter = { types, resolve, reject, settled: false };
|
|
3211
|
+
let timer = null;
|
|
3212
|
+
const settle = (fn) => (a) => {
|
|
3213
|
+
if (waiter.settled) return;
|
|
3214
|
+
waiter.settled = true;
|
|
3215
|
+
if (timer !== null) clearTimeout(timer);
|
|
3216
|
+
this.waiters = this.waiters.filter((w) => w !== waiter);
|
|
3217
|
+
fn(a);
|
|
3218
|
+
};
|
|
3219
|
+
waiter.resolve = settle(resolve);
|
|
3220
|
+
waiter.reject = settle(reject);
|
|
3221
|
+
if (timeoutMs !== void 0) {
|
|
3222
|
+
timer = setTimeout(() => {
|
|
3223
|
+
const err = new Error(
|
|
3224
|
+
`Pathway.wait timed out after ${timeoutMs}ms on trace '${this.traceId}'`
|
|
3225
|
+
);
|
|
3226
|
+
err.name = "TimeoutError";
|
|
3227
|
+
waiter.reject(err);
|
|
3228
|
+
}, timeoutMs);
|
|
3229
|
+
}
|
|
3230
|
+
this.waiters.push(waiter);
|
|
3231
|
+
});
|
|
3232
|
+
}
|
|
3233
|
+
// -- consumer shape #2: callbacks ----------------------------------
|
|
3234
|
+
/** Register a callback fired for each Signal of the given type. */
|
|
3235
|
+
on(type, fn) {
|
|
3236
|
+
const list = this.handlers.get(type) ?? [];
|
|
3237
|
+
list.push(fn);
|
|
3238
|
+
this.handlers.set(type, list);
|
|
3239
|
+
return fn;
|
|
3240
|
+
}
|
|
3241
|
+
// -- consumer shape #3: async iteration ----------------------------
|
|
3242
|
+
[Symbol.asyncIterator]() {
|
|
3243
|
+
return {
|
|
3244
|
+
next: () => {
|
|
3245
|
+
if (this.iterPush.length > 0) {
|
|
3246
|
+
const v = this.iterPush.shift();
|
|
3247
|
+
return Promise.resolve(
|
|
3248
|
+
v === null ? { value: void 0, done: true } : { value: v, done: false }
|
|
3249
|
+
);
|
|
3250
|
+
}
|
|
3251
|
+
if (this.closed_) {
|
|
3252
|
+
return Promise.resolve({ value: void 0, done: true });
|
|
3253
|
+
}
|
|
3254
|
+
return new Promise((resolve) => this.iterPull.push(resolve));
|
|
3255
|
+
}
|
|
3256
|
+
};
|
|
3257
|
+
}
|
|
3258
|
+
iterEmit(v) {
|
|
3259
|
+
const pull = this.iterPull.shift();
|
|
3260
|
+
if (pull) {
|
|
3261
|
+
pull(v === null ? { value: void 0, done: true } : { value: v, done: false });
|
|
3262
|
+
} else {
|
|
3263
|
+
this.iterPush.push(v);
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
// -- lifecycle ------------------------------------------------------
|
|
3267
|
+
/** Close the Pathway. Idempotent. Pending waits reject with
|
|
3268
|
+
* PathwayClosedError; iteration completes; the onClose hook fires once. */
|
|
3269
|
+
async close() {
|
|
3270
|
+
if (this.closed_) return;
|
|
3271
|
+
this.closed_ = true;
|
|
3272
|
+
for (const w of [...this.waiters]) {
|
|
3273
|
+
w.reject(
|
|
3274
|
+
new PathwayClosedError(
|
|
3275
|
+
`Pathway for trace '${this.traceId}' closed before a matching Signal arrived`
|
|
3276
|
+
)
|
|
3277
|
+
);
|
|
3278
|
+
}
|
|
3279
|
+
this.waiters = [];
|
|
3280
|
+
this.iterEmit(null);
|
|
3281
|
+
if (this.onCloseHook) {
|
|
3282
|
+
try {
|
|
3283
|
+
await this.onCloseHook(this);
|
|
3284
|
+
} catch {
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
/** `await using pathway = ...` support. */
|
|
3289
|
+
async [Symbol.asyncDispose]() {
|
|
3290
|
+
await this.close();
|
|
3291
|
+
}
|
|
3292
|
+
// -- internal: signal delivery (called by the owning Dendrite) ------
|
|
3293
|
+
/** @internal */
|
|
3294
|
+
async _deliver(signal) {
|
|
3295
|
+
if (this.closed_) return;
|
|
3296
|
+
if (this.scopeFilter !== null && !this.scopeFilter.has(signal.type)) {
|
|
3297
|
+
await this.fireHandlers(signal);
|
|
3298
|
+
if (TERMINAL_TYPES.has(signal.type)) await this.close();
|
|
3299
|
+
return;
|
|
3300
|
+
}
|
|
3301
|
+
let consumed = false;
|
|
3302
|
+
for (const w of [...this.waiters]) {
|
|
3303
|
+
if (w.types.has(signal.type)) {
|
|
3304
|
+
w.resolve(signal);
|
|
3305
|
+
consumed = true;
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
if (!consumed) this.buffered.push(signal);
|
|
3309
|
+
await this.fireHandlers(signal);
|
|
3310
|
+
this.iterEmit(signal);
|
|
3311
|
+
if (TERMINAL_TYPES.has(signal.type)) await this.close();
|
|
3312
|
+
}
|
|
3313
|
+
async fireHandlers(signal) {
|
|
3314
|
+
for (const h of this.handlers.get(signal.type) ?? []) {
|
|
3315
|
+
try {
|
|
3316
|
+
await h(signal);
|
|
3317
|
+
} catch {
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
};
|
|
3322
|
+
|
|
3323
|
+
// src/engram-client.ts
|
|
3324
|
+
function deferred() {
|
|
3325
|
+
let resolve;
|
|
3326
|
+
let reject;
|
|
3327
|
+
const promise = new Promise((res, rej) => {
|
|
3328
|
+
resolve = res;
|
|
3329
|
+
reject = rej;
|
|
3330
|
+
});
|
|
3331
|
+
return { promise, resolve, reject };
|
|
3332
|
+
}
|
|
3333
|
+
var EngramClient = class {
|
|
3334
|
+
constructor(publisher) {
|
|
3335
|
+
this.publisher = publisher;
|
|
3336
|
+
}
|
|
3337
|
+
publisher;
|
|
3338
|
+
pendingRecalls = /* @__PURE__ */ new Map();
|
|
3339
|
+
pendingImprints = /* @__PURE__ */ new Map();
|
|
3340
|
+
byTrace = /* @__PURE__ */ new Map();
|
|
3341
|
+
async recall(args) {
|
|
3342
|
+
let engramId = args.engramId;
|
|
3343
|
+
let engramKind = args.engramKind;
|
|
3344
|
+
let deadlineMs = args.deadlineMs;
|
|
3345
|
+
let recallMode = args.recallMode;
|
|
3346
|
+
if (args.binding) {
|
|
3347
|
+
engramId = engramId ?? args.binding.directedId ?? void 0;
|
|
3348
|
+
engramKind = engramKind ?? args.binding.directedType ?? void 0;
|
|
3349
|
+
if (deadlineMs === void 0) deadlineMs = args.binding.defaultDeadlineMs ?? void 0;
|
|
3350
|
+
if (recallMode === void 0) recallMode = args.binding.defaultRecallMode;
|
|
3351
|
+
}
|
|
3352
|
+
const mode = recallMode ?? "first";
|
|
3353
|
+
const sig = recallSignal({
|
|
3354
|
+
traceId: args.traceId,
|
|
3355
|
+
parentId: args.parentId,
|
|
3356
|
+
directed: directedTo(engramId ?? null, { type: engramKind ?? null }),
|
|
3357
|
+
query: args.query,
|
|
3358
|
+
...args.filters !== void 0 ? { filters: args.filters } : {},
|
|
3359
|
+
...args.contextRef !== void 0 ? { contextRef: args.contextRef } : {},
|
|
3360
|
+
...deadlineMs !== void 0 ? { deadlineMs } : {},
|
|
3361
|
+
...args.minConfidence !== void 0 ? { minConfidence: args.minConfidence } : {},
|
|
3362
|
+
recallMode: mode,
|
|
3363
|
+
...args.meta !== void 0 ? { meta: args.meta } : {}
|
|
3364
|
+
});
|
|
3365
|
+
const d = deferred();
|
|
3366
|
+
const pending = { deferred: d, mode, timer: null, done: false, hitsSoFar: [], engrams: [] };
|
|
3367
|
+
this.pendingRecalls.set(sig.id, pending);
|
|
3368
|
+
this.track(args.traceId, sig.id);
|
|
3369
|
+
if (deadlineMs !== void 0 && deadlineMs > 0) {
|
|
3370
|
+
pending.timer = setTimeout(() => this.onRecallDeadline(sig.id), deadlineMs);
|
|
3371
|
+
}
|
|
2463
3372
|
try {
|
|
2464
|
-
|
|
2465
|
-
if (this.outputParser) rawOutput = this.outputParser(rawOutput);
|
|
2466
|
-
rawOutput = await this.applyRecognisers(rawOutput);
|
|
3373
|
+
await this.publisher.publish(sig);
|
|
2467
3374
|
} catch (err) {
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
parentId,
|
|
2471
|
-
directed: { id: this.neuronId },
|
|
2472
|
-
code: "NEURON_EXCEPTION",
|
|
2473
|
-
message: err instanceof Error ? err.message : String(err),
|
|
2474
|
-
recoverable: false
|
|
2475
|
-
});
|
|
3375
|
+
this.cleanupRecall(args.traceId, sig.id);
|
|
3376
|
+
throw err;
|
|
2476
3377
|
}
|
|
2477
|
-
|
|
2478
|
-
return
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
directed: { id: this.neuronId },
|
|
2482
|
-
code: rawOutput.code ?? "NEURON_ERROR",
|
|
2483
|
-
message: rawOutput.message ?? "",
|
|
2484
|
-
recoverable: Boolean(rawOutput.recoverable)
|
|
2485
|
-
});
|
|
3378
|
+
try {
|
|
3379
|
+
return await d.promise;
|
|
3380
|
+
} finally {
|
|
3381
|
+
this.cleanupRecall(args.traceId, sig.id);
|
|
2486
3382
|
}
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
});
|
|
3383
|
+
}
|
|
3384
|
+
async imprint(args) {
|
|
3385
|
+
let engramId = args.engramId;
|
|
3386
|
+
let engramKind = args.engramKind;
|
|
3387
|
+
if (args.binding) {
|
|
3388
|
+
engramId = engramId ?? args.binding.directedId ?? void 0;
|
|
3389
|
+
engramKind = engramKind ?? args.binding.directedType ?? void 0;
|
|
2495
3390
|
}
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
3391
|
+
const sig = imprintSignal({
|
|
3392
|
+
traceId: args.traceId,
|
|
3393
|
+
parentId: args.parentId,
|
|
3394
|
+
directed: directedTo(engramId ?? null, { type: engramKind ?? null }),
|
|
3395
|
+
op: args.op,
|
|
3396
|
+
entry: args.entry,
|
|
3397
|
+
...args.mergeKey !== void 0 ? { mergeKey: args.mergeKey } : {},
|
|
3398
|
+
...args.meta !== void 0 ? { meta: args.meta } : {}
|
|
3399
|
+
});
|
|
3400
|
+
if (!args.awaitAck) {
|
|
3401
|
+
await this.publisher.publish(sig);
|
|
3402
|
+
return null;
|
|
3403
|
+
}
|
|
3404
|
+
const d = deferred();
|
|
3405
|
+
const pending = { deferred: d, timer: null, done: false };
|
|
3406
|
+
this.pendingImprints.set(sig.id, pending);
|
|
3407
|
+
this.track(args.traceId, sig.id);
|
|
3408
|
+
if (args.deadlineMs !== void 0 && args.deadlineMs > 0) {
|
|
3409
|
+
pending.timer = setTimeout(() => this.onImprintDeadline(sig.id), args.deadlineMs);
|
|
2506
3410
|
}
|
|
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
3411
|
try {
|
|
2522
|
-
|
|
2523
|
-
} catch {
|
|
2524
|
-
|
|
3412
|
+
await this.publisher.publish(sig);
|
|
3413
|
+
} catch (err) {
|
|
3414
|
+
this.cleanupImprint(args.traceId, sig.id);
|
|
3415
|
+
throw err;
|
|
2525
3416
|
}
|
|
2526
|
-
|
|
2527
|
-
return
|
|
3417
|
+
try {
|
|
3418
|
+
return await d.promise;
|
|
3419
|
+
} finally {
|
|
3420
|
+
this.cleanupImprint(args.traceId, sig.id);
|
|
2528
3421
|
}
|
|
2529
3422
|
}
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
3423
|
+
/** Match RECALLED / IMPRINTED by parent_id and resolve pendings. */
|
|
3424
|
+
deliver(sig) {
|
|
3425
|
+
const pid = sig.parent_id;
|
|
3426
|
+
if (pid === null) return;
|
|
3427
|
+
if (sig.type === SignalType.RECALLED) {
|
|
3428
|
+
const pending = this.pendingRecalls.get(pid);
|
|
3429
|
+
if (pending === void 0) return;
|
|
3430
|
+
const hits = hitsFromPayload(sig.payload["hits"]);
|
|
3431
|
+
const engramId = typeof sig.payload["engram_id"] === "string" ? sig.payload["engram_id"] : "";
|
|
3432
|
+
const tookMs = typeof sig.payload["took_ms"] === "number" ? sig.payload["took_ms"] : null;
|
|
3433
|
+
const truncated = sig.payload["truncated"] === true;
|
|
3434
|
+
if (pending.mode === "first") {
|
|
3435
|
+
if (!pending.done) {
|
|
3436
|
+
pending.done = true;
|
|
3437
|
+
pending.deferred.resolve({
|
|
3438
|
+
hits,
|
|
3439
|
+
engramIds: engramId ? [engramId] : [],
|
|
3440
|
+
truncated,
|
|
3441
|
+
tookMs
|
|
3442
|
+
});
|
|
3443
|
+
}
|
|
3444
|
+
} else {
|
|
3445
|
+
pending.hitsSoFar.push(...hits);
|
|
3446
|
+
if (engramId) pending.engrams.push(engramId);
|
|
3447
|
+
}
|
|
3448
|
+
} else if (sig.type === SignalType.IMPRINTED) {
|
|
3449
|
+
const pending = this.pendingImprints.get(pid);
|
|
3450
|
+
if (pending === void 0 || pending.done) return;
|
|
3451
|
+
pending.done = true;
|
|
3452
|
+
pending.deferred.resolve({
|
|
3453
|
+
engramId: typeof sig.payload["engram_id"] === "string" ? sig.payload["engram_id"] : "",
|
|
3454
|
+
op: typeof sig.payload["op"] === "string" ? sig.payload["op"] : "",
|
|
3455
|
+
id: typeof sig.payload["id"] === "string" ? sig.payload["id"] : null,
|
|
3456
|
+
version: typeof sig.payload["version"] === "number" ? sig.payload["version"] : null,
|
|
3457
|
+
tookMs: typeof sig.payload["took_ms"] === "number" ? sig.payload["took_ms"] : null,
|
|
3458
|
+
error: typeof sig.payload["error"] === "string" ? sig.payload["error"] : null,
|
|
3459
|
+
ok: !(typeof sig.payload["error"] === "string")
|
|
3460
|
+
});
|
|
3461
|
+
}
|
|
2549
3462
|
}
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
3463
|
+
/** Cancel every in-flight recall/imprint on a trace (FINAL/ERROR or shutdown). */
|
|
3464
|
+
cancelTrace(traceId) {
|
|
3465
|
+
const ids = this.byTrace.get(traceId);
|
|
3466
|
+
this.byTrace.delete(traceId);
|
|
3467
|
+
if (ids === void 0) return;
|
|
3468
|
+
for (const id of ids) {
|
|
3469
|
+
const pr = this.pendingRecalls.get(id);
|
|
3470
|
+
if (pr !== void 0 && !pr.done) {
|
|
3471
|
+
pr.done = true;
|
|
3472
|
+
if (pr.timer !== null) clearTimeout(pr.timer);
|
|
3473
|
+
pr.deferred.reject(new EngramCancelled(`trace ${traceId} terminated while recall ${id} in flight`));
|
|
3474
|
+
this.pendingRecalls.delete(id);
|
|
3475
|
+
}
|
|
3476
|
+
const pi = this.pendingImprints.get(id);
|
|
3477
|
+
if (pi !== void 0 && !pi.done) {
|
|
3478
|
+
pi.done = true;
|
|
3479
|
+
if (pi.timer !== null) clearTimeout(pi.timer);
|
|
3480
|
+
pi.deferred.reject(new EngramCancelled(`trace ${traceId} terminated while imprint ${id} in flight`));
|
|
3481
|
+
this.pendingImprints.delete(id);
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
2557
3484
|
}
|
|
2558
|
-
|
|
2559
|
-
const
|
|
2560
|
-
return out !== null && typeof out === "object" ? out : { value: out };
|
|
3485
|
+
cancelAll() {
|
|
3486
|
+
for (const traceId of [...this.byTrace.keys()]) this.cancelTrace(traceId);
|
|
2561
3487
|
}
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
3488
|
+
onRecallDeadline(id) {
|
|
3489
|
+
const pending = this.pendingRecalls.get(id);
|
|
3490
|
+
if (pending === void 0 || pending.done) return;
|
|
3491
|
+
pending.done = true;
|
|
3492
|
+
if (pending.mode === "first") {
|
|
3493
|
+
pending.deferred.reject(new EngramTimeout(`RECALL ${id} elapsed deadline without any responder`));
|
|
3494
|
+
} else {
|
|
3495
|
+
pending.deferred.resolve({
|
|
3496
|
+
hits: [...pending.hitsSoFar].sort((a, b) => b.score - a.score),
|
|
3497
|
+
engramIds: [...pending.engrams],
|
|
3498
|
+
truncated: false,
|
|
3499
|
+
tookMs: null
|
|
3500
|
+
});
|
|
2572
3501
|
}
|
|
2573
3502
|
}
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
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) };
|
|
3503
|
+
onImprintDeadline(id) {
|
|
3504
|
+
const pending = this.pendingImprints.get(id);
|
|
3505
|
+
if (pending === void 0 || pending.done) return;
|
|
3506
|
+
pending.done = true;
|
|
3507
|
+
pending.deferred.reject(new EngramTimeout(`IMPRINT ${id} elapsed deadline without IMPRINTED`));
|
|
2582
3508
|
}
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
3509
|
+
track(traceId, id) {
|
|
3510
|
+
const bucket = this.byTrace.get(traceId);
|
|
3511
|
+
if (bucket) bucket.add(id);
|
|
3512
|
+
else this.byTrace.set(traceId, /* @__PURE__ */ new Set([id]));
|
|
3513
|
+
}
|
|
3514
|
+
cleanupRecall(traceId, id) {
|
|
3515
|
+
const p = this.pendingRecalls.get(id);
|
|
3516
|
+
if (p?.timer != null) clearTimeout(p.timer);
|
|
3517
|
+
this.pendingRecalls.delete(id);
|
|
3518
|
+
this.discardTrace(traceId, id);
|
|
3519
|
+
}
|
|
3520
|
+
cleanupImprint(traceId, id) {
|
|
3521
|
+
const p = this.pendingImprints.get(id);
|
|
3522
|
+
if (p?.timer != null) clearTimeout(p.timer);
|
|
3523
|
+
this.pendingImprints.delete(id);
|
|
3524
|
+
this.discardTrace(traceId, id);
|
|
3525
|
+
}
|
|
3526
|
+
discardTrace(traceId, id) {
|
|
3527
|
+
const bucket = this.byTrace.get(traceId);
|
|
3528
|
+
if (bucket === void 0) return;
|
|
3529
|
+
bucket.delete(id);
|
|
3530
|
+
if (bucket.size === 0) this.byTrace.delete(traceId);
|
|
2590
3531
|
}
|
|
2591
|
-
|
|
3532
|
+
};
|
|
3533
|
+
function hitsFromPayload(raw) {
|
|
3534
|
+
if (!Array.isArray(raw)) return [];
|
|
3535
|
+
const out = [];
|
|
3536
|
+
for (const h of raw) {
|
|
3537
|
+
if (h === null || typeof h !== "object") continue;
|
|
3538
|
+
const obj = h;
|
|
3539
|
+
const entryVal = obj["entry"];
|
|
3540
|
+
out.push({
|
|
3541
|
+
id: typeof obj["id"] === "string" ? obj["id"] : "",
|
|
3542
|
+
entry: entryVal !== null && typeof entryVal === "object" && !Array.isArray(entryVal) ? entryVal : { value: entryVal },
|
|
3543
|
+
score: typeof obj["score"] === "number" ? obj["score"] : 1
|
|
3544
|
+
});
|
|
3545
|
+
}
|
|
3546
|
+
return out;
|
|
2592
3547
|
}
|
|
2593
3548
|
|
|
2594
3549
|
// src/dendrite.ts
|
|
@@ -2599,33 +3554,67 @@ var DendriteProtocolError = class extends Error {
|
|
|
2599
3554
|
this.name = "DendriteProtocolError";
|
|
2600
3555
|
}
|
|
2601
3556
|
};
|
|
2602
|
-
var Dendrite = class {
|
|
3557
|
+
var Dendrite = class _Dendrite {
|
|
2603
3558
|
synapse;
|
|
2604
3559
|
registryStore;
|
|
2605
3560
|
namespace;
|
|
2606
3561
|
dendriteId;
|
|
3562
|
+
role;
|
|
2607
3563
|
heartbeatMs;
|
|
2608
3564
|
reregisterOnHeartbeat;
|
|
3565
|
+
autoBid;
|
|
3566
|
+
staleAfterMs;
|
|
2609
3567
|
_axons = /* @__PURE__ */ new Map();
|
|
2610
3568
|
handlers = /* @__PURE__ */ new Map();
|
|
2611
3569
|
taskSub = null;
|
|
3570
|
+
routedTaskSub = null;
|
|
2612
3571
|
inboundSubs = /* @__PURE__ */ new Map();
|
|
2613
|
-
|
|
3572
|
+
inflightSubs = /* @__PURE__ */ new Map();
|
|
3573
|
+
pendingSubs = /* @__PURE__ */ new Set();
|
|
3574
|
+
/** Recently seen CLARIFICATION_ANSWER / PERMISSION_DECISION signals keyed
|
|
3575
|
+
* by parent_id, so {@link awaitDecision} can serve an answer that arrived
|
|
3576
|
+
* before it was called (an in-process synapse can deliver the whole
|
|
3577
|
+
* request->answer chain within the original publish). Bounded FIFO. */
|
|
3578
|
+
recentDecisions = /* @__PURE__ */ new Map();
|
|
3579
|
+
/** Hosted Engrams keyed by engramId, plus a kind index so RECALL/IMPRINT
|
|
3580
|
+
* addressed by engramKind reach every matching host. */
|
|
3581
|
+
_engrams = /* @__PURE__ */ new Map();
|
|
3582
|
+
engramKindIndex = /* @__PURE__ */ new Map();
|
|
3583
|
+
/** Engrams learned from peer REGISTER signals (possibly out-of-process). */
|
|
3584
|
+
_engramRegistrations = /* @__PURE__ */ new Map();
|
|
3585
|
+
engramRegKindIndex = /* @__PURE__ */ new Map();
|
|
3586
|
+
/** Caller-side correlation table for RECALL/IMPRINT awaiting
|
|
3587
|
+
* RECALLED/IMPRINTED. The Dendrite owns the subscriptions and feeds it. */
|
|
3588
|
+
engramClient = new EngramClient(this);
|
|
2614
3589
|
heartbeatTimer = null;
|
|
2615
|
-
// Set true by stop() so an in-flight tick won't re-arm the loop.
|
|
2616
3590
|
heartbeatStopped = true;
|
|
2617
3591
|
running = false;
|
|
3592
|
+
/** Open Pathways keyed by trace_id (dispatch / observePathway). */
|
|
3593
|
+
pathways = /* @__PURE__ */ new Map();
|
|
3594
|
+
/** Per-operation Pathways keyed by the issuing request's id (matched
|
|
3595
|
+
* against inbound parent_id) - the generic request/reply primitive
|
|
3596
|
+
* behind awaitDecision (and a future EngramClient wiring). */
|
|
3597
|
+
opPathways = /* @__PURE__ */ new Map();
|
|
2618
3598
|
/** @internal - lifecycle hooks for this Dendrite. */
|
|
2619
3599
|
hooks = new LifecycleHooks(this);
|
|
2620
3600
|
constructor(opts) {
|
|
2621
3601
|
if (!opts.synapse) throw new TypeError("Dendrite requires a synapse");
|
|
3602
|
+
const role = opts.role ?? "orchestrator";
|
|
3603
|
+
if (role !== "orchestrator" && role !== "worker") {
|
|
3604
|
+
throw new Error(`role must be 'orchestrator' or 'worker', got '${role}'`);
|
|
3605
|
+
}
|
|
2622
3606
|
this.synapse = opts.synapse;
|
|
2623
3607
|
this.registryStore = opts.registryStore ?? null;
|
|
2624
3608
|
this.namespace = opts.namespace ?? "default";
|
|
2625
3609
|
this.dendriteId = opts.dendriteId ?? "dendrite";
|
|
2626
3610
|
this.heartbeatMs = opts.heartbeatMs ?? 3e4;
|
|
2627
3611
|
this.reregisterOnHeartbeat = opts.reregisterOnHeartbeat ?? true;
|
|
2628
|
-
|
|
3612
|
+
this.role = role;
|
|
3613
|
+
this.autoBid = opts.autoBid ?? true;
|
|
3614
|
+
this.staleAfterMs = opts.staleAfterMs ?? (this.heartbeatMs > 0 ? this.heartbeatMs * 3 : 0);
|
|
3615
|
+
for (const t of Object.values(SignalType)) {
|
|
3616
|
+
this.handlers.set(t, []);
|
|
3617
|
+
}
|
|
2629
3618
|
}
|
|
2630
3619
|
// -- properties ----------------------------------------------------
|
|
2631
3620
|
get axons() {
|
|
@@ -2634,70 +3623,393 @@ var Dendrite = class {
|
|
|
2634
3623
|
axon(neuronId) {
|
|
2635
3624
|
return this._axons.get(neuronId);
|
|
2636
3625
|
}
|
|
3626
|
+
/** Aggregate of every attached Axon's capabilities, deduplicated + sorted. */
|
|
3627
|
+
get capabilities() {
|
|
3628
|
+
const caps = /* @__PURE__ */ new Set();
|
|
3629
|
+
for (const ax of this._axons.values()) for (const c of ax.capabilities) caps.add(c);
|
|
3630
|
+
return [...caps].sort();
|
|
3631
|
+
}
|
|
3632
|
+
/** Canonical queue-group name for this Dendrite's aggregate caps, or null
|
|
3633
|
+
* when no Axons are attached. Identical Dendrites share a group. */
|
|
3634
|
+
capQueueGroup() {
|
|
3635
|
+
const caps = this.capabilities;
|
|
3636
|
+
return caps.length ? `caps:${caps.join(",")}` : null;
|
|
3637
|
+
}
|
|
3638
|
+
requireOrchestrator(op) {
|
|
3639
|
+
if (this.role !== "orchestrator") {
|
|
3640
|
+
throw new DendriteProtocolError(
|
|
3641
|
+
`Dendrite role='${this.role}' cannot perform '${op}': only role='orchestrator' Dendrites may dispatch TASK signals. Workers host Axons and emit replies / cognition signals freely.`
|
|
3642
|
+
);
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
2637
3645
|
// -- attachment ----------------------------------------------------
|
|
3646
|
+
/**
|
|
3647
|
+
* Attach an Axon to a *stopped* Dendrite. Throws if the Dendrite is
|
|
3648
|
+
* running - a running Dendrite needs the async activation path
|
|
3649
|
+
* (subscriptions, queue-group refresh, REGISTER): use
|
|
3650
|
+
* `await dendrite.addAxon(axon)` instead, which works in both states.
|
|
3651
|
+
*/
|
|
2638
3652
|
attachAxon(axon) {
|
|
3653
|
+
if (this.running) {
|
|
3654
|
+
throw new Error(
|
|
3655
|
+
"attachAxon on a running Dendrite would never receive TASKs (no subscription / REGISTER is set up after start). Use `await dendrite.addAxon(axon)` instead."
|
|
3656
|
+
);
|
|
3657
|
+
}
|
|
3658
|
+
this.attachAxonRecord(axon);
|
|
3659
|
+
}
|
|
3660
|
+
attachAxonRecord(axon) {
|
|
2639
3661
|
if (this._axons.has(axon.neuronId)) {
|
|
2640
3662
|
throw new Error(`Dendrite already has an Axon for neuronId='${axon.neuronId}'`);
|
|
2641
3663
|
}
|
|
2642
3664
|
this._axons.set(axon.neuronId, axon);
|
|
2643
3665
|
axon[ATTACH](this);
|
|
2644
3666
|
}
|
|
3667
|
+
/**
|
|
3668
|
+
* Attach an Axon; if the Dendrite is running, activate it live: ensure the
|
|
3669
|
+
* addressed + routed TASK subscriptions exist (re-keying the routed queue
|
|
3670
|
+
* group for the new aggregate cap profile), subscribe TASK_AWARDED /
|
|
3671
|
+
* DISCOVER (and TASK_OFFER when autoBid), mirror to the registry store,
|
|
3672
|
+
* emit REGISTER, and fire the Axon's onConnect hooks.
|
|
3673
|
+
*/
|
|
3674
|
+
async addAxon(axon) {
|
|
3675
|
+
this.attachAxonRecord(axon);
|
|
3676
|
+
if (!this.running) return;
|
|
3677
|
+
if (this.taskSub === null) {
|
|
3678
|
+
this.taskSub = await this.synapse.subscribe(
|
|
3679
|
+
this.subject(SignalType.TASK),
|
|
3680
|
+
(s) => this.onTask(s)
|
|
3681
|
+
);
|
|
3682
|
+
}
|
|
3683
|
+
await this.refreshRoutedSub();
|
|
3684
|
+
await this.ensureInboundSub(SignalType.TASK_AWARDED);
|
|
3685
|
+
await this.ensureInboundSub(SignalType.DISCOVER);
|
|
3686
|
+
if (this.autoBid) await this.ensureInboundSub(SignalType.TASK_OFFER);
|
|
3687
|
+
await this.mirrorToStore(axon, "registered");
|
|
3688
|
+
await this.emitRegister(axon);
|
|
3689
|
+
await axon.hooks._fireConnect();
|
|
3690
|
+
axon.hooks._launchSchedule();
|
|
3691
|
+
}
|
|
3692
|
+
/** Detach an Axon. If running: deregister, tear down its hooks, and re-key
|
|
3693
|
+
* (or drop) the TASK subscriptions for the changed cap profile. */
|
|
3694
|
+
async detachAxon(neuronId, opts = {}) {
|
|
3695
|
+
const axon = this._axons.get(neuronId);
|
|
3696
|
+
if (!axon) {
|
|
3697
|
+
throw new Error(`Dendrite has no Axon for neuronId='${neuronId}'`);
|
|
3698
|
+
}
|
|
3699
|
+
if (this.running) {
|
|
3700
|
+
axon.hooks._stopHooks();
|
|
3701
|
+
if (this.registryStore !== null) {
|
|
3702
|
+
try {
|
|
3703
|
+
await this.registryStore.markDeregistered(neuronId);
|
|
3704
|
+
} catch {
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
await this.emitDeregister(axon, opts.reason);
|
|
3708
|
+
}
|
|
3709
|
+
this._axons.delete(neuronId);
|
|
3710
|
+
axon[DETACH]();
|
|
3711
|
+
if (this.running && this._axons.size === 0) {
|
|
3712
|
+
if (this.taskSub !== null) {
|
|
3713
|
+
try {
|
|
3714
|
+
await this.taskSub.unsubscribe();
|
|
3715
|
+
} catch {
|
|
3716
|
+
}
|
|
3717
|
+
this.taskSub = null;
|
|
3718
|
+
}
|
|
3719
|
+
if (this.routedTaskSub !== null) {
|
|
3720
|
+
try {
|
|
3721
|
+
await this.routedTaskSub.unsubscribe();
|
|
3722
|
+
} catch {
|
|
3723
|
+
}
|
|
3724
|
+
this.routedTaskSub = null;
|
|
3725
|
+
}
|
|
3726
|
+
} else if (this.running) {
|
|
3727
|
+
await this.refreshRoutedSub();
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
/**
|
|
3731
|
+
* Mount an Engram on this Dendrite. After attachment (and start), the
|
|
3732
|
+
* Dendrite subscribes to RECALL/IMPRINT, routes Signals addressed to
|
|
3733
|
+
* `engram.engramId` or matching `engram.engramKind` to the instance, and
|
|
3734
|
+
* announces it on the Synapse with an engram REGISTER. The Engram still
|
|
3735
|
+
* owns its backend lifecycle: `connect()` on start(), `close()` on stop().
|
|
3736
|
+
* When the Dendrite is already running, the backend is connected and the
|
|
3737
|
+
* subscriptions/REGISTER are established immediately.
|
|
3738
|
+
*/
|
|
3739
|
+
async attachEngram(engram) {
|
|
3740
|
+
if (this._engrams.has(engram.engramId)) {
|
|
3741
|
+
throw new Error(`Dendrite already hosts an Engram with engramId='${engram.engramId}'`);
|
|
3742
|
+
}
|
|
3743
|
+
this._engrams.set(engram.engramId, engram);
|
|
3744
|
+
const bucket = this.engramKindIndex.get(engram.engramKind) ?? [];
|
|
3745
|
+
bucket.push(engram.engramId);
|
|
3746
|
+
this.engramKindIndex.set(engram.engramKind, bucket);
|
|
3747
|
+
if (this.running) {
|
|
3748
|
+
await engram.connect();
|
|
3749
|
+
await this.ensureInboundSub(SignalType.RECALL);
|
|
3750
|
+
await this.ensureInboundSub(SignalType.IMPRINT);
|
|
3751
|
+
await this.ensureInboundSub(SignalType.REGISTER);
|
|
3752
|
+
await this.emitEngramRegister(engram);
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
/** Remove a hosted Engram. Closes its backend if the Dendrite is running. */
|
|
3756
|
+
async detachEngram(engramId) {
|
|
3757
|
+
const engram = this._engrams.get(engramId);
|
|
3758
|
+
if (!engram) {
|
|
3759
|
+
throw new Error(`Dendrite has no Engram with engramId='${engramId}'`);
|
|
3760
|
+
}
|
|
3761
|
+
if (this.running) {
|
|
3762
|
+
try {
|
|
3763
|
+
await engram.close();
|
|
3764
|
+
} catch {
|
|
3765
|
+
}
|
|
3766
|
+
}
|
|
3767
|
+
const bucket = this.engramKindIndex.get(engram.engramKind) ?? [];
|
|
3768
|
+
const kept = bucket.filter((id) => id !== engramId);
|
|
3769
|
+
if (kept.length) this.engramKindIndex.set(engram.engramKind, kept);
|
|
3770
|
+
else this.engramKindIndex.delete(engram.engramKind);
|
|
3771
|
+
this._engrams.delete(engramId);
|
|
3772
|
+
}
|
|
3773
|
+
get engrams() {
|
|
3774
|
+
return new Map(this._engrams);
|
|
3775
|
+
}
|
|
3776
|
+
/** Engrams learned via REGISTER, keyed by directed.id (or directed.type
|
|
3777
|
+
* when no id), including in-process ones. */
|
|
3778
|
+
get engramRegistrations() {
|
|
3779
|
+
return new Map(this._engramRegistrations);
|
|
3780
|
+
}
|
|
3781
|
+
/** True when an Engram with this id/kind is reachable - hosted
|
|
3782
|
+
* in-process or learned from a peer's REGISTER. */
|
|
3783
|
+
isEngramKnown(opts) {
|
|
3784
|
+
if (opts.engramId) {
|
|
3785
|
+
if (this._engrams.has(opts.engramId) || this._engramRegistrations.has(opts.engramId)) {
|
|
3786
|
+
return true;
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
if (opts.engramKind) {
|
|
3790
|
+
if (this.engramKindIndex.has(opts.engramKind) || this.engramRegKindIndex.has(opts.engramKind)) {
|
|
3791
|
+
return true;
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
return false;
|
|
3795
|
+
}
|
|
3796
|
+
/** (Re)subscribe the capability-routed TASK subscription so its queue
|
|
3797
|
+
* group matches the *current* aggregate cap profile. */
|
|
3798
|
+
async refreshRoutedSub() {
|
|
3799
|
+
const qgroup = this.capQueueGroup();
|
|
3800
|
+
if (this.routedTaskSub !== null) {
|
|
3801
|
+
try {
|
|
3802
|
+
await this.routedTaskSub.unsubscribe();
|
|
3803
|
+
} catch {
|
|
3804
|
+
}
|
|
3805
|
+
this.routedTaskSub = null;
|
|
3806
|
+
}
|
|
3807
|
+
if (qgroup !== null) {
|
|
3808
|
+
this.routedTaskSub = await this.synapse.subscribe(
|
|
3809
|
+
this.routedSubject(),
|
|
3810
|
+
(s) => this.onTask(s),
|
|
3811
|
+
{ queueGroup: qgroup }
|
|
3812
|
+
);
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
2645
3815
|
// -- inbound handler registration ----------------------------------
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
throw new DendriteProtocolError(`Cannot handle non-Axon type '${type}'`);
|
|
3816
|
+
wrapWithFilter(fn, filter) {
|
|
3817
|
+
if (!filter || filter.neuron === void 0 && filter.capability === void 0 && filter.traceId === void 0) {
|
|
3818
|
+
return fn;
|
|
2650
3819
|
}
|
|
2651
|
-
|
|
3820
|
+
return async (sig) => {
|
|
3821
|
+
const sigNeuron = sig.directed?.id ?? null;
|
|
3822
|
+
if (filter.neuron !== void 0 && sigNeuron !== filter.neuron) return;
|
|
3823
|
+
if (filter.traceId !== void 0 && sig.trace_id !== filter.traceId) return;
|
|
3824
|
+
if (filter.capability !== void 0) {
|
|
3825
|
+
if (!await this.neuronHasCapability(sigNeuron, filter.capability)) return;
|
|
3826
|
+
}
|
|
3827
|
+
await fn(sig);
|
|
3828
|
+
};
|
|
3829
|
+
}
|
|
3830
|
+
async neuronHasCapability(neuronId, capability) {
|
|
3831
|
+
if (!neuronId) return false;
|
|
3832
|
+
const axon = this._axons.get(neuronId);
|
|
3833
|
+
if (axon) return axon.capabilities.includes(capability);
|
|
3834
|
+
if (this.registryStore !== null) {
|
|
3835
|
+
try {
|
|
3836
|
+
const recs = await this.registryStore.list({ includeDeregistered: true });
|
|
3837
|
+
const rec = recs.find((r) => r.neuron_id === neuronId);
|
|
3838
|
+
return rec ? rec.capabilities.includes(capability) : false;
|
|
3839
|
+
} catch {
|
|
3840
|
+
return false;
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
return false;
|
|
3844
|
+
}
|
|
3845
|
+
on(type, fn, filter) {
|
|
3846
|
+
const list = this.handlers.get(type);
|
|
3847
|
+
list.push(this.wrapWithFilter(fn, filter));
|
|
2652
3848
|
if (this.running && !this.inboundSubs.has(type)) {
|
|
2653
|
-
|
|
3849
|
+
const p = this.ensureInboundSub(type).finally(() => this.pendingSubs.delete(p));
|
|
3850
|
+
this.pendingSubs.add(p);
|
|
3851
|
+
p.catch(() => {
|
|
3852
|
+
});
|
|
2654
3853
|
}
|
|
2655
3854
|
return fn;
|
|
2656
3855
|
}
|
|
2657
|
-
|
|
2658
|
-
|
|
3856
|
+
/**
|
|
3857
|
+
* Generic handler registration for *any* SignalType - the escape hatch
|
|
3858
|
+
* behind every named `on*` helper. New protocol types are observable the
|
|
3859
|
+
* day they exist. Supports the same filters as the named helpers.
|
|
3860
|
+
*/
|
|
3861
|
+
onSignal(type, fn, filter) {
|
|
3862
|
+
return this.on(type, fn, filter);
|
|
3863
|
+
}
|
|
3864
|
+
/** Await until inbound subscriptions exist for `types` - removes the
|
|
3865
|
+
* late-registration race deterministically. Idempotent. */
|
|
3866
|
+
async ensureSubscribed(...types) {
|
|
3867
|
+
for (const t of types) await this.ensureInboundSub(t);
|
|
2659
3868
|
}
|
|
2660
|
-
|
|
2661
|
-
|
|
3869
|
+
// -- lifecycle / reply handlers --
|
|
3870
|
+
onAgentOutput(fn, filter) {
|
|
3871
|
+
return this.on(SignalType.AGENT_OUTPUT, fn, filter);
|
|
3872
|
+
}
|
|
3873
|
+
onClarification(fn, filter) {
|
|
3874
|
+
return this.on(SignalType.CLARIFICATION, fn, filter);
|
|
2662
3875
|
}
|
|
2663
3876
|
/**
|
|
2664
3877
|
* Register a handler fired on inbound PERMISSION requests - the *answering*
|
|
2665
|
-
* side.
|
|
2666
|
-
*
|
|
2667
|
-
*
|
|
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.
|
|
3878
|
+
* side. Reply via {@link respondToPermission} (re-dispatch a TASK with the
|
|
3879
|
+
* verdict) or {@link grantPermission} / {@link denyPermission} (emit a
|
|
3880
|
+
* discrete PERMISSION_DECISION).
|
|
2671
3881
|
*/
|
|
2672
|
-
onPermission(fn) {
|
|
2673
|
-
return this.on(SignalType.PERMISSION, fn);
|
|
2674
|
-
}
|
|
2675
|
-
onErrorSignal(fn) {
|
|
2676
|
-
return this.on(SignalType.ERROR, fn);
|
|
3882
|
+
onPermission(fn, filter) {
|
|
3883
|
+
return this.on(SignalType.PERMISSION, fn, filter);
|
|
2677
3884
|
}
|
|
2678
|
-
|
|
2679
|
-
return this.on(SignalType.
|
|
3885
|
+
onErrorSignal(fn, filter) {
|
|
3886
|
+
return this.on(SignalType.ERROR, fn, filter);
|
|
2680
3887
|
}
|
|
2681
|
-
|
|
2682
|
-
|
|
3888
|
+
/** Register a handler fired on FINAL - workflow conclusion. */
|
|
3889
|
+
onFinal(fn, filter) {
|
|
3890
|
+
return this.on(SignalType.FINAL, fn, filter);
|
|
2683
3891
|
}
|
|
2684
|
-
|
|
2685
|
-
|
|
3892
|
+
/** Observe inbound TASKs (audit/logging). Observation only - Axon routing
|
|
3893
|
+
* happens on its own subscription and is unaffected. */
|
|
3894
|
+
onTaskSignal(fn, filter) {
|
|
3895
|
+
return this.on(SignalType.TASK, fn, filter);
|
|
3896
|
+
}
|
|
3897
|
+
onRegister(fn, filter) {
|
|
3898
|
+
return this.on(SignalType.REGISTER, fn, filter);
|
|
3899
|
+
}
|
|
3900
|
+
onDeregister(fn, filter) {
|
|
3901
|
+
return this.on(SignalType.DEREGISTER, fn, filter);
|
|
3902
|
+
}
|
|
3903
|
+
onHeartbeat(fn, filter) {
|
|
3904
|
+
return this.on(SignalType.HEARTBEAT, fn, filter);
|
|
3905
|
+
}
|
|
3906
|
+
onDiscover(fn, filter) {
|
|
3907
|
+
return this.on(SignalType.DISCOVER, fn, filter);
|
|
3908
|
+
}
|
|
3909
|
+
// -- cognition handlers --
|
|
3910
|
+
onPlan(fn, filter) {
|
|
3911
|
+
return this.on(SignalType.PLAN, fn, filter);
|
|
3912
|
+
}
|
|
3913
|
+
onThoughtDelta(fn, filter) {
|
|
3914
|
+
return this.on(SignalType.THOUGHT_DELTA, fn, filter);
|
|
3915
|
+
}
|
|
3916
|
+
onToolCall(fn, filter) {
|
|
3917
|
+
return this.on(SignalType.TOOL_CALL, fn, filter);
|
|
3918
|
+
}
|
|
3919
|
+
onToolResult(fn, filter) {
|
|
3920
|
+
return this.on(SignalType.TOOL_RESULT, fn, filter);
|
|
3921
|
+
}
|
|
3922
|
+
onMemoryAppend(fn, filter) {
|
|
3923
|
+
return this.on(SignalType.MEMORY_APPEND, fn, filter);
|
|
3924
|
+
}
|
|
3925
|
+
onCritique(fn, filter) {
|
|
3926
|
+
return this.on(SignalType.CRITIQUE, fn, filter);
|
|
3927
|
+
}
|
|
3928
|
+
onEscalation(fn, filter) {
|
|
3929
|
+
return this.on(SignalType.ESCALATION, fn, filter);
|
|
3930
|
+
}
|
|
3931
|
+
onConsensus(fn, filter) {
|
|
3932
|
+
return this.on(SignalType.CONSENSUS, fn, filter);
|
|
3933
|
+
}
|
|
3934
|
+
onContextSync(fn, filter) {
|
|
3935
|
+
return this.on(SignalType.CONTEXT_SYNC, fn, filter);
|
|
3936
|
+
}
|
|
3937
|
+
// -- routing / market handlers --
|
|
3938
|
+
/** Workers use this to evaluate offers and call {@link bid} to compete.
|
|
3939
|
+
* Registering it suppresses the default auto-bidder entirely. */
|
|
3940
|
+
onTaskOffer(fn, filter) {
|
|
3941
|
+
return this.on(SignalType.TASK_OFFER, fn, filter);
|
|
3942
|
+
}
|
|
3943
|
+
/** Observe BIDs (market observability). dispatchOffer collects its own. */
|
|
3944
|
+
onBid(fn, filter) {
|
|
3945
|
+
return this.on(SignalType.BID, fn, filter);
|
|
3946
|
+
}
|
|
3947
|
+
/** Observe TASK_AWARDED. The hosting Dendrite's award-to-TASK synthesis is
|
|
3948
|
+
* unaffected by handlers here. */
|
|
3949
|
+
onTaskAwarded(fn, filter) {
|
|
3950
|
+
return this.on(SignalType.TASK_AWARDED, fn, filter);
|
|
3951
|
+
}
|
|
3952
|
+
/** e.g. release a reservation made while bidding. */
|
|
3953
|
+
onTaskDeclined(fn, filter) {
|
|
3954
|
+
return this.on(SignalType.TASK_DECLINED, fn, filter);
|
|
3955
|
+
}
|
|
3956
|
+
// -- discrete answer-path consumers --
|
|
3957
|
+
/** Fired on CLARIFICATION_ANSWER - correlate by `sig.parent_id === the
|
|
3958
|
+
* CLARIFICATION's id`, or use {@link awaitDecision}. */
|
|
3959
|
+
onClarificationAnswer(fn, filter) {
|
|
3960
|
+
return this.on(SignalType.CLARIFICATION_ANSWER, fn, filter);
|
|
3961
|
+
}
|
|
3962
|
+
/** Fired on PERMISSION_DECISION - correlate by parent_id, or use
|
|
3963
|
+
* {@link awaitDecision}. */
|
|
3964
|
+
onPermissionDecision(fn, filter) {
|
|
3965
|
+
return this.on(SignalType.PERMISSION_DECISION, fn, filter);
|
|
3966
|
+
}
|
|
3967
|
+
// -- memory-traffic observers --
|
|
3968
|
+
onRecalled(fn, filter) {
|
|
3969
|
+
return this.on(SignalType.RECALLED, fn, filter);
|
|
3970
|
+
}
|
|
3971
|
+
onImprinted(fn, filter) {
|
|
3972
|
+
return this.on(SignalType.IMPRINTED, fn, filter);
|
|
3973
|
+
}
|
|
3974
|
+
onRecallSignal(fn, filter) {
|
|
3975
|
+
return this.on(SignalType.RECALL, fn, filter);
|
|
3976
|
+
}
|
|
3977
|
+
onImprintSignal(fn, filter) {
|
|
3978
|
+
return this.on(SignalType.IMPRINT, fn, filter);
|
|
3979
|
+
}
|
|
3980
|
+
// -- trace-scoped helper --
|
|
3981
|
+
static TRACE_DEFAULT_TYPES = [
|
|
3982
|
+
SignalType.AGENT_OUTPUT,
|
|
3983
|
+
SignalType.FINAL,
|
|
3984
|
+
SignalType.ERROR,
|
|
3985
|
+
SignalType.PLAN,
|
|
3986
|
+
SignalType.THOUGHT_DELTA,
|
|
3987
|
+
SignalType.TOOL_CALL,
|
|
3988
|
+
SignalType.TOOL_RESULT,
|
|
3989
|
+
SignalType.MEMORY_APPEND,
|
|
3990
|
+
SignalType.CRITIQUE,
|
|
3991
|
+
SignalType.ESCALATION,
|
|
3992
|
+
SignalType.CONSENSUS,
|
|
3993
|
+
SignalType.CONTEXT_SYNC,
|
|
3994
|
+
SignalType.CLARIFICATION
|
|
3995
|
+
];
|
|
3996
|
+
/** Register one handler for multiple types narrowed to a single workflow. */
|
|
3997
|
+
onTrace(traceId, fn, types) {
|
|
3998
|
+
for (const t of types ?? _Dendrite.TRACE_DEFAULT_TYPES) {
|
|
3999
|
+
this.on(t, fn, { traceId });
|
|
4000
|
+
}
|
|
4001
|
+
return fn;
|
|
2686
4002
|
}
|
|
2687
4003
|
// -- lifecycle hooks ----------------------------------------------
|
|
2688
|
-
/** Register a fire-once handler called after start() completes. */
|
|
2689
4004
|
onConnect(fn) {
|
|
2690
4005
|
return this.hooks.onConnect(fn);
|
|
2691
4006
|
}
|
|
2692
|
-
/** Register a handler called whenever this Dendrite's state refreshes. */
|
|
2693
4007
|
onRefresh(fn) {
|
|
2694
4008
|
return this.hooks.onRefresh(fn);
|
|
2695
4009
|
}
|
|
2696
|
-
/** Register a periodic handler that runs every `everyMs` until stop(). */
|
|
2697
4010
|
onSchedule(everyMs, fn) {
|
|
2698
4011
|
return this.hooks.onSchedule(everyMs, fn);
|
|
2699
4012
|
}
|
|
2700
|
-
/** Manually fire a refresh event (reason defaults to "manual"). */
|
|
2701
4013
|
async refresh(opts = {}) {
|
|
2702
4014
|
await this.hooks.refresh(opts);
|
|
2703
4015
|
}
|
|
@@ -2710,11 +4022,41 @@ var Dendrite = class {
|
|
|
2710
4022
|
this.subject(SignalType.TASK),
|
|
2711
4023
|
(s) => this.onTask(s)
|
|
2712
4024
|
);
|
|
4025
|
+
const qgroup = this.capQueueGroup();
|
|
4026
|
+
if (qgroup !== null) {
|
|
4027
|
+
this.routedTaskSub = await this.synapse.subscribe(
|
|
4028
|
+
this.routedSubject(),
|
|
4029
|
+
(s) => this.onTask(s),
|
|
4030
|
+
{ queueGroup: qgroup }
|
|
4031
|
+
);
|
|
4032
|
+
}
|
|
4033
|
+
await this.ensureInboundSub(SignalType.TASK_AWARDED);
|
|
4034
|
+
await this.ensureInboundSub(SignalType.DISCOVER);
|
|
4035
|
+
if (this.autoBid) await this.ensureInboundSub(SignalType.TASK_OFFER);
|
|
2713
4036
|
for (const axon of this._axons.values()) {
|
|
2714
4037
|
await this.mirrorToStore(axon, "registered");
|
|
2715
4038
|
await this.emitRegister(axon);
|
|
2716
4039
|
}
|
|
2717
4040
|
}
|
|
4041
|
+
if (this._engrams.size > 0) {
|
|
4042
|
+
for (const engram of this._engrams.values()) {
|
|
4043
|
+
try {
|
|
4044
|
+
await engram.connect();
|
|
4045
|
+
} catch {
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
4048
|
+
await this.ensureInboundSub(SignalType.RECALL);
|
|
4049
|
+
await this.ensureInboundSub(SignalType.IMPRINT);
|
|
4050
|
+
await this.ensureInboundSub(SignalType.REGISTER);
|
|
4051
|
+
for (const engram of this._engrams.values()) {
|
|
4052
|
+
try {
|
|
4053
|
+
await this.emitEngramRegister(engram);
|
|
4054
|
+
} catch {
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
}
|
|
4058
|
+
await this.ensureInboundSub(SignalType.RECALLED);
|
|
4059
|
+
await this.ensureInboundSub(SignalType.IMPRINTED);
|
|
2718
4060
|
for (const [type, hs] of this.handlers) {
|
|
2719
4061
|
if (hs.length) await this.ensureInboundSub(type);
|
|
2720
4062
|
}
|
|
@@ -2734,17 +4076,6 @@ var Dendrite = class {
|
|
|
2734
4076
|
axon.hooks._launchSchedule();
|
|
2735
4077
|
}
|
|
2736
4078
|
}
|
|
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
4079
|
startHeartbeatLoop() {
|
|
2749
4080
|
this.heartbeatStopped = false;
|
|
2750
4081
|
const schedule = () => {
|
|
@@ -2764,8 +4095,29 @@ var Dendrite = class {
|
|
|
2764
4095
|
schedule();
|
|
2765
4096
|
}
|
|
2766
4097
|
async stop(reason) {
|
|
4098
|
+
for (const pw of [...this.pathways.values()]) {
|
|
4099
|
+
try {
|
|
4100
|
+
await pw.close();
|
|
4101
|
+
} catch {
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
this.pathways.clear();
|
|
4105
|
+
for (const pw of [...this.opPathways.values()]) {
|
|
4106
|
+
try {
|
|
4107
|
+
await pw.close();
|
|
4108
|
+
} catch {
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
4111
|
+
this.opPathways.clear();
|
|
4112
|
+
this.engramClient.cancelAll();
|
|
2767
4113
|
if (!this.running) return;
|
|
2768
4114
|
this.running = false;
|
|
4115
|
+
for (const engram of this._engrams.values()) {
|
|
4116
|
+
try {
|
|
4117
|
+
await engram.close();
|
|
4118
|
+
} catch {
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
2769
4121
|
this.hooks._stopHooks();
|
|
2770
4122
|
for (const axon of this._axons.values()) axon.hooks._stopHooks();
|
|
2771
4123
|
this.heartbeatStopped = true;
|
|
@@ -2777,6 +4129,10 @@ var Dendrite = class {
|
|
|
2777
4129
|
await this.taskSub.unsubscribe();
|
|
2778
4130
|
this.taskSub = null;
|
|
2779
4131
|
}
|
|
4132
|
+
if (this.routedTaskSub !== null) {
|
|
4133
|
+
await this.routedTaskSub.unsubscribe();
|
|
4134
|
+
this.routedTaskSub = null;
|
|
4135
|
+
}
|
|
2780
4136
|
for (const sub of this.inboundSubs.values()) {
|
|
2781
4137
|
try {
|
|
2782
4138
|
await sub.unsubscribe();
|
|
@@ -2794,20 +4150,6 @@ var Dendrite = class {
|
|
|
2794
4150
|
await this.emitDeregister(axon, reason);
|
|
2795
4151
|
}
|
|
2796
4152
|
}
|
|
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
4153
|
async [Symbol.asyncDispose]() {
|
|
2812
4154
|
await this.stop();
|
|
2813
4155
|
}
|
|
@@ -2822,29 +4164,387 @@ var Dendrite = class {
|
|
|
2822
4164
|
}
|
|
2823
4165
|
/** All known records, optionally filtered (live records only by default). */
|
|
2824
4166
|
async registrySnapshot(opts = {}) {
|
|
2825
|
-
|
|
4167
|
+
const { maxAgeMs, ...listOpts } = opts;
|
|
4168
|
+
const records = await this.requireStore().list(listOpts);
|
|
4169
|
+
return maxAgeMs !== void 0 ? _Dendrite.filterFresh(records, maxAgeMs) : records;
|
|
2826
4170
|
}
|
|
2827
|
-
/** Live (non-deregistered) records, optionally filtered by capability.
|
|
4171
|
+
/** Live (non-deregistered) records, optionally filtered by capability.
|
|
4172
|
+
* `maxAgeMs` additionally drops records whose last heartbeat is older -
|
|
4173
|
+
* a read-side freshness guard when the background sweep can't be relied on. */
|
|
2828
4174
|
async findNeurons(opts = {}) {
|
|
2829
|
-
|
|
4175
|
+
const records = await this.requireStore().list({
|
|
2830
4176
|
...opts.capability !== void 0 ? { capability: opts.capability } : {},
|
|
2831
4177
|
includeDeregistered: false
|
|
2832
4178
|
});
|
|
4179
|
+
return opts.maxAgeMs !== void 0 ? _Dendrite.filterFresh(records, opts.maxAgeMs) : records;
|
|
4180
|
+
}
|
|
4181
|
+
static filterFresh(records, maxAgeMs) {
|
|
4182
|
+
const now = Date.now();
|
|
4183
|
+
return records.filter((r) => {
|
|
4184
|
+
const seen = r.last_heartbeat ?? r.registered_at;
|
|
4185
|
+
if (!seen) return false;
|
|
4186
|
+
return now - Date.parse(seen) <= maxAgeMs;
|
|
4187
|
+
});
|
|
2833
4188
|
}
|
|
2834
4189
|
// -- outbound primitives ------------------------------------------
|
|
4190
|
+
/**
|
|
4191
|
+
* Emit a TASK. Addressed (`neuron`) or capability-routed (`capabilities`)
|
|
4192
|
+
* - at least one must be set. `finalize: true` tags the TASK so the
|
|
4193
|
+
* handling worker Dendrite promotes a successful AGENT_OUTPUT to FINAL
|
|
4194
|
+
* (terminal-handler finalize - see {@link dispatch}). Only
|
|
4195
|
+
* orchestrator-role Dendrites may dispatch.
|
|
4196
|
+
*/
|
|
2835
4197
|
async dispatchTask(args) {
|
|
4198
|
+
this.requireOrchestrator("dispatchTask");
|
|
4199
|
+
if (!args.neuron && !args.capabilities?.length) {
|
|
4200
|
+
throw new Error(
|
|
4201
|
+
"dispatchTask requires either neuron (addressed) or capabilities (capability-routed)"
|
|
4202
|
+
);
|
|
4203
|
+
}
|
|
2836
4204
|
const sig = taskSignal({
|
|
2837
|
-
directed: { id: args.neuron },
|
|
2838
4205
|
input: args.input,
|
|
4206
|
+
...args.neuron ? { directed: { id: args.neuron } } : {},
|
|
2839
4207
|
...args.traceId !== void 0 ? { traceId: args.traceId } : {},
|
|
2840
4208
|
...args.parentId !== void 0 ? { parentId: args.parentId } : {},
|
|
2841
4209
|
...args.contextRef !== void 0 ? { contextRef: args.contextRef } : {},
|
|
2842
4210
|
...args.capabilities !== void 0 ? { capabilities: args.capabilities } : {},
|
|
4211
|
+
...args.finalize !== void 0 ? { finalize: args.finalize } : {},
|
|
2843
4212
|
...args.meta !== void 0 ? { meta: args.meta } : {}
|
|
2844
4213
|
});
|
|
2845
|
-
await this.
|
|
4214
|
+
await this.publishTask(sig);
|
|
4215
|
+
return sig;
|
|
4216
|
+
}
|
|
4217
|
+
/** Publish a TASK to the right subject for its routing mode. Addressed ->
|
|
4218
|
+
* broadcast subject; capability-routed -> queue-grouped routed subject. */
|
|
4219
|
+
async publishTask(sig) {
|
|
4220
|
+
const subject = sig.directed?.id ? this.subject(SignalType.TASK) : sig.payload["capabilities"]?.length ? this.routedSubject() : this.subject(SignalType.TASK);
|
|
4221
|
+
await this.synapse.publish(subject, sig);
|
|
4222
|
+
}
|
|
4223
|
+
// -- Pathway-based dispatch ----------------------------------------
|
|
4224
|
+
/**
|
|
4225
|
+
* Dispatch a TASK and return a {@link Pathway} scoped to its trace -
|
|
4226
|
+
* await it, attach callbacks, or iterate:
|
|
4227
|
+
*
|
|
4228
|
+
* ```ts
|
|
4229
|
+
* // 1) sequential / request-reply
|
|
4230
|
+
* const pw = await orch.dispatch({ neuron: "summarize", input });
|
|
4231
|
+
* const out = await pw.wait();
|
|
4232
|
+
*
|
|
4233
|
+
* // 2) reactive
|
|
4234
|
+
* pw.on(SignalType.PLAN, (sig) => { ... });
|
|
4235
|
+
*
|
|
4236
|
+
* // 3) streaming
|
|
4237
|
+
* for await (const sig of pw) { ... }
|
|
4238
|
+
* ```
|
|
4239
|
+
*
|
|
4240
|
+
* `capabilities` instead of `neuron` gives event-driven dispatch. Delivery
|
|
4241
|
+
* is exactly-once within a queue group (identical cap profiles) but
|
|
4242
|
+
* **at-least-once across heterogeneous groups** - use
|
|
4243
|
+
* {@link dispatchOffer} when overlapping profiles need an atomic claim.
|
|
4244
|
+
*
|
|
4245
|
+
* `scope: "terminal"` filters delivery to FINAL / ERROR / CLARIFICATION /
|
|
4246
|
+
* PERMISSION. `finalize` (default: true exactly when scope is "terminal")
|
|
4247
|
+
* tags the TASK for terminal-handler finalize: the worker Dendrite promotes
|
|
4248
|
+
* a successful AGENT_OUTPUT by also emitting FINAL - a default Axon never
|
|
4249
|
+
* emits FINAL itself, so a terminal-scoped Pathway would otherwise never
|
|
4250
|
+
* resolve against stock workers.
|
|
4251
|
+
*/
|
|
4252
|
+
async dispatch(args) {
|
|
4253
|
+
this.requireOrchestrator("dispatch");
|
|
4254
|
+
if (!args.neuron && !args.capabilities?.length) {
|
|
4255
|
+
throw new Error(
|
|
4256
|
+
"dispatch requires either neuron (addressed) or capabilities (capability-routed)"
|
|
4257
|
+
);
|
|
4258
|
+
}
|
|
4259
|
+
const scope = args.scope ?? "all";
|
|
4260
|
+
const finalize = args.finalize ?? scope === "terminal";
|
|
4261
|
+
const tid = args.traceId ?? newTraceId();
|
|
4262
|
+
await this.ensurePathwaySubs();
|
|
4263
|
+
const pathway = new Pathway({
|
|
4264
|
+
traceId: tid,
|
|
4265
|
+
role: "originator",
|
|
4266
|
+
scope,
|
|
4267
|
+
onClose: (pw) => {
|
|
4268
|
+
this.pathways.delete(pw.traceId);
|
|
4269
|
+
}
|
|
4270
|
+
});
|
|
4271
|
+
this.pathways.set(tid, pathway);
|
|
4272
|
+
const sig = taskSignal({
|
|
4273
|
+
input: args.input,
|
|
4274
|
+
traceId: tid,
|
|
4275
|
+
...args.neuron ? { directed: { id: args.neuron } } : {},
|
|
4276
|
+
...args.parentId !== void 0 ? { parentId: args.parentId } : {},
|
|
4277
|
+
...args.contextRef !== void 0 ? { contextRef: args.contextRef } : {},
|
|
4278
|
+
...args.capabilities !== void 0 ? { capabilities: args.capabilities } : {},
|
|
4279
|
+
finalize,
|
|
4280
|
+
...args.meta !== void 0 ? { meta: args.meta } : {}
|
|
4281
|
+
});
|
|
4282
|
+
try {
|
|
4283
|
+
await this.publishTask(sig);
|
|
4284
|
+
} catch (err) {
|
|
4285
|
+
this.pathways.delete(tid);
|
|
4286
|
+
await pathway.close();
|
|
4287
|
+
throw err;
|
|
4288
|
+
}
|
|
4289
|
+
return pathway;
|
|
4290
|
+
}
|
|
4291
|
+
/** Sync-shape sugar: dispatch, await the first matching Signal, close the
|
|
4292
|
+
* Pathway, return the Signal. Use `scope: "terminal"` to wait only for
|
|
4293
|
+
* FINAL / ERROR / CLARIFICATION / PERMISSION. */
|
|
4294
|
+
async dispatchAndWait(args) {
|
|
4295
|
+
const { timeoutMs, ...rest } = args;
|
|
4296
|
+
const pathway = await this.dispatch(rest);
|
|
4297
|
+
try {
|
|
4298
|
+
return await pathway.wait(timeoutMs ?? 3e4);
|
|
4299
|
+
} finally {
|
|
4300
|
+
await pathway.close();
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
/** Async-shape sugar: dispatch, return the live Pathway immediately. The
|
|
4304
|
+
* caller attaches `pw.on(...)` callbacks or iterates. */
|
|
4305
|
+
async dispatchAndSubscribe(args) {
|
|
4306
|
+
return this.dispatch(args);
|
|
4307
|
+
}
|
|
4308
|
+
/** Open a Pathway in *observer* role for a trace another peer started. */
|
|
4309
|
+
async observePathway(traceId) {
|
|
4310
|
+
if (this.pathways.has(traceId)) {
|
|
4311
|
+
throw new Error(`Dendrite already has a Pathway open for trace '${traceId}'`);
|
|
4312
|
+
}
|
|
4313
|
+
await this.ensurePathwaySubs();
|
|
4314
|
+
const pathway = new Pathway({
|
|
4315
|
+
traceId,
|
|
4316
|
+
role: "observer",
|
|
4317
|
+
onClose: (pw) => {
|
|
4318
|
+
this.pathways.delete(pw.traceId);
|
|
4319
|
+
}
|
|
4320
|
+
});
|
|
4321
|
+
this.pathways.set(traceId, pathway);
|
|
4322
|
+
return pathway;
|
|
4323
|
+
}
|
|
4324
|
+
async ensurePathwaySubs() {
|
|
4325
|
+
for (const t of PATHWAY_TYPES) await this.ensureInboundSub(t);
|
|
4326
|
+
}
|
|
4327
|
+
// -- per-operation (request/reply) Pathways -------------------------
|
|
4328
|
+
openOpPathway(opId, traceId) {
|
|
4329
|
+
const pathway = new Pathway({
|
|
4330
|
+
traceId,
|
|
4331
|
+
parentId: opId,
|
|
4332
|
+
role: "originator",
|
|
4333
|
+
onClose: (pw) => {
|
|
4334
|
+
if (pw.parentId !== null) this.opPathways.delete(pw.parentId);
|
|
4335
|
+
}
|
|
4336
|
+
});
|
|
4337
|
+
this.opPathways.set(opId, pathway);
|
|
4338
|
+
return pathway;
|
|
4339
|
+
}
|
|
4340
|
+
async cancelOpPathways(traceId) {
|
|
4341
|
+
for (const pw of [...this.opPathways.values()].filter((p) => p.traceId === traceId)) {
|
|
4342
|
+
try {
|
|
4343
|
+
await pw.close();
|
|
4344
|
+
} catch {
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
}
|
|
4348
|
+
/**
|
|
4349
|
+
* Await the discrete answer to a CLARIFICATION or PERMISSION request.
|
|
4350
|
+
*
|
|
4351
|
+
* Opens a per-operation Pathway keyed on `request.id` and resolves on the
|
|
4352
|
+
* first CLARIFICATION_ANSWER / PERMISSION_DECISION whose parent_id matches.
|
|
4353
|
+
* The awaitable counterpart to {@link onClarificationAnswer} /
|
|
4354
|
+
* {@link onPermissionDecision}.
|
|
4355
|
+
*/
|
|
4356
|
+
async awaitDecision(request, opts = {}) {
|
|
4357
|
+
let expected;
|
|
4358
|
+
if (request.type === SignalType.CLARIFICATION) {
|
|
4359
|
+
expected = SignalType.CLARIFICATION_ANSWER;
|
|
4360
|
+
} else if (request.type === SignalType.PERMISSION) {
|
|
4361
|
+
expected = SignalType.PERMISSION_DECISION;
|
|
4362
|
+
} else {
|
|
4363
|
+
throw new DendriteProtocolError(
|
|
4364
|
+
`awaitDecision expects a CLARIFICATION or PERMISSION signal, got '${request.type}'`
|
|
4365
|
+
);
|
|
4366
|
+
}
|
|
4367
|
+
await this.ensureInboundSub(expected);
|
|
4368
|
+
const cached = this.recentDecisions.get(request.id);
|
|
4369
|
+
if (cached && cached.type === expected) {
|
|
4370
|
+
this.recentDecisions.delete(request.id);
|
|
4371
|
+
return cached;
|
|
4372
|
+
}
|
|
4373
|
+
const pathway = this.openOpPathway(request.id, request.trace_id);
|
|
4374
|
+
try {
|
|
4375
|
+
return await pathway.waitFor(expected, opts.timeoutMs ?? 3e4);
|
|
4376
|
+
} finally {
|
|
4377
|
+
await pathway.close();
|
|
4378
|
+
}
|
|
4379
|
+
}
|
|
4380
|
+
// -- competitive bidding: TASK_OFFER / BID / TASK_AWARDED ------------
|
|
4381
|
+
/**
|
|
4382
|
+
* Broadcast a TASK_OFFER, collect BIDs for `deadlineMs`, award the winner
|
|
4383
|
+
* per `select` ("first_bid" | "lowest_cost" | "highest_confidence"), and
|
|
4384
|
+
* return a Pathway scoped to the resulting workflow. Losers get
|
|
4385
|
+
* TASK_DECLINED. Throws a TimeoutError-named Error when no BID arrives.
|
|
4386
|
+
* `finalize` follows the same rule as {@link dispatch}.
|
|
4387
|
+
*/
|
|
4388
|
+
async dispatchOffer(args) {
|
|
4389
|
+
this.requireOrchestrator("dispatchOffer");
|
|
4390
|
+
const select = args.select ?? "first_bid";
|
|
4391
|
+
if (!["first_bid", "lowest_cost", "highest_confidence"].includes(select)) {
|
|
4392
|
+
throw new Error(
|
|
4393
|
+
`select must be 'first_bid' / 'lowest_cost' / 'highest_confidence', got '${select}'`
|
|
4394
|
+
);
|
|
4395
|
+
}
|
|
4396
|
+
const deadlineMs = args.deadlineMs ?? 250;
|
|
4397
|
+
const scope = args.scope ?? "all";
|
|
4398
|
+
const finalize = args.finalize ?? scope === "terminal";
|
|
4399
|
+
const tid = args.traceId ?? newTraceId();
|
|
4400
|
+
await this.ensurePathwaySubs();
|
|
4401
|
+
await this.ensureInboundSub(SignalType.BID);
|
|
4402
|
+
const pathway = new Pathway({
|
|
4403
|
+
traceId: tid,
|
|
4404
|
+
role: "originator",
|
|
4405
|
+
scope,
|
|
4406
|
+
onClose: (pw) => {
|
|
4407
|
+
this.pathways.delete(pw.traceId);
|
|
4408
|
+
}
|
|
4409
|
+
});
|
|
4410
|
+
this.pathways.set(tid, pathway);
|
|
4411
|
+
const offer = taskOfferSignal({
|
|
4412
|
+
traceId: tid,
|
|
4413
|
+
input: args.input,
|
|
4414
|
+
deadlineMs,
|
|
4415
|
+
...args.parentId !== void 0 ? { parentId: args.parentId } : {},
|
|
4416
|
+
...args.capabilities !== void 0 ? { capabilities: args.capabilities } : {},
|
|
4417
|
+
...args.meta !== void 0 ? { meta: args.meta } : {}
|
|
4418
|
+
});
|
|
4419
|
+
const bids = [];
|
|
4420
|
+
let firstBid = null;
|
|
4421
|
+
const firstBidArrived = new Promise((resolve) => {
|
|
4422
|
+
firstBid = resolve;
|
|
4423
|
+
});
|
|
4424
|
+
pathway.on(SignalType.BID, (sig) => {
|
|
4425
|
+
bids.push(sig);
|
|
4426
|
+
if (select === "first_bid") firstBid?.(sig);
|
|
4427
|
+
});
|
|
4428
|
+
try {
|
|
4429
|
+
await this.emit(offer);
|
|
4430
|
+
} catch (err) {
|
|
4431
|
+
this.pathways.delete(tid);
|
|
4432
|
+
await pathway.close();
|
|
4433
|
+
throw err;
|
|
4434
|
+
}
|
|
4435
|
+
const sleep = (ms) => new Promise((r) => setTimeout(() => r(null), ms));
|
|
4436
|
+
if (select === "first_bid") {
|
|
4437
|
+
const winnerOrNull = await Promise.race([firstBidArrived, sleep(deadlineMs)]);
|
|
4438
|
+
if (winnerOrNull === null && bids.length === 0) {
|
|
4439
|
+
await pathway.close();
|
|
4440
|
+
const err = new Error(`dispatchOffer: no BID arrived within ${deadlineMs}ms`);
|
|
4441
|
+
err.name = "TimeoutError";
|
|
4442
|
+
throw err;
|
|
4443
|
+
}
|
|
4444
|
+
} else {
|
|
4445
|
+
await sleep(deadlineMs);
|
|
4446
|
+
if (bids.length === 0) {
|
|
4447
|
+
await pathway.close();
|
|
4448
|
+
const err = new Error(`dispatchOffer: no BID arrived within ${deadlineMs}ms`);
|
|
4449
|
+
err.name = "TimeoutError";
|
|
4450
|
+
throw err;
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
let winner;
|
|
4454
|
+
if (select === "first_bid") {
|
|
4455
|
+
winner = bids[0];
|
|
4456
|
+
} else if (select === "lowest_cost") {
|
|
4457
|
+
winner = bids.reduce(
|
|
4458
|
+
(a, b) => (b.payload["cost"] ?? Infinity) < (a.payload["cost"] ?? Infinity) ? b : a
|
|
4459
|
+
);
|
|
4460
|
+
} else {
|
|
4461
|
+
winner = bids.reduce(
|
|
4462
|
+
(a, b) => (b.payload["confidence"] ?? -Infinity) > (a.payload["confidence"] ?? -Infinity) ? b : a
|
|
4463
|
+
);
|
|
4464
|
+
}
|
|
4465
|
+
for (const b of bids) {
|
|
4466
|
+
if (b.id === winner.id) continue;
|
|
4467
|
+
const bNeuron = b.directed?.id ?? null;
|
|
4468
|
+
try {
|
|
4469
|
+
await this.emit(
|
|
4470
|
+
taskDeclinedSignal({
|
|
4471
|
+
traceId: tid,
|
|
4472
|
+
parentId: b.id,
|
|
4473
|
+
reason: "not selected",
|
|
4474
|
+
...bNeuron ? { directed: { id: bNeuron } } : {}
|
|
4475
|
+
})
|
|
4476
|
+
);
|
|
4477
|
+
} catch {
|
|
4478
|
+
}
|
|
4479
|
+
}
|
|
4480
|
+
const winnerNeuron = winner.directed?.id ?? null;
|
|
4481
|
+
const winningBid = {};
|
|
4482
|
+
for (const k of ["cost", "eta_ms", "confidence"]) {
|
|
4483
|
+
if (k in winner.payload) winningBid[k] = winner.payload[k];
|
|
4484
|
+
}
|
|
4485
|
+
const awarded = taskAwardedSignal({
|
|
4486
|
+
traceId: tid,
|
|
4487
|
+
parentId: winner.id,
|
|
4488
|
+
input: args.input,
|
|
4489
|
+
winningBid,
|
|
4490
|
+
finalize,
|
|
4491
|
+
...winnerNeuron ? { directed: { id: winnerNeuron } } : {},
|
|
4492
|
+
...args.contextRef !== void 0 ? { contextRef: args.contextRef } : {}
|
|
4493
|
+
});
|
|
4494
|
+
try {
|
|
4495
|
+
await this.emit(awarded);
|
|
4496
|
+
} catch (err) {
|
|
4497
|
+
await pathway.close();
|
|
4498
|
+
throw err;
|
|
4499
|
+
}
|
|
4500
|
+
return pathway;
|
|
4501
|
+
}
|
|
4502
|
+
/**
|
|
4503
|
+
* Emit a BID in response to a TASK_OFFER, on behalf of the local Axon named
|
|
4504
|
+
* by `neuron`. Bypasses the role guard - a worker bidding announces
|
|
4505
|
+
* capability, not orchestration.
|
|
4506
|
+
*/
|
|
4507
|
+
async bid(offer, args) {
|
|
4508
|
+
if (offer.type !== SignalType.TASK_OFFER) {
|
|
4509
|
+
throw new DendriteProtocolError(
|
|
4510
|
+
`bid() expects a TASK_OFFER signal, got '${offer.type}'`
|
|
4511
|
+
);
|
|
4512
|
+
}
|
|
4513
|
+
const sig = bidSignal({
|
|
4514
|
+
traceId: offer.trace_id,
|
|
4515
|
+
parentId: offer.id,
|
|
4516
|
+
directed: { id: args.neuron },
|
|
4517
|
+
cost: args.cost,
|
|
4518
|
+
...args.etaMs !== void 0 ? { etaMs: args.etaMs } : {},
|
|
4519
|
+
...args.confidence !== void 0 ? { confidence: args.confidence } : {},
|
|
4520
|
+
...args.meta !== void 0 ? { meta: args.meta } : {}
|
|
4521
|
+
});
|
|
4522
|
+
await this.publish(sig);
|
|
2846
4523
|
return sig;
|
|
2847
4524
|
}
|
|
4525
|
+
/** Default bidder: first hosted Axon whose caps cover the offer answers
|
|
4526
|
+
* (cost 0, confidence 1). No-op when nothing matches. */
|
|
4527
|
+
async maybeAutoBid(offer) {
|
|
4528
|
+
const requested = new Set(
|
|
4529
|
+
offer.payload["capabilities"] ?? []
|
|
4530
|
+
);
|
|
4531
|
+
for (const axon of this._axons.values()) {
|
|
4532
|
+
if (requested.size && ![...requested].every((c) => axon.capabilities.includes(c))) {
|
|
4533
|
+
continue;
|
|
4534
|
+
}
|
|
4535
|
+
try {
|
|
4536
|
+
await this.bid(offer, {
|
|
4537
|
+
neuron: axon.neuronId,
|
|
4538
|
+
cost: 0,
|
|
4539
|
+
confidence: 1,
|
|
4540
|
+
meta: { auto_bid: true }
|
|
4541
|
+
});
|
|
4542
|
+
} catch {
|
|
4543
|
+
}
|
|
4544
|
+
return;
|
|
4545
|
+
}
|
|
4546
|
+
}
|
|
4547
|
+
// -- reply / cognition emit helpers ---------------------------------
|
|
2848
4548
|
async emitFinal(args) {
|
|
2849
4549
|
const sig = finalSignal({
|
|
2850
4550
|
traceId: args.traceId,
|
|
@@ -2869,767 +4569,755 @@ var Dendrite = class {
|
|
|
2869
4569
|
await this.emit(sig);
|
|
2870
4570
|
return sig;
|
|
2871
4571
|
}
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
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 } : {}
|
|
4572
|
+
async emitPlan(args) {
|
|
4573
|
+
const sig = planSignal({
|
|
4574
|
+
traceId: args.traceId,
|
|
4575
|
+
parentId: args.parentId,
|
|
4576
|
+
steps: args.steps,
|
|
4577
|
+
directed: { id: args.neuron ?? this.dendriteId },
|
|
4578
|
+
...args.rationale !== void 0 ? { rationale: args.rationale } : {},
|
|
4579
|
+
...args.meta !== void 0 ? { meta: args.meta } : {}
|
|
2908
4580
|
});
|
|
4581
|
+
await this.emit(sig);
|
|
4582
|
+
return sig;
|
|
2909
4583
|
}
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
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 } : {}
|
|
4584
|
+
async emitThoughtDelta(args) {
|
|
4585
|
+
const sig = thoughtDeltaSignal({
|
|
4586
|
+
traceId: args.traceId,
|
|
4587
|
+
parentId: args.parentId,
|
|
4588
|
+
delta: args.delta,
|
|
4589
|
+
directed: { id: args.neuron ?? this.dendriteId },
|
|
4590
|
+
...args.seq !== void 0 ? { seq: args.seq } : {},
|
|
4591
|
+
...args.meta !== void 0 ? { meta: args.meta } : {}
|
|
2940
4592
|
});
|
|
2941
|
-
await this.
|
|
4593
|
+
await this.emit(sig);
|
|
2942
4594
|
return sig;
|
|
2943
4595
|
}
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
traceId: request.trace_id,
|
|
2954
|
-
parentId: request.id,
|
|
2955
|
-
answer,
|
|
2956
|
-
directed: { id: this.dendriteId },
|
|
2957
|
-
...opts.meta !== void 0 ? { meta: opts.meta } : {}
|
|
4596
|
+
async emitToolCall(args) {
|
|
4597
|
+
const sig = toolCallSignal({
|
|
4598
|
+
traceId: args.traceId,
|
|
4599
|
+
parentId: args.parentId,
|
|
4600
|
+
tool: args.tool,
|
|
4601
|
+
args: args.args_,
|
|
4602
|
+
directed: { id: args.neuron ?? this.dendriteId },
|
|
4603
|
+
...args.callId !== void 0 ? { callId: args.callId } : {},
|
|
4604
|
+
...args.meta !== void 0 ? { meta: args.meta } : {}
|
|
2958
4605
|
});
|
|
2959
|
-
await this.
|
|
4606
|
+
await this.emit(sig);
|
|
2960
4607
|
return sig;
|
|
2961
4608
|
}
|
|
2962
|
-
async
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
return
|
|
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;
|
|
4609
|
+
async emitToolResult(args) {
|
|
4610
|
+
const sig = toolResultSignal({
|
|
4611
|
+
traceId: args.traceId,
|
|
4612
|
+
parentId: args.parentId,
|
|
4613
|
+
tool: args.tool,
|
|
4614
|
+
directed: { id: args.neuron ?? this.dendriteId },
|
|
4615
|
+
...args.result !== void 0 ? { result: args.result } : {},
|
|
4616
|
+
...args.error !== void 0 ? { error: args.error } : {},
|
|
4617
|
+
...args.callId !== void 0 ? { callId: args.callId } : {},
|
|
4618
|
+
...args.meta !== void 0 ? { meta: args.meta } : {}
|
|
4619
|
+
});
|
|
4620
|
+
await this.emit(sig);
|
|
4621
|
+
return sig;
|
|
3148
4622
|
}
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
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";
|
|
4623
|
+
async emitMemoryAppend(args) {
|
|
4624
|
+
const sig = memoryAppendSignal({
|
|
4625
|
+
traceId: args.traceId,
|
|
4626
|
+
parentId: args.parentId,
|
|
4627
|
+
key: args.key,
|
|
4628
|
+
value: args.value,
|
|
4629
|
+
directed: { id: args.neuron ?? this.dendriteId },
|
|
4630
|
+
...args.meta !== void 0 ? { meta: args.meta } : {}
|
|
4631
|
+
});
|
|
4632
|
+
await this.emit(sig);
|
|
4633
|
+
return sig;
|
|
3194
4634
|
}
|
|
3195
|
-
async
|
|
3196
|
-
|
|
4635
|
+
async emitCritique(args) {
|
|
4636
|
+
const sig = critiqueSignal({
|
|
4637
|
+
traceId: args.traceId,
|
|
4638
|
+
parentId: args.parentId,
|
|
4639
|
+
targetEventId: args.targetEventId,
|
|
4640
|
+
issues: args.issues,
|
|
4641
|
+
verdict: args.verdict,
|
|
4642
|
+
directed: { id: args.neuron ?? this.dendriteId },
|
|
4643
|
+
...args.meta !== void 0 ? { meta: args.meta } : {}
|
|
4644
|
+
});
|
|
4645
|
+
await this.emit(sig);
|
|
4646
|
+
return sig;
|
|
3197
4647
|
}
|
|
3198
|
-
async
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
4648
|
+
async emitEscalation(args) {
|
|
4649
|
+
const sig = escalationSignal({
|
|
4650
|
+
traceId: args.traceId,
|
|
4651
|
+
parentId: args.parentId,
|
|
4652
|
+
reason: args.reason,
|
|
4653
|
+
directed: { id: args.neuron ?? this.dendriteId },
|
|
4654
|
+
...args.target !== void 0 ? { target: args.target } : {},
|
|
4655
|
+
...args.context !== void 0 ? { context: args.context } : {},
|
|
4656
|
+
...args.meta !== void 0 ? { meta: args.meta } : {}
|
|
4657
|
+
});
|
|
4658
|
+
await this.emit(sig);
|
|
4659
|
+
return sig;
|
|
3202
4660
|
}
|
|
3203
|
-
async
|
|
3204
|
-
const
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
4661
|
+
async emitConsensus(args) {
|
|
4662
|
+
const sig = consensusSignal({
|
|
4663
|
+
traceId: args.traceId,
|
|
4664
|
+
parentId: args.parentId,
|
|
4665
|
+
members: args.members,
|
|
4666
|
+
verdict: args.verdict,
|
|
4667
|
+
directed: { id: args.neuron ?? this.dendriteId },
|
|
4668
|
+
...args.votes !== void 0 ? { votes: args.votes } : {},
|
|
4669
|
+
...args.meta !== void 0 ? { meta: args.meta } : {}
|
|
4670
|
+
});
|
|
4671
|
+
await this.emit(sig);
|
|
4672
|
+
return sig;
|
|
4673
|
+
}
|
|
4674
|
+
async emitContextSync(args) {
|
|
4675
|
+
const sig = contextSyncSignal({
|
|
4676
|
+
traceId: args.traceId,
|
|
4677
|
+
parentId: args.parentId,
|
|
4678
|
+
snapshot: args.snapshot,
|
|
4679
|
+
directed: { id: args.neuron ?? this.dendriteId },
|
|
4680
|
+
...args.version !== void 0 ? { version: args.version } : {},
|
|
4681
|
+
...args.meta !== void 0 ? { meta: args.meta } : {}
|
|
4682
|
+
});
|
|
4683
|
+
await this.emit(sig);
|
|
4684
|
+
return sig;
|
|
4685
|
+
}
|
|
4686
|
+
// -- close-the-loop helpers ------------------------------------------
|
|
4687
|
+
/**
|
|
4688
|
+
* Reply to a CLARIFICATION by re-dispatching a TASK with the answer. The
|
|
4689
|
+
* new TASK is addressed by default to the asking Neuron, with parentId =
|
|
4690
|
+
* the clarification's id and the original traceId carried over. Input
|
|
4691
|
+
* shape: `{ clarification: { question, answer, ...extra } }`.
|
|
4692
|
+
*/
|
|
4693
|
+
async respondToClarification(request, opts) {
|
|
4694
|
+
if (request.type !== SignalType.CLARIFICATION) {
|
|
4695
|
+
throw new DendriteProtocolError(
|
|
4696
|
+
`respondToClarification expects a CLARIFICATION signal, got '${request.type}'`
|
|
4697
|
+
);
|
|
3219
4698
|
}
|
|
3220
|
-
const
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
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 });
|
|
4699
|
+
const target = opts.neuron ?? request.directed?.id ?? null;
|
|
4700
|
+
if (!target) {
|
|
4701
|
+
throw new DendriteProtocolError(
|
|
4702
|
+
"respondToClarification: signal has no neuron and no neuron override - nowhere to dispatch the follow-up TASK"
|
|
4703
|
+
);
|
|
3235
4704
|
}
|
|
3236
|
-
|
|
3237
|
-
|
|
4705
|
+
const clarification = {
|
|
4706
|
+
question: request.payload["question"] ?? null,
|
|
4707
|
+
answer: opts.answer
|
|
4708
|
+
};
|
|
4709
|
+
if (opts.extra !== void 0) Object.assign(clarification, opts.extra);
|
|
4710
|
+
return this.dispatchTask({
|
|
4711
|
+
neuron: target,
|
|
4712
|
+
input: { clarification },
|
|
4713
|
+
traceId: request.trace_id,
|
|
4714
|
+
parentId: request.id,
|
|
4715
|
+
...opts.meta !== void 0 ? { meta: opts.meta } : {}
|
|
4716
|
+
});
|
|
3238
4717
|
}
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
version: existing ? existing.version : null,
|
|
3250
|
-
tookMs: tookMs()
|
|
3251
|
-
});
|
|
3252
|
-
}
|
|
4718
|
+
/**
|
|
4719
|
+
* Reply to an ESCALATION by dispatching a TASK to the escalation target
|
|
4720
|
+
* (default: `payload.target`). Default input:
|
|
4721
|
+
* `{ escalation: { reason, context, from } }`.
|
|
4722
|
+
*/
|
|
4723
|
+
async respondToEscalation(request, opts = {}) {
|
|
4724
|
+
if (request.type !== SignalType.ESCALATION) {
|
|
4725
|
+
throw new DendriteProtocolError(
|
|
4726
|
+
`respondToEscalation expects an ESCALATION signal, got '${request.type}'`
|
|
4727
|
+
);
|
|
3253
4728
|
}
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
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() });
|
|
4729
|
+
const target = opts.neuron ?? request.payload["target"] ?? null;
|
|
4730
|
+
if (!target) {
|
|
4731
|
+
throw new DendriteProtocolError(
|
|
4732
|
+
"respondToEscalation: signal has no payload.target and no neuron override - nowhere to dispatch the follow-up TASK"
|
|
4733
|
+
);
|
|
4734
|
+
}
|
|
4735
|
+
const input = opts.input ?? {
|
|
4736
|
+
escalation: {
|
|
4737
|
+
reason: request.payload["reason"] ?? null,
|
|
4738
|
+
context: request.payload["context"] ?? null,
|
|
4739
|
+
from: request.directed?.id ?? null
|
|
3321
4740
|
}
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
4741
|
+
};
|
|
4742
|
+
return this.dispatchTask({
|
|
4743
|
+
neuron: target,
|
|
4744
|
+
input,
|
|
4745
|
+
traceId: request.trace_id,
|
|
4746
|
+
parentId: request.id,
|
|
4747
|
+
...opts.meta !== void 0 ? { meta: opts.meta } : {}
|
|
4748
|
+
});
|
|
4749
|
+
}
|
|
4750
|
+
/**
|
|
4751
|
+
* Reply to a PERMISSION by re-dispatching a TASK carrying the verdict.
|
|
4752
|
+
* Input shape: `{ permission: { action, granted, reason?, ttl_ms?, ...extra } }`.
|
|
4753
|
+
*/
|
|
4754
|
+
async respondToPermission(request, opts) {
|
|
4755
|
+
if (request.type !== SignalType.PERMISSION) {
|
|
4756
|
+
throw new DendriteProtocolError(
|
|
4757
|
+
`respondToPermission expects a PERMISSION signal, got '${request.type}'`
|
|
4758
|
+
);
|
|
4759
|
+
}
|
|
4760
|
+
const target = opts.neuron ?? request.directed?.id ?? null;
|
|
4761
|
+
if (!target) {
|
|
4762
|
+
throw new DendriteProtocolError(
|
|
4763
|
+
"respondToPermission: signal has no neuron and no neuron override - nowhere to dispatch the follow-up TASK"
|
|
4764
|
+
);
|
|
4765
|
+
}
|
|
4766
|
+
const permission = {
|
|
4767
|
+
action: request.payload["action"] ?? null,
|
|
4768
|
+
granted: opts.granted
|
|
4769
|
+
};
|
|
4770
|
+
if (opts.reason !== void 0) permission["reason"] = opts.reason;
|
|
4771
|
+
if (opts.ttlMs !== void 0) permission["ttl_ms"] = opts.ttlMs;
|
|
4772
|
+
if (opts.extra !== void 0) Object.assign(permission, opts.extra);
|
|
4773
|
+
return this.dispatchTask({
|
|
4774
|
+
neuron: target,
|
|
4775
|
+
input: { permission },
|
|
4776
|
+
traceId: request.trace_id,
|
|
4777
|
+
parentId: request.id,
|
|
4778
|
+
...opts.meta !== void 0 ? { meta: opts.meta } : {}
|
|
4779
|
+
});
|
|
4780
|
+
}
|
|
4781
|
+
// -- cognition decision signals (discrete, decentralised option) -----
|
|
4782
|
+
/** Approve a PERMISSION request. */
|
|
4783
|
+
async grantPermission(request, opts = {}) {
|
|
4784
|
+
return this.decidePermission(request, true, opts);
|
|
4785
|
+
}
|
|
4786
|
+
/** Reject a PERMISSION request. */
|
|
4787
|
+
async denyPermission(request, opts = {}) {
|
|
4788
|
+
return this.decidePermission(request, false, opts);
|
|
4789
|
+
}
|
|
4790
|
+
async decidePermission(request, granted, opts) {
|
|
4791
|
+
if (request.type !== SignalType.PERMISSION) {
|
|
4792
|
+
throw new DendriteProtocolError(
|
|
4793
|
+
`grant/denyPermission expects a PERMISSION signal, got '${request.type}'`
|
|
4794
|
+
);
|
|
3327
4795
|
}
|
|
3328
|
-
|
|
3329
|
-
|
|
4796
|
+
const sig = permissionDecisionSignal({
|
|
4797
|
+
traceId: request.trace_id,
|
|
4798
|
+
parentId: request.id,
|
|
4799
|
+
granted,
|
|
4800
|
+
directed: { id: this.dendriteId },
|
|
4801
|
+
...opts.reason !== void 0 ? { reason: opts.reason } : {},
|
|
4802
|
+
...opts.ttlMs !== void 0 ? { ttlMs: opts.ttlMs } : {},
|
|
4803
|
+
...opts.meta !== void 0 ? { meta: opts.meta } : {}
|
|
4804
|
+
});
|
|
4805
|
+
await this.publish(sig);
|
|
4806
|
+
return sig;
|
|
4807
|
+
}
|
|
4808
|
+
/** Answer a CLARIFICATION with a discrete CLARIFICATION_ANSWER signal
|
|
4809
|
+
* (parent_id = the request's id). Consumers pick it up via
|
|
4810
|
+
* {@link onClarificationAnswer} or {@link awaitDecision}. Distinct from
|
|
4811
|
+
* {@link respondToClarification}, which re-dispatches a TASK. */
|
|
4812
|
+
async answerClarification(request, answer, opts = {}) {
|
|
4813
|
+
if (request.type !== SignalType.CLARIFICATION) {
|
|
4814
|
+
throw new DendriteProtocolError(
|
|
4815
|
+
`answerClarification expects a CLARIFICATION signal, got '${request.type}'`
|
|
4816
|
+
);
|
|
3330
4817
|
}
|
|
3331
|
-
|
|
4818
|
+
const sig = clarificationAnswerSignal({
|
|
4819
|
+
traceId: request.trace_id,
|
|
4820
|
+
parentId: request.id,
|
|
4821
|
+
answer,
|
|
4822
|
+
directed: { id: this.dendriteId },
|
|
4823
|
+
...opts.meta !== void 0 ? { meta: opts.meta } : {}
|
|
4824
|
+
});
|
|
4825
|
+
await this.publish(sig);
|
|
4826
|
+
return sig;
|
|
3332
4827
|
}
|
|
3333
|
-
/**
|
|
3334
|
-
|
|
3335
|
-
|
|
4828
|
+
/** Emit a synapse-side Signal. Refuses Axon-owned types; TASK initiation
|
|
4829
|
+
* additionally requires orchestrator role. */
|
|
4830
|
+
async emit(signal) {
|
|
4831
|
+
if (signal.type === SignalType.TASK) {
|
|
4832
|
+
this.requireOrchestrator(`emit(${signal.type})`);
|
|
4833
|
+
}
|
|
4834
|
+
if (!SYNAPSE_TYPES.has(signal.type)) {
|
|
4835
|
+
throw new DendriteProtocolError(
|
|
4836
|
+
`Dendrite refuses to emit '${signal.type}': only synapse-side types may be emitted this way. '${signal.type}' is an Axon-owned type.`
|
|
4837
|
+
);
|
|
4838
|
+
}
|
|
4839
|
+
await this.publish(signal);
|
|
3336
4840
|
}
|
|
3337
|
-
|
|
3338
|
-
|
|
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
|
-
};
|
|
4841
|
+
async publish(signal) {
|
|
4842
|
+
await this.synapse.publish(this.subject(signal.type), signal);
|
|
3350
4843
|
}
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
4844
|
+
async subscribe(type, handler, opts) {
|
|
4845
|
+
return this.synapse.subscribe(this.subject(type), handler, opts);
|
|
4846
|
+
}
|
|
4847
|
+
// -- internal ------------------------------------------------------
|
|
4848
|
+
subject(type) {
|
|
4849
|
+
return `cosmonapse.${this.namespace}.${type}`;
|
|
4850
|
+
}
|
|
4851
|
+
/** Subject for capability-routed TASKs (queue-grouped consumers). */
|
|
4852
|
+
routedSubject() {
|
|
4853
|
+
return `cosmonapse.${this.namespace}.${SignalType.TASK}.routed`;
|
|
4854
|
+
}
|
|
4855
|
+
ensureInboundSub(type) {
|
|
4856
|
+
if (this.inboundSubs.has(type)) return Promise.resolve();
|
|
4857
|
+
let p = this.inflightSubs.get(type);
|
|
4858
|
+
if (!p) {
|
|
4859
|
+
p = (async () => {
|
|
4860
|
+
const sub = await this.subscribe(type, (s) => this.dispatchInbound(s));
|
|
4861
|
+
this.inboundSubs.set(type, sub);
|
|
4862
|
+
})().finally(() => this.inflightSubs.delete(type));
|
|
4863
|
+
this.inflightSubs.set(type, p);
|
|
4864
|
+
}
|
|
4865
|
+
return p;
|
|
4866
|
+
}
|
|
4867
|
+
/**
|
|
4868
|
+
* Route an inbound TASK to a local Axon. Addressed: by neuron_id (drop if
|
|
4869
|
+
* not hosted here). Capability-routed: first local Axon whose caps superset
|
|
4870
|
+
* the request. After publishing the reply, apply terminal-handler finalize:
|
|
4871
|
+
* a TASK tagged `payload.finalize` promotes a successful AGENT_OUTPUT by
|
|
4872
|
+
* also emitting FINAL (parented to the AGENT_OUTPUT, attributed to the
|
|
4873
|
+
* producing Neuron). Only AGENT_OUTPUT is promoted - CLARIFICATION /
|
|
4874
|
+
* PERMISSION pause the workflow and ERROR is already terminal.
|
|
4875
|
+
*/
|
|
4876
|
+
async onTask(task) {
|
|
4877
|
+
let target = task.directed?.id ?? null;
|
|
4878
|
+
let axon;
|
|
4879
|
+
if (target) {
|
|
4880
|
+
axon = this._axons.get(target);
|
|
4881
|
+
if (!axon) return;
|
|
4882
|
+
} else {
|
|
4883
|
+
const requested = task.payload["capabilities"] ?? [];
|
|
4884
|
+
if (!requested.length) return;
|
|
4885
|
+
for (const candidate of this._axons.values()) {
|
|
4886
|
+
if (requested.every((c) => candidate.capabilities.includes(c))) {
|
|
4887
|
+
axon = candidate;
|
|
4888
|
+
break;
|
|
3360
4889
|
}
|
|
3361
4890
|
}
|
|
4891
|
+
if (!axon) return;
|
|
4892
|
+
target = axon.neuronId;
|
|
3362
4893
|
}
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
4894
|
+
let reply2;
|
|
4895
|
+
try {
|
|
4896
|
+
reply2 = await axon.handleTask(task);
|
|
4897
|
+
} catch (err) {
|
|
4898
|
+
reply2 = errorSignal({
|
|
4899
|
+
traceId: task.trace_id,
|
|
4900
|
+
parentId: task.id,
|
|
4901
|
+
directed: { id: target },
|
|
4902
|
+
code: "AXON_EXCEPTION",
|
|
4903
|
+
message: err instanceof Error ? err.message : String(err),
|
|
4904
|
+
recoverable: false
|
|
4905
|
+
});
|
|
3368
4906
|
}
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
4907
|
+
await this.publish(reply2);
|
|
4908
|
+
if (reply2.type === SignalType.AGENT_OUTPUT && task.payload["finalize"]) {
|
|
4909
|
+
try {
|
|
4910
|
+
await this.publish(
|
|
4911
|
+
finalSignal({
|
|
4912
|
+
traceId: reply2.trace_id,
|
|
4913
|
+
parentId: reply2.id,
|
|
4914
|
+
directed: { id: target },
|
|
4915
|
+
result: reply2.payload["output"] ?? {}
|
|
4916
|
+
})
|
|
4917
|
+
);
|
|
4918
|
+
} catch {
|
|
3380
4919
|
}
|
|
3381
4920
|
}
|
|
3382
4921
|
}
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
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;
|
|
4922
|
+
async emitRegister(axon) {
|
|
4923
|
+
await this.publish(
|
|
4924
|
+
registerSignal({
|
|
4925
|
+
directed: {
|
|
4926
|
+
id: axon.neuronId,
|
|
4927
|
+
type: axon.neuronKind ?? "neuron",
|
|
4928
|
+
capabilities: [...axon.capabilities]
|
|
4929
|
+
},
|
|
4930
|
+
capabilities: axon.capabilities,
|
|
4931
|
+
role: "neuron",
|
|
4932
|
+
...axon.version !== void 0 ? { version: axon.version } : {}
|
|
4933
|
+
})
|
|
4934
|
+
);
|
|
3404
4935
|
}
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
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;
|
|
4936
|
+
async emitDeregister(axon, reason) {
|
|
4937
|
+
await this.publish(
|
|
4938
|
+
deregisterSignal({
|
|
4939
|
+
directed: { id: axon.neuronId },
|
|
4940
|
+
...reason !== void 0 ? { reason } : {}
|
|
4941
|
+
})
|
|
4942
|
+
);
|
|
3421
4943
|
}
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
if (
|
|
3435
|
-
|
|
4944
|
+
async heartbeatTick() {
|
|
4945
|
+
if (!this.running) return;
|
|
4946
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4947
|
+
for (const axon of this._axons.values()) {
|
|
4948
|
+
try {
|
|
4949
|
+
if (this.reregisterOnHeartbeat) await this.emitRegister(axon);
|
|
4950
|
+
await this.synapse.publish(
|
|
4951
|
+
this.subject(SignalType.HEARTBEAT),
|
|
4952
|
+
heartbeatSignal({ directed: { id: axon.neuronId } })
|
|
4953
|
+
);
|
|
4954
|
+
} catch {
|
|
4955
|
+
}
|
|
4956
|
+
if (this.registryStore !== null) {
|
|
4957
|
+
try {
|
|
4958
|
+
await this.registryStore.touchHeartbeat(axon.neuronId, now);
|
|
4959
|
+
} catch {
|
|
4960
|
+
}
|
|
4961
|
+
}
|
|
4962
|
+
await this.hooks._fireRefresh({ reason: "heartbeat", neuronId: axon.neuronId, extra: {} });
|
|
4963
|
+
await axon.hooks._fireRefresh({ reason: "heartbeat", neuronId: axon.neuronId, extra: {} });
|
|
3436
4964
|
}
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
const
|
|
3451
|
-
const
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
4965
|
+
if (this.registryStore !== null && this.staleAfterMs > 0) {
|
|
4966
|
+
try {
|
|
4967
|
+
await this.sweepStaleNeurons(Date.now());
|
|
4968
|
+
} catch {
|
|
4969
|
+
}
|
|
4970
|
+
}
|
|
4971
|
+
}
|
|
4972
|
+
/** Mark Neurons deregistered when their last heartbeat is older than
|
|
4973
|
+
* `staleAfterMs`. Own hosted Axons were touched immediately before the
|
|
4974
|
+
* sweep, so they never qualify. */
|
|
4975
|
+
async sweepStaleNeurons(nowMs) {
|
|
4976
|
+
const store = this.registryStore;
|
|
4977
|
+
if (store === null) return;
|
|
4978
|
+
const records = await store.list({ includeDeregistered: false });
|
|
4979
|
+
for (const rec of records) {
|
|
4980
|
+
const seen = rec.last_heartbeat ?? rec.registered_at;
|
|
4981
|
+
if (!seen) continue;
|
|
4982
|
+
if (nowMs - Date.parse(seen) > this.staleAfterMs) {
|
|
4983
|
+
try {
|
|
4984
|
+
await store.markDeregistered(rec.neuron_id);
|
|
4985
|
+
await this.hooks._fireRefresh({
|
|
4986
|
+
reason: "stale",
|
|
4987
|
+
neuronId: rec.neuron_id,
|
|
4988
|
+
extra: {}
|
|
4989
|
+
});
|
|
4990
|
+
} catch {
|
|
4991
|
+
}
|
|
4992
|
+
}
|
|
3456
4993
|
}
|
|
4994
|
+
}
|
|
4995
|
+
async mirrorToStore(axon, status) {
|
|
4996
|
+
if (this.registryStore === null) return;
|
|
3457
4997
|
try {
|
|
3458
|
-
await this.
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
4998
|
+
await this.registryStore.upsert(
|
|
4999
|
+
neuronRecord({
|
|
5000
|
+
neuron_id: axon.neuronId,
|
|
5001
|
+
capabilities: [...axon.capabilities],
|
|
5002
|
+
version: axon.version ?? null,
|
|
5003
|
+
status,
|
|
5004
|
+
last_heartbeat: (/* @__PURE__ */ new Date()).toISOString()
|
|
5005
|
+
})
|
|
5006
|
+
);
|
|
5007
|
+
} catch {
|
|
3462
5008
|
}
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
5009
|
+
}
|
|
5010
|
+
/** Respond to a DISCOVER by re-emitting REGISTER for matching Axons. */
|
|
5011
|
+
async respondToDiscover(signal) {
|
|
5012
|
+
if (this._axons.size === 0) return;
|
|
5013
|
+
const target = signal.payload["neuron"];
|
|
5014
|
+
const capsFilter = signal.payload["capabilities"];
|
|
5015
|
+
for (const axon of this._axons.values()) {
|
|
5016
|
+
if (target && axon.neuronId !== target) continue;
|
|
5017
|
+
if (capsFilter?.length && !capsFilter.every((c) => axon.capabilities.includes(c))) {
|
|
5018
|
+
continue;
|
|
5019
|
+
}
|
|
5020
|
+
try {
|
|
5021
|
+
await this.emitRegister(axon);
|
|
5022
|
+
} catch {
|
|
5023
|
+
}
|
|
3467
5024
|
}
|
|
3468
5025
|
}
|
|
3469
|
-
async
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
5026
|
+
async dispatchInbound(signal) {
|
|
5027
|
+
if (signal.type === SignalType.DISCOVER) {
|
|
5028
|
+
const hs = this.handlers.get(SignalType.DISCOVER) ?? [];
|
|
5029
|
+
if (hs.length) await Promise.allSettled(hs.map((h) => h(signal)));
|
|
5030
|
+
else await this.respondToDiscover(signal);
|
|
5031
|
+
return;
|
|
3475
5032
|
}
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
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;
|
|
5033
|
+
if (signal.type === SignalType.RECALL) {
|
|
5034
|
+
await this.onRecall(signal);
|
|
5035
|
+
const hs = this.handlers.get(SignalType.RECALL) ?? [];
|
|
5036
|
+
if (hs.length) await Promise.allSettled(hs.map((h) => h(signal)));
|
|
5037
|
+
return;
|
|
3488
5038
|
}
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
pending.timer = setTimeout(() => this.onImprintDeadline(sig.id), args.deadlineMs);
|
|
5039
|
+
if (signal.type === SignalType.IMPRINT) {
|
|
5040
|
+
await this.onImprint(signal);
|
|
5041
|
+
const hs = this.handlers.get(SignalType.IMPRINT) ?? [];
|
|
5042
|
+
if (hs.length) await Promise.allSettled(hs.map((h) => h(signal)));
|
|
5043
|
+
return;
|
|
3495
5044
|
}
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
} catch (err) {
|
|
3499
|
-
this.cleanupImprint(args.traceId, sig.id);
|
|
3500
|
-
throw err;
|
|
5045
|
+
if (signal.type === SignalType.RECALLED || signal.type === SignalType.IMPRINTED) {
|
|
5046
|
+
this.engramClient.deliver(signal);
|
|
3501
5047
|
}
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
this.cleanupImprint(args.traceId, sig.id);
|
|
5048
|
+
if (signal.type === SignalType.REGISTER && this.isEngramRegister(signal)) {
|
|
5049
|
+
this.recordEngramRegistration(signal);
|
|
5050
|
+
return;
|
|
3506
5051
|
}
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
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
|
-
});
|
|
5052
|
+
if (signal.parent_id) {
|
|
5053
|
+
const opPw = this.opPathways.get(signal.parent_id);
|
|
5054
|
+
if (opPw) {
|
|
5055
|
+
try {
|
|
5056
|
+
await opPw._deliver(signal);
|
|
5057
|
+
} catch {
|
|
3528
5058
|
}
|
|
3529
|
-
} else {
|
|
3530
|
-
pending.hitsSoFar.push(...hits);
|
|
3531
|
-
if (engramId) pending.engrams.push(engramId);
|
|
3532
5059
|
}
|
|
3533
|
-
}
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
5060
|
+
}
|
|
5061
|
+
if ((signal.type === SignalType.CLARIFICATION_ANSWER || signal.type === SignalType.PERMISSION_DECISION) && signal.parent_id) {
|
|
5062
|
+
this.recentDecisions.set(signal.parent_id, signal);
|
|
5063
|
+
while (this.recentDecisions.size > 256) {
|
|
5064
|
+
const oldest = this.recentDecisions.keys().next().value;
|
|
5065
|
+
this.recentDecisions.delete(oldest);
|
|
5066
|
+
}
|
|
5067
|
+
}
|
|
5068
|
+
if ((signal.type === SignalType.FINAL || signal.type === SignalType.ERROR) && signal.trace_id) {
|
|
5069
|
+
await this.cancelOpPathways(signal.trace_id);
|
|
5070
|
+
this.engramClient.cancelTrace(signal.trace_id);
|
|
5071
|
+
}
|
|
5072
|
+
if (signal.type === SignalType.TASK_AWARDED) {
|
|
5073
|
+
const target = signal.directed?.id ?? null;
|
|
5074
|
+
if (target && this._axons.has(target)) {
|
|
5075
|
+
const synthetic = taskSignal({
|
|
5076
|
+
traceId: signal.trace_id,
|
|
5077
|
+
parentId: signal.id,
|
|
5078
|
+
directed: { id: target },
|
|
5079
|
+
input: signal.payload["input"] ?? {},
|
|
5080
|
+
finalize: Boolean(signal.payload["finalize"]),
|
|
5081
|
+
...signal.payload["context_ref"] !== void 0 ? { contextRef: signal.payload["context_ref"] } : {},
|
|
5082
|
+
meta: signal.meta
|
|
5083
|
+
});
|
|
5084
|
+
await this.onTask(synthetic);
|
|
5085
|
+
}
|
|
5086
|
+
}
|
|
5087
|
+
if (signal.trace_id && PATHWAY_TYPES.has(signal.type)) {
|
|
5088
|
+
const pathway = this.pathways.get(signal.trace_id);
|
|
5089
|
+
if (pathway) {
|
|
5090
|
+
try {
|
|
5091
|
+
await pathway._deliver(signal);
|
|
5092
|
+
} catch {
|
|
5093
|
+
}
|
|
5094
|
+
}
|
|
5095
|
+
}
|
|
5096
|
+
if (AXON_TYPES.has(signal.type) && this.registryStore !== null) {
|
|
5097
|
+
try {
|
|
5098
|
+
await this.updateRegistry(signal);
|
|
5099
|
+
} catch {
|
|
5100
|
+
}
|
|
5101
|
+
}
|
|
5102
|
+
if (signal.type === SignalType.TASK_OFFER && this.autoBid && this._axons.size > 0 && (this.handlers.get(SignalType.TASK_OFFER) ?? []).length === 0) {
|
|
5103
|
+
await this.maybeAutoBid(signal);
|
|
5104
|
+
}
|
|
5105
|
+
const handlers = this.handlers.get(signal.type) ?? [];
|
|
5106
|
+
if (handlers.length) await Promise.allSettled(handlers.map((h) => h(signal)));
|
|
5107
|
+
}
|
|
5108
|
+
// -- Engram: hosted-side handlers -----------------------------------
|
|
5109
|
+
/** Pick the hosted Engrams that should respond to a RECALL/IMPRINT.
|
|
5110
|
+
* directed.id (engramId) wins over directed.type (engramKind). */
|
|
5111
|
+
resolveEngramTargets(signal) {
|
|
5112
|
+
const eid = signal.directed?.id ?? null;
|
|
5113
|
+
if (eid) {
|
|
5114
|
+
const ent = this._engrams.get(eid);
|
|
5115
|
+
return ent ? [ent] : [];
|
|
5116
|
+
}
|
|
5117
|
+
const ekind = signal.directed?.type ?? null;
|
|
5118
|
+
if (ekind) {
|
|
5119
|
+
return (this.engramKindIndex.get(ekind) ?? []).map((id) => this._engrams.get(id)).filter((e) => e !== void 0);
|
|
5120
|
+
}
|
|
5121
|
+
return [];
|
|
5122
|
+
}
|
|
5123
|
+
async onRecall(signal) {
|
|
5124
|
+
const targets = this.resolveEngramTargets(signal);
|
|
5125
|
+
if (!targets.length) return;
|
|
5126
|
+
const query = signal.payload["query"] ?? {};
|
|
5127
|
+
const filters = signal.payload["filters"];
|
|
5128
|
+
const contextRef = signal.payload["context_ref"];
|
|
5129
|
+
const deadlineMs = signal.payload["deadline_ms"];
|
|
5130
|
+
const minConfidence = signal.payload["min_confidence"];
|
|
5131
|
+
for (const engram of targets) {
|
|
5132
|
+
let hits;
|
|
5133
|
+
try {
|
|
5134
|
+
if (!await engram.canServe(query)) continue;
|
|
5135
|
+
hits = await engram.recall(query, {
|
|
5136
|
+
...filters !== void 0 ? { filters } : {},
|
|
5137
|
+
...contextRef !== void 0 ? { contextRef } : {},
|
|
5138
|
+
...deadlineMs !== void 0 ? { deadlineMs } : {},
|
|
5139
|
+
...minConfidence !== void 0 ? { minConfidence } : {}
|
|
5140
|
+
});
|
|
5141
|
+
} catch {
|
|
5142
|
+
continue;
|
|
5143
|
+
}
|
|
5144
|
+
try {
|
|
5145
|
+
await this.publish(
|
|
5146
|
+
recalledSignal({
|
|
5147
|
+
traceId: signal.trace_id,
|
|
5148
|
+
parentId: signal.id,
|
|
5149
|
+
engramId: engram.engramId,
|
|
5150
|
+
hits: hits.map((h) => ({ id: h.id, entry: h.entry, score: h.score })),
|
|
5151
|
+
// Attribute the reply to the Engram that answered, not the host
|
|
5152
|
+
// Dendrite, so observers classify it by the Engram's REGISTER.
|
|
5153
|
+
directed: { id: engram.engramId, type: engram.engramKind }
|
|
5154
|
+
})
|
|
5155
|
+
);
|
|
5156
|
+
} catch {
|
|
5157
|
+
}
|
|
3546
5158
|
}
|
|
3547
5159
|
}
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
5160
|
+
async onImprint(signal) {
|
|
5161
|
+
const targets = this.resolveEngramTargets(signal);
|
|
5162
|
+
if (!targets.length) return;
|
|
5163
|
+
const op = signal.payload["op"] ?? "add";
|
|
5164
|
+
const entry = signal.payload["entry"] ?? {};
|
|
5165
|
+
const mergeKey = signal.payload["merge_key"];
|
|
5166
|
+
for (const engram of targets) {
|
|
5167
|
+
let reply2;
|
|
5168
|
+
try {
|
|
5169
|
+
const receipt2 = await engram.imprint(op, entry, {
|
|
5170
|
+
imprintId: signal.id,
|
|
5171
|
+
...mergeKey !== void 0 ? { mergeKey } : {}
|
|
5172
|
+
});
|
|
5173
|
+
reply2 = imprintedSignal({
|
|
5174
|
+
traceId: signal.trace_id,
|
|
5175
|
+
parentId: signal.id,
|
|
5176
|
+
engramId: receipt2.engramId || engram.engramId,
|
|
5177
|
+
op: receipt2.op,
|
|
5178
|
+
...receipt2.id !== null ? { id: receipt2.id } : {},
|
|
5179
|
+
...receipt2.version !== null ? { version: receipt2.version } : {},
|
|
5180
|
+
...receipt2.tookMs !== null ? { tookMs: receipt2.tookMs } : {},
|
|
5181
|
+
...receipt2.error !== null ? { error: receipt2.error } : {},
|
|
5182
|
+
directed: { id: engram.engramId, type: engram.engramKind }
|
|
5183
|
+
});
|
|
5184
|
+
} catch (err) {
|
|
5185
|
+
reply2 = imprintedSignal({
|
|
5186
|
+
traceId: signal.trace_id,
|
|
5187
|
+
parentId: signal.id,
|
|
5188
|
+
engramId: engram.engramId,
|
|
5189
|
+
op,
|
|
5190
|
+
error: `engram_exception: ${err instanceof Error ? err.message : String(err)}`,
|
|
5191
|
+
directed: { id: engram.engramId, type: engram.engramKind }
|
|
5192
|
+
});
|
|
3560
5193
|
}
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
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);
|
|
5194
|
+
try {
|
|
5195
|
+
await this.publish(reply2);
|
|
5196
|
+
} catch {
|
|
3567
5197
|
}
|
|
3568
5198
|
}
|
|
3569
5199
|
}
|
|
3570
|
-
|
|
3571
|
-
|
|
5200
|
+
// -- Engram: registration (announce + learn) -------------------------
|
|
5201
|
+
async emitEngramRegister(engram) {
|
|
5202
|
+
await this.publish(
|
|
5203
|
+
registerSignal({
|
|
5204
|
+
directed: {
|
|
5205
|
+
id: engram.engramId,
|
|
5206
|
+
type: engram.engramKind,
|
|
5207
|
+
capabilities: [...engram.capabilities]
|
|
5208
|
+
},
|
|
5209
|
+
capabilities: engram.capabilities,
|
|
5210
|
+
role: "engram",
|
|
5211
|
+
...engram.version !== null ? { version: engram.version } : {}
|
|
5212
|
+
})
|
|
5213
|
+
);
|
|
3572
5214
|
}
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
5215
|
+
isEngramRegister(signal) {
|
|
5216
|
+
if (signal.payload["role"] === "engram" || signal.payload["engram"]) return true;
|
|
5217
|
+
const dtype = signal.directed?.type ?? null;
|
|
5218
|
+
if (dtype && (this.engramKindIndex.has(dtype) || this.engramRegKindIndex.has(dtype))) {
|
|
5219
|
+
return true;
|
|
5220
|
+
}
|
|
5221
|
+
return false;
|
|
5222
|
+
}
|
|
5223
|
+
recordEngramRegistration(signal) {
|
|
5224
|
+
const d = signal.directed;
|
|
5225
|
+
if (!d || !d.id && !d.type) return;
|
|
5226
|
+
let caps = [...d.capabilities];
|
|
5227
|
+
if (!caps.length) {
|
|
5228
|
+
caps = [...signal.payload["capabilities"] ?? []];
|
|
5229
|
+
}
|
|
5230
|
+
const key = d.id ?? d.type;
|
|
5231
|
+
this._engramRegistrations.set(key, { id: d.id, type: d.type, capabilities: caps });
|
|
5232
|
+
if (d.type) {
|
|
5233
|
+
const bucket = this.engramRegKindIndex.get(d.type) ?? /* @__PURE__ */ new Set();
|
|
5234
|
+
bucket.add(key);
|
|
5235
|
+
this.engramRegKindIndex.set(d.type, bucket);
|
|
3586
5236
|
}
|
|
3587
5237
|
}
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
5238
|
+
// -- Engram: caller-side helpers -------------------------------------
|
|
5239
|
+
/** Resolve (traceId, parentId) for a caller-side engram op: explicit ids
|
|
5240
|
+
* win, then the ambient task context (bound by Axon.handleTask), then a
|
|
5241
|
+
* freshly minted trace (the pre-task-hydration shape). */
|
|
5242
|
+
static resolveTrace(traceId, parentId) {
|
|
5243
|
+
let tid = traceId;
|
|
5244
|
+
let pid = parentId;
|
|
5245
|
+
if (tid === void 0) {
|
|
5246
|
+
const amb = ambientTrace();
|
|
5247
|
+
if (amb !== null) {
|
|
5248
|
+
tid = amb[0];
|
|
5249
|
+
if (pid === void 0) pid = amb[1];
|
|
5250
|
+
}
|
|
5251
|
+
}
|
|
5252
|
+
return [tid ?? newTraceId(), pid ?? newEventId()];
|
|
3598
5253
|
}
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
this.
|
|
5254
|
+
/** Emit RECALL and await RECALLED. Trace attribution: explicit ids win,
|
|
5255
|
+
* then the ambient task context, then a fresh trace. */
|
|
5256
|
+
async recall(args) {
|
|
5257
|
+
const [tid, pid] = _Dendrite.resolveTrace(args.traceId, args.parentId);
|
|
5258
|
+
return this.engramClient.recall({
|
|
5259
|
+
query: args.query,
|
|
5260
|
+
traceId: tid,
|
|
5261
|
+
parentId: pid,
|
|
5262
|
+
...args.engramId !== void 0 ? { engramId: args.engramId } : {},
|
|
5263
|
+
...args.engramKind !== void 0 ? { engramKind: args.engramKind } : {},
|
|
5264
|
+
...args.filters !== void 0 ? { filters: args.filters } : {},
|
|
5265
|
+
...args.contextRef !== void 0 ? { contextRef: args.contextRef } : {},
|
|
5266
|
+
...args.deadlineMs !== void 0 ? { deadlineMs: args.deadlineMs } : {},
|
|
5267
|
+
...args.recallMode !== void 0 ? { recallMode: args.recallMode } : {},
|
|
5268
|
+
...args.minConfidence !== void 0 ? { minConfidence: args.minConfidence } : {},
|
|
5269
|
+
...args.meta !== void 0 ? { meta: args.meta } : {}
|
|
5270
|
+
});
|
|
3604
5271
|
}
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
this.
|
|
5272
|
+
/** Emit IMPRINT. Resolves null unless `awaitAck: true`. Trace attribution
|
|
5273
|
+
* as {@link recall}. */
|
|
5274
|
+
async imprint(args) {
|
|
5275
|
+
const [tid, pid] = _Dendrite.resolveTrace(args.traceId, args.parentId);
|
|
5276
|
+
return this.engramClient.imprint({
|
|
5277
|
+
op: args.op,
|
|
5278
|
+
entry: args.entry,
|
|
5279
|
+
traceId: tid,
|
|
5280
|
+
parentId: pid,
|
|
5281
|
+
...args.engramId !== void 0 ? { engramId: args.engramId } : {},
|
|
5282
|
+
...args.engramKind !== void 0 ? { engramKind: args.engramKind } : {},
|
|
5283
|
+
...args.mergeKey !== void 0 ? { mergeKey: args.mergeKey } : {},
|
|
5284
|
+
...args.awaitAck !== void 0 ? { awaitAck: args.awaitAck } : {},
|
|
5285
|
+
...args.deadlineMs !== void 0 ? { deadlineMs: args.deadlineMs } : {},
|
|
5286
|
+
...args.meta !== void 0 ? { meta: args.meta } : {}
|
|
5287
|
+
});
|
|
3610
5288
|
}
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
if (
|
|
3614
|
-
|
|
3615
|
-
if (
|
|
5289
|
+
async updateRegistry(signal) {
|
|
5290
|
+
if (this.registryStore === null) return;
|
|
5291
|
+
if (signal.payload["role"] === "engram" || signal.payload["engram"]) return;
|
|
5292
|
+
const neuronId = signal.directed?.id ?? null;
|
|
5293
|
+
if (!neuronId) return;
|
|
5294
|
+
let reason = null;
|
|
5295
|
+
if (signal.type === SignalType.REGISTER) {
|
|
5296
|
+
await this.registryStore.upsert(
|
|
5297
|
+
neuronRecord({
|
|
5298
|
+
neuron_id: neuronId,
|
|
5299
|
+
capabilities: signal.payload["capabilities"] ?? [],
|
|
5300
|
+
version: signal.payload["version"] ?? null,
|
|
5301
|
+
status: "registered",
|
|
5302
|
+
last_heartbeat: signal.ts
|
|
5303
|
+
})
|
|
5304
|
+
);
|
|
5305
|
+
reason = "register";
|
|
5306
|
+
} else if (signal.type === SignalType.DEREGISTER) {
|
|
5307
|
+
await this.registryStore.markDeregistered(neuronId);
|
|
5308
|
+
reason = "deregister";
|
|
5309
|
+
} else if (signal.type === SignalType.HEARTBEAT) {
|
|
5310
|
+
const status = signal.payload["status"];
|
|
5311
|
+
if (status) await this.registryStore.touchHeartbeat(neuronId, signal.ts, status);
|
|
5312
|
+
else await this.registryStore.touchHeartbeat(neuronId, signal.ts);
|
|
5313
|
+
reason = "heartbeat";
|
|
5314
|
+
}
|
|
5315
|
+
if (reason !== null) {
|
|
5316
|
+
await this.hooks._fireRefresh({ reason, neuronId, extra: {} });
|
|
5317
|
+
}
|
|
3616
5318
|
}
|
|
3617
5319
|
};
|
|
3618
|
-
|
|
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
|
-
}
|
|
5320
|
+
var Cortex = Dendrite;
|
|
3633
5321
|
|
|
3634
5322
|
// src/engram-sqlite.ts
|
|
3635
5323
|
var SCHEMA3 = `
|
|
@@ -4167,11 +5855,12 @@ var PostgresEngram = class extends Engram {
|
|
|
4167
5855
|
};
|
|
4168
5856
|
|
|
4169
5857
|
// src/index.ts
|
|
4170
|
-
var VERSION = true ? "0.1.
|
|
5858
|
+
var VERSION = true ? "0.1.3" : "0.0.0-dev";
|
|
4171
5859
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4172
5860
|
0 && (module.exports = {
|
|
4173
5861
|
AXON_TYPES,
|
|
4174
5862
|
Axon,
|
|
5863
|
+
COSMO_INTENT_SYSTEM_PROMPT,
|
|
4175
5864
|
Cortex,
|
|
4176
5865
|
CortexProtocolError,
|
|
4177
5866
|
Dendrite,
|
|
@@ -4192,14 +5881,19 @@ var VERSION = true ? "0.1.2" : "0.0.0-dev";
|
|
|
4192
5881
|
MemoryRegistryStore,
|
|
4193
5882
|
MemorySynapse,
|
|
4194
5883
|
NatsSynapse,
|
|
5884
|
+
PATHWAY_TYPES,
|
|
5885
|
+
Pathway,
|
|
5886
|
+
PathwayClosedError,
|
|
4195
5887
|
PostgresEngram,
|
|
4196
5888
|
PostgresRegistryStore,
|
|
4197
5889
|
SYNAPSE_TYPES,
|
|
4198
5890
|
SignalType,
|
|
4199
5891
|
SqliteEngram,
|
|
4200
5892
|
SqliteRegistryStore,
|
|
5893
|
+
TERMINAL_TYPES,
|
|
4201
5894
|
VERSION,
|
|
4202
5895
|
agentOutputSignal,
|
|
5896
|
+
ambientTrace,
|
|
4203
5897
|
anthropicNeuron,
|
|
4204
5898
|
bidSignal,
|
|
4205
5899
|
clarificationAnswerSignal,
|
|
@@ -4220,6 +5914,7 @@ var VERSION = true ? "0.1.2" : "0.0.0-dev";
|
|
|
4220
5914
|
errorSignal,
|
|
4221
5915
|
escalationSignal,
|
|
4222
5916
|
finalSignal,
|
|
5917
|
+
followupPrompt,
|
|
4223
5918
|
heartbeatSignal,
|
|
4224
5919
|
huggingFaceNeuron,
|
|
4225
5920
|
imprintSignal,
|
|
@@ -4247,8 +5942,11 @@ var VERSION = true ? "0.1.2" : "0.0.0-dev";
|
|
|
4247
5942
|
recalledSignal,
|
|
4248
5943
|
registerSignal,
|
|
4249
5944
|
reply,
|
|
5945
|
+
runWithTraceContext,
|
|
4250
5946
|
standardMcpServers,
|
|
4251
5947
|
synapseFromUrl,
|
|
5948
|
+
taskAwardedSignal,
|
|
5949
|
+
taskDeclinedSignal,
|
|
4252
5950
|
taskOfferSignal,
|
|
4253
5951
|
taskSignal,
|
|
4254
5952
|
thoughtDeltaSignal,
|