@gajae-code/coding-agent 0.15.5 → 0.15.6
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 +26 -1
- package/dist/types/async/job-manager.d.ts +83 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
- package/dist/types/cli/model-presets-cli.d.ts +9 -0
- package/dist/types/cli/update-cli.d.ts +2 -0
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/commands/gc.d.ts +13 -0
- package/dist/types/commands/model-presets.d.ts +21 -0
- package/dist/types/config/autorouting-tier-map.d.ts +96 -0
- package/dist/types/config/file-lock.d.ts +49 -2
- package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
- package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +3 -2
- package/dist/types/config/model-preset-registry.d.ts +368 -0
- package/dist/types/config/model-profile-activation.d.ts +39 -0
- package/dist/types/config/model-profiles.d.ts +5 -3
- package/dist/types/config/model-registry.d.ts +11 -5
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
- package/dist/types/coordinator-mcp/projection-scan.d.ts +46 -0
- package/dist/types/extensibility/extensions/types.d.ts +2 -5
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/main.d.ts +3 -1
- package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
- package/dist/types/modes/jobs-observer.d.ts +36 -0
- package/dist/types/sdk/broker/broker.d.ts +3 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
- package/dist/types/sdk/client/discovery.d.ts +1 -0
- package/dist/types/sdk/host/model-pin.d.ts +7 -2
- package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +15 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
- package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +9 -0
- package/dist/types/session/account-inventory.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +19 -7
- package/dist/types/session/fold-coordinator.d.ts +201 -0
- package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
- package/dist/types/session/sdk-run-capability.d.ts +2 -0
- package/dist/types/telemetry/control.d.ts +11 -0
- package/dist/types/telemetry/events.d.ts +20 -0
- package/dist/types/telemetry/index.d.ts +3 -0
- package/dist/types/telemetry/transport.d.ts +22 -0
- package/dist/types/tools/bash-interactive.d.ts +23 -0
- package/dist/types/tools/index.d.ts +6 -5
- package/dist/types/web/search/providers/base.d.ts +9 -4
- package/dist/types/web/search/providers/xai.d.ts +2 -1
- package/dist/types/web/search/types.d.ts +14 -0
- package/package.json +10 -7
- package/scripts/generate-hotkeys-docs.ts +4 -0
- package/scripts/generate-sdk-operation-inventory.ts +3 -3
- package/src/async/job-manager.ts +671 -39
- package/src/cli/auth-broker-cli.ts +40 -12
- package/src/cli/auth-gateway-cli.ts +273 -97
- package/src/cli/config-cli.ts +2 -2
- package/src/cli/fast-help.ts +3 -0
- package/src/cli/model-presets-cli.ts +100 -0
- package/src/cli/update-cli.ts +40 -5
- package/src/cli.ts +6 -2
- package/src/commands/auth-gateway.ts +12 -8
- package/src/commands/gc.ts +19 -0
- package/src/commands/model-presets.ts +24 -0
- package/src/config/autorouting-tier-map.ts +32 -0
- package/src/config/file-lock-gc.ts +9 -4
- package/src/config/file-lock.ts +775 -59
- package/src/config/internal/model-preset-registry-test-state.ts +28 -0
- package/src/config/internal/model-preset-registry-test-support.ts +11 -0
- package/src/config/keybindings.ts +6 -2
- package/src/config/model-preset-registry.ts +2799 -0
- package/src/config/model-profile-activation.ts +115 -28
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +51 -9
- package/src/config/model-registry.ts +1021 -122
- package/src/config/settings-schema.ts +11 -0
- package/src/config/settings.ts +6 -1
- package/src/coordinator-mcp/model-preset.ts +32 -17
- package/src/coordinator-mcp/projection-scan.ts +571 -0
- package/src/coordinator-mcp/server.ts +19 -24
- package/src/coordinator-mcp/session-reaper.ts +9 -3
- package/src/extensibility/extensions/loader.ts +0 -1
- package/src/extensibility/extensions/types.ts +2 -3
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
- package/src/gjc-runtime/empty-delete-gc.ts +301 -0
- package/src/gjc-runtime/gc-render.ts +29 -0
- package/src/gjc-runtime/gc-runtime.ts +174 -11
- package/src/gjc-runtime/session-state-lock.ts +1242 -141
- package/src/gjc-runtime/session-state-sidecar.ts +76 -3
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +9 -8
- package/src/lsp/config.ts +4 -3
- package/src/lsp/path-trust.ts +12 -6
- package/src/main.ts +9 -2
- package/src/modes/acp/acp-agent.ts +11 -5
- package/src/modes/components/jobs-overlay-model.ts +99 -16
- package/src/modes/components/model-selector.ts +158 -18
- package/src/modes/components/status-line/segments.ts +6 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +23 -6
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/jobs-observer.ts +149 -16
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/utils/injected-user-submission.ts +15 -1
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/fold-receipt.md +3 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +17 -2
- package/src/sdk/broker/broker.ts +48 -21
- package/src/sdk/broker/lifecycle.ts +32 -6
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +11 -1
- package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
- package/src/sdk/bus/prompt-reconciliation.ts +28 -4
- package/src/sdk/bus/slack-conversation.ts +5 -1
- package/src/sdk/cli/session-cli.ts +12 -1
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +3 -0
- package/src/sdk/host/model-pin.ts +185 -25
- package/src/sdk/host/sdk-run-capability.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +371 -37
- package/src/sdk/host/session-runtime.ts +364 -52
- package/src/sdk/prompt-deadline-manager.ts +16 -1
- package/src/sdk/protocol/adapter-validation.ts +27 -0
- package/src/sdk/protocol/operation-inventory.generated.json +2 -2
- package/src/sdk/protocol/operation-registry.ts +4 -1
- package/src/sdk/router/session-router.ts +228 -96
- package/src/sdk/session.ts +83 -12
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +678 -103
- package/src/session/fold-coordinator.ts +433 -0
- package/src/session/sdk-run-capability-internal.ts +15 -0
- package/src/session/sdk-run-capability.ts +2 -0
- package/src/session/session-manager.ts +5 -4
- package/src/setup/provider-presets.json +1 -1
- package/src/slash-commands/helpers/credential-switch.ts +6 -1
- package/src/task/executor.ts +19 -3
- package/src/telemetry/control.ts +26 -0
- package/src/telemetry/events.ts +142 -0
- package/src/telemetry/index.ts +3 -0
- package/src/telemetry/transport.ts +87 -0
- package/src/tools/bash-interactive.ts +182 -85
- package/src/tools/bash.ts +772 -67
- package/src/tools/index.ts +7 -5
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/web/search/provider.ts +120 -11
- package/src/web/search/providers/anthropic.ts +89 -17
- package/src/web/search/providers/base.ts +9 -4
- package/src/web/search/providers/gemini.ts +87 -15
- package/src/web/search/providers/openai-compatible.ts +23 -8
- package/src/web/search/providers/xai.ts +98 -12
- package/src/web/search/types.ts +16 -0
|
@@ -21,6 +21,8 @@ import type {
|
|
|
21
21
|
ReconciliationStore,
|
|
22
22
|
} from "./reconciliation-store";
|
|
23
23
|
|
|
24
|
+
const EMPTY_PROMPT_FAILURE = { code: "prompt_failed", message: "Prompt submission failed." } as const;
|
|
25
|
+
|
|
24
26
|
export type { ReconciliationKind };
|
|
25
27
|
|
|
26
28
|
export interface KindCorrelation extends PromptCorrelation {
|
|
@@ -51,8 +53,13 @@ export interface KindAwareReconciliation {
|
|
|
51
53
|
kind: ReconciliationKind,
|
|
52
54
|
correlation: PromptCorrelation | undefined,
|
|
53
55
|
frame:
|
|
54
|
-
| {
|
|
55
|
-
|
|
56
|
+
| {
|
|
57
|
+
type: "agent_start" | "agent_end";
|
|
58
|
+
content?: TurnResultContent;
|
|
59
|
+
hasActivity?: boolean;
|
|
60
|
+
outcome?: SdkPromptTerminalOutcome;
|
|
61
|
+
}
|
|
62
|
+
| { type: "agent_failed"; error: unknown; content?: TurnResultContent; hasActivity?: boolean },
|
|
56
63
|
): Promise<void>;
|
|
57
64
|
claimPendingOutcome(
|
|
58
65
|
kind: ReconciliationKind,
|
|
@@ -340,8 +347,13 @@ export function createKindAwareReconciliation(
|
|
|
340
347
|
kind: ReconciliationKind,
|
|
341
348
|
correlation: PromptCorrelation | undefined,
|
|
342
349
|
frame:
|
|
343
|
-
| {
|
|
344
|
-
|
|
350
|
+
| {
|
|
351
|
+
type: "agent_start" | "agent_end";
|
|
352
|
+
content?: TurnResultContent;
|
|
353
|
+
hasActivity?: boolean;
|
|
354
|
+
outcome?: SdkPromptTerminalOutcome;
|
|
355
|
+
}
|
|
356
|
+
| { type: "agent_failed"; error: unknown; content?: TurnResultContent; hasActivity?: boolean },
|
|
345
357
|
) => {
|
|
346
358
|
if (!correlation) return;
|
|
347
359
|
await queueMutation(candidate => {
|
|
@@ -412,7 +424,17 @@ export function createKindAwareReconciliation(
|
|
|
412
424
|
record.receiptState = record.pendingReceiptState ?? "missing";
|
|
413
425
|
record.pendingReceiptState = undefined;
|
|
414
426
|
} else {
|
|
415
|
-
|
|
427
|
+
if (
|
|
428
|
+
kind === "prompt" &&
|
|
429
|
+
record.error === undefined &&
|
|
430
|
+
frame.type === "agent_end" &&
|
|
431
|
+
frame.outcome?.kind !== "stopped" &&
|
|
432
|
+
!frame.content?.text.trim() &&
|
|
433
|
+
!frame.hasActivity
|
|
434
|
+
) {
|
|
435
|
+
record.status = "failed";
|
|
436
|
+
record.error = EMPTY_PROMPT_FAILURE;
|
|
437
|
+
} else record.status = record.error === undefined ? "terminal_ok" : "failed";
|
|
416
438
|
record.receiptState = frame.content?.text?.trim() ? "present" : "missing";
|
|
417
439
|
}
|
|
418
440
|
cleanupRecords(candidate);
|
|
@@ -453,6 +475,7 @@ export function createKindAwareReconciliation(
|
|
|
453
475
|
: undefined;
|
|
454
476
|
const content =
|
|
455
477
|
typeof arg4 === "function" ? arg6 : typeof arg5 === "object" && arg5 !== null && "code" in arg5 ? arg6 : arg5;
|
|
478
|
+
const sanitizedContent = sanitizeTurnResultContent(content);
|
|
456
479
|
await queueMutation(candidate => {
|
|
457
480
|
if (isCurrent !== undefined && !isCurrent()) return { value: undefined, changed: false };
|
|
458
481
|
const record = candidate.get(keyOf(kind, correlation));
|
|
@@ -463,8 +486,15 @@ export function createKindAwareReconciliation(
|
|
|
463
486
|
if (finalOutcome?.kind === "failed") {
|
|
464
487
|
record.status = "failed";
|
|
465
488
|
record.error = recordError ?? { code: finalOutcome.code, message: finalOutcome.message };
|
|
489
|
+
} else if (
|
|
490
|
+
kind === "prompt" &&
|
|
491
|
+
((finalOutcome === undefined && !sanitizedContent?.text.trim()) ||
|
|
492
|
+
(finalOutcome?.kind !== "stopped" && content !== undefined && !sanitizedContent?.text.trim()))
|
|
493
|
+
) {
|
|
494
|
+
record.status = "failed";
|
|
495
|
+
record.error = EMPTY_PROMPT_FAILURE;
|
|
466
496
|
} else record.status = "terminal_ok";
|
|
467
|
-
record.content =
|
|
497
|
+
record.content = sanitizedContent;
|
|
468
498
|
record.outcome = finalOutcome;
|
|
469
499
|
record.receiptState = record.pendingReceiptState ?? "unknown";
|
|
470
500
|
record.pendingOutcome = undefined;
|
|
@@ -23,11 +23,13 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { PROMPT_FAILURE_CODE_MAX, sanitizePromptFailure } from "../prompt-failure";
|
|
26
|
+
import type { SdkPromptTerminalOutcome } from "../prompt-status";
|
|
26
27
|
import type { ReceiptState } from "../receipt-state";
|
|
27
28
|
|
|
28
29
|
export { PROMPT_FAILURE_CODE_MAX, sanitizePromptFailure };
|
|
29
30
|
export const PROMPT_RECONCILIATION_ACTIVE_CAPACITY = 128;
|
|
30
31
|
export const PROMPT_RECONCILIATION_TERMINAL_CAPACITY = 256;
|
|
32
|
+
const EMPTY_PROMPT_FAILURE = { code: "prompt_failed", message: "Prompt submission failed." } as const;
|
|
31
33
|
|
|
32
34
|
export type PromptReconciliationStatus = "accepted" | "in_flight" | "terminal_ok" | "failed";
|
|
33
35
|
|
|
@@ -101,8 +103,13 @@ export interface PromptReconciliation {
|
|
|
101
103
|
correlation: PromptCorrelation | undefined,
|
|
102
104
|
frame:
|
|
103
105
|
| { type: "agent_start" }
|
|
104
|
-
| {
|
|
105
|
-
|
|
106
|
+
| {
|
|
107
|
+
type: "agent_end";
|
|
108
|
+
finalText?: string;
|
|
109
|
+
hasActivity?: boolean;
|
|
110
|
+
outcome?: SdkPromptTerminalOutcome;
|
|
111
|
+
}
|
|
112
|
+
| { type: "agent_failed"; error: unknown; finalText?: string; hasActivity?: boolean },
|
|
106
113
|
): void;
|
|
107
114
|
lookup(selector: { commandId?: string; turnId?: string; clientRef?: string }): TurnPromptReconciliation;
|
|
108
115
|
cleanup(): void;
|
|
@@ -190,8 +197,13 @@ export function createPromptReconciliation(options: { now?: () => number } = {})
|
|
|
190
197
|
correlation: PromptCorrelation | undefined,
|
|
191
198
|
frame:
|
|
192
199
|
| { type: "agent_start" }
|
|
193
|
-
| {
|
|
194
|
-
|
|
200
|
+
| {
|
|
201
|
+
type: "agent_end";
|
|
202
|
+
finalText?: string;
|
|
203
|
+
hasActivity?: boolean;
|
|
204
|
+
outcome?: SdkPromptTerminalOutcome;
|
|
205
|
+
}
|
|
206
|
+
| { type: "agent_failed"; error: unknown; finalText?: string; hasActivity?: boolean },
|
|
195
207
|
) => {
|
|
196
208
|
if (!correlation) return;
|
|
197
209
|
const record = records.get(keyOf(correlation));
|
|
@@ -221,6 +233,18 @@ export function createPromptReconciliation(options: { now?: () => number } = {})
|
|
|
221
233
|
if (frame.type === "agent_failed") {
|
|
222
234
|
record.status = "failed";
|
|
223
235
|
record.error = sanitizePromptFailure(frame.error);
|
|
236
|
+
} else if (record.error !== undefined) {
|
|
237
|
+
// Preserve an actionable provider failure already recorded before a late
|
|
238
|
+
// empty agent_end; the empty frame cannot downgrade or replace its cause.
|
|
239
|
+
record.status = "failed";
|
|
240
|
+
} else if (frame.type === "agent_end" && frame.outcome?.kind === "stopped") {
|
|
241
|
+
record.status = "terminal_ok";
|
|
242
|
+
} else if (!frame.finalText?.trim() && !frame.hasActivity) {
|
|
243
|
+
// A prompt cannot claim successful completion without assistant content.
|
|
244
|
+
// Keep the terminal transition first-wins, but fail closed instead of
|
|
245
|
+
// exposing an empty zero-token turn as terminal_ok.
|
|
246
|
+
record.status = "failed";
|
|
247
|
+
record.error = EMPTY_PROMPT_FAILURE;
|
|
224
248
|
} else {
|
|
225
249
|
record.status = "terminal_ok";
|
|
226
250
|
}
|
|
@@ -74,5 +74,9 @@ export function normalizeSlackConversation(record: SlackConversation): SlackConv
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
export function acceptsSlackInbound(record: SlackConversation, rootTs: string, endpointGeneration: number): boolean {
|
|
77
|
-
return
|
|
77
|
+
return (
|
|
78
|
+
(record.state === "active" || record.state === "resumed_root") &&
|
|
79
|
+
record.rootTs === rootTs &&
|
|
80
|
+
record.endpointGeneration === endpointGeneration
|
|
81
|
+
);
|
|
78
82
|
}
|
|
@@ -16,7 +16,11 @@ import type {
|
|
|
16
16
|
SessionReconcileUncertainTarget,
|
|
17
17
|
} from "../lifecycle/service";
|
|
18
18
|
import { PROMPT_CLIENT_REF_MAX_LENGTH } from "../prompt-status";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
validateAdapterControl,
|
|
21
|
+
validateAdapterSecretFields,
|
|
22
|
+
validateRequiredPromptText,
|
|
23
|
+
} from "../protocol/adapter-validation";
|
|
20
24
|
import { adapterDispositionError, findOperation, type OperationKind } from "../protocol/operation-registry";
|
|
21
25
|
import { type SessionAttachment, SessionRouter, SessionRouterError, type SessionRouterFrame } from "../router";
|
|
22
26
|
import { SessionListTraversalError, sessionListPageFromResponse, traverseSessionList } from "../session-list";
|
|
@@ -654,6 +658,8 @@ async function runSend(agentDir: string, sessionId: string, args: SdkSessionCliA
|
|
|
654
658
|
if (args.text !== undefined && Object.keys(input).length > 0)
|
|
655
659
|
throw new SdkSessionCliError("usage", "Use either --text or one JSON input source for the prompt, not both.", 2);
|
|
656
660
|
const promptInput: JsonRecord = args.text === undefined ? { ...input } : { text: args.text };
|
|
661
|
+
const promptError = validateRequiredPromptText("turn.prompt", promptInput);
|
|
662
|
+
if (promptError) throw new SdkSessionCliError(promptError.code, promptError.message, 2);
|
|
657
663
|
const inputRef = clientRefFromInput(promptInput);
|
|
658
664
|
const clientRef = inputRef ?? args.opRef?.trim() ?? createOperationRef();
|
|
659
665
|
if (args.opRef !== undefined && inputRef !== undefined && inputRef !== args.opRef.trim())
|
|
@@ -914,6 +920,11 @@ function publicationSequence(publicationId: string): number | undefined {
|
|
|
914
920
|
|
|
915
921
|
function tailItemFromRouterFrame(frame: SessionRouterFrame): SdkTailItemV1 | undefined {
|
|
916
922
|
if (frame.name === undefined) return undefined;
|
|
923
|
+
// The replay response is an out-of-band control frame, not an event-ring
|
|
924
|
+
// item. Its generation/lastSeq metadata is a response position, and it
|
|
925
|
+
// intentionally has no event `seq`; projecting it as a tail item would make
|
|
926
|
+
// the strict event-position validator reject every otherwise valid tail.
|
|
927
|
+
if (frame.name === "event_replay_result") return undefined;
|
|
917
928
|
const seq = frame.publicationId === undefined ? undefined : publicationSequence(frame.publicationId);
|
|
918
929
|
return toTailItemV1(
|
|
919
930
|
{
|
|
@@ -35,7 +35,7 @@ export class SdkDiscoveryError extends Error {
|
|
|
35
35
|
|
|
36
36
|
export type SdkSessionEndpointScope = "default" | "chat";
|
|
37
37
|
|
|
38
|
-
function endpointDirectory(repo: string, scope: SdkSessionEndpointScope = "default"): string {
|
|
38
|
+
export function endpointDirectory(repo: string, scope: SdkSessionEndpointScope = "default"): string {
|
|
39
39
|
return scope === "chat" ? path.join(repo, ".gjc", "state", "chat", "sdk") : path.join(repo, ".gjc", "state", "sdk");
|
|
40
40
|
}
|
|
41
41
|
function isUsableSessionId(sessionId: string): boolean {
|
|
@@ -132,6 +132,8 @@ export async function readSdkSessionEndpoint(
|
|
|
132
132
|
if (!isUsableSessionId(sessionId)) return null;
|
|
133
133
|
const file = path.join(endpointDirectory(repo, scope), `${sessionId}.json`);
|
|
134
134
|
try {
|
|
135
|
+
const stat = await fs.lstat(file);
|
|
136
|
+
if (!stat.isFile()) return null;
|
|
135
137
|
return parseEndpoint(sessionId, file, JSON.parse(await fs.readFile(file, "utf8")));
|
|
136
138
|
} catch (error) {
|
|
137
139
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
DEFAULT_MODEL_SELECTION_RECOVERY_MESSAGE,
|
|
4
4
|
parseDefaultModelSelectionRecovery,
|
|
5
5
|
} from "../../../session/default-model-selection";
|
|
6
|
+
import { validateRequiredPromptText } from "../../protocol/adapter-validation";
|
|
6
7
|
import { OPERATIONS, type Operation } from "../../protocol/operation-registry";
|
|
7
8
|
import type { ControlInput, ControlSurface, ControlValue } from "./operations";
|
|
8
9
|
import { brokerRuntimeCloseCapability, hasBrokerRuntimeCloseCapability } from "./runtime-gate";
|
|
@@ -462,6 +463,8 @@ export function dispatchControl(
|
|
|
462
463
|
);
|
|
463
464
|
if (!isInput(request.input))
|
|
464
465
|
return Promise.resolve(failure(request.id, "invalid_input", "Control input must be an object."));
|
|
466
|
+
const promptError = validateRequiredPromptText(row.sdkId, request.input);
|
|
467
|
+
if (promptError) return Promise.resolve(failure(request.id, promptError.code, promptError.message));
|
|
465
468
|
if ((row.sdkId === "context.clear" || row.sdkId === "session.delete") && request.confirm !== true)
|
|
466
469
|
return Promise.resolve(
|
|
467
470
|
failure(request.id, "invalid_input", "confirm: true is required for this destructive operation."),
|
|
@@ -17,9 +17,19 @@ export interface SdkHostModelResolveContext {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/** Registry loader owned by the SDK host, not by machine-facing adapters. */
|
|
20
|
-
export type SdkHostModelRegistryLoader = (
|
|
20
|
+
export type SdkHostModelRegistryLoader = ((
|
|
21
21
|
context?: SdkHostModelResolveContext,
|
|
22
|
-
) => ModelRegistry | Promise<ModelRegistry
|
|
22
|
+
) => ModelRegistry | Promise<ModelRegistry>) & {
|
|
23
|
+
acquire?: (context?: SdkHostModelResolveContext) => () => void;
|
|
24
|
+
dispose?: () => Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type OwnedModelRegistry = {
|
|
28
|
+
registry: ModelRegistry;
|
|
29
|
+
storage: AuthStorage;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const MAX_CACHED_MODEL_REGISTRIES = 32;
|
|
23
33
|
|
|
24
34
|
/**
|
|
25
35
|
* Builds an offline model resolver for a single SDK host process.
|
|
@@ -32,16 +42,112 @@ export function createSdkHostModelRegistryLoader(
|
|
|
32
42
|
modelsPath?: string,
|
|
33
43
|
loadSettings?: (context?: SdkHostModelResolveContext) => Promise<Pick<Settings, "get" | "getGlobal">>,
|
|
34
44
|
): SdkHostModelRegistryLoader {
|
|
35
|
-
const cachedRegistries = new Map<string, Promise<
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
const cachedRegistries = new Map<string, Promise<OwnedModelRegistry>>();
|
|
46
|
+
const retiredRegistries = new Map<string, Promise<OwnedModelRegistry>[]>();
|
|
47
|
+
const activeScopes = new Map<string, number>();
|
|
48
|
+
const disposedEntries = new WeakSet<Promise<OwnedModelRegistry>>();
|
|
49
|
+
const pendingDisposals = new Set<Promise<void>>();
|
|
50
|
+
const disposalBarrier = Promise.withResolvers<void>();
|
|
51
|
+
const registryAgentDir = path.resolve(path.dirname(modelsPath ?? "."));
|
|
52
|
+
let disposed = false;
|
|
53
|
+
const scopeKeyFor = (context?: SdkHostModelResolveContext): string =>
|
|
54
|
+
context?.cwd === undefined ? "" : path.resolve(context.cwd);
|
|
55
|
+
const disposeEntry = async (entry: Promise<OwnedModelRegistry>): Promise<void> => {
|
|
56
|
+
let owned: OwnedModelRegistry;
|
|
57
|
+
try {
|
|
58
|
+
owned = await entry;
|
|
59
|
+
} catch {
|
|
60
|
+
// Initialization failures close their storage before rejecting. Disposal is
|
|
61
|
+
// best-effort and must not block broker shutdown on one broken scope.
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
owned.registry.dispose();
|
|
66
|
+
} finally {
|
|
67
|
+
owned.storage.close();
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const maybeResolveDisposal = (): void => {
|
|
71
|
+
if (
|
|
72
|
+
disposed &&
|
|
73
|
+
cachedRegistries.size === 0 &&
|
|
74
|
+
retiredRegistries.size === 0 &&
|
|
75
|
+
activeScopes.size === 0 &&
|
|
76
|
+
pendingDisposals.size === 0
|
|
77
|
+
)
|
|
78
|
+
disposalBarrier.resolve();
|
|
79
|
+
};
|
|
80
|
+
const disposeEntries = (entries: Promise<OwnedModelRegistry>[]): void => {
|
|
81
|
+
const seen = new Set<Promise<OwnedModelRegistry>>();
|
|
82
|
+
const unique = entries.filter(entry => {
|
|
83
|
+
if (seen.has(entry) || disposedEntries.has(entry)) return false;
|
|
84
|
+
seen.add(entry);
|
|
85
|
+
return true;
|
|
86
|
+
});
|
|
87
|
+
if (unique.length === 0) {
|
|
88
|
+
maybeResolveDisposal();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
for (const entry of unique) disposedEntries.add(entry);
|
|
92
|
+
const pending = Promise.all(unique.map(entry => disposeEntry(entry))).then(
|
|
93
|
+
() => undefined,
|
|
94
|
+
() => undefined,
|
|
95
|
+
);
|
|
96
|
+
pendingDisposals.add(pending);
|
|
97
|
+
void pending.finally(() => {
|
|
98
|
+
pendingDisposals.delete(pending);
|
|
99
|
+
maybeResolveDisposal();
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
const retireEntry = (scopeKey: string, entry: Promise<OwnedModelRegistry>): void => {
|
|
103
|
+
if ((activeScopes.get(scopeKey) ?? 0) > 0) {
|
|
104
|
+
const retired = retiredRegistries.get(scopeKey) ?? [];
|
|
105
|
+
if (!retired.includes(entry)) retired.push(entry);
|
|
106
|
+
retiredRegistries.set(scopeKey, retired);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
disposeEntries([entry]);
|
|
110
|
+
};
|
|
111
|
+
const disposeRetired = (scopeKey: string): void => {
|
|
112
|
+
const retired = retiredRegistries.get(scopeKey);
|
|
113
|
+
if (!retired || (activeScopes.get(scopeKey) ?? 0) > 0) return;
|
|
114
|
+
retiredRegistries.delete(scopeKey);
|
|
115
|
+
disposeEntries(retired);
|
|
116
|
+
};
|
|
117
|
+
const evictIdleEntries = (): Promise<OwnedModelRegistry>[] => {
|
|
118
|
+
const evicted: Promise<OwnedModelRegistry>[] = [];
|
|
119
|
+
while (cachedRegistries.size > MAX_CACHED_MODEL_REGISTRIES) {
|
|
120
|
+
const oldest = [...cachedRegistries.keys()].find(key => (activeScopes.get(key) ?? 0) === 0);
|
|
121
|
+
if (oldest === undefined) break;
|
|
122
|
+
const entry = cachedRegistries.get(oldest);
|
|
123
|
+
cachedRegistries.delete(oldest);
|
|
124
|
+
if (entry) evicted.push(entry);
|
|
125
|
+
}
|
|
126
|
+
return evicted;
|
|
127
|
+
};
|
|
128
|
+
const loadRegistry = async (context?: SdkHostModelResolveContext): Promise<ModelRegistry> => {
|
|
129
|
+
if (disposed) throw new Error("SDK host model registry loader is disposed.");
|
|
130
|
+
const scopeKey = scopeKeyFor(context);
|
|
38
131
|
let cachedRegistry = cachedRegistries.get(scopeKey);
|
|
39
132
|
if (cachedRegistry === undefined) {
|
|
40
|
-
const initializing =
|
|
41
|
-
|
|
42
|
-
|
|
133
|
+
const initializing = discoverStorage().then(async storage => {
|
|
134
|
+
try {
|
|
135
|
+
const registrySettings = await loadSettings?.(context);
|
|
136
|
+
return {
|
|
137
|
+
storage,
|
|
138
|
+
registry: new ModelRegistry(storage, modelsPath, registrySettings, {
|
|
139
|
+
agentDir: registryAgentDir,
|
|
140
|
+
automaticRefresh: false,
|
|
141
|
+
}),
|
|
142
|
+
};
|
|
143
|
+
} catch (error) {
|
|
144
|
+
storage.close();
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
43
148
|
cachedRegistries.set(scopeKey, initializing);
|
|
44
149
|
cachedRegistry = initializing;
|
|
150
|
+
disposeEntries(evictIdleEntries());
|
|
45
151
|
try {
|
|
46
152
|
await initializing;
|
|
47
153
|
} catch (error) {
|
|
@@ -49,21 +155,67 @@ export function createSdkHostModelRegistryLoader(
|
|
|
49
155
|
throw error;
|
|
50
156
|
}
|
|
51
157
|
}
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
158
|
+
const owned = await cachedRegistry;
|
|
159
|
+
if (disposed || disposedEntries.has(cachedRegistry) || cachedRegistries.get(scopeKey) !== cachedRegistry) {
|
|
160
|
+
retireEntry(scopeKey, cachedRegistry);
|
|
161
|
+
throw new Error("SDK host model registry loader is disposed.");
|
|
162
|
+
}
|
|
163
|
+
cachedRegistries.delete(scopeKey);
|
|
164
|
+
cachedRegistries.set(scopeKey, cachedRegistry);
|
|
165
|
+
const registry = owned.registry;
|
|
166
|
+
if (loadSettings) {
|
|
167
|
+
const registrySettings = await loadSettings(context);
|
|
168
|
+
if (disposed || disposedEntries.has(cachedRegistry)) {
|
|
169
|
+
retireEntry(scopeKey, cachedRegistry);
|
|
170
|
+
throw new Error("SDK host model registry loader is disposed.");
|
|
171
|
+
}
|
|
172
|
+
registry.setScopedSettings(registrySettings);
|
|
173
|
+
}
|
|
54
174
|
await registry.refresh("offline");
|
|
175
|
+
if (disposed || disposedEntries.has(cachedRegistry)) {
|
|
176
|
+
retireEntry(scopeKey, cachedRegistry);
|
|
177
|
+
throw new Error("SDK host model registry loader is disposed.");
|
|
178
|
+
}
|
|
55
179
|
return registry;
|
|
56
180
|
};
|
|
181
|
+
return Object.assign(loadRegistry, {
|
|
182
|
+
dispose: async (): Promise<void> => {
|
|
183
|
+
if (disposed) return await disposalBarrier.promise;
|
|
184
|
+
disposed = true;
|
|
185
|
+
for (const [scopeKey, entry] of cachedRegistries) retireEntry(scopeKey, entry);
|
|
186
|
+
cachedRegistries.clear();
|
|
187
|
+
for (const scopeKey of retiredRegistries.keys()) disposeRetired(scopeKey);
|
|
188
|
+
maybeResolveDisposal();
|
|
189
|
+
await disposalBarrier.promise;
|
|
190
|
+
},
|
|
191
|
+
acquire: (context?: SdkHostModelResolveContext): (() => void) => {
|
|
192
|
+
const scopeKey = scopeKeyFor(context);
|
|
193
|
+
activeScopes.set(scopeKey, (activeScopes.get(scopeKey) ?? 0) + 1);
|
|
194
|
+
let released = false;
|
|
195
|
+
return () => {
|
|
196
|
+
if (released) return;
|
|
197
|
+
released = true;
|
|
198
|
+
const next = (activeScopes.get(scopeKey) ?? 1) - 1;
|
|
199
|
+
if (next <= 0) activeScopes.delete(scopeKey);
|
|
200
|
+
else activeScopes.set(scopeKey, next);
|
|
201
|
+
disposeRetired(scopeKey);
|
|
202
|
+
disposeEntries(evictIdleEntries());
|
|
203
|
+
maybeResolveDisposal();
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
});
|
|
57
207
|
}
|
|
58
208
|
|
|
59
209
|
export type SdkHostModelResolution =
|
|
60
210
|
| { ok: true; model: string | null }
|
|
61
211
|
| { ok: false; reason: "unknown_model"; model: string; error: string };
|
|
62
212
|
|
|
63
|
-
export type SdkHostModelResolver = (
|
|
213
|
+
export type SdkHostModelResolver = ((
|
|
64
214
|
raw: unknown,
|
|
65
215
|
context?: SdkHostModelResolveContext,
|
|
66
|
-
) => Promise<SdkHostModelResolution
|
|
216
|
+
) => Promise<SdkHostModelResolution>) & {
|
|
217
|
+
dispose?: () => Promise<void>;
|
|
218
|
+
};
|
|
67
219
|
|
|
68
220
|
/** Resolve the explicit model pin at the SDK host boundary. */
|
|
69
221
|
export async function resolveSdkHostModel(
|
|
@@ -74,19 +226,24 @@ export async function resolveSdkHostModel(
|
|
|
74
226
|
if (raw === undefined || raw === null) return { ok: true, model: null };
|
|
75
227
|
const requested = typeof raw === "string" ? raw : "";
|
|
76
228
|
const echoed = requested.trim().slice(0, MAX_ECHOED_MODEL_LENGTH);
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
229
|
+
const release = loadRegistry.acquire?.(context) ?? (() => {});
|
|
230
|
+
try {
|
|
231
|
+
const registry = await loadRegistry(context);
|
|
232
|
+
const resolved: ResolveCliModelResult = resolveCliModel({ cliModel: requested, modelRegistry: registry });
|
|
233
|
+
if (!resolved.model)
|
|
234
|
+
return {
|
|
235
|
+
ok: false,
|
|
236
|
+
reason: "unknown_model",
|
|
237
|
+
model: echoed,
|
|
238
|
+
error: resolved.error ?? "No models available. Check your installation or add models to models.json.",
|
|
239
|
+
};
|
|
80
240
|
return {
|
|
81
|
-
ok:
|
|
82
|
-
|
|
83
|
-
model: echoed,
|
|
84
|
-
error: resolved.error ?? "No models available. Check your installation or add models to models.json.",
|
|
241
|
+
ok: true,
|
|
242
|
+
model: formatModelSelectorValue(formatModelString(resolved.model), resolved.thinkingLevel),
|
|
85
243
|
};
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
};
|
|
244
|
+
} finally {
|
|
245
|
+
release();
|
|
246
|
+
}
|
|
90
247
|
}
|
|
91
248
|
|
|
92
249
|
/** Default resolver used by the SDK broker host. */
|
|
@@ -96,6 +253,9 @@ export function createDefaultSdkHostModelResolver(agentDir: string): SdkHostMode
|
|
|
96
253
|
path.join(agentDir, "models.yml"),
|
|
97
254
|
context => Settings.loadReadonly({ agentDir, cwd: context?.cwd }),
|
|
98
255
|
);
|
|
99
|
-
return (
|
|
100
|
-
|
|
256
|
+
return Object.assign(
|
|
257
|
+
(raw: unknown, context?: SdkHostModelResolveContext): Promise<SdkHostModelResolution> =>
|
|
258
|
+
resolveSdkHostModel(raw, loadRegistry, context),
|
|
259
|
+
{ dispose: loadRegistry.dispose },
|
|
260
|
+
);
|
|
101
261
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createSdkRunCapability } from "../../session/sdk-run-capability-internal";
|