@roll-agent/client-node 0.1.0 → 0.2.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 +38 -0
- package/dist/index.d.ts +23 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
# `@roll-agent/client-node`
|
|
2
2
|
|
|
3
3
|
面向 Node.js、Electron 主进程和本地 Companion 的 Roll Runtime Protocol v1 客户端。
|
|
4
|
+
注册 Runtime→Client handler 时优先协商 `"1.1"`,否则保持 `"1.0"` 兼容路径。
|
|
4
5
|
|
|
5
6
|
它负责启动或连接 Runtime、初始化协商、JSON-RPC 请求、流式事件、协议校验、未知 Turn
|
|
6
7
|
结果跟踪,以及可等待的有界子进程关闭。Renderer 不应直接持有该客户端或子进程句柄。
|
|
7
8
|
|
|
9
|
+
本包只解决本地 Runtime 连接。它不包含 `@roll-agent/companion`、Cloud Relay 或远程访问
|
|
10
|
+
能力;Local-only Desktop 只需要本包。只有产品需要让远程 Web 访问用户本机 Runtime 时,
|
|
11
|
+
才在独立的本机 Host 层增加 `@roll-agent/companion`。
|
|
12
|
+
|
|
13
|
+
`RollNodeClient.start()` 默认执行 `roll runtime serve --stdio`。宿主必须确保 `roll` 命令
|
|
14
|
+
已安装,或通过 `command` 指向随应用分发的 Runtime;这仍然只建立本地 stdio 连接。
|
|
15
|
+
|
|
8
16
|
## 安装
|
|
9
17
|
|
|
10
18
|
```bash
|
|
@@ -19,6 +27,13 @@ import { RollNodeClient } from "@roll-agent/client-node";
|
|
|
19
27
|
|
|
20
28
|
const client = await RollNodeClient.start({
|
|
21
29
|
cwd: "/absolute/path/to/workspace",
|
|
30
|
+
serverRequestHandlers: {
|
|
31
|
+
"approval.request": async ({ approval }, { signal }) => {
|
|
32
|
+
signal.throwIfAborted();
|
|
33
|
+
console.error(`Approval required: ${approval.toolName}`);
|
|
34
|
+
return { decision: "reject", reason: "最小示例不自动批准 Tool" };
|
|
35
|
+
},
|
|
36
|
+
},
|
|
22
37
|
onTurnOutcomeUnknown: (turnId) => {
|
|
23
38
|
console.error(`Turn ${turnId} 的结果未知;不要自动重放`);
|
|
24
39
|
},
|
|
@@ -44,6 +59,29 @@ try {
|
|
|
44
59
|
`onTurnOutcomeUnknown()` 作为本地活动状态的收敛信号:停止 Working 状态、结束本地 waiter、
|
|
45
60
|
保留部分输出,随后在健康连接上或重连后读取 Snapshot。未知副作用不能自动重放。
|
|
46
61
|
|
|
62
|
+
## Runtime→Client Request
|
|
63
|
+
|
|
64
|
+
- `serverRequestHandlers` 覆盖目标版本的全部必需方法时,Client 才广告该版本;`"1.1"`
|
|
65
|
+
当前唯一必需方法是 `approval.request`,否则只广告 `["1.0"]`;
|
|
66
|
+
- `"1.1"` 的 `approval.request` handler 是唯一审批写入路径,
|
|
67
|
+
`approval.required` 只是只读 View Event;
|
|
68
|
+
- Handler context 提供 `requestId` 和 `AbortSignal`。Runtime cancel、Client shutdown
|
|
69
|
+
或连接退出都会 abort signal,并抑制迟到 Response;
|
|
70
|
+
- 用户拒绝必须返回正常 `{ decision: "reject" }`;Handler 缺失、抛错或返回非法结果会
|
|
71
|
+
让 Runtime 以系统失败终止当前 Turn,不会伪装成 `user_rejected`;
|
|
72
|
+
- `approval.resolved` 用于关闭或更新审批 View;
|
|
73
|
+
- 已协商 `"1.0"` 后不能通过 `registerServerRequestHandler()` 动态升级;应在
|
|
74
|
+
`start()` / `connect()` 时传入初始 handler;
|
|
75
|
+
- 已协商 `"1.1"` 后可以原子替换 handler;旧 handler 的 disposer 不会误删替代者。
|
|
76
|
+
如果卸载当前版本的必需 handler,Client 会把连接视为协议能力失效并执行有界关闭,
|
|
77
|
+
不会继续以 `"1.1"` 返回 `Method not found`。
|
|
78
|
+
|
|
79
|
+
未显式传入 `clientVersion` 时,初始化信息直接读取当前安装的
|
|
80
|
+
`@roll-agent/client-node` 包版本,不需要在源码与 `package.json` 之间手工同步。
|
|
81
|
+
|
|
82
|
+
当前 `stdio` Transport 不提供 Request replay/resume。断线后终止本地交互,并通过
|
|
83
|
+
Snapshot 收敛;不要重放旧 Approval 或 Turn。
|
|
84
|
+
|
|
47
85
|
## 文档
|
|
48
86
|
|
|
49
87
|
- [Node 客户端 API 参考](https://github.com/steveoon/roll-agent/blob/main/docs/client-node-reference.md)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Readable, Writable } from "node:stream";
|
|
2
|
-
import { type InitializeResult, type RuntimeEventEnvelope, type RuntimeMethod, type RuntimeMethodInput, type RuntimeMethodResult, type RuntimeProtocolErrorData, type TurnId } from "@roll-agent/protocol";
|
|
2
|
+
import { type InitializeResult, type JsonRpcId, type RuntimeEventEnvelope, type RuntimeMethod, type RuntimeMethodInput, type RuntimeMethodResult, type RuntimeProtocolErrorData, type RuntimeServerRequestMethod, type RuntimeServerRequestParams, type RuntimeServerRequestResult, type TurnId } from "@roll-agent/protocol";
|
|
3
3
|
export interface RuntimeClientTransport {
|
|
4
4
|
readonly stdin: Writable;
|
|
5
5
|
readonly stdout: Readable;
|
|
@@ -14,6 +14,14 @@ export interface RuntimeShutdownOptions {
|
|
|
14
14
|
readonly terminateTimeoutMs?: number;
|
|
15
15
|
readonly forceKillTimeoutMs?: number;
|
|
16
16
|
}
|
|
17
|
+
export interface RuntimeServerRequestContext {
|
|
18
|
+
readonly requestId: JsonRpcId;
|
|
19
|
+
readonly signal: AbortSignal;
|
|
20
|
+
}
|
|
21
|
+
export type RuntimeServerRequestHandler<TMethod extends RuntimeServerRequestMethod> = (params: RuntimeServerRequestParams<TMethod>, context: RuntimeServerRequestContext) => RuntimeServerRequestResult<TMethod> | Promise<RuntimeServerRequestResult<TMethod>>;
|
|
22
|
+
export type RuntimeServerRequestHandlers = {
|
|
23
|
+
readonly [TMethod in RuntimeServerRequestMethod]?: RuntimeServerRequestHandler<TMethod>;
|
|
24
|
+
};
|
|
17
25
|
export interface RollNodeClientOptions {
|
|
18
26
|
readonly cwd: string;
|
|
19
27
|
readonly command?: string;
|
|
@@ -28,6 +36,7 @@ export interface RollNodeClientOptions {
|
|
|
28
36
|
readonly maxReadRetries?: number;
|
|
29
37
|
readonly readRetryDelayMs?: number;
|
|
30
38
|
readonly shutdownOptions?: RuntimeShutdownOptions;
|
|
39
|
+
readonly serverRequestHandlers?: RuntimeServerRequestHandlers;
|
|
31
40
|
}
|
|
32
41
|
export interface ConnectRuntimeClientOptions {
|
|
33
42
|
readonly transport: RuntimeClientTransport;
|
|
@@ -40,6 +49,7 @@ export interface ConnectRuntimeClientOptions {
|
|
|
40
49
|
readonly maxReadRetries?: number;
|
|
41
50
|
readonly readRetryDelayMs?: number;
|
|
42
51
|
readonly shutdownOptions?: RuntimeShutdownOptions;
|
|
52
|
+
readonly serverRequestHandlers?: RuntimeServerRequestHandlers;
|
|
43
53
|
}
|
|
44
54
|
export interface RuntimeClientExit {
|
|
45
55
|
readonly code: number | null;
|
|
@@ -98,6 +108,9 @@ export declare class RollNodeClient {
|
|
|
98
108
|
private readonly exitListeners;
|
|
99
109
|
private readonly activeTurns;
|
|
100
110
|
private readonly unknownTurns;
|
|
111
|
+
private readonly serverRequestHandlers;
|
|
112
|
+
private readonly advertisedProtocolVersions;
|
|
113
|
+
private readonly inFlightServerRequests;
|
|
101
114
|
private readonly onTurnOutcomeUnknown;
|
|
102
115
|
private readonly requestTimeoutMs;
|
|
103
116
|
private readonly maxReadRetries;
|
|
@@ -123,6 +136,7 @@ export declare class RollNodeClient {
|
|
|
123
136
|
onEvent(listener: (event: RuntimeEventEnvelope) => void): () => void;
|
|
124
137
|
onExit(listener: (exit: RuntimeClientExit) => void): () => void;
|
|
125
138
|
getInitializationResult(): InitializeResult;
|
|
139
|
+
registerServerRequestHandler<TMethod extends RuntimeServerRequestMethod>(method: TMethod, handler: RuntimeServerRequestHandler<TMethod>): () => void;
|
|
126
140
|
request<TMethod extends RuntimeMethod>(method: TMethod, input: RuntimeMethodInput<TMethod>): Promise<RuntimeMethodResult<TMethod>>;
|
|
127
141
|
private requestOnce;
|
|
128
142
|
getOutcomeUnknownTurnIds(): readonly TurnId[];
|
|
@@ -131,8 +145,16 @@ export declare class RollNodeClient {
|
|
|
131
145
|
private performShutdown;
|
|
132
146
|
private write;
|
|
133
147
|
private handleLine;
|
|
148
|
+
private handleNotification;
|
|
149
|
+
private handleServerRequest;
|
|
150
|
+
private observeServerRequestResponse;
|
|
151
|
+
private dispatchServerRequest;
|
|
152
|
+
private isServerRequestActive;
|
|
153
|
+
private writeServerRequestError;
|
|
154
|
+
private acceptInitializationResult;
|
|
134
155
|
private handleEvent;
|
|
135
156
|
private handleExit;
|
|
157
|
+
private abortServerRequests;
|
|
136
158
|
private failProtocol;
|
|
137
159
|
private failConnection;
|
|
138
160
|
private closeOutputReader;
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{spawn as e}from"node:child_process";import{once as t}from"node:events";import{createInterface as r}from"node:readline";import{RUNTIME_EVENT_NOTIFICATION as o,RUNTIME_METHODS as i,RUNTIME_PROTOCOL_VERSION as n,parseRuntimeMethodParams as s,parseRuntimeMethodResult as l,runtimeEventEnvelopeSchema as a,runtimeProtocolErrorDataSchema as u}from"@roll-agent/protocol";const d=4194304,c=1,h=100,m=3e4,R=3e4,w=1e4,f=5e3,p=new Set([i.threadList,i.threadSnapshot,i.threadCapabilities,i.operationGet]);export class RollRpcError extends Error{code;data;constructor(e){super(e.message),this.name="RollRpcError",this.code=e.code,this.data=e.data}}export class RollRuntimeExitedError extends Error{code;signal;constructor(e,t){super(`Roll Runtime exited before the request completed (code=${String(e)}, signal=${String(t)})`),this.name="RollRuntimeExitedError",this.code=e,this.signal=t}}export class RollRequestTimeoutError extends Error{method;timeoutMs;constructor(e,t){super(`Roll Runtime request "${e}" timed out after ${String(t)} ms`),this.name="RollRequestTimeoutError",this.method=e,this.timeoutMs=t}}export class RollRequestFrameTooLargeError extends Error{frameBytes;maxFrameBytes;constructor(e,t){super(`Roll Runtime request frame is ${String(e)} bytes, exceeding the ${String(t)} byte limit`),this.name="RollRequestFrameTooLargeError",this.frameBytes=e,this.maxFrameBytes=t}}export class RollRuntimeClosingError extends Error{constructor(){super("Roll Runtime client is shutting down"),this.name="RollRuntimeClosingError"}}export class RollRuntimeShutdownTimeoutError extends Error{gracefulTimeoutMs;terminateTimeoutMs;forceKillTimeoutMs;constructor(e,t,r){super(`Roll Runtime did not exit within ${String(e+t+r)} ms`),this.name="RollRuntimeShutdownTimeoutError",this.gracefulTimeoutMs=e,this.terminateTimeoutMs=t,this.forceKillTimeoutMs=r}}export class RollProtocolViolationError extends Error{constructor(e,t){super(e,void 0===t?void 0:{cause:t}),this.name="RollProtocolViolationError"}}export class RollUncorrelatedRpcError extends RollRpcError{constructor(e){super(e),this.name="RollUncorrelatedRpcError"}}function x(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function v(e){return"string"==typeof e||"number"==typeof e&&Number.isFinite(e)}function E(e,t){if(!Number.isInteger(e)||e<=0)throw new Error(`${t} must be a positive integer`);return e}function y(e={}){return{gracefulTimeoutMs:E(e.gracefulTimeoutMs??R,"gracefulTimeoutMs"),terminateTimeoutMs:E(e.terminateTimeoutMs??w,"terminateTimeoutMs"),forceKillTimeoutMs:E(e.forceKillTimeoutMs??f,"forceKillTimeoutMs")}}function g(e,t){if(!x(e)||"number"!=typeof e.code||!Number.isInteger(e.code)||"string"!=typeof e.message)throw new RollProtocolViolationError(`Roll Runtime returned an invalid error for ${t}`);const r=void 0===e.data?void 0:u.safeParse(e.data);if(void 0!==r&&!r.success)throw new RollProtocolViolationError(`Roll Runtime returned invalid error data for ${t}`,r.error);return{code:e.code,message:e.message,...!0===r?.success?{data:r.data}:{}}}function T(e,t){return new Promise((r,o)=>{const i=setTimeout(()=>r(void 0),t);e.then(e=>{clearTimeout(i),r(e)},e=>{clearTimeout(i),o(e instanceof Error?e:new Error(String(e)))})})}function M(e){return{stdin:e.stdin,stdout:e.stdout,stderr:e.stderr,onExit(t){let r=!1;const o=(e,o)=>{r||(r=!0,t(e,o))};e.once("close",o),e.once("error",()=>o(null,null))},close(){e.stdin.end()},terminate(){null===e.exitCode&&null===e.signalCode&&e.kill("SIGTERM")},forceClose(){null===e.exitCode&&null===e.signalCode&&e.kill("SIGKILL")}}}export class RollNodeClient{transport;maxFrameBytes;outboundMaxFrameBytes;pending=new Map;listeners=new Set;exitListeners=new Set;activeTurns=new Set;unknownTurns=new Set;onTurnOutcomeUnknown;requestTimeoutMs;maxReadRetries;readRetryDelayMs;defaultShutdownOptions;outputReader;stderrReader;requestId=0;writeQueue=Promise.resolve();exitPromise;resolveExit;exited=!1;closing=!1;outputReaderClosed=!1;stderrReaderClosed=!1;initializationResult;connectionFailure;exitResult;shutdownPromise;constructor(e){if(this.transport=e.transport,this.maxFrameBytes=e.maxFrameBytes??d,this.outboundMaxFrameBytes=this.maxFrameBytes,this.onTurnOutcomeUnknown=e.onTurnOutcomeUnknown,this.requestTimeoutMs=e.requestTimeoutMs??m,this.maxReadRetries=e.maxReadRetries??1,this.readRetryDelayMs=e.readRetryDelayMs??h,this.defaultShutdownOptions=y(e.shutdownOptions),!Number.isInteger(this.maxFrameBytes)||this.maxFrameBytes<=0)throw new Error("maxFrameBytes must be a positive integer");if(!Number.isInteger(this.requestTimeoutMs)||this.requestTimeoutMs<=0)throw new Error("requestTimeoutMs must be a positive integer");if(!Number.isInteger(this.maxReadRetries)||this.maxReadRetries<0)throw new Error("maxReadRetries must be a non-negative integer");if(!Number.isInteger(this.readRetryDelayMs)||this.readRetryDelayMs<0)throw new Error("readRetryDelayMs must be a non-negative integer");this.exitPromise=new Promise(e=>{this.resolveExit=e}),this.outputReader=r({input:e.transport.stdout}),this.outputReader.on("line",e=>this.handleLine(e)),this.stderrReader=void 0===e.transport.stderr?void 0:r({input:e.transport.stderr}),this.stderrReader?.on("line",t=>e.onStderr?.(t)),this.transport.onExit((e,t)=>this.handleExit(e,t))}static async start(t){if(0===t.cwd.trim().length)throw new Error("RollNodeClient requires an explicit non-empty cwd");const r=e(t.command??"roll",[...t.args??["runtime","serve","--stdio"]],{cwd:t.cwd,env:t.env??process.env,shell:!1,stdio:"pipe"});return await RollNodeClient.connect({transport:M(r),...void 0!==t.clientName?{clientName:t.clientName}:{},...void 0!==t.clientVersion?{clientVersion:t.clientVersion}:{},...void 0!==t.onStderr?{onStderr:t.onStderr}:{},...void 0!==t.onTurnOutcomeUnknown?{onTurnOutcomeUnknown:t.onTurnOutcomeUnknown}:{},...void 0!==t.maxFrameBytes?{maxFrameBytes:t.maxFrameBytes}:{},...void 0!==t.requestTimeoutMs?{requestTimeoutMs:t.requestTimeoutMs}:{},...void 0!==t.maxReadRetries?{maxReadRetries:t.maxReadRetries}:{},...void 0!==t.readRetryDelayMs?{readRetryDelayMs:t.readRetryDelayMs}:{},...void 0!==t.shutdownOptions?{shutdownOptions:t.shutdownOptions}:{}})}static async connect(e){let t;try{return t=new RollNodeClient(e),t.initializationResult=await t.request(i.initialize,{protocolVersions:[n],client:{name:e.clientName??"@roll-agent/client-node",version:e.clientVersion??"0.1.0"}}),t.outboundMaxFrameBytes=Math.min(t.maxFrameBytes,t.initializationResult.limits.maxFrameBytes),t}catch(r){throw void 0===t?e.transport.close():await t.shutdown().catch(()=>{}),r}}onEvent(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}onExit(e){const t=this.exitResult;return void 0!==t?(queueMicrotask(()=>e(t)),()=>{}):(this.exitListeners.add(e),()=>{this.exitListeners.delete(e)})}getInitializationResult(){if(void 0===this.initializationResult)throw new Error("Roll Runtime client has not completed initialization");return this.initializationResult}async request(e,t){if(void 0!==this.connectionFailure)throw this.connectionFailure;if(this.exited)throw this.exitResult?.error??new RollRuntimeExitedError(null,null);if(this.closing)throw new RollRuntimeClosingError;const r=s(e,t),o=e===i.turnStart?s(i.turnStart,r).turnId:void 0;void 0!==o&&this.activeTurns.add(o);let n=0;for(;;)try{return await this.requestOnce(e,r)}catch(t){if(!p.has(e)||!(t instanceof RollRpcError)||!0!==t.data?.retryable||n>=this.maxReadRetries)throw void 0!==o&&(t instanceof RollRpcError&&!(t instanceof RollUncorrelatedRpcError)||t instanceof RollRequestFrameTooLargeError?this.activeTurns.delete(o):this.markTurnOutcomeUnknown(o)),t;n+=1,await new Promise(e=>{setTimeout(e,this.readRetryDelayMs)})}}async requestOnce(e,t){const r=this.requestId;this.requestId+=1;const o=new Promise((t,o)=>{const i=setTimeout(()=>{this.pending.delete(r)&&o(new RollRequestTimeoutError(e,this.requestTimeoutMs))},this.requestTimeoutMs);this.pending.set(r,{method:e,resolve:t,reject:o,timer:i})});try{await this.write({jsonrpc:"2.0",id:r,method:e,params:t});const i=await o;try{return l(e,i)}catch(t){const r=new RollProtocolViolationError(`Roll Runtime returned an invalid result for "${e}"`,t);throw this.failProtocol(r),r}}catch(e){const t=this.pending.get(r);throw void 0!==t&&(clearTimeout(t.timer),this.pending.delete(r)),e}}getOutcomeUnknownTurnIds(){return[...this.unknownTurns]}close(){this.shutdown().catch(()=>{})}shutdown(e){const t=this.exitResult;return void 0!==t?Promise.resolve(t):(this.shutdownPromise??=this.performShutdown(void 0===e?this.defaultShutdownOptions:y(e)),this.shutdownPromise)}async performShutdown(e){const{gracefulTimeoutMs:t,terminateTimeoutMs:r,forceKillTimeoutMs:o}=e;this.closing=!0,this.transport.close();const i=await T(this.exitPromise,t);if(void 0!==i)return i;if(void 0!==this.transport.terminate){this.transport.terminate();const e=await T(this.exitPromise,r);if(void 0!==e)return e}if(void 0===this.transport.forceClose){const e=new RollRuntimeShutdownTimeoutError(t,r,o);throw this.finishExit(e,null,null),this.closeReaders(),e}this.transport.forceClose();const n=await T(this.exitPromise,o);if(void 0!==n)return n;const s=new RollRuntimeShutdownTimeoutError(t,r,o);throw this.finishExit(s,null,null),this.closeReaders(),s}async write(e){const r=JSON.stringify(e),o=Buffer.byteLength(r,"utf8");if(o>this.outboundMaxFrameBytes)throw new RollRequestFrameTooLargeError(o,this.outboundMaxFrameBytes);const i=`${r}\n`;this.writeQueue=this.writeQueue.then(async()=>{this.transport.stdin.write(i)||await t(this.transport.stdin,"drain")});try{await this.writeQueue}catch(e){const t=new RollProtocolViolationError("Failed to write to the Roll Runtime transport",e);throw this.failProtocol(t),t}}handleLine(e){if(Buffer.byteLength(e,"utf8")>this.maxFrameBytes)return void this.failProtocol(new RollProtocolViolationError(`Roll Runtime frame exceeds ${String(this.maxFrameBytes)} bytes`));let t;try{t=JSON.parse(e)}catch(e){return void this.failProtocol(new RollProtocolViolationError("Roll Runtime emitted invalid JSON",e))}if(!x(t)||"2.0"!==t.jsonrpc)return void this.failProtocol(new RollProtocolViolationError("Roll Runtime emitted an invalid JSON-RPC frame"));if(t.method===o){const e=a.safeParse(t.params);return e.success?void this.handleEvent(e.data):void this.failProtocol(new RollProtocolViolationError("Roll Runtime emitted an invalid runtime.event",e.error))}if(null===t.id){if(!Object.hasOwn(t,"error")||Object.hasOwn(t,"result"))return void this.failProtocol(new RollProtocolViolationError("Roll Runtime response with a null id must contain exactly one error"));try{this.failConnection(new RollUncorrelatedRpcError(g(t.error,"an uncorrelated request")))}catch(e){this.failProtocol(e instanceof RollProtocolViolationError?e:new RollProtocolViolationError("Roll Runtime returned an invalid uncorrelated error",e))}return}if(!v(t.id))return void this.failProtocol(new RollProtocolViolationError("Roll Runtime response has an invalid id"));const r=this.pending.get(t.id);if(void 0===r)return;const i=void 0!==t.error;if(i!==Object.hasOwn(t,"result")){if(i){let e;try{e=new RollRpcError(g(t.error,`"${r.method}"`))}catch(e){return void this.failProtocol(e instanceof RollProtocolViolationError?e:new RollProtocolViolationError(`Roll Runtime returned an invalid error for "${r.method}"`,e))}return this.pending.delete(t.id),clearTimeout(r.timer),void r.reject(e)}this.pending.delete(t.id),clearTimeout(r.timer),r.resolve(t.result)}else this.failProtocol(new RollProtocolViolationError(`Roll Runtime response for "${r.method}" must contain exactly one result or error`))}handleEvent(e){void 0===e.turnId||"turn.completed"!==e.event.type&&"turn.cancelled"!==e.event.type&&"turn.failed"!==e.event.type||this.activeTurns.delete(e.turnId);for(const t of this.listeners)t(e)}handleExit(e,t){this.finishExit(this.connectionFailure??new RollRuntimeExitedError(e,t),e,t),this.closeReaders()}failProtocol(e){this.failConnection(e)}failConnection(e){void 0!==this.connectionFailure||this.exited||(this.connectionFailure=e,this.closing=!0,this.rejectPending(e),this.markActiveTurnsOutcomeUnknown(),this.closeOutputReader(),this.shutdown().catch(()=>{}))}closeOutputReader(){this.outputReaderClosed||(this.outputReaderClosed=!0,this.outputReader.close())}closeReaders(){this.closeOutputReader(),void 0===this.stderrReader||this.stderrReaderClosed||(this.stderrReaderClosed=!0,this.stderrReader.close())}finishExit(e,t,r){if(!this.exited){this.exited=!0,this.closing=!0,this.rejectPending(e),this.markActiveTurnsOutcomeUnknown(),this.exitResult={code:t,signal:r,error:e},this.resolveExit(this.exitResult);for(const e of this.exitListeners)e(this.exitResult);this.exitListeners.clear()}}rejectPending(e){for(const t of this.pending.values())clearTimeout(t.timer),t.reject(e);this.pending.clear()}markActiveTurnsOutcomeUnknown(){for(const e of this.activeTurns)this.markTurnOutcomeUnknown(e);this.activeTurns.clear()}markTurnOutcomeUnknown(e){this.activeTurns.delete(e),this.unknownTurns.has(e)||(this.unknownTurns.add(e),this.onTurnOutcomeUnknown?.(e))}}
|
|
1
|
+
import{spawn as e}from"node:child_process";import{once as t}from"node:events";import{createInterface as r}from"node:readline";import i from"../package.json"with{type:"json"};import{RUNTIME_EVENT_NOTIFICATION as o,RUNTIME_METHODS as s,RUNTIME_SERVER_REQUEST_CANCEL_NOTIFICATION as n,SUPPORTED_RUNTIME_PROTOCOL_VERSIONS as l,getRuntimeProtocolCapabilities as a,isRuntimeServerRequestMethod as u,isRuntimeServerRequestMethodRequired as c,parseRuntimeMethodParams as d,parseRuntimeMethodResult as h,parseRuntimeServerRequestParams as R,parseRuntimeServerRequestResult as m,runtimeEventEnvelopeSchema as v,runtimeProtocolErrorDataSchema as f,runtimeServerRequestCancelParamsSchema as w}from"@roll-agent/protocol";const p=4194304,g=1,E=100,y=3e4,q=3e4,x=1e4,P=5e3;function S(e){if("object"!=typeof e||null===e||!("version"in e)||"string"!=typeof e.version||0===e.version.trim().length)throw new Error("@roll-agent/client-node package metadata is missing a valid version");return e.version}const T=S(i),M={methodNotFound:-32601,invalidParams:-32602,internalError:-32603},F=new Set([s.threadList,s.threadSnapshot,s.threadCapabilities,s.operationGet]);function V(e,t){return a(e).requiredServerRequestMethods.every(e=>"function"==typeof t[e])}export class RollRpcError extends Error{code;data;constructor(e){super(e.message),this.name="RollRpcError",this.code=e.code,this.data=e.data}}export class RollRuntimeExitedError extends Error{code;signal;constructor(e,t){super(`Roll Runtime exited before the request completed (code=${String(e)}, signal=${String(t)})`),this.name="RollRuntimeExitedError",this.code=e,this.signal=t}}export class RollRequestTimeoutError extends Error{method;timeoutMs;constructor(e,t){super(`Roll Runtime request "${e}" timed out after ${String(t)} ms`),this.name="RollRequestTimeoutError",this.method=e,this.timeoutMs=t}}export class RollRequestFrameTooLargeError extends Error{frameBytes;maxFrameBytes;constructor(e,t){super(`Roll Runtime request frame is ${String(e)} bytes, exceeding the ${String(t)} byte limit`),this.name="RollRequestFrameTooLargeError",this.frameBytes=e,this.maxFrameBytes=t}}export class RollRuntimeClosingError extends Error{constructor(){super("Roll Runtime client is shutting down"),this.name="RollRuntimeClosingError"}}export class RollRuntimeShutdownTimeoutError extends Error{gracefulTimeoutMs;terminateTimeoutMs;forceKillTimeoutMs;constructor(e,t,r){super(`Roll Runtime did not exit within ${String(e+t+r)} ms`),this.name="RollRuntimeShutdownTimeoutError",this.gracefulTimeoutMs=e,this.terminateTimeoutMs=t,this.forceKillTimeoutMs=r}}export class RollProtocolViolationError extends Error{constructor(e,t){super(e,void 0===t?void 0:{cause:t}),this.name="RollProtocolViolationError"}}export class RollUncorrelatedRpcError extends RollRpcError{constructor(e){super(e),this.name="RollUncorrelatedRpcError"}}function b(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function O(e){return"string"==typeof e||"number"==typeof e&&Number.isFinite(e)}function C(e,t){if(!Number.isInteger(e)||e<=0)throw new Error(`${t} must be a positive integer`);return e}function k(e={}){return{gracefulTimeoutMs:C(e.gracefulTimeoutMs??q,"gracefulTimeoutMs"),terminateTimeoutMs:C(e.terminateTimeoutMs??x,"terminateTimeoutMs"),forceKillTimeoutMs:C(e.forceKillTimeoutMs??P,"forceKillTimeoutMs")}}function N(e,t){if(!b(e)||"number"!=typeof e.code||!Number.isInteger(e.code)||"string"!=typeof e.message)throw new RollProtocolViolationError(`Roll Runtime returned an invalid error for ${t}`);const r=void 0===e.data?void 0:f.safeParse(e.data);if(void 0!==r&&!r.success)throw new RollProtocolViolationError(`Roll Runtime returned invalid error data for ${t}`,r.error);return{code:e.code,message:e.message,...!0===r?.success?{data:r.data}:{}}}function z(e,t){return new Promise((r,i)=>{const o=setTimeout(()=>r(void 0),t);e.then(e=>{clearTimeout(o),r(e)},e=>{clearTimeout(o),i(e instanceof Error?e:new Error(String(e)))})})}function I(e){return{stdin:e.stdin,stdout:e.stdout,stderr:e.stderr,onExit(t){let r=!1;const i=(e,i)=>{r||(r=!0,t(e,i))};e.once("close",i),e.once("error",()=>i(null,null))},close(){e.stdin.end()},terminate(){null===e.exitCode&&null===e.signalCode&&e.kill("SIGTERM")},forceClose(){null===e.exitCode&&null===e.signalCode&&e.kill("SIGKILL")}}}export class RollNodeClient{transport;maxFrameBytes;outboundMaxFrameBytes;pending=new Map;listeners=new Set;exitListeners=new Set;activeTurns=new Set;unknownTurns=new Set;serverRequestHandlers;advertisedProtocolVersions;inFlightServerRequests=new Map;onTurnOutcomeUnknown;requestTimeoutMs;maxReadRetries;readRetryDelayMs;defaultShutdownOptions;outputReader;stderrReader;requestId=0;writeQueue=Promise.resolve();exitPromise;resolveExit;exited=!1;closing=!1;outputReaderClosed=!1;stderrReaderClosed=!1;initializationResult;connectionFailure;exitResult;shutdownPromise;constructor(e){if(this.transport=e.transport,this.maxFrameBytes=e.maxFrameBytes??p,this.outboundMaxFrameBytes=this.maxFrameBytes,this.onTurnOutcomeUnknown=e.onTurnOutcomeUnknown,this.requestTimeoutMs=e.requestTimeoutMs??y,this.maxReadRetries=e.maxReadRetries??1,this.readRetryDelayMs=e.readRetryDelayMs??E,this.defaultShutdownOptions=k(e.shutdownOptions),this.serverRequestHandlers={...e.serverRequestHandlers},this.advertisedProtocolVersions=l.filter(e=>V(e,this.serverRequestHandlers)),!Number.isInteger(this.maxFrameBytes)||this.maxFrameBytes<=0)throw new Error("maxFrameBytes must be a positive integer");if(!Number.isInteger(this.requestTimeoutMs)||this.requestTimeoutMs<=0)throw new Error("requestTimeoutMs must be a positive integer");if(!Number.isInteger(this.maxReadRetries)||this.maxReadRetries<0)throw new Error("maxReadRetries must be a non-negative integer");if(!Number.isInteger(this.readRetryDelayMs)||this.readRetryDelayMs<0)throw new Error("readRetryDelayMs must be a non-negative integer");this.exitPromise=new Promise(e=>{this.resolveExit=e}),this.outputReader=r({input:e.transport.stdout}),this.outputReader.on("line",e=>this.handleLine(e)),this.stderrReader=void 0===e.transport.stderr?void 0:r({input:e.transport.stderr}),this.stderrReader?.on("line",t=>{try{e.onStderr?.(t)}catch{}}),this.transport.onExit((e,t)=>this.handleExit(e,t))}static async start(t){if(0===t.cwd.trim().length)throw new Error("RollNodeClient requires an explicit non-empty cwd");const r=e(t.command??"roll",[...t.args??["runtime","serve","--stdio"]],{cwd:t.cwd,env:t.env??process.env,shell:!1,stdio:"pipe"});return await RollNodeClient.connect({transport:I(r),...void 0!==t.clientName?{clientName:t.clientName}:{},...void 0!==t.clientVersion?{clientVersion:t.clientVersion}:{},...void 0!==t.onStderr?{onStderr:t.onStderr}:{},...void 0!==t.onTurnOutcomeUnknown?{onTurnOutcomeUnknown:t.onTurnOutcomeUnknown}:{},...void 0!==t.maxFrameBytes?{maxFrameBytes:t.maxFrameBytes}:{},...void 0!==t.requestTimeoutMs?{requestTimeoutMs:t.requestTimeoutMs}:{},...void 0!==t.maxReadRetries?{maxReadRetries:t.maxReadRetries}:{},...void 0!==t.readRetryDelayMs?{readRetryDelayMs:t.readRetryDelayMs}:{},...void 0!==t.shutdownOptions?{shutdownOptions:t.shutdownOptions}:{},...void 0!==t.serverRequestHandlers?{serverRequestHandlers:t.serverRequestHandlers}:{}})}static async connect(e){let t;try{return t=new RollNodeClient(e),t.initializationResult=await t.request(s.initialize,{protocolVersions:[...t.advertisedProtocolVersions],client:{name:e.clientName??"@roll-agent/client-node",version:e.clientVersion??T}}),t.acceptInitializationResult(t.initializationResult),t}catch(r){throw void 0===t?e.transport.close():await t.shutdown().catch(()=>{}),r}}onEvent(e){return this.listeners.add(e),()=>{this.listeners.delete(e)}}onExit(e){const t=this.exitResult;return void 0!==t?(queueMicrotask(()=>{try{e(t)}catch{}}),()=>{}):(this.exitListeners.add(e),()=>{this.exitListeners.delete(e)})}getInitializationResult(){if(void 0===this.initializationResult)throw new Error("Roll Runtime client has not completed initialization");return this.initializationResult}registerServerRequestHandler(e,t){if(!u(e))throw new Error(`Unknown Runtime server request method: ${String(e)}`);const r=this.initializationResult?.protocolVersion;if(void 0!==r&&!a(r).serverRequests)throw new Error(`Cannot register a Runtime server request handler after Protocol ${r} was negotiated; pass serverRequestHandlers to RollNodeClient.connect() or RollNodeClient.start()`);return this.serverRequestHandlers[e]=t,()=>{if(this.serverRequestHandlers[e]!==t)return;const r=this.initializationResult?.protocolVersion;void 0!==r&&c(r,e)?this.failProtocol(new RollProtocolViolationError(`Cannot unregister required Runtime server request handler "${e}" while Protocol ${r} is active`)):delete this.serverRequestHandlers[e]}}async request(e,t){if(void 0!==this.connectionFailure)throw this.connectionFailure;if(this.exited)throw this.exitResult?.error??new RollRuntimeExitedError(null,null);if(this.closing)throw new RollRuntimeClosingError;if(e===s.initialize&&void 0!==this.initializationResult)throw new Error("Roll Runtime client has already completed initialization");const r=d(e,t),i=e===s.turnStart?d(s.turnStart,r).turnId:void 0;void 0!==i&&this.activeTurns.add(i);let o=0;for(;;)try{return await this.requestOnce(e,r)}catch(t){if(!F.has(e)||!(t instanceof RollRpcError)||!0!==t.data?.retryable||o>=this.maxReadRetries)throw void 0!==i&&(t instanceof RollRpcError&&!(t instanceof RollUncorrelatedRpcError)||t instanceof RollRequestFrameTooLargeError?this.activeTurns.delete(i):this.markTurnOutcomeUnknown(i)),t;o+=1,await new Promise(e=>{setTimeout(e,this.readRetryDelayMs)})}}async requestOnce(e,t){const r=this.requestId;this.requestId+=1;const i=new Promise((t,i)=>{const o=setTimeout(()=>{this.pending.delete(r)&&i(new RollRequestTimeoutError(e,this.requestTimeoutMs))},this.requestTimeoutMs);this.pending.set(r,{method:e,resolve:t,reject:i,timer:o})});try{await this.write({jsonrpc:"2.0",id:r,method:e,params:t});const o=await i;try{return h(e,o)}catch(t){const r=new RollProtocolViolationError(`Roll Runtime returned an invalid result for "${e}"`,t);throw this.failProtocol(r),r}}catch(e){const t=this.pending.get(r);throw void 0!==t&&(clearTimeout(t.timer),this.pending.delete(r)),e}}getOutcomeUnknownTurnIds(){return[...this.unknownTurns]}close(){this.shutdown().catch(()=>{})}shutdown(e){const t=this.exitResult;return void 0!==t?Promise.resolve(t):(this.shutdownPromise??=this.performShutdown(void 0===e?this.defaultShutdownOptions:k(e)),this.shutdownPromise)}async performShutdown(e){const{gracefulTimeoutMs:t,terminateTimeoutMs:r,forceKillTimeoutMs:i}=e;this.closing=!0,this.abortServerRequests(new RollRuntimeClosingError),this.transport.close();const o=await z(this.exitPromise,t);if(void 0!==o)return o;if(void 0!==this.transport.terminate){this.transport.terminate();const e=await z(this.exitPromise,r);if(void 0!==e)return e}if(void 0===this.transport.forceClose){const e=new RollRuntimeShutdownTimeoutError(t,r,i);throw this.finishExit(e,null,null),this.closeReaders(),e}this.transport.forceClose();const s=await z(this.exitPromise,i);if(void 0!==s)return s;const n=new RollRuntimeShutdownTimeoutError(t,r,i);throw this.finishExit(n,null,null),this.closeReaders(),n}async write(e,r){const i=JSON.stringify(e),o=Buffer.byteLength(i,"utf8");if(o>this.outboundMaxFrameBytes)throw new RollRequestFrameTooLargeError(o,this.outboundMaxFrameBytes);const s=`${i}\n`;this.writeQueue=this.writeQueue.then(async()=>{this.closing||void 0!==this.connectionFailure||this.exited||!1===r?.()||this.transport.stdin.write(s)||await t(this.transport.stdin,"drain")});try{await this.writeQueue}catch(e){const t=new RollProtocolViolationError("Failed to write to the Roll Runtime transport",e);throw this.failProtocol(t),t}}handleLine(e){if(Buffer.byteLength(e,"utf8")>this.maxFrameBytes)return void this.failProtocol(new RollProtocolViolationError(`Roll Runtime frame exceeds ${String(this.maxFrameBytes)} bytes`));let t;try{t=JSON.parse(e)}catch(e){return void this.failProtocol(new RollProtocolViolationError("Roll Runtime emitted invalid JSON",e))}if(!b(t)||"2.0"!==t.jsonrpc)return void this.failProtocol(new RollProtocolViolationError("Roll Runtime emitted an invalid JSON-RPC frame"));const r=Object.hasOwn(t,"method"),i=Object.hasOwn(t,"id"),o=Object.hasOwn(t,"result"),n=Object.hasOwn(t,"error");if(r)return"string"!=typeof t.method||o||n?void this.failProtocol(new RollProtocolViolationError("Roll Runtime emitted an invalid JSON-RPC request or notification")):i?O(t.id)?void this.handleServerRequest(t.id,t.method,t.params):void this.failProtocol(new RollProtocolViolationError("Roll Runtime request has an invalid id")):void this.handleNotification(t.method,t.params);if(!i)return void this.failProtocol(new RollProtocolViolationError("Roll Runtime response is missing an id"));if(null===t.id){if(!n||o)return void this.failProtocol(new RollProtocolViolationError("Roll Runtime response with a null id must contain exactly one error"));try{this.failConnection(new RollUncorrelatedRpcError(N(t.error,"an uncorrelated request")))}catch(e){this.failProtocol(e instanceof RollProtocolViolationError?e:new RollProtocolViolationError("Roll Runtime returned an invalid uncorrelated error",e))}return}if(!O(t.id))return void this.failProtocol(new RollProtocolViolationError("Roll Runtime response has an invalid id"));const l=this.pending.get(t.id);if(void 0!==l)if(n!==o){if(n){let e;try{e=new RollRpcError(N(t.error,`"${l.method}"`))}catch(e){return void this.failProtocol(e instanceof RollProtocolViolationError?e:new RollProtocolViolationError(`Roll Runtime returned an invalid error for "${l.method}"`,e))}return this.pending.delete(t.id),clearTimeout(l.timer),void l.reject(e)}if(l.method===s.initialize)try{this.acceptInitializationResult(h(s.initialize,t.result))}catch(e){return void this.failProtocol(e instanceof RollProtocolViolationError?e:new RollProtocolViolationError('Roll Runtime returned an invalid result for "initialize"',e))}this.pending.delete(t.id),clearTimeout(l.timer),l.resolve(t.result)}else this.failProtocol(new RollProtocolViolationError(`Roll Runtime response for "${l.method}" must contain exactly one result or error`))}handleNotification(e,t){if(e===o){const e=v.safeParse(t);return e.success?void 0===this.initializationResult||e.data.protocolVersion!==this.initializationResult.protocolVersion?void this.failProtocol(new RollProtocolViolationError("Roll Runtime emitted runtime.event outside the negotiated Protocol")):void this.handleEvent(e.data):void this.failProtocol(new RollProtocolViolationError("Roll Runtime emitted an invalid runtime.event",e.error))}if(e===n){if(void 0===this.initializationResult||!a(this.initializationResult.protocolVersion).serverRequests)return void this.failProtocol(new RollProtocolViolationError("Roll Runtime emitted runtime.serverRequest.cancel outside a negotiated server-request capability"));const e=w.safeParse(t);if(!e.success)return void this.failProtocol(new RollProtocolViolationError("Roll Runtime emitted an invalid runtime.serverRequest.cancel",e.error));const r=this.inFlightServerRequests.get(e.data.serverRequestId);return void(void 0!==r&&(this.inFlightServerRequests.delete(e.data.serverRequestId),r.controller.abort(new Error(e.data.reason))))}this.failProtocol(new RollProtocolViolationError(`Roll Runtime emitted an unknown notification: ${e}`))}handleServerRequest(e,t,r){if(this.closing||void 0!==this.connectionFailure||this.exited)return;if(this.inFlightServerRequests.has(e))return void this.failProtocol(new RollProtocolViolationError(`Roll Runtime reused in-flight server request id ${JSON.stringify(e)}`));if(void 0===this.initializationResult||!a(this.initializationResult.protocolVersion).serverRequests||!u(t))return void this.observeServerRequestResponse(this.writeServerRequestError(e,M.methodNotFound,"Method not found"));const i=this.serverRequestHandlers[t];void 0!==i?this.observeServerRequestResponse(this.dispatchServerRequest(e,t,r,i)):this.observeServerRequestResponse(this.writeServerRequestError(e,M.methodNotFound,"Method not found"))}observeServerRequestResponse(e){e.catch(e=>{void 0!==this.connectionFailure||this.exited||this.failProtocol(new RollProtocolViolationError("Failed to respond to Runtime server request",e))})}async dispatchServerRequest(e,t,r,i){const o={controller:new AbortController};this.inFlightServerRequests.set(e,o);try{let s,n,l;try{s=R(t,r)}catch{return void await this.writeServerRequestError(e,M.invalidParams,"Invalid params",()=>this.isServerRequestActive(e,o))}try{n=await i(s,{requestId:e,signal:o.controller.signal})}catch{return void(this.isServerRequestActive(e,o)&&await this.writeServerRequestError(e,M.internalError,"Internal error",()=>this.isServerRequestActive(e,o)))}if(!this.isServerRequestActive(e,o))return;try{l=m(t,n)}catch{return void await this.writeServerRequestError(e,M.internalError,"Internal error",()=>this.isServerRequestActive(e,o))}this.isServerRequestActive(e,o)&&await this.write({jsonrpc:"2.0",id:e,result:l},()=>this.isServerRequestActive(e,o))}finally{this.inFlightServerRequests.get(e)===o&&this.inFlightServerRequests.delete(e)}}isServerRequestActive(e,t){return!t.controller.signal.aborted&&this.inFlightServerRequests.get(e)===t}async writeServerRequestError(e,t,r,i){await this.write({jsonrpc:"2.0",id:e,error:{code:t,message:r}},i)}acceptInitializationResult(e){if(!this.advertisedProtocolVersions.includes(e.protocolVersion))throw new RollProtocolViolationError(`Roll Runtime selected unadvertised Protocol ${e.protocolVersion}`);this.initializationResult=e,this.outboundMaxFrameBytes=Math.min(this.maxFrameBytes,e.limits.maxFrameBytes)}handleEvent(e){void 0===e.turnId||"turn.completed"!==e.event.type&&"turn.cancelled"!==e.event.type&&"turn.failed"!==e.event.type||this.activeTurns.delete(e.turnId);for(const t of this.listeners)try{t(e)}catch{}}handleExit(e,t){const r=this.connectionFailure??new RollRuntimeExitedError(e,t);this.abortServerRequests(r),this.finishExit(r,e,t),this.closeReaders()}abortServerRequests(e){const t=[...this.inFlightServerRequests.values()];this.inFlightServerRequests.clear();for(const r of t)r.controller.abort(e)}failProtocol(e){this.failConnection(e)}failConnection(e){void 0!==this.connectionFailure||this.exited||(this.connectionFailure=e,this.closing=!0,this.rejectPending(e),this.markActiveTurnsOutcomeUnknown(),this.closeOutputReader(),this.shutdown().catch(()=>{}))}closeOutputReader(){this.outputReaderClosed||(this.outputReaderClosed=!0,this.outputReader.close())}closeReaders(){this.closeOutputReader(),void 0===this.stderrReader||this.stderrReaderClosed||(this.stderrReaderClosed=!0,this.stderrReader.close())}finishExit(e,t,r){if(!this.exited){this.exited=!0,this.closing=!0,this.rejectPending(e),this.markActiveTurnsOutcomeUnknown(),this.exitResult={code:t,signal:r,error:e},this.resolveExit(this.exitResult);for(const e of this.exitListeners)try{e(this.exitResult)}catch{}this.exitListeners.clear()}}rejectPending(e){for(const t of this.pending.values())clearTimeout(t.timer),t.reject(e);this.pending.clear()}markActiveTurnsOutcomeUnknown(){for(const e of this.activeTurns)this.markTurnOutcomeUnknown(e);this.activeTurns.clear()}markTurnOutcomeUnknown(e){if(this.activeTurns.delete(e),!this.unknownTurns.has(e)){this.unknownTurns.add(e);try{this.onTurnOutcomeUnknown?.(e)}catch{}}}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roll-agent/client-node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"node": ">=22.6.0"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@roll-agent/protocol": "0.
|
|
29
|
+
"@roll-agent/protocol": "0.2.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^22.0.0"
|