@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 +18 -1
- package/dist/agent.d.ts +7 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +12 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -0
- package/dist/session.d.ts +32 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +183 -9
- package/dist/steering.d.ts +16 -0
- package/dist/steering.d.ts.map +1 -0
- package/dist/steering.js +41 -0
- package/package.json +1 -1
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.
|
|
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;
|
package/dist/agent.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
}
|
package/dist/server.d.ts.map
CHANGED
|
@@ -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;
|
|
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>;
|
package/dist/session.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAClG,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAExD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAc3C,OAAO,KAAK,EAAE,SAAS,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAuB,KAAK,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAClG,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAExD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAc3C,OAAO,KAAK,EAAE,SAAS,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAuB,KAAK,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AACjG,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AA4BvE,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,YAAY,CAAC;IACtB,OAAO,EAAE,cAAc,CAAC;IACxB,MAAM,EAAE,YAAY,CAAC;IACrB,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,EAAE,SAAS,CAAC;IACrB,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IACnD,eAAe,EAAE,SAAS,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;IACvC,aAAa,EAAE,wBAAwB,CAAC;IACxC,mBAAmB,EAAE,eAAe,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,oBAAoB,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/F;AAED,qBAAa,SAAS;IACpB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,EAAE,YAAY,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAY;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAmC;IACpE,OAAO,CAAC,eAAe,CAAwB;IAC/C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA2B;IACzD,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAkB;IACtD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA+B;IACxD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuB;IAC/C,OAAO,CAAC,IAAI,CAA4B;IACxC,OAAO,CAAC,WAAW,CAAU;IAC7B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,WAAW,CAA2B;IAC9C,OAAO,CAAC,UAAU,CAAyB;IAC3C,OAAO,CAAC,UAAU,CAAoC;IACtD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAgC;IACnE,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,iBAAiB,CAAgC;IACzD,OAAO,CAAC,sBAAsB,CAA4B;IAC1D,OAAO,CAAC,kBAAkB,CAA4B;gBAE1C,OAAO,EAAE,gBAAgB;IAuCrC,IAAI,IAAI,IAAI,OAAO,CAElB;IAED,aAAa;IAIb,sBAAsB,CAAC,MAAM,EAAE,SAAS,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,IAAI;IAI3D,gBAAgB,IAAI,WAAW,GAAG,SAAS;IAE3C,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IASrC,OAAO,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI;IAMpC,OAAO,CAAC,SAAS;IAwBX,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAMtB,MAAM,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAWvD,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;IAkBzD,OAAO,CAAC,MAAM;IAmBd,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,SAAS;IASjB,OAAO,CAAC,aAAa;IAcrB,OAAO,CAAC,WAAW;IA8GnB,OAAO,CAAC,SAAS;IAiBjB,OAAO,CAAC,WAAW;YAOL,aAAa;YAQb,gBAAgB;YAoBhB,gBAAgB;IA0BxB,MAAM,CAAC,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,GAAG,OAAO,CAAC,cAAc,CAAC;IAKxF;;;;6EAIyE;IACzE,OAAO,CAAC,SAAS;IAmFjB;;;0EAGsE;IAChE,KAAK,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;YAUjD,YAAY;IAmC1B;;;+DAG2D;IAC3D,OAAO,CAAC,YAAY;IAoBpB;;;;2FAIuF;YACzE,kBAAkB;IAkChC;;;;4FAIwF;IACxF,OAAO,CAAC,0BAA0B;IA0BlC,MAAM,IAAI,IAAI;IAId,mBAAmB,IAAI,IAAI;IASrB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAgBxB,0BAA0B,IAAI,OAAO,CAAC,IAAI,CAAC;IAKjD,IAAI,iBAAiB,IAAI,MAAM,CAAiD;IAEhF,IAAI,oBAAoB,IAAI,OAAO,CAElC;IAEK,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAgCvC,MAAM,IAAI,IAAI;CAOf"}
|
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
|
-
|
|
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
|
-
|
|
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
|
|
443
|
-
if (requestSignal
|
|
444
|
-
abortFromRequest();
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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, {
|
|
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"}
|
package/dist/steering.js
ADDED
|
@@ -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
|
+
};
|