@apnex/network-adapter 0.1.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.
Files changed (52) hide show
  1. package/dist/hub-error.d.ts +42 -0
  2. package/dist/hub-error.js +74 -0
  3. package/dist/hub-error.js.map +1 -0
  4. package/dist/index.d.ts +29 -0
  5. package/dist/index.js +31 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/kernel/agent-client.d.ts +192 -0
  8. package/dist/kernel/agent-client.js +44 -0
  9. package/dist/kernel/agent-client.js.map +1 -0
  10. package/dist/kernel/event-router.d.ts +49 -0
  11. package/dist/kernel/event-router.js +150 -0
  12. package/dist/kernel/event-router.js.map +1 -0
  13. package/dist/kernel/handshake.d.ts +161 -0
  14. package/dist/kernel/handshake.js +257 -0
  15. package/dist/kernel/handshake.js.map +1 -0
  16. package/dist/kernel/instance.d.ts +40 -0
  17. package/dist/kernel/instance.js +79 -0
  18. package/dist/kernel/instance.js.map +1 -0
  19. package/dist/kernel/mcp-agent-client.d.ts +139 -0
  20. package/dist/kernel/mcp-agent-client.js +505 -0
  21. package/dist/kernel/mcp-agent-client.js.map +1 -0
  22. package/dist/kernel/poll-backstop.d.ts +108 -0
  23. package/dist/kernel/poll-backstop.js +243 -0
  24. package/dist/kernel/poll-backstop.js.map +1 -0
  25. package/dist/kernel/session-claim.d.ts +67 -0
  26. package/dist/kernel/session-claim.js +106 -0
  27. package/dist/kernel/session-claim.js.map +1 -0
  28. package/dist/kernel/state-sync.d.ts +43 -0
  29. package/dist/kernel/state-sync.js +85 -0
  30. package/dist/kernel/state-sync.js.map +1 -0
  31. package/dist/logger.d.ts +82 -0
  32. package/dist/logger.js +114 -0
  33. package/dist/logger.js.map +1 -0
  34. package/dist/notification-log.d.ts +29 -0
  35. package/dist/notification-log.js +66 -0
  36. package/dist/notification-log.js.map +1 -0
  37. package/dist/prompt-format.d.ts +38 -0
  38. package/dist/prompt-format.js +220 -0
  39. package/dist/prompt-format.js.map +1 -0
  40. package/dist/tool-manager/dispatcher.d.ts +180 -0
  41. package/dist/tool-manager/dispatcher.js +379 -0
  42. package/dist/tool-manager/dispatcher.js.map +1 -0
  43. package/dist/tool-manager/tool-catalog-cache.d.ts +85 -0
  44. package/dist/tool-manager/tool-catalog-cache.js +137 -0
  45. package/dist/tool-manager/tool-catalog-cache.js.map +1 -0
  46. package/dist/wire/mcp-transport.d.ts +120 -0
  47. package/dist/wire/mcp-transport.js +447 -0
  48. package/dist/wire/mcp-transport.js.map +1 -0
  49. package/dist/wire/transport.d.ts +174 -0
  50. package/dist/wire/transport.js +43 -0
  51. package/dist/wire/transport.js.map +1 -0
  52. package/package.json +32 -0
