@automatalabs/pi-acp 0.2.8 → 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/README.md CHANGED
@@ -69,7 +69,24 @@ pi-acp owns the `mcp__` prefix for injected MCP tools. Pi extensions must not re
69
69
 
70
70
  ## Version 1 limitations
71
71
 
72
- Load replay is the active linear branch and excludes branch topology and compaction summaries. `additionalDirectories` is accepted but ignored because pi is not root-confined. ACP prompt audio is degraded to a text note. Mid-turn steering and terminal-login authentication are not exposed.
72
+ Load replay is the active linear branch and excludes branch topology and compaction summaries. `additionalDirectories` is accepted but ignored because pi is not root-confined. ACP prompt audio is degraded to a text note. Terminal-login authentication is not exposed.
73
+
74
+ ### Native session steering
75
+
76
+ Pi advertises the `_session/steering` extension at top-level initialize metadata as
77
+ `_meta: { steering: { supported: true } }`, with codex-shaped semantics. While a Pi prompt turn is
78
+ live, the server converts the supplied ACP content with the same text/image conversion as
79
+ `session/prompt` and calls native `AgentSession.steer(text, images)`, returning
80
+ `{ outcome: "injected" }`; the original `session/prompt` still owns all output, usage, and
81
+ settlement. When the session is idle — or a steer arrives just as the turn settles, before pi could
82
+ consume it — the content runs as a fire-and-forget turn instead and the call returns
83
+ `{ outcome: "startedNewTurn" }`: never an error, never dropped, never silently prepended to the next
84
+ prompt. That turn occupies the single turn slot (a concurrent `session/prompt` is `session_busy`)
85
+ and streams through normal `session/update`s with no owned `PromptResponse`. A steer racing an
86
+ in-progress cancel returns `{ outcome: "failed" }` — cancellation wins, and pi's queued steering is
87
+ cleared synchronously before aborting so queued content never restarts generation after
88
+ cancellation. Unexpected internal failures also resolve as `{ outcome: "failed" }` rather than a
89
+ JSON-RPC error.
73
90
 
74
91
  ## Development
75
92
 
package/dist/agent.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { AgentNotificationContext, AgentRequestContext, AuthenticateRequest, CloseSessionRequest, ForkSessionRequest, InitializeRequest, ListSessionsRequest, LoadSessionRequest, NewSessionRequest, PromptRequest, ResumeSessionRequest, SetSessionConfigOptionRequest } from "@agentclientprotocol/sdk";
2
2
  import type { PiAcpDeps } from "./deps.js";
3
+ import type { SteeringRequest, SteeringResponse } from "./steering.js";
3
4
  export { PKG_VERSION } from "./version.js";
4
5
  export declare class PiAcpAgent {
5
6
  private readonly deps;
@@ -38,6 +39,11 @@ export declare class PiAcpAgent {
38
39
  };
39
40
  };
40
41
  authMethods: import("@agentclientprotocol/sdk").AuthMethod[];
42
+ _meta: {
43
+ steering: {
44
+ supported: boolean;
45
+ };
46
+ };
41
47
  };
42
48
  authenticate(context: AgentRequestContext<AuthenticateRequest>): Record<string, never>;
43
49
  private ensureMayOpen;
@@ -81,6 +87,7 @@ export declare class PiAcpAgent {
81
87
  configOptions: import("@agentclientprotocol/sdk").SessionConfigOption[];
82
88
  }>;
83
89
  prompt(context: AgentRequestContext<PromptRequest>): Promise<import("@agentclientprotocol/sdk").PromptResponse>;
90
+ steer(context: AgentRequestContext<SteeringRequest>): Promise<SteeringResponse>;
84
91
  cancel(context: AgentNotificationContext<{
85
92
  sessionId: string;
86
93
  }>): void;
