nanocodex 0.1.1 → 0.3.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/node/host.mjs CHANGED
@@ -1,10 +1,13 @@
1
1
  import { Console } from "node:console";
2
2
  import { createRequire } from "node:module";
3
+ import { resolve } from "node:path";
3
4
  import WebSocket from "ws";
5
+ import packageMetadata from "../package.json" with { type: "json" };
4
6
 
5
7
  import { createCodeRuntime } from "../runtime/code-runtime.mjs";
6
8
 
7
9
  const RESPONSES_WEBSOCKETS_BETA = "responses_websockets=2026-02-06";
10
+ const USER_AGENT = `nanocodex-wasm/${packageMetadata.version}`;
8
11
  const DEFAULT_MAX_QUEUED_MESSAGES = 4_096;
9
12
  const DEFAULT_MAX_QUEUED_BYTES = 32 * 1024 * 1024;
10
13
  const DEFAULT_MAX_FRAME_BYTES = 16 * 1024 * 1024;
@@ -12,7 +15,7 @@ const DEFAULT_MAX_FRAME_BYTES = 16 * 1024 * 1024;
12
15
  export function createNodeHost(options = {}) {
13
16
  const connections = new Map();
14
17
  const code = createCodeRuntime(options.tools, {
15
- require: createRequire(`${process.cwd()}/`),
18
+ require: createRequire(resolve(options.workspace ?? process.cwd(), ".nanocodex-code-mode.cjs")),
16
19
  console: new Console({ stdout: process.stderr, stderr: process.stderr }),
17
20
  });
18
21
  const onEvent = options.onEvent || (() => {});
@@ -23,28 +26,47 @@ export function createNodeHost(options = {}) {
23
26
  const maxFrameBytes = options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES;
24
27
  let nextHandle = 1;
25
28
 
26
- function connect(endpoint, apiKey, sessionId) {
29
+ function connect(endpoint, apiKey, sessionId, metadata = {}) {
30
+ if (options.mpp) return connectMpp(endpoint);
27
31
  return new Promise((resolve, reject) => {
28
32
  let settled = false;
29
33
  let upgradeResponse;
34
+ const headers = {
35
+ Authorization: `Bearer ${apiKey}`,
36
+ "OpenAI-Beta": RESPONSES_WEBSOCKETS_BETA,
37
+ "x-openai-internal-codex-responses-lite": "true",
38
+ "session-id": sessionId,
39
+ "thread-id": sessionId,
40
+ "x-client-request-id": sessionId,
41
+ "x-responsesapi-include-timing-metrics": "true",
42
+ "User-Agent": USER_AGENT,
43
+ };
44
+ if (metadata.accountId) headers["ChatGPT-Account-ID"] = metadata.accountId;
45
+ if (metadata.fedramp) headers["X-OpenAI-Fedramp"] = "true";
46
+ if (metadata.turnState) headers["x-codex-turn-state"] = metadata.turnState;
30
47
  const socket = new WebSocket(endpoint, {
31
48
  handshakeTimeout: connectTimeoutMs,
32
49
  maxPayload: maxFrameBytes,
33
- headers: {
34
- Authorization: `Bearer ${apiKey}`,
35
- "OpenAI-Beta": RESPONSES_WEBSOCKETS_BETA,
36
- "x-openai-internal-codex-responses-lite": "true",
37
- "session-id": sessionId,
38
- "thread-id": sessionId,
39
- "x-client-request-id": sessionId,
40
- "x-responsesapi-include-timing-metrics": "true",
41
- "User-Agent": "nanocodex-wasm/0.1.0",
42
- },
50
+ headers,
43
51
  });
44
52
  const handle = nextHandle++;
45
53
  const connection = queueState(socket);
46
54
 
47
55
  socket.on("upgrade", (response) => { upgradeResponse = response; });
56
+ socket.on("unexpected-response", (_request, response) => {
57
+ if (settled) return;
58
+ settled = true;
59
+ const chunks = [];
60
+ response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
61
+ response.on("end", () => {
62
+ const error = new Error(`WebSocket handshake was rejected with HTTP ${response.statusCode}`);
63
+ error.status = response.statusCode;
64
+ error.body = chunks.length ? Buffer.concat(chunks).toString("utf8") : "empty response body";
65
+ const retryAfter = Number(header(response.headers, "retry-after"));
66
+ if (Number.isFinite(retryAfter) && retryAfter >= 0) error.retryAfter = retryAfter;
67
+ reject(error);
68
+ });
69
+ });
48
70
  socket.on("open", () => {
49
71
  settled = true;
50
72
  connections.set(handle, connection);
@@ -80,6 +102,35 @@ export function createNodeHost(options = {}) {
80
102
  });
81
103
  }
82
104
 
105
+ async function connectMpp(endpoint) {
106
+ if (typeof options.mpp.ws !== "function") {
107
+ throw new TypeError("mpp must provide ws(endpoint)");
108
+ }
109
+ const socket = await options.mpp.ws(endpoint);
110
+ if (!socket || typeof socket.addEventListener !== "function") {
111
+ throw new TypeError("mpp.ws(endpoint) must return a WebSocket");
112
+ }
113
+ const handle = nextHandle++;
114
+ const connection = queueState(socket);
115
+ connection.managed = true;
116
+ connections.set(handle, connection);
117
+ socket.addEventListener("message", (event) => {
118
+ enqueue(connection, typeof event.data === "string"
119
+ ? { kind: "text", text: event.data }
120
+ : { kind: "binary" });
121
+ });
122
+ socket.addEventListener("close", (event) => {
123
+ if (!connection.intentionallyClosed && !connection.overflowed) {
124
+ const suffix = event.reason ? `: ${event.reason}` : "";
125
+ enqueue(connection, { kind: "closed", detail: `with code ${event.code ?? 1000}${suffix}` });
126
+ }
127
+ });
128
+ socket.addEventListener("error", () => {
129
+ enqueue(connection, { kind: "error", detail: "MPP WebSocket connection failed" });
130
+ });
131
+ return JSON.stringify({ handle, status: 101, reasoning_included: false });
132
+ }
133
+
83
134
  function send(handle, message) {
84
135
  const connection = connections.get(handle);
85
136
  if (!connection || connection.socket.readyState !== WebSocket.OPEN) {
@@ -89,6 +140,18 @@ export function createNodeHost(options = {}) {
89
140
  error: "WebSocket is no longer open",
90
141
  }));
91
142
  }
143
+ if (connection.managed) {
144
+ try {
145
+ connection.socket.send(JSON.stringify({ mpp: "message", data: message }));
146
+ return Promise.resolve(JSON.stringify({ ok: true }));
147
+ } catch (error) {
148
+ return Promise.resolve(JSON.stringify({
149
+ ok: false,
150
+ reconnectable: connection.socket.readyState !== WebSocket.OPEN,
151
+ error: errorMessage(error),
152
+ }));
153
+ }
154
+ }
92
155
  return new Promise((resolve) => {
93
156
  let completed = false;
94
157
  const timer = setTimeout(() => finish({
@@ -159,7 +222,8 @@ export function createNodeHost(options = {}) {
159
222
  detail: `receive queue exceeded ${maxQueuedMessages} messages or ${maxQueuedBytes} bytes`,
160
223
  };
161
224
  connection.queue.push({ message: error, bytes: messageBytes(error) });
162
- connection.socket.terminate();
225
+ if (typeof connection.socket.terminate === "function") connection.socket.terminate();
226
+ else connection.socket.close(1009, "receive queue exceeded configured bounds");
163
227
  return;
164
228
  }
165
229
  connection.queue.push({ message, bytes });
@@ -187,6 +251,7 @@ function queueState(socket) {
187
251
  waiter: undefined,
188
252
  intentionallyClosed: false,
189
253
  overflowed: false,
254
+ managed: false,
190
255
  };
191
256
  }
192
257
 
package/node/index.d.mts CHANGED
@@ -1,13 +1,18 @@
1
1
  export { Actions } from "../index.mjs";
2
2
  export type {
3
3
  AgentEvent,
4
+ CostStatus,
5
+ EstimatedUsdCost,
4
6
  PromptInput,
5
7
  PromptItem,
6
8
  ReasoningMode,
9
+ SessionSnapshot,
7
10
  Thinking,
8
11
  Tool,
9
12
  ToolContext,
10
13
  ToolMap,
11
14
  Turn,
15
+ TurnResult,
16
+ TurnUsage,
12
17
  } from "../types.mjs";
13
18
  export * as Agent from "./Agent.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nanocodex",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Viem-style Node and browser bindings for the Nanocodex WASM agent",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -25,7 +25,6 @@
25
25
  "pkg-node",
26
26
  "pkg-web",
27
27
  "runtime",
28
- "scripts/check-package.mjs",
29
28
  "internal.mjs",
30
29
  "index.d.mts",
31
30
  "index.mjs",
@@ -39,7 +38,8 @@
39
38
  "scripts": {
40
39
  "check:package": "node scripts/check-package.mjs",
41
40
  "prepack": "npm run check:package",
42
- "test": "node --test test/*.test.mjs && npm run test:typecheck && npm run check:package",
41
+ "test": "node --test test/*.test.mjs && npm run test:performance && npm run test:typecheck && npm run check:package",
42
+ "test:performance": "node --test test/performance.bench.mjs",
43
43
  "test:typecheck": "tsc -p test/tsconfig.json"
44
44
  },
45
45
  "dependencies": {
@@ -2,13 +2,21 @@
2
2
  /* eslint-disable */
3
3
 
4
4
  /**
5
- * Persistent WASM agent handle hosted by Node.js or a browser Worker.
5
+ * JavaScript binding over the shared Rust agent lifecycle.
6
6
  */
7
7
  export class Nanocodex {
8
8
  free(): void;
9
9
  [Symbol.dispose](): void;
10
10
  /**
11
- * Fork the latest safe committed model boundary.
11
+ * Compacts retained history immediately without fabricating a user prompt.
12
+ *
13
+ * # Errors
14
+ *
15
+ * Throws when compaction or the agent driver fails.
16
+ */
17
+ compact(): Promise<void>;
18
+ /**
19
+ * Forks the latest safe committed model boundary.
12
20
  *
13
21
  * # Errors
14
22
  *
@@ -16,41 +24,63 @@ export class Nanocodex {
16
24
  */
17
25
  fork(): Promise<Nanocodex>;
18
26
  /**
19
- * Fork from the exact checkpoint retained by a completed historical turn.
27
+ * Forks from an exact completed historical turn.
20
28
  *
21
29
  * # Errors
22
30
  *
23
- * Rejects if the turn is incomplete, belongs to another lineage, or the
24
- * driver stops.
31
+ * Rejects if the result belongs to another agent or the driver stopped.
25
32
  */
26
- forkFrom(turn: Turn): Promise<Nanocodex>;
33
+ forkFrom(result: TurnResult): Promise<Nanocodex>;
27
34
  /**
28
- * Build a persistent agent from a JSON configuration object.
35
+ * Builds an agent from its JavaScript JSON configuration.
29
36
  *
30
37
  * # Errors
31
38
  *
32
- * Throws when the JSON or required configuration is invalid.
39
+ * Throws when the JSON or agent policy is invalid.
33
40
  */
34
41
  constructor(config_json: string);
35
42
  /**
36
- * Accept a prompt immediately and return its independently awaitable turn.
43
+ * Accepts a text prompt and returns its independently awaitable turn.
37
44
  *
38
45
  * # Errors
39
46
  *
40
- * Throws when the prompt is empty or the agent driver has stopped.
47
+ * Throws when the prompt is empty.
41
48
  */
42
49
  prompt(instruction: string): Turn;
43
50
  /**
44
- * Accept ordered browser-safe text, image, or audio input encoded as JSON.
51
+ * Accepts browser-safe multimodal input encoded as JSON.
45
52
  *
46
53
  * # Errors
47
54
  *
48
- * Throws for malformed or empty content, local filesystem inputs, or a
49
- * stopped driver.
55
+ * Throws for malformed, empty, or local-filesystem input.
50
56
  */
51
57
  promptContent(content_json: string): Turn;
52
58
  /**
53
- * Start a clean sibling with the same configuration and tools.
59
+ * Enables or disables priority processing for subsequently accepted turns.
60
+ *
61
+ * # Errors
62
+ *
63
+ * Rejects after the driver stops.
64
+ */
65
+ setFastMode(enabled: boolean): Promise<void>;
66
+ /**
67
+ * Changes the reasoning effort for subsequently accepted turns.
68
+ *
69
+ * # Errors
70
+ *
71
+ * Rejects an invalid effort or a stopped driver.
72
+ */
73
+ setThinking(thinking: string): Promise<void>;
74
+ /**
75
+ * Gracefully stops the driver and joins every resource owned by this agent.
76
+ *
77
+ * # Errors
78
+ *
79
+ * Rejects when the driver had already stopped or cleanup fails.
80
+ */
81
+ shutdown(): Promise<void>;
82
+ /**
83
+ * Starts a clean sibling with the same private agent policy.
54
84
  *
55
85
  * # Errors
56
86
  *
@@ -58,52 +88,77 @@ export class Nanocodex {
58
88
  */
59
89
  spawn(): Promise<Nanocodex>;
60
90
  /**
61
- * Stable session identifier used to route this agent's event stream.
91
+ * Returns the stable `UUIDv7` session identity.
62
92
  */
63
93
  readonly sessionId: string;
64
94
  }
65
95
 
66
96
  /**
67
- * Completion and control handle for one accepted WASM turn.
97
+ * JavaScript binding over one shared Rust turn.
68
98
  */
69
99
  export class Turn {
70
100
  private constructor();
71
101
  free(): void;
72
102
  [Symbol.dispose](): void;
73
103
  /**
74
- * Cancel this exact active or queued turn.
104
+ * Cancels this exact active or queued turn.
75
105
  *
76
106
  * # Errors
77
107
  *
78
- * Rejects if the turn is already terminal or the driver stops.
108
+ * Rejects if the turn is already terminal or its driver stopped.
79
109
  */
80
110
  cancel(): Promise<void>;
81
111
  /**
82
- * Wait for the turn and return its final assistant message.
112
+ * Waits for the final assistant message.
83
113
  *
84
114
  * # Errors
85
115
  *
86
- * Rejects when the model run fails, the driver stops, or two consumers
87
- * await the same pending turn concurrently.
116
+ * Rejects when the model run or driver fails.
88
117
  */
89
- result(): Promise<string>;
118
+ result(): Promise<TurnResult>;
90
119
  /**
91
- * Inject input at this active turn's next safe model boundary.
120
+ * Injects text input at the active turn's next safe model boundary.
92
121
  *
93
122
  * # Errors
94
123
  *
95
- * Rejects for empty input, a queued or terminal turn, a full steer queue,
96
- * or a stopped driver.
124
+ * Rejects if the turn is not active or its driver stopped.
97
125
  */
98
126
  steer(instruction: string): Promise<void>;
99
127
  /**
100
- * Inject ordered browser-safe text, image, or audio input at this active
101
- * turn's next safe model boundary.
128
+ * Injects browser-safe multimodal input at the active turn's next boundary.
102
129
  *
103
130
  * # Errors
104
131
  *
105
- * Rejects for malformed or empty content, local filesystem inputs, a
106
- * queued or terminal turn, a full steer queue, or a stopped driver.
132
+ * Rejects malformed input or a turn that is no longer active.
107
133
  */
108
134
  steerContent(content_json: string): Promise<void>;
109
135
  }
136
+
137
+ /**
138
+ * JavaScript binding over one completed Rust turn result.
139
+ */
140
+ export class TurnResult {
141
+ private constructor();
142
+ free(): void;
143
+ [Symbol.dispose](): void;
144
+ /**
145
+ * Serializes this completed boundary's resumable session snapshot.
146
+ *
147
+ * # Errors
148
+ *
149
+ * Throws when serialization fails.
150
+ */
151
+ snapshot(): string;
152
+ /**
153
+ * Serializes exact aggregate usage for this completed logical turn.
154
+ *
155
+ * # Errors
156
+ *
157
+ * Throws when serialization fails.
158
+ */
159
+ usage(): string;
160
+ /**
161
+ * Returns the final assistant message.
162
+ */
163
+ readonly finalMessage: string;
164
+ }