claude-code-rust 0.12.1 → 0.12.3
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 +3 -7
- package/agent-sdk/README.md +1 -1
- package/agent-sdk/dist/bridge/account_metadata.js +44 -0
- package/agent-sdk/dist/bridge/available_commands.js +129 -0
- package/agent-sdk/dist/bridge/commands.js +58 -36
- package/agent-sdk/dist/bridge/error_classification.js +20 -7
- package/agent-sdk/dist/bridge/events.js +18 -0
- package/agent-sdk/dist/bridge/history.js +183 -11
- package/agent-sdk/dist/bridge/logger.js +3 -0
- package/agent-sdk/dist/bridge/mcp.js +49 -79
- package/agent-sdk/dist/bridge/mcp_metadata.js +369 -0
- package/agent-sdk/dist/bridge/message_handlers.js +401 -57
- package/agent-sdk/dist/bridge/model_metadata.js +228 -0
- package/agent-sdk/dist/bridge/session_lifecycle.js +197 -326
- package/agent-sdk/dist/bridge/state_parsing.js +7 -1
- package/agent-sdk/dist/bridge/task_links.js +34 -0
- package/agent-sdk/dist/bridge/tasks.js +862 -0
- package/agent-sdk/dist/bridge/tool_calls.js +88 -31
- package/agent-sdk/dist/bridge/tooling.js +1278 -42
- package/agent-sdk/dist/bridge.js +96 -44
- package/agent-sdk/dist/bridge.test.js +3691 -252
- package/package.json +8 -3
- package/scripts/jscpd-warning-summary.mjs +132 -0
|
@@ -5,15 +5,21 @@ import { query, } from "@anthropic-ai/claude-agent-sdk";
|
|
|
5
5
|
import { bridgeLogger, LOG_TARGETS, logSdkStderrLine } from "./logger.js";
|
|
6
6
|
import { AsyncQueue } from "./shared.js";
|
|
7
7
|
import { permissionOptionsFromSuggestions, permissionResultFromOutcome, } from "./permissions.js";
|
|
8
|
-
import { failConnection, emitSessionUpdate, emitConnectEvent, emitPermissionRequestEvent, emitElicitationRequestEvent, } from "./events.js";
|
|
8
|
+
import { failConnection, emitSessionUpdate, emitConnectEvent, emitPermissionRequestEvent, emitElicitationRequestEvent, emitUserDialogRequestEvent, } from "./events.js";
|
|
9
9
|
import { ensureToolCallVisible, setToolCallStatus, } from "./tool_calls.js";
|
|
10
10
|
import { isToolSearchToolName } from "./tooling.js";
|
|
11
11
|
import { requestExitPlanModeApproval, requestAskUserQuestionAnswers, EXIT_PLAN_MODE_TOOL_NAME, ASK_USER_QUESTION_TOOL_NAME, } from "./user_interaction.js";
|
|
12
12
|
import { mapAvailableAgents, emitAvailableAgentsIfChanged, refreshAvailableAgents } from "./agents.js";
|
|
13
|
+
import { mapSdkSlashCommands, updateAvailableCommands, } from "./available_commands.js";
|
|
13
14
|
import { emitAuthRequired, emitFastModeUpdateIfChanged } from "./error_classification.js";
|
|
15
|
+
import { mapAvailableModels, resolveCurrentModel, currentModelsEqual, } from "./model_metadata.js";
|
|
16
|
+
import { shouldEmitStartupAuthRequiredForAccount } from "./account_metadata.js";
|
|
17
|
+
export { mapAvailableModels, resolveCurrentModel } from "./model_metadata.js";
|
|
18
|
+
export { shouldEmitStartupAuthRequiredForAccount } from "./account_metadata.js";
|
|
14
19
|
const BRIDGE_RUNTIME_PROCESS_NAME = process.platform === "win32" ? "claude-rs-bridge-node.exe" : "claude-rs-bridge-node";
|
|
15
20
|
const BRIDGE_RUNTIME_GUARD_PROMPT = `Do not terminate the Claude Rust bridge runtime process \`${BRIDGE_RUNTIME_PROCESS_NAME}\`; ` +
|
|
16
21
|
"when cleaning up development servers, only stop processes by explicit PIDs you started in this session.";
|
|
22
|
+
const STARTUP_FALLBACK_MODEL_ALIAS = "opus";
|
|
17
23
|
function permissionDisplayFromCanUseOptions(options) {
|
|
18
24
|
const title = typeof options.title === "string" ? options.title.trim() : "";
|
|
19
25
|
const displayName = typeof options.displayName === "string" ? options.displayName.trim() : "";
|
|
@@ -27,20 +33,8 @@ function permissionDisplayFromCanUseOptions(options) {
|
|
|
27
33
|
...(description ? { description } : {}),
|
|
28
34
|
};
|
|
29
35
|
}
|
|
30
|
-
const requestUserDialogInterceptorInstalled = Symbol("requestUserDialogInterceptorInstalled");
|
|
31
36
|
export const sessions = new Map();
|
|
32
|
-
function nonEmptyString(value) {
|
|
33
|
-
return typeof value === "string" && value.trim().length > 0;
|
|
34
|
-
}
|
|
35
|
-
export function shouldEmitStartupAuthRequiredForAccount(account) {
|
|
36
|
-
const provider = account.apiProvider;
|
|
37
|
-
if (nonEmptyString(provider) && provider !== "firstParty") {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
return !nonEmptyString(account.email) && !nonEmptyString(account.apiKeySource);
|
|
41
|
-
}
|
|
42
37
|
const DEFAULT_SETTING_SOURCES = ["user", "project", "local"];
|
|
43
|
-
const OPUS_MODEL_ALIAS = "opus";
|
|
44
38
|
const DEFAULT_PERMISSION_MODE = "default";
|
|
45
39
|
function isSdkElicitationContentValue(value) {
|
|
46
40
|
return (typeof value === "string" ||
|
|
@@ -60,6 +54,48 @@ function normalizeSdkElicitationContent(content) {
|
|
|
60
54
|
}
|
|
61
55
|
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
62
56
|
}
|
|
57
|
+
const REFUSAL_FALLBACK_DIALOG_KIND = "refusal_fallback_prompt";
|
|
58
|
+
function optionalString(value) {
|
|
59
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
60
|
+
}
|
|
61
|
+
function optionalStringArray(value) {
|
|
62
|
+
if (!Array.isArray(value)) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
const entries = value.filter((entry) => typeof entry === "string");
|
|
66
|
+
return entries.length > 0 ? entries : undefined;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Normalize the camelCase `refusal_fallback_prompt` payload built by the CLI
|
|
70
|
+
* into the snake-case host wire shape. The dialog descriptor requires
|
|
71
|
+
* `originalModel`/`fallbackModel`; the rest are optional metadata.
|
|
72
|
+
*/
|
|
73
|
+
function normalizeRefusalFallbackPayload(payload) {
|
|
74
|
+
return {
|
|
75
|
+
original_model: typeof payload.originalModel === "string" ? payload.originalModel : "",
|
|
76
|
+
fallback_model: typeof payload.fallbackModel === "string" ? payload.fallbackModel : "",
|
|
77
|
+
...(optionalString(payload.apiRefusalCategory) !== undefined
|
|
78
|
+
? { api_refusal_category: optionalString(payload.apiRefusalCategory) }
|
|
79
|
+
: {}),
|
|
80
|
+
...(optionalString(payload.guidanceText) !== undefined
|
|
81
|
+
? { guidance_text: optionalString(payload.guidanceText) }
|
|
82
|
+
: {}),
|
|
83
|
+
...(optionalStringArray(payload.retractedMessageUuids) !== undefined
|
|
84
|
+
? { retracted_message_uuids: optionalStringArray(payload.retractedMessageUuids) }
|
|
85
|
+
: {}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Build the selectable options the host renders, mirroring the labels the CLI
|
|
90
|
+
* itself produces (`function Wg$`). `cancelled` is the Esc/decline default and
|
|
91
|
+
* is not a listed option.
|
|
92
|
+
*/
|
|
93
|
+
function buildRefusalFallbackOptions(payload) {
|
|
94
|
+
return [
|
|
95
|
+
{ option_id: "retry_fallback", label: `Switch to ${payload.fallback_model}` },
|
|
96
|
+
{ option_id: "edit_prompt", label: `Edit prompt and retry with ${payload.original_model}` },
|
|
97
|
+
];
|
|
98
|
+
}
|
|
63
99
|
function settingsObjectFromLaunchSettings(launchSettings) {
|
|
64
100
|
return launchSettings.settings;
|
|
65
101
|
}
|
|
@@ -93,77 +129,6 @@ export function updateSessionId(session, newSessionId) {
|
|
|
93
129
|
session.sessionId = newSessionId;
|
|
94
130
|
sessions.set(newSessionId, session);
|
|
95
131
|
}
|
|
96
|
-
function isRequestUserDialogControlRequest(value) {
|
|
97
|
-
if (!value || typeof value !== "object") {
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
const record = value;
|
|
101
|
-
const request = record.request;
|
|
102
|
-
if (!request || typeof request !== "object") {
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
const inner = request;
|
|
106
|
-
const payload = inner.payload;
|
|
107
|
-
return (typeof record.request_id === "string" &&
|
|
108
|
-
inner.subtype === "request_user_dialog" &&
|
|
109
|
-
typeof inner.dialog_kind === "string" &&
|
|
110
|
-
Boolean(payload && typeof payload === "object" && !Array.isArray(payload)));
|
|
111
|
-
}
|
|
112
|
-
export function attachRequestUserDialogInterceptor(query, sessionIdForLogs) {
|
|
113
|
-
const internalQuery = query;
|
|
114
|
-
if (internalQuery[requestUserDialogInterceptorInstalled]) {
|
|
115
|
-
return true;
|
|
116
|
-
}
|
|
117
|
-
if (typeof internalQuery.processControlRequest !== "function") {
|
|
118
|
-
bridgeLogger.warn({
|
|
119
|
-
target: LOG_TARGETS.APP_SESSION,
|
|
120
|
-
eventName: "request_user_dialog_interceptor_unavailable",
|
|
121
|
-
message: "request_user_dialog interceptor could not be installed",
|
|
122
|
-
outcome: "failure",
|
|
123
|
-
sessionId: sessionIdForLogs(),
|
|
124
|
-
});
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
|
-
const originalProcessControlRequest = internalQuery.processControlRequest.bind(query);
|
|
128
|
-
internalQuery.processControlRequest = async (request, signal) => {
|
|
129
|
-
// SDK 0.2.104 also added cancel_async_message and seed_read_state control
|
|
130
|
-
// requests. Keep those delegated to the SDK internals: claude-rs does not
|
|
131
|
-
// own the SDK async-message queue or read-state cache, so TUI-level commands
|
|
132
|
-
// for them would add unsupported host behavior without user-visible value.
|
|
133
|
-
if (isRequestUserDialogControlRequest(request)) {
|
|
134
|
-
bridgeLogger.warn({
|
|
135
|
-
target: LOG_TARGETS.APP_SESSION,
|
|
136
|
-
eventName: "request_user_dialog_received",
|
|
137
|
-
message: "request_user_dialog control request received",
|
|
138
|
-
outcome: "failure",
|
|
139
|
-
sessionId: sessionIdForLogs(),
|
|
140
|
-
requestId: request.request_id,
|
|
141
|
-
...(typeof request.request.tool_use_id === "string"
|
|
142
|
-
? { toolCallId: request.request.tool_use_id }
|
|
143
|
-
: {}),
|
|
144
|
-
fields: {
|
|
145
|
-
dialog_kind: request.request.dialog_kind,
|
|
146
|
-
raw_payload: request.request.payload,
|
|
147
|
-
raw_request: request.request,
|
|
148
|
-
},
|
|
149
|
-
});
|
|
150
|
-
// TODO(request_user_dialog): Revisit this when a real claude-rs host flow needs it.
|
|
151
|
-
// For now we only log the full control request and reject it explicitly because
|
|
152
|
-
// normal TUI sessions do not appear to exercise these dialog kinds.
|
|
153
|
-
throw new Error(`request_user_dialog is not supported by claude-rs yet (dialog_kind: ${request.request.dialog_kind})`);
|
|
154
|
-
}
|
|
155
|
-
return await originalProcessControlRequest(request, signal);
|
|
156
|
-
};
|
|
157
|
-
internalQuery[requestUserDialogInterceptorInstalled] = true;
|
|
158
|
-
bridgeLogger.info({
|
|
159
|
-
target: LOG_TARGETS.APP_SESSION,
|
|
160
|
-
eventName: "request_user_dialog_interceptor_installed",
|
|
161
|
-
message: "request_user_dialog interceptor installed",
|
|
162
|
-
outcome: "success",
|
|
163
|
-
sessionId: sessionIdForLogs(),
|
|
164
|
-
});
|
|
165
|
-
return true;
|
|
166
|
-
}
|
|
167
132
|
export async function closeSession(session) {
|
|
168
133
|
session.input.close();
|
|
169
134
|
session.query.close();
|
|
@@ -176,6 +141,10 @@ export async function closeSession(session) {
|
|
|
176
141
|
pending.onOutcome({ outcome: "cancelled" });
|
|
177
142
|
}
|
|
178
143
|
session.pendingQuestions.clear();
|
|
144
|
+
for (const pending of session.pendingUserDialogs.values()) {
|
|
145
|
+
pending.resolve("cancelled");
|
|
146
|
+
}
|
|
147
|
+
session.pendingUserDialogs.clear();
|
|
179
148
|
for (const pending of session.pendingElicitations.values()) {
|
|
180
149
|
pending.resolve({ action: "cancel" });
|
|
181
150
|
}
|
|
@@ -305,7 +274,6 @@ export async function createSession(params) {
|
|
|
305
274
|
sessionIdForLogs,
|
|
306
275
|
}),
|
|
307
276
|
});
|
|
308
|
-
attachRequestUserDialogInterceptor(queryHandle, sessionIdForLogs);
|
|
309
277
|
}
|
|
310
278
|
catch (error) {
|
|
311
279
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -343,9 +311,13 @@ export async function createSession(params) {
|
|
|
343
311
|
connectEvent: params.connectEvent,
|
|
344
312
|
connectRequestId: params.requestId,
|
|
345
313
|
toolCalls: new Map(),
|
|
314
|
+
tasksById: new Map(),
|
|
315
|
+
taskOrder: [],
|
|
346
316
|
taskToolUseIds: new Map(),
|
|
317
|
+
taskIdsByToolUseId: new Map(),
|
|
347
318
|
pendingPermissions: new Map(),
|
|
348
319
|
pendingQuestions: new Map(),
|
|
320
|
+
pendingUserDialogs: new Map(),
|
|
349
321
|
pendingElicitations: new Map(),
|
|
350
322
|
mcpStatusRevalidatedAt: new Map(),
|
|
351
323
|
hiddenToolUseIds: new Set(),
|
|
@@ -430,16 +402,7 @@ export async function createSession(params) {
|
|
|
430
402
|
emitAuthRequired(session);
|
|
431
403
|
}
|
|
432
404
|
emitFastModeUpdateIfChanged(session, result.fast_mode_state);
|
|
433
|
-
|
|
434
|
-
? result.commands.map((command) => ({
|
|
435
|
-
name: command.name,
|
|
436
|
-
description: command.description ?? "",
|
|
437
|
-
input_hint: command.argumentHint ?? undefined,
|
|
438
|
-
}))
|
|
439
|
-
: [];
|
|
440
|
-
if (commands.length > 0) {
|
|
441
|
-
emitSessionUpdate(session.sessionId, { type: "available_commands_update", commands });
|
|
442
|
-
}
|
|
405
|
+
updateAvailableCommands(session, "session_result_commands", mapSdkSlashCommands(result.commands));
|
|
443
406
|
emitAvailableAgentsIfChanged(session, mapAvailableAgents(result.agents));
|
|
444
407
|
refreshAvailableAgents(session);
|
|
445
408
|
})
|
|
@@ -555,7 +518,7 @@ function permissionModeFromSettingsValue(rawMode) {
|
|
|
555
518
|
function initialSessionModel(launchSettings) {
|
|
556
519
|
const settings = settingsObjectFromLaunchSettings(launchSettings);
|
|
557
520
|
const model = typeof settings?.model === "string" ? settings.model.trim() : "";
|
|
558
|
-
return model ||
|
|
521
|
+
return model || STARTUP_FALLBACK_MODEL_ALIAS;
|
|
559
522
|
}
|
|
560
523
|
function startupModelOption(launchSettings) {
|
|
561
524
|
const settings = settingsObjectFromLaunchSettings(launchSettings);
|
|
@@ -721,40 +684,92 @@ export function buildQueryOptions(params) {
|
|
|
721
684
|
});
|
|
722
685
|
});
|
|
723
686
|
},
|
|
687
|
+
// The SDK "fails closed" and never emits a dialog kind unless it is declared
|
|
688
|
+
// here. We declare the one refusal-related kind we render: when the API
|
|
689
|
+
// returns a hard `stop_reason: "refusal"` and a fallback model is configured,
|
|
690
|
+
// the CLI emits `request_user_dialog` and the host renders the chooser below.
|
|
691
|
+
supportedDialogKinds: [REFUSAL_FALLBACK_DIALOG_KIND],
|
|
692
|
+
// Host policy for `request_user_dialog` control requests. Unknown kinds are
|
|
693
|
+
// logged and answered `{ behavior: "cancelled" }` (the spec-required answer
|
|
694
|
+
// for unrecognized kinds). For `refusal_fallback_prompt`, we surface an
|
|
695
|
+
// interactive chooser in the TUI and route the user's decision back to the
|
|
696
|
+
// CLI as `{ behavior: "completed", result: <choice> }` (or cancelled on
|
|
697
|
+
// decline/abort/teardown).
|
|
698
|
+
onUserDialog: async (request, options) => {
|
|
699
|
+
if (request.dialogKind !== REFUSAL_FALLBACK_DIALOG_KIND) {
|
|
700
|
+
bridgeLogger.warn({
|
|
701
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
702
|
+
eventName: "user_dialog_received",
|
|
703
|
+
message: "request_user_dialog received for unknown kind; cancelled",
|
|
704
|
+
outcome: "cancelled",
|
|
705
|
+
sessionId: params.sessionIdForLogs(),
|
|
706
|
+
...(typeof request.toolUseID === "string" ? { toolCallId: request.toolUseID } : {}),
|
|
707
|
+
fields: { dialog_kind: request.dialogKind },
|
|
708
|
+
});
|
|
709
|
+
return { behavior: "cancelled" };
|
|
710
|
+
}
|
|
711
|
+
const payload = normalizeRefusalFallbackPayload(request.payload);
|
|
712
|
+
const requestId = randomUUID();
|
|
713
|
+
const dialogRequest = {
|
|
714
|
+
request_id: requestId,
|
|
715
|
+
dialog_kind: REFUSAL_FALLBACK_DIALOG_KIND,
|
|
716
|
+
payload,
|
|
717
|
+
options: buildRefusalFallbackOptions(payload),
|
|
718
|
+
};
|
|
719
|
+
bridgeLogger.info({
|
|
720
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
721
|
+
eventName: "user_dialog_received",
|
|
722
|
+
message: "request_user_dialog received; awaiting host decision",
|
|
723
|
+
outcome: "start",
|
|
724
|
+
sessionId: params.sessionIdForLogs(),
|
|
725
|
+
requestId,
|
|
726
|
+
...(typeof request.toolUseID === "string" ? { toolCallId: request.toolUseID } : {}),
|
|
727
|
+
fields: {
|
|
728
|
+
dialog_kind: request.dialogKind,
|
|
729
|
+
original_model: payload.original_model,
|
|
730
|
+
fallback_model: payload.fallback_model,
|
|
731
|
+
},
|
|
732
|
+
});
|
|
733
|
+
const choice = await new Promise((resolve) => {
|
|
734
|
+
const currentSession = sessions.get(params.sessionIdForLogs());
|
|
735
|
+
if (!currentSession) {
|
|
736
|
+
bridgeLogger.warn({
|
|
737
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
738
|
+
eventName: "user_dialog_request_dropped",
|
|
739
|
+
message: "user dialog request dropped without an active session",
|
|
740
|
+
outcome: "dropped",
|
|
741
|
+
sessionId: params.sessionIdForLogs(),
|
|
742
|
+
requestId,
|
|
743
|
+
fields: { reason: "unknown_session" },
|
|
744
|
+
});
|
|
745
|
+
resolve("cancelled");
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
let settled = false;
|
|
749
|
+
const settle = (value) => {
|
|
750
|
+
if (settled) {
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
settled = true;
|
|
754
|
+
currentSession.pendingUserDialogs.delete(requestId);
|
|
755
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
756
|
+
resolve(value);
|
|
757
|
+
};
|
|
758
|
+
const onAbort = () => settle("cancelled");
|
|
759
|
+
if (options.signal.aborted) {
|
|
760
|
+
settle("cancelled");
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
options.signal.addEventListener("abort", onAbort);
|
|
764
|
+
currentSession.pendingUserDialogs.set(requestId, { resolve: settle });
|
|
765
|
+
emitUserDialogRequestEvent(params.sessionIdForLogs(), dialogRequest);
|
|
766
|
+
});
|
|
767
|
+
return choice === "cancelled"
|
|
768
|
+
? { behavior: "cancelled" }
|
|
769
|
+
: { behavior: "completed", result: choice };
|
|
770
|
+
},
|
|
724
771
|
};
|
|
725
772
|
}
|
|
726
|
-
export function mapAvailableModels(models) {
|
|
727
|
-
if (!Array.isArray(models)) {
|
|
728
|
-
return [];
|
|
729
|
-
}
|
|
730
|
-
return models
|
|
731
|
-
.filter((entry) => {
|
|
732
|
-
return (typeof entry?.value === "string" &&
|
|
733
|
-
entry.value.trim().length > 0 &&
|
|
734
|
-
typeof entry.displayName === "string" &&
|
|
735
|
-
entry.displayName.trim().length > 0);
|
|
736
|
-
})
|
|
737
|
-
.map((entry) => ({
|
|
738
|
-
id: entry.value,
|
|
739
|
-
display_name: entry.displayName,
|
|
740
|
-
supports_effort: entry.supportsEffort === true,
|
|
741
|
-
supported_effort_levels: Array.isArray(entry.supportedEffortLevels)
|
|
742
|
-
? entry.supportedEffortLevels.filter((level) => level === "low" || level === "medium" || level === "high")
|
|
743
|
-
: [],
|
|
744
|
-
...(typeof entry.supportsAdaptiveThinking === "boolean"
|
|
745
|
-
? { supports_adaptive_thinking: entry.supportsAdaptiveThinking }
|
|
746
|
-
: {}),
|
|
747
|
-
...(typeof entry.supportsFastMode === "boolean"
|
|
748
|
-
? { supports_fast_mode: entry.supportsFastMode }
|
|
749
|
-
: {}),
|
|
750
|
-
...(typeof entry.supportsAutoMode === "boolean"
|
|
751
|
-
? { supports_auto_mode: entry.supportsAutoMode }
|
|
752
|
-
: {}),
|
|
753
|
-
...(typeof entry.description === "string" && entry.description.trim().length > 0
|
|
754
|
-
? { description: entry.description }
|
|
755
|
-
: {}),
|
|
756
|
-
}));
|
|
757
|
-
}
|
|
758
773
|
export function handlePermissionResponse(command) {
|
|
759
774
|
bridgeLogger.info({
|
|
760
775
|
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
@@ -908,6 +923,60 @@ export function handleQuestionResponse(command) {
|
|
|
908
923
|
});
|
|
909
924
|
resolver.onOutcome(command.outcome);
|
|
910
925
|
}
|
|
926
|
+
export function handleUserDialogResponse(command) {
|
|
927
|
+
bridgeLogger.info({
|
|
928
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
929
|
+
eventName: "user_dialog_response_received",
|
|
930
|
+
message: "user dialog response received",
|
|
931
|
+
outcome: "success",
|
|
932
|
+
sessionId: command.session_id,
|
|
933
|
+
requestId: command.request_id,
|
|
934
|
+
fields: {
|
|
935
|
+
response_kind: command.outcome.outcome,
|
|
936
|
+
selected_option: command.outcome.outcome === "selected" ? command.outcome.option_id : "cancelled",
|
|
937
|
+
},
|
|
938
|
+
});
|
|
939
|
+
const session = sessionById(command.session_id);
|
|
940
|
+
if (!session) {
|
|
941
|
+
bridgeLogger.warn({
|
|
942
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
943
|
+
eventName: "user_dialog_response_dropped",
|
|
944
|
+
message: "user dialog response dropped for unknown session",
|
|
945
|
+
outcome: "dropped",
|
|
946
|
+
sessionId: command.session_id,
|
|
947
|
+
requestId: command.request_id,
|
|
948
|
+
fields: { reason: "unknown_session" },
|
|
949
|
+
});
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
const pending = session.pendingUserDialogs.get(command.request_id);
|
|
953
|
+
if (!pending) {
|
|
954
|
+
// Idempotent: a late or duplicate response for an already-resolved request
|
|
955
|
+
// (e.g. the dialog was cancelled on abort/teardown first) is a no-op.
|
|
956
|
+
bridgeLogger.warn({
|
|
957
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
958
|
+
eventName: "user_dialog_response_dropped",
|
|
959
|
+
message: "user dialog response dropped without a pending request",
|
|
960
|
+
outcome: "dropped",
|
|
961
|
+
sessionId: command.session_id,
|
|
962
|
+
requestId: command.request_id,
|
|
963
|
+
fields: { reason: "missing_pending_request" },
|
|
964
|
+
});
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
session.pendingUserDialogs.delete(command.request_id);
|
|
968
|
+
const choice = command.outcome.outcome === "selected" ? command.outcome.option_id : "cancelled";
|
|
969
|
+
bridgeLogger.info({
|
|
970
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
971
|
+
eventName: "user_dialog_response_applied",
|
|
972
|
+
message: "user dialog response applied",
|
|
973
|
+
outcome: "success",
|
|
974
|
+
sessionId: command.session_id,
|
|
975
|
+
requestId: command.request_id,
|
|
976
|
+
fields: { choice },
|
|
977
|
+
});
|
|
978
|
+
pending.resolve(choice);
|
|
979
|
+
}
|
|
911
980
|
export function handleElicitationResponse(command) {
|
|
912
981
|
bridgeLogger.info({
|
|
913
982
|
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
@@ -968,208 +1037,10 @@ export function handleElicitationResponse(command) {
|
|
|
968
1037
|
} : {}),
|
|
969
1038
|
});
|
|
970
1039
|
}
|
|
971
|
-
const MAX_MODEL_VERSION_PARTS = 2;
|
|
972
|
-
const RELEASE_BUILD_TOKEN = /^20\d{6}$/;
|
|
973
|
-
function normalizeModelKey(id) {
|
|
974
|
-
const original = id.trim();
|
|
975
|
-
if (!original) {
|
|
976
|
-
return { original, family: "unknown", versionParts: [], variantParts: [], buildParts: [] };
|
|
977
|
-
}
|
|
978
|
-
const lower = original.toLowerCase();
|
|
979
|
-
const contextMatch = lower.match(/\[([^\]]+)\]$/);
|
|
980
|
-
const contextSuffix = contextMatch?.[1];
|
|
981
|
-
const withoutContext = contextMatch ? lower.slice(0, contextMatch.index) : lower;
|
|
982
|
-
const withoutPrefix = withoutContext.startsWith("claude-")
|
|
983
|
-
? withoutContext.slice("claude-".length)
|
|
984
|
-
: withoutContext;
|
|
985
|
-
const parts = withoutPrefix.split("-").filter((part) => part.length > 0);
|
|
986
|
-
const familyPart = parts[0] ?? "";
|
|
987
|
-
const family = familyPart === "opus" || familyPart === "sonnet" || familyPart === "haiku"
|
|
988
|
-
? familyPart
|
|
989
|
-
: "unknown";
|
|
990
|
-
const versionParts = [];
|
|
991
|
-
const variantParts = [];
|
|
992
|
-
const buildParts = [];
|
|
993
|
-
if (family !== "unknown") {
|
|
994
|
-
for (const part of parts.slice(1)) {
|
|
995
|
-
if (/^\d+$/.test(part)) {
|
|
996
|
-
if (versionParts.length < MAX_MODEL_VERSION_PARTS) {
|
|
997
|
-
const parsed = Number.parseInt(part, 10);
|
|
998
|
-
if (Number.isFinite(parsed)) {
|
|
999
|
-
versionParts.push(parsed);
|
|
1000
|
-
}
|
|
1001
|
-
continue;
|
|
1002
|
-
}
|
|
1003
|
-
if (RELEASE_BUILD_TOKEN.test(part)) {
|
|
1004
|
-
buildParts.push(part);
|
|
1005
|
-
continue;
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
variantParts.push(part);
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
return {
|
|
1012
|
-
original,
|
|
1013
|
-
family,
|
|
1014
|
-
versionParts,
|
|
1015
|
-
variantParts,
|
|
1016
|
-
buildParts,
|
|
1017
|
-
...(contextSuffix ? { contextSuffix } : {}),
|
|
1018
|
-
};
|
|
1019
|
-
}
|
|
1020
|
-
function modelKeysAreCompatible(leftId, rightId) {
|
|
1021
|
-
const left = normalizeModelKey(leftId);
|
|
1022
|
-
const right = normalizeModelKey(rightId);
|
|
1023
|
-
if (left.family === "unknown" || right.family === "unknown") {
|
|
1024
|
-
return left.original.toLowerCase() === right.original.toLowerCase();
|
|
1025
|
-
}
|
|
1026
|
-
if (left.family !== right.family) {
|
|
1027
|
-
return false;
|
|
1028
|
-
}
|
|
1029
|
-
if (left.variantParts.join(".") !== right.variantParts.join(".")) {
|
|
1030
|
-
return false;
|
|
1031
|
-
}
|
|
1032
|
-
if (left.versionParts.length === 0 || right.versionParts.length === 0) {
|
|
1033
|
-
return true;
|
|
1034
|
-
}
|
|
1035
|
-
return left.versionParts.join(".") === right.versionParts.join(".");
|
|
1036
|
-
}
|
|
1037
|
-
function sameContextSuffix(leftId, rightId) {
|
|
1038
|
-
const left = normalizeModelKey(leftId);
|
|
1039
|
-
const right = normalizeModelKey(rightId);
|
|
1040
|
-
return (left.contextSuffix?.toLowerCase() ?? "") === (right.contextSuffix?.toLowerCase() ?? "");
|
|
1041
|
-
}
|
|
1042
|
-
function sameFamilyAndVersion(leftId, rightId) {
|
|
1043
|
-
const left = normalizeModelKey(leftId);
|
|
1044
|
-
const right = normalizeModelKey(rightId);
|
|
1045
|
-
if (left.family === "unknown" || right.family === "unknown") {
|
|
1046
|
-
return left.original.toLowerCase() === right.original.toLowerCase();
|
|
1047
|
-
}
|
|
1048
|
-
if (left.family !== right.family) {
|
|
1049
|
-
return false;
|
|
1050
|
-
}
|
|
1051
|
-
if (left.versionParts.length === 0 || right.versionParts.length === 0) {
|
|
1052
|
-
return left.versionParts.length === right.versionParts.length;
|
|
1053
|
-
}
|
|
1054
|
-
return left.versionParts.join(".") === right.versionParts.join(".");
|
|
1055
|
-
}
|
|
1056
|
-
function hasVariantSiblingConflict(availableModels, candidateId, resolvedId) {
|
|
1057
|
-
if (sameContextSuffix(candidateId, resolvedId)) {
|
|
1058
|
-
return false;
|
|
1059
|
-
}
|
|
1060
|
-
const resolvedContext = normalizeModelKey(resolvedId).contextSuffix?.toLowerCase() ?? "";
|
|
1061
|
-
if (!resolvedContext) {
|
|
1062
|
-
return false;
|
|
1063
|
-
}
|
|
1064
|
-
return availableModels.some((entry) => {
|
|
1065
|
-
if (entry.id === candidateId) {
|
|
1066
|
-
return false;
|
|
1067
|
-
}
|
|
1068
|
-
if (!sameFamilyAndVersion(entry.id, resolvedId)) {
|
|
1069
|
-
return false;
|
|
1070
|
-
}
|
|
1071
|
-
const entryContext = normalizeModelKey(entry.id).contextSuffix?.toLowerCase() ?? "";
|
|
1072
|
-
return entryContext === resolvedContext;
|
|
1073
|
-
});
|
|
1074
|
-
}
|
|
1075
|
-
function humanizeModelId(id) {
|
|
1076
|
-
const normalized = normalizeModelKey(id);
|
|
1077
|
-
if (normalized.family === "unknown") {
|
|
1078
|
-
return id;
|
|
1079
|
-
}
|
|
1080
|
-
const familyLabel = normalized.family === "opus"
|
|
1081
|
-
? "Opus"
|
|
1082
|
-
: normalized.family === "sonnet"
|
|
1083
|
-
? "Sonnet"
|
|
1084
|
-
: "Haiku";
|
|
1085
|
-
const versionLabel = normalized.versionParts.length > 0 ? ` ${normalized.versionParts.join(".")}` : "";
|
|
1086
|
-
const contextLabel = normalized.contextSuffix?.toLowerCase() === "1m"
|
|
1087
|
-
? " [1M]"
|
|
1088
|
-
: normalized.contextSuffix
|
|
1089
|
-
? ` [${normalized.contextSuffix}]`
|
|
1090
|
-
: "";
|
|
1091
|
-
return `${familyLabel}${versionLabel}${contextLabel}`;
|
|
1092
|
-
}
|
|
1093
|
-
function shortDisplayNameForModelId(id) {
|
|
1094
|
-
const normalized = normalizeModelKey(id);
|
|
1095
|
-
if (normalized.family === "unknown") {
|
|
1096
|
-
return id;
|
|
1097
|
-
}
|
|
1098
|
-
const familyLabel = normalized.family === "opus"
|
|
1099
|
-
? "Opus"
|
|
1100
|
-
: normalized.family === "sonnet"
|
|
1101
|
-
? "Sonnet"
|
|
1102
|
-
: "Haiku";
|
|
1103
|
-
const versionLabel = normalized.versionParts.length > 0 ? ` ${normalized.versionParts.join(".")}` : "";
|
|
1104
|
-
const contextLabel = normalized.contextSuffix?.toLowerCase() === "1m"
|
|
1105
|
-
? " [1M]"
|
|
1106
|
-
: normalized.contextSuffix
|
|
1107
|
-
? ` [${normalized.contextSuffix}]`
|
|
1108
|
-
: "";
|
|
1109
|
-
return `${familyLabel}${versionLabel}${contextLabel}`;
|
|
1110
|
-
}
|
|
1111
|
-
function currentModelIsAuthoritative(resolvedId, requestedId) {
|
|
1112
|
-
const resolved = resolvedId.trim();
|
|
1113
|
-
if (!resolved || resolved === "Connecting...") {
|
|
1114
|
-
return Boolean(requestedId?.trim());
|
|
1115
|
-
}
|
|
1116
|
-
return true;
|
|
1117
|
-
}
|
|
1118
|
-
function resolveCatalogModel(availableModels, resolvedId, requestedId) {
|
|
1119
|
-
const exactResolved = availableModels.find((entry) => entry.id === resolvedId);
|
|
1120
|
-
if (exactResolved) {
|
|
1121
|
-
return exactResolved;
|
|
1122
|
-
}
|
|
1123
|
-
if (requestedId) {
|
|
1124
|
-
const exactRequested = availableModels.find((entry) => entry.id === requestedId);
|
|
1125
|
-
if (exactRequested &&
|
|
1126
|
-
modelKeysAreCompatible(exactRequested.id, resolvedId) &&
|
|
1127
|
-
!hasVariantSiblingConflict(availableModels, exactRequested.id, resolvedId)) {
|
|
1128
|
-
return exactRequested;
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
const compatible = availableModels.filter((entry) => modelKeysAreCompatible(entry.id, resolvedId) &&
|
|
1132
|
-
!hasVariantSiblingConflict(availableModels, entry.id, resolvedId));
|
|
1133
|
-
return compatible.length === 1 ? compatible[0] : undefined;
|
|
1134
|
-
}
|
|
1135
|
-
export function resolveCurrentModel(session) {
|
|
1136
|
-
const requestedId = session.requestedModelId?.trim() || undefined;
|
|
1137
|
-
const resolvedId = session.resolvedRuntimeModelId?.trim() ||
|
|
1138
|
-
session.model.trim() ||
|
|
1139
|
-
requestedId ||
|
|
1140
|
-
OPUS_MODEL_ALIAS;
|
|
1141
|
-
const catalogModel = resolveCatalogModel(session.availableModels, resolvedId, requestedId);
|
|
1142
|
-
const runtimeDisplayId = resolvedId || requestedId || OPUS_MODEL_ALIAS;
|
|
1143
|
-
const displayNameShort = shortDisplayNameForModelId(runtimeDisplayId);
|
|
1144
|
-
const displayNameLong = humanizeModelId(runtimeDisplayId);
|
|
1145
|
-
const currentModel = {
|
|
1146
|
-
resolved_id: resolvedId,
|
|
1147
|
-
display_name_short: displayNameShort,
|
|
1148
|
-
display_name_long: displayNameLong,
|
|
1149
|
-
supports_effort: catalogModel?.supports_effort === true,
|
|
1150
|
-
supported_effort_levels: catalogModel?.supported_effort_levels ?? [],
|
|
1151
|
-
is_authoritative: currentModelIsAuthoritative(resolvedId, requestedId),
|
|
1152
|
-
...(requestedId ? { requested_id: requestedId } : {}),
|
|
1153
|
-
...(catalogModel ? { catalog_id: catalogModel.id } : {}),
|
|
1154
|
-
...(catalogModel?.supports_fast_mode !== undefined
|
|
1155
|
-
? { supports_fast_mode: catalogModel.supports_fast_mode }
|
|
1156
|
-
: {}),
|
|
1157
|
-
...(catalogModel?.supports_auto_mode !== undefined
|
|
1158
|
-
? { supports_auto_mode: catalogModel.supports_auto_mode }
|
|
1159
|
-
: {}),
|
|
1160
|
-
...(catalogModel?.supports_adaptive_thinking !== undefined
|
|
1161
|
-
? { supports_adaptive_thinking: catalogModel.supports_adaptive_thinking }
|
|
1162
|
-
: {}),
|
|
1163
|
-
};
|
|
1164
|
-
return currentModel;
|
|
1165
|
-
}
|
|
1166
1040
|
export function shouldInvalidateResolvedRuntimeModel(previousRequestedId, previousSessionModel, nextRequestedId) {
|
|
1167
1041
|
const previousRequested = previousRequestedId?.trim() || previousSessionModel.trim();
|
|
1168
1042
|
return previousRequested !== nextRequestedId.trim();
|
|
1169
1043
|
}
|
|
1170
|
-
function currentModelsEqual(left, right) {
|
|
1171
|
-
return JSON.stringify(left) === JSON.stringify(right);
|
|
1172
|
-
}
|
|
1173
1044
|
export function emitCurrentModelUpdate(session) {
|
|
1174
1045
|
if (!session.connected || !session.currentModel) {
|
|
1175
1046
|
return false;
|
|
@@ -36,9 +36,12 @@ export function parseRuntimeSessionState(value) {
|
|
|
36
36
|
export function parseApiRetryError(value) {
|
|
37
37
|
switch (value) {
|
|
38
38
|
case "authentication_failed":
|
|
39
|
+
case "oauth_org_not_allowed":
|
|
39
40
|
case "billing_error":
|
|
40
41
|
case "rate_limit":
|
|
42
|
+
case "overloaded":
|
|
41
43
|
case "invalid_request":
|
|
44
|
+
case "model_not_found":
|
|
42
45
|
case "server_error":
|
|
43
46
|
case "max_output_tokens":
|
|
44
47
|
return value;
|
|
@@ -81,7 +84,10 @@ export function buildRateLimitUpdate(rateLimitInfo) {
|
|
|
81
84
|
if (typeof info.overageDisabledReason === "string" && info.overageDisabledReason.length > 0) {
|
|
82
85
|
update.overage_disabled_reason = info.overageDisabledReason;
|
|
83
86
|
}
|
|
84
|
-
if (typeof info.
|
|
87
|
+
if (typeof info.overageInUse === "boolean") {
|
|
88
|
+
update.is_using_overage = info.overageInUse;
|
|
89
|
+
}
|
|
90
|
+
else if (typeof info.isUsingOverage === "boolean") {
|
|
85
91
|
update.is_using_overage = info.isUsingOverage;
|
|
86
92
|
}
|
|
87
93
|
const surpassedThreshold = numberField(info, "surpassedThreshold");
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function linkTaskToolUse(session, taskId, toolUseId) {
|
|
2
|
+
if (!taskId || !toolUseId) {
|
|
3
|
+
return;
|
|
4
|
+
}
|
|
5
|
+
const previousToolUseId = session.taskToolUseIds.get(taskId);
|
|
6
|
+
if (previousToolUseId && previousToolUseId !== toolUseId) {
|
|
7
|
+
const reverseTaskId = session.taskIdsByToolUseId.get(previousToolUseId);
|
|
8
|
+
if (reverseTaskId === taskId) {
|
|
9
|
+
session.taskIdsByToolUseId.delete(previousToolUseId);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const previousTaskId = session.taskIdsByToolUseId.get(toolUseId);
|
|
13
|
+
if (previousTaskId && previousTaskId !== taskId) {
|
|
14
|
+
const forwardToolUseId = session.taskToolUseIds.get(previousTaskId);
|
|
15
|
+
if (forwardToolUseId === toolUseId) {
|
|
16
|
+
session.taskToolUseIds.delete(previousTaskId);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
session.taskToolUseIds.set(taskId, toolUseId);
|
|
20
|
+
session.taskIdsByToolUseId.set(toolUseId, taskId);
|
|
21
|
+
}
|
|
22
|
+
export function unlinkTaskToolUse(session, taskId) {
|
|
23
|
+
if (!taskId) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const toolUseId = session.taskToolUseIds.get(taskId);
|
|
27
|
+
session.taskToolUseIds.delete(taskId);
|
|
28
|
+
if (toolUseId && session.taskIdsByToolUseId.get(toolUseId) === taskId) {
|
|
29
|
+
session.taskIdsByToolUseId.delete(toolUseId);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function activeTaskIdForToolUse(session, toolUseId) {
|
|
33
|
+
return toolUseId ? session.taskIdsByToolUseId.get(toolUseId) : undefined;
|
|
34
|
+
}
|