@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.
- package/LICENSE +21 -0
- package/README.md +98 -6
- package/package.json +39 -8
- package/src/claude-agent-sdk-session.ts +316 -0
- package/src/claude-code-session.ts +715 -195
- package/src/claude-primed.ts +655 -0
- package/src/claude-usage.ts +29 -0
- package/src/codex-primed.ts +1482 -0
- package/src/codex-session.ts +1317 -241
- package/src/harness-session.ts +182 -24
- package/src/index.ts +106 -10
- package/src/json-rpc.ts +240 -0
- package/src/limits-probe.ts +78 -0
- package/src/limits.ts +216 -0
- package/src/pool.ts +652 -0
- package/src/primed.ts +284 -0
- package/src/retained-evidence.ts +20 -0
- package/src/routing.ts +273 -0
- package/src/transport.ts +215 -0
- package/src/transports.ts +97 -0
- package/src/turn-state.ts +72 -0
package/src/harness-session.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
29
|
-
|
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
/**
|
|
53
|
-
readonly tokens?:
|
|
54
|
-
/**
|
|
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?:
|
|
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:
|
|
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
|
-
|
|
95
|
-
|
|
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<
|
|
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?:
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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';
|
package/src/json-rpc.ts
ADDED
|
@@ -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
|
+
}
|