@tangle-network/agent-runtime 0.104.0 → 0.106.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +176 -42
- package/dist/activation-CRjVfOiy.js +203 -0
- package/dist/activation-CRjVfOiy.js.map +1 -0
- package/dist/activation-DyWB0K6E.d.ts +98 -0
- package/dist/agent.d.ts +216 -234
- package/dist/agent.js +615 -547
- package/dist/agent.js.map +1 -1
- package/dist/analyst-loop-DGdtVENg.js +517 -0
- package/dist/analyst-loop-DGdtVENg.js.map +1 -0
- package/dist/analyst-loop.d.ts +10 -43
- package/dist/analyst-loop.js +2 -11
- package/dist/backends-Dy8NujB4.js +713 -0
- package/dist/backends-Dy8NujB4.js.map +1 -0
- package/dist/candidate-execution/index.d.ts +4 -192
- package/dist/candidate-execution/index.js +5 -66
- package/dist/candidate-execution-DqMbv1rU.js +488 -0
- package/dist/candidate-execution-DqMbv1rU.js.map +1 -0
- package/dist/coder-yhVWbdWc.js +40 -0
- package/dist/coder-yhVWbdWc.js.map +1 -0
- package/dist/conversation-XucG_K3l.js +1265 -0
- package/dist/conversation-XucG_K3l.js.map +1 -0
- package/dist/conversation.d.ts +2 -804
- package/dist/conversation.js +2 -55
- package/dist/environment-provider-CfLUmKmf.js +1168 -0
- package/dist/environment-provider-CfLUmKmf.js.map +1 -0
- package/dist/environment-provider-IUGU3epE.d.ts +964 -0
- package/dist/environment-provider.d.ts +2 -78
- package/dist/environment-provider.js +2 -18
- package/dist/errors-DEAvWQPy.js +111 -0
- package/dist/errors-DEAvWQPy.js.map +1 -0
- package/dist/improvement-adapter-Cr5TBSji.d.ts +166 -0
- package/dist/improvement-cycle-C095kS_g.js +2624 -0
- package/dist/improvement-cycle-C095kS_g.js.map +1 -0
- package/dist/index-BGdxAwfO.d.ts +838 -0
- package/dist/index-C-FYUuFG.d.ts +193 -0
- package/dist/index-CbNM40xG.d.ts +140 -0
- package/dist/index-DruSKs3Z.d.ts +7224 -0
- package/dist/index-Efjb3nrQ.d.ts +671 -0
- package/dist/index.d.ts +14 -1134
- package/dist/index.js +2134 -1972
- package/dist/index.js.map +1 -1
- package/dist/intelligence.d.ts +776 -888
- package/dist/intelligence.js +1466 -1410
- package/dist/intelligence.js.map +1 -1
- package/dist/{kb-gate-CwHO0vz6.d.ts → kb-gate-C8z2juK8.d.ts} +33 -31
- package/dist/kb-gate-DpaSwXVx.js +80 -0
- package/dist/kb-gate-DpaSwXVx.js.map +1 -0
- package/dist/knowledge-Ku-mPP4f.js +433 -0
- package/dist/knowledge-Ku-mPP4f.js.map +1 -0
- package/dist/knowledge.d.ts +2 -150
- package/dist/knowledge.js +2 -40
- package/dist/local-harness-Dh8PJ0ot.d.ts +455 -0
- package/dist/loop-runner-bin-BXsFEUcf.d.ts +162 -0
- package/dist/loop-runner-bin-BwjQ1NfH.js +268 -0
- package/dist/loop-runner-bin-BwjQ1NfH.js.map +1 -0
- package/dist/loop-runner-bin.d.ts +2 -17
- package/dist/loop-runner-bin.js +2 -24
- package/dist/loops.d.ts +5 -4164
- package/dist/loops.js +8 -419
- package/dist/mcp/bin.d.ts +1 -1
- package/dist/mcp/bin.js +116 -146
- package/dist/mcp/bin.js.map +1 -1
- package/dist/mcp/index.d.ts +295 -437
- package/dist/mcp/index.js +1242 -1177
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/memory-bin.d.ts +1 -1
- package/dist/mcp/memory-bin.js +37 -16
- package/dist/mcp/memory-bin.js.map +1 -1
- package/dist/memory-server-DL6cE2Ag.js +234 -0
- package/dist/memory-server-DL6cE2Ag.js.map +1 -0
- package/dist/model-resolution-Btd9iIKV.js +98 -0
- package/dist/model-resolution-Btd9iIKV.js.map +1 -0
- package/dist/openai-tools-B68JaOCx.d.ts +24 -0
- package/dist/openai-tools-CKLy1C7M.js +44 -0
- package/dist/openai-tools-CKLy1C7M.js.map +1 -0
- package/dist/otel-export-D34IQW1C.js +843 -0
- package/dist/otel-export-D34IQW1C.js.map +1 -0
- package/dist/platform.d.ts +188 -185
- package/dist/platform.js +185 -218
- package/dist/platform.js.map +1 -1
- package/dist/prepare-B53sPV7M.js +4292 -0
- package/dist/prepare-B53sPV7M.js.map +1 -0
- package/dist/primeintellect/index.d.ts +150 -147
- package/dist/primeintellect/index.js +626 -1004
- package/dist/primeintellect/index.js.map +1 -1
- package/dist/profiles.d.ts +296 -490
- package/dist/profiles.js +722 -749
- package/dist/profiles.js.map +1 -1
- package/dist/protected-model-port-BLPH5VVv.js +722 -0
- package/dist/protected-model-port-BLPH5VVv.js.map +1 -0
- package/dist/protected-model-port-BP6Z4eau.d.ts +283 -0
- package/dist/protected-redaction--F3v1oo8.js +200 -0
- package/dist/protected-redaction--F3v1oo8.js.map +1 -0
- package/dist/redact-D-u-rrcn.js +82 -0
- package/dist/redact-D-u-rrcn.js.map +1 -0
- package/dist/redact-kbIOjfjM.d.ts +452 -0
- package/dist/researcher-CoVqNhfI.js +433 -0
- package/dist/researcher-CoVqNhfI.js.map +1 -0
- package/dist/runtime-BG0BziaP.js +5408 -0
- package/dist/runtime-BG0BziaP.js.map +1 -0
- package/dist/sandbox-events-DeI5xX8P.js +316 -0
- package/dist/sandbox-events-DeI5xX8P.js.map +1 -0
- package/dist/spawn-journal-CwPvKUTa.js +456 -0
- package/dist/spawn-journal-CwPvKUTa.js.map +1 -0
- package/dist/structural-rollout-IXUEplky.js +1445 -0
- package/dist/structural-rollout-IXUEplky.js.map +1 -0
- package/dist/substrate-B0TYNrXn.js +27 -0
- package/dist/substrate-B0TYNrXn.js.map +1 -0
- package/dist/{substrate-Dyiqmd3o.d.ts → substrate-BcnuSHXm.d.ts} +57 -54
- package/dist/supervise-BiRutHS9.js +7321 -0
- package/dist/supervise-BiRutHS9.js.map +1 -0
- package/dist/supervisor-DTKhF-RV.js +3826 -0
- package/dist/supervisor-DTKhF-RV.js.map +1 -0
- package/dist/testing.d.ts +4 -3
- package/dist/testing.js +2478 -2637
- package/dist/testing.js.map +1 -1
- package/dist/tool-server-RcWgLIsL.js +141 -0
- package/dist/tool-server-RcWgLIsL.js.map +1 -0
- package/dist/trace-propagation-CJJC7SVB.js +57 -0
- package/dist/trace-propagation-CJJC7SVB.js.map +1 -0
- package/dist/types-BaIpb0XT.d.ts +492 -0
- package/dist/types-BevOjfTY.d.ts +765 -0
- package/dist/types-C9j4qg6l.d.ts +500 -0
- package/dist/types-Cja5OTGh.d.ts +190 -0
- package/dist/util-MVgdwuIS.js +145 -0
- package/dist/util-MVgdwuIS.js.map +1 -0
- package/dist/workspace-archive-BosQiotA.js +575 -0
- package/dist/workspace-archive-BosQiotA.js.map +1 -0
- package/package.json +43 -55
- package/skills/build-with-agent-runtime/SKILL.md +16 -9
- package/skills/loop-writer/SKILL.md +1 -1
- package/dist/activation-Cm-9MMX1.d.ts +0 -69
- package/dist/analyst-loop.js.map +0 -1
- package/dist/backends-BO4Jqoe2.d.ts +0 -134
- package/dist/candidate-execution/index.js.map +0 -1
- package/dist/chunk-2KGAN2HM.js +0 -1256
- package/dist/chunk-2KGAN2HM.js.map +0 -1
- package/dist/chunk-2QJSNOGK.js +0 -48
- package/dist/chunk-2QJSNOGK.js.map +0 -1
- package/dist/chunk-2UABGGUB.js +0 -474
- package/dist/chunk-2UABGGUB.js.map +0 -1
- package/dist/chunk-2VZYRB55.js +0 -834
- package/dist/chunk-2VZYRB55.js.map +0 -1
- package/dist/chunk-2X65HYMI.js +0 -3526
- package/dist/chunk-2X65HYMI.js.map +0 -1
- package/dist/chunk-3MDZX7YU.js +0 -125
- package/dist/chunk-3MDZX7YU.js.map +0 -1
- package/dist/chunk-3PPVCS75.js +0 -199
- package/dist/chunk-3PPVCS75.js.map +0 -1
- package/dist/chunk-5HNDYWUT.js +0 -6424
- package/dist/chunk-5HNDYWUT.js.map +0 -1
- package/dist/chunk-5IBQAPVB.js +0 -29
- package/dist/chunk-5IBQAPVB.js.map +0 -1
- package/dist/chunk-5QOB7H74.js +0 -387
- package/dist/chunk-5QOB7H74.js.map +0 -1
- package/dist/chunk-ATDFUJYI.js +0 -4728
- package/dist/chunk-ATDFUJYI.js.map +0 -1
- package/dist/chunk-B7K7V22Y.js +0 -699
- package/dist/chunk-B7K7V22Y.js.map +0 -1
- package/dist/chunk-DPEUKJRO.js +0 -27
- package/dist/chunk-DPEUKJRO.js.map +0 -1
- package/dist/chunk-FD2MBMOH.js +0 -494
- package/dist/chunk-FD2MBMOH.js.map +0 -1
- package/dist/chunk-FJI3EXU3.js +0 -2402
- package/dist/chunk-FJI3EXU3.js.map +0 -1
- package/dist/chunk-HAO4CXHP.js +0 -200
- package/dist/chunk-HAO4CXHP.js.map +0 -1
- package/dist/chunk-ISPWRSEC.js +0 -408
- package/dist/chunk-ISPWRSEC.js.map +0 -1
- package/dist/chunk-KRBFHMV6.js +0 -1228
- package/dist/chunk-KRBFHMV6.js.map +0 -1
- package/dist/chunk-L5DST3QC.js +0 -351
- package/dist/chunk-L5DST3QC.js.map +0 -1
- package/dist/chunk-M22PL4GH.js +0 -171
- package/dist/chunk-M22PL4GH.js.map +0 -1
- package/dist/chunk-NURIQBJQ.js +0 -72
- package/dist/chunk-NURIQBJQ.js.map +0 -1
- package/dist/chunk-PZZKQVQV.js +0 -52
- package/dist/chunk-PZZKQVQV.js.map +0 -1
- package/dist/chunk-Q2JSAVQ3.js +0 -671
- package/dist/chunk-Q2JSAVQ3.js.map +0 -1
- package/dist/chunk-SBTWKPVR.js +0 -5576
- package/dist/chunk-SBTWKPVR.js.map +0 -1
- package/dist/chunk-SGKPNBXE.js +0 -60
- package/dist/chunk-SGKPNBXE.js.map +0 -1
- package/dist/chunk-SNSMRT6H.js +0 -680
- package/dist/chunk-SNSMRT6H.js.map +0 -1
- package/dist/chunk-STJEHM5F.js +0 -1338
- package/dist/chunk-STJEHM5F.js.map +0 -1
- package/dist/chunk-UPWGXKXB.js +0 -254
- package/dist/chunk-UPWGXKXB.js.map +0 -1
- package/dist/chunk-YEJR7IXO.js +0 -68
- package/dist/chunk-YEJR7IXO.js.map +0 -1
- package/dist/chunk-ZXICDSAK.js +0 -907
- package/dist/chunk-ZXICDSAK.js.map +0 -1
- package/dist/completion-gate-Bv47cOnn.d.ts +0 -256
- package/dist/conversation.js.map +0 -1
- package/dist/coordination-CxtzMxSM.d.ts +0 -2570
- package/dist/environment-provider.js.map +0 -1
- package/dist/improve--2A0gK1-.d.ts +0 -440
- package/dist/improvement-adapter-HAZz-7vK.d.ts +0 -203
- package/dist/knowledge.js.map +0 -1
- package/dist/local-harness-CtK9dSny.d.ts +0 -175
- package/dist/loop-runner-bin-DUOGtvOl.d.ts +0 -207
- package/dist/loop-runner-bin.js.map +0 -1
- package/dist/loops.js.map +0 -1
- package/dist/openai-tools-fnj6SRVg.d.ts +0 -46
- package/dist/protected-model-port-DzkUY3HQ.d.ts +0 -271
- package/dist/sanitize-DEbPNtyI.d.ts +0 -328
- package/dist/structural-rollout-C4mabf87.d.ts +0 -446
- package/dist/supervise-7q9F5brx.d.ts +0 -379
- package/dist/types-BCoemcXU.d.ts +0 -802
- package/dist/types-BwoZWq-i.d.ts +0 -510
- package/dist/types-Cbp72qyu.d.ts +0 -491
- package/dist/types-CmYCMbFT.d.ts +0 -203
- package/dist/types-CvNwMzQt.d.ts +0 -957
- package/dist/worktree-fanout-WsOzt8qu.d.ts +0 -999
package/dist/mcp/memory-bin.js
CHANGED
|
@@ -1,22 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
import { a as createMemoryToolServer, c as resolveMemoryFromEnv } from "../memory-server-DL6cE2Ag.js";
|
|
3
|
+
//#region src/mcp/memory-bin.ts
|
|
4
|
+
/**
|
|
5
|
+
* `agent-runtime-memory-mcp` — stdio memory MCP server entry point.
|
|
6
|
+
*
|
|
7
|
+
* Serves `memory_search` / `memory_get` over an agent's curated memory rows,
|
|
8
|
+
* on the generic in-repo JSON-RPC core (`createStdioToolServer`). This is the
|
|
9
|
+
* process `memoryMcpServer` (lifecycle/memory.ts) mounts into `profile.mcp`,
|
|
10
|
+
* which the same-host client (`materializeLocalMcp`) spawns next to the
|
|
11
|
+
* driven worker during a scored run.
|
|
12
|
+
*
|
|
13
|
+
* Environment variables (the `memoryMcpServer` contract):
|
|
14
|
+
* AGENT_MEMORY_FILE optional — path to the durable row store (JSON array
|
|
15
|
+
* or JSONL of MemoryItem).
|
|
16
|
+
* AGENT_MEMORY_ITEMS optional — inline JSON array of MemoryItem rows;
|
|
17
|
+
* wins over file rows on id collision.
|
|
18
|
+
* AGENT_MEMORY_LOG optional — JSONL retrieval log, one row per
|
|
19
|
+
* memory_search (the retrieval-holdout seam).
|
|
20
|
+
* AGENT_MEMORY_NAME optional — server display name (default 'agent-memory').
|
|
21
|
+
*
|
|
22
|
+
* At least one of AGENT_MEMORY_FILE / AGENT_MEMORY_ITEMS must resolve rows:
|
|
23
|
+
* an EMPTY memory refuses to boot (fail-closed, so a memory-carrying profile
|
|
24
|
+
* whose store is missing fails the materialization, never a silent no-tool run).
|
|
25
|
+
*
|
|
26
|
+
* @experimental
|
|
27
|
+
*/
|
|
9
28
|
async function main() {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
await server.serve();
|
|
29
|
+
const { items, serverName, logPath } = resolveMemoryFromEnv(process.env);
|
|
30
|
+
await createMemoryToolServer({
|
|
31
|
+
items,
|
|
32
|
+
...serverName ? { serverName } : {},
|
|
33
|
+
...logPath ? { logPath } : {}
|
|
34
|
+
}).serve();
|
|
17
35
|
}
|
|
18
36
|
main().catch((err) => {
|
|
19
|
-
|
|
20
|
-
|
|
37
|
+
console.error(`agent-runtime-memory-mcp: ${err instanceof Error ? err.message : String(err)}`);
|
|
38
|
+
process.exit(1);
|
|
21
39
|
});
|
|
40
|
+
//#endregion
|
|
41
|
+
export {};
|
|
42
|
+
|
|
22
43
|
//# sourceMappingURL=memory-bin.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/mcp/memory-bin.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * `agent-runtime-memory-mcp` — stdio memory MCP server entry point.\n *\n * Serves `memory_search` / `memory_get` over an agent's curated memory rows,\n * on the generic in-repo JSON-RPC core (`createStdioToolServer`). This is the\n * process `memoryMcpServer` (lifecycle/memory.ts) mounts into `profile.mcp`,\n * which the same-host client (`materializeLocalMcp`) spawns next to the\n * driven worker during a scored run.\n *\n * Environment variables (the `memoryMcpServer` contract):\n * AGENT_MEMORY_FILE optional — path to the durable row store (JSON array\n * or JSONL of MemoryItem).\n * AGENT_MEMORY_ITEMS optional — inline JSON array of MemoryItem rows;\n * wins over file rows on id collision.\n * AGENT_MEMORY_LOG optional — JSONL retrieval log, one row per\n * memory_search (the retrieval-holdout seam).\n * AGENT_MEMORY_NAME optional — server display name (default 'agent-memory').\n *\n * At least one of AGENT_MEMORY_FILE / AGENT_MEMORY_ITEMS must resolve rows:\n * an EMPTY memory refuses to boot (fail-closed, so a memory-carrying profile\n * whose store is missing fails the materialization, never a silent no-tool run).\n *\n * @experimental\n */\n\nimport { createMemoryToolServer, resolveMemoryFromEnv } from './memory-server'\n\nasync function main(): Promise<void> {\n const { items, serverName, logPath } = resolveMemoryFromEnv(process.env)\n const server = createMemoryToolServer({\n items,\n ...(serverName ? { serverName } : {}),\n ...(logPath ? { logPath } : {}),\n })\n await server.serve()\n}\n\nmain().catch((err: unknown) => {\n console.error(`agent-runtime-memory-mcp: ${err instanceof Error ? err.message : String(err)}`)\n process.exit(1)\n})\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"memory-bin.js","names":[],"sources":["../../src/mcp/memory-bin.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * `agent-runtime-memory-mcp` — stdio memory MCP server entry point.\n *\n * Serves `memory_search` / `memory_get` over an agent's curated memory rows,\n * on the generic in-repo JSON-RPC core (`createStdioToolServer`). This is the\n * process `memoryMcpServer` (lifecycle/memory.ts) mounts into `profile.mcp`,\n * which the same-host client (`materializeLocalMcp`) spawns next to the\n * driven worker during a scored run.\n *\n * Environment variables (the `memoryMcpServer` contract):\n * AGENT_MEMORY_FILE optional — path to the durable row store (JSON array\n * or JSONL of MemoryItem).\n * AGENT_MEMORY_ITEMS optional — inline JSON array of MemoryItem rows;\n * wins over file rows on id collision.\n * AGENT_MEMORY_LOG optional — JSONL retrieval log, one row per\n * memory_search (the retrieval-holdout seam).\n * AGENT_MEMORY_NAME optional — server display name (default 'agent-memory').\n *\n * At least one of AGENT_MEMORY_FILE / AGENT_MEMORY_ITEMS must resolve rows:\n * an EMPTY memory refuses to boot (fail-closed, so a memory-carrying profile\n * whose store is missing fails the materialization, never a silent no-tool run).\n *\n * @experimental\n */\n\nimport { createMemoryToolServer, resolveMemoryFromEnv } from './memory-server'\n\nasync function main(): Promise<void> {\n const { items, serverName, logPath } = resolveMemoryFromEnv(process.env)\n const server = createMemoryToolServer({\n items,\n ...(serverName ? { serverName } : {}),\n ...(logPath ? { logPath } : {}),\n })\n await server.serve()\n}\n\nmain().catch((err: unknown) => {\n console.error(`agent-runtime-memory-mcp: ${err instanceof Error ? err.message : String(err)}`)\n process.exit(1)\n})\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,eAAe,OAAsB;CACnC,MAAM,EAAE,OAAO,YAAY,YAAY,qBAAqB,QAAQ,GAAG;CAMvE,MALe,uBAAuB;EACpC;EACA,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;EACnC,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;CAC/B,CACW,CAAC,CAAC,MAAM;AACrB;AAEA,KAAK,CAAC,CAAC,OAAO,QAAiB;CAC7B,QAAQ,MAAM,6BAA6B,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,GAAG;CAC7F,QAAQ,KAAK,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { u as ValidationError } from "./errors-DEAvWQPy.js";
|
|
2
|
+
import { t as createStdioToolServer } from "./tool-server-RcWgLIsL.js";
|
|
3
|
+
import { appendFileSync, mkdirSync, readFileSync } from "node:fs";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
//#region src/mcp/memory-server.ts
|
|
6
|
+
/**
|
|
7
|
+
* Memory MCP server — the LIVE serving half of the `memory` profile surface
|
|
8
|
+
* (Phase 5). A curated memory (lessons distilled from prior runs) is only
|
|
9
|
+
* real if the DRIVEN agent can query it mid-task; this module serves a flat
|
|
10
|
+
* store of `MemoryItem` rows as `memory_search` / `memory_get` tools over the
|
|
11
|
+
* ONE in-repo stdio JSON-RPC core (`createStdioToolServer`), so the exact
|
|
12
|
+
* wire protocol the same-host client (`connectStdioMcp` /
|
|
13
|
+
* `materializeLocalMcp`, runtime/stdio-mcp-client.ts) speaks is guaranteed by
|
|
14
|
+
* construction — serve and consume cannot drift.
|
|
15
|
+
*
|
|
16
|
+
* Retrieval is DETERMINISTIC lexical overlap (no vectors, no LLM): a lift
|
|
17
|
+
* measured with this memory mounted is attributable to the lessons
|
|
18
|
+
* themselves, never to retrieval-model noise. Every
|
|
19
|
+
* `memory_search` can append one JSONL row to a retrieval log (`logPath`) —
|
|
20
|
+
* the per-query record an off-policy retrieval estimator (agent-knowledge's
|
|
21
|
+
* `RetrievalHoldout`) consumes. agent-knowledge is NOT a dependency of this
|
|
22
|
+
* repo, so the log file is the flagged cross-package seam, not an import.
|
|
23
|
+
*
|
|
24
|
+
* Fail-loud discipline (mirrors `materializeLocalMcp`): an EMPTY memory is
|
|
25
|
+
* never served — a profile without memory simply omits the artifact, and
|
|
26
|
+
* silently serving zero rows would fake the with/without ablation.
|
|
27
|
+
*
|
|
28
|
+
* @experimental
|
|
29
|
+
*/
|
|
30
|
+
/** Env var naming the durable row store file the memory bin loads (the
|
|
31
|
+
* `memoryMcpServer` ↔ memory-bin contract). */
|
|
32
|
+
const MEMORY_FILE_ENV = "AGENT_MEMORY_FILE";
|
|
33
|
+
/** Env var carrying inline JSON `MemoryItem` rows (win over file rows on id). */
|
|
34
|
+
const MEMORY_ITEMS_ENV = "AGENT_MEMORY_ITEMS";
|
|
35
|
+
/** Env var naming the JSONL retrieval log (one row per `memory_search`). */
|
|
36
|
+
const MEMORY_LOG_ENV = "AGENT_MEMORY_LOG";
|
|
37
|
+
/** Env var overriding the served display name (default 'agent-memory'). */
|
|
38
|
+
const MEMORY_NAME_ENV = "AGENT_MEMORY_NAME";
|
|
39
|
+
/**
|
|
40
|
+
* Build the memory MCP server: `memory_search` (lexical top-k over the rows)
|
|
41
|
+
* and `memory_get` (one row by id) on the generic stdio JSON-RPC core.
|
|
42
|
+
*/
|
|
43
|
+
function createMemoryToolServer(opts) {
|
|
44
|
+
if (opts.items.length === 0) throw new ValidationError("createMemoryToolServer: refusing to serve an EMPTY memory — a profile without memory omits the artifact; serving zero rows would fake the with/without ablation");
|
|
45
|
+
const byId = /* @__PURE__ */ new Map();
|
|
46
|
+
for (const item of opts.items) {
|
|
47
|
+
if (byId.has(item.id)) throw new ValidationError(`createMemoryToolServer: duplicate memory item id '${item.id}'`);
|
|
48
|
+
byId.set(item.id, item);
|
|
49
|
+
}
|
|
50
|
+
const items = [...byId.values()];
|
|
51
|
+
const defaultK = opts.defaultK ?? 5;
|
|
52
|
+
const logPath = opts.logPath;
|
|
53
|
+
if (logPath) mkdirSync(dirname(logPath), { recursive: true });
|
|
54
|
+
const search = {
|
|
55
|
+
name: "memory_search",
|
|
56
|
+
description: "Search the agent memory of lessons learned from prior runs. Give what you are about to do or the problem you face; returns the top-k lessons ranked by relevance. Consult it BEFORE repeating work a prior run already learned from.",
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: "object",
|
|
59
|
+
properties: {
|
|
60
|
+
query: {
|
|
61
|
+
type: "string",
|
|
62
|
+
description: "The task/problem at hand — matched against lesson text and tags."
|
|
63
|
+
},
|
|
64
|
+
k: {
|
|
65
|
+
type: "number",
|
|
66
|
+
description: `Max results (default ${defaultK}).`
|
|
67
|
+
},
|
|
68
|
+
tags: {
|
|
69
|
+
type: "array",
|
|
70
|
+
items: { type: "string" },
|
|
71
|
+
description: "Only return items carrying at least one of these tags."
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
required: ["query"]
|
|
75
|
+
},
|
|
76
|
+
handler: async (raw) => {
|
|
77
|
+
const args = raw ?? {};
|
|
78
|
+
if (typeof args.query !== "string" || args.query.trim().length === 0) throw new TypeError("memory_search: query must be a non-empty string");
|
|
79
|
+
const k = args.k === void 0 ? defaultK : Math.floor(Number(args.k));
|
|
80
|
+
if (!Number.isFinite(k) || k < 1) throw new TypeError("memory_search: k must be a positive integer");
|
|
81
|
+
let tagFilter;
|
|
82
|
+
if (args.tags !== void 0) {
|
|
83
|
+
if (!Array.isArray(args.tags) || args.tags.some((t) => typeof t !== "string")) throw new TypeError("memory_search: tags must be an array of strings");
|
|
84
|
+
tagFilter = args.tags;
|
|
85
|
+
}
|
|
86
|
+
const queryTokens = tokenize(args.query);
|
|
87
|
+
const results = (tagFilter ? items.filter((i) => (i.tags ?? []).some((t) => tagFilter.includes(t))) : items).map((item) => ({
|
|
88
|
+
item,
|
|
89
|
+
score: scoreItem(queryTokens, item)
|
|
90
|
+
})).filter((r) => r.score > 0).sort((a, b) => b.score - a.score).slice(0, k).map(({ item, score }) => ({
|
|
91
|
+
id: item.id,
|
|
92
|
+
text: item.text,
|
|
93
|
+
score: Number(score.toFixed(4)),
|
|
94
|
+
...item.tags ? { tags: item.tags } : {},
|
|
95
|
+
...item.source ? { source: item.source } : {}
|
|
96
|
+
}));
|
|
97
|
+
if (logPath) appendFileSync(logPath, `${JSON.stringify({
|
|
98
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
99
|
+
query: args.query,
|
|
100
|
+
k,
|
|
101
|
+
returned: results.map((r) => ({
|
|
102
|
+
id: r.id,
|
|
103
|
+
score: r.score
|
|
104
|
+
}))
|
|
105
|
+
})}\n`);
|
|
106
|
+
return {
|
|
107
|
+
query: args.query,
|
|
108
|
+
results
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
return createStdioToolServer({
|
|
113
|
+
serverName: opts.serverName ?? "agent-memory",
|
|
114
|
+
serverVersion: opts.serverVersion ?? "0",
|
|
115
|
+
tools: [search, {
|
|
116
|
+
name: "memory_get",
|
|
117
|
+
description: "Fetch one memory item verbatim by its id (ids come from memory_search results).",
|
|
118
|
+
inputSchema: {
|
|
119
|
+
type: "object",
|
|
120
|
+
properties: { id: {
|
|
121
|
+
type: "string",
|
|
122
|
+
description: "The memory item id."
|
|
123
|
+
} },
|
|
124
|
+
required: ["id"]
|
|
125
|
+
},
|
|
126
|
+
handler: async (raw) => {
|
|
127
|
+
const args = raw ?? {};
|
|
128
|
+
if (typeof args.id !== "string" || args.id.trim().length === 0) throw new TypeError("memory_get: id must be a non-empty string");
|
|
129
|
+
const item = byId.get(args.id);
|
|
130
|
+
if (!item) throw new Error(`memory_get: no memory item with id '${args.id}'`);
|
|
131
|
+
return item;
|
|
132
|
+
}
|
|
133
|
+
}]
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/** Coerce an untrusted JSON array into validated `MemoryItem` rows. */
|
|
137
|
+
function parseMemoryItems(value, source) {
|
|
138
|
+
if (!Array.isArray(value)) throw new ValidationError(`${source}: expected a JSON array of memory items`);
|
|
139
|
+
return value.map((row, i) => coerceMemoryItem(row, `${source}[${i}]`));
|
|
140
|
+
}
|
|
141
|
+
/** Read a memory store file: a JSON array, or JSONL (one `MemoryItem` per line). */
|
|
142
|
+
function readMemoryItemsFile(path) {
|
|
143
|
+
let raw;
|
|
144
|
+
try {
|
|
145
|
+
raw = readFileSync(path, "utf8");
|
|
146
|
+
} catch (err) {
|
|
147
|
+
throw new ValidationError(`readMemoryItemsFile: cannot read '${path}': ${err instanceof Error ? err.message : String(err)}`);
|
|
148
|
+
}
|
|
149
|
+
const trimmed = raw.trim();
|
|
150
|
+
if (trimmed.length === 0) return [];
|
|
151
|
+
if (trimmed.startsWith("[")) {
|
|
152
|
+
let parsed;
|
|
153
|
+
try {
|
|
154
|
+
parsed = JSON.parse(trimmed);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
throw new ValidationError(`readMemoryItemsFile: '${path}' is not valid JSON: ${err.message}`);
|
|
157
|
+
}
|
|
158
|
+
return parseMemoryItems(parsed, path);
|
|
159
|
+
}
|
|
160
|
+
return trimmed.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).map((line, i) => {
|
|
161
|
+
let parsed;
|
|
162
|
+
try {
|
|
163
|
+
parsed = JSON.parse(line);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
throw new ValidationError(`readMemoryItemsFile: '${path}' line ${i + 1} is not valid JSON: ${err.message}`);
|
|
166
|
+
}
|
|
167
|
+
return coerceMemoryItem(parsed, `${path}:${i + 1}`);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Resolve the bin's memory from `AGENT_MEMORY_FILE` (durable store) and/or
|
|
172
|
+
* `AGENT_MEMORY_ITEMS` (inline JSON rows; wins on id collision). Zero rows is
|
|
173
|
+
* a boot FAILURE, matching the fail-closed materialization discipline.
|
|
174
|
+
*/
|
|
175
|
+
function resolveMemoryFromEnv(env) {
|
|
176
|
+
const filePath = env[MEMORY_FILE_ENV];
|
|
177
|
+
const inlineRaw = env[MEMORY_ITEMS_ENV];
|
|
178
|
+
const fromFile = filePath ? readMemoryItemsFile(filePath) : [];
|
|
179
|
+
let inline = [];
|
|
180
|
+
if (inlineRaw) {
|
|
181
|
+
let parsed;
|
|
182
|
+
try {
|
|
183
|
+
parsed = JSON.parse(inlineRaw);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
throw new ValidationError(`${MEMORY_ITEMS_ENV} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
186
|
+
}
|
|
187
|
+
inline = parseMemoryItems(parsed, MEMORY_ITEMS_ENV);
|
|
188
|
+
}
|
|
189
|
+
const byId = /* @__PURE__ */ new Map();
|
|
190
|
+
for (const item of fromFile) byId.set(item.id, item);
|
|
191
|
+
for (const item of inline) byId.set(item.id, item);
|
|
192
|
+
if (byId.size === 0) throw new ValidationError(`memory bin: no memory items — set ${MEMORY_FILE_ENV} and/or ${MEMORY_ITEMS_ENV}; an EMPTY memory must never be served (a profile without memory omits the artifact)`);
|
|
193
|
+
const serverName = env[MEMORY_NAME_ENV];
|
|
194
|
+
const logPath = env[MEMORY_LOG_ENV];
|
|
195
|
+
return {
|
|
196
|
+
items: [...byId.values()],
|
|
197
|
+
...serverName ? { serverName } : {},
|
|
198
|
+
...logPath ? { logPath } : {}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function coerceMemoryItem(row, at) {
|
|
202
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) throw new ValidationError(`${at}: memory item must be an object`);
|
|
203
|
+
const r = row;
|
|
204
|
+
if (typeof r.id !== "string" || r.id.trim().length === 0) throw new ValidationError(`${at}: 'id' must be a non-empty string`);
|
|
205
|
+
if (typeof r.text !== "string" || r.text.trim().length === 0) throw new ValidationError(`${at}: 'text' must be a non-empty string`);
|
|
206
|
+
let tags;
|
|
207
|
+
if (r.tags !== void 0) {
|
|
208
|
+
if (!Array.isArray(r.tags) || r.tags.some((t) => typeof t !== "string")) throw new ValidationError(`${at}: 'tags' must be an array of strings`);
|
|
209
|
+
tags = r.tags;
|
|
210
|
+
}
|
|
211
|
+
if (r.source !== void 0 && typeof r.source !== "string") throw new ValidationError(`${at}: 'source' must be a string`);
|
|
212
|
+
return {
|
|
213
|
+
id: r.id,
|
|
214
|
+
text: r.text,
|
|
215
|
+
...tags ? { tags } : {},
|
|
216
|
+
...typeof r.source === "string" ? { source: r.source } : {}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/** Unique lowercase alphanumeric tokens of length >= 2. */
|
|
220
|
+
function tokenize(text) {
|
|
221
|
+
return [...new Set(text.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2))];
|
|
222
|
+
}
|
|
223
|
+
/** Fraction of query tokens present in the item's text + tags (0..1). */
|
|
224
|
+
function scoreItem(queryTokens, item) {
|
|
225
|
+
if (queryTokens.length === 0) return 0;
|
|
226
|
+
const hay = new Set(tokenize(`${item.text} ${(item.tags ?? []).join(" ")}`));
|
|
227
|
+
let hit = 0;
|
|
228
|
+
for (const t of queryTokens) if (hay.has(t)) hit += 1;
|
|
229
|
+
return hit / queryTokens.length;
|
|
230
|
+
}
|
|
231
|
+
//#endregion
|
|
232
|
+
export { createMemoryToolServer as a, resolveMemoryFromEnv as c, MEMORY_NAME_ENV as i, MEMORY_ITEMS_ENV as n, parseMemoryItems as o, MEMORY_LOG_ENV as r, readMemoryItemsFile as s, MEMORY_FILE_ENV as t };
|
|
233
|
+
|
|
234
|
+
//# sourceMappingURL=memory-server-DL6cE2Ag.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-server-DL6cE2Ag.js","names":[],"sources":["../src/mcp/memory-server.ts"],"sourcesContent":["/**\n * Memory MCP server — the LIVE serving half of the `memory` profile surface\n * (Phase 5). A curated memory (lessons distilled from prior runs) is only\n * real if the DRIVEN agent can query it mid-task; this module serves a flat\n * store of `MemoryItem` rows as `memory_search` / `memory_get` tools over the\n * ONE in-repo stdio JSON-RPC core (`createStdioToolServer`), so the exact\n * wire protocol the same-host client (`connectStdioMcp` /\n * `materializeLocalMcp`, runtime/stdio-mcp-client.ts) speaks is guaranteed by\n * construction — serve and consume cannot drift.\n *\n * Retrieval is DETERMINISTIC lexical overlap (no vectors, no LLM): a lift\n * measured with this memory mounted is attributable to the lessons\n * themselves, never to retrieval-model noise. Every\n * `memory_search` can append one JSONL row to a retrieval log (`logPath`) —\n * the per-query record an off-policy retrieval estimator (agent-knowledge's\n * `RetrievalHoldout`) consumes. agent-knowledge is NOT a dependency of this\n * repo, so the log file is the flagged cross-package seam, not an import.\n *\n * Fail-loud discipline (mirrors `materializeLocalMcp`): an EMPTY memory is\n * never served — a profile without memory simply omits the artifact, and\n * silently serving zero rows would fake the with/without ablation.\n *\n * @experimental\n */\n\nimport { appendFileSync, mkdirSync, readFileSync } from 'node:fs'\nimport { dirname } from 'node:path'\nimport type { AgentProfileMcpServer } from '@tangle-network/agent-interface'\nimport { ValidationError } from '../errors'\nimport { createStdioToolServer, type McpToolDescriptor, type StdioToolServer } from './tool-server'\n\n/** One row of agent memory: a crisp lesson/fact with provenance. */\nexport interface MemoryItem {\n /** Stable id (content-hash by convention; see `memoryArtifactFromLessons`). */\n id: string\n /** The lesson itself — one imperative or observation the agent should recall. */\n text: string\n /** Optional retrieval tags, matched by `memory_search` alongside the text. */\n tags?: string[]\n /** Provenance: the finding / trace / curation pass this row came from. */\n source?: string\n}\n\n/**\n * The `memory` artifact payload — HOW a profile's memory is stored and served:\n *\n * - `store: 'file'` — served by the in-repo memory bin\n * (`agent-runtime-memory-mcp`, src/mcp/memory-bin.ts): rows load from\n * `path` (a JSON array or JSONL file of `MemoryItem`) and/or the inline\n * `items` seed (inline wins on id collision). At least one of\n * `path`/`items` is required.\n * - `store: 'mcp'` — an EXTERNAL, already-runnable MCP server that exposes\n * the memory tools itself; `server` is required and mounts verbatim.\n *\n * `logPath` makes the served memory append one JSONL row per `memory_search`\n * — the retrieval log a holdout estimator reads (see module doc).\n */\nexport interface AgentMemorySpec {\n store: 'file' | 'mcp'\n /** `store:'file'` — host path to the durable row store (JSON array or JSONL). */\n path?: string\n /** Inline seed rows, served alongside (and winning over) `path` rows. */\n items?: MemoryItem[]\n /** `store:'mcp'` — the external server that already serves memory tools. */\n server?: AgentProfileMcpServer\n /** JSONL retrieval log: one row per `memory_search` (ts, query, k, returned). */\n logPath?: string\n}\n\n/** Env var naming the durable row store file the memory bin loads (the\n * `memoryMcpServer` ↔ memory-bin contract). */\nexport const MEMORY_FILE_ENV = 'AGENT_MEMORY_FILE'\n/** Env var carrying inline JSON `MemoryItem` rows (win over file rows on id). */\nexport const MEMORY_ITEMS_ENV = 'AGENT_MEMORY_ITEMS'\n/** Env var naming the JSONL retrieval log (one row per `memory_search`). */\nexport const MEMORY_LOG_ENV = 'AGENT_MEMORY_LOG'\n/** Env var overriding the served display name (default 'agent-memory'). */\nexport const MEMORY_NAME_ENV = 'AGENT_MEMORY_NAME'\n\nexport interface CreateMemoryToolServerOptions {\n /** The rows to serve. MUST be non-empty (an empty memory is never served). */\n items: readonly MemoryItem[]\n /** Server display name surfaced via `initialize`. Default 'agent-memory'. */\n serverName?: string\n /** Server version surfaced via `initialize`. Default '0'. */\n serverVersion?: string\n /** Default result count for `memory_search`. Default 5. */\n defaultK?: number\n /** Append one JSONL row per `memory_search` (the retrieval-holdout seam). */\n logPath?: string\n}\n\n/**\n * Build the memory MCP server: `memory_search` (lexical top-k over the rows)\n * and `memory_get` (one row by id) on the generic stdio JSON-RPC core.\n */\nexport function createMemoryToolServer(opts: CreateMemoryToolServerOptions): StdioToolServer {\n if (opts.items.length === 0) {\n throw new ValidationError(\n 'createMemoryToolServer: refusing to serve an EMPTY memory — a profile without memory omits the artifact; serving zero rows would fake the with/without ablation',\n )\n }\n const byId = new Map<string, MemoryItem>()\n for (const item of opts.items) {\n if (byId.has(item.id)) {\n throw new ValidationError(`createMemoryToolServer: duplicate memory item id '${item.id}'`)\n }\n byId.set(item.id, item)\n }\n const items = [...byId.values()]\n const defaultK = opts.defaultK ?? 5\n const logPath = opts.logPath\n if (logPath) mkdirSync(dirname(logPath), { recursive: true })\n\n const search: McpToolDescriptor = {\n name: 'memory_search',\n description:\n 'Search the agent memory of lessons learned from prior runs. Give what you are about to do or the problem you face; returns the top-k lessons ranked by relevance. Consult it BEFORE repeating work a prior run already learned from.',\n inputSchema: {\n type: 'object',\n properties: {\n query: {\n type: 'string',\n description: 'The task/problem at hand — matched against lesson text and tags.',\n },\n k: { type: 'number', description: `Max results (default ${defaultK}).` },\n tags: {\n type: 'array',\n items: { type: 'string' },\n description: 'Only return items carrying at least one of these tags.',\n },\n },\n required: ['query'],\n },\n handler: async (raw) => {\n const args = (raw ?? {}) as { query?: unknown; k?: unknown; tags?: unknown }\n if (typeof args.query !== 'string' || args.query.trim().length === 0) {\n throw new TypeError('memory_search: query must be a non-empty string')\n }\n const k = args.k === undefined ? defaultK : Math.floor(Number(args.k))\n if (!Number.isFinite(k) || k < 1) {\n throw new TypeError('memory_search: k must be a positive integer')\n }\n let tagFilter: string[] | undefined\n if (args.tags !== undefined) {\n if (!Array.isArray(args.tags) || args.tags.some((t) => typeof t !== 'string')) {\n throw new TypeError('memory_search: tags must be an array of strings')\n }\n tagFilter = args.tags as string[]\n }\n const queryTokens = tokenize(args.query)\n const pool = tagFilter\n ? items.filter((i) => (i.tags ?? []).some((t) => tagFilter.includes(t)))\n : items\n const results = pool\n .map((item) => ({ item, score: scoreItem(queryTokens, item) }))\n .filter((r) => r.score > 0)\n // Highest score first; stable on ties (curation order is the tiebreak).\n .sort((a, b) => b.score - a.score)\n .slice(0, k)\n .map(({ item, score }) => ({\n id: item.id,\n text: item.text,\n score: Number(score.toFixed(4)),\n ...(item.tags ? { tags: item.tags } : {}),\n ...(item.source ? { source: item.source } : {}),\n }))\n if (logPath) {\n // The retrieval log: what was asked, what came back, at what rank —\n // exactly the per-query record an off-policy estimator needs.\n appendFileSync(\n logPath,\n `${JSON.stringify({\n ts: new Date().toISOString(),\n query: args.query,\n k,\n returned: results.map((r) => ({ id: r.id, score: r.score })),\n })}\\n`,\n )\n }\n return { query: args.query, results }\n },\n }\n\n const get: McpToolDescriptor = {\n name: 'memory_get',\n description: 'Fetch one memory item verbatim by its id (ids come from memory_search results).',\n inputSchema: {\n type: 'object',\n properties: { id: { type: 'string', description: 'The memory item id.' } },\n required: ['id'],\n },\n handler: async (raw) => {\n const args = (raw ?? {}) as { id?: unknown }\n if (typeof args.id !== 'string' || args.id.trim().length === 0) {\n throw new TypeError('memory_get: id must be a non-empty string')\n }\n const item = byId.get(args.id)\n if (!item) throw new Error(`memory_get: no memory item with id '${args.id}'`)\n return item\n },\n }\n\n return createStdioToolServer({\n serverName: opts.serverName ?? 'agent-memory',\n serverVersion: opts.serverVersion ?? '0',\n tools: [search, get],\n })\n}\n\n/** Coerce an untrusted JSON array into validated `MemoryItem` rows. */\nexport function parseMemoryItems(value: unknown, source: string): MemoryItem[] {\n if (!Array.isArray(value)) {\n throw new ValidationError(`${source}: expected a JSON array of memory items`)\n }\n return value.map((row, i) => coerceMemoryItem(row, `${source}[${i}]`))\n}\n\n/** Read a memory store file: a JSON array, or JSONL (one `MemoryItem` per line). */\nexport function readMemoryItemsFile(path: string): MemoryItem[] {\n let raw: string\n try {\n raw = readFileSync(path, 'utf8')\n } catch (err) {\n throw new ValidationError(\n `readMemoryItemsFile: cannot read '${path}': ${err instanceof Error ? err.message : String(err)}`,\n )\n }\n const trimmed = raw.trim()\n if (trimmed.length === 0) return []\n if (trimmed.startsWith('[')) {\n let parsed: unknown\n try {\n parsed = JSON.parse(trimmed)\n } catch (err) {\n throw new ValidationError(\n `readMemoryItemsFile: '${path}' is not valid JSON: ${(err as Error).message}`,\n )\n }\n return parseMemoryItems(parsed, path)\n }\n return trimmed\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0)\n .map((line, i) => {\n let parsed: unknown\n try {\n parsed = JSON.parse(line)\n } catch (err) {\n throw new ValidationError(\n `readMemoryItemsFile: '${path}' line ${i + 1} is not valid JSON: ${(err as Error).message}`,\n )\n }\n return coerceMemoryItem(parsed, `${path}:${i + 1}`)\n })\n}\n\n/** What the memory bin resolved from its environment. */\nexport interface ResolvedMemoryEnv {\n items: MemoryItem[]\n serverName?: string\n logPath?: string\n}\n\n/**\n * Resolve the bin's memory from `AGENT_MEMORY_FILE` (durable store) and/or\n * `AGENT_MEMORY_ITEMS` (inline JSON rows; wins on id collision). Zero rows is\n * a boot FAILURE, matching the fail-closed materialization discipline.\n */\nexport function resolveMemoryFromEnv(env: Record<string, string | undefined>): ResolvedMemoryEnv {\n const filePath = env[MEMORY_FILE_ENV]\n const inlineRaw = env[MEMORY_ITEMS_ENV]\n const fromFile = filePath ? readMemoryItemsFile(filePath) : []\n let inline: MemoryItem[] = []\n if (inlineRaw) {\n let parsed: unknown\n try {\n parsed = JSON.parse(inlineRaw)\n } catch (err) {\n throw new ValidationError(\n `${MEMORY_ITEMS_ENV} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,\n )\n }\n inline = parseMemoryItems(parsed, MEMORY_ITEMS_ENV)\n }\n const byId = new Map<string, MemoryItem>()\n for (const item of fromFile) byId.set(item.id, item)\n for (const item of inline) byId.set(item.id, item)\n if (byId.size === 0) {\n throw new ValidationError(\n `memory bin: no memory items — set ${MEMORY_FILE_ENV} and/or ${MEMORY_ITEMS_ENV}; an EMPTY memory must never be served (a profile without memory omits the artifact)`,\n )\n }\n const serverName = env[MEMORY_NAME_ENV]\n const logPath = env[MEMORY_LOG_ENV]\n return {\n items: [...byId.values()],\n ...(serverName ? { serverName } : {}),\n ...(logPath ? { logPath } : {}),\n }\n}\n\nfunction coerceMemoryItem(row: unknown, at: string): MemoryItem {\n if (!row || typeof row !== 'object' || Array.isArray(row)) {\n throw new ValidationError(`${at}: memory item must be an object`)\n }\n const r = row as Record<string, unknown>\n if (typeof r.id !== 'string' || r.id.trim().length === 0) {\n throw new ValidationError(`${at}: 'id' must be a non-empty string`)\n }\n if (typeof r.text !== 'string' || r.text.trim().length === 0) {\n throw new ValidationError(`${at}: 'text' must be a non-empty string`)\n }\n let tags: string[] | undefined\n if (r.tags !== undefined) {\n if (!Array.isArray(r.tags) || r.tags.some((t) => typeof t !== 'string')) {\n throw new ValidationError(`${at}: 'tags' must be an array of strings`)\n }\n tags = r.tags as string[]\n }\n if (r.source !== undefined && typeof r.source !== 'string') {\n throw new ValidationError(`${at}: 'source' must be a string`)\n }\n return {\n id: r.id,\n text: r.text,\n ...(tags ? { tags } : {}),\n ...(typeof r.source === 'string' ? { source: r.source } : {}),\n }\n}\n\n/** Unique lowercase alphanumeric tokens of length >= 2. */\nfunction tokenize(text: string): string[] {\n return [\n ...new Set(\n text\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((t) => t.length >= 2),\n ),\n ]\n}\n\n/** Fraction of query tokens present in the item's text + tags (0..1). */\nfunction scoreItem(queryTokens: readonly string[], item: MemoryItem): number {\n if (queryTokens.length === 0) return 0\n const hay = new Set(tokenize(`${item.text} ${(item.tags ?? []).join(' ')}`))\n let hit = 0\n for (const t of queryTokens) if (hay.has(t)) hit += 1\n return hit / queryTokens.length\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
//#region src/model-resolution.ts
|
|
2
|
+
/** Default Tangle Router base URL used when no env override is set. */
|
|
3
|
+
const DEFAULT_ROUTER_BASE_URL = "https://router.tangle.tools";
|
|
4
|
+
/** Resolve the router base URL from env, normalised — no trailing `/v1` or `/`. */
|
|
5
|
+
function resolveRouterBaseUrl(env = {}) {
|
|
6
|
+
return (env.TANGLE_ROUTER_URL ?? env.TANGLE_ROUTER_BASE_URL ?? "https://router.tangle.tools").replace(/\/v1\/?$/, "").replace(/\/$/, "");
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Fetch the model catalog from the router's `/v1/models`. Throws on a non-2xx
|
|
10
|
+
* response — callers decide whether to fail open (empty catalog) or closed.
|
|
11
|
+
*/
|
|
12
|
+
async function getModels(routerBaseUrl = DEFAULT_ROUTER_BASE_URL) {
|
|
13
|
+
const res = await fetch(`${routerBaseUrl}/v1/models`, { headers: { Accept: "application/json" } });
|
|
14
|
+
if (!res.ok) throw new Error(`router /v1/models ${res.status}`);
|
|
15
|
+
const body = await res.json();
|
|
16
|
+
return Array.isArray(body.data) ? body.data : [];
|
|
17
|
+
}
|
|
18
|
+
/** Trim a candidate model id; `undefined` for non-strings and blanks. */
|
|
19
|
+
function cleanModelId(value) {
|
|
20
|
+
if (typeof value !== "string") return void 0;
|
|
21
|
+
const trimmed = value.trim();
|
|
22
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolve a chat model by precedence: the first candidate carrying a
|
|
26
|
+
* non-blank model wins, else `fallback`. The caller owns the precedence
|
|
27
|
+
* order, so each product keeps its own policy (request → workspace → env,
|
|
28
|
+
* etc.) while the first-non-blank logic and the telemetry shape stay shared.
|
|
29
|
+
*/
|
|
30
|
+
function resolveChatModel(candidates, fallback) {
|
|
31
|
+
for (const candidate of candidates) {
|
|
32
|
+
const model = cleanModelId(candidate.model);
|
|
33
|
+
if (model) return {
|
|
34
|
+
source: candidate.source,
|
|
35
|
+
model
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return fallback;
|
|
39
|
+
}
|
|
40
|
+
const WELL_FORMED_MODEL_ID = /^[A-Za-z0-9._/@:-]+$/;
|
|
41
|
+
function isWellFormedModelId(modelId) {
|
|
42
|
+
return modelId.length <= 200 && WELL_FORMED_MODEL_ID.test(modelId);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Every id a catalog entry can be addressed by — its bare id, plus a
|
|
46
|
+
* `provider/id` form when the router exposes a separate provider slug.
|
|
47
|
+
*/
|
|
48
|
+
function catalogIdsForModel(model) {
|
|
49
|
+
const ids = /* @__PURE__ */ new Set();
|
|
50
|
+
const id = cleanModelId(model.id);
|
|
51
|
+
if (id) ids.add(id);
|
|
52
|
+
const provider = cleanModelId(model._provider) ?? cleanModelId(model.provider);
|
|
53
|
+
if (provider && id && !id.includes("/")) ids.add(`${provider}/${id}`);
|
|
54
|
+
return [...ids];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Validate a caller-supplied chat-model id. Rejects non-strings, malformed
|
|
58
|
+
* ids, and ids absent from both the caller's `allowlist` and the live router
|
|
59
|
+
* catalog. Fails closed: when the catalog cannot be fetched, an unverifiable
|
|
60
|
+
* id is rejected rather than admitted — a bad model never reaches the agent.
|
|
61
|
+
*/
|
|
62
|
+
async function validateChatModelId(modelId, options = {}) {
|
|
63
|
+
const { allowlist = [], routerBaseUrl = DEFAULT_ROUTER_BASE_URL, loadModels = getModels } = options;
|
|
64
|
+
const cleaned = cleanModelId(modelId);
|
|
65
|
+
if (!cleaned) return {
|
|
66
|
+
succeeded: false,
|
|
67
|
+
error: "Model id must be a non-empty string."
|
|
68
|
+
};
|
|
69
|
+
if (!isWellFormedModelId(cleaned)) return {
|
|
70
|
+
succeeded: false,
|
|
71
|
+
error: `Model id is malformed: ${cleaned}`
|
|
72
|
+
};
|
|
73
|
+
if (allowlist.some((id) => cleanModelId(id) === cleaned)) return {
|
|
74
|
+
succeeded: true,
|
|
75
|
+
value: cleaned
|
|
76
|
+
};
|
|
77
|
+
let catalog;
|
|
78
|
+
try {
|
|
79
|
+
catalog = await loadModels(routerBaseUrl);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return {
|
|
82
|
+
succeeded: false,
|
|
83
|
+
error: `Could not validate model catalog: ${err instanceof Error ? err.message : String(err)}`
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (!new Set(catalog.flatMap(catalogIdsForModel)).has(cleaned)) return {
|
|
87
|
+
succeeded: false,
|
|
88
|
+
error: `Model is not available: ${cleaned}`
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
succeeded: true,
|
|
92
|
+
value: cleaned
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
export { resolveRouterBaseUrl as a, resolveChatModel as i, cleanModelId as n, validateChatModelId as o, getModels as r, DEFAULT_ROUTER_BASE_URL as t };
|
|
97
|
+
|
|
98
|
+
//# sourceMappingURL=model-resolution-Btd9iIKV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-resolution-Btd9iIKV.js","names":[],"sources":["../src/model-resolution.ts"],"sourcesContent":["/**\n *\n * Chat-model resolution + catalog validation — the shared primitive every\n * product chat handler needs and was, until now, hand-rolling. Lifts the\n * router `/v1/models` fetch, the fail-closed id validation, and the\n * precedence resolver out of four near-identical per-repo copies.\n *\n * Policy-free by design: callers pass their own precedence order\n * (`resolveChatModel`) and their own known-good `allowlist`\n * (`validateChatModelId`), so each product keeps its resolution policy while\n * sharing the catalog fetch, the malformed-id guard, and the fail-closed\n * admission rule. No React, no `process.env` assumption — `env` is an\n * explicit narrow record so this runs unchanged in Node and in Workers.\n *\n * @stable\n */\n\n/**\n * A model entry as returned by the Tangle Router `/v1/models` endpoint.\n * Intentionally minimal — only the fields resolution + validation read.\n */\nexport interface ModelInfo {\n id: string\n name?: string\n description?: string\n /** Provider slug, when the router exposes it (`provider` or `_provider`). */\n provider?: string\n _provider?: string\n architecture?: {\n modality?: string\n input_modalities?: string[]\n output_modalities?: string[]\n }\n}\n\n/** Env keys the router base URL is resolved from. */\nexport interface RouterEnv {\n TANGLE_ROUTER_URL?: string\n TANGLE_ROUTER_BASE_URL?: string\n}\n\n/** Default Tangle Router base URL used when no env override is set. */\nexport const DEFAULT_ROUTER_BASE_URL = 'https://router.tangle.tools'\n\n/** Resolve the router base URL from env, normalised — no trailing `/v1` or `/`. */\nexport function resolveRouterBaseUrl(env: RouterEnv = {}): string {\n return (env.TANGLE_ROUTER_URL ?? env.TANGLE_ROUTER_BASE_URL ?? DEFAULT_ROUTER_BASE_URL)\n .replace(/\\/v1\\/?$/, '')\n .replace(/\\/$/, '')\n}\n\n/**\n * Fetch the model catalog from the router's `/v1/models`. Throws on a non-2xx\n * response — callers decide whether to fail open (empty catalog) or closed.\n */\nexport async function getModels(\n routerBaseUrl: string = DEFAULT_ROUTER_BASE_URL,\n): Promise<ModelInfo[]> {\n const res = await fetch(`${routerBaseUrl}/v1/models`, {\n headers: { Accept: 'application/json' },\n })\n if (!res.ok) throw new Error(`router /v1/models ${res.status}`)\n const body = (await res.json()) as { data?: ModelInfo[] }\n return Array.isArray(body.data) ? body.data : []\n}\n\n/** Trim a candidate model id; `undefined` for non-strings and blanks. */\nexport function cleanModelId(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\nexport interface ChatModelCandidate {\n /** Stable label for telemetry — e.g. `request`, `workspace`, `env`. */\n source: string\n model: string | undefined\n}\n\nexport interface ResolvedChatModel {\n source: string\n model: string\n}\n\n/**\n * Resolve a chat model by precedence: the first candidate carrying a\n * non-blank model wins, else `fallback`. The caller owns the precedence\n * order, so each product keeps its own policy (request → workspace → env,\n * etc.) while the first-non-blank logic and the telemetry shape stay shared.\n */\nexport function resolveChatModel(\n candidates: ChatModelCandidate[],\n fallback: ResolvedChatModel,\n): ResolvedChatModel {\n for (const candidate of candidates) {\n const model = cleanModelId(candidate.model)\n if (model) return { source: candidate.source, model }\n }\n return fallback\n}\n\nexport type ChatModelValidation =\n | { succeeded: true; value: string }\n | { succeeded: false; error: string }\n\nconst WELL_FORMED_MODEL_ID = /^[A-Za-z0-9._/@:-]+$/\n\nfunction isWellFormedModelId(modelId: string): boolean {\n return modelId.length <= 200 && WELL_FORMED_MODEL_ID.test(modelId)\n}\n\n/**\n * Every id a catalog entry can be addressed by — its bare id, plus a\n * `provider/id` form when the router exposes a separate provider slug.\n */\nfunction catalogIdsForModel(model: ModelInfo): string[] {\n const ids = new Set<string>()\n const id = cleanModelId(model.id)\n if (id) ids.add(id)\n const provider = cleanModelId(model._provider) ?? cleanModelId(model.provider)\n if (provider && id && !id.includes('/')) ids.add(`${provider}/${id}`)\n return [...ids]\n}\n\n/**\n * Validate a caller-supplied chat-model id. Rejects non-strings, malformed\n * ids, and ids absent from both the caller's `allowlist` and the live router\n * catalog. Fails closed: when the catalog cannot be fetched, an unverifiable\n * id is rejected rather than admitted — a bad model never reaches the agent.\n */\nexport async function validateChatModelId(\n modelId: unknown,\n options: {\n /**\n * Known-good ids that skip the catalog round trip — e.g. the product's\n * default model plus any env-configured ids.\n */\n allowlist?: string[]\n routerBaseUrl?: string\n /** Injectable catalog loader — overridden in tests. */\n loadModels?: (routerBaseUrl: string) => Promise<ModelInfo[]>\n } = {},\n): Promise<ChatModelValidation> {\n const {\n allowlist = [],\n routerBaseUrl = DEFAULT_ROUTER_BASE_URL,\n loadModels = getModels,\n } = options\n\n const cleaned = cleanModelId(modelId)\n if (!cleaned) return { succeeded: false, error: 'Model id must be a non-empty string.' }\n if (!isWellFormedModelId(cleaned)) {\n return { succeeded: false, error: `Model id is malformed: ${cleaned}` }\n }\n if (allowlist.some((id) => cleanModelId(id) === cleaned)) {\n return { succeeded: true, value: cleaned }\n }\n\n let catalog: ModelInfo[]\n try {\n catalog = await loadModels(routerBaseUrl)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n return { succeeded: false, error: `Could not validate model catalog: ${message}` }\n }\n\n const ids = new Set(catalog.flatMap(catalogIdsForModel))\n if (!ids.has(cleaned)) return { succeeded: false, error: `Model is not available: ${cleaned}` }\n return { succeeded: true, value: cleaned }\n}\n"],"mappings":";;AA0CA,MAAa,0BAA0B;;AAGvC,SAAgB,qBAAqB,MAAiB,CAAC,GAAW;CAChE,QAAQ,IAAI,qBAAqB,IAAI,0BAAA,8BAAA,CAClC,QAAQ,YAAY,EAAE,CAAC,CACvB,QAAQ,OAAO,EAAE;AACtB;;;;;AAMA,eAAsB,UACpB,gBAAwB,yBACF;CACtB,MAAM,MAAM,MAAM,MAAM,GAAG,cAAc,aAAa,EACpD,SAAS,EAAE,QAAQ,mBAAmB,EACxC,CAAC;CACD,IAAI,CAAC,IAAI,IAAI,MAAM,IAAI,MAAM,qBAAqB,IAAI,QAAQ;CAC9D,MAAM,OAAQ,MAAM,IAAI,KAAK;CAC7B,OAAO,MAAM,QAAQ,KAAK,IAAI,IAAI,KAAK,OAAO,CAAC;AACjD;;AAGA,SAAgB,aAAa,OAAoC;CAC/D,IAAI,OAAO,UAAU,UAAU,OAAO,KAAA;CACtC,MAAM,UAAU,MAAM,KAAK;CAC3B,OAAO,QAAQ,SAAS,IAAI,UAAU,KAAA;AACxC;;;;;;;AAmBA,SAAgB,iBACd,YACA,UACmB;CACnB,KAAK,MAAM,aAAa,YAAY;EAClC,MAAM,QAAQ,aAAa,UAAU,KAAK;EAC1C,IAAI,OAAO,OAAO;GAAE,QAAQ,UAAU;GAAQ;EAAM;CACtD;CACA,OAAO;AACT;AAMA,MAAM,uBAAuB;AAE7B,SAAS,oBAAoB,SAA0B;CACrD,OAAO,QAAQ,UAAU,OAAO,qBAAqB,KAAK,OAAO;AACnE;;;;;AAMA,SAAS,mBAAmB,OAA4B;CACtD,MAAM,sBAAM,IAAI,IAAY;CAC5B,MAAM,KAAK,aAAa,MAAM,EAAE;CAChC,IAAI,IAAI,IAAI,IAAI,EAAE;CAClB,MAAM,WAAW,aAAa,MAAM,SAAS,KAAK,aAAa,MAAM,QAAQ;CAC7E,IAAI,YAAY,MAAM,CAAC,GAAG,SAAS,GAAG,GAAG,IAAI,IAAI,GAAG,SAAS,GAAG,IAAI;CACpE,OAAO,CAAC,GAAG,GAAG;AAChB;;;;;;;AAQA,eAAsB,oBACpB,SACA,UASI,CAAC,GACyB;CAC9B,MAAM,EACJ,YAAY,CAAC,GACb,gBAAgB,yBAChB,aAAa,cACX;CAEJ,MAAM,UAAU,aAAa,OAAO;CACpC,IAAI,CAAC,SAAS,OAAO;EAAE,WAAW;EAAO,OAAO;CAAuC;CACvF,IAAI,CAAC,oBAAoB,OAAO,GAC9B,OAAO;EAAE,WAAW;EAAO,OAAO,0BAA0B;CAAU;CAExE,IAAI,UAAU,MAAM,OAAO,aAAa,EAAE,MAAM,OAAO,GACrD,OAAO;EAAE,WAAW;EAAM,OAAO;CAAQ;CAG3C,IAAI;CACJ,IAAI;EACF,UAAU,MAAM,WAAW,aAAa;CAC1C,SAAS,KAAK;EAEZ,OAAO;GAAE,WAAW;GAAO,OAAO,qCADlB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;EACkB;CACnF;CAGA,IAAI,CAAC,IADW,IAAI,QAAQ,QAAQ,kBAAkB,CAC/C,CAAC,CAAC,IAAI,OAAO,GAAG,OAAO;EAAE,WAAW;EAAO,OAAO,2BAA2B;CAAU;CAC9F,OAAO;EAAE,WAAW;EAAM,OAAO;CAAQ;AAC3C"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { h as OpenAIChatTool } from "./types-C9j4qg6l.js";
|
|
2
|
+
//#region src/mcp/openai-tools.d.ts
|
|
3
|
+
/**
|
|
4
|
+
*
|
|
5
|
+
* Returns the queue-bound delegation tools projected into OpenAI Chat
|
|
6
|
+
* Completions `tools[]` shape. The order is stable: `delegate_feedback`,
|
|
7
|
+
* `delegation_status`, `delegation_history`.
|
|
8
|
+
*
|
|
9
|
+
* @experimental
|
|
10
|
+
*/
|
|
11
|
+
declare function mcpToolsForRuntimeMcp(): OpenAIChatTool[];
|
|
12
|
+
/**
|
|
13
|
+
*
|
|
14
|
+
* Subset filter — return only the projected tools whose `function.name`
|
|
15
|
+
* appears in `names`. Useful for curated mounts (e.g. only the queue-bound
|
|
16
|
+
* delegation tools, omitting `delegate_feedback`). Unknown names are
|
|
17
|
+
* silently ignored; pass an empty array to get an empty result.
|
|
18
|
+
*
|
|
19
|
+
* @experimental
|
|
20
|
+
*/
|
|
21
|
+
declare function mcpToolsForRuntimeMcpSubset(names: ReadonlyArray<string>): OpenAIChatTool[];
|
|
22
|
+
//#endregion
|
|
23
|
+
export { mcpToolsForRuntimeMcpSubset as n, mcpToolsForRuntimeMcp as t };
|
|
24
|
+
//# sourceMappingURL=openai-tools-B68JaOCx.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { C as DELEGATE_FEEDBACK_INPUT_SCHEMA, S as DELEGATE_FEEDBACK_DESCRIPTION, c as DELEGATION_STATUS_INPUT_SCHEMA, f as DELEGATION_HISTORY_DESCRIPTION, l as DELEGATION_STATUS_TOOL_NAME, m as DELEGATION_HISTORY_TOOL_NAME, p as DELEGATION_HISTORY_INPUT_SCHEMA, s as DELEGATION_STATUS_DESCRIPTION, w as DELEGATE_FEEDBACK_TOOL_NAME } from "./supervise-BiRutHS9.js";
|
|
2
|
+
//#region src/mcp/openai-tools.ts
|
|
3
|
+
function buildTool(name, description, parameters) {
|
|
4
|
+
return {
|
|
5
|
+
type: "function",
|
|
6
|
+
function: {
|
|
7
|
+
name,
|
|
8
|
+
description,
|
|
9
|
+
parameters: { ...parameters }
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
*
|
|
15
|
+
* Returns the queue-bound delegation tools projected into OpenAI Chat
|
|
16
|
+
* Completions `tools[]` shape. The order is stable: `delegate_feedback`,
|
|
17
|
+
* `delegation_status`, `delegation_history`.
|
|
18
|
+
*
|
|
19
|
+
* @experimental
|
|
20
|
+
*/
|
|
21
|
+
function mcpToolsForRuntimeMcp() {
|
|
22
|
+
return [
|
|
23
|
+
buildTool(DELEGATE_FEEDBACK_TOOL_NAME, DELEGATE_FEEDBACK_DESCRIPTION, DELEGATE_FEEDBACK_INPUT_SCHEMA),
|
|
24
|
+
buildTool(DELEGATION_STATUS_TOOL_NAME, DELEGATION_STATUS_DESCRIPTION, DELEGATION_STATUS_INPUT_SCHEMA),
|
|
25
|
+
buildTool(DELEGATION_HISTORY_TOOL_NAME, DELEGATION_HISTORY_DESCRIPTION, DELEGATION_HISTORY_INPUT_SCHEMA)
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
*
|
|
30
|
+
* Subset filter — return only the projected tools whose `function.name`
|
|
31
|
+
* appears in `names`. Useful for curated mounts (e.g. only the queue-bound
|
|
32
|
+
* delegation tools, omitting `delegate_feedback`). Unknown names are
|
|
33
|
+
* silently ignored; pass an empty array to get an empty result.
|
|
34
|
+
*
|
|
35
|
+
* @experimental
|
|
36
|
+
*/
|
|
37
|
+
function mcpToolsForRuntimeMcpSubset(names) {
|
|
38
|
+
const allowed = new Set(names);
|
|
39
|
+
return mcpToolsForRuntimeMcp().filter((tool) => allowed.has(tool.function.name));
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { mcpToolsForRuntimeMcpSubset as n, mcpToolsForRuntimeMcp as t };
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=openai-tools-CKLy1C7M.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openai-tools-CKLy1C7M.js","names":[],"sources":["../src/mcp/openai-tools.ts"],"sourcesContent":["/**\n *\n * OpenAI Chat Completions `tools[]` projection of the queue-bound agent-runtime\n * MCP delegation tools.\n *\n * Use when configuring `createOpenAICompatibleBackend({ tools: ... })` so the\n * model can call `delegate_feedback`, `delegation_status`, and\n * `delegation_history` through the OpenAI-compat transport (tcloud, OpenRouter,\n * OpenAI direct, cli-bridge). The runtime surfaces tool calls as `tool_call`\n * stream events — execution is the caller's responsibility (typically the\n * parent sandbox runtime's MCP mount).\n *\n * Sandbox-SDK callers do NOT need this helper: the sandbox runtime mounts\n * MCP servers natively and the in-sandbox harness discovers tools via the\n * runtime, not via an OpenAI tools array.\n *\n * Tool name + description + JSON-schema are pulled from the canonical\n * `DELEGATE_*` constants exported by `./tools/*` so the projection cannot\n * drift from the server's own validators.\n *\n * @experimental\n */\n\nimport type { OpenAIChatTool } from '../types'\nimport {\n DELEGATE_FEEDBACK_DESCRIPTION,\n DELEGATE_FEEDBACK_INPUT_SCHEMA,\n DELEGATE_FEEDBACK_TOOL_NAME,\n} from './tools/delegate-feedback'\nimport {\n DELEGATION_HISTORY_DESCRIPTION,\n DELEGATION_HISTORY_INPUT_SCHEMA,\n DELEGATION_HISTORY_TOOL_NAME,\n} from './tools/delegation-history'\nimport {\n DELEGATION_STATUS_DESCRIPTION,\n DELEGATION_STATUS_INPUT_SCHEMA,\n DELEGATION_STATUS_TOOL_NAME,\n} from './tools/delegation-status'\n\nfunction buildTool(\n name: string,\n description: string,\n parameters: Readonly<Record<string, unknown>>,\n): OpenAIChatTool {\n // `parameters` arrives as a deeply-readonly `as const` literal. The\n // OpenAI-compat backend JSON-serializes the body so a shallow copy\n // into a plain object is sufficient — and shields callers that mutate\n // the returned descriptor from corrupting the source constant.\n return {\n type: 'function',\n function: { name, description, parameters: { ...parameters } },\n }\n}\n\n/**\n *\n * Returns the queue-bound delegation tools projected into OpenAI Chat\n * Completions `tools[]` shape. The order is stable: `delegate_feedback`,\n * `delegation_status`, `delegation_history`.\n *\n * @experimental\n */\nexport function mcpToolsForRuntimeMcp(): OpenAIChatTool[] {\n return [\n buildTool(\n DELEGATE_FEEDBACK_TOOL_NAME,\n DELEGATE_FEEDBACK_DESCRIPTION,\n DELEGATE_FEEDBACK_INPUT_SCHEMA as Readonly<Record<string, unknown>>,\n ),\n buildTool(\n DELEGATION_STATUS_TOOL_NAME,\n DELEGATION_STATUS_DESCRIPTION,\n DELEGATION_STATUS_INPUT_SCHEMA as Readonly<Record<string, unknown>>,\n ),\n buildTool(\n DELEGATION_HISTORY_TOOL_NAME,\n DELEGATION_HISTORY_DESCRIPTION,\n DELEGATION_HISTORY_INPUT_SCHEMA as Readonly<Record<string, unknown>>,\n ),\n ]\n}\n\n/**\n *\n * Subset filter — return only the projected tools whose `function.name`\n * appears in `names`. Useful for curated mounts (e.g. only the queue-bound\n * delegation tools, omitting `delegate_feedback`). Unknown names are\n * silently ignored; pass an empty array to get an empty result.\n *\n * @experimental\n */\nexport function mcpToolsForRuntimeMcpSubset(names: ReadonlyArray<string>): OpenAIChatTool[] {\n const allowed = new Set(names)\n return mcpToolsForRuntimeMcp().filter((tool) => allowed.has(tool.function.name))\n}\n"],"mappings":";;AAwCA,SAAS,UACP,MACA,aACA,YACgB;CAKhB,OAAO;EACL,MAAM;EACN,UAAU;GAAE;GAAM;GAAa,YAAY,EAAE,GAAG,WAAW;EAAE;CAC/D;AACF;;;;;;;;;AAUA,SAAgB,wBAA0C;CACxD,OAAO;EACL,UACE,6BACA,+BACA,8BACF;EACA,UACE,6BACA,+BACA,8BACF;EACA,UACE,8BACA,gCACA,+BACF;CACF;AACF;;;;;;;;;;AAWA,SAAgB,4BAA4B,OAAgD;CAC1F,MAAM,UAAU,IAAI,IAAI,KAAK;CAC7B,OAAO,sBAAsB,CAAC,CAAC,QAAQ,SAAS,QAAQ,IAAI,KAAK,SAAS,IAAI,CAAC;AACjF"}
|