operon-agents 0.1.0-alpha.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 (71) hide show
  1. package/LICENSE +55 -0
  2. package/README.md +18 -0
  3. package/dist/app-server/client.d.ts +80 -0
  4. package/dist/app-server/client.js +187 -0
  5. package/dist/app-server/codec.d.ts +52 -0
  6. package/dist/app-server/codec.js +252 -0
  7. package/dist/app-server/index.d.ts +22 -0
  8. package/dist/app-server/index.js +22 -0
  9. package/dist/app-server/main.d.ts +3 -0
  10. package/dist/app-server/main.js +85 -0
  11. package/dist/app-server/protocol.d.ts +217 -0
  12. package/dist/app-server/protocol.js +100 -0
  13. package/dist/app-server/server.d.ts +51 -0
  14. package/dist/app-server/server.js +274 -0
  15. package/dist/core.d.ts +12 -0
  16. package/dist/core.js +12 -0
  17. package/dist/cron/clock.d.ts +11 -0
  18. package/dist/cron/clock.js +19 -0
  19. package/dist/cron/cron-expr.d.ts +15 -0
  20. package/dist/cron/cron-expr.js +267 -0
  21. package/dist/cron/extension.d.ts +25 -0
  22. package/dist/cron/extension.js +141 -0
  23. package/dist/cron/index.d.ts +20 -0
  24. package/dist/cron/index.js +11 -0
  25. package/dist/cron/jitter.d.ts +18 -0
  26. package/dist/cron/jitter.js +46 -0
  27. package/dist/cron/manager.d.ts +55 -0
  28. package/dist/cron/manager.js +123 -0
  29. package/dist/cron/persist.d.ts +10 -0
  30. package/dist/cron/persist.js +22 -0
  31. package/dist/cron/scheduler.d.ts +25 -0
  32. package/dist/cron/scheduler.js +192 -0
  33. package/dist/cron/session-store.d.ts +14 -0
  34. package/dist/cron/session-store.js +50 -0
  35. package/dist/cron/time-format.d.ts +2 -0
  36. package/dist/cron/time-format.js +12 -0
  37. package/dist/cron/tools.d.ts +7 -0
  38. package/dist/cron/tools.js +161 -0
  39. package/dist/cron/types.d.ts +9 -0
  40. package/dist/cron/types.js +2 -0
  41. package/dist/extensions/index.d.ts +23 -0
  42. package/dist/extensions/index.js +41 -0
  43. package/dist/extensions/loader.d.ts +121 -0
  44. package/dist/extensions/loader.js +285 -0
  45. package/dist/extensions/manager.d.ts +103 -0
  46. package/dist/extensions/manager.js +190 -0
  47. package/dist/extensions/runtime.d.ts +188 -0
  48. package/dist/extensions/runtime.js +1127 -0
  49. package/dist/extensions/services.d.ts +85 -0
  50. package/dist/extensions/services.js +265 -0
  51. package/dist/extensions/types.d.ts +594 -0
  52. package/dist/extensions/types.js +2 -0
  53. package/dist/harness.d.ts +746 -0
  54. package/dist/harness.js +1483 -0
  55. package/dist/index.d.ts +24 -0
  56. package/dist/index.js +29 -0
  57. package/dist/internal.d.ts +9 -0
  58. package/dist/internal.js +9 -0
  59. package/dist/llm.d.ts +2 -0
  60. package/dist/llm.js +2 -0
  61. package/dist/local.d.ts +24 -0
  62. package/dist/local.js +58 -0
  63. package/dist/mcp.d.ts +2 -0
  64. package/dist/mcp.js +2 -0
  65. package/dist/protocol.d.ts +2 -0
  66. package/dist/protocol.js +2 -0
  67. package/dist/tools.d.ts +2 -0
  68. package/dist/tools.js +2 -0
  69. package/dist/tracing.d.ts +2 -0
  70. package/dist/tracing.js +2 -0
  71. package/package.json +78 -0