@@ -1 +1 @@
1
- {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,wBAAwB,EACxB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,EAElB,iBAAiB,EACjB,aAAa,EACb,oBAAoB,EACpB,6BAA6B,EAC9B,MAAM,0BAA0B,CAAC;AAWlC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAW3C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AA0H3C,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAY;IACjC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAgC;IACrD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA8B;IACtD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA8B;IACjE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA+B;IAC5D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;IAChD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAmC;IAClE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAM;IACpC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,cAAc,CAA4B;IAClD,OAAO,CAAC,gBAAgB,CAAS;gBAErB,IAAI,EAAE,SAAS;IAI3B,UAAU,CAAC,QAAQ,EAAE,mBAAmB,CAAC,iBAAiB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;IAkB3D,YAAY,CAAC,OAAO,EAAE,mBAAmB,CAAC,mBAAmB,CAAC;IAI9D,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,OAAO;IAKf,OAAO,CAAC,YAAY;IAmBpB,OAAO,CAAC,KAAK;IAMb,OAAO,CAAC,IAAI;YAOE,UAAU;YAkCV,SAAS;IAqLvB,OAAO,CAAC,YAAY;IAKpB,UAAU,CAAC,OAAO,EAAE,mBAAmB,CAAC,iBAAiB,CAAC;;;;;IA6B1D,OAAO,CAAC,QAAQ;IAgDhB,WAAW,CAAC,OAAO,EAAE,mBAAmB,CAAC,kBAAkB,CAAC;;;;IAI5D,aAAa,CAAC,OAAO,EAAE,mBAAmB,CAAC,oBAAoB,CAAC;;;;IAIhE,WAAW,CAAC,OAAO,EAAE,mBAAmB,CAAC,kBAAkB,CAAC;;;;;IA8EtD,YAAY,CAAC,OAAO,EAAE,mBAAmB,CAAC,mBAAmB,CAAC;;;;;;;;IAqC9D,YAAY,CAAC,OAAO,EAAE,mBAAmB,CAAC,mBAAmB,CAAC;IA0BpE,OAAO,CAAC,WAAW;IAOnB,eAAe,CAAC,OAAO,EAAE,mBAAmB,CAAC,6BAA6B,CAAC;;;IAU3E,MAAM,CAAC,OAAO,EAAE,mBAAmB,CAAC,aAAa,CAAC;IAIlD,MAAM,CAAC,OAAO,EAAE,wBAAwB,CAAC;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,IAAI;YAIxD,eAAe;IAwB7B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;YAWV,iBAAiB;CA4BhC"}
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,wBAAwB,EACxB,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,EAElB,iBAAiB,EACjB,aAAa,EACb,oBAAoB,EACpB,6BAA6B,EAC9B,MAAM,0BAA0B,CAAC;AAWlC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAS3C,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAGvE,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AA0H3C,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAY;IACjC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAgC;IACrD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA8B;IACtD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA8B;IACjE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA+B;IAC5D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;IAChD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAmC;IAClE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAM;IACpC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,cAAc,CAA4B;IAClD,OAAO,CAAC,gBAAgB,CAAS;gBAErB,IAAI,EAAE,SAAS;IAI3B,UAAU,CAAC,QAAQ,EAAE,mBAAmB,CAAC,iBAAiB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAmB3D,YAAY,CAAC,OAAO,EAAE,mBAAmB,CAAC,mBAAmB,CAAC;IAI9D,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,OAAO;IAKf,OAAO,CAAC,YAAY;IAmBpB,OAAO,CAAC,KAAK;IAMb,OAAO,CAAC,IAAI;YAOE,UAAU;YAkCV,SAAS;IAqLvB,OAAO,CAAC,YAAY;IAKpB,UAAU,CAAC,OAAO,EAAE,mBAAmB,CAAC,iBAAiB,CAAC;;;;;IA6B1D,OAAO,CAAC,QAAQ;IAgDhB,WAAW,CAAC,OAAO,EAAE,mBAAmB,CAAC,kBAAkB,CAAC;;;;IAI5D,aAAa,CAAC,OAAO,EAAE,mBAAmB,CAAC,oBAAoB,CAAC;;;;IAIhE,WAAW,CAAC,OAAO,EAAE,mBAAmB,CAAC,kBAAkB,CAAC;;;;;IA8EtD,YAAY,CAAC,OAAO,EAAE,mBAAmB,CAAC,mBAAmB,CAAC;;;;;;;;IAqC9D,YAAY,CAAC,OAAO,EAAE,mBAAmB,CAAC,mBAAmB,CAAC;IA0BpE,OAAO,CAAC,WAAW;IAOnB,eAAe,CAAC,OAAO,EAAE,mBAAmB,CAAC,6BAA6B,CAAC;;;IAU3E,MAAM,CAAC,OAAO,EAAE,mBAAmB,CAAC,aAAa,CAAC;IAIlD,KAAK,CAAC,OAAO,EAAE,mBAAmB,CAAC,eAAe,CAAC,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAW/E,MAAM,CAAC,OAAO,EAAE,wBAAwB,CAAC;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,IAAI;YAIxD,eAAe;IAwB7B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;YAWV,iBAAiB;CA4BhC"}
package/dist/agent.js CHANGED
@@ -148,6 +148,7 @@ export class PiAcpAgent {
148
148
  sessionCapabilities: { resume: {}, fork: {}, list: {}, close: {} },
149
149
  },
150
150
  authMethods: AUTH_METHODS,
151
+ _meta: { steering: { supported: true } },
151
152
  };
