@estebanforge/pi-antigravity-bridge 1.4.9 → 1.4.10
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/CHANGELOG.md +13 -0
- package/README.md +36 -1
- package/docs/ACP-ADOPTION-PLAN.md +83 -18
- package/docs/ACP-PROTOCOL-REFERENCE.md +4 -3
- package/docs/ARCHITECTURE.md +12 -5
- package/docs/DEVELOPMENT.md +16 -0
- package/docs/PI-BRIDGE-GAPS.md +26 -5
- package/docs/TODO.md +21 -0
- package/extensions/index.ts +169 -14
- package/package.json +1 -1
- package/src/acp/driver.ts +9 -8
- package/src/approval-detect.ts +146 -0
- package/src/approval-gate.ts +208 -0
- package/src/approval-hook.ts +252 -0
- package/src/config.ts +43 -0
- package/src/driver-types.ts +9 -9
- package/src/driver.ts +6 -5
- package/src/mcp-registration.ts +127 -0
- package/src/mcp-server.ts +192 -10
- package/src/models.ts +2 -2
- package/src/provider.ts +209 -26
package/src/provider.ts
CHANGED
|
@@ -36,6 +36,8 @@ import { mapAgyToolToNative } from "./native-tools.js";
|
|
|
36
36
|
import { type AgyEffort, type AgyModelEntry } from "./models.js";
|
|
37
37
|
import { SessionStore } from "./sessions.js";
|
|
38
38
|
import { loadConfig } from "./config.js";
|
|
39
|
+
import { GATE_MARKER, mapNativeToShadow, stripMarkerFields } from "./approval-gate.js";
|
|
40
|
+
import type { ApprovalDecision, ApprovalPayload, ApprovalParkApi } from "./mcp-server.js";
|
|
39
41
|
import path from "node:path";
|
|
40
42
|
import { TurnDiffContext, createExecGitOps, formatInlineDiff, parseEditToolInput } from "./diff-render.js";
|
|
41
43
|
|
|
@@ -72,7 +74,7 @@ function extractUserPrompt(context: Context): string | null {
|
|
|
72
74
|
|
|
73
75
|
/** Image blocks of the latest user message (pi-ai ImageContent: base64 data
|
|
74
76
|
* + mimeType). The ACP engine forwards them as typed content blocks; the
|
|
75
|
-
*
|
|
77
|
+
* stream-json CLI prompt is text-only, so its driver simply ignores these. */
|
|
76
78
|
function extractImages(context: Context): Array<{ data: string; mimeType: string }> {
|
|
77
79
|
const last = context.messages[context.messages.length - 1];
|
|
78
80
|
if (!last || last.role !== "user" || typeof last.content === "string") return [];
|
|
@@ -294,8 +296,8 @@ function sessionKey(
|
|
|
294
296
|
): string {
|
|
295
297
|
const sid = (options as { sessionId?: string } | undefined)?.sessionId;
|
|
296
298
|
const base = sid && sid.length > 0 ? `sid:${sid}` : `cwd:${cwd}`;
|
|
297
|
-
// Engine-scoped keys (plan 9.4): one ACP turn must never touch the
|
|
298
|
-
// binding and vice versa. Un-suffixed keys =
|
|
299
|
+
// Engine-scoped keys (plan 9.4): one ACP turn must never touch the stream
|
|
300
|
+
// binding and vice versa. Un-suffixed keys = stream-json, byte-compatible with
|
|
299
301
|
// every store that predates the ACP engine.
|
|
300
302
|
return base + (engine === "acp" ? "@acp" : "");
|
|
301
303
|
}
|
|
@@ -312,12 +314,12 @@ export interface BlockState {
|
|
|
312
314
|
export interface StreamSimpleDeps {
|
|
313
315
|
entries: AgyModelEntry[];
|
|
314
316
|
store: SessionStore;
|
|
315
|
-
/**
|
|
317
|
+
/** Stream-json driver (the tested default engine). Turns run on the
|
|
316
318
|
* driver and bridge calls park as toolUse round-trips. Required with
|
|
317
319
|
* roundTrips. */
|
|
318
320
|
driver?: TurnDriver;
|
|
319
321
|
/** Official-server ACP engine. Opt-in via config.engine = "acp"; when
|
|
320
|
-
* absent the config switch falls back to the
|
|
322
|
+
* absent the config switch falls back to the stream driver. */
|
|
321
323
|
acpDriver?: TurnDriver;
|
|
322
324
|
roundTrips?: ToolRoundTrips;
|
|
323
325
|
/** Replay store for the display-only antigravity wrapper tool. Required
|
|
@@ -390,8 +392,19 @@ const BRIDGE_TIMEOUT_MS = 480_000;
|
|
|
390
392
|
/** Bounded memory of failed bridge parks (late-delivery tombstones). */
|
|
391
393
|
const MAX_PARK_TOMBSTONES = 64;
|
|
392
394
|
|
|
395
|
+
/** One MCP tool-result content block: text always; image blocks carry base64
|
|
396
|
+
* pixels and ride to the model on BOTH engines (ACP probe 2026-09-05;
|
|
397
|
+
* stream-json probe 2026-09-07: the CLI's MCP client delivers tool-result
|
|
398
|
+
* image content to the model). */
|
|
399
|
+
export interface BridgeContentBlock {
|
|
400
|
+
type: string;
|
|
401
|
+
text?: string;
|
|
402
|
+
data?: string;
|
|
403
|
+
mimeType?: string;
|
|
404
|
+
}
|
|
405
|
+
|
|
393
406
|
export interface BridgeCallResultShape {
|
|
394
|
-
content:
|
|
407
|
+
content: BridgeContentBlock[];
|
|
395
408
|
isError: boolean;
|
|
396
409
|
}
|
|
397
410
|
|
|
@@ -415,6 +428,11 @@ export const ESCALATE_AFTER_MS = 20_000;
|
|
|
415
428
|
/** Escalated parks carry a longer TTL: human-gated tools (commit previews,
|
|
416
429
|
* permission dialogs) legitimately block for many minutes. */
|
|
417
430
|
export const ESCALATED_TIMEOUT_MS = 1_800_000;
|
|
431
|
+
/** Human-decision budget for one parked approval (docs/TODO.md 2.5). Same
|
|
432
|
+
* envelope as the G9 park; the staged hook timeout exceeds it with margin
|
|
433
|
+
* (approval-hook.stagedTimeoutSeconds). Exported: the extension needs the
|
|
434
|
+
* same number for hooks.json staging and the hook script deadline. */
|
|
435
|
+
export const APPROVAL_PARK_MS = BRIDGE_TIMEOUT_MS;
|
|
418
436
|
|
|
419
437
|
export interface PollView {
|
|
420
438
|
state: "running" | "done" | "failed";
|
|
@@ -422,6 +440,7 @@ export interface PollView {
|
|
|
422
440
|
text?: string;
|
|
423
441
|
isError?: boolean;
|
|
424
442
|
reason?: string;
|
|
443
|
+
images?: Array<{ data: string; mimeType: string }>;
|
|
425
444
|
}
|
|
426
445
|
|
|
427
446
|
/** Escalated bridge calls. Bounded: past the cap, oldest settled entries
|
|
@@ -442,12 +461,13 @@ export class EscalationRegistry {
|
|
|
442
461
|
this.#calls.set(callId, { name, state: "running" });
|
|
443
462
|
this.#trim();
|
|
444
463
|
}
|
|
445
|
-
settleDone(callId: string, text: string, isError: boolean): void {
|
|
464
|
+
settleDone(callId: string, text: string, isError: boolean, images: Array<{ data: string; mimeType: string }> = []): void {
|
|
446
465
|
const e = this.#calls.get(callId);
|
|
447
466
|
if (!e) return;
|
|
448
467
|
e.state = "done";
|
|
449
468
|
e.text = text;
|
|
450
469
|
e.isError = isError;
|
|
470
|
+
if (images.length > 0) e.images = images;
|
|
451
471
|
this.#trim();
|
|
452
472
|
}
|
|
453
473
|
settleFailed(callId: string, reason: string): void {
|
|
@@ -503,15 +523,27 @@ export function formatPollAnswer(callId: string, view: PollView | undefined): Br
|
|
|
503
523
|
isError: true,
|
|
504
524
|
};
|
|
505
525
|
}
|
|
506
|
-
return {
|
|
526
|
+
return {
|
|
527
|
+
content: [
|
|
528
|
+
...(view.images ?? []).map((i) => ({ type: "image", data: i.data, mimeType: i.mimeType })),
|
|
529
|
+
{ type: "text", text: view.text || "(no output)" },
|
|
530
|
+
],
|
|
531
|
+
isError: view.isError ?? false,
|
|
532
|
+
};
|
|
507
533
|
}
|
|
508
534
|
|
|
509
535
|
interface PendingRoundTrip {
|
|
510
536
|
/** "bridge": parked MCP HTTP call; resolve() completes it.
|
|
511
537
|
* "rt": native re-exec / wrapper round-trip; pi already executed, the
|
|
512
|
-
* toolResult only confirms continuation, nothing remote to settle.
|
|
513
|
-
|
|
538
|
+
* toolResult only confirms continuation, nothing remote to settle.
|
|
539
|
+
* "approval": parked approval gate decision; resolve() maps the shadow
|
|
540
|
+
* tool result to allow/deny and completes the /approval ticket. */
|
|
541
|
+
kind: "bridge" | "rt" | "approval";
|
|
514
542
|
name: string;
|
|
543
|
+
/** Approval entries only: the agy native tool this decision is for. */
|
|
544
|
+
nativeName?: string;
|
|
545
|
+
/** Approval entries only: park start, for the latency audit field. */
|
|
546
|
+
started?: number;
|
|
515
547
|
resolve?: (r: BridgeCallResultShape | BridgeEscalation) => void;
|
|
516
548
|
reject?: (e: Error) => void;
|
|
517
549
|
timer?: NodeJS.Timeout;
|
|
@@ -556,13 +588,17 @@ export class ToolRoundTrips {
|
|
|
556
588
|
#escalations = new EscalationRegistry();
|
|
557
589
|
#escalateAfterMs: number;
|
|
558
590
|
#getDriver: () => TurnDriver;
|
|
559
|
-
#log: (s: string, d?: unknown) => void;
|
|
591
|
+
#log: (s: string, d?: unknown, level?: "debug" | "info" | "warn" | "error") => void;
|
|
592
|
+
/** Approval park controls (mcp-server handle). Assigned by the extension
|
|
593
|
+
* only after at least one shadow tool is registered, so an approval
|
|
594
|
+
* toolUse can never dispatch to the REAL builtin and execute locally. */
|
|
595
|
+
#approvalPark?: ApprovalParkApi;
|
|
560
596
|
|
|
561
597
|
/** Accepts a driver or a getter: with two engines wired, the ACTIVE driver
|
|
562
598
|
* is resolved at call time from config (plan §9.5). */
|
|
563
599
|
constructor(
|
|
564
600
|
driver: TurnDriver | (() => TurnDriver),
|
|
565
|
-
log?: (s: string, d?: unknown) => void,
|
|
601
|
+
log?: (s: string, d?: unknown, level?: "debug" | "info" | "warn" | "error") => void,
|
|
566
602
|
opts: { escalateAfterMs?: number } = {},
|
|
567
603
|
) {
|
|
568
604
|
this.#getDriver = typeof driver === "function" ? driver : () => driver;
|
|
@@ -593,6 +629,79 @@ export class ToolRoundTrips {
|
|
|
593
629
|
return this.#escalations.poll(callId);
|
|
594
630
|
}
|
|
595
631
|
|
|
632
|
+
/** Wire the approval park (mcp-server handle.approvals). The extension
|
|
633
|
+
* assigns this AFTER the shadow tools are registered; see onApproval. */
|
|
634
|
+
set approvalPark(api: ApprovalParkApi | undefined) {
|
|
635
|
+
this.#approvalPark = api;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/** Approval gate (docs/TODO.md 2.5): a PreToolUse hook parked a native agy
|
|
639
|
+
* tool call. Interrupt the pi-side view of the still-running agy turn
|
|
640
|
+
* with a toolUse for the SHADOW tool (same bridge_call mechanism as G9,
|
|
641
|
+
* so both drivers pause their turn timers); pi's permission extensions
|
|
642
|
+
* gate it, the shadow execute() consults the fallback policy, and the
|
|
643
|
+
* arriving toolResult maps to the terminal decision (resolve()).
|
|
644
|
+
* Every failure path denies fail-closed: an approval must never be
|
|
645
|
+
* granted by accident. */
|
|
646
|
+
onApproval(ticket: string, payload: ApprovalPayload): void {
|
|
647
|
+
const deny = (reason: string): void => {
|
|
648
|
+
this.#log("approval-denied-pre-park", { ticket, reason });
|
|
649
|
+
this.#approvalPark?.resolve(ticket, { allow: false, reason });
|
|
650
|
+
};
|
|
651
|
+
if (!this.#approvalPark) return deny("shadow tools are not registered");
|
|
652
|
+
const native = payload?.toolCall?.name;
|
|
653
|
+
if (typeof native !== "string" || native.length === 0) return deny("approval payload has no tool name");
|
|
654
|
+
const handle = this.#getDriver().activeHandle;
|
|
655
|
+
if (!handle) return deny("no active antigravity turn");
|
|
656
|
+
const args = (payload.toolCall.args && typeof payload.toolCall.args === "object"
|
|
657
|
+
? payload.toolCall.args
|
|
658
|
+
: {}) as Record<string, unknown>;
|
|
659
|
+
const mapped = mapNativeToShadow(native, args);
|
|
660
|
+
if (!mapped) return deny(`tool ${native} is not in the approval matcher set`);
|
|
661
|
+
const entry: PendingRoundTrip = {
|
|
662
|
+
kind: "approval",
|
|
663
|
+
name: mapped.shadow,
|
|
664
|
+
nativeName: native,
|
|
665
|
+
started: Date.now(),
|
|
666
|
+
timer: setTimeout(() => {
|
|
667
|
+
this.#failApproval(
|
|
668
|
+
ticket,
|
|
669
|
+
`approval gate timed out after ${Math.round(APPROVAL_PARK_MS / 1000)}s`,
|
|
670
|
+
"timeout",
|
|
671
|
+
);
|
|
672
|
+
}, APPROVAL_PARK_MS),
|
|
673
|
+
};
|
|
674
|
+
this.#pending.set(ticket, entry);
|
|
675
|
+
handle.pushExternal({
|
|
676
|
+
type: "bridge_call",
|
|
677
|
+
callId: ticket,
|
|
678
|
+
name: mapped.shadow,
|
|
679
|
+
args: {
|
|
680
|
+
...mapped.input,
|
|
681
|
+
[GATE_MARKER]: true,
|
|
682
|
+
__agyTicket: ticket,
|
|
683
|
+
__agyTool: native,
|
|
684
|
+
},
|
|
685
|
+
});
|
|
686
|
+
this.#log("approval-parked", { ticket, native, shadow: mapped.shadow });
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/** Settle a parked approval with a deny. Used by the park timeout and
|
|
690
|
+
* failAll; the ticket is answered (fail closed) and the pending entry
|
|
691
|
+
* dropped, so the late shadow tool result logs as approval-late. */
|
|
692
|
+
#failApproval(ticket: string, reason: string, cause: "timeout" | "shutdown"): void {
|
|
693
|
+
const entry = this.#pending.get(ticket);
|
|
694
|
+
this.#pending.delete(ticket);
|
|
695
|
+
if (entry?.timer) clearTimeout(entry.timer);
|
|
696
|
+
const resolved = this.#approvalPark?.resolve(ticket, { allow: false, reason }) ?? false;
|
|
697
|
+
this.#log(
|
|
698
|
+
resolved ? `approval-${cause}` : "approval-late",
|
|
699
|
+
{ ticket, native: entry?.nativeName, shadow: entry?.name, reason },
|
|
700
|
+
resolved && cause === "timeout" ? "warn" : "debug",
|
|
701
|
+
);
|
|
702
|
+
this.#getDriver().kickIdle();
|
|
703
|
+
}
|
|
704
|
+
|
|
596
705
|
/** Fail all pending calls (driver recycle/shutdown path). Escalated bridge
|
|
597
706
|
* calls are skipped: their HTTP request was already answered with a poll
|
|
598
707
|
* handle, and the agy turn ending does NOT make the still-running pi tool
|
|
@@ -601,6 +710,10 @@ export class ToolRoundTrips {
|
|
|
601
710
|
failAll(reason: string): void {
|
|
602
711
|
for (const id of [...this.#pending.keys()]) {
|
|
603
712
|
const entry = this.#pending.get(id);
|
|
713
|
+
if (entry?.kind === "approval") {
|
|
714
|
+
this.#failApproval(id, reason, "shutdown");
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
604
717
|
if (entry?.kind === "bridge" && entry.escalated) continue;
|
|
605
718
|
this.#fail(id, reason);
|
|
606
719
|
}
|
|
@@ -677,7 +790,10 @@ export class ToolRoundTrips {
|
|
|
677
790
|
}, this.#escalateAfterMs);
|
|
678
791
|
}
|
|
679
792
|
this.#pending.set(callId, entry);
|
|
680
|
-
|
|
793
|
+
// Strip the internal marker fields from model-supplied args (peer
|
|
794
|
+
// review 2026-09-07): a real bridge call must never arrive at the
|
|
795
|
+
// shadow's gate branch with a forged __agyGate/__agyTicket.
|
|
796
|
+
handle.pushExternal({ type: "bridge_call", callId, name, args: stripMarkerFields(args) });
|
|
681
797
|
});
|
|
682
798
|
};
|
|
683
799
|
|
|
@@ -688,8 +804,15 @@ export class ToolRoundTrips {
|
|
|
688
804
|
}
|
|
689
805
|
|
|
690
806
|
/** Complete a parked call from a pi toolResult message. Returns false when
|
|
691
|
-
* the id matches nothing pending.
|
|
692
|
-
|
|
807
|
+
* the id matches nothing pending. Image blocks ride the result on both
|
|
808
|
+
* engines (probe-verified on each); the late-delivery prompt stays
|
|
809
|
+
* text-only (see PI-BRIDGE-GAPS). */
|
|
810
|
+
resolve(
|
|
811
|
+
toolCallId: string,
|
|
812
|
+
text: string,
|
|
813
|
+
isError: boolean,
|
|
814
|
+
images: Array<{ data: string; mimeType: string }> = [],
|
|
815
|
+
): boolean {
|
|
693
816
|
const entry = this.#pending.get(toolCallId);
|
|
694
817
|
if (!entry) return false;
|
|
695
818
|
this.#pending.delete(toolCallId);
|
|
@@ -700,14 +823,47 @@ export class ToolRoundTrips {
|
|
|
700
823
|
this.#log("round-trip-rt-done", { callId: toolCallId, name: entry.name, isError });
|
|
701
824
|
return true;
|
|
702
825
|
}
|
|
826
|
+
if (entry.kind === "approval") {
|
|
827
|
+
clearTimeout(entry.timer);
|
|
828
|
+
this.#pending.delete(toolCallId);
|
|
829
|
+
// Decision mapping (docs/TODO.md 2.5): block/error -> deny with the
|
|
830
|
+
// text (pi turns a tool_call block into an error tool result, so both
|
|
831
|
+
// paths land here); synthetic success -> allow.
|
|
832
|
+
const decision: ApprovalDecision = isError
|
|
833
|
+
? { allow: false, reason: text || `blocked by approval gate (${entry.nativeName})` }
|
|
834
|
+
: { allow: true };
|
|
835
|
+
const delivered = this.#approvalPark?.resolve(toolCallId, decision) ?? false;
|
|
836
|
+
// Audit trail (docs/TODO.md 2.7): decision, source, latency.
|
|
837
|
+
this.#log(
|
|
838
|
+
delivered ? "approval-decision" : "approval-late",
|
|
839
|
+
{
|
|
840
|
+
ticket: toolCallId,
|
|
841
|
+
native: entry.nativeName,
|
|
842
|
+
shadow: entry.name,
|
|
843
|
+
decision: decision.allow ? "allow" : "deny",
|
|
844
|
+
source: isError ? "extension-block" : "policy",
|
|
845
|
+
reason: decision.allow ? undefined : decision.reason,
|
|
846
|
+
latencyMs: Date.now() - (entry.started ?? 0),
|
|
847
|
+
},
|
|
848
|
+
delivered ? "info" : "debug",
|
|
849
|
+
);
|
|
850
|
+
this.#getDriver().kickIdle();
|
|
851
|
+
return true;
|
|
852
|
+
}
|
|
703
853
|
// Escalated call: the HTTP response already carried the poll handle, so
|
|
704
854
|
// the result lands in the registry for the next bridge_poll_result. The
|
|
705
855
|
// original promise settled with the sentinel; re-resolving is a silent
|
|
706
856
|
// no-op, so gate it to keep that explicit.
|
|
707
857
|
if (entry.escalated) {
|
|
708
|
-
this.#escalations.settleDone(toolCallId, text, isError);
|
|
858
|
+
this.#escalations.settleDone(toolCallId, text, isError, images);
|
|
709
859
|
} else {
|
|
710
|
-
entry.resolve!({
|
|
860
|
+
entry.resolve!({
|
|
861
|
+
content: [
|
|
862
|
+
...images.map((i) => ({ type: "image", data: i.data, mimeType: i.mimeType })),
|
|
863
|
+
{ type: "text", text },
|
|
864
|
+
],
|
|
865
|
+
isError,
|
|
866
|
+
});
|
|
711
867
|
}
|
|
712
868
|
this.#getDriver().kickIdle();
|
|
713
869
|
this.#log("round-trip-resolved", { callId: toolCallId, name: entry.name, isError });
|
|
@@ -715,19 +871,40 @@ export class ToolRoundTrips {
|
|
|
715
871
|
}
|
|
716
872
|
}
|
|
717
873
|
|
|
718
|
-
/**
|
|
874
|
+
/** Image blocks of a tool result (pi's read on an image file, screenshots).
|
|
875
|
+
* Forwarded to agy as MCP image content (see BridgeContentBlock) on both
|
|
876
|
+
* engines. Size relies on pi's own inline-image resize cap upstream; no
|
|
877
|
+
* second cap here. */
|
|
878
|
+
function extractResultImages(content: unknown): Array<{ data: string; mimeType: string }> {
|
|
879
|
+
if (!Array.isArray(content)) return [];
|
|
880
|
+
return content
|
|
881
|
+
.filter(
|
|
882
|
+
(b): b is { type: "image"; data: string; mimeType: string } =>
|
|
883
|
+
typeof b === "object" && b !== null && (b as { type?: string }).type === "image",
|
|
884
|
+
)
|
|
885
|
+
.map((b) => ({ data: b.data, mimeType: b.mimeType }))
|
|
886
|
+
.filter((i) => typeof i.mimeType === "string" && typeof i.data === "string" && i.data.length > 0);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/** Extract toolResult messages whose toolCallId is still parked, as text plus
|
|
890
|
+
* any image blocks (forwarded as MCP image content on both engines). */
|
|
719
891
|
export function collectToolResults(
|
|
720
892
|
messages: Message[],
|
|
721
893
|
pendingIds: readonly string[],
|
|
722
|
-
): Array<{ toolCallId: string; text: string; isError: boolean }> {
|
|
894
|
+
): Array<{ toolCallId: string; text: string; isError: boolean; images: Array<{ data: string; mimeType: string }> }> {
|
|
723
895
|
if (pendingIds.length === 0) return [];
|
|
724
896
|
const pending = new Set(pendingIds);
|
|
725
|
-
const out: Array<{ toolCallId: string; text: string; isError: boolean }> = [];
|
|
897
|
+
const out: Array<{ toolCallId: string; text: string; isError: boolean; images: Array<{ data: string; mimeType: string }> }> = [];
|
|
726
898
|
for (const m of messages) {
|
|
727
899
|
if (m.role !== "toolResult") continue;
|
|
728
900
|
const id = (m as { toolCallId?: string }).toolCallId;
|
|
729
901
|
if (!id || !pending.has(id)) continue;
|
|
730
|
-
out.push({
|
|
902
|
+
out.push({
|
|
903
|
+
toolCallId: id,
|
|
904
|
+
text: blocksToText(m.content).trim(),
|
|
905
|
+
isError: m.isError === true,
|
|
906
|
+
images: extractResultImages(m.content),
|
|
907
|
+
});
|
|
731
908
|
}
|
|
732
909
|
return out;
|
|
733
910
|
}
|
|
@@ -825,7 +1002,7 @@ export function consumeActivity(
|
|
|
825
1002
|
appendText(stream, blocks, activity.delta);
|
|
826
1003
|
return "continue";
|
|
827
1004
|
case "thought":
|
|
828
|
-
//
|
|
1005
|
+
// Stream-json: token count only (no body). ACP: thought TEXT deltas —
|
|
829
1006
|
// rendered through the same thinking block pipeline (9.2).
|
|
830
1007
|
if (typeof activity.delta === "string" && activity.delta.length > 0) {
|
|
831
1008
|
appendThinking(stream, blocks, activity.delta);
|
|
@@ -965,7 +1142,13 @@ async function runTurnDriver(
|
|
|
965
1142
|
const escalatedNames = results
|
|
966
1143
|
.map((r) => deps.roundTrips.poll(r.toolCallId)?.name)
|
|
967
1144
|
.filter((n): n is string => Boolean(n));
|
|
968
|
-
|
|
1145
|
+
// Images ride tool results on BOTH engines (ACP probe 2026-09-05;
|
|
1146
|
+
// stream-json probe 2026-09-07: the CLI's MCP client delivers tool-result
|
|
1147
|
+
// image content to the model — two-tone PNG named from the result alone,
|
|
1148
|
+
// no decoders in the frame trail). The late-delivery prompt and the
|
|
1149
|
+
// stream-json prompt attachments stay text-only by design.
|
|
1150
|
+
for (const r of results)
|
|
1151
|
+
deps.roundTrips.resolve(r.toolCallId, r.text, r.isError, r.images);
|
|
969
1152
|
|
|
970
1153
|
// Late delivery: a toolResult whose park already failed (the abort/timeout
|
|
971
1154
|
// path failed the park while the pi tool kept running). The work is done,
|
|
@@ -1109,7 +1292,7 @@ async function runTurnDriver(
|
|
|
1109
1292
|
|
|
1110
1293
|
/** Build the streamSimple closure. Captures the model catalog + session store
|
|
1111
1294
|
* resolved at extension load. When a driver is provided, turns run on the
|
|
1112
|
-
* persistent stream-json engine (config.engine selects;
|
|
1295
|
+
* persistent stream-json engine (config.engine selects; stream remains as
|
|
1113
1296
|
* fallback). */
|
|
1114
1297
|
export function createStreamSimple(
|
|
1115
1298
|
deps: StreamSimpleDeps,
|
|
@@ -1141,8 +1324,8 @@ export function createStreamSimple(
|
|
|
1141
1324
|
replay: deps.replay,
|
|
1142
1325
|
nativeActive: deps.nativeActive,
|
|
1143
1326
|
// Record the engine of the driver that will ACTUALLY run: if the
|
|
1144
|
-
// ACP driver is absent, the config switch falls back to
|
|
1145
|
-
// and keying the session as @acp would store a
|
|
1327
|
+
// ACP driver is absent, the config switch falls back to stream,
|
|
1328
|
+
// and keying the session as @acp would store a stream
|
|
1146
1329
|
// conversationId under the wrong engine scope.
|
|
1147
1330
|
engine: selected === deps.acpDriver ? "acp" : "stream-json",
|
|
1148
1331
|
log: deps.log,
|