package/LICENSE ADDED
@@ -0,0 +1,55 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yingjie Qiao
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+
24
+ -------------------------------------------------------------------------------
25
+
26
+ THIRD-PARTY LICENSES
27
+
28
+ Portions of this software were adapted from kimi-code, which is distributed
29
+ under the MIT License. Its copyright and permission notice is reproduced below.
30
+
31
+ -------------------------------------------------------------------------------
32
+
33
+ kimi-code
34
+
35
+ MIT License
36
+
37
+ Copyright (c) 2026 Moonshot AI
38
+
39
+ Permission is hereby granted, free of charge, to any person obtaining a copy
40
+ of this software and associated documentation files (the "Software"), to deal
41
+ in the Software without restriction, including without limitation the rights
42
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
43
+ copies of the Software, and to permit persons to whom the Software is
44
+ furnished to do so, subject to the following conditions:
45
+
46
+ The above copyright notice and this permission notice shall be included in all
47
+ copies or substantial portions of the Software.
48
+
49
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
50
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
51
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
52
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
53
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
54
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
55
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # operon-agents
2
+
3
+ Public SDK entrypoint for building agents.
4
+
5
+ ```ts
6
+ import { Agent, Runner, Session, ChatModel, tool } from "operon-agents";
7
+ ```
8
+
9
+ Use `operon-agents` from applications. `operon-agents-core` is the lower-level
10
+ engine package.
11
+
12
+ Focused subpaths are available for larger capability areas:
13
+
14
+ ```ts
15
+ import { TransportMCPServer } from "operon-agents/mcp";
16
+ import { bashTool, readTool } from "operon-agents/tools";
17
+ import { OTelTracingProcessor } from "operon-agents/tracing";
18
+ ```
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Reference TypeScript client for the app-server protocol.
3
+ *
4
+ * It is both (1) a drop-in-ish, Harness-shaped wrapper around a spawned
5
+ * `operon-app-server` process and (2) the executable spec other-language clients
6
+ * mirror. The transport is injectable, so it also runs over `pairedTransports`
7
+ * in-process for tests.
8
+ */
9
+ import { type ChildProcess } from "node:child_process";
10
+ import type { AgentEvent, ApprovalRequest, ApprovalResponse, QuestionRequest, QuestionResult, RunResult, SessionSummary } from "operon-agents-core";
11
+ import { type JsonRpcTransport } from "./codec.ts";
12
+ import { type ClientCapabilities, type InitializeResult, type SessionFollowUpResult, type SessionInfo, type SessionSnapshotResult, type SessionSteerResult } from "./protocol.ts";
13
+ export type ApprovalHandler = (request: ApprovalRequest, sessionId: string) => Promise<ApprovalResponse> | ApprovalResponse;
14
+ export type QuestionHandler = (request: QuestionRequest, sessionId: string, options: {
15
+ readonly signal: AbortSignal;
16
+ }) => Promise<QuestionResult> | QuestionResult;
17
+ export type EventListener = (event: AgentEvent) => void;
18
+ export interface SpawnOptions {
19
+ readonly command?: string;
20
+ readonly args?: readonly string[];
21
+ readonly env?: Readonly<Record<string, string | undefined>>;
22
+ readonly cwd?: string;
23
+ }
24
+ export declare class AppServerClient {
25
+ private readonly transport;
26
+ private readonly child?;
27
+ private readonly pending;
28
+ private readonly eventListeners;
29
+ private readonly inboundAbort;
30
+ private seq;
31
+ private approvalHandler;
32
+ private questionHandler;
33
+ private closed;
34
+ constructor(transport: JsonRpcTransport, child?: ChildProcess);
35
+ /** Spawn the app-server binary and connect over its stdio. */
36
+ static spawn(opts?: SpawnOptions): AppServerClient;
37
+ private readLoop;
38
+ private onResponse;
39
+ private onNotification;
40
+ private onReverseRequest;
41
+ private dispatchReverse;
42
+ private request;
43
+ private failAllPending;
44
+ initialize(capabilities?: ClientCapabilities, info?: {
45
+ name: string;
46
+ version?: string;
47
+ }): Promise<InitializeResult>;
48
+ newSession(opts?: {
49
+ workDir?: string;
50
+ title?: string;
51
+ model?: string;
52
+ }): Promise<SessionInfo>;
53
+ resumeSession(sessionId: string): Promise<SessionInfo>;
54
+ forkSession(sessionId: string, opts?: {
55
+ title?: string;
56
+ }): Promise<SessionInfo>;
57
+ listSessions(): Promise<readonly SessionSummary[]>;
58
+ closeSession(sessionId: string): Promise<unknown>;
59
+ prompt(sessionId: string, input: string): Promise<RunResult>;
60
+ /** The session's folded state. Exact late-join seam: events received before this
61
+ * resolves are already in the snapshot; events after it are not (see protocol doc). */
62
+ snapshot(sessionId: string, opts?: {
63
+ addresses?: readonly string[];
64
+ maxMessages?: number;
65
+ }): Promise<SessionSnapshotResult>;
66
+ /** Continue a durably-interrupted run: `answers` maps each pending `toolCallId` (from the
67
+ * prior result's `interruptions`) to an approval decision. The session must be open. */
68
+ respond(sessionId: string, answers: Record<string, ApprovalResponse>): Promise<RunResult>;
69
+ steer(sessionId: string, text: string): Promise<SessionSteerResult>;
70
+ followUp(sessionId: string, text: string): Promise<SessionFollowUpResult>;
71
+ cancel(sessionId: string): Promise<unknown>;
72
+ /** Call a whitelisted control-plane method (see `INVOKABLE_METHODS`). */
73
+ invoke<R = unknown>(sessionId: string, method: string, ...args: unknown[]): Promise<R>;
74
+ onEvent(listener: EventListener): () => void;
75
+ setApprovalHandler(handler: ApprovalHandler | undefined): void;
76
+ setQuestionHandler(handler: QuestionHandler | undefined): void;
77
+ /** Close the transport and (if spawned) the child process. */
78
+ close(): void;
79
+ }
80
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Reference TypeScript client for the app-server protocol.
3
+ *
4
+ * It is both (1) a drop-in-ish, Harness-shaped wrapper around a spawned
5
+ * `operon-app-server` process and (2) the executable spec other-language clients
6
+ * mirror. The transport is injectable, so it also runs over `pairedTransports`
7
+ * in-process for tests.
8
+ */
9
+ import { spawn } from "node:child_process";
10
+ import { classify, isFailure, nodeStreamTransport, request as makeRequest, RpcError, success, } from "./codec.js";
11
+ import { Method, PROTOCOL_VERSION, } from "./protocol.js";
12
+ export class AppServerClient {
13
+ transport;
14
+ child;
15
+ pending = new Map();
16
+ eventListeners = new Set();
17
+ inboundAbort = new Map();
18
+ seq = 0;
19
+ approvalHandler;
20
+ questionHandler;
21
+ closed = false;
22
+ constructor(transport, child) {
23
+ this.transport = transport;
24
+ this.child = child;
25
+ void this.readLoop();
26
+ }
27
+ /** Spawn the app-server binary and connect over its stdio. */
28
+ static spawn(opts = {}) {
29
+ const command = opts.command ?? process.execPath;
30
+ const args = [...(opts.args ?? [])];
31
+ const child = spawn(command, args, {
32
+ stdio: ["pipe", "pipe", "inherit"],
33
+ ...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
34
+ env: { ...process.env, ...opts.env },
35
+ });
36
+ const transport = nodeStreamTransport(child.stdout, child.stdin);
37
+ return new AppServerClient(transport, child);
38
+ }
39
+ async readLoop() {
40
+ for await (const message of this.transport.incoming()) {
41
+ const c = classify(message);
42
+ if (c.kind === "response")
43
+ this.onResponse(c.message);
44
+ else if (c.kind === "request")
45
+ void this.onReverseRequest(c.message.id, c.message.method, c.message.params);
46
+ else if (c.kind === "notification")
47
+ this.onNotification(c.message.method, c.message.params);
48
+ }
49
+ this.failAllPending(new RpcError(-32011, "transport closed"));
50
+ }
51
+ onResponse(response) {
52
+ const pending = this.pending.get(response.id);
53
+ if (!pending)
54
+ return;
55
+ this.pending.delete(response.id);
56
+ if (isFailure(response))
57
+ pending.reject(new RpcError(response.error.code, response.error.message, response.error.data));
58
+ else
59
+ pending.resolve(response.result);
60
+ }
61
+ onNotification(method, params) {
62
+ if (method === Method.SessionEvent) {
63
+ const event = params;
64
+ for (const listener of this.eventListeners)
65
+ listener(event);
66
+ return;
67
+ }
68
+ if (method === Method.ClientCancelRequest) {
69
+ const { id } = (params ?? {});
70
+ this.inboundAbort.get(id)?.abort();
71
+ this.inboundAbort.delete(id);
72
+ }
73
+ }
74
+ async onReverseRequest(id, method, params) {
75
+ const controller = new AbortController();
76
+ this.inboundAbort.set(id, controller);
77
+ try {
78
+ const result = await this.dispatchReverse(method, params, controller.signal);
79
+ this.transport.send(success(id, result));
80
+ }
81
+ catch (error) {
82
+ const body = error instanceof RpcError ? error.toBody() : { code: -32603, message: error instanceof Error ? error.message : String(error) };
83
+ this.transport.send({ jsonrpc: "2.0", id, error: body });
84
+ }
85
+ finally {
86
+ this.inboundAbort.delete(id);
87
+ }
88
+ }
89
+ async dispatchReverse(method, params, signal) {
90
+ if (method === Method.ClientRequestApproval) {
91
+ const p = params;
92
+ if (!this.approvalHandler)
93
+ return { decision: "rejected", feedback: "no approval handler" };
94
+ return this.approvalHandler(p.request, p.sessionId);
95
+ }
96
+ if (method === Method.ClientRequestQuestion) {
97
+ const p = params;
98
+ if (!this.questionHandler)
99
+ return null;
100
+ return this.questionHandler(p.request, p.sessionId, { signal });
101
+ }
102
+ throw new RpcError(-32601, `unknown reverse method: ${method}`);
103
+ }
104
+ request(method, params) {
105
+ if (this.closed)
106
+ return Promise.reject(new RpcError(-32011, "client closed"));
107
+ const id = `c-${++this.seq}`;
108
+ return new Promise((resolve, reject) => {
109
+ this.pending.set(id, { resolve: resolve, reject });
110
+ this.transport.send(makeRequest(id, method, params));
111
+ });
112
+ }
113
+ failAllPending(error) {
114
+ for (const pending of this.pending.values())
115
+ pending.reject(error);
116
+ this.pending.clear();
117
+ }
118
+ // ── Public API (mirrors the in-process Harness) ──────────────────────────────
119
+ initialize(capabilities = { approval: true, question: true }, info) {
120
+ return this.request(Method.Initialize, {
121
+ protocolVersion: PROTOCOL_VERSION,
122
+ clientInfo: info ?? { name: "app-server-client", version: "0.0.0" },
123
+ clientCapabilities: capabilities,
124
+ });
125
+ }
126
+ newSession(opts = {}) {
127
+ return this.request(Method.SessionNew, opts);
128
+ }
129
+ resumeSession(sessionId) {
130
+ return this.request(Method.SessionResume, { sessionId });
131
+ }
132
+ forkSession(sessionId, opts = {}) {
133
+ return this.request(Method.SessionFork, { sessionId, ...opts });
134
+ }
135
+ listSessions() {
136
+ return this.request(Method.SessionList, {});
137
+ }
138
+ closeSession(sessionId) {
139
+ return this.request(Method.SessionClose, { sessionId });
140
+ }
141
+ prompt(sessionId, input) {
142
+ return this.request(Method.SessionPrompt, { sessionId, input });
143
+ }
144
+ /** The session's folded state. Exact late-join seam: events received before this
145
+ * resolves are already in the snapshot; events after it are not (see protocol doc). */
146
+ snapshot(sessionId, opts = {}) {
147
+ return this.request(Method.SessionSnapshot, { sessionId, ...opts });
148
+ }
149
+ /** Continue a durably-interrupted run: `answers` maps each pending `toolCallId` (from the
150
+ * prior result's `interruptions`) to an approval decision. The session must be open. */
151
+ respond(sessionId, answers) {
152
+ return this.request(Method.SessionRespond, { sessionId, answers });
153
+ }
154
+ steer(sessionId, text) {
155
+ return this.request(Method.SessionSteer, { sessionId, text });
156
+ }
157
+ followUp(sessionId, text) {
158
+ return this.request(Method.SessionFollowUp, { sessionId, text });
159
+ }
160
+ cancel(sessionId) {
161
+ return this.request(Method.SessionCancel, { sessionId });
162
+ }
163
+ /** Call a whitelisted control-plane method (see `INVOKABLE_METHODS`). */
164
+ invoke(sessionId, method, ...args) {
165
+ return this.request(Method.SessionInvoke, { sessionId, method, args }).then((r) => r.value);
166
+ }
167
+ onEvent(listener) {
168
+ this.eventListeners.add(listener);
169
+ return () => this.eventListeners.delete(listener);
170
+ }
171
+ setApprovalHandler(handler) {
172
+ this.approvalHandler = handler;
173
+ }
174
+ setQuestionHandler(handler) {
175
+ this.questionHandler = handler;
176
+ }
177
+ /** Close the transport and (if spawned) the child process. */
178
+ close() {
179
+ if (this.closed)
180
+ return;
181
+ this.closed = true;
182
+ this.failAllPending(new RpcError(-32011, "client closed"));
183
+ this.transport.close();
184
+ this.child?.kill();
185
+ }
186
+ }
187
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,52 @@
1
+ import type { Readable, Writable } from "node:stream";
2
+ import { type JsonRpcErrorBody, type JsonRpcFailure, type JsonRpcId, type JsonRpcMessage, type JsonRpcNotification, type JsonRpcRequest, type JsonRpcResponse, type JsonRpcSuccess } from "./protocol.ts";
3
+ export interface JsonRpcTransport {
4
+ /** Queue a message for delivery (respects backpressure internally). */
5
+ send(message: JsonRpcMessage): void;
6
+ /** Inbound messages, in order, until the peer closes. Consume once. */
7
+ incoming(): AsyncIterable<JsonRpcMessage>;
8
+ /** Stop reading/writing and release resources. */
9
+ close(): void;
10
+ }
11
+ export type ClassifiedMessage = {
12
+ readonly kind: "request";
13
+ readonly message: JsonRpcRequest;
14
+ } | {
15
+ readonly kind: "response";
16
+ readonly message: JsonRpcResponse;
17
+ } | {
18
+ readonly kind: "notification";
19
+ readonly message: JsonRpcNotification;
20
+ } | {
21
+ readonly kind: "invalid";
22
+ readonly raw: unknown;
23
+ };
24
+ export declare function classify(raw: unknown): ClassifiedMessage;
25
+ export declare function isFailure(response: JsonRpcResponse): response is JsonRpcFailure;
26
+ export declare function request(id: JsonRpcId, method: string, params?: unknown): JsonRpcRequest;
27
+ export declare function notification(method: string, params?: unknown): JsonRpcNotification;
28
+ export declare function success(id: JsonRpcId, result: unknown): JsonRpcSuccess;
29
+ export declare function failure(id: JsonRpcId, error: JsonRpcErrorBody): JsonRpcFailure;
30
+ export declare class RpcError extends Error {
31
+ readonly code: number;
32
+ readonly data?: unknown;
33
+ constructor(code: number, message: string, data?: unknown);
34
+ toBody(): JsonRpcErrorBody;
35
+ }
36
+ /** Map any thrown value to a JSON-RPC error body. */
37
+ export declare function errorToBody(error: unknown): JsonRpcErrorBody;
38
+ /** Recursively coerce a value into something `JSON.stringify` can handle without
39
+ * throwing or leaking host objects: Error → plain object, circular refs and
40
+ * functions/symbols/undefined dropped, depth bounded. */
41
+ export declare function toWireSafe(value: unknown, seen?: WeakSet<object>, depth?: number): unknown;
42
+ export declare function encode(message: JsonRpcMessage): string;
43
+ /** NDJSON JSON-RPC over a Readable/Writable pair (e.g. a child's stdout/stdin, or
44
+ * this process's stdin/stdout). Malformed lines are dropped with an onError hook. */
45
+ export declare function nodeStreamTransport(input: Readable, output: Writable, opts?: {
46
+ readonly onParseError?: (line: string, error: unknown) => void;
47
+ }): JsonRpcTransport;
48
+ /** Two transports wired back-to-back: what `a` sends, `b` receives, and vice
49
+ * versa. Messages still round-trip through `toWireSafe` so tests exercise the
50
+ * same serialization path as real stdio. */
51
+ export declare function pairedTransports(): readonly [JsonRpcTransport, JsonRpcTransport];
52
+ //# sourceMappingURL=codec.d.ts.map
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Transport + framing for the app-server protocol.
3
+ *
4
+ * - `JsonRpcTransport` is the seam the server and client both sit on, so neither
5
+ * hardcodes stdio: real processes use `nodeStreamTransport`, tests use
6
+ * `pairedTransports`.
7
+ * - NDJSON framing: one JSON value per line. `JSON.stringify` never emits a bare
8
+ * newline, so a line is always one whole message.
9
+ * - `toWireSafe` sanitizes values before they cross the wire (Error → plain
10
+ * object, circular refs / functions / undefined dropped) so a stray `error.cause`
11
+ * or a tool display can't crash serialization.
12
+ */
13
+ import { createInterface } from "node:readline";
14
+ import { isGuardrailTripwireError } from "operon-agents-core";
15
+ import { ErrorCode, } from "./protocol.js";
16
+ export function classify(raw) {
17
+ if (typeof raw !== "object" || raw === null)
18
+ return { kind: "invalid", raw };
19
+ const m = raw;
20
+ const hasId = "id" in m && (typeof m.id === "string" || typeof m.id === "number");
21
+ const hasMethod = typeof m.method === "string";
22
+ const hasResult = "result" in m;
23
+ const hasError = "error" in m;
24
+ if (hasMethod && hasId)
25
+ return { kind: "request", message: raw };
26
+ if (hasMethod)
27
+ return { kind: "notification", message: raw };
28
+ if (hasId && (hasResult || hasError))
29
+ return { kind: "response", message: raw };
30
+ return { kind: "invalid", raw };
31
+ }
32
+ export function isFailure(response) {
33
+ return "error" in response;
34
+ }
35
+ // ── Envelope builders ────────────────────────────────────────────────────────
36
+ export function request(id, method, params) {
37
+ return params === undefined ? { jsonrpc: "2.0", id, method } : { jsonrpc: "2.0", id, method, params };
38
+ }
39
+ export function notification(method, params) {
40
+ return params === undefined ? { jsonrpc: "2.0", method } : { jsonrpc: "2.0", method, params };
41
+ }
42
+ export function success(id, result) {
43
+ return { jsonrpc: "2.0", id, result: result === undefined ? null : result };
44
+ }
45
+ export function failure(id, error) {
46
+ return { jsonrpc: "2.0", id, error };
47
+ }
48
+ // ── Errors ───────────────────────────────────────────────────────────────────
49
+ export class RpcError extends Error {
50
+ code;
51
+ data;
52
+ constructor(code, message, data) {
53
+ super(message);
54
+ this.name = "RpcError";
55
+ this.code = code;
56
+ this.data = data;
57
+ }
58
+ toBody() {
59
+ return this.data === undefined
60
+ ? { code: this.code, message: this.message }
61
+ : { code: this.code, message: this.message, data: toWireSafe(this.data) };
62
+ }
63
+ }
64
+ /** Map any thrown value to a JSON-RPC error body. */
65
+ export function errorToBody(error) {
66
+ if (error instanceof RpcError)
67
+ return error.toBody();
68
+ // A guardrail tripwire is a policy block, not an internal failure — give it a distinct code
69
+ // and structured data so the host can distinguish it (and locate the run) from a crash.
70
+ if (isGuardrailTripwireError(error)) {
71
+ return {
72
+ code: ErrorCode.GuardrailBlocked,
73
+ message: error.message,
74
+ data: toWireSafe({
75
+ stage: error.stage,
76
+ guardrail: error.guardrailName,
77
+ agent: error.agentName,
78
+ sessionId: error.sessionId,
79
+ address: error.address,
80
+ }),
81
+ };
82
+ }
83
+ const message = error instanceof Error ? error.message : String(error);
84
+ return { code: ErrorCode.InternalError, message };
85
+ }
86
+ // ── Wire-safe serialization ──────────────────────────────────────────────────
87
+ const MAX_DEPTH = 64;
88
+ /** Recursively coerce a value into something `JSON.stringify` can handle without
89
+ * throwing or leaking host objects: Error → plain object, circular refs and
90
+ * functions/symbols/undefined dropped, depth bounded. */
91
+ export function toWireSafe(value, seen = new WeakSet(), depth = 0) {
92
+ if (value === null)
93
+ return null;
94
+ const t = typeof value;
95
+ if (t === "string" || t === "boolean")
96
+ return value;
97
+ if (t === "number")
98
+ return Number.isFinite(value) ? value : null;
99
+ if (t === "bigint")
100
+ return value.toString();
101
+ if (t === "undefined" || t === "function" || t === "symbol")
102
+ return undefined;
103
+ if (value instanceof Error) {
104
+ return { name: value.name, message: value.message, stack: value.stack };
105
+ }
106
+ if (depth >= MAX_DEPTH)
107
+ return undefined;
108
+ if (Array.isArray(value)) {
109
+ if (seen.has(value))
110
+ return undefined;
111
+ seen.add(value);
112
+ const out = value.map((v) => {
113
+ const safe = toWireSafe(v, seen, depth + 1);
114
+ return safe === undefined ? null : safe;
115
+ });
116
+ seen.delete(value);
117
+ return out;
118
+ }
119
+ if (t === "object") {
120
+ const obj = value;
121
+ if (seen.has(obj))
122
+ return undefined;
123
+ // Honor toJSON (Date, etc.) before generic object walking.
124
+ const maybe = obj;
125
+ if (typeof maybe.toJSON === "function")
126
+ return toWireSafe(maybe.toJSON(), seen, depth + 1);
127
+ seen.add(obj);
128
+ const out = {};
129
+ for (const key of Object.keys(obj)) {
130
+ const safe = toWireSafe(obj[key], seen, depth + 1);
131
+ if (safe !== undefined)
132
+ out[key] = safe;
133
+ }
134
+ seen.delete(obj);
135
+ return out;
136
+ }
137
+ return undefined;
138
+ }
139
+ export function encode(message) {
140
+ return `${JSON.stringify(toWireSafe(message))}\n`;
141
+ }
142
+ // ── A minimal async queue (single producer / single consumer) ────────────────
143
+ class AsyncQueue {
144
+ buffer = [];
145
+ waiters = [];
146
+ closed = false;
147
+ push(value) {
148
+ if (this.closed)
149
+ return;
150
+ const waiter = this.waiters.shift();
151
+ if (waiter)
152
+ waiter({ value, done: false });
153
+ else
154
+ this.buffer.push(value);
155
+ }
156
+ close() {
157
+ if (this.closed)
158
+ return;
159
+ this.closed = true;
160
+ for (const w of this.waiters.splice(0))
161
+ w({ value: undefined, done: true });
162
+ }
163
+ [Symbol.asyncIterator]() {
164
+ return {
165
+ next: () => {
166
+ const next = this.buffer.shift();
167
+ if (next !== undefined)
168
+ return Promise.resolve({ value: next, done: false });
169
+ if (this.closed)
170
+ return Promise.resolve({ value: undefined, done: true });
171
+ return new Promise((resolve) => this.waiters.push(resolve));
172
+ },
173
+ };
174
+ }
175
+ }
176
+ // ── Node stdio transport ─────────────────────────────────────────────────────
177
+ /** NDJSON JSON-RPC over a Readable/Writable pair (e.g. a child's stdout/stdin, or
178
+ * this process's stdin/stdout). Malformed lines are dropped with an onError hook. */
179
+ export function nodeStreamTransport(input, output, opts = {}) {
180
+ const queue = new AsyncQueue();
181
+ const rl = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });
182
+ rl.on("line", (line) => {
183
+ const trimmed = line.trim();
184
+ if (trimmed.length === 0)
185
+ return;
186
+ let parsed;
187
+ try {
188
+ parsed = JSON.parse(trimmed);
189
+ }
190
+ catch (error) {
191
+ opts.onParseError?.(trimmed, error);
192
+ return;
193
+ }
194
+ queue.push(parsed);
195
+ });
196
+ rl.once("close", () => queue.close());
197
+ input.once("error", () => queue.close());
198
+ // Serialize writes; respect backpressure by chaining on `drain`.
199
+ let writeChain = Promise.resolve();
200
+ let closed = false;
201
+ const writeLine = (text) => {
202
+ writeChain = writeChain.then(() => new Promise((resolve) => {
203
+ if (closed || output.writableEnded)
204
+ return resolve();
205
+ const ok = output.write(text);
206
+ if (ok)
207
+ resolve();
208
+ else
209
+ output.once("drain", () => resolve());
210
+ }));
211
+ };
212
+ return {
213
+ send(message) {
214
+ writeLine(encode(message));
215
+ },
216
+ incoming() {
217
+ return queue;
218
+ },
219
+ close() {
220
+ closed = true;
221
+ rl.close();
222
+ queue.close();
223
+ },
224
+ };
225
+ }
226
+ // ── Paired in-memory transports (for tests / embedding) ──────────────────────
227
+ /** Two transports wired back-to-back: what `a` sends, `b` receives, and vice
228
+ * versa. Messages still round-trip through `toWireSafe` so tests exercise the
229
+ * same serialization path as real stdio. */
230
+ export function pairedTransports() {
231
+ const aIn = new AsyncQueue();
232
+ const bIn = new AsyncQueue();
233
+ const hop = (message) => JSON.parse(JSON.stringify(toWireSafe(message)));
234
+ const a = {
235
+ send: (m) => bIn.push(hop(m)),
236
+ incoming: () => aIn,
237
+ close: () => {
238
+ aIn.close();
239
+ bIn.close();
240
+ },
241
+ };
242
+ const b = {
243
+ send: (m) => aIn.push(hop(m)),
244
+ incoming: () => bIn,
245
+ close: () => {
246
+ aIn.close();
247
+ bIn.close();
248
+ },
249
+ };
250
+ return [a, b];
251
+ }
252
+ //# sourceMappingURL=codec.js.map
@@ -0,0 +1,22 @@
1
+ /**
2
+ * app-server — drive the harness from another process (any language) over an
3
+ * NDJSON JSON-RPC 2.0 stdio protocol, so non-JS hosts can `spawn` the binary
4
+ * and talk to it.
5
+ *
6
+ * // host side (TypeScript reference client)
7
+ * import { AppServerClient } from "operon-agents/app-server"
8
+ * const client = AppServerClient.spawn({ args: ["--workdir", "/repo", "--model", "anthropic/claude-opus-4-8"] })
9
+ * await client.initialize()
10
+ * const { sessionId } = await client.newSession()
11
+ * client.onEvent(render)
12
+ * client.setApprovalHandler(askUser)
13
+ * const result = await client.prompt(sessionId, "fix the bug")
14
+ *
15
+ * `./protocol` (wire types + constants) is the single source of truth for clients
16
+ * in other languages.
17
+ */
18
+ export * from "./protocol.ts";
19
+ export { type JsonRpcTransport, nodeStreamTransport, pairedTransports, RpcError, toWireSafe, classify, } from "./codec.ts";
20
+ export { AppServer, type AppServerOptions } from "./server.ts";
21
+ export { AppServerClient, type ApprovalHandler, type EventListener, type QuestionHandler, type SpawnOptions, } from "./client.ts";
22
+ //# sourceMappingURL=index.d.ts.map