152
153
  }
153
154
  authenticate(context) {
@@ -640,6 +641,17 @@ export class PiAcpAgent {
640
641
  prompt(context) {
641
642
  return this.requireLive(context.params.sessionId).prompt(context.params, context.signal);
642
643
  }
644
+ steer(context) {
645
+ return this.requireLive(context.params.sessionId).steer(context.params)
646
+ .catch((error) => {
647
+ if (isRequestError(error))
648
+ throw error;
649
+ // Codex-shaped catch-all: an unexpected internal failure resolves as a failed
650
+ // steering outcome; only typed adapter errors surface as JSON-RPC errors.
651
+ console.error("pi-acp steering failed:", error);
652
+ return { outcome: "failed" };
653
+ });
654
+ }
643
655
  cancel(context) {
644
656
  this.live.get(context.params.sessionId)?.cancel();
645
657
  }
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,MAAM,EACZ,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAExD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,MAAM,CAAC,OAAO,GAAE,aAAkB;;;GAoBvD"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,MAAM,EACZ,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAGxD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,MAAM,CAAC,OAAO,GAAE,aAAkB;;;GAqBvD"}
package/dist/server.js CHANGED
@@ -2,6 +2,7 @@ import { Readable, Writable } from "node:stream";
2
2
  import { agent as acpAgent, methods, ndJsonStream, } from "@agentclientprotocol/sdk";
3
3
  import { PiAcpAgent } from "./agent.js";
4
4
  import { resolveDeps } from "./deps.js";
5
+ import { SESSION_STEERING_METHOD, steeringRequestParser } from "./steering.js";
5
6
  export { PiAcpAgent } from "./agent.js";
6
7
  export async function runAcp(options = {}) {
7
8
  const impl = new PiAcpAgent(await resolveDeps(options.deps));
@@ -16,6 +17,7 @@ export async function runAcp(options = {}) {
16
17
  .onRequest(methods.agent.session.close, (context) => impl.closeSession(context))
17
18
  .onRequest(methods.agent.session.setConfigOption, (context) => impl.setConfigOption(context))
18
19
  .onRequest(methods.agent.session.prompt, (context) => impl.prompt(context))
20
+ .onRequest(SESSION_STEERING_METHOD, steeringRequestParser, (context) => impl.steer(context))
19
21
  .onNotification(methods.agent.session.cancel, (context) => impl.cancel(context));
20
22
  const stream = options.stream ?? ndJsonStream(Writable.toWeb(process.stdout), Readable.toWeb(process.stdin));
21
23
  const connection = app.connect(stream);
package/dist/session.d.ts CHANGED
@@ -4,6 +4,7 @@ import type { Api, Model } from "@earendil-works/pi-ai";
4
4
  import type { PiAcpDeps } from "./deps.js";
5
5
  import type { McpBridge, McpResultProjection } from "./mcp-bridge.js";
6
6
  import { type ChildProcessRegistrySlot } from "./child-process-registry.js";
7
+ import type { SteeringRequest, SteeringResponse } from "./steering.js";
7
8
  export interface PiSessionOptions {
8
9
  sessionId: string;
9
10
  session: AgentSession;
@@ -37,6 +38,8 @@ export declare class PiSession {
37
38
  private disposed;
38
39
  private unsubscribe;
39
40
  private activeTurn;
41
+ private steerChain;
42
+ private readonly pendingSteerImages;
40
43
  private configReserved;
41
44
  private cleanupDirty;
42
45
  private cleanupGeneration;
@@ -64,6 +67,35 @@ export declare class PiSession {
64
67
  private handlePiResolved;
65
68
  private handlePiRejected;
66
69
  prompt(params: PromptRequest, requestSignal: AbortSignal): Promise<PromptResponse>;
70
+ /** Open the session's single turn slot and run one pi prompt through the full turn
71
+ * machinery (turn boundary, cancellation, settlement, usage). Admission is the caller's
72
+ * job: prompt() rejects while busy, and the steering paths start a turn only when the
73
+ * slot is free. Steer-started turns pass no requestSignal (cancel() still aborts them)
74
+ * and use preflightResult as their turn-committed acceptance signal. */
75
+ private startTurn;
76
+ /** `_session/steering`, codex-shaped: inject into the live turn when there is one,
77
+ * otherwise start a fire-and-forget turn with the content and answer `startedNewTurn`.
78
+ * Requests are serialized per session (pi rejects concurrent runs), so a second steer
79
+ * behind an idle one injects into the turn the first one started. */
80
+ steer(params: SteeringRequest): Promise<SteeringResponse>;
81
+ private performSteer;
82
+ /** Pull one undelivered steering message back out of pi's queues. Returns false when pi
83
+ * already consumed it (or a settlement drain already redispatched it). clearQueue is
84
+ * all-or-nothing, so any other queued texts are re-enqueued untouched (text-only: pi
85
+ * surfaces only the text mirror of messages it holds). */
86
+ private recoverSteer;
87
+ /** Idle steer: start a normal turn with the content so it occupies the single turn slot
88
+ * (session/prompt during it is legitimately busy; cancel/close still work) and resolve
89
+ * as soon as the turn is committed — pi's preflightResult is the acceptance signal, the
90
+ * analogue of codex's onTurnStarted. The turn itself is fire-and-forget: nothing owns
91
+ * its PromptResponse and its output streams through the usual session/update path. */
92
+ private startTurnFromSteer;
93
+ /** Settlement-time orphan recovery. pi's steering/follow-up queues are polled only from
94
+ * inside a run, so a message still queued when the run settles would silently prepend
95
+ * itself to the NEXT session/prompt. Both reference adapters run late content instead of
96
+ * leaking or dropping it (codex falls through to a new turn; claude's streaming input
97
+ * absorbs it as background generation), so redispatch it as a fire-and-forget turn. */
98
+ private redispatchOrphanedSteering;
67
99
  cancel(): void;
68
100
  childCleanupFailure(): void;
69
101
  dispose(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
package/dist/session.js CHANGED
@@ -29,6 +29,8 @@ export class PiSession {
29
29
  disposed = false;
30
30
  unsubscribe;
31
31
  activeTurn;
32
+ steerChain = Promise.resolve();
33
+ pendingSteerImages = new Map();
32
34
  configReserved = false;
33
35
  cleanupDirty = false;
34
36
  cleanupGeneration;
@@ -259,6 +261,15 @@ export class PiSession {
259
261
  const turn = this.activeTurn;
260
262
  if (turn && !turn.controller.signal.aborted)
261
263
  turn.controller.abort();
264
+ let clearQueuePi = Promise.resolve();
265
+ this.pendingSteerImages.clear();
266
+ try {
267
+ this.pi.clearQueue();
268
+ }
269
+ catch (error) {
270
+ clearQueuePi = Promise.reject(error);
271
+ }
272
+ clearQueuePi.catch(() => undefined);
262
273
  let abortPi;
263
274
  try {
264
275
  abortPi = this.pi.abort();
@@ -267,7 +278,10 @@ export class PiSession {
267
278
  abortPi = Promise.reject(error);
268
279
  }
269
280
  abortPi.catch(() => undefined);
270
- const operations = Promise.allSettled([abortPi, captured.drain]);
281
+ // Keep the established abort-before-child-drain failure precedence. Queue
282
+ // clearing is invoked first, but its failure is considered only after both
283
+ // pre-existing cleanup operations.
284
+ const operations = Promise.allSettled([abortPi, captured.drain, clearQueuePi]);
271
285
  generation.promise = new Promise((resolve, reject) => {
272
286
  let claimed = false;
273
287
  const fail = (error) => {
@@ -366,6 +380,8 @@ export class PiSession {
366
380
  const terminal = terminalAssistant(messages);
367
381
  const stopReason = stopReasonFor(terminal, turn.controller.signal.aborted);
368
382
  this.finish(turn, { response: { stopReason, usage: promptUsage(messages) } });
383
+ if (!turn.controller.signal.aborted)
384
+ this.redispatchOrphanedSteering();
369
385
  }
370
386
  catch (error) {
371
387
  if (this.pumpFailure !== undefined)
@@ -382,6 +398,7 @@ export class PiSession {
382
398
  try {
383
399
  await this.drain();
384
400
  this.finish(turn, { error: classifyPreflight(error) });
401
+ this.redispatchOrphanedSteering();
385
402
  }
386
403
  catch {
387
404
  this.notificationFailure();
@@ -407,7 +424,14 @@ export class PiSession {
407
424
  async prompt(params, requestSignal) {
408
425
  if (this.busy)
409
426
  throw adapterError("session_busy");
410
- const converted = convertPromptContent(params.prompt);
427
+ return this.startTurn(convertPromptContent(params.prompt), { requestSignal });
428
+ }
429
+ /** Open the session's single turn slot and run one pi prompt through the full turn
430
+ * machinery (turn boundary, cancellation, settlement, usage). Admission is the caller's
431
+ * job: prompt() rejects while busy, and the steering paths start a turn only when the
432
+ * slot is free. Steer-started turns pass no requestSignal (cancel() still aborts them)
433
+ * and use preflightResult as their turn-committed acceptance signal. */
434
+ startTurn(converted, opts) {
411
435
  const text = converted.text;
412
436
  let startMessageIndex;
413
437
  try {
@@ -439,12 +463,15 @@ export class PiSession {
439
463
  startMessageIndex,
440
464
  };
441
465
  this.activeTurn = turn;
442
- const abortFromRequest = () => this.abortTurn(turn);
443
- if (requestSignal.aborted)
444
- abortFromRequest();
445
- else {
446
- requestSignal.addEventListener("abort", abortFromRequest, { once: true });
447
- turn.removeRequestAbort = () => requestSignal.removeEventListener("abort", abortFromRequest);
466
+ const requestSignal = opts.requestSignal;
467
+ if (requestSignal) {
468
+ const abortFromRequest = () => this.abortTurn(turn);
469
+ if (requestSignal.aborted)
470
+ abortFromRequest();
471
+ else {
472
+ requestSignal.addEventListener("abort", abortFromRequest, { once: true });
473
+ turn.removeRequestAbort = () => requestSignal.removeEventListener("abort", abortFromRequest);
474
+ }
448
475
  }
449
476
  this.runTurnTask(turn, (async () => {
450
477
  turn.releaseBoundary = await this.mcpBridge.acquireTurnBoundary();
@@ -469,7 +496,10 @@ export class PiSession {
469
496
  }
470
497
  let piPromise;
471
498
  try {
472
- piPromise = this.pi.prompt(text, { images: converted.images });
499
+ piPromise = this.pi.prompt(text, {
500
+ images: converted.images,
501
+ ...(opts.preflightResult ? { preflightResult: opts.preflightResult } : {}),
502
+ });
473
503
  }
474
504
  catch (error) {
475
505
  await this.handlePiRejected(turn, error);
@@ -479,6 +509,150 @@ export class PiSession {
479
509
  })());
480
510
  return result;
481
511
  }
512
+ /** `_session/steering`, codex-shaped: inject into the live turn when there is one,
513
+ * otherwise start a fire-and-forget turn with the content and answer `startedNewTurn`.
514
+ * Requests are serialized per session (pi rejects concurrent runs), so a second steer
515
+ * behind an idle one injects into the turn the first one started. */
516
+ async steer(params) {
517
+ const converted = convertPromptContent(params.prompt);
518
+ const run = this.steerChain.then(() => this.performSteer(converted));
519
+ this.steerChain = run.then(() => undefined, () => undefined);
520
+ return run;
521
+ }
522
+ async performSteer(converted) {
523
+ for (;;) {
524
+ if (this.closing || this.disposed)
525
+ throw adapterError("session_terminated");
526
+ const turn = this.activeTurn;
527
+ if (!turn) {
528
+ if (this.busy)
529
+ throw adapterError("session_busy");
530
+ return this.startTurnFromSteer(converted);
531
+ }
532
+ if (turn.completed) {
533
+ await turn.settlement;
534
+ continue;
535
+ }
536
+ if (turn.controller.signal.aborted) {
537
+ // Cancellation wins: cleanupTurn clears pi's queues so a steer racing a cancel
538
+ // must not restart the generation the user just stopped. The steer did not take.
539
+ return { outcome: "failed" };
540
+ }
541
+ // A committed-but-not-yet-streaming turn is steerable too: pi's run loop polls the
542
+ // steering queue before its first LLM call, so the message joins the imminent turn.
543
+ this.pendingSteerImages.set(converted.text, converted.images);
544
+ await this.pi.steer(converted.text, converted.images);
545
+ if (this.activeTurn === turn && !turn.completed && !turn.controller.signal.aborted) {
546
+ return { outcome: "injected" };
547
+ }
548
+ if (turn.controller.signal.aborted) {
549
+ this.recoverSteer(converted.text);
550
+ return { outcome: "failed" };
551
+ }
552
+ // The run settled underneath the enqueue. pi's queues are polled only from inside a
553
+ // run, so an undelivered message would silently prepend itself to the next
554
+ // session/prompt; recover it and place it into a live turn or a new one instead.
555
+ if (!this.recoverSteer(converted.text))
556
+ return { outcome: "injected" };
557
+ }
558
+ }
559
+ /** Pull one undelivered steering message back out of pi's queues. Returns false when pi
560
+ * already consumed it (or a settlement drain already redispatched it). clearQueue is
561
+ * all-or-nothing, so any other queued texts are re-enqueued untouched (text-only: pi
562
+ * surfaces only the text mirror of messages it holds). */
563
+ recoverSteer(text) {
564
+ try {
565
+ if (!this.pi.getSteeringMessages().includes(text)) {
566
+ this.pendingSteerImages.delete(text);
567
+ return false;
568
+ }
569
+ const cleared = this.pi.clearQueue();
570
+ const steering = [...cleared.steering];
571
+ const index = steering.indexOf(text);
572
+ if (index !== -1)
573
+ steering.splice(index, 1);
574
+ for (const other of steering)
575
+ void this.pi.steer(other).catch(() => undefined);
576
+ for (const other of cleared.followUp)
577
+ void this.pi.followUp(other).catch(() => undefined);
578
+ this.pendingSteerImages.delete(text);
579
+ return index !== -1;
580
+ }
581
+ catch (error) {
582
+ console.error("pi-acp steering recovery error:", error);
583
+ return false;
584
+ }
585
+ }
586
+ /** Idle steer: start a normal turn with the content so it occupies the single turn slot
587
+ * (session/prompt during it is legitimately busy; cancel/close still work) and resolve
588
+ * as soon as the turn is committed — pi's preflightResult is the acceptance signal, the
589
+ * analogue of codex's onTurnStarted. The turn itself is fire-and-forget: nothing owns
590
+ * its PromptResponse and its output streams through the usual session/update path. */
591
+ async startTurnFromSteer(converted) {
592
+ let commit;
593
+ const committed = new Promise((resolve) => {
594
+ commit = (result) => resolve({ kind: result });
595
+ });
596
+ const done = this.startTurn(converted, {
597
+ preflightResult: (success) => commit(success ? "committed" : "preflight-failed"),
598
+ });
599
+ const doneTagged = done.then((response) => ({ kind: "settled", response }), (error) => ({ kind: "rejected", error }));
600
+ const raced = await Promise.race([committed, doneTagged]);
601
+ switch (raced.kind) {
602
+ case "committed":
603
+ done.catch((error) => console.error("pi-acp steer-started turn failed:", error));
604
+ return { outcome: "startedNewTurn" };
605
+ case "settled":
606
+ // The prompt finished without a generation (e.g. extension-handled input). A
607
+ // cancel that beat the turn start means the steer never took.
608
+ return raced.response.stopReason === "cancelled"
609
+ ? { outcome: "failed" }
610
+ : { outcome: "startedNewTurn" };
611
+ case "preflight-failed": {
612
+ // Surface the same mapped preflight rejection a prompt() caller would see.
613
+ const settled = await doneTagged;
614
+ if (settled.kind === "rejected")
615
+ throw settled.error;
616
+ return { outcome: "startedNewTurn" };
617
+ }
618
+ case "rejected":
619
+ throw raced.error;
620
+ }
621
+ }
622
+ /** Settlement-time orphan recovery. pi's steering/follow-up queues are polled only from
623
+ * inside a run, so a message still queued when the run settles would silently prepend
624
+ * itself to the NEXT session/prompt. Both reference adapters run late content instead of
625
+ * leaking or dropping it (codex falls through to a new turn; claude's streaming input
626
+ * absorbs it as background generation), so redispatch it as a fire-and-forget turn. */
627
+ redispatchOrphanedSteering() {
628
+ if (this.closing || this.disposed || this.busy)
629
+ return;
630
+ let texts;
631
+ let images;
632
+ try {
633
+ if (this.pi.pendingMessageCount === 0) {
634
+ this.pendingSteerImages.clear();
635
+ return;
636
+ }
637
+ const cleared = this.pi.clearQueue();
638
+ texts = [...cleared.steering, ...cleared.followUp].filter((text) => text.length > 0);
639
+ images = cleared.steering.flatMap((text) => this.pendingSteerImages.get(text) ?? []);
640
+ }
641
+ catch (error) {
642
+ console.error("pi-acp orphaned steering recovery error:", error);
643
+ return;
644
+ }
645
+ this.pendingSteerImages.clear();
646
+ if (texts.length === 0)
647
+ return;
648
+ try {
649
+ const done = this.startTurn({ text: texts.join("\n\n"), images }, {});
650
+ done.catch((error) => console.error("pi-acp orphaned steering turn failed:", error));
651
+ }
652
+ catch (error) {
653
+ console.error("pi-acp orphaned steering turn failed:", error);
654
+ }
655
+ }
482
656
  cancel() {
483
657
  if (this.activeTurn)
484
658
  this.abortTurn(this.activeTurn);
@@ -0,0 +1,16 @@
1
+ import { type ContentBlock, type PromptRequest } from "@agentclientprotocol/sdk";
2
+ export declare const SESSION_STEERING_METHOD: "_session/steering";
3
+ export type SteeringOutcome = "injected" | "startedNewTurn" | "failed";
4
+ export interface SteeringRequest {
5
+ sessionId: string;
6
+ prompt: ContentBlock[];
7
+ _meta?: PromptRequest["_meta"];
8
+ }
9
+ export interface SteeringResponse {
10
+ outcome: SteeringOutcome;
11
+ }
12
+ /** Runtime parser used by the ACP SDK's custom-request overload. */
13
+ export declare const steeringRequestParser: {
14
+ parse(value: unknown): SteeringRequest;
15
+ };
16
+ //# sourceMappingURL=steering.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"steering.d.ts","sourceRoot":"","sources":["../src/steering.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,aAAa,EACnB,MAAM,0BAA0B,CAAC;AAElC,eAAO,MAAM,uBAAuB,EAAG,mBAA4B,CAAC;AAEpE,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,gBAAgB,GAAG,QAAQ,CAAC;AAEvE,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,KAAK,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,eAAe,CAAC;CAC1B;AA2BD,oEAAoE;AACpE,eAAO,MAAM,qBAAqB;iBACnB,OAAO,GAAG,eAAe;CAgBvC,CAAC"}
@@ -0,0 +1,41 @@
1
+ import { RequestError, } from "@agentclientprotocol/sdk";
2
+ export const SESSION_STEERING_METHOD = "_session/steering";
3
+ function isRecord(value) {
4
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5
+ }
6
+ function isContentBlock(value) {
7
+ if (!isRecord(value) || typeof value.type !== "string")
8
+ return false;
9
+ switch (value.type) {
10
+ case "text":
11
+ return typeof value.text === "string";
12
+ case "image":
13
+ case "audio":
14
+ return typeof value.data === "string" && typeof value.mimeType === "string";
15
+ case "resource_link":
16
+ return typeof value.uri === "string" && typeof value.name === "string";
17
+ case "resource": {
18
+ const resource = value.resource;
19
+ return isRecord(resource)
20
+ && typeof resource.uri === "string"
21
+ && (typeof resource.text === "string" || typeof resource.blob === "string");
22
+ }
23
+ default:
24
+ return false;
25
+ }
26
+ }
27
+ /** Runtime parser used by the ACP SDK's custom-request overload. */
28
+ export const steeringRequestParser = {
29
+ parse(value) {
30
+ if (!isRecord(value)
31
+ || typeof value.sessionId !== "string"
32
+ || !Array.isArray(value.prompt)
33
+ || !value.prompt.every(isContentBlock)
34
+ || (value._meta !== undefined
35
+ && value._meta !== null
36
+ && !isRecord(value._meta))) {
37
+ throw RequestError.invalidParams(undefined, "invalid _session/steering request");
38
+ }
39
+ return value;
40
+ },
41
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automatalabs/pi-acp",
3
- "version": "0.2.8",
3
+ "version": "0.3.0",
4
4
  "license": "Apache-2.0",
5
5
  "engines": {
6
6
  "node": ">=22.19.0"