@inixiative/agent-session 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,35 +10,154 @@
10
10
  // HarnessSession is stateful: it owns a running process, tracks turns,
11
11
  // captures every event for Oracle, and supports fork/interrupt.
12
12
  //
13
- // Implementations: ClaudeCodeSession (now), CodexSession / CursorSession (future)
13
+ // Implementations: ClaudeCodeSession, CodexMcpSession, CodexAppServerSession,
14
+ // ClaudeAgentSdkSession. How a model is reached is the session's transport
15
+ // (see transport.ts); every transport implements this one contract.
14
16
  // ---------------------------------------------------------------------------
15
17
 
18
+ import type { LimitSnapshot } from './limits';
19
+ import type { TransportDescriptor } from './transport';
20
+
16
21
  // ---------------------------------------------------------------------------
17
22
  // Event taxonomy — classified events from the agent stream
18
23
  // ---------------------------------------------------------------------------
19
24
 
25
+ /** IDs are native only when observed. admissionId is generated locally, never a native turn ID. */
26
+ export interface SessionIdentity {
27
+ readonly admissionId?: string;
28
+ readonly nativeSessionId?: string;
29
+ readonly threadId?: string;
30
+ readonly turnId?: string;
31
+ readonly itemId?: string;
32
+ readonly callId?: string;
33
+ readonly messageId?: string;
34
+ readonly rpcRequestId?: number;
35
+ readonly correlation?: 'native-turn' | 'ordered-stream' | 'unknown';
36
+ }
37
+ export type NativeOutcome = 'unknown' | 'completed' | 'failed';
38
+ export interface NativeTerminal {
39
+ readonly type: string;
40
+ readonly eventId?: string;
41
+ readonly turnId?: string;
42
+ readonly subtype?: string;
43
+ readonly reason?: string;
44
+ readonly apiErrorStatus?: number;
45
+ }
46
+ export interface SessionAttempt extends SessionIdentity {
47
+ readonly nativeOutcome: NativeOutcome;
48
+ readonly localOutcome: 'pending' | 'resolved' | 'rejected';
49
+ readonly dispatch: 'not-dispatched' | 'attempted';
50
+ readonly transportOutcome: 'open' | 'failed' | 'closed';
51
+ readonly terminal?: NativeTerminal;
52
+ readonly rpcOutcome?: 'pending' | 'resolved' | 'failed' | 'unknown';
53
+ readonly localFailure?:
54
+ | 'timeout'
55
+ | 'interrupt-request'
56
+ | 'transport'
57
+ | 'rpc'
58
+ | 'validation'
59
+ | 'blocked'
60
+ | 'killed'
61
+ | 'unrecognized-terminal'
62
+ | 'registration';
63
+ readonly content: string;
64
+ readonly events: readonly SessionEvent[];
65
+ readonly tokens?: SessionTokens;
66
+ }
67
+ /** A rejected local send can still have a later native terminal; read attempt again to reconcile. */
68
+ export class SessionTurnError extends Error {
69
+ constructor(
70
+ message: string,
71
+ private readonly evidence: () => SessionAttempt,
72
+ options?: ErrorOptions,
73
+ ) {
74
+ super(message, options);
75
+ this.name = 'SessionTurnError';
76
+ }
77
+ get attempt(): SessionAttempt {
78
+ return this.evidence();
79
+ }
80
+ }
81
+
20
82
  export type SessionEventKind =
