@gajae-code/coding-agent 0.13.0 → 0.13.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.
- package/CHANGELOG.md +18 -0
- package/dist/types/config/file-lock.d.ts +2 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +17 -5
- package/dist/types/config/settings.d.ts +10 -1
- package/dist/types/edit/streaming.d.ts +5 -5
- package/dist/types/eval/py/kernel.d.ts +2 -2
- package/dist/types/eval/py/runtime.d.ts +5 -1
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +3 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +3 -1
- package/dist/types/modes/components/model-selector.d.ts +3 -2
- package/dist/types/runtime/process-lifecycle.d.ts +12 -31
- package/dist/types/sdk/broker/identity.d.ts +2 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +17 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +11 -6
- package/dist/types/sdk/broker/startup-failure.d.ts +32 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +10 -1
- package/dist/types/sdk/bus/reconciliation-store.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -2
- package/dist/types/sdk/bus/telegram-daemon.d.ts +17 -1
- package/dist/types/sdk/host/query/cursor.d.ts +4 -0
- package/dist/types/sdk/host/query/handlers.d.ts +6 -0
- package/dist/types/sdk/host/session-runtime.d.ts +7 -6
- package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/turn-result.d.ts +37 -0
- package/dist/types/session/agent-session.d.ts +8 -11
- package/dist/types/session/blob-store.d.ts +6 -3
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +12 -3
- package/dist/types/task/executor.d.ts +4 -7
- package/dist/types/tools/index.d.ts +1 -1
- package/dist/types/tools/read.d.ts +1 -1
- package/dist/types/tools/search.d.ts +1 -1
- package/dist/types/utils/edit-mode.d.ts +69 -0
- package/package.json +8 -8
- package/scripts/generate-hotkeys-docs.ts +8 -6
- package/src/commands/sdk.ts +23 -2
- package/src/config/file-lock.ts +25 -5
- package/src/config/keybindings.ts +4 -2
- package/src/config/model-registry.ts +20 -0
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +29 -22
- package/src/config/settings.ts +42 -10
- package/src/coordinator-mcp/server.ts +84 -106
- package/src/edit/index.ts +5 -19
- package/src/edit/renderer.ts +3 -2
- package/src/edit/streaming.ts +28 -15
- package/src/eval/py/executor.ts +108 -10
- package/src/eval/py/kernel.ts +86 -12
- package/src/eval/py/runtime.ts +200 -16
- package/src/exec/bash-executor.ts +36 -37
- package/src/gjc-runtime/gc-runtime.ts +25 -3
- package/src/gjc-runtime/managed-owner-supervisor.ts +27 -1
- package/src/gjc-runtime/session-state-sidecar.ts +110 -4
- package/src/gjc-runtime/tmux-sessions.ts +34 -5
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/modes/acp/acp-agent.ts +10 -2
- package/src/modes/components/model-selector.ts +28 -8
- package/src/modes/controllers/plan-mode-controller.ts +3 -3
- package/src/modes/controllers/selector-controller.ts +4 -2
- package/src/runtime/process-lifecycle.ts +126 -27
- package/src/runtime-mcp/transports/stdio.ts +11 -6
- package/src/sdk/acp/adapter.ts +18 -10
- package/src/sdk/broker/broker.ts +55 -4
- package/src/sdk/broker/ensure.ts +10 -7
- package/src/sdk/broker/identity.ts +10 -0
- package/src/sdk/broker/lifecycle-ledger.ts +56 -6
- package/src/sdk/broker/lifecycle.ts +53 -10
- package/src/sdk/broker/session-index.ts +28 -11
- package/src/sdk/broker/startup-failure.ts +101 -0
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/chat-daemon-runtime.ts +8 -1
- package/src/sdk/bus/conversation-store.ts +171 -50
- package/src/sdk/bus/index.ts +101 -47
- package/src/sdk/bus/kind-aware-reconciliation.ts +52 -2
- package/src/sdk/bus/notification-service.ts +3 -3
- package/src/sdk/bus/reconciliation-store.ts +16 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +11 -2
- package/src/sdk/bus/telegram-daemon.ts +131 -29
- package/src/sdk/bus/topic-registry.ts +38 -9
- package/src/sdk/cli/session-cli.ts +20 -7
- package/src/sdk/client/discovery.ts +23 -2
- package/src/sdk/host/query/cursor.ts +4 -0
- package/src/sdk/host/query/handlers.ts +99 -100
- package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
- package/src/sdk/host/session-runtime.test.ts +98 -1
- package/src/sdk/host/session-runtime.ts +57 -15
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +192 -2
- package/src/sdk/host/websocket-transport.ts +54 -30
- package/src/sdk/index.ts +1 -0
- package/src/sdk/mcp/server.ts +19 -6
- package/src/sdk/protocol/operation-inventory.generated.json +1 -1
- package/src/sdk/protocol/operation-registry.ts +8 -12
- package/src/sdk/session.ts +5 -14
- package/src/sdk/turn-result.ts +53 -0
- package/src/session/agent-session.ts +112 -69
- package/src/session/blob-store.ts +91 -21
- package/src/session/session-manager.ts +408 -81
- package/src/session/session-storage.ts +68 -10
- package/src/slash-commands/helpers/fast-status-report.ts +24 -8
- package/src/task/executor.ts +25 -8
- package/src/task/index.ts +0 -2
- package/src/tools/index.ts +1 -1
- package/src/tools/read.ts +12 -11
- package/src/tools/search.ts +7 -7
- package/src/utils/edit-mode.ts +180 -9
- package/src/web/search/providers/insane.ts +55 -1
|
@@ -55,6 +55,12 @@ export interface SessionSurface {
|
|
|
55
55
|
turnId?: string;
|
|
56
56
|
clientRef?: string;
|
|
57
57
|
}): unknown | Promise<unknown>;
|
|
58
|
+
getTurnResult?(selector: {
|
|
59
|
+
kind: "prompt" | "skill";
|
|
60
|
+
clientRef?: string;
|
|
61
|
+
commandId?: string;
|
|
62
|
+
turnId?: string;
|
|
63
|
+
}): unknown | Promise<unknown>;
|
|
58
64
|
/** Q27 effective model-profile catalog from the live session registry. */
|
|
59
65
|
getModelProfiles?(): unknown[] | Promise<unknown[]>;
|
|
60
66
|
/**
|
|
@@ -167,7 +173,7 @@ const names = [
|
|
|
167
173
|
"resource.body",
|
|
168
174
|
"artifact.read",
|
|
169
175
|
"runtime.jobs.list",
|
|
170
|
-
"turn.
|
|
176
|
+
"turn.result",
|
|
171
177
|
"models.profiles.list",
|
|
172
178
|
"skill.invoke_status",
|
|
173
179
|
"providers.list/active",
|
|
@@ -183,11 +189,35 @@ export class QueryHandlers {
|
|
|
183
189
|
) {}
|
|
184
190
|
async dispatch(request: QueryRequest): Promise<QueryResponse> {
|
|
185
191
|
try {
|
|
192
|
+
if (request.query === "turn.result") {
|
|
193
|
+
if (this.surface.installedQueries instanceof Set && !this.surface.installedQueries.has("turn.result"))
|
|
194
|
+
return this.#error(
|
|
195
|
+
request,
|
|
196
|
+
"operation_not_session_owned",
|
|
197
|
+
false,
|
|
198
|
+
"turn.result is not installed for this session.",
|
|
199
|
+
);
|
|
200
|
+
return await this.#turnResult(request);
|
|
201
|
+
}
|
|
202
|
+
if (request.query === "turn.prompt_status") return await this.#promptTurnResult(request);
|
|
203
|
+
if (request.query === "skill.invoke_status") return await this.#skillTurnResult(request);
|
|
204
|
+
|
|
186
205
|
const query = request.query.startsWith("Q")
|
|
187
206
|
? request.query
|
|
188
207
|
: request.query === "models.list" || request.query === "models.current"
|
|
189
208
|
? "Q10"
|
|
190
209
|
: `Q${String(names.indexOf(request.query) + 1).padStart(2, "0")}`;
|
|
210
|
+
if (query === "Q26") {
|
|
211
|
+
if (this.surface.installedQueries instanceof Set && !this.surface.installedQueries.has("turn.result"))
|
|
212
|
+
return this.#error(
|
|
213
|
+
request,
|
|
214
|
+
"operation_not_session_owned",
|
|
215
|
+
false,
|
|
216
|
+
"turn.result is not installed for this session.",
|
|
217
|
+
);
|
|
218
|
+
return await this.#turnResult(request);
|
|
219
|
+
}
|
|
220
|
+
if (query === "Q28") return await this.#skillTurnResult(request);
|
|
191
221
|
if (
|
|
192
222
|
this.surface.installedQueries instanceof Set &&
|
|
193
223
|
!this.surface.installedQueries.has(names[Number(query.slice(1)) - 1] ?? "")
|
|
@@ -220,8 +250,6 @@ export class QueryHandlers {
|
|
|
220
250
|
}
|
|
221
251
|
if (query === "Q02") return await this.#transcriptBody(request);
|
|
222
252
|
if (query === "Q23") return await this.#resourceBody(request);
|
|
223
|
-
if (query === "Q26") return await this.#promptStatus(request);
|
|
224
|
-
if (query === "Q28") return await this.#skillInvokeStatus(request);
|
|
225
253
|
if (query === "Q24") return await this.#artifact(request);
|
|
226
254
|
if (query === "Q30") return await this.#checkpoint(request);
|
|
227
255
|
if (query === "Q27" && request.input && Object.keys(request.input).length > 0)
|
|
@@ -532,6 +560,35 @@ export class QueryHandlers {
|
|
|
532
560
|
if (!range) return this.#error(request, "resource_gone");
|
|
533
561
|
return this.#chunkRange(request, "transcript", "default", revision, range, selector, { entryId, highWatermark });
|
|
534
562
|
}
|
|
563
|
+
async #turnResult(request: QueryRequest): Promise<QueryResponse> {
|
|
564
|
+
const input = request.input ?? {};
|
|
565
|
+
const keys = Object.keys(input);
|
|
566
|
+
const kind = input.kind;
|
|
567
|
+
const rawClientRef = typeof input.clientRef === "string" ? input.clientRef : undefined;
|
|
568
|
+
const clientRef = rawClientRef?.trim();
|
|
569
|
+
const hasClient = clientRef !== undefined;
|
|
570
|
+
const hasPair = typeof input.commandId === "string" && typeof input.turnId === "string";
|
|
571
|
+
if (
|
|
572
|
+
(kind !== "prompt" && kind !== "skill") ||
|
|
573
|
+
hasClient === hasPair ||
|
|
574
|
+
keys.some(k => !["kind", "clientRef", "commandId", "turnId"].includes(k)) ||
|
|
575
|
+
(hasClient && (!clientRef || clientRef.length > PROMPT_CLIENT_REF_MAX_LENGTH || keys.length !== 2)) ||
|
|
576
|
+
(hasPair && (!input.commandId || !input.turnId || keys.length !== 3))
|
|
577
|
+
)
|
|
578
|
+
return this.#error(request, "invalid_request", false, "turn.result requires kind and exactly one selector");
|
|
579
|
+
// Turn results carry bounded content (≤ TURN_RESULT_CONTENT_MAX_BYTES, 16 KiB)
|
|
580
|
+
// and fit inline within a single query response. Cursored continuation is
|
|
581
|
+
// unreachable: the bounded content cap is well under both RESPONSE_CEILING_BYTES
|
|
582
|
+
// and TARGET_PAGE_BYTES, so no revision is ever created. Reject cursors
|
|
583
|
+
// upfront instead of advertising a continuation path that can never fire.
|
|
584
|
+
if (request.cursor) return this.#error(request, "invalid_cursor", false, "turn.result does not support cursors.");
|
|
585
|
+
if (typeof this.surface.getTurnResult !== "function") return this.#error(request, "unavailable");
|
|
586
|
+
const result = await this.surface.getTurnResult({
|
|
587
|
+
kind,
|
|
588
|
+
...(hasClient ? { clientRef } : { commandId: input.commandId as string, turnId: input.turnId as string }),
|
|
589
|
+
});
|
|
590
|
+
return { id: request.id, ok: true, result };
|
|
591
|
+
}
|
|
535
592
|
|
|
536
593
|
async #resourceBody(request: QueryRequest): Promise<QueryResponse> {
|
|
537
594
|
let selector = selectorFor("Q23", request.input);
|
|
@@ -566,6 +623,32 @@ export class QueryHandlers {
|
|
|
566
623
|
});
|
|
567
624
|
}
|
|
568
625
|
|
|
626
|
+
async #promptTurnResult(request: QueryRequest): Promise<QueryResponse> {
|
|
627
|
+
if (this.surface.installedQueries instanceof Set && !this.surface.installedQueries.has("turn.result"))
|
|
628
|
+
return this.#error(
|
|
629
|
+
request,
|
|
630
|
+
"operation_not_session_owned",
|
|
631
|
+
false,
|
|
632
|
+
"turn.result is not installed for this session.",
|
|
633
|
+
);
|
|
634
|
+
const input = request.input ?? {};
|
|
635
|
+
if (input.kind !== undefined && input.kind !== "prompt")
|
|
636
|
+
return this.#error(request, "invalid_request", false, "turn.prompt_status only accepts prompt selectors.");
|
|
637
|
+
return await this.#turnResult({ ...request, input: { ...input, kind: "prompt" } });
|
|
638
|
+
}
|
|
639
|
+
async #skillTurnResult(request: QueryRequest): Promise<QueryResponse> {
|
|
640
|
+
if (this.surface.installedQueries instanceof Set && !this.surface.installedQueries.has("skill.invoke_status"))
|
|
641
|
+
return this.#error(
|
|
642
|
+
request,
|
|
643
|
+
"operation_not_session_owned",
|
|
644
|
+
false,
|
|
645
|
+
"skill.invoke_status is not installed for this session.",
|
|
646
|
+
);
|
|
647
|
+
const input = request.input ?? {};
|
|
648
|
+
if (input.kind !== undefined && input.kind !== "skill")
|
|
649
|
+
return this.#error(request, "invalid_request", false, "skill.invoke_status only accepts skill selectors.");
|
|
650
|
+
return await this.#turnResult({ ...request, input: { ...input, kind: "skill" } });
|
|
651
|
+
}
|
|
569
652
|
async #artifact(request: QueryRequest): Promise<QueryResponse> {
|
|
570
653
|
const input = request.input ?? {};
|
|
571
654
|
const artifactId = String(input.artifactId ?? "");
|
|
@@ -696,102 +779,6 @@ export class QueryHandlers {
|
|
|
696
779
|
return { id: request.id, ok: true, page };
|
|
697
780
|
}
|
|
698
781
|
|
|
699
|
-
async #skillInvokeStatus(request: QueryRequest): Promise<QueryResponse> {
|
|
700
|
-
if (request.cursor)
|
|
701
|
-
return this.#error(request, "invalid_request", false, "skill.invoke_status does not support cursors.");
|
|
702
|
-
const input = request.input ?? {};
|
|
703
|
-
for (const key of Object.keys(input))
|
|
704
|
-
if (key !== "commandId" && key !== "turnId" && key !== "clientRef")
|
|
705
|
-
return this.#error(
|
|
706
|
-
request,
|
|
707
|
-
"invalid_request",
|
|
708
|
-
false,
|
|
709
|
-
`skill.invoke_status does not accept selector field "${key}".`,
|
|
710
|
-
);
|
|
711
|
-
const commandId = typeof input.commandId === "string" && input.commandId ? input.commandId : undefined;
|
|
712
|
-
const turnId = typeof input.turnId === "string" && input.turnId ? input.turnId : undefined;
|
|
713
|
-
const rawClientRef = typeof input.clientRef === "string" ? input.clientRef : undefined;
|
|
714
|
-
const trimmedClientRef = rawClientRef?.trim();
|
|
715
|
-
if (rawClientRef !== undefined && (!trimmedClientRef || trimmedClientRef.length > PROMPT_CLIENT_REF_MAX_LENGTH))
|
|
716
|
-
return this.#error(
|
|
717
|
-
request,
|
|
718
|
-
"invalid_request",
|
|
719
|
-
false,
|
|
720
|
-
"clientRef must be a non-empty string of at most 128 characters.",
|
|
721
|
-
);
|
|
722
|
-
const clientRef = trimmedClientRef || undefined;
|
|
723
|
-
if ((commandId === undefined) !== (turnId === undefined))
|
|
724
|
-
return this.#error(request, "invalid_request", false, "commandId and turnId must be provided together.");
|
|
725
|
-
if (commandId !== undefined && clientRef !== undefined)
|
|
726
|
-
return this.#error(
|
|
727
|
-
request,
|
|
728
|
-
"invalid_request",
|
|
729
|
-
false,
|
|
730
|
-
"Provide exactly one selector: a commandId/turnId pair or a clientRef.",
|
|
731
|
-
);
|
|
732
|
-
if (commandId === undefined && clientRef === undefined)
|
|
733
|
-
return this.#error(
|
|
734
|
-
request,
|
|
735
|
-
"invalid_request",
|
|
736
|
-
false,
|
|
737
|
-
"skill.invoke_status requires a commandId/turnId pair or a clientRef.",
|
|
738
|
-
);
|
|
739
|
-
if (typeof this.surface.getSkillInvokeStatus !== "function")
|
|
740
|
-
return this.#error(request, "unavailable", false, "skill.invoke_status is unavailable for this session.");
|
|
741
|
-
const result = await this.surface.getSkillInvokeStatus(
|
|
742
|
-
clientRef !== undefined ? { clientRef } : { commandId: commandId!, turnId: turnId! },
|
|
743
|
-
);
|
|
744
|
-
return { id: request.id, ok: true, result };
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
async #promptStatus(request: QueryRequest): Promise<QueryResponse> {
|
|
748
|
-
if (request.cursor)
|
|
749
|
-
return this.#error(request, "invalid_request", false, "turn.prompt_status does not support cursors.");
|
|
750
|
-
const input = request.input ?? {};
|
|
751
|
-
for (const key of Object.keys(input))
|
|
752
|
-
if (key !== "commandId" && key !== "turnId" && key !== "clientRef")
|
|
753
|
-
return this.#error(
|
|
754
|
-
request,
|
|
755
|
-
"invalid_request",
|
|
756
|
-
false,
|
|
757
|
-
`turn.prompt_status does not accept selector field "${key}".`,
|
|
758
|
-
);
|
|
759
|
-
const commandId = typeof input.commandId === "string" && input.commandId ? input.commandId : undefined;
|
|
760
|
-
const turnId = typeof input.turnId === "string" && input.turnId ? input.turnId : undefined;
|
|
761
|
-
const rawClientRef = typeof input.clientRef === "string" ? input.clientRef : undefined;
|
|
762
|
-
const trimmedClientRef = rawClientRef?.trim();
|
|
763
|
-
if (rawClientRef !== undefined && (!trimmedClientRef || trimmedClientRef.length > PROMPT_CLIENT_REF_MAX_LENGTH))
|
|
764
|
-
return this.#error(
|
|
765
|
-
request,
|
|
766
|
-
"invalid_request",
|
|
767
|
-
false,
|
|
768
|
-
"clientRef must be a non-empty string of at most 128 characters.",
|
|
769
|
-
);
|
|
770
|
-
const clientRef = trimmedClientRef || undefined;
|
|
771
|
-
if ((commandId === undefined) !== (turnId === undefined))
|
|
772
|
-
return this.#error(request, "invalid_request", false, "commandId and turnId must be provided together.");
|
|
773
|
-
if (commandId !== undefined && clientRef !== undefined)
|
|
774
|
-
return this.#error(
|
|
775
|
-
request,
|
|
776
|
-
"invalid_request",
|
|
777
|
-
false,
|
|
778
|
-
"Provide exactly one selector: a commandId/turnId pair or a clientRef.",
|
|
779
|
-
);
|
|
780
|
-
if (commandId === undefined && clientRef === undefined)
|
|
781
|
-
return this.#error(
|
|
782
|
-
request,
|
|
783
|
-
"invalid_request",
|
|
784
|
-
false,
|
|
785
|
-
"turn.prompt_status requires a commandId/turnId pair or a clientRef.",
|
|
786
|
-
);
|
|
787
|
-
if (typeof this.surface.getPromptStatus !== "function")
|
|
788
|
-
return this.#error(request, "unavailable", false, "turn.prompt_status is unavailable for this session.");
|
|
789
|
-
const result = await this.surface.getPromptStatus(
|
|
790
|
-
clientRef !== undefined ? { clientRef } : { commandId: commandId!, turnId: turnId! },
|
|
791
|
-
);
|
|
792
|
-
return { id: request.id, ok: true, result };
|
|
793
|
-
}
|
|
794
|
-
|
|
795
782
|
#error(request: QueryRequest, code: string, restartQuery = false, message = code, details?: unknown): QueryResponse {
|
|
796
783
|
return {
|
|
797
784
|
id: request.id,
|
|
@@ -802,7 +789,19 @@ export class QueryHandlers {
|
|
|
802
789
|
}
|
|
803
790
|
function selectorFor(queryId: string, input: Record<string, unknown> | undefined): CursorSelector {
|
|
804
791
|
const selector: CursorSelector = { queryId };
|
|
805
|
-
for (const key of [
|
|
792
|
+
for (const key of [
|
|
793
|
+
"entryId",
|
|
794
|
+
"field",
|
|
795
|
+
"fileId",
|
|
796
|
+
"hunkId",
|
|
797
|
+
"resourceKind",
|
|
798
|
+
"resourceId",
|
|
799
|
+
"itemId",
|
|
800
|
+
"kind",
|
|
801
|
+
"clientRef",
|
|
802
|
+
"commandId",
|
|
803
|
+
"turnId",
|
|
804
|
+
] as const)
|
|
806
805
|
if (input?.[key] !== undefined) selector[key] = String(input[key]);
|
|
807
806
|
return selector;
|
|
808
807
|
}
|
|
@@ -114,7 +114,7 @@ describe("SDK surface parity", () => {
|
|
|
114
114
|
expect([...nativePolicy.installedControls]).toEqual([...loopbackPolicy.installedControls]);
|
|
115
115
|
expect([...nativePolicy.installedQueries]).toEqual([...loopbackPolicy.installedQueries]);
|
|
116
116
|
expect(createSdkCapabilities(nativePolicy, false)).toEqual(createSdkCapabilities(loopbackPolicy, false));
|
|
117
|
-
expect(nativePolicy.installedQueries).toContain("turn.
|
|
117
|
+
expect(nativePolicy.installedQueries).toContain("turn.result");
|
|
118
118
|
expect(nativePolicy.installedQueries).toContain("skill.invoke_status");
|
|
119
119
|
expect(nativePolicy.installedQueries).toContain("session.checkpoint");
|
|
120
120
|
});
|
|
@@ -440,6 +440,7 @@ async function invocationHarness(
|
|
|
440
440
|
sendUserMessage?: (content: unknown, options?: PreflightHooks & { deliverAs?: string }) => Promise<void>;
|
|
441
441
|
invokeSkill?: (name: string, args?: string, options?: PreflightHooks) => Promise<unknown>;
|
|
442
442
|
abort?: () => void;
|
|
443
|
+
isIdle?: () => boolean;
|
|
443
444
|
},
|
|
444
445
|
): Promise<InvocationHarness> {
|
|
445
446
|
const waiters = new Map<string, (frame: ResponseFrame) => void>();
|
|
@@ -477,7 +478,7 @@ async function invocationHarness(
|
|
|
477
478
|
cwd,
|
|
478
479
|
workflowGate: undefined,
|
|
479
480
|
sdkBindings: () => (hooks.invokeSkill ? ["invokeSkill"] : []),
|
|
480
|
-
isIdle: () => true,
|
|
481
|
+
isIdle: hooks.isIdle ?? (() => true),
|
|
481
482
|
abort: hooks.abort ?? (() => {}),
|
|
482
483
|
...(hooks.invokeSkill ? { invokeSkill: hooks.invokeSkill } : {}),
|
|
483
484
|
sessionManager: { getSessionId: () => sessionId, getSessionName: () => undefined },
|
|
@@ -595,6 +596,102 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
595
596
|
await rm(cwd, { recursive: true, force: true });
|
|
596
597
|
}
|
|
597
598
|
});
|
|
599
|
+
test("a completed prompt reports a terminal successful status", async () => {
|
|
600
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-terminalize-completed-prompt-"));
|
|
601
|
+
try {
|
|
602
|
+
const harness = await invocationHarness("terminalize-completed-prompt", cwd, {
|
|
603
|
+
sendUserMessage: async (_content, options) => {
|
|
604
|
+
await options?.onPreflightAcceptCommit?.();
|
|
605
|
+
},
|
|
606
|
+
});
|
|
607
|
+
const accepted = await harness.control("turn.prompt", { text: "hello" });
|
|
608
|
+
expect(accepted.ok).toBe(true);
|
|
609
|
+
const { commandId, turnId } = accepted.result ?? {};
|
|
610
|
+
expect(await settledStatus(harness, "turn.prompt_status", { commandId, turnId })).toMatchObject({
|
|
611
|
+
status: "terminal_ok",
|
|
612
|
+
terminalAt: expect.any(Number),
|
|
613
|
+
});
|
|
614
|
+
await harness.stop();
|
|
615
|
+
} finally {
|
|
616
|
+
await Bun.sleep(50);
|
|
617
|
+
await rm(cwd, { recursive: true, force: true });
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
test("a queued follow-up prompt is not terminalized before the turn runs", async () => {
|
|
621
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-terminalize-followup-"));
|
|
622
|
+
try {
|
|
623
|
+
const turnRunning = Promise.withResolvers<void>();
|
|
624
|
+
const harness = await invocationHarness("terminalize-followup", cwd, {
|
|
625
|
+
sendUserMessage: async (_content, options) => {
|
|
626
|
+
if (options?.deliverAs === "followUp") {
|
|
627
|
+
await options?.onPreflightAcceptCommit?.();
|
|
628
|
+
// #queueFollowUp resolves immediately; the turn has not run yet.
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
await options?.onPreflightAcceptCommit?.();
|
|
632
|
+
await turnRunning.promise;
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
const accepted = await harness.control("turn.follow_up", { text: "hello" });
|
|
636
|
+
expect(accepted.ok).toBe(true);
|
|
637
|
+
const { commandId, turnId } = accepted.result ?? {};
|
|
638
|
+
// The follow-up submission must NOT report terminal_ok while the turn is still pending.
|
|
639
|
+
const status = await harness.query("turn.prompt_status", { commandId, turnId });
|
|
640
|
+
expect(status.result?.status).toMatch(/accepted|in_flight|unknown/);
|
|
641
|
+
await harness.stop();
|
|
642
|
+
} finally {
|
|
643
|
+
await Bun.sleep(50);
|
|
644
|
+
await rm(cwd, { recursive: true, force: true });
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
test("a prompt queued as steer while streaming is not terminalized before the turn runs", async () => {
|
|
648
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-terminalize-prompt-while-busy-"));
|
|
649
|
+
try {
|
|
650
|
+
const harness = await invocationHarness("terminalize-prompt-while-busy", cwd, {
|
|
651
|
+
sendUserMessage: async (_content, options) => {
|
|
652
|
+
// Session is streaming: sendUserMessage diverts to #queueSteer and resolves.
|
|
653
|
+
await options?.onPreflightAcceptCommit?.();
|
|
654
|
+
},
|
|
655
|
+
isIdle: () => false,
|
|
656
|
+
});
|
|
657
|
+
const accepted = await harness.control("turn.prompt", { text: "hello" });
|
|
658
|
+
expect(accepted.ok).toBe(true);
|
|
659
|
+
const { commandId, turnId } = accepted.result ?? {};
|
|
660
|
+
// The diverted prompt must NOT report terminal_ok while the turn is still pending.
|
|
661
|
+
const status = await harness.query("turn.prompt_status", { commandId, turnId });
|
|
662
|
+
expect(status.result?.status).toMatch(/accepted|in_flight|unknown/);
|
|
663
|
+
await harness.stop();
|
|
664
|
+
} finally {
|
|
665
|
+
await Bun.sleep(50);
|
|
666
|
+
await rm(cwd, { recursive: true, force: true });
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
test("a queued prompt stays non-terminal even if isIdle flips during the accept window", async () => {
|
|
670
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-terminalize-race-"));
|
|
671
|
+
let idle = false;
|
|
672
|
+
try {
|
|
673
|
+
const harness = await invocationHarness("terminalize-race", cwd, {
|
|
674
|
+
sendUserMessage: async (_content, options) => {
|
|
675
|
+
// The session is streaming when the submission starts (divert to steer).
|
|
676
|
+
// During accept()->persist(), the prior turn unwinds and isIdle flips to true.
|
|
677
|
+
await options?.onPreflightAcceptCommit?.();
|
|
678
|
+
idle = true;
|
|
679
|
+
},
|
|
680
|
+
isIdle: () => idle,
|
|
681
|
+
});
|
|
682
|
+
const accepted = await harness.control("turn.prompt", { text: "hello" });
|
|
683
|
+
expect(accepted.ok).toBe(true);
|
|
684
|
+
const { commandId, turnId } = accepted.result ?? {};
|
|
685
|
+
// The snapshot taken at dispatch time (idle=false) must hold: the prompt was
|
|
686
|
+
// queued, so it must not report terminal_ok even though isIdle is now true.
|
|
687
|
+
const status = await harness.query("turn.prompt_status", { commandId, turnId });
|
|
688
|
+
expect(status.result?.status).toMatch(/accepted|in_flight|unknown/);
|
|
689
|
+
await harness.stop();
|
|
690
|
+
} finally {
|
|
691
|
+
await Bun.sleep(50);
|
|
692
|
+
await rm(cwd, { recursive: true, force: true });
|
|
693
|
+
}
|
|
694
|
+
});
|
|
598
695
|
|
|
599
696
|
test("a pre-acceptance failure rejects the submission without creating a record", async () => {
|
|
600
697
|
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-terminalize-preflight-"));
|
|
@@ -270,6 +270,7 @@ export interface InvocationReconciliation {
|
|
|
270
270
|
frame: { type: "agent_start" | "agent_end" } | { type: "agent_failed"; error: unknown },
|
|
271
271
|
): Promise<void>;
|
|
272
272
|
lookup(kind: InvocationKind, selector: { commandId?: string; turnId?: string; clientRef?: string }): unknown;
|
|
273
|
+
lookupResult(kind: InvocationKind, selector: { commandId?: string; turnId?: string; clientRef?: string }): unknown;
|
|
273
274
|
hydrate(): Promise<void>;
|
|
274
275
|
}
|
|
275
276
|
|
|
@@ -473,6 +474,10 @@ export function createInvocationReconciliation(
|
|
|
473
474
|
...(record.error === undefined ? {} : { error: record.error }),
|
|
474
475
|
};
|
|
475
476
|
},
|
|
477
|
+
lookupResult(kind, selector) {
|
|
478
|
+
const result = this.lookup(kind, selector) as Record<string, unknown>;
|
|
479
|
+
return result.status === "unknown" ? result : { kind, ...result };
|
|
480
|
+
},
|
|
476
481
|
hydrate,
|
|
477
482
|
};
|
|
478
483
|
}
|
|
@@ -487,8 +492,12 @@ export interface SdkSurfaceFactoryOptions {
|
|
|
487
492
|
configOverrides?: ReadonlyMap<string, unknown>;
|
|
488
493
|
/** Session settings; used for model-usage preferences in profile-limit resolution. */
|
|
489
494
|
settings?: Settings;
|
|
490
|
-
|
|
491
|
-
|
|
495
|
+
turnResultLookup?: (selector: {
|
|
496
|
+
kind: "prompt" | "skill";
|
|
497
|
+
commandId?: string;
|
|
498
|
+
turnId?: string;
|
|
499
|
+
clientRef?: string;
|
|
500
|
+
}) => unknown;
|
|
492
501
|
hostTools?: boolean | (() => boolean);
|
|
493
502
|
/** Q30 event-ring watermark source (the live session event stream); synchronous. */
|
|
494
503
|
getEventWatermark?: () => { generation: number; seq: number };
|
|
@@ -513,8 +522,12 @@ function createQuerySurface(
|
|
|
513
522
|
configOverrides?: ReadonlyMap<string, unknown>;
|
|
514
523
|
/** Session settings; used for model-usage preferences in profile-limit resolution. */
|
|
515
524
|
settings?: Settings;
|
|
516
|
-
|
|
517
|
-
|
|
525
|
+
turnResultLookup?: (selector: {
|
|
526
|
+
kind: "prompt" | "skill";
|
|
527
|
+
commandId?: string;
|
|
528
|
+
turnId?: string;
|
|
529
|
+
clientRef?: string;
|
|
530
|
+
}) => unknown;
|
|
518
531
|
hostTools?: boolean | (() => boolean);
|
|
519
532
|
/** Q30 event-ring watermark source (the live session event stream); synchronous. */
|
|
520
533
|
getEventWatermark?: () => { generation: number; seq: number };
|
|
@@ -782,9 +795,15 @@ function createQuerySurface(
|
|
|
782
795
|
getArtifactRange: (artifactId, offset, length) => ctx.getArtifactRange?.(artifactId, offset, length),
|
|
783
796
|
getJobs: () => ctx.getJobs(),
|
|
784
797
|
getPromptStatus: (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
|
|
785
|
-
|
|
798
|
+
reconciliation.lookup("prompt", selector),
|
|
786
799
|
getSkillInvokeStatus: (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
|
|
787
|
-
|
|
800
|
+
reconciliation.lookup("skill", selector),
|
|
801
|
+
getTurnResult: (selector: {
|
|
802
|
+
kind: "prompt" | "skill";
|
|
803
|
+
commandId?: string;
|
|
804
|
+
turnId?: string;
|
|
805
|
+
clientRef?: string;
|
|
806
|
+
}) => (options.turnResultLookup ?? (value => reconciliation.lookupResult(value.kind, value)))(selector),
|
|
788
807
|
getModelProfiles: async () => {
|
|
789
808
|
const profiles = ctx.modelRegistry.getModelProfiles();
|
|
790
809
|
const authenticatedProviders = await collectAuthenticatedProfileProviders(profiles, provider =>
|
|
@@ -845,8 +864,7 @@ export function createSdkSurfaceFactory(
|
|
|
845
864
|
getLiveState: options.getLiveState,
|
|
846
865
|
configOverrides: options.configOverrides,
|
|
847
866
|
settings: options.settings,
|
|
848
|
-
|
|
849
|
-
skillStatusLookup: options.skillStatusLookup,
|
|
867
|
+
turnResultLookup: options.turnResultLookup,
|
|
850
868
|
hostTools: options.hostTools,
|
|
851
869
|
getEventWatermark: options.getEventWatermark,
|
|
852
870
|
});
|
|
@@ -1039,9 +1057,10 @@ function createControlSurface(
|
|
|
1039
1057
|
run: (options: {
|
|
1040
1058
|
onPreflightAccepted: () => void;
|
|
1041
1059
|
onPreflightAcceptCommit: () => Promise<void>;
|
|
1042
|
-
}) => Promise<
|
|
1060
|
+
}) => Promise<unknown>,
|
|
1043
1061
|
acceptedFields?: () => Record<string, unknown>,
|
|
1044
1062
|
allowCompletionFallback = false,
|
|
1063
|
+
alwaysQueued = false,
|
|
1045
1064
|
): Promise<unknown> => {
|
|
1046
1065
|
const retainedClientRef = normalizeClientRef(clientRef);
|
|
1047
1066
|
reconciliation.admit(kind, retainedClientRef);
|
|
@@ -1051,6 +1070,7 @@ function createControlSurface(
|
|
|
1051
1070
|
let settled = false;
|
|
1052
1071
|
const accept = async (): Promise<void> => {
|
|
1053
1072
|
if (settled) return;
|
|
1073
|
+
|
|
1054
1074
|
try {
|
|
1055
1075
|
await reconciliation.noteAccepted(kind, correlation, retainedClientRef);
|
|
1056
1076
|
accepted = true;
|
|
@@ -1063,6 +1083,11 @@ function createControlSurface(
|
|
|
1063
1083
|
throw error;
|
|
1064
1084
|
}
|
|
1065
1085
|
};
|
|
1086
|
+
// Snapshot before run(): if the session is streaming when the submission starts,
|
|
1087
|
+
// sendUserMessage will divert to steer-queue and resolve before the turn runs.
|
|
1088
|
+
// Re-reading ctx.isIdle() after run() would race — accept() does async fs I/O that
|
|
1089
|
+
// yields, so isStreaming can flip during the persist window.
|
|
1090
|
+
const queuedAtDispatch = alwaysQueued || !ctx.isIdle();
|
|
1066
1091
|
try {
|
|
1067
1092
|
const submission = Promise.resolve(
|
|
1068
1093
|
run({
|
|
@@ -1071,9 +1096,20 @@ function createControlSurface(
|
|
|
1071
1096
|
}),
|
|
1072
1097
|
);
|
|
1073
1098
|
void submission.then(
|
|
1074
|
-
|
|
1099
|
+
result => {
|
|
1075
1100
|
if (settled) {
|
|
1076
|
-
|
|
1101
|
+
// A resolved submission after preflight acceptance means the work is over
|
|
1102
|
+
// for every kind. `noteTransition` ignores an already-terminal record, so
|
|
1103
|
+
// terminalizing here is safe — unless the submission resolved at queue time
|
|
1104
|
+
// (followUp, or a prompt diverted to steer while streaming), in which case
|
|
1105
|
+
// the turn's own lifecycle events drive terminalization.
|
|
1106
|
+
if (!queuedAtDispatch)
|
|
1107
|
+
void reconciliation.noteTransition(kind, correlation, {
|
|
1108
|
+
type: "agent_end",
|
|
1109
|
+
...(typeof result === "string"
|
|
1110
|
+
? { content: { version: 1, type: "text", text: result, byteLength: 0, truncated: false } }
|
|
1111
|
+
: {}),
|
|
1112
|
+
});
|
|
1077
1113
|
return;
|
|
1078
1114
|
}
|
|
1079
1115
|
if (allowCompletionFallback) {
|
|
@@ -1128,7 +1164,14 @@ function createControlSurface(
|
|
|
1128
1164
|
return { commandId: crypto.randomUUID(), accepted: true };
|
|
1129
1165
|
},
|
|
1130
1166
|
followUp: async text =>
|
|
1131
|
-
submit(
|
|
1167
|
+
submit(
|
|
1168
|
+
"prompt",
|
|
1169
|
+
undefined,
|
|
1170
|
+
options => api.sendUserMessage(text, { ...options, deliverAs: "followUp" }),
|
|
1171
|
+
undefined,
|
|
1172
|
+
false,
|
|
1173
|
+
true,
|
|
1174
|
+
),
|
|
1132
1175
|
abort: () => {
|
|
1133
1176
|
ctx.abort();
|
|
1134
1177
|
return { aborted: true };
|
|
@@ -1168,7 +1211,7 @@ function createControlSurface(
|
|
|
1168
1211
|
onSkillPrepared: meta => {
|
|
1169
1212
|
prepared = meta;
|
|
1170
1213
|
},
|
|
1171
|
-
}).then(
|
|
1214
|
+
}).then(result => result),
|
|
1172
1215
|
() => ({
|
|
1173
1216
|
name: prepared?.name ?? String(name),
|
|
1174
1217
|
path: prepared?.path ?? "",
|
|
@@ -1413,8 +1456,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
1413
1456
|
id: sessionId,
|
|
1414
1457
|
api,
|
|
1415
1458
|
reconciliation,
|
|
1416
|
-
|
|
1417
|
-
skillStatusLookup: selector => reconciliation.lookup("skill", selector),
|
|
1459
|
+
turnResultLookup: selector => reconciliation.lookupResult(selector.kind, selector),
|
|
1418
1460
|
configOverrides: options.configOverrides,
|
|
1419
1461
|
settings: options.settings,
|
|
1420
1462
|
getEventWatermark: () => eventWatermarkSource(),
|