@harapter/adapter-pi 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1584 @@
1
+ import { spawn, } from 'node:child_process';
2
+ import { isAbsolute, resolve } from 'node:path';
3
+ import { ExtensionRegistry, HarnessError, assertSessionCompatibility, assertSessionOwnership, providerSessionId, runId, } from '@harapter/core';
4
+ import { JsonlProcessTransport, JsonlTransportError, } from '@harapter/transport-jsonl-process';
5
+ import { PI_OBSERVATION_EXTENSION, PI_PROVIDER_ID, PI_SESSION_COMPATIBILITY_REF, mapPiRunEvent, parsePiAssistantOutcome, parsePiSessionState, parsePiVersionOutput, piCompatibilityIdentity, preparePiPrompt, redactPiObservation, } from './protocol.js';
6
+ const descriptor = {
7
+ providerId: PI_PROVIDER_ID,
8
+ displayName: 'Pi Agent',
9
+ connectionKinds: ['process'],
10
+ documentationUrl: 'https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/rpc.md',
11
+ };
12
+ const defaultOperationTimeoutMs = 30_000;
13
+ const defaultCancelSettlementTimeoutMs = 10_000;
14
+ const defaultMaxRunEvents = 128;
15
+ const defaultMaxPendingRequests = 32;
16
+ const defaultMaxPendingInteractions = 32;
17
+ const maximumRunEvents = 4_096;
18
+ const maximumTimerMilliseconds = 2_147_483_647;
19
+ const maximumVersionBytes = 256;
20
+ const childTerminationTimeoutMs = 2_000;
21
+ const forbiddenRuntimeArguments = new Set([
22
+ '--api-key',
23
+ '--append-system-prompt',
24
+ '--continue',
25
+ '--export',
26
+ '--extension',
27
+ '--fork',
28
+ '--help',
29
+ '--mode',
30
+ '--no-extensions',
31
+ '--no-prompt-templates',
32
+ '--no-session',
33
+ '--no-skills',
34
+ '--print',
35
+ '--prompt-template',
36
+ '--resume',
37
+ '--session',
38
+ '--session-dir',
39
+ '--session-id',
40
+ '--skill',
41
+ '--system-prompt',
42
+ '--version',
43
+ '-c',
44
+ '-e',
45
+ '-h',
46
+ '-ne',
47
+ '-np',
48
+ '-ns',
49
+ '-p',
50
+ '-r',
51
+ '-v',
52
+ ]);
53
+ const nativeReadCommands = new Set([
54
+ 'get_available_models',
55
+ 'get_available_thinking_levels',
56
+ 'get_commands',
57
+ 'get_entries',
58
+ 'get_last_assistant_text',
59
+ 'get_messages',
60
+ 'get_session_stats',
61
+ 'get_state',
62
+ 'get_tree',
63
+ ]);
64
+ const interactiveMethods = new Set(['select', 'confirm', 'input', 'editor']);
65
+ /** Create a fresh Pi Agent RPC Provider Adapter factory. */
66
+ export function createPiProviderFactory() {
67
+ return {
68
+ descriptor: () => ({
69
+ ...descriptor,
70
+ connectionKinds: [...descriptor.connectionKinds],
71
+ }),
72
+ connect: async (profile) => connectPi(profile),
73
+ };
74
+ }
75
+ async function connectPi(profile) {
76
+ const stableProfile = snapshotProfile(profile);
77
+ validateProfile(stableProfile);
78
+ const options = connectionOptions(stableProfile.providerOptions, stableProfile);
79
+ let runtimeVersion;
80
+ try {
81
+ runtimeVersion = await probeRuntimeVersion(stableProfile, options.operationTimeoutMs);
82
+ }
83
+ catch (error) {
84
+ throw mapError(error, stableProfile, 'probe Runtime', true);
85
+ }
86
+ return new PiClient(stableProfile, runtimeVersion, options);
87
+ }
88
+ class PiClient {
89
+ profile;
90
+ runtimeVersion;
91
+ options;
92
+ extensionRegistry = new ExtensionRegistry(PI_PROVIDER_ID);
93
+ observationListeners = new Set();
94
+ openingSessions = new Map();
95
+ sessions = new Map();
96
+ nativeClient;
97
+ closed = false;
98
+ closePromise;
99
+ interactionObserved = false;
100
+ runSerial = 0;
101
+ constructor(profile, runtimeVersion, options) {
102
+ this.profile = profile;
103
+ this.runtimeVersion = runtimeVersion;
104
+ this.options = options;
105
+ const observer = Object.freeze({
106
+ onObservation: (listener) => {
107
+ this.observationListeners.add(listener);
108
+ return () => this.observationListeners.delete(listener);
109
+ },
110
+ });
111
+ this.extensionRegistry.register({
112
+ name: PI_OBSERVATION_EXTENSION,
113
+ providerId: PI_PROVIDER_ID,
114
+ displayName: 'Pi Agent RPC observation channel',
115
+ description: 'Bounded, redacted Pi RPC observations.',
116
+ documentationUrl: 'https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/rpc.md',
117
+ stability: 'experimental',
118
+ }, observer);
119
+ this.nativeClient = Object.freeze({
120
+ runtimeIdentity: this.runtimeIdentity(),
121
+ request: (sessionId, command, requestOptions) => this.nativeRequest(sessionId, command, requestOptions),
122
+ onObservation: (listener) => {
123
+ this.observationListeners.add(listener);
124
+ return () => this.observationListeners.delete(listener);
125
+ },
126
+ });
127
+ }
128
+ descriptor() {
129
+ return Promise.resolve({
130
+ providerId: PI_PROVIDER_ID,
131
+ profileId: this.profile.profileId,
132
+ displayName: this.profile.displayName,
133
+ connectionKind: 'process',
134
+ runtime: {
135
+ name: 'pi',
136
+ version: this.runtimeVersion,
137
+ protocol: 'Pi RPC over strict JSONL stdio',
138
+ protocolVersion: 'current',
139
+ },
140
+ compatibility: 'experimental',
141
+ warnings: [
142
+ {
143
+ code: 'live_runtime_evidence_optional',
144
+ message: 'Compatibility is established by official interface review and synthetic conformance; live Pi runtime evidence is host opt-in.',
145
+ },
146
+ ],
147
+ });
148
+ }
149
+ capabilities() {
150
+ return Promise.resolve(this.capabilityManifest());
151
+ }
152
+ async createSession(input = {}) {
153
+ this.assertOpen();
154
+ const state = prepareSessionInput(input, this.profile, this.options);
155
+ const session = await this.openSession(state);
156
+ return session;
157
+ }
158
+ async resumeSession(ref) {
159
+ this.assertOpen();
160
+ if (!this.options.persistSessions) {
161
+ throw unsupported(this.profile, 'session.resume', 'This Pi Profile disables native Session persistence.');
162
+ }
163
+ assertSessionOwnership(ref, PI_PROVIDER_ID, this.profile.profileId);
164
+ assertSessionCompatibility(ref, PI_SESSION_COMPATIBILITY_REF);
165
+ const state = sessionStateFromRef(ref);
166
+ if (!state.persisted) {
167
+ throw unsupported(this.profile, 'session.resume', 'The Pi Session reference was created without persistence.');
168
+ }
169
+ return this.openSession(state, ref.providerSessionId);
170
+ }
171
+ extensions() {
172
+ return this.extensionRegistry;
173
+ }
174
+ native(guard) {
175
+ const value = this.nativeClient;
176
+ return guard !== undefined && !guard(value) ? undefined : value;
177
+ }
178
+ close() {
179
+ this.closePromise ??= this.closeOnce();
180
+ return this.closePromise;
181
+ }
182
+ capabilityManifest() {
183
+ return piCapabilities(this.profile, this.options, this.runtimeIdentity(), this.interactionObserved);
184
+ }
185
+ forget(session) {
186
+ const id = session.ref().providerSessionId;
187
+ if (this.sessions.get(id) === session)
188
+ this.sessions.delete(id);
189
+ }
190
+ observe(value) {
191
+ const observation = redactPiObservation(value);
192
+ for (const listener of [...this.observationListeners]) {
193
+ try {
194
+ listener(structuredClone(observation));
195
+ }
196
+ catch {
197
+ // Provider observers cannot affect lifecycle processing.
198
+ }
199
+ }
200
+ }
201
+ markInteractionObserved() {
202
+ this.interactionObserved = true;
203
+ }
204
+ allocateRunId() {
205
+ return runId(`pi-run-${String(++this.runSerial)}`);
206
+ }
207
+ openSession(state, resumeId) {
208
+ const controller = new AbortController();
209
+ const opening = this.openSessionOnce(state, resumeId, controller.signal);
210
+ this.openingSessions.set(opening, controller);
211
+ void opening.then(() => this.openingSessions.delete(opening), (error) => {
212
+ if (!isChildCleanupFailure(error)) {
213
+ this.openingSessions.delete(opening);
214
+ }
215
+ });
216
+ return opening;
217
+ }
218
+ async openSessionOnce(state, resumeId, signal) {
219
+ let peer;
220
+ try {
221
+ peer = await spawnPiPeer(this.profile, this.options, state, resumeId, (observation) => {
222
+ this.observe(observation);
223
+ });
224
+ const response = await peer.request({ type: 'get_state' }, { signal, timeoutMs: this.options.operationTimeoutMs });
225
+ const nativeState = parsePiSessionState(response);
226
+ this.assertOpen();
227
+ if (nativeState.isStreaming || nativeState.isCompacting) {
228
+ throw new HarnessError('provider_api_incompatible', 'Pi Agent opened a Session that was not idle.', {
229
+ retryable: false,
230
+ providerId: PI_PROVIDER_ID,
231
+ profileId: this.profile.profileId,
232
+ });
233
+ }
234
+ const sessionId = providerSessionId(nativeState.sessionId);
235
+ if (resumeId !== undefined && sessionId !== resumeId) {
236
+ throw new HarnessError('session_provider_mismatch', 'Pi Agent resumed a different native Session.', {
237
+ retryable: false,
238
+ providerId: PI_PROVIDER_ID,
239
+ profileId: this.profile.profileId,
240
+ });
241
+ }
242
+ if (this.sessions.has(sessionId)) {
243
+ throw new HarnessError('session_provider_mismatch', 'The Pi Session is already active on this Client.', {
244
+ retryable: false,
245
+ providerId: PI_PROVIDER_ID,
246
+ profileId: this.profile.profileId,
247
+ });
248
+ }
249
+ const session = new PiProcessSession(this, this.profile, sessionId, state, peer, this.options);
250
+ peer.bind(session);
251
+ this.sessions.set(sessionId, session);
252
+ return session;
253
+ }
254
+ catch (error) {
255
+ if (peer !== undefined) {
256
+ try {
257
+ await peer.close();
258
+ }
259
+ catch {
260
+ throw childCleanupFailure(this.profile);
261
+ }
262
+ }
263
+ if (signal.aborted && this.closed)
264
+ this.assertOpen();
265
+ throw mapError(error, this.profile, 'open Session');
266
+ }
267
+ }
268
+ async nativeRequest(sessionId, command, options) {
269
+ this.assertOpen();
270
+ if (!nativeReadCommands.has(command.type) || 'id' in command) {
271
+ throw new HarnessError('unsupported_capability', 'Pi native access permits only ownership-preserving read commands.', {
272
+ retryable: false,
273
+ providerId: PI_PROVIDER_ID,
274
+ profileId: this.profile.profileId,
275
+ details: { capability: 'native.client' },
276
+ });
277
+ }
278
+ const session = this.sessions.get(sessionId);
279
+ if (session === undefined) {
280
+ throw new HarnessError('session_not_found', 'Pi Session is not active.', {
281
+ retryable: false,
282
+ providerId: PI_PROVIDER_ID,
283
+ profileId: this.profile.profileId,
284
+ });
285
+ }
286
+ return session.nativeRequest(command, options);
287
+ }
288
+ async closeOnce() {
289
+ if (this.closed)
290
+ return;
291
+ this.closed = true;
292
+ const openings = [...this.openingSessions.entries()];
293
+ for (const [, controller] of openings)
294
+ controller.abort();
295
+ const sessions = [...this.sessions.values()];
296
+ const results = await Promise.allSettled([
297
+ ...sessions.map(async (session) => session.forceClose('client_closed')),
298
+ ...openings.map(async ([opening]) => {
299
+ const opened = await opening;
300
+ await opened.forceClose('client_closed');
301
+ }),
302
+ ]);
303
+ const cleanupFailure = results.find((result) => result.status === 'rejected' && isChildCleanupFailure(result.reason));
304
+ if (cleanupFailure !== undefined)
305
+ throw cleanupFailure.reason;
306
+ }
307
+ runtimeIdentity() {
308
+ return piCompatibilityIdentity(this.runtimeVersion);
309
+ }
310
+ assertOpen() {
311
+ if (this.closed) {
312
+ throw new HarnessError('connection_aborted', 'The Pi Agent Client is closed.', {
313
+ retryable: false,
314
+ providerId: PI_PROVIDER_ID,
315
+ profileId: this.profile.profileId,
316
+ });
317
+ }
318
+ }
319
+ }
320
+ class PiProcessSession {
321
+ client;
322
+ profile;
323
+ sessionId;
324
+ state;
325
+ peer;
326
+ options;
327
+ pendingInteractions = new Map();
328
+ lifecycleState = 'open';
329
+ closePromise;
330
+ forceClosePromise;
331
+ activeRun;
332
+ connectionAborted = false;
333
+ interactionSerial = 0;
334
+ constructor(client, profile, sessionId, state, peer, options) {
335
+ this.client = client;
336
+ this.profile = profile;
337
+ this.sessionId = sessionId;
338
+ this.state = state;
339
+ this.peer = peer;
340
+ this.options = options;
341
+ }
342
+ ref() {
343
+ return {
344
+ providerId: PI_PROVIDER_ID,
345
+ profileId: this.profile.profileId,
346
+ providerSessionId: this.sessionId,
347
+ compatibilityRef: PI_SESSION_COMPATIBILITY_REF,
348
+ providerState: { ...this.state },
349
+ };
350
+ }
351
+ capabilities() {
352
+ return Promise.resolve(this.client.capabilityManifest());
353
+ }
354
+ start(input, options = {}) {
355
+ try {
356
+ return Promise.resolve(this.startOnce(input, options));
357
+ }
358
+ catch (error) {
359
+ return Promise.reject(error instanceof Error
360
+ ? error
361
+ : new HarnessError('provider_error', 'Pi Agent Run setup failed.', {
362
+ retryable: false,
363
+ providerId: PI_PROVIDER_ID,
364
+ profileId: this.profile.profileId,
365
+ }));
366
+ }
367
+ }
368
+ startOnce(input, options) {
369
+ this.assertOpen();
370
+ if (this.activeRun !== undefined) {
371
+ throw new HarnessError('run_conflict', 'Pi Agent permits one active Run per Session process.', {
372
+ retryable: false,
373
+ providerId: PI_PROVIDER_ID,
374
+ profileId: this.profile.profileId,
375
+ });
376
+ }
377
+ validateRunOptions(options);
378
+ const prompt = preparePiPrompt(input);
379
+ const run = new PiRun({
380
+ providerId: PI_PROVIDER_ID,
381
+ profileId: this.profile.profileId,
382
+ sessionId: this.sessionId,
383
+ runId: this.client.allocateRunId(),
384
+ }, this.options.maxRunEvents, options.timeoutMs, this.options.cancelSettlementTimeoutMs, () => this.sendAbort(), (reason) => {
385
+ this.abortConnection(reason);
386
+ }, (terminal) => {
387
+ this.settleInteractions(terminal);
388
+ if (this.activeRun === terminal)
389
+ this.activeRun = undefined;
390
+ });
391
+ this.activeRun = run;
392
+ void this.peer
393
+ .request({ type: 'prompt', message: prompt }, { timeoutMs: this.options.operationTimeoutMs })
394
+ .then(() => {
395
+ run.acknowledgePrompt();
396
+ })
397
+ .catch((error) => {
398
+ this.handlePromptFailure(run, error);
399
+ });
400
+ return run;
401
+ }
402
+ async respond(requestId, response) {
403
+ this.assertOpen();
404
+ const pending = this.pendingInteractions.get(requestId);
405
+ if (pending === undefined || response.kind !== 'provider') {
406
+ throw invalidInteraction(this.profile);
407
+ }
408
+ const outbound = prepareInteractionResponse(pending, response.value);
409
+ try {
410
+ await this.peer.send(outbound);
411
+ }
412
+ catch {
413
+ this.abortConnection('interaction_response_failed');
414
+ throw new HarnessError('connection_aborted', 'Pi Agent interaction response could not be confirmed.', {
415
+ retryable: false,
416
+ providerId: PI_PROVIDER_ID,
417
+ profileId: this.profile.profileId,
418
+ });
419
+ }
420
+ this.pendingInteractions.delete(requestId);
421
+ pending.run.resolveInteraction(requestId, pending.method);
422
+ }
423
+ close() {
424
+ if (this.closePromise)
425
+ return this.closePromise;
426
+ this.lifecycleState = 'closing';
427
+ const attempt = this.closeOnce();
428
+ this.closePromise = attempt;
429
+ void attempt.catch((error) => {
430
+ if (this.closePromise === attempt) {
431
+ this.closePromise = undefined;
432
+ if (!(error instanceof HarnessError) ||
433
+ error.code !== 'connection_aborted') {
434
+ this.lifecycleState = 'open';
435
+ }
436
+ }
437
+ });
438
+ return attempt;
439
+ }
440
+ receive(value) {
441
+ const run = this.activeRun;
442
+ if (run === undefined) {
443
+ this.client.observe(value);
444
+ return;
445
+ }
446
+ run.receive(value);
447
+ }
448
+ receiveExtensionRequest(value) {
449
+ const method = value['method'];
450
+ const nativeId = value['id'];
451
+ const run = this.activeRun;
452
+ if (run === undefined ||
453
+ typeof nativeId !== 'string' ||
454
+ typeof method !== 'string' ||
455
+ !interactiveMethods.has(method)) {
456
+ this.client.observe(value);
457
+ if (typeof nativeId === 'string' && typeof method === 'string') {
458
+ void this.peer
459
+ .send({
460
+ type: 'extension_ui_response',
461
+ id: nativeId,
462
+ cancelled: true,
463
+ })
464
+ .catch(() => {
465
+ this.abortConnection('interaction_response_failed');
466
+ });
467
+ }
468
+ return;
469
+ }
470
+ if (this.pendingInteractions.size >= this.options.maxPendingInteractions) {
471
+ this.abortConnection('interaction_capacity_exceeded');
472
+ return;
473
+ }
474
+ const localId = `pi-interaction-${String(++this.interactionSerial)}`;
475
+ const typedMethod = method;
476
+ this.pendingInteractions.set(localId, {
477
+ nativeId,
478
+ method: typedMethod,
479
+ run,
480
+ });
481
+ this.client.markInteractionObserved();
482
+ run.requestInteraction(localId, typedMethod, value);
483
+ }
484
+ failConnection(reason) {
485
+ this.abortConnection(reason);
486
+ }
487
+ failProtocol() {
488
+ if (this.connectionAborted)
489
+ return;
490
+ this.connectionAborted = true;
491
+ this.activeRun?.failProtocol('provider_api_incompatible');
492
+ this.settleInteractions(this.activeRun);
493
+ void this.peer.close().catch(() => undefined);
494
+ }
495
+ async nativeRequest(command, options) {
496
+ this.assertOpen();
497
+ try {
498
+ return (await this.peer.request(command, options));
499
+ }
500
+ catch (error) {
501
+ throw mapError(error, this.profile, 'native request');
502
+ }
503
+ }
504
+ forceClose(reason) {
505
+ this.forceClosePromise ??= this.forceCloseOnce(reason);
506
+ return this.forceClosePromise;
507
+ }
508
+ async forceCloseOnce(reason) {
509
+ this.lifecycleState = 'closed';
510
+ this.connectionAborted = true;
511
+ this.activeRun?.abortConnection(reason);
512
+ this.settleInteractions(this.activeRun);
513
+ try {
514
+ await this.peer.close();
515
+ }
516
+ catch {
517
+ throw childCleanupFailure(this.profile);
518
+ }
519
+ this.client.forget(this);
520
+ }
521
+ async closeOnce() {
522
+ if (this.activeRun !== undefined) {
523
+ throw new HarnessError('run_conflict', 'Cannot close a Pi Session with an active Run.', {
524
+ retryable: false,
525
+ providerId: PI_PROVIDER_ID,
526
+ profileId: this.profile.profileId,
527
+ });
528
+ }
529
+ await this.forceClose('session_closed');
530
+ }
531
+ sendAbort() {
532
+ return this.peer.request({ type: 'abort' }, { timeoutMs: this.options.operationTimeoutMs });
533
+ }
534
+ handlePromptFailure(run, error) {
535
+ if (run.isTerminal())
536
+ return;
537
+ if (error instanceof PiRpcFailure && error.code === 'remote_rejected') {
538
+ run.failProtocol('prompt_rejected');
539
+ return;
540
+ }
541
+ this.abortConnection('prompt_ack_uncertain');
542
+ }
543
+ settleInteractions(run) {
544
+ if (run === undefined)
545
+ return;
546
+ for (const [requestId, pending] of [...this.pendingInteractions]) {
547
+ if (pending.run !== run)
548
+ continue;
549
+ this.pendingInteractions.delete(requestId);
550
+ pending.run.resolveInteraction(requestId, 'cancelled');
551
+ }
552
+ }
553
+ abortConnection(reason) {
554
+ if (this.connectionAborted)
555
+ return;
556
+ this.connectionAborted = true;
557
+ this.activeRun?.abortConnection(reason);
558
+ this.settleInteractions(this.activeRun);
559
+ void this.peer.close().catch(() => undefined);
560
+ }
561
+ assertOpen() {
562
+ if (this.lifecycleState !== 'open' ||
563
+ this.connectionAborted ||
564
+ !this.peer.isOpen()) {
565
+ throw new HarnessError(this.lifecycleState === 'closed'
566
+ ? 'session_not_found'
567
+ : 'connection_aborted', 'The Pi Session process is not available.', {
568
+ retryable: false,
569
+ providerId: PI_PROVIDER_ID,
570
+ profileId: this.profile.profileId,
571
+ });
572
+ }
573
+ }
574
+ }
575
+ class PiRun {
576
+ reference;
577
+ cancelSettlementTimeoutMs;
578
+ sendCancel;
579
+ abortOwnerConnection;
580
+ onTerminal;
581
+ eventQueue;
582
+ settlement;
583
+ timeout;
584
+ acknowledged = false;
585
+ settled = false;
586
+ cancelPromise;
587
+ cancelReason;
588
+ cancelConfirmed = false;
589
+ cancelRequested = false;
590
+ finalResult;
591
+ lastAssistant;
592
+ resolveSettlement;
593
+ sequence = 0;
594
+ constructor(reference, maxRunEvents, timeoutMs, cancelSettlementTimeoutMs, sendCancel, abortOwnerConnection, onTerminal) {
595
+ this.reference = reference;
596
+ this.cancelSettlementTimeoutMs = cancelSettlementTimeoutMs;
597
+ this.sendCancel = sendCancel;
598
+ this.abortOwnerConnection = abortOwnerConnection;
599
+ this.onTerminal = onTerminal;
600
+ validateRunTimeout(timeoutMs);
601
+ this.eventQueue = new EventQueue(maxRunEvents);
602
+ this.settlement = new Promise((resolveSettlement) => {
603
+ this.resolveSettlement = resolveSettlement;
604
+ });
605
+ this.timeout =
606
+ timeoutMs === undefined
607
+ ? undefined
608
+ : setTimeout(() => {
609
+ this.cancelReason = 'timeout';
610
+ void this.cancel().catch(() => {
611
+ this.abortOwnerConnection('timeout_cancellation_failed');
612
+ });
613
+ }, timeoutMs);
614
+ this.timeout?.unref();
615
+ this.emit({ type: 'run.started', data: {} });
616
+ }
617
+ ref() {
618
+ return { ...this.reference };
619
+ }
620
+ events() {
621
+ return this.eventQueue.iterable();
622
+ }
623
+ cancel() {
624
+ if (this.isTerminal())
625
+ return Promise.resolve({ mode: 'already_terminal' });
626
+ this.cancelPromise ??= this.cancelOnce();
627
+ return this.cancelPromise;
628
+ }
629
+ result() {
630
+ return this.settlement;
631
+ }
632
+ acknowledgePrompt() {
633
+ if (this.isTerminal())
634
+ return;
635
+ this.acknowledged = true;
636
+ this.finishIfReady();
637
+ }
638
+ receive(value) {
639
+ if (this.isTerminal())
640
+ return;
641
+ const type = value['type'];
642
+ if (type === 'agent_settled') {
643
+ this.settled = true;
644
+ this.finishIfReady();
645
+ return;
646
+ }
647
+ if (type === 'message_end') {
648
+ const outcome = parsePiAssistantOutcome(value['message']);
649
+ if (outcome !== undefined) {
650
+ this.lastAssistant = outcome;
651
+ if (outcome.text.length > 0) {
652
+ this.emit({
653
+ type: 'message.completed',
654
+ data: { text: outcome.text },
655
+ providerEventType: 'message_end',
656
+ });
657
+ }
658
+ if (outcome.reasoning.length > 0) {
659
+ this.emit({
660
+ type: 'reasoning.completed',
661
+ data: { text: outcome.reasoning },
662
+ providerEventType: 'message_end',
663
+ });
664
+ }
665
+ this.emit({
666
+ type: 'usage.updated',
667
+ data: outcome.usage,
668
+ usage: outcome.usage,
669
+ providerEventType: 'message_end',
670
+ });
671
+ return;
672
+ }
673
+ }
674
+ for (const mapped of mapPiRunEvent(value))
675
+ this.emit(mapped);
676
+ }
677
+ requestInteraction(requestId, method, value) {
678
+ if (this.isTerminal())
679
+ return;
680
+ this.emit({
681
+ type: 'interaction.requested',
682
+ data: {
683
+ requestId,
684
+ kind: 'provider',
685
+ title: boundedText(value['title']),
686
+ ...(method === 'confirm'
687
+ ? { prompt: boundedText(value['message']) }
688
+ : {}),
689
+ schema: interactionSchema(method, value),
690
+ providerState: { method },
691
+ },
692
+ providerEventType: 'extension_ui_request',
693
+ });
694
+ }
695
+ resolveInteraction(requestId, outcome) {
696
+ if (this.isTerminal())
697
+ return;
698
+ this.emit({
699
+ type: 'interaction.resolved',
700
+ data: { requestId, kind: 'provider', outcome },
701
+ providerEventType: 'extension_ui_response',
702
+ });
703
+ }
704
+ failProtocol(reason) {
705
+ if (this.isTerminal())
706
+ return;
707
+ this.finish({ status: 'failed', providerResult: { reason } }, 'run.failed');
708
+ }
709
+ abortConnection(reason) {
710
+ if (this.isTerminal())
711
+ return;
712
+ this.finish({ status: 'connection_aborted', providerResult: { reason } }, 'connection.aborted');
713
+ }
714
+ isTerminal() {
715
+ return this.finalResult !== undefined;
716
+ }
717
+ finishIfReady() {
718
+ if (!this.acknowledged || !this.settled || this.isTerminal())
719
+ return;
720
+ const outcome = this.lastAssistant;
721
+ if (outcome === undefined) {
722
+ this.failProtocol('missing_assistant_terminal');
723
+ return;
724
+ }
725
+ if (outcome.stopReason === 'stop') {
726
+ this.finish({
727
+ status: 'completed',
728
+ ...(outcome.text.length === 0 ? {} : { finalMessage: outcome.text }),
729
+ usage: outcome.usage,
730
+ providerResult: { stopReason: outcome.stopReason },
731
+ }, 'run.completed');
732
+ return;
733
+ }
734
+ if (outcome.stopReason === 'aborted') {
735
+ if (!this.cancelRequested) {
736
+ this.failProtocol('unconfirmed_native_cancellation');
737
+ return;
738
+ }
739
+ if (!this.cancelConfirmed)
740
+ return;
741
+ this.finish({
742
+ status: 'cancelled',
743
+ usage: outcome.usage,
744
+ providerResult: {
745
+ stopReason: outcome.stopReason,
746
+ ...(this.cancelReason === undefined
747
+ ? {}
748
+ : { reason: this.cancelReason }),
749
+ },
750
+ }, 'run.cancelled');
751
+ return;
752
+ }
753
+ this.finish({
754
+ status: 'failed',
755
+ usage: outcome.usage,
756
+ providerResult: { stopReason: outcome.stopReason },
757
+ }, 'run.failed');
758
+ }
759
+ async cancelOnce() {
760
+ this.cancelRequested = true;
761
+ try {
762
+ await this.sendCancel();
763
+ }
764
+ catch {
765
+ this.abortOwnerConnection('cancel_confirmation_failed');
766
+ return { mode: 'connection_aborted' };
767
+ }
768
+ this.cancelConfirmed = true;
769
+ this.finishIfReady();
770
+ const result = await withTimeout(this.settlement, this.cancelSettlementTimeoutMs);
771
+ if (result === undefined) {
772
+ this.abortOwnerConnection('cancel_terminal_timeout');
773
+ return { mode: 'connection_aborted' };
774
+ }
775
+ if (result.status === 'cancelled')
776
+ return { mode: 'native' };
777
+ if (result.status === 'connection_aborted') {
778
+ return { mode: 'connection_aborted' };
779
+ }
780
+ return { mode: 'already_terminal' };
781
+ }
782
+ finish(result, type) {
783
+ if (this.isTerminal())
784
+ return;
785
+ if (this.timeout !== undefined)
786
+ clearTimeout(this.timeout);
787
+ this.finalResult = result;
788
+ this.onTerminal(this);
789
+ this.eventQueue.pushTerminal(this.portableEvent({ type, data: result }));
790
+ this.eventQueue.close();
791
+ this.resolveSettlement(result);
792
+ }
793
+ emit(mapped) {
794
+ if (this.finalResult !== undefined)
795
+ return;
796
+ if (!this.eventQueue.push(this.portableEvent(mapped))) {
797
+ this.abortOwnerConnection('event_buffer_overflow');
798
+ }
799
+ }
800
+ portableEvent(mapped) {
801
+ const sequence = this.sequence++;
802
+ return {
803
+ id: `${this.reference.runId}:event:${String(sequence)}`,
804
+ type: mapped.type,
805
+ providerId: this.reference.providerId,
806
+ profileId: this.reference.profileId,
807
+ sessionId: this.reference.sessionId,
808
+ runId: this.reference.runId,
809
+ sequence,
810
+ timestamp: new Date().toISOString(),
811
+ data: mapped.data,
812
+ ...(mapped.providerEventType === undefined
813
+ ? {}
814
+ : { providerEventType: mapped.providerEventType }),
815
+ ...(mapped.raw === undefined ? {} : { raw: mapped.raw }),
816
+ };
817
+ }
818
+ }
819
+ class PiRpcPeer {
820
+ transport;
821
+ maxPendingRequests;
822
+ operationTimeoutMs;
823
+ observe;
824
+ abandoned = new Map();
825
+ pending = new Map();
826
+ owner;
827
+ closed = false;
828
+ requestSerial = 0;
829
+ constructor(transport, maxPendingRequests, operationTimeoutMs, observe) {
830
+ this.transport = transport;
831
+ this.maxPendingRequests = maxPendingRequests;
832
+ this.operationTimeoutMs = operationTimeoutMs;
833
+ this.observe = observe;
834
+ void this.pump();
835
+ }
836
+ bind(owner) {
837
+ if (this.owner !== undefined) {
838
+ throw new PiRpcFailure('protocol', 'Pi RPC peer is already bound.');
839
+ }
840
+ this.owner = owner;
841
+ }
842
+ request(command, options = {}) {
843
+ if (this.closed || !this.transport.isOpen()) {
844
+ return Promise.reject(new PiRpcFailure('connection', 'Pi RPC peer is closed.'));
845
+ }
846
+ if (this.pending.size >= this.maxPendingRequests) {
847
+ return Promise.reject(new PiRpcFailure('capacity', 'Pi RPC request capacity was reached.'));
848
+ }
849
+ if (options.signal?.aborted) {
850
+ return Promise.reject(new PiRpcFailure('aborted', 'Pi RPC request wait was aborted.'));
851
+ }
852
+ const timeoutMs = positiveTimer(options.timeoutMs ?? this.operationTimeoutMs, 'request timeout');
853
+ const id = `harapter-pi-${String(++this.requestSerial)}`;
854
+ return new Promise((resolve, reject) => {
855
+ const abortListener = options.signal === undefined
856
+ ? undefined
857
+ : () => {
858
+ this.abandonPending(id, new PiRpcFailure('aborted', 'Pi RPC request wait was aborted.'));
859
+ };
860
+ const timer = setTimeout(() => {
861
+ this.abandonPending(id, new PiRpcFailure('timeout', 'Pi RPC request wait timed out.'));
862
+ }, timeoutMs);
863
+ timer.unref();
864
+ this.pending.set(id, {
865
+ command: command.type,
866
+ resolve,
867
+ reject,
868
+ signal: options.signal,
869
+ abortListener,
870
+ timer,
871
+ });
872
+ if (abortListener !== undefined) {
873
+ options.signal?.addEventListener('abort', abortListener, {
874
+ once: true,
875
+ });
876
+ }
877
+ void this.transport.send({ ...command, id }, { timeoutMs }).catch(() => {
878
+ this.fail('connection');
879
+ });
880
+ });
881
+ }
882
+ send(message) {
883
+ if (this.closed) {
884
+ return Promise.reject(new PiRpcFailure('connection', 'Pi RPC peer is closed.'));
885
+ }
886
+ return this.transport.send(message, {
887
+ timeoutMs: this.operationTimeoutMs,
888
+ });
889
+ }
890
+ isOpen() {
891
+ return !this.closed && this.transport.isOpen();
892
+ }
893
+ async close() {
894
+ if (!this.closed) {
895
+ this.closed = true;
896
+ this.rejectAll(new PiRpcFailure('connection', 'Pi RPC peer was closed.'));
897
+ this.abandoned.clear();
898
+ }
899
+ await this.transport.close();
900
+ }
901
+ async pump() {
902
+ try {
903
+ for await (const message of this.transport.incoming()) {
904
+ this.handleMessage(message);
905
+ }
906
+ if (!this.closed)
907
+ this.fail('connection');
908
+ }
909
+ catch (error) {
910
+ if (this.closed)
911
+ return;
912
+ const protocol = error instanceof HarnessError &&
913
+ error.code === 'provider_api_incompatible';
914
+ const malformed = error instanceof JsonlTransportError &&
915
+ error.code === 'malformed_message';
916
+ this.fail(protocol || malformed ? 'protocol' : 'connection');
917
+ }
918
+ }
919
+ handleMessage(message) {
920
+ const type = message['type'];
921
+ if (type === 'response') {
922
+ this.handleResponse(message);
923
+ return;
924
+ }
925
+ if (type === 'extension_ui_request') {
926
+ this.owner?.receiveExtensionRequest(message);
927
+ if (this.owner === undefined)
928
+ this.observe(message);
929
+ return;
930
+ }
931
+ if (typeof type !== 'string') {
932
+ throw new HarnessError('provider_api_incompatible', 'Pi Agent emitted an incompatible RPC event envelope.', { retryable: false, providerId: PI_PROVIDER_ID });
933
+ }
934
+ this.observe(message);
935
+ this.owner?.receive(message);
936
+ }
937
+ handleResponse(message) {
938
+ const id = message['id'];
939
+ const command = message['command'];
940
+ const success = message['success'];
941
+ if (typeof id !== 'string' ||
942
+ typeof command !== 'string' ||
943
+ typeof success !== 'boolean') {
944
+ this.fail('protocol');
945
+ return;
946
+ }
947
+ const pending = this.pending.get(id);
948
+ if (pending === undefined) {
949
+ if (this.abandoned.get(id) === command) {
950
+ this.abandoned.delete(id);
951
+ return;
952
+ }
953
+ this.fail('protocol');
954
+ return;
955
+ }
956
+ if (pending.command !== command) {
957
+ this.fail('protocol');
958
+ return;
959
+ }
960
+ this.removePending(id, pending);
961
+ if (success)
962
+ pending.resolve(message['data']);
963
+ else {
964
+ pending.reject(new PiRpcFailure('remote_rejected', 'Pi RPC command was rejected.'));
965
+ }
966
+ }
967
+ abandonPending(id, error) {
968
+ const pending = this.pending.get(id);
969
+ if (pending === undefined)
970
+ return;
971
+ this.removePending(id, pending);
972
+ pending.reject(error);
973
+ if (this.abandoned.size >= this.maxPendingRequests) {
974
+ this.fail('connection');
975
+ return;
976
+ }
977
+ this.abandoned.set(id, pending.command);
978
+ }
979
+ rejectAll(error) {
980
+ for (const [id, pending] of [...this.pending]) {
981
+ this.removePending(id, pending);
982
+ pending.reject(error);
983
+ }
984
+ }
985
+ removePending(id, pending) {
986
+ this.pending.delete(id);
987
+ clearTimeout(pending.timer);
988
+ if (pending.abortListener !== undefined) {
989
+ pending.signal?.removeEventListener('abort', pending.abortListener);
990
+ }
991
+ }
992
+ fail(kind) {
993
+ if (this.closed)
994
+ return;
995
+ this.closed = true;
996
+ this.abandoned.clear();
997
+ this.rejectAll(new PiRpcFailure(kind, `Pi RPC ${kind} failure ended the connection.`));
998
+ if (kind === 'protocol')
999
+ this.owner?.failProtocol();
1000
+ else
1001
+ this.owner?.failConnection('transport_ended');
1002
+ void this.transport.close().catch(() => undefined);
1003
+ }
1004
+ }
1005
+ class PiRpcFailure extends Error {
1006
+ code;
1007
+ constructor(code, message) {
1008
+ super(message);
1009
+ this.code = code;
1010
+ this.name = 'PiRpcFailure';
1011
+ }
1012
+ }
1013
+ class EventQueue {
1014
+ capacity;
1015
+ values = [];
1016
+ closed = false;
1017
+ consumed = false;
1018
+ waiter;
1019
+ constructor(capacity) {
1020
+ this.capacity = capacity;
1021
+ }
1022
+ push(event) {
1023
+ if (this.closed)
1024
+ return false;
1025
+ if (this.waiter !== undefined) {
1026
+ const waiter = this.waiter;
1027
+ this.waiter = undefined;
1028
+ waiter({ done: false, value: event });
1029
+ return true;
1030
+ }
1031
+ if (this.values.length >= this.capacity - 1)
1032
+ return false;
1033
+ this.values.push(event);
1034
+ return true;
1035
+ }
1036
+ pushTerminal(event) {
1037
+ if (this.waiter !== undefined) {
1038
+ const waiter = this.waiter;
1039
+ this.waiter = undefined;
1040
+ waiter({ done: false, value: event });
1041
+ return;
1042
+ }
1043
+ this.values.push(event);
1044
+ }
1045
+ close() {
1046
+ this.closed = true;
1047
+ }
1048
+ iterable() {
1049
+ if (this.consumed) {
1050
+ return {
1051
+ [Symbol.asyncIterator]: () => ({
1052
+ next: () => Promise.reject(new HarnessError('run_conflict', 'Pi Agent Run events already have a consumer.', { retryable: false, providerId: PI_PROVIDER_ID })),
1053
+ }),
1054
+ };
1055
+ }
1056
+ this.consumed = true;
1057
+ return {
1058
+ [Symbol.asyncIterator]: () => ({ next: () => this.next() }),
1059
+ };
1060
+ }
1061
+ next() {
1062
+ const event = this.values.shift();
1063
+ if (event !== undefined)
1064
+ return Promise.resolve({ done: false, value: event });
1065
+ if (this.closed)
1066
+ return Promise.resolve({ done: true, value: undefined });
1067
+ if (this.waiter !== undefined) {
1068
+ return Promise.reject(new HarnessError('run_conflict', 'A Pi Agent Run event read is already pending.', { retryable: false, providerId: PI_PROVIDER_ID }));
1069
+ }
1070
+ return new Promise((resolveNext) => {
1071
+ this.waiter = resolveNext;
1072
+ });
1073
+ }
1074
+ }
1075
+ async function spawnPiPeer(profile, options, state, resumeId, observe) {
1076
+ if (profile.connection.kind !== 'process')
1077
+ throw profileInvalid(profile);
1078
+ const child = spawn(profile.connection.command, buildSessionArguments(profile, state, resumeId), {
1079
+ cwd: profile.connection.cwd,
1080
+ shell: false,
1081
+ stdio: ['pipe', 'pipe', 'ignore'],
1082
+ });
1083
+ await processStarted(child);
1084
+ try {
1085
+ const transport = new JsonlProcessTransport({
1086
+ readable: child.stdout,
1087
+ writable: child.stdin,
1088
+ cleanup: () => terminateChild(child),
1089
+ ...options.transport,
1090
+ });
1091
+ return new PiRpcPeer(transport, options.maxPendingRequests, options.operationTimeoutMs, observe);
1092
+ }
1093
+ catch (error) {
1094
+ try {
1095
+ await terminateChild(child);
1096
+ }
1097
+ catch {
1098
+ throw childCleanupFailure(profile);
1099
+ }
1100
+ throw error;
1101
+ }
1102
+ }
1103
+ async function probeRuntimeVersion(profile, timeoutMs) {
1104
+ if (profile.connection.kind !== 'process')
1105
+ throw profileInvalid(profile);
1106
+ const child = spawn(profile.connection.command, [...(profile.connection.args ?? []), '--version'], {
1107
+ cwd: profile.connection.cwd,
1108
+ shell: false,
1109
+ stdio: ['ignore', 'pipe', 'ignore'],
1110
+ });
1111
+ await processStarted(child);
1112
+ const output = await collectVersionOutput(child, timeoutMs);
1113
+ return parsePiVersionOutput(output);
1114
+ }
1115
+ async function collectVersionOutput(child, timeoutMs) {
1116
+ try {
1117
+ return await waitForVersionOutput(child, timeoutMs);
1118
+ }
1119
+ catch (error) {
1120
+ await terminateChild(child);
1121
+ throw error;
1122
+ }
1123
+ }
1124
+ function waitForVersionOutput(child, timeoutMs) {
1125
+ return new Promise((resolveOutput, rejectOutput) => {
1126
+ const chunks = [];
1127
+ let bytes = 0;
1128
+ let settled = false;
1129
+ const finish = (error) => {
1130
+ if (settled)
1131
+ return;
1132
+ settled = true;
1133
+ clearTimeout(timer);
1134
+ child.stdout.off('data', onData);
1135
+ child.off('exit', onExit);
1136
+ child.off('error', onError);
1137
+ if (error !== undefined)
1138
+ rejectOutput(error);
1139
+ else
1140
+ resolveOutput(Buffer.concat(chunks).toString('utf8'));
1141
+ };
1142
+ const onData = (chunk) => {
1143
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1144
+ bytes += buffer.byteLength;
1145
+ if (bytes > maximumVersionBytes) {
1146
+ finish(new PiRpcFailure('protocol', 'Pi version output exceeded its bound.'));
1147
+ return;
1148
+ }
1149
+ chunks.push(buffer);
1150
+ };
1151
+ const onExit = (code) => {
1152
+ finish(code === 0
1153
+ ? undefined
1154
+ : new PiRpcFailure('connection', 'Pi version probe exited unsuccessfully.'));
1155
+ };
1156
+ const onError = () => {
1157
+ finish(new PiRpcFailure('connection', 'Pi version probe failed.'));
1158
+ };
1159
+ const timer = setTimeout(() => {
1160
+ finish(new PiRpcFailure('timeout', 'Pi version probe timed out.'));
1161
+ }, timeoutMs);
1162
+ timer.unref();
1163
+ child.stdout.on('data', onData);
1164
+ child.once('exit', onExit);
1165
+ child.once('error', onError);
1166
+ });
1167
+ }
1168
+ function processStarted(child) {
1169
+ return new Promise((resolveStarted, rejectStarted) => {
1170
+ const onSpawn = () => {
1171
+ child.off('error', onError);
1172
+ child.on('error', () => undefined);
1173
+ resolveStarted();
1174
+ };
1175
+ const onError = (error) => {
1176
+ child.off('spawn', onSpawn);
1177
+ rejectStarted(error);
1178
+ };
1179
+ child.once('spawn', onSpawn);
1180
+ child.once('error', onError);
1181
+ });
1182
+ }
1183
+ async function terminateChild(child) {
1184
+ if (child.exitCode !== null || child.signalCode !== null)
1185
+ return;
1186
+ child.kill();
1187
+ if (await waitForChildExit(child, childTerminationTimeoutMs))
1188
+ return;
1189
+ child.kill('SIGKILL');
1190
+ if (await waitForChildExit(child, childTerminationTimeoutMs))
1191
+ return;
1192
+ throw new Error('Pi Agent child process did not exit.');
1193
+ }
1194
+ function waitForChildExit(child, timeoutMs) {
1195
+ if (child.exitCode !== null || child.signalCode !== null) {
1196
+ return Promise.resolve(true);
1197
+ }
1198
+ return new Promise((resolveExit) => {
1199
+ let settled = false;
1200
+ const onExit = () => {
1201
+ finish(true);
1202
+ };
1203
+ const timer = setTimeout(() => {
1204
+ finish(false);
1205
+ }, timeoutMs);
1206
+ function finish(exited) {
1207
+ if (settled)
1208
+ return;
1209
+ settled = true;
1210
+ clearTimeout(timer);
1211
+ child.off('exit', onExit);
1212
+ resolveExit(exited);
1213
+ }
1214
+ child.once('exit', onExit);
1215
+ if (child.exitCode !== null || child.signalCode !== null)
1216
+ finish(true);
1217
+ });
1218
+ }
1219
+ function piCapabilities(profile, options, runtimeIdentity, interactionObserved) {
1220
+ const native = { mode: 'native', source: 'schema' };
1221
+ const unsupportedStatus = {
1222
+ mode: 'unsupported',
1223
+ source: 'schema',
1224
+ };
1225
+ return {
1226
+ providerId: PI_PROVIDER_ID,
1227
+ profileId: profile.profileId,
1228
+ capabilities: {
1229
+ 'session.create': native,
1230
+ 'session.resume': options.persistSessions ? native : unsupportedStatus,
1231
+ 'session.close': {
1232
+ mode: 'adapter_controlled',
1233
+ reason: 'Closing the process does not delete the native Pi Session.',
1234
+ source: 'configuration',
1235
+ },
1236
+ 'session.workspace': unsupportedStatus,
1237
+ 'session.fork': unsupportedStatus,
1238
+ 'run.stream': native,
1239
+ 'run.cancel': native,
1240
+ 'run.timeout': {
1241
+ mode: 'emulated',
1242
+ reason: 'A local timer requests native Pi abort.',
1243
+ source: 'configuration',
1244
+ },
1245
+ 'run.concurrent': {
1246
+ mode: 'unsupported',
1247
+ limits: { perSession: 1 },
1248
+ source: 'configuration',
1249
+ },
1250
+ 'connection.abort': {
1251
+ mode: 'adapter_controlled',
1252
+ source: 'configuration',
1253
+ },
1254
+ 'input.text': native,
1255
+ 'input.image': unsupportedStatus,
1256
+ 'input.file': unsupportedStatus,
1257
+ 'interaction.approval': unsupportedStatus,
1258
+ 'interaction.user_input': unsupportedStatus,
1259
+ 'interaction.provider': interactionObserved
1260
+ ? native
1261
+ : {
1262
+ mode: 'unknown',
1263
+ reason: 'Pi extension UI interactions are runtime-extension dependent.',
1264
+ source: 'schema',
1265
+ },
1266
+ 'event.raw': { mode: 'adapter_controlled', source: 'configuration' },
1267
+ 'native.client': native,
1268
+ },
1269
+ observedAt: new Date().toISOString(),
1270
+ runtimeIdentity,
1271
+ };
1272
+ }
1273
+ function connectionOptions(value, profile) {
1274
+ const options = value ?? {};
1275
+ const allowed = new Set([
1276
+ 'cancelSettlementTimeoutMs',
1277
+ 'maxBufferedMessages',
1278
+ 'maxMessageBytes',
1279
+ 'maxPendingInteractions',
1280
+ 'maxPendingRequests',
1281
+ 'maxPendingWrites',
1282
+ 'maxRunEvents',
1283
+ 'operationTimeoutMs',
1284
+ 'persistSessions',
1285
+ 'writeTimeoutMs',
1286
+ ]);
1287
+ if (Object.keys(options).some((key) => !allowed.has(key))) {
1288
+ throw profileInvalid(profile);
1289
+ }
1290
+ if (options['persistSessions'] !== undefined &&
1291
+ typeof options['persistSessions'] !== 'boolean') {
1292
+ throw profileInvalid(profile);
1293
+ }
1294
+ const maxRunEvents = options['maxRunEvents'] === undefined
1295
+ ? defaultMaxRunEvents
1296
+ : positiveInteger(options['maxRunEvents'], profile);
1297
+ if (maxRunEvents < 2 || maxRunEvents > maximumRunEvents) {
1298
+ throw profileInvalid(profile);
1299
+ }
1300
+ const transport = {};
1301
+ for (const key of [
1302
+ 'maxBufferedMessages',
1303
+ 'maxMessageBytes',
1304
+ 'maxPendingWrites',
1305
+ 'writeTimeoutMs',
1306
+ ]) {
1307
+ const option = options[key];
1308
+ if (option !== undefined)
1309
+ transport[key] = positiveInteger(option, profile);
1310
+ }
1311
+ return {
1312
+ cancelSettlementTimeoutMs: options['cancelSettlementTimeoutMs'] === undefined
1313
+ ? defaultCancelSettlementTimeoutMs
1314
+ : positiveTimer(options['cancelSettlementTimeoutMs'], 'cancel settlement timeout', profile),
1315
+ maxPendingInteractions: options['maxPendingInteractions'] === undefined
1316
+ ? defaultMaxPendingInteractions
1317
+ : positiveInteger(options['maxPendingInteractions'], profile),
1318
+ maxPendingRequests: options['maxPendingRequests'] === undefined
1319
+ ? defaultMaxPendingRequests
1320
+ : positiveInteger(options['maxPendingRequests'], profile),
1321
+ maxRunEvents,
1322
+ operationTimeoutMs: options['operationTimeoutMs'] === undefined
1323
+ ? defaultOperationTimeoutMs
1324
+ : positiveTimer(options['operationTimeoutMs'], 'operation timeout', profile),
1325
+ persistSessions: options['persistSessions'] !== false,
1326
+ transport,
1327
+ };
1328
+ }
1329
+ function validateProfile(profile) {
1330
+ if (profile.providerId !== PI_PROVIDER_ID ||
1331
+ profile.connection.kind !== 'process' ||
1332
+ profile.connection.ownership !== 'adapter' ||
1333
+ profile.connection.command.length === 0 ||
1334
+ !isAbsolute(profile.connection.command) ||
1335
+ profile.connection.envRefs !== undefined ||
1336
+ (profile.connection.args ?? []).some((argument) => {
1337
+ const name = argument.split('=', 1)[0];
1338
+ return name === '--' || forbiddenRuntimeArguments.has(name ?? argument);
1339
+ })) {
1340
+ throw profileInvalid(profile);
1341
+ }
1342
+ }
1343
+ function prepareSessionInput(input, profile, options) {
1344
+ if (input.workspace !== undefined ||
1345
+ input.systemContext !== undefined ||
1346
+ input.model !== undefined ||
1347
+ input.providerOptions !== undefined ||
1348
+ input.metadata !== undefined) {
1349
+ throw new HarnessError('unsupported_capability', 'Pi Session creation does not map portable Session options.', {
1350
+ retryable: false,
1351
+ providerId: PI_PROVIDER_ID,
1352
+ profileId: profile.profileId,
1353
+ details: { capability: 'session.create.options' },
1354
+ });
1355
+ }
1356
+ return {
1357
+ strategy: 'isolated-process',
1358
+ persisted: options.persistSessions,
1359
+ };
1360
+ }
1361
+ function sessionStateFromRef(ref) {
1362
+ const state = record(ref.providerState);
1363
+ if (state?.['strategy'] !== 'isolated-process' ||
1364
+ typeof state['persisted'] !== 'boolean' ||
1365
+ Object.keys(state).some((key) => key !== 'strategy' && key !== 'persisted')) {
1366
+ throw new HarnessError('session_provider_mismatch', 'Pi Session reference has incompatible native state.', { retryable: false, providerId: PI_PROVIDER_ID });
1367
+ }
1368
+ return {
1369
+ strategy: 'isolated-process',
1370
+ persisted: state['persisted'],
1371
+ };
1372
+ }
1373
+ function buildSessionArguments(profile, state, resumeId) {
1374
+ const args = [
1375
+ ...(profile.connection.kind === 'process'
1376
+ ? (profile.connection.args ?? [])
1377
+ : []),
1378
+ ];
1379
+ args.push('--no-extensions', '--no-skills', '--no-prompt-templates', '--mode', 'rpc');
1380
+ if (!state.persisted)
1381
+ args.push('--no-session');
1382
+ if (resumeId !== undefined)
1383
+ args.push('--session', resumeId);
1384
+ return args;
1385
+ }
1386
+ function validateRunOptions(options) {
1387
+ if (options.providerOptions !== undefined || options.metadata !== undefined) {
1388
+ throw new HarnessError('unsupported_capability', 'Pi Agent Run Provider options and metadata are not mapped.', {
1389
+ retryable: false,
1390
+ providerId: PI_PROVIDER_ID,
1391
+ details: { capability: 'run.options' },
1392
+ });
1393
+ }
1394
+ validateRunTimeout(options.timeoutMs);
1395
+ }
1396
+ function validateRunTimeout(value) {
1397
+ if (value === undefined)
1398
+ return;
1399
+ positiveTimer(value, 'Run timeout');
1400
+ }
1401
+ function prepareInteractionResponse(pending, value) {
1402
+ const response = record(value);
1403
+ if (response?.['cancelled'] === true && Object.keys(response).length === 1) {
1404
+ return {
1405
+ type: 'extension_ui_response',
1406
+ id: pending.nativeId,
1407
+ cancelled: true,
1408
+ };
1409
+ }
1410
+ if (pending.method === 'confirm' &&
1411
+ typeof response?.['confirmed'] === 'boolean' &&
1412
+ Object.keys(response).length === 1) {
1413
+ return {
1414
+ type: 'extension_ui_response',
1415
+ id: pending.nativeId,
1416
+ confirmed: response['confirmed'],
1417
+ };
1418
+ }
1419
+ if (pending.method !== 'confirm' &&
1420
+ typeof response?.['value'] === 'string' &&
1421
+ Object.keys(response).length === 1) {
1422
+ return {
1423
+ type: 'extension_ui_response',
1424
+ id: pending.nativeId,
1425
+ value: response['value'],
1426
+ };
1427
+ }
1428
+ throw new HarnessError('invalid_request', 'Pi interaction response does not match the requested method.', { retryable: false, providerId: PI_PROVIDER_ID });
1429
+ }
1430
+ function interactionSchema(method, value) {
1431
+ if (method === 'confirm')
1432
+ return { method, response: 'confirmed' };
1433
+ if (method === 'select') {
1434
+ const options = Array.isArray(value['options'])
1435
+ ? value['options']
1436
+ .filter((option) => typeof option === 'string')
1437
+ .slice(0, 64)
1438
+ .map((option) => option.slice(0, 128))
1439
+ : [];
1440
+ return { method, options, response: 'value' };
1441
+ }
1442
+ return { method, response: 'value' };
1443
+ }
1444
+ function boundedText(value) {
1445
+ return typeof value === 'string' && value.length > 0
1446
+ ? value.slice(0, 256)
1447
+ : undefined;
1448
+ }
1449
+ function positiveInteger(value, profile) {
1450
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) {
1451
+ if (profile !== undefined)
1452
+ throw profileInvalid(profile);
1453
+ throw new PiRpcFailure('protocol', 'Pi RPC limit is invalid.');
1454
+ }
1455
+ return value;
1456
+ }
1457
+ function positiveTimer(value, label, profile) {
1458
+ const result = positiveInteger(value, profile);
1459
+ if (result > maximumTimerMilliseconds) {
1460
+ if (profile !== undefined)
1461
+ throw profileInvalid(profile);
1462
+ throw new PiRpcFailure('protocol', `Pi ${label} is invalid.`);
1463
+ }
1464
+ return result;
1465
+ }
1466
+ function invalidInteraction(profile) {
1467
+ return new HarnessError('invalid_request', 'Pi interaction response does not match an active Provider request.', {
1468
+ retryable: false,
1469
+ providerId: PI_PROVIDER_ID,
1470
+ profileId: profile.profileId,
1471
+ });
1472
+ }
1473
+ function unsupported(profile, capability, message) {
1474
+ return new HarnessError('unsupported_capability', message, {
1475
+ retryable: false,
1476
+ providerId: PI_PROVIDER_ID,
1477
+ profileId: profile.profileId,
1478
+ details: { capability },
1479
+ });
1480
+ }
1481
+ function profileInvalid(profile) {
1482
+ return new HarnessError('profile_invalid', 'Pi Agent requires an absolute adapter-owned process command without unresolved secrets or lifecycle-conflicting arguments.', {
1483
+ retryable: false,
1484
+ providerId: PI_PROVIDER_ID,
1485
+ profileId: profile.profileId,
1486
+ });
1487
+ }
1488
+ function childCleanupFailure(profile) {
1489
+ return new HarnessError('connection_aborted', 'Pi Agent child process cleanup could not be confirmed.', {
1490
+ retryable: false,
1491
+ providerId: PI_PROVIDER_ID,
1492
+ profileId: profile.profileId,
1493
+ providerCode: 'child_cleanup_failed',
1494
+ });
1495
+ }
1496
+ function isChildCleanupFailure(error) {
1497
+ return (error instanceof HarnessError &&
1498
+ error.providerCode === 'child_cleanup_failed');
1499
+ }
1500
+ function snapshotProfile(profile) {
1501
+ return {
1502
+ ...profile,
1503
+ connection: {
1504
+ ...profile.connection,
1505
+ ...(profile.connection.kind === 'process' && profile.connection.args
1506
+ ? { args: [...profile.connection.args] }
1507
+ : {}),
1508
+ ...(profile.connection.kind === 'process'
1509
+ ? { cwd: resolve(profile.connection.cwd ?? process.cwd()) }
1510
+ : {}),
1511
+ },
1512
+ ...(profile.providerOptions === undefined
1513
+ ? {}
1514
+ : { providerOptions: { ...profile.providerOptions } }),
1515
+ };
1516
+ }
1517
+ function mapError(error, profile, phase, connecting = false) {
1518
+ if (error instanceof HarnessError)
1519
+ return error;
1520
+ const systemCode = typeof error === 'object' && error !== null && 'code' in error
1521
+ ? error.code
1522
+ : undefined;
1523
+ if (connecting && systemCode === 'ENOENT') {
1524
+ return new HarnessError('runtime_not_found', 'The configured Pi Agent runtime was not found.', {
1525
+ retryable: false,
1526
+ providerId: PI_PROVIDER_ID,
1527
+ profileId: profile.profileId,
1528
+ });
1529
+ }
1530
+ if (error instanceof PiRpcFailure) {
1531
+ const code = error.code === 'protocol'
1532
+ ? 'provider_api_incompatible'
1533
+ : error.code === 'timeout' || error.code === 'aborted'
1534
+ ? 'timeout'
1535
+ : error.code === 'remote_rejected'
1536
+ ? 'provider_error'
1537
+ : error.code === 'capacity'
1538
+ ? 'provider_error'
1539
+ : connecting
1540
+ ? 'connection_failed'
1541
+ : 'connection_aborted';
1542
+ return new HarnessError(code, `Pi Agent ${phase} did not complete.`, {
1543
+ retryable: error.code === 'timeout',
1544
+ providerId: PI_PROVIDER_ID,
1545
+ profileId: profile.profileId,
1546
+ providerCode: error.code,
1547
+ });
1548
+ }
1549
+ if (error instanceof JsonlTransportError) {
1550
+ return new HarnessError(connecting ? 'connection_failed' : 'connection_aborted', `Pi Agent ${phase} did not complete.`, {
1551
+ retryable: false,
1552
+ providerId: PI_PROVIDER_ID,
1553
+ profileId: profile.profileId,
1554
+ providerCode: error.code,
1555
+ });
1556
+ }
1557
+ return new HarnessError(connecting ? 'connection_failed' : 'provider_error', `Pi Agent ${phase} did not complete.`, {
1558
+ retryable: false,
1559
+ providerId: PI_PROVIDER_ID,
1560
+ profileId: profile.profileId,
1561
+ });
1562
+ }
1563
+ function record(value) {
1564
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
1565
+ ? value
1566
+ : undefined;
1567
+ }
1568
+ async function withTimeout(promise, timeoutMs) {
1569
+ let timer;
1570
+ const timeout = new Promise((resolveTimeout) => {
1571
+ timer = setTimeout(() => {
1572
+ resolveTimeout(undefined);
1573
+ }, timeoutMs);
1574
+ timer.unref();
1575
+ });
1576
+ try {
1577
+ return await Promise.race([promise, timeout]);
1578
+ }
1579
+ finally {
1580
+ if (timer !== undefined)
1581
+ clearTimeout(timer);
1582
+ }
1583
+ }
1584
+ //# sourceMappingURL=adapter.js.map