21
- | "session_start"
22
- | "session_end"
23
- | "session_compact"
24
- | "text"
25
- | "tool_use"
26
- | "tool_result"
27
- | "thinking"
28
- | "result"
29
- | "error";
30
-
31
- export interface SessionEvent {
83
+ | 'text_delta'
84
+ | 'native_status'
85
+ | 'session_start'
86
+ | 'session_end'
87
+ | 'session_compact'
88
+ | 'text'
89
+ | 'tool_use'
90
+ | 'tool_result'
91
+ | 'thinking'
92
+ | 'usage'
93
+ | 'rate_limit'
94
+ | 'result'
95
+ | 'error';
96
+
97
+ /** Input excludes cache reads/writes when those disjoint counters are supplied.
98
+ * Thinking is a subset of output; TTL counters are subsets of cacheWrite.
99
+ * Missing optional counters mean the runtime did not report them.
100
+ */
101
+ export interface SessionTokens {
102
+ input: number;
103
+ output: number;
104
+ cacheRead?: number;
105
+ cacheWrite?: number;
106
+ cacheWrite5m?: number;
107
+ cacheWrite1h?: number;
108
+ thinking?: number;
109
+ /** Original provider usage, including tags and future fields. Never summed. */
110
+ providerUsage?: Readonly<Record<string, unknown>>;
111
+ }
112
+
113
+ export interface SessionEvent extends SessionIdentity {
114
+ /**
115
+ * Session-history evidence excluded from admission results and state transitions.
116
+ * "session-configuration": a native lifecycle/configuration envelope (Claude system/init)
117
+ * retained with its binding; it belongs to the session, never to an admission, and a
118
+ * repeated one is not evidence of compaction or restart. "duplicate-boundary": an explicit
119
+ * compaction boundary whose native uuid was already recorded.
120
+ */
121
+ readonly unattributedReason?:
122
+ | 'no-admission'
123
+ | 'foreign-session'
124
+ | 'foreign-turn'
125
+ | 'duplicate-terminal'
126
+ | 'after-terminal'
127
+ | 'unrecognized-event'
128
+ | 'session-configuration'
129
+ | 'duplicate-boundary'
130
+ | 'duplicate-tool'
131
+ | 'unmatched-tool'
132
+ | 'malformed-tool'
133
+ | 'account-status';
134
+ /** Local transport observation, not a native terminal acknowledgment. */
135
+ readonly transportOutcome?: SessionAttempt['transportOutcome'];
136
+ readonly nativeOutcome?: NativeOutcome;
137
+ readonly terminal?: NativeTerminal;
32
138
  readonly kind: SessionEventKind;
33
139
  readonly timestamp: number;
34
140
  /** Text content (for text, thinking, result, error). */
35
141
  readonly text?: string;
36
142
  /** Tool name (for tool_use). */
37
143
  readonly toolName?: string;
144
+ /** Native MCP invocation identity, separate from call ID and SDK request IDs.
145
+ * Display name is mcp__<server>__<tool>; use these fields for exact comparisons. */
146
+ readonly toolServer?: string;
147
+ readonly toolMethod?: string;
38
148
  /** Tool input arguments (for tool_use). */
39
149
  readonly toolInput?: Record<string, unknown>;
40
150
  /** Tool output (for tool_result). */
41
151
  readonly toolOutput?: string;
152
+ /** Public text content is retained; unsupported/non-text result payload is omitted. */
153
+ readonly toolOutputOmitted?: boolean;
154
+ /** Exact public tool names from `tool_reference` result blocks (tool search), in order,
155
+ * duplicates preserved; a separately owned guard judges ownership. Absent when none. */
156
+ readonly toolReferences?: readonly string[];
157
+ /** Public type labels of omitted non-text result blocks, in first-seen order; unknown or
158
+ * malformed shapes are "unsupported". Present only when toolOutputOmitted is true. */
159
+ readonly toolOutputOmittedTypes?: readonly string[];
160
+ readonly toolInputOmitted?: string;
42
161
  /** Whether the tool call failed (for tool_result). */
43
162
  readonly toolError?: boolean;
44
163
  /** Compaction source runtime (for session_compact): "claude-code" | "codex" | etc. */
@@ -49,30 +168,45 @@ export interface SessionEvent {
49
168
  * this is NOT the Foundry thread ID. A SessionAdapter maps between the two.
50
169
  */
51
170
  readonly externalSessionId?: string;
52
- /** Token usage (for result). */
53
- readonly tokens?: { input: number; output: number };
54
- /** Raw message from the stream (for Oracle introspection). */
171
+ /** Turn totals on result; request snapshots on usage. Do not sum both. */
172
+ readonly tokens?: SessionTokens;
173
+ /** Account-level usage limits (for rate_limit). Never attributed to an admission. */
174
+ readonly limits?: LimitSnapshot;
175
+ /** Owned, deeply frozen native JSON data (for Oracle introspection). */
55
176
  readonly raw?: unknown;
56
177
  }
57
178
 
58
179
  /** Result of a single send() — the turn's content plus all classified events. */
59
- export interface SessionResult {
180
+ export interface SessionResult extends SessionIdentity {
181
+ readonly localOutcome?: SessionAttempt['localOutcome'];
182
+ readonly transportOutcome?: SessionAttempt['transportOutcome'];
183
+ readonly rpcOutcome?: SessionAttempt['rpcOutcome'];
184
+ readonly nativeOutcome?: NativeOutcome;
185
+ readonly terminal?: NativeTerminal;
60
186
  readonly content: string;
61
187
  readonly events: readonly SessionEvent[];
62
- readonly tokens?: { input: number; output: number };
188
+ readonly tokens?: SessionTokens;
63
189
  /** The runtime's native session ID. See SessionEvent.externalSessionId. */
64
190
  readonly externalSessionId?: string;
65
191
  }
66
192
 
67
193
  /** Full session record for Oracle evaluation. */
194
+ export interface SessionDiagnostics {
195
+ /** Counts only: observer exception messages/objects are never retained. */
196
+ readonly observerFailures: { readonly synchronous: number; readonly asynchronous: number };
197
+ }
68
198
  export interface SessionArtifact {
199
+ readonly diagnostics?: SessionDiagnostics;
200
+ readonly attempts?: readonly SessionAttempt[];
201
+ /** Legacy totalTokens contains observed counts only, not proof of complete accounting. */
202
+ readonly accounting?: 'observed-only';
69
203
  /** The runtime's native session ID. See SessionEvent.externalSessionId. */
70
204
  readonly externalSessionId?: string;
71
205
  readonly events: readonly SessionEvent[];
72
206
  readonly startedAt: number;
73
207
  readonly endedAt?: number;
74
208
  readonly turns: number;
75
- readonly totalTokens: { input: number; output: number };
209
+ readonly totalTokens: SessionTokens;
76
210
  readonly toolCalls: number;
77
211
  readonly toolResults: number;
78
212
  readonly errors: number;
@@ -90,15 +224,39 @@ export type SessionEventHandler = (event: SessionEvent) => void;
90
224
  * computes "what's new since the last injection," formats it as a prefix,
91
225
  * and the composed message goes to send().
92
226
  */
93
- export type BeforeSendHook = (
94
- message: string,
95
- ) => string | Promise<string>;
227
+ export type BeforeSendHook = (message: string) => string | Promise<string>;
228
+
229
+ export interface SessionSendOptions {
230
+ timeout?: number;
231
+ /** Required ownership registration, not an observer. Rejection prevents the native write. */
232
+ onAdmission?: (attempt: SessionAttempt) => void | Promise<void>;
233
+ }
96
234
 
97
235
  // ---------------------------------------------------------------------------
98
236
  // HarnessSession interface
99
237
  // ---------------------------------------------------------------------------
100
238
 
239
+ /** `acknowledged`: the runtime confirmed the stop and the turn reached a native terminal. */
240
+ export type NativeInterruptOutcome = 'acknowledged' | 'no-turn' | 'unacknowledged';
241
+
101
242
  export interface HarnessSession {
243
+ /** How this session reaches its model, with the capabilities that transport declares. */
244
+ readonly transport?: TransportDescriptor;
245
+ /**
246
+ * Ask the runtime to stop the in-flight turn and wait (bounded) for its
247
+ * acknowledgment. Present only where the transport declares
248
+ * `interrupt: "acknowledged"`. Unlike interrupt(), a confirmed stop settles
249
+ * native ownership through the turn's own terminal.
250
+ */
251
+ interruptNative?(opts?: { timeoutMs?: number }): Promise<NativeInterruptOutcome>;
252
+ /** Poll account usage limits without a model turn. Present where the transport declares `limits.poll`. */
253
+ readLimits?(opts?: { timeoutMs?: number }): Promise<LimitSnapshot | undefined>;
254
+ /** One immutable admission snapshot, without copying unrelated session history. */
255
+ inspectAttempt?(admissionId: string): SessionAttempt | undefined;
256
+ readonly admissionProtocol?: 'prewrite-v1';
257
+ readonly diagnostics?: SessionDiagnostics;
258
+ readonly attempts?: readonly SessionAttempt[];
259
+ readonly accounting?: 'observed-only';
102
260
  readonly alive: boolean;
103
261
  /**
104
262
  * The agent runtime's native session ID — external to Foundry.
@@ -109,13 +267,13 @@ export interface HarnessSession {
109
267
  readonly externalSessionId: string | undefined;
110
268
  readonly events: readonly SessionEvent[];
111
269
  readonly turns: number;
112
- readonly totalTokens: Readonly<{ input: number; output: number }>;
270
+ readonly totalTokens: Readonly<SessionTokens>;
113
271
 
114
272
  /** Spawn the underlying process. Must be called before send(). */
115
273
  start(): Promise<void>;
116
274
 
117
275
  /** Send a message. Queued if another turn is in-flight. */
118
- send(message: string, opts?: { timeout?: number }): Promise<SessionResult>;
276
+ send(message: string, opts?: SessionSendOptions): Promise<SessionResult>;
119
277
 
120
278
  /**
121
279
  * Fork: create a new (unstarted) session branching from current state.
@@ -123,7 +281,7 @@ export interface HarnessSession {
123
281
  */
124
282
  fork(opts?: { cwd?: string; baseContext?: string }): HarnessSession;
125
283
 
126
- /** Interrupt the current in-flight turn (best-effort). */
284
+ /** Reject the local waiter. No native cancellation acknowledgment is currently implemented. */
127
285
  interrupt(): void;
128
286
 
129
287
  /** Kill the session process and reject any pending turns. */
package/src/index.ts CHANGED
@@ -1,21 +1,117 @@
1
- // @inixiative/agent-session — drive coding-agent CLIs as persistent,
2
- // streaming, event-captured sessions.
3
-
4
- // The provider-agnostic interface + event taxonomy.
5
- export * from "./harness-session";
1
+ // @inixiative/agent-session — drive coding-agent runtimes as persistent,
2
+ // streaming, event-captured sessions, over interchangeable transports.
6
3
 
4
+ // Claude through the Agent SDK (caller supplies `query`; zero dependencies here).
5
+ export {
6
+ type ClaudeAgentSdkCanUseTool,
7
+ type ClaudeAgentSdkQuery,
8
+ type ClaudeAgentSdkQueryFunction,
9
+ ClaudeAgentSdkSession,
10
+ type ClaudeAgentSdkSessionConfig,
11
+ sdkOptionsFromArgv,
12
+ } from './claude-agent-sdk-session';
7
13
  // Claude Code adapter (persistent stream-json session, subscription auth).
8
14
  export {
15
+ CLAUDE_TEXT_ONLY_ARGS,
9
16
  ClaudeCodeSession,
10
17
  type ClaudeCodeSessionConfig,
11
- } from "./claude-code-session";
18
+ } from './claude-code-session';
19
+ export { type ClaudePrimedConfig, ClaudePrimedSessions } from './claude-primed';
20
+ export { parseClaudeUsage } from './claude-usage';
21
+ export {
22
+ CODEX_DECISION_DISABLED_FEATURES,
23
+ CODEX_DECISION_THREAD_CONFIG,
24
+ type CodexPrimedConfig,
25
+ CodexPrimedSessions,
26
+ codexDecisionLaunchArgs,
27
+ codexTokens,
28
+ } from './codex-primed';
12
29
 
13
30
  // Codex CLI adapter (persistent JSON-RPC session). CodexSession defaults to the
14
- // mcp-server variant; CodexAppServerSession is the experimental app-server one.
31
+ // mcp-server variant; CodexAppServerSession is the app-server one.
15
32
  export {
16
- CodexSession,
17
- CodexMcpSession,
18
33
  CodexAppServerSession,
34
+ CodexMcpSession,
35
+ CodexSession,
19
36
  type CodexSessionConfig,
20
37
  type CodexSpawn,
21
- } from "./codex-session";
38
+ } from './codex-session';
39
+ // The provider-agnostic interface + event taxonomy.
40
+ export * from './harness-session';
41
+ export {
42
+ JsonRpcClosedError,
43
+ JsonRpcConnection,
44
+ JsonRpcError,
45
+ JsonRpcTimeoutError,
46
+ } from './json-rpc';
47
+ // Subscription limits, polling, routing and pools.
48
+ export {
49
+ claudeRateLimitSnapshot,
50
+ claudeUsageSnapshot,
51
+ codexLimitSnapshot,
52
+ type LimitRuntime,
53
+ type LimitSnapshot,
54
+ type LimitWindow,
55
+ limitUtilization,
56
+ mergeLimits,
57
+ } from './limits';
58
+ export { type LimitProbeOptions, probeClaudeLimits, probeCodexLimits } from './limits-probe';
59
+ export {
60
+ ContinuityError,
61
+ type ContinuityRefusal,
62
+ type FailureKind,
63
+ type InstanceConfig,
64
+ type InstanceStatus,
65
+ type Lease,
66
+ type PoolEvent,
67
+ PoolExhaustedError,
68
+ type PooledSessionConfig,
69
+ type PooledTransport,
70
+ type PoolRequest,
71
+ SubscriptionPool,
72
+ type SubscriptionPoolOptions,
73
+ } from './pool';
74
+ // Primed decision sessions: one live, warm session per middleware role.
75
+ export {
76
+ type DecisionAdmission,
77
+ DecisionError,
78
+ type DecisionFailure,
79
+ type DecisionRequest,
80
+ type DecisionResult,
81
+ type PrimedEvent,
82
+ type PrimedSessions,
83
+ type PrimedSnapshot,
84
+ type PrimeSpec,
85
+ primeHash,
86
+ } from './primed';
87
+ export {
88
+ assessCandidate,
89
+ type CandidateRequest,
90
+ type ExclusionReason,
91
+ type RankedCandidate,
92
+ type RepositoryRoute,
93
+ type RoutingMode,
94
+ type RoutingRequest,
95
+ rankCandidates,
96
+ rankSubscriptionAccounts,
97
+ repositoryIdentity,
98
+ type SubscriptionCandidate,
99
+ } from './routing';
100
+ // Transports: how a session reaches its model, with declared capabilities.
101
+ export {
102
+ TRANSPORTS,
103
+ type TransportCapabilities,
104
+ type TransportDescriptor,
105
+ type TransportKind,
106
+ type TransportRuntime,
107
+ TransportUnavailableError,
108
+ } from './transport';
109
+ export {
110
+ AcpSession,
111
+ type AcpSessionConfig,
112
+ ApiSession,
113
+ type ApiSessionConfig,
114
+ createSession,
115
+ describeTransport,
116
+ type TransportConfigs,
117
+ } from './transports';
@@ -0,0 +1,240 @@
1
+ // Minimal JSON-RPC 2.0 over a child's stdio, one JSON object per line.
2
+ // Used where a caller multiplexes many threads over one native process
3
+ // (Codex app-server) or makes short control reads (limit polls).
4
+
5
+ import type { PipedSubprocess } from './codex-session';
6
+
7
+ export class JsonRpcError extends Error {
8
+ constructor(
9
+ readonly code: number,
10
+ message: string,
11
+ ) {
12
+ super(message);
13
+ this.name = 'JsonRpcError';
14
+ }
15
+ }
16
+ /** No response within the caller's bound; the request may still be processed. */
17
+ export class JsonRpcTimeoutError extends Error {
18
+ constructor(message: string) {
19
+ super(message);
20
+ this.name = 'JsonRpcTimeoutError';
21
+ }
22
+ }
23
+ export class JsonRpcClosedError extends Error {
24
+ constructor(message: string) {
25
+ super(message);
26
+ this.name = 'JsonRpcClosedError';
27
+ }
28
+ }
29
+
30
+ export interface JsonRpcHandlers {
31
+ onNotification?(method: string, params: Record<string, unknown> | undefined): void;
32
+ /** Return true when the request was answered; unanswered requests are refused (-32601). */
33
+ onRequest?(
34
+ id: string | number,
35
+ method: string,
36
+ params: Record<string, unknown> | undefined,
37
+ ): boolean;
38
+ onClose?(error: Error): void;
39
+ }
40
+
41
+ const object = (value: unknown): Record<string, unknown> | undefined =>
42
+ value !== null && typeof value === 'object' && !Array.isArray(value)
43
+ ? (value as Record<string, unknown>)
44
+ : undefined;
45
+
46
+ export class JsonRpcConnection {
47
+ private _id = 0;
48
+ private _pending = new Map<
49
+ number,
50
+ {
51
+ resolve(value: unknown): void;
52
+ reject(error: Error): void;
53
+ timer?: ReturnType<typeof setTimeout>;
54
+ }
55
+ >();
56
+ private _stderr = '';
57
+ private _closeError?: Error;
58
+ readonly exited: Promise<number>;
59
+
60
+ constructor(
61
+ private readonly _proc: PipedSubprocess,
62
+ private readonly _handlers: JsonRpcHandlers = {},
63
+ ) {
64
+ this.exited = _proc.exited;
65
+ void this._read();
66
+ void this._readStderr();
67
+ _proc.exited.then(
68
+ (code) =>
69
+ this._close(
70
+ new JsonRpcClosedError(
71
+ `Native process exited (code ${code})${this._stderr.trim() ? `: ${this._stderr.trim().slice(-300)}` : ''}`,
72
+ ),
73
+ ),
74
+ () => this._close(new JsonRpcClosedError('Native process exit unobserved')),
75
+ );
76
+ }
77
+
78
+ get closed(): boolean {
79
+ return this._closeError !== undefined;
80
+ }
81
+ get stderrTail(): string {
82
+ return this._stderr.slice(-2_000);
83
+ }
84
+
85
+ request(method: string, params?: unknown, timeoutMs?: number): Promise<unknown> {
86
+ if (this._closeError) return Promise.reject(this._closeError);
87
+ const id = ++this._id;
88
+ return new Promise((resolve, reject) => {
89
+ const entry: {
90
+ resolve(value: unknown): void;
91
+ reject(error: Error): void;
92
+ timer?: ReturnType<typeof setTimeout>;
93
+ } = { resolve, reject };
94
+ if (timeoutMs !== undefined && timeoutMs > 0)
95
+ entry.timer = setTimeout(() => {
96
+ this._pending.delete(id);
97
+ reject(new JsonRpcTimeoutError(`${method} timed out after ${timeoutMs}ms`));
98
+ }, timeoutMs);
99
+ this._pending.set(id, entry);
100
+ try {
101
+ this._write({ jsonrpc: '2.0', id, method, ...(params === undefined ? {} : { params }) });
102
+ } catch (error) {
103
+ clearTimeout(entry.timer);
104
+ this._pending.delete(id);
105
+ reject(error as Error);
106
+ }
107
+ });
108
+ }
109
+
110
+ notify(method: string, params?: unknown): void {
111
+ this._write({ jsonrpc: '2.0', method, ...(params === undefined ? {} : { params }) });
112
+ }
113
+
114
+ /** Kill the process. Pending requests reject once its exit is observed. */
115
+ kill(): void {
116
+ try {
117
+ this._proc.stdin.end();
118
+ } catch {
119
+ /* already closed */
120
+ }
121
+ try {
122
+ this._proc.kill();
123
+ } catch {
124
+ /* already dead */
125
+ }
126
+ }
127
+
128
+ private _write(message: unknown): void {
129
+ if (this._closeError) throw this._closeError;
130
+ this._proc.stdin.write(`${JSON.stringify(message)}\n`);
131
+ this._proc.stdin.flush();
132
+ }
133
+
134
+ private _close(error: Error): void {
135
+ if (this._closeError) return;
136
+ this._closeError = error;
137
+ for (const entry of this._pending.values()) {
138
+ clearTimeout(entry.timer);
139
+ entry.reject(error);
140
+ }
141
+ this._pending.clear();
142
+ try {
143
+ this._handlers.onClose?.(error);
144
+ } catch {
145
+ /* observer */
146
+ }
147
+ }
148
+
149
+ private async _read(): Promise<void> {
150
+ const reader = this._proc.stdout.getReader();
151
+ const decoder = new TextDecoder();
152
+ let buffer = '';
153
+ try {
154
+ while (true) {
155
+ const { done, value } = await reader.read();
156
+ if (done) break;
157
+ buffer += decoder.decode(value, { stream: true });
158
+ const lines = buffer.split('\n');
159
+ buffer = lines.pop()!;
160
+ for (const line of lines) this._line(line);
161
+ }
162
+ this._line(buffer + decoder.decode());
163
+ this._close(
164
+ new JsonRpcClosedError(
165
+ `Native stdout closed${this._stderr.trim() ? `: ${this._stderr.trim().slice(-300)}` : ''}`,
166
+ ),
167
+ );
168
+ } catch (error) {
169
+ this._close(error instanceof Error ? error : new JsonRpcClosedError(String(error)));
170
+ }
171
+ }
172
+
173
+ private async _readStderr(): Promise<void> {
174
+ const reader = this._proc.stderr.getReader();
175
+ const decoder = new TextDecoder();
176
+ try {
177
+ while (true) {
178
+ const { done, value } = await reader.read();
179
+ if (done) break;
180
+ this._stderr = (this._stderr + decoder.decode(value, { stream: true })).slice(-8_192);
181
+ }
182
+ } catch {
183
+ /* diagnostics only */
184
+ }
185
+ }
186
+
187
+ private _line(line: string): void {
188
+ if (!line.trim()) return;
189
+ let message: Record<string, unknown> | undefined;
190
+ try {
191
+ message = object(JSON.parse(line));
192
+ } catch {
193
+ return;
194
+ }
195
+ if (!message) return;
196
+ const id = message.id,
197
+ method = typeof message.method === 'string' ? message.method : undefined;
198
+ if (method && (typeof id === 'string' || typeof id === 'number')) {
199
+ let answered = false;
200
+ try {
201
+ answered = this._handlers.onRequest?.(id, method, object(message.params)) === true;
202
+ } catch {
203
+ answered = false;
204
+ }
205
+ if (!answered)
206
+ this.respondError(id, -32601, 'Client interaction is unsupported by this adapter');
207
+ return;
208
+ }
209
+ if (method) {
210
+ try {
211
+ this._handlers.onNotification?.(method, object(message.params));
212
+ } catch {
213
+ /* observer */
214
+ }
215
+ return;
216
+ }
217
+ if (typeof id !== 'number') return;
218
+ const entry = this._pending.get(id);
219
+ if (!entry) return;
220
+ this._pending.delete(id);
221
+ clearTimeout(entry.timer);
222
+ const error = object(message.error);
223
+ if (error)
224
+ entry.reject(
225
+ new JsonRpcError(
226
+ typeof error.code === 'number' ? error.code : -32000,
227
+ typeof error.message === 'string' ? error.message : 'JSON-RPC error',
228
+ ),
229
+ );
230
+ else entry.resolve(message.result);
231
+ }
232
+
233
+ respondError(id: string | number, code: number, message: string): void {
234
+ try {
235
+ this._write({ jsonrpc: '2.0', id, error: { code, message } });
236
+ } catch {
237
+ /* closing */
238
+ }
239
+ }
240
+ }