nanocodex 0.1.1

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 ADDED
@@ -0,0 +1,116 @@
1
+ # Nanocodex for JavaScript
2
+
3
+ The Node and browser entrypoints expose the same viem-v3-style API over the
4
+ same Rust/WASM agent. Runtime-specific host options are flattened into
5
+ `Agent.create(...)`; generated WASM handles and host routing remain private.
6
+
7
+ ```js
8
+ import { Actions, Agent } from "nanocodex/node";
9
+
10
+ const agent = await Agent.create({
11
+ apiKey: process.env.OPENAI_API_KEY,
12
+ reasoningMode: "pro",
13
+ thinking: "high",
14
+ tools,
15
+ });
16
+
17
+ const turn = agent.turn.prompt({ input: "Build the thing." });
18
+ console.log(await turn.result());
19
+
20
+ const branch = await agent.session.fork({ at: turn });
21
+ console.log(await branch.turn.prompt({ input: "Try another approach." }).result());
22
+
23
+ const followOn = Actions.turn.prompt(agent, { input: "Now explain it." });
24
+ console.log(await Actions.turn.getResult(followOn));
25
+ ```
26
+
27
+ `Agent` and `Actions` are module namespaces, not classes. `Agent.create` returns
28
+ an owned client decorated with matching domain actions:
29
+
30
+ - `agent.turn.prompt(...)` / `Actions.turn.prompt(agent, ...)`
31
+ - `agent.session.fork(...)` / `Actions.session.fork(agent, ...)`
32
+ - `agent.session.spawn()` / `Actions.session.spawn(agent)`
33
+ - `agent.events.watch(...)` / `Actions.events.watch(agent, ...)`
34
+
35
+ Every action owns its types, for example `Actions.turn.prompt.Options`,
36
+ `Actions.turn.prompt.ReturnType`, and `Actions.events.watch.Watcher`.
37
+
38
+ Event watches are lazy, terminal handles:
39
+
40
+ ```js
41
+ const watch = agent.events.watch();
42
+ const unlisten = watch.onEvent(console.log);
43
+
44
+ unlisten();
45
+ watch.off();
46
+ ```
47
+
48
+ The same watcher can instead be consumed as an ordered async iterable; breaking
49
+ the loop releases that iterator, while `watch.off()` terminates the whole watch.
50
+
51
+ ```js
52
+ const watch = agent.events.watch();
53
+ for await (const event of watch) {
54
+ console.log(event);
55
+ if (done) break;
56
+ }
57
+ watch.off();
58
+ ```
59
+
60
+ Applications add typed action domains with decorators:
61
+
62
+ ```js
63
+ const extended = agent.extend((client) => ({
64
+ inspect: {
65
+ session: () => client.sessionId,
66
+ },
67
+ }));
68
+
69
+ extended.inspect.session();
70
+ ```
71
+
72
+ Browser Workers use the identical shape:
73
+
74
+ ```js
75
+ import { Agent } from "nanocodex/browser";
76
+
77
+ const agent = await Agent.create({
78
+ websocketUrl: signedOrCookieAuthorizedEndpoint,
79
+ createWebSocket(endpoint, sessionId) {
80
+ const url = new URL(endpoint);
81
+ url.searchParams.set("session_id", sessionId);
82
+ return new WebSocket(url);
83
+ },
84
+ tools,
85
+ });
86
+ ```
87
+
88
+ After publication, a browser can load the same entrypoint without a package
89
+ manager or build step:
90
+
91
+ ```html
92
+ <script type="module">
93
+ import { Agent } from "https://cdn.jsdelivr.net/npm/nanocodex@0.1.0/browser/index.mjs";
94
+ const agent = await Agent.create({ websocketUrl: "/api/responses" });
95
+ console.log(await agent.turn.prompt({ input: "Hello." }).result());
96
+ </script>
97
+ ```
98
+
99
+ Pin the package version in production. The adjacent WASM file is part of the
100
+ npm package and is resolved relative to the browser module. The endpoint must
101
+ be authorized by the embedding application because browser WebSockets cannot
102
+ attach OpenAI's upgrade authorization header.
103
+
104
+ The owned Rust session retains follow-on history, response state, tool output,
105
+ its WebSocket, and stable prompt-cache identity. Typed browser content accepts
106
+ ordered text, remote/data-URL image, and audio items. JavaScript tools are
107
+ ordinary async handlers described by JSON Schema and appear in the same ordered
108
+ agent event stream as built-in code mode.
109
+
110
+ Run the standalone Node proof with:
111
+
112
+ ```sh
113
+ cd examples/node
114
+ npm install
115
+ OPENAI_API_KEY=... npm start
116
+ ```
@@ -0,0 +1,10 @@
1
+ import type { Agent, AgentEvent, EventWatcher, WatchEventsOptions } from "../types.mjs";
2
+
3
+ /** Creates a lazy, terminal watcher for an Agent's ordered event stream. */
4
+ export function watch(agent: Agent<object>, options?: watch.Options): watch.ReturnType;
5
+ export declare namespace watch {
6
+ type OnEventFn = (event: AgentEvent) => void;
7
+ type Options = WatchEventsOptions;
8
+ type ReturnType = EventWatcher;
9
+ type Watcher = EventWatcher;
10
+ }
@@ -0,0 +1,91 @@
1
+ import { subscribeAgentEvents } from "../internal.mjs";
2
+
3
+ export function watch(agent, options = {}) {
4
+ const listeners = new Set();
5
+ const iterators = new Set();
6
+ let unsubscribe;
7
+ let closed = false;
8
+
9
+ const emit = (event) => {
10
+ for (const listener of listeners) listener(event);
11
+ for (const iterator of iterators) iterator.push(event);
12
+ };
13
+
14
+ const start = () => {
15
+ if (closed || unsubscribe) return;
16
+ unsubscribe = subscribeAgentEvents(agent, emit, options);
17
+ };
18
+
19
+ const watcher = {
20
+ onEvent(listener) {
21
+ if (typeof listener !== "function") throw new TypeError("events.watch.onEvent requires a listener");
22
+ if (closed) return () => {};
23
+ listeners.add(listener);
24
+ start();
25
+ return () => listeners.delete(listener);
26
+ },
27
+ off() {
28
+ if (closed) return;
29
+ closed = true;
30
+ unsubscribe?.();
31
+ unsubscribe = undefined;
32
+ listeners.clear();
33
+ for (const iterator of [...iterators]) iterator.end();
34
+ iterators.clear();
35
+ },
36
+ [Symbol.asyncIterator]() {
37
+ if (closed) return emptyIterator();
38
+ const iterator = eventIterator(() => iterators.delete(iterator));
39
+ iterators.add(iterator);
40
+ start();
41
+ return iterator;
42
+ },
43
+ };
44
+ return Object.freeze(watcher);
45
+ }
46
+
47
+ function eventIterator(onEnd) {
48
+ const queue = [];
49
+ let pending;
50
+ let done = false;
51
+
52
+ const iterator = {
53
+ push(event) {
54
+ if (done) return;
55
+ if (pending) {
56
+ const resolve = pending;
57
+ pending = undefined;
58
+ resolve({ done: false, value: event });
59
+ } else {
60
+ queue.push(event);
61
+ }
62
+ },
63
+ end() {
64
+ if (done) return;
65
+ done = true;
66
+ onEnd();
67
+ pending?.({ done: true, value: undefined });
68
+ pending = undefined;
69
+ queue.length = 0;
70
+ },
71
+ next() {
72
+ if (queue.length) return Promise.resolve({ done: false, value: queue.shift() });
73
+ if (done) return Promise.resolve({ done: true, value: undefined });
74
+ return new Promise((resolve) => { pending = resolve; });
75
+ },
76
+ return() {
77
+ iterator.end();
78
+ return Promise.resolve({ done: true, value: undefined });
79
+ },
80
+ [Symbol.asyncIterator]() { return this; },
81
+ };
82
+ return iterator;
83
+ }
84
+
85
+ function emptyIterator() {
86
+ return {
87
+ next: () => Promise.resolve({ done: true, value: undefined }),
88
+ return: () => Promise.resolve({ done: true, value: undefined }),
89
+ [Symbol.asyncIterator]() { return this; },
90
+ };
91
+ }
@@ -0,0 +1,12 @@
1
+ import type { Agent, AgentActions } from "../types.mjs";
2
+
3
+ export * as events from "./events.mjs";
4
+ export * as session from "./session.mjs";
5
+ export * as turn from "./turn.mjs";
6
+
7
+ /** Decorates a base Agent with the standard `turn`, `session`, and `events` domains. */
8
+ export function agentActions(): agentActions.DecoratorFn;
9
+ export declare namespace agentActions {
10
+ type Decorator = AgentActions;
11
+ type DecoratorFn = (agent: Agent<object>) => Decorator;
12
+ }
@@ -0,0 +1,20 @@
1
+ import * as events from "./events.mjs";
2
+ import * as session from "./session.mjs";
3
+ import * as turn from "./turn.mjs";
4
+
5
+ export { events, session, turn };
6
+
7
+ export function agentActions() {
8
+ return (agent) => ({
9
+ events: {
10
+ watch: (options) => events.watch(agent, options),
11
+ },
12
+ session: {
13
+ fork: (options) => session.fork(agent, options),
14
+ spawn: () => session.spawn(agent),
15
+ },
16
+ turn: {
17
+ prompt: (options) => turn.prompt(agent, options),
18
+ },
19
+ });
20
+ }
@@ -0,0 +1,14 @@
1
+ import type { Agent, DefaultAgent, ForkOptions } from "../types.mjs";
2
+
3
+ /** Forks the latest checkpoint, or the exact completed Turn supplied in `options.at`. */
4
+ export function fork(agent: Agent<object>, options?: fork.Options): Promise<fork.ReturnType>;
5
+ export declare namespace fork {
6
+ type Options = ForkOptions;
7
+ type ReturnType = DefaultAgent;
8
+ }
9
+
10
+ /** Creates a clean sibling with the Agent's configuration and tools. */
11
+ export function spawn(agent: Agent<object>): Promise<spawn.ReturnType>;
12
+ export declare namespace spawn {
13
+ type ReturnType = DefaultAgent;
14
+ }
@@ -0,0 +1,9 @@
1
+ import { fork as forkAgent, spawn as spawnAgent } from "../internal.mjs";
2
+
3
+ export function fork(agent, options = {}) {
4
+ return forkAgent(agent, options);
5
+ }
6
+
7
+ export function spawn(agent) {
8
+ return spawnAgent(agent);
9
+ }
@@ -0,0 +1,30 @@
1
+ import type { Agent, PromptInput, Turn } from "../types.mjs";
2
+
3
+ /** Accepts a prompt on an owned Agent and returns its independently awaitable Turn. */
4
+ export function prompt<const agent extends Agent<object>>(
5
+ agent: agent,
6
+ options: prompt.Options,
7
+ ): prompt.ReturnType<agent>;
8
+ export declare namespace prompt {
9
+ type Options = { input: PromptInput };
10
+ type ReturnType<agent extends Agent<object> = Agent<object>> = Turn<agent>;
11
+ }
12
+
13
+ /** Waits for a Turn's final assistant message. */
14
+ export function getResult(turn: Turn): Promise<getResult.ReturnType>;
15
+ export declare namespace getResult {
16
+ type ReturnType = string;
17
+ }
18
+
19
+ /** Adds input to an active Turn. */
20
+ export function steer(turn: Turn, options: steer.Options): Promise<void>;
21
+ export declare namespace steer {
22
+ type Options = { input: PromptInput };
23
+ type ReturnType = void;
24
+ }
25
+
26
+ /** Cancels an active or queued Turn. */
27
+ export function cancel(turn: Turn): Promise<void>;
28
+ export declare namespace cancel {
29
+ type ReturnType = void;
30
+ }
@@ -0,0 +1,22 @@
1
+ import {
2
+ cancel as cancelTurn,
3
+ getTurnResult,
4
+ prompt as promptTurn,
5
+ steer as steerTurn,
6
+ } from "../internal.mjs";
7
+
8
+ export function prompt(agent, options) {
9
+ return promptTurn(agent, options);
10
+ }
11
+
12
+ export function getResult(turn) {
13
+ return getTurnResult(turn);
14
+ }
15
+
16
+ export function steer(turn, options) {
17
+ return steerTurn(turn, options);
18
+ }
19
+
20
+ export function cancel(turn) {
21
+ return cancelTurn(turn);
22
+ }
@@ -0,0 +1,25 @@
1
+ import type {
2
+ AgentOptions,
3
+ DefaultAgent,
4
+ ToolMap,
5
+ } from "../types.mjs";
6
+
7
+ export type Agent = DefaultAgent;
8
+
9
+ /** Creates a browser- or Worker-hosted Rust/WASM Agent. */
10
+ export function create(options?: create.Options): Promise<create.ReturnType>;
11
+ export declare namespace create {
12
+ type Options = AgentOptions & {
13
+ WebSocketImpl?: typeof WebSocket | undefined;
14
+ apiBaseUrl?: string | undefined;
15
+ apiKey?: string | undefined;
16
+ createWebSocket?(endpoint: string, sessionId: string): WebSocket;
17
+ maxBufferedSendBytes?: number | undefined;
18
+ maxQueuedBytes?: number | undefined;
19
+ maxQueuedMessages?: number | undefined;
20
+ module?: unknown;
21
+ tools?: ToolMap | undefined;
22
+ websocketUrl?: string | undefined;
23
+ };
24
+ type ReturnType = Agent;
25
+ }
@@ -0,0 +1,54 @@
1
+ import init, { Nanocodex } from "../pkg-web/nanocodex.js";
2
+
3
+ import { agentActions } from "../actions/index.mjs";
4
+ import {
5
+ activateHost,
6
+ bindHostSession,
7
+ createAgentClient,
8
+ createEventChannel,
9
+ defineRuntime,
10
+ releaseHostSession,
11
+ toWasmConfig,
12
+ } from "../internal.mjs";
13
+ import { createBrowserHost } from "./host.mjs";
14
+
15
+ let initialized;
16
+
17
+ export function create(options = {}) {
18
+ const {
19
+ apiKey = "host-managed",
20
+ websocketUrl,
21
+ apiBaseUrl,
22
+ module,
23
+ thinking,
24
+ reasoningMode,
25
+ instructions,
26
+ sessionId,
27
+ ...hostOptions
28
+ } = options;
29
+ const events = createEventChannel();
30
+ const host = createBrowserHost({ ...hostOptions, onEvent: events.emit });
31
+ activateHost(host);
32
+ const runtime = defineRuntime({
33
+ key: "browser-wasm",
34
+ name: "Nanocodex Browser WASM",
35
+ type: "browser",
36
+ async create(config) {
37
+ activateHost(host);
38
+ initialized ||= module === undefined ? init() : init({ module_or_path: module });
39
+ await initialized;
40
+ activateHost(host);
41
+ return new Nanocodex(JSON.stringify(toWasmConfig({
42
+ apiKey,
43
+ websocketUrl,
44
+ apiBaseUrl,
45
+ ...config,
46
+ })));
47
+ },
48
+ subscribe: events.subscribe,
49
+ adopt: (raw) => bindHostSession(host, raw.sessionId),
50
+ release: (raw) => releaseHostSession(host, raw.sessionId),
51
+ decorate: (agent) => agent.extend(agentActions()),
52
+ });
53
+ return createAgentClient(runtime, { thinking, reasoningMode, instructions, sessionId });
54
+ }
@@ -0,0 +1,20 @@
1
+ export type BrowserTool = {
2
+ description: string;
3
+ parameters: Record<string, unknown>;
4
+ handler: (
5
+ input: unknown,
6
+ context: { sessionId: string },
7
+ ) => unknown | Promise<unknown>;
8
+ };
9
+
10
+ export type BrowserToolMap = Record<string, BrowserTool>;
11
+
12
+ export function createBrowserHost(options?: {
13
+ WebSocketImpl?: typeof WebSocket;
14
+ createWebSocket?: (endpoint: string, sessionId: string) => WebSocket;
15
+ onEvent?: (eventJson: string) => void;
16
+ tools?: BrowserToolMap;
17
+ maxQueuedMessages?: number;
18
+ maxQueuedBytes?: number;
19
+ maxBufferedSendBytes?: number;
20
+ }): unknown;
@@ -0,0 +1,161 @@
1
+ import { createCodeRuntime } from "../runtime/code-runtime.mjs";
2
+
3
+ const DEFAULT_MAX_QUEUED_MESSAGES = 4_096;
4
+ const DEFAULT_MAX_QUEUED_BYTES = 32 * 1024 * 1024;
5
+ const DEFAULT_MAX_BUFFERED_SEND_BYTES = 16 * 1024 * 1024;
6
+
7
+ export function createBrowserHost(options = {}) {
8
+ const WebSocketImpl = options.WebSocketImpl || globalThis.WebSocket;
9
+ if (!WebSocketImpl) throw new Error("WebSocket is unavailable in this runtime");
10
+ const connections = new Map();
11
+ const code = createCodeRuntime(options.tools);
12
+ const onEvent = options.onEvent || (() => {});
13
+ const createWebSocket = options.createWebSocket || ((endpoint) => new WebSocketImpl(endpoint));
14
+ const maxQueuedMessages = options.maxQueuedMessages ?? DEFAULT_MAX_QUEUED_MESSAGES;
15
+ const maxQueuedBytes = options.maxQueuedBytes ?? DEFAULT_MAX_QUEUED_BYTES;
16
+ const maxBufferedSendBytes = options.maxBufferedSendBytes ?? DEFAULT_MAX_BUFFERED_SEND_BYTES;
17
+ const encoder = new TextEncoder();
18
+ let nextHandle = 1;
19
+
20
+ function connect(endpoint, _apiKey, sessionId) {
21
+ return new Promise((resolve, reject) => {
22
+ let settled = false;
23
+ const handle = nextHandle++;
24
+ const socket = createWebSocket(endpoint, sessionId);
25
+ const connection = {
26
+ socket,
27
+ queue: [],
28
+ queuedBytes: 0,
29
+ waiter: undefined,
30
+ intentionallyClosed: false,
31
+ overflowed: false,
32
+ };
33
+ socket.addEventListener("open", () => {
34
+ settled = true;
35
+ connections.set(handle, connection);
36
+ resolve(JSON.stringify({ handle, status: 101, reasoning_included: false }));
37
+ }, { once: true });
38
+ socket.addEventListener("message", (event) => {
39
+ enqueue(connection, typeof event.data === "string"
40
+ ? { kind: "text", text: event.data }
41
+ : { kind: "binary" });
42
+ });
43
+ socket.addEventListener("close", (event) => {
44
+ if (!settled) {
45
+ settled = true;
46
+ reject(new Error(`WebSocket closed during connection with code ${event.code}`));
47
+ } else if (!connection.intentionallyClosed && !connection.overflowed) {
48
+ enqueue(connection, { kind: "closed", detail: `with code ${event.code}` });
49
+ }
50
+ });
51
+ socket.addEventListener("error", () => {
52
+ if (!settled) {
53
+ settled = true;
54
+ reject(new Error("WebSocket connection failed"));
55
+ } else {
56
+ enqueue(connection, { kind: "error", detail: "WebSocket connection failed" });
57
+ }
58
+ });
59
+ });
60
+ }
61
+
62
+ function send(handle, message) {
63
+ const connection = connections.get(handle);
64
+ if (!connection || connection.socket.readyState !== WebSocketImpl.OPEN) {
65
+ return Promise.resolve(JSON.stringify({
66
+ ok: false,
67
+ reconnectable: true,
68
+ error: "WebSocket is no longer open",
69
+ }));
70
+ }
71
+ try {
72
+ const frameBytes = encoder.encode(message).byteLength;
73
+ if (frameBytes > maxBufferedSendBytes
74
+ || connection.socket.bufferedAmount + frameBytes > maxBufferedSendBytes) {
75
+ return Promise.resolve(JSON.stringify({
76
+ ok: false,
77
+ reconnectable: false,
78
+ error: `buffered WebSocket sends exceeded ${maxBufferedSendBytes} bytes`,
79
+ }));
80
+ }
81
+ connection.socket.send(message);
82
+ return Promise.resolve(JSON.stringify({ ok: true }));
83
+ } catch (error) {
84
+ return Promise.resolve(JSON.stringify({
85
+ ok: false,
86
+ reconnectable: connection.socket.readyState !== WebSocketImpl.OPEN,
87
+ error: error instanceof Error ? error.message : String(error),
88
+ }));
89
+ }
90
+ }
91
+
92
+ function next(handle, timeoutMs) {
93
+ const connection = connections.get(handle);
94
+ if (!connection) {
95
+ return Promise.resolve(JSON.stringify({ kind: "closed", detail: "before the next frame" }));
96
+ }
97
+ if (connection.queue.length) {
98
+ const entry = connection.queue.shift();
99
+ connection.queuedBytes -= entry.bytes;
100
+ return Promise.resolve(JSON.stringify(entry.message));
101
+ }
102
+ if (connection.waiter) return Promise.reject(new Error("concurrent reads are unsupported"));
103
+ return new Promise((resolve) => {
104
+ const timer = setTimeout(() => {
105
+ connection.waiter = undefined;
106
+ resolve(JSON.stringify({ kind: "timeout" }));
107
+ }, timeoutMs);
108
+ connection.waiter = (message) => {
109
+ clearTimeout(timer);
110
+ connection.waiter = undefined;
111
+ resolve(JSON.stringify(message));
112
+ };
113
+ });
114
+ }
115
+
116
+ function close(handle) {
117
+ const connection = connections.get(handle);
118
+ if (!connection) return;
119
+ connections.delete(handle);
120
+ connection.intentionallyClosed = true;
121
+ connection.waiter?.({ kind: "closed", detail: "by the WASM runtime" });
122
+ connection.socket.close();
123
+ }
124
+
125
+ function enqueue(connection, message) {
126
+ if (connection.overflowed) return;
127
+ if (connection.waiter) {
128
+ connection.waiter(message);
129
+ return;
130
+ }
131
+ const bytes = encoder.encode(message.kind === "text" ? message.text : JSON.stringify(message)).byteLength;
132
+ if (connection.queue.length >= maxQueuedMessages || connection.queuedBytes + bytes > maxQueuedBytes) {
133
+ connection.queue.length = 0;
134
+ connection.queuedBytes = 0;
135
+ connection.overflowed = true;
136
+ const error = {
137
+ kind: "error",
138
+ detail: `receive queue exceeded ${maxQueuedMessages} messages or ${maxQueuedBytes} bytes`,
139
+ };
140
+ const errorBytes = encoder.encode(JSON.stringify(error)).byteLength;
141
+ connection.queue.push({ message: error, bytes: errorBytes });
142
+ connection.queuedBytes = errorBytes;
143
+ connection.socket.close(1009, "receive queue exceeded configured bounds");
144
+ return;
145
+ }
146
+ connection.queue.push({ message, bytes });
147
+ connection.queuedBytes += bytes;
148
+ }
149
+
150
+ return Object.freeze({
151
+ connect,
152
+ send,
153
+ next,
154
+ close,
155
+ sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
156
+ executeCode: code.executeCode,
157
+ toolDefinitions: code.toolDefinitions,
158
+ emitEvent: onEvent,
159
+ reset: code.reset,
160
+ });
161
+ }
@@ -0,0 +1,13 @@
1
+ export { Actions } from "../index.mjs";
2
+ export type {
3
+ AgentEvent,
4
+ PromptInput,
5
+ PromptItem,
6
+ ReasoningMode,
7
+ Thinking,
8
+ Tool,
9
+ ToolContext,
10
+ ToolMap,
11
+ Turn,
12
+ } from "../types.mjs";
13
+ export * as Agent from "./Agent.mjs";
@@ -0,0 +1,2 @@
1
+ export { Actions } from "../index.mjs";
2
+ export * as Agent from "./Agent.mjs";
package/index.d.mts ADDED
@@ -0,0 +1,19 @@
1
+ export * as Actions from "./actions/index.mjs";
2
+ export type {
3
+ Agent,
4
+ AgentActions,
5
+ AgentEvent,
6
+ AgentOptions,
7
+ DefaultAgent,
8
+ EventWatcher,
9
+ ForkOptions,
10
+ PromptInput,
11
+ PromptItem,
12
+ ReasoningMode,
13
+ Thinking,
14
+ Tool,
15
+ ToolContext,
16
+ ToolMap,
17
+ Turn,
18
+ WatchEventsOptions,
19
+ } from "./types.mjs";
package/index.mjs ADDED
@@ -0,0 +1 @@
1
+ export * as Actions from "./actions/index.mjs";