@@ -0,0 +1,137 @@
1
+ /**
2
+ * tool-catalog-cache.ts — per-WORK_DIR Hub tool catalog cache.
3
+ *
4
+ * Probe-safe ListTools support: when the host calls tools/list before
5
+ * the adapter's identityReady has resolved (e.g. `claude mcp list`
6
+ * spawning the adapter just to enumerate available tools), the
7
+ * dispatcher serves the catalog from a persisted cache without
8
+ * touching the Hub. Together with the lazy session-claim path this
9
+ * makes probes fully Hub-free against a warm cache.
10
+ *
11
+ * Storage: $WORK_DIR/.ois/tool-catalog.json
12
+ * {
13
+ * schemaVersion: 1,
14
+ * hubVersion: "1.0.0",
15
+ * fetchedAt: "2026-04-22T...Z",
16
+ * catalog: [...]
17
+ * }
18
+ *
19
+ * Invalidation: Hub-version mismatch only. No TTL — the catalog is
20
+ * static between Hub deploys; TTL would add noise without correctness
21
+ * value. Schema-version mismatch on read returns null (cache treated
22
+ * as invalid; future schema evolution bumps CATALOG_SCHEMA_VERSION).
23
+ *
24
+ * Atomicity: writeCache uses tmp-file + rename so partial writes on
25
+ * crash don't corrupt the cache. Parse errors on read also return
26
+ * null — the cache self-heals on next bootstrap.
27
+ *
28
+ * Failure modes (best-effort; readCache + writeCache never throw on
29
+ * the primary flow):
30
+ * - missing file: readCache returns null
31
+ * - parse error: readCache returns null + logs
32
+ * - schema-version mismatch: readCache returns null
33
+ * - $WORK_DIR readonly: writeCache logs + no-ops
34
+ * - disk full: writeCache logs + no-ops
35
+ */
36
+ import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync, unlinkSync, } from "node:fs";
37
+ import { join, dirname } from "node:path";
38
+ /**
39
+ * Bumping CATALOG_SCHEMA_VERSION forces all existing cache files to
40
+ * be treated as invalid + re-bootstrapped. Use when changing the
41
+ * cache file shape.
42
+ */
43
+ export const CATALOG_SCHEMA_VERSION = 1;
44
+ /** Compute the canonical cache path for a given WORK_DIR. */
45
+ export function cachePathFor(workDir) {
46
+ return join(workDir, ".ois", "tool-catalog.json");
47
+ }
48
+ /**
49
+ * Read the cache file. Returns null on missing file, parse error,
50
+ * schema-version mismatch, or shape mismatch. Never throws — the
51
+ * primary ListTools flow always falls through to a live Hub fetch
52
+ * when readCache returns null.
53
+ */
54
+ export function readCache(workDir, log) {
55
+ const path = cachePathFor(workDir);
56
+ if (!existsSync(path))
57
+ return null;
58
+ try {
59
+ const raw = readFileSync(path, "utf8");
60
+ const parsed = JSON.parse(raw);
61
+ if (typeof parsed.schemaVersion !== "number" ||
62
+ parsed.schemaVersion !== CATALOG_SCHEMA_VERSION ||
63
+ typeof parsed.hubVersion !== "string" ||
64
+ typeof parsed.fetchedAt !== "string" ||
65
+ !Array.isArray(parsed.catalog)) {
66
+ log?.(`[tool-catalog-cache] readCache: cache invalid (schema/shape mismatch) at ${path}`);
67
+ return null;
68
+ }
69
+ return {
70
+ schemaVersion: parsed.schemaVersion,
71
+ hubVersion: parsed.hubVersion,
72
+ fetchedAt: parsed.fetchedAt,
73
+ catalog: parsed.catalog,
74
+ };
75
+ }
76
+ catch (err) {
77
+ log?.(`[tool-catalog-cache] readCache: parse error at ${path}: ${err.message ?? err}`);
78
+ return null;
79
+ }
80
+ }
81
+ /**
82
+ * Persist the catalog atomically. Writes a sibling tmp file then
83
+ * renames — so a crash mid-write leaves either the previous cache
84
+ * intact OR the new cache fully landed. Best-effort: failures log
85
+ * and return; the caller's primary flow continues.
86
+ */
87
+ export function writeCache(workDir, catalog, hubVersion, log) {
88
+ const path = cachePathFor(workDir);
89
+ const tmpPath = `${path}.tmp.${process.pid}`;
90
+ const body = {
91
+ schemaVersion: CATALOG_SCHEMA_VERSION,
92
+ hubVersion,
93
+ fetchedAt: new Date().toISOString(),
94
+ catalog,
95
+ };
96
+ try {
97
+ const dir = dirname(path);
98
+ if (!existsSync(dir))
99
+ mkdirSync(dir, { recursive: true });
100
+ writeFileSync(tmpPath, JSON.stringify(body), { encoding: "utf8" });
101
+ renameSync(tmpPath, path);
102
+ }
103
+ catch (err) {
104
+ log?.(`[tool-catalog-cache] writeCache: failed at ${path}: ${err.message ?? err} — cache will not populate this run`);
105
+ try {
106
+ if (existsSync(tmpPath))
107
+ unlinkSync(tmpPath);
108
+ }
109
+ catch {
110
+ /* ignore */
111
+ }
112
+ }
113
+ }
114
+ /**
115
+ * Check cache validity against the current Hub version.
116
+ *
117
+ * Two semantics:
118
+ * - currentHubVersion is a non-empty string: strict equality vs
119
+ * cached.hubVersion. Mismatch → invalid → caller re-bootstraps.
120
+ * - currentHubVersion is null/undefined/empty: caller doesn't know
121
+ * the current Hub version yet (e.g. /health fetch in flight at
122
+ * startup). Trust the cache (probe-friendly default) — worst
123
+ * case is serving a stale catalog ONCE until the next /health
124
+ * fetch completes and a real session refreshes the cache.
125
+ *
126
+ * Schema-version check is enforced inside readCache, so isCacheValid
127
+ * never sees a wrong-schema cached object.
128
+ */
129
+ export function isCacheValid(cached, currentHubVersion) {
130
+ if (currentHubVersion === null ||
131
+ currentHubVersion === undefined ||
132
+ currentHubVersion === "") {
133
+ return true;
134
+ }
135
+ return cached.hubVersion === currentHubVersion;
136
+ }
137
+ //# sourceMappingURL=tool-catalog-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-catalog-cache.js","sourceRoot":"","sources":["../../src/tool-manager/tool-catalog-cache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,120 @@
1
+ /**
2
+ * McpTransport — first `ITransport` implementation, backed by the MCP
3
+ * SDK's `Client` + `StreamableHTTPClientTransport`.
4
+ *
5
+ * Phase 2 of the L4/L7 refactor: extracted alongside the existing
6
+ * `McpConnectionManager`, which stays intact for the rest of the
7
+ * migration. Nothing in the current runtime uses this file yet — it
8
+ * exists so the Phase-2 gap tests can exercise the L4 surface in
9
+ * isolation and prove the split holds before Phase 3 relocates the
10
+ * session FSM.
11
+ *
12
+ * ## What this class owns (L4)
13
+ *
14
+ * - The MCP wire: SDK `Client` + `StreamableHTTPClientTransport`.
15
+ * - SSE liveness: first-keepalive deadline, mid-stream watchdog,
16
+ * heartbeat POST loop.
17
+ * - Wire-level reconnection: on SSE death or heartbeat failure it
18
+ * tears down the SDK client and rebuilds a fresh one. Callers see
19
+ * only `reconnecting` + `reconnected` wire events.
20
+ * - Untyped `request(method, params)` over MCP `callTool`.
21
+ * - `listMethods()` via MCP `listTools`.
22
+ *
23
+ * ## What this class deliberately does NOT own
24
+ *
25
+ * - `register_role`. The bare register_role call that the legacy
26
+ * `McpConnectionManager` fires during `connect()` is a session
27
+ * concern — it binds the MCP session to an engineer role on the
28
+ * Hub. `McpTransport.connect()` returns once the MCP wire is
29
+ * initialized, *without* calling register_role. The caller
30
+ * (future `IAgentClient`) issues the role call via
31
+ * `transport.request("register_role", {role})` itself.
32
+ * - Session FSM. No synchronizing / streaming vocabulary, no
33
+ * sync buffer, no `completeSync()`.
34
+ * - `session_invalid` classification and retry. Callers see the
35
+ * raw error from `request()` and reconnect at the session layer.
36
+ *
37
+ * ## Hub-event push delivery
38
+ *
39
+ * SSE notifications land on the MCP client's `LoggingMessageNotification`
40
+ * handler. This class splits them into two streams:
41
+ *
42
+ * - `logger === "keepalive"` frames feed the SSE watchdog and are
43
+ * NOT forwarded to subscribers — they are wire noise.
44
+ * - `logger === "hub-event"` frames are emitted as
45
+ * `{ type: "push", method: "hub-event", payload }` via the
46
+ * `onWireEvent` stream. Classification, dedup, and routing happen
47
+ * above this layer.
48
+ */
49
+ import type { ITransport, TransportConfig, TransportMetrics, WireState, WireEventHandler } from "./transport.js";
50
+ /**
51
+ * Wire-level reconnect backoff curve. Pure function so the invariant
52
+ * (start at `baseDelay`, double each consecutive failure, clamp at
53
+ * `maxDelay`) is unit-testable without spinning a real wire.
54
+ *
55
+ * The cap on the exponent (`6`) matches the legacy manager: after six
56
+ * consecutive failures we're pegged at 60s and further failures don't
57
+ * push the delay higher — they just sit at the cap until a keepalive
58
+ * arrives and resets `consecutiveReconnects` to zero.
59
+ */
60
+ export declare function computeReconnectBackoff(consecutiveReconnects: number, baseDelay: number, maxDelay?: number): number;
61
+ export declare class McpTransport implements ITransport {
62
+ private readonly cfg;
63
+ private readonly log;
64
+ private client;
65
+ private sdkTransport;
66
+ private _wireState;
67
+ private handlers;
68
+ private lastKeepaliveAt;
69
+ private sseVerified;
70
+ private heartbeatTimer;
71
+ private sseWatchdogTimer;
72
+ private firstKeepaliveTimer;
73
+ private reconnectTimer;
74
+ private totalReconnects;
75
+ private consecutiveReconnects;
76
+ private lastReconnectCause?;
77
+ private requestsInFlight;
78
+ private reconnecting;
79
+ private closed;
80
+ constructor(config: TransportConfig);
81
+ get wireState(): WireState;
82
+ connect(): Promise<void>;
83
+ close(): Promise<void>;
84
+ request(method: string, params: Record<string, unknown>): Promise<unknown>;
85
+ listMethods(): Promise<string[]>;
86
+ /**
87
+ * Escape hatch for shims that need full MCP tool definitions
88
+ * (name + inputSchema + description), not just method names.
89
+ * Consumed by proxy shims that re-advertise the Hub's tool surface
90
+ * to another MCP client (e.g. Claude Code stdio proxy).
91
+ */
92
+ listToolsRaw(): Promise<Array<Record<string, unknown>>>;
93
+ onWireEvent(handler: WireEventHandler): void;
94
+ getMetrics(): TransportMetrics;
95
+ /**
96
+ * Test-only helper. Session IDs are an MCP-specific concept exposed
97
+ * here so Phase-2 integration tests can assert that a wire reconnect
98
+ * rebuilds the session. Not part of `ITransport`.
99
+ */
100
+ getSessionId(): string | undefined;
101
+ private createWire;
102
+ private teardownWire;
103
+ /**
104
+ * Wire-level reconnect loop. Entered on any wire-death signal
105
+ * (SSE drop, heartbeat failure, transport error). Emits
106
+ * `reconnecting` + (on success) `reconnected` wire events so the
107
+ * AgentClient above can re-run its L7 handshake.
108
+ */
109
+ private reconnectWire;
110
+ private scheduleReconnectTimer;
111
+ private cancelReconnectTimer;
112
+ private startHeartbeat;
113
+ private stopHeartbeat;
114
+ private startSseWatchdog;
115
+ private stopSseWatchdog;
116
+ private startFirstKeepaliveDeadline;
117
+ private cancelFirstKeepaliveDeadline;
118
+ private transition;
119
+ private emit;
120
+ }