claude-code-rust 0.12.0 → 0.12.2
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 +10 -6
- 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 +1262 -32
- package/agent-sdk/dist/bridge.js +95 -43
- package/agent-sdk/dist/bridge.test.js +3680 -269
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@ A native Rust terminal interface for Claude Code. Drop-in replacement for Anthro
|
|
|
5
5
|
[](https://www.npmjs.com/package/claude-code-rust)
|
|
6
6
|
[](https://www.npmjs.com/package/claude-code-rust)
|
|
7
7
|
[](https://github.com/srothgan/claude-code-rust/actions/workflows/ci.yml)
|
|
8
|
+
[](https://srothgan.github.io/claude-code-rust/)
|
|
8
9
|
[](https://www.apache.org/licenses/LICENSE-2.0)
|
|
9
10
|
[](https://nodejs.org/)
|
|
10
11
|
|
|
@@ -35,6 +36,8 @@ If `claude-rs` resolves to an older global shim, ensure your npm global bin dire
|
|
|
35
36
|
claude-rs
|
|
36
37
|
```
|
|
37
38
|
|
|
39
|
+
Full documentation is available at [srothgan.github.io/claude-code-rust](https://srothgan.github.io/claude-code-rust/).
|
|
40
|
+
|
|
38
41
|
> [!WARNING]
|
|
39
42
|
> **Agent SDK billing changes on June 15, 2026.** Anthropic says Agent SDK usage, `claude -p`, Claude Code GitHub Actions, and third-party Agent SDK apps will use a separate monthly Agent SDK credit instead of normal interactive Claude or Claude Code subscription limits. Because Claude Code Rust wraps the Agent SDK, treat usage through this project as Agent SDK usage. If that credit is exhausted, continued use may require enabling extra usage billed at standard API rates, or requests may pause until the credit refreshes.
|
|
40
43
|
>
|
|
@@ -54,14 +57,15 @@ The stock Claude Code TUI runs on Node.js with React Ink. This causes real probl
|
|
|
54
57
|
|
|
55
58
|
Claude Code Rust fixes all of these by compiling to a single native binary with direct terminal control via Crossterm.
|
|
56
59
|
|
|
57
|
-
##
|
|
60
|
+
## Documentation
|
|
58
61
|
|
|
59
|
-
|
|
62
|
+
The manual covers installation from npm and source, help, slash commands, keyboard shortcuts, settings, diagnostics, architecture, and the changelog:
|
|
60
63
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
- [Installation](https://srothgan.github.io/claude-code-rust/installation.html)
|
|
65
|
+
- [Usage](https://srothgan.github.io/claude-code-rust/usage.html)
|
|
66
|
+
- [Help](https://srothgan.github.io/claude-code-rust/help.html)
|
|
67
|
+
- [Slash commands](https://srothgan.github.io/claude-code-rust/commands.html)
|
|
68
|
+
- [Settings](https://srothgan.github.io/claude-code-rust/settings.html)
|
|
65
69
|
|
|
66
70
|
## Status
|
|
67
71
|
|
package/agent-sdk/README.md
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const KNOWN_API_PROVIDERS = new Set([
|
|
2
|
+
"firstParty",
|
|
3
|
+
"bedrock",
|
|
4
|
+
"vertex",
|
|
5
|
+
"foundry",
|
|
6
|
+
"anthropicAws",
|
|
7
|
+
"mantle",
|
|
8
|
+
"gateway",
|
|
9
|
+
]);
|
|
10
|
+
function trimmedString(value) {
|
|
11
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
12
|
+
}
|
|
13
|
+
export function isKnownApiProvider(provider) {
|
|
14
|
+
return provider !== undefined && KNOWN_API_PROVIDERS.has(provider);
|
|
15
|
+
}
|
|
16
|
+
export function apiProviderIsExternal(provider) {
|
|
17
|
+
return provider !== undefined && provider !== "firstParty";
|
|
18
|
+
}
|
|
19
|
+
export function mapSdkAccountInfo(account) {
|
|
20
|
+
const apiProvider = trimmedString(account.apiProvider);
|
|
21
|
+
return {
|
|
22
|
+
...(trimmedString(account.email) ? { email: trimmedString(account.email) } : {}),
|
|
23
|
+
...(trimmedString(account.organization)
|
|
24
|
+
? { organization: trimmedString(account.organization) }
|
|
25
|
+
: {}),
|
|
26
|
+
...(trimmedString(account.subscriptionType)
|
|
27
|
+
? { subscription_type: trimmedString(account.subscriptionType) }
|
|
28
|
+
: {}),
|
|
29
|
+
...(trimmedString(account.tokenSource)
|
|
30
|
+
? { token_source: trimmedString(account.tokenSource) }
|
|
31
|
+
: {}),
|
|
32
|
+
...(trimmedString(account.apiKeySource)
|
|
33
|
+
? { api_key_source: trimmedString(account.apiKeySource) }
|
|
34
|
+
: {}),
|
|
35
|
+
...(apiProvider ? { api_provider: apiProvider } : {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function shouldEmitStartupAuthRequiredForAccount(account) {
|
|
39
|
+
const provider = trimmedString(account.apiProvider);
|
|
40
|
+
if (apiProviderIsExternal(provider)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
return !trimmedString(account.email) && !trimmedString(account.apiKeySource);
|
|
44
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { emitSessionUpdate } from "./events.js";
|
|
2
|
+
import { bridgeLogger, LOG_TARGETS } from "./logger.js";
|
|
3
|
+
function isDynamicSource(source) {
|
|
4
|
+
return source === "commands_changed" || source === "reload_plugins";
|
|
5
|
+
}
|
|
6
|
+
function commandSignature(commands) {
|
|
7
|
+
return JSON.stringify(commands.map((command) => [command.name, command.description, command.input_hint ?? ""]));
|
|
8
|
+
}
|
|
9
|
+
function logAvailableCommandsDecision(session, source, commands, outcome, reason, generation) {
|
|
10
|
+
bridgeLogger.info({
|
|
11
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
12
|
+
eventName: outcome === "accepted"
|
|
13
|
+
? "available_commands_snapshot_accepted"
|
|
14
|
+
: "available_commands_snapshot_ignored",
|
|
15
|
+
message: outcome === "accepted"
|
|
16
|
+
? "available commands snapshot accepted"
|
|
17
|
+
: "available commands snapshot ignored",
|
|
18
|
+
outcome,
|
|
19
|
+
sessionId: session.sessionId,
|
|
20
|
+
count: commands.length,
|
|
21
|
+
fields: {
|
|
22
|
+
source,
|
|
23
|
+
previous_source: session.availableCommands?.source,
|
|
24
|
+
generation,
|
|
25
|
+
command_count: commands.length,
|
|
26
|
+
command_names: commands.map((command) => command.name),
|
|
27
|
+
reason,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function shouldAcceptAvailableCommandsSnapshot(session, source, commands) {
|
|
32
|
+
const current = session.availableCommands;
|
|
33
|
+
if (isDynamicSource(source)) {
|
|
34
|
+
return { accept: true, reason: "dynamic source is authoritative" };
|
|
35
|
+
}
|
|
36
|
+
if (!current) {
|
|
37
|
+
if (commands.length === 0) {
|
|
38
|
+
return { accept: false, reason: "empty bootstrap snapshot ignored before first command list" };
|
|
39
|
+
}
|
|
40
|
+
return { accept: true, reason: "initial command snapshot" };
|
|
41
|
+
}
|
|
42
|
+
if (current.dynamicSeen) {
|
|
43
|
+
return {
|
|
44
|
+
accept: false,
|
|
45
|
+
reason: "bootstrap source cannot replace dynamic command snapshot",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (source === "supportedCommands" && current.commands.length > 0) {
|
|
49
|
+
return {
|
|
50
|
+
accept: false,
|
|
51
|
+
reason: "supportedCommands is an initialize-time fallback and current snapshot already exists",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (commands.length === 0) {
|
|
55
|
+
return { accept: false, reason: "empty bootstrap snapshot ignored" };
|
|
56
|
+
}
|
|
57
|
+
return { accept: true, reason: "bootstrap refresh before dynamic command source" };
|
|
58
|
+
}
|
|
59
|
+
export function updateAvailableCommands(session, source, commands) {
|
|
60
|
+
const decision = shouldAcceptAvailableCommandsSnapshot(session, source, commands);
|
|
61
|
+
const currentGeneration = session.availableCommands?.generation ?? 0;
|
|
62
|
+
const nextGeneration = currentGeneration + 1;
|
|
63
|
+
if (!decision.accept) {
|
|
64
|
+
logAvailableCommandsDecision(session, source, commands, "ignored", decision.reason, currentGeneration);
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
const dynamicSeen = session.availableCommands?.dynamicSeen === true || isDynamicSource(source);
|
|
68
|
+
session.availableCommands = {
|
|
69
|
+
generation: nextGeneration,
|
|
70
|
+
source,
|
|
71
|
+
signature: commandSignature(commands),
|
|
72
|
+
dynamicSeen,
|
|
73
|
+
commands,
|
|
74
|
+
};
|
|
75
|
+
logAvailableCommandsDecision(session, source, commands, "accepted", decision.reason, nextGeneration);
|
|
76
|
+
bridgeLogger.info({
|
|
77
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
78
|
+
eventName: "available_commands_update_emitted",
|
|
79
|
+
message: "available commands update emitted",
|
|
80
|
+
outcome: "success",
|
|
81
|
+
sessionId: session.sessionId,
|
|
82
|
+
count: commands.length,
|
|
83
|
+
fields: {
|
|
84
|
+
source,
|
|
85
|
+
generation: nextGeneration,
|
|
86
|
+
command_count: commands.length,
|
|
87
|
+
command_names: commands.map((command) => command.name),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
emitSessionUpdate(session.sessionId, {
|
|
91
|
+
type: "available_commands_update",
|
|
92
|
+
commands,
|
|
93
|
+
source,
|
|
94
|
+
generation: nextGeneration,
|
|
95
|
+
});
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
export function mapSdkSlashCommand(command) {
|
|
99
|
+
if (!command || typeof command !== "object") {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const record = command;
|
|
103
|
+
const name = typeof record.name === "string" ? record.name : "";
|
|
104
|
+
if (!name) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
name,
|
|
109
|
+
description: typeof record.description === "string" ? record.description : "",
|
|
110
|
+
input_hint: typeof record.argumentHint === "string" ? record.argumentHint : undefined,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function mapSdkSlashCommands(commands) {
|
|
114
|
+
if (!Array.isArray(commands)) {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
return commands.flatMap((command) => {
|
|
118
|
+
const mapped = mapSdkSlashCommand(command);
|
|
119
|
+
return mapped ? [mapped] : [];
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
export function mapInitSlashCommands(commands) {
|
|
123
|
+
if (!Array.isArray(commands)) {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
return commands
|
|
127
|
+
.filter((entry) => typeof entry === "string")
|
|
128
|
+
.map((name) => ({ name, description: "", input_hint: undefined }));
|
|
129
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseMcpServersRecord } from "./mcp_metadata.js";
|
|
1
2
|
import { resolveCurrentModel } from "./session_lifecycle.js";
|
|
2
3
|
const MODE_NAMES = {
|
|
3
4
|
default: "Default",
|
|
@@ -83,6 +84,27 @@ function expectString(record, key, context) {
|
|
|
83
84
|
}
|
|
84
85
|
return value;
|
|
85
86
|
}
|
|
87
|
+
function expectEffortLevel(record, key, context) {
|
|
88
|
+
const value = expectString(record, key, context);
|
|
89
|
+
if (value !== "low" &&
|
|
90
|
+
value !== "medium" &&
|
|
91
|
+
value !== "high" &&
|
|
92
|
+
value !== "xhigh" &&
|
|
93
|
+
value !== "max") {
|
|
94
|
+
throw new Error(`${context}.${key} must be one of low, medium, high, xhigh, max`);
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
function expectNonEmptyStringOrNull(record, key, context) {
|
|
99
|
+
const value = record[key];
|
|
100
|
+
if (value === null) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
104
|
+
throw new Error(`${context}.${key} must be a non-empty string or null`);
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
86
108
|
function optionalString(record, key, context) {
|
|
87
109
|
const value = record[key];
|
|
88
110
|
if (value === undefined || value === null) {
|
|
@@ -162,33 +184,12 @@ function expectBoolean(record, key, context) {
|
|
|
162
184
|
}
|
|
163
185
|
return value;
|
|
164
186
|
}
|
|
165
|
-
function
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
case "stdio":
|
|
170
|
-
return {
|
|
171
|
-
type,
|
|
172
|
-
command: expectString(record, "command", context),
|
|
173
|
-
...(record.args === undefined ? {} : { args: expectStringArray(record, "args", context) }),
|
|
174
|
-
...(record.env === undefined ? {} : { env: expectStringMap(record, "env", context) }),
|
|
175
|
-
};
|
|
176
|
-
case "sse":
|
|
177
|
-
case "http":
|
|
178
|
-
return {
|
|
179
|
-
type,
|
|
180
|
-
url: expectString(record, "url", context),
|
|
181
|
-
...(record.headers === undefined
|
|
182
|
-
? {}
|
|
183
|
-
: { headers: expectStringMap(record, "headers", context) }),
|
|
184
|
-
};
|
|
185
|
-
default:
|
|
186
|
-
throw new Error(`${context}.type must be one of stdio, sse, http`);
|
|
187
|
+
function expectRefusalFallbackPromptChoice(record, key, context) {
|
|
188
|
+
const value = expectString(record, key, context);
|
|
189
|
+
if (value !== "retry_fallback" && value !== "edit_prompt") {
|
|
190
|
+
throw new Error(`${context}.${key} must be 'retry_fallback' or 'edit_prompt'`);
|
|
187
191
|
}
|
|
188
|
-
|
|
189
|
-
function parseMcpServersRecord(value, context) {
|
|
190
|
-
const record = asRecord(value, context);
|
|
191
|
-
return Object.fromEntries(Object.entries(record).map(([key, entry]) => [key, parseMcpServerConfig(entry, `${context}.${key}`)]));
|
|
192
|
+
return value;
|
|
192
193
|
}
|
|
193
194
|
export function parseCommandEnvelope(line) {
|
|
194
195
|
const raw = asRecord(JSON.parse(line), "command envelope");
|
|
@@ -246,6 +247,18 @@ export function parseCommandEnvelope(line) {
|
|
|
246
247
|
session_id: expectString(raw, "session_id", "set_mode"),
|
|
247
248
|
mode: expectString(raw, "mode", "set_mode"),
|
|
248
249
|
};
|
|
250
|
+
case "set_effort":
|
|
251
|
+
return {
|
|
252
|
+
command: "set_effort",
|
|
253
|
+
session_id: expectString(raw, "session_id", "set_effort"),
|
|
254
|
+
effort: expectEffortLevel(raw, "effort", "set_effort"),
|
|
255
|
+
};
|
|
256
|
+
case "set_agent":
|
|
257
|
+
return {
|
|
258
|
+
command: "set_agent",
|
|
259
|
+
session_id: expectString(raw, "session_id", "set_agent"),
|
|
260
|
+
agent: expectNonEmptyStringOrNull(raw, "agent", "set_agent"),
|
|
261
|
+
};
|
|
249
262
|
case "generate_session_title":
|
|
250
263
|
return {
|
|
251
264
|
command: "generate_session_title",
|
|
@@ -339,6 +352,25 @@ export function parseCommandEnvelope(line) {
|
|
|
339
352
|
outcome: parsedOutcome,
|
|
340
353
|
};
|
|
341
354
|
}
|
|
355
|
+
case "user_dialog_response": {
|
|
356
|
+
const outcome = asRecord(raw.outcome, "user_dialog_response.outcome");
|
|
357
|
+
const outcomeType = expectString(outcome, "outcome", "user_dialog_response.outcome");
|
|
358
|
+
if (outcomeType !== "selected" && outcomeType !== "cancelled") {
|
|
359
|
+
throw new Error("user_dialog_response.outcome.outcome must be 'selected' or 'cancelled'");
|
|
360
|
+
}
|
|
361
|
+
const parsedOutcome = outcomeType === "selected"
|
|
362
|
+
? {
|
|
363
|
+
outcome: "selected",
|
|
364
|
+
option_id: expectRefusalFallbackPromptChoice(outcome, "option_id", "user_dialog_response.outcome"),
|
|
365
|
+
}
|
|
366
|
+
: { outcome: "cancelled" };
|
|
367
|
+
return {
|
|
368
|
+
command: "user_dialog_response",
|
|
369
|
+
session_id: expectString(raw, "session_id", "user_dialog_response"),
|
|
370
|
+
request_id: expectString(raw, "request_id", "user_dialog_response"),
|
|
371
|
+
outcome: parsedOutcome,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
342
374
|
case "elicitation_response":
|
|
343
375
|
return {
|
|
344
376
|
command: "elicitation_response",
|
|
@@ -388,16 +420,6 @@ function expectStringArray(record, key, context) {
|
|
|
388
420
|
return entry;
|
|
389
421
|
});
|
|
390
422
|
}
|
|
391
|
-
function expectStringMap(record, key, context) {
|
|
392
|
-
const value = record[key];
|
|
393
|
-
const parsed = asRecord(value, `${context}.${key}`);
|
|
394
|
-
return Object.fromEntries(Object.entries(parsed).map(([entryKey, entryValue]) => {
|
|
395
|
-
if (typeof entryValue !== "string") {
|
|
396
|
-
throw new Error(`${context}.${key}.${entryKey} must be a string`);
|
|
397
|
-
}
|
|
398
|
-
return [entryKey, entryValue];
|
|
399
|
-
}));
|
|
400
|
-
}
|
|
401
423
|
function parseQuestionAnnotation(value) {
|
|
402
424
|
const record = asRecord(value, "question_response.outcome.annotation");
|
|
403
425
|
const preview = optionalString(record, "preview", "question_response.outcome.annotation");
|
|
@@ -29,20 +29,33 @@ export function looksLikePlanLimitError(input) {
|
|
|
29
29
|
}
|
|
30
30
|
export function classifyTurnErrorKind(subtype, errors, assistantError) {
|
|
31
31
|
const combined = errors.join("\n");
|
|
32
|
+
switch (assistantError) {
|
|
33
|
+
case "billing_error":
|
|
34
|
+
case "rate_limit":
|
|
35
|
+
return "plan_limit";
|
|
36
|
+
case "authentication_failed":
|
|
37
|
+
return "auth_required";
|
|
38
|
+
case "oauth_org_not_allowed":
|
|
39
|
+
return "account_access";
|
|
40
|
+
case "model_not_found":
|
|
41
|
+
return "model_unavailable";
|
|
42
|
+
case "overloaded":
|
|
43
|
+
case "server_error":
|
|
44
|
+
return "transient_service";
|
|
45
|
+
case "invalid_request":
|
|
46
|
+
case "max_output_tokens":
|
|
47
|
+
case "unknown":
|
|
48
|
+
case undefined:
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
32
51
|
if (subtype === "error_max_turns" ||
|
|
33
52
|
subtype === "error_max_budget_usd" ||
|
|
34
|
-
assistantError === "billing_error" ||
|
|
35
|
-
assistantError === "rate_limit" ||
|
|
36
53
|
(combined.length > 0 && looksLikePlanLimitError(combined))) {
|
|
37
54
|
return "plan_limit";
|
|
38
55
|
}
|
|
39
|
-
if (
|
|
40
|
-
errors.some((entry) => looksLikeAuthRequired(entry))) {
|
|
56
|
+
if (errors.some((entry) => looksLikeAuthRequired(entry))) {
|
|
41
57
|
return "auth_required";
|
|
42
58
|
}
|
|
43
|
-
if (assistantError === "server_error") {
|
|
44
|
-
return "internal";
|
|
45
|
-
}
|
|
46
59
|
return "other";
|
|
47
60
|
}
|
|
48
61
|
export function emitFastModeUpdateIfChanged(session, value) {
|
|
@@ -75,6 +75,24 @@ export function emitQuestionRequestEvent(sessionId, request) {
|
|
|
75
75
|
});
|
|
76
76
|
writeEvent({ event: "question_request", session_id: sessionId, request });
|
|
77
77
|
}
|
|
78
|
+
export function emitUserDialogRequestEvent(sessionId, request) {
|
|
79
|
+
bridgeLogger.info({
|
|
80
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
81
|
+
eventName: "user_dialog_request_emitted",
|
|
82
|
+
message: "user dialog request emitted",
|
|
83
|
+
outcome: "success",
|
|
84
|
+
sessionId,
|
|
85
|
+
requestId: request.request_id,
|
|
86
|
+
count: request.options.length,
|
|
87
|
+
fields: {
|
|
88
|
+
dialog_kind: request.dialog_kind,
|
|
89
|
+
option_count: request.options.length,
|
|
90
|
+
original_model: request.payload.original_model,
|
|
91
|
+
fallback_model: request.payload.fallback_model,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
writeEvent({ event: "user_dialog_request", session_id: sessionId, request });
|
|
95
|
+
}
|
|
78
96
|
export function emitElicitationRequestEvent(sessionId, request) {
|
|
79
97
|
bridgeLogger.info({
|
|
80
98
|
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
@@ -19,17 +19,166 @@ function messageCandidates(raw) {
|
|
|
19
19
|
}
|
|
20
20
|
return candidates;
|
|
21
21
|
}
|
|
22
|
-
function
|
|
22
|
+
function jsonValue(value) {
|
|
23
|
+
if (value === undefined) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const text = JSON.stringify(value);
|
|
28
|
+
if (text === undefined) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
return JSON.parse(text);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function mergeTaskMetadata(existing, patch) {
|
|
38
|
+
if (!patch) {
|
|
39
|
+
return existing;
|
|
40
|
+
}
|
|
41
|
+
const existingRecord = existing && typeof existing === "object" && !Array.isArray(existing)
|
|
42
|
+
? { ...existing }
|
|
43
|
+
: {};
|
|
44
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
45
|
+
if (value === null) {
|
|
46
|
+
delete existingRecord[key];
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
existingRecord[key] = value;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return Object.keys(existingRecord).length > 0 ? existingRecord : null;
|
|
53
|
+
}
|
|
54
|
+
function normalizeLifecycleTaskStatus(value) {
|
|
55
|
+
switch (value) {
|
|
56
|
+
case "pending":
|
|
57
|
+
return "pending";
|
|
58
|
+
case "running":
|
|
59
|
+
case "in_progress":
|
|
60
|
+
return "in_progress";
|
|
61
|
+
case "completed":
|
|
62
|
+
case "failed":
|
|
63
|
+
case "killed":
|
|
64
|
+
case "stopped":
|
|
65
|
+
return "completed";
|
|
66
|
+
default:
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function taskSystemMetadata(msg, patch) {
|
|
71
|
+
const metadata = {};
|
|
72
|
+
const copyValue = (from, key) => {
|
|
73
|
+
if (!from || !Object.hasOwn(from, key)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const value = jsonValue(from[key]);
|
|
77
|
+
if (value !== undefined) {
|
|
78
|
+
metadata[key] = value;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
for (const key of [
|
|
82
|
+
"error",
|
|
83
|
+
"is_backgrounded",
|
|
84
|
+
"request_id",
|
|
85
|
+
"subagent_type",
|
|
86
|
+
"task_description",
|
|
87
|
+
"task_type",
|
|
88
|
+
"workflow_name",
|
|
89
|
+
"prompt",
|
|
90
|
+
"output_file",
|
|
91
|
+
"summary",
|
|
92
|
+
"end_time",
|
|
93
|
+
"total_paused_ms",
|
|
94
|
+
]) {
|
|
95
|
+
copyValue(msg, key);
|
|
96
|
+
copyValue(patch, key);
|
|
97
|
+
}
|
|
98
|
+
const terminalStatus = nonEmptyTrimmed(msg.status) ?? nonEmptyTrimmed(patch?.status);
|
|
99
|
+
if (terminalStatus === "completed" ||
|
|
100
|
+
terminalStatus === "failed" ||
|
|
101
|
+
terminalStatus === "killed" ||
|
|
102
|
+
terminalStatus === "stopped") {
|
|
103
|
+
metadata.terminal_status = terminalStatus;
|
|
104
|
+
}
|
|
105
|
+
return Object.keys(metadata).length > 0 ? metadata : undefined;
|
|
106
|
+
}
|
|
107
|
+
function pushResumeTaskSystemUpdate(updates, tasksById, taskToolUseIds, msg) {
|
|
108
|
+
const subtype = nonEmptyTrimmed(msg.subtype);
|
|
109
|
+
if (subtype !== "task_started" &&
|
|
110
|
+
subtype !== "task_progress" &&
|
|
111
|
+
subtype !== "task_updated" &&
|
|
112
|
+
subtype !== "task_notification") {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
const taskId = nonEmptyTrimmed(msg.task_id);
|
|
116
|
+
if (!taskId) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
const explicitToolUseId = nonEmptyTrimmed(msg.tool_use_id);
|
|
120
|
+
if (explicitToolUseId) {
|
|
121
|
+
taskToolUseIds.set(taskId, explicitToolUseId);
|
|
122
|
+
}
|
|
123
|
+
const existing = tasksById.get(taskId);
|
|
124
|
+
const patch = asRecordOrNull(msg.patch) ?? undefined;
|
|
125
|
+
const status = normalizeLifecycleTaskStatus(msg.status) ??
|
|
126
|
+
normalizeLifecycleTaskStatus(patch?.status) ??
|
|
127
|
+
(subtype === "task_started" || subtype === "task_progress" ? "in_progress" : undefined);
|
|
128
|
+
const description = nonEmptyTrimmed(patch?.description) ??
|
|
129
|
+
nonEmptyTrimmed(msg.description) ??
|
|
130
|
+
nonEmptyTrimmed(msg.summary);
|
|
131
|
+
const activeForm = nonEmptyTrimmed(patch?.activeForm) ?? nonEmptyTrimmed(patch?.active_form);
|
|
132
|
+
const subject = nonEmptyTrimmed(patch?.subject) ??
|
|
133
|
+
nonEmptyTrimmed(msg.subject) ??
|
|
134
|
+
existing?.subject ??
|
|
135
|
+
nonEmptyTrimmed(msg.workflow_name) ??
|
|
136
|
+
nonEmptyTrimmed(msg.task_description) ??
|
|
137
|
+
description ??
|
|
138
|
+
taskId;
|
|
139
|
+
const metadata = mergeTaskMetadata(existing?.metadata, taskSystemMetadata(msg, patch));
|
|
140
|
+
const sourceToolCallId = taskToolUseIds.get(taskId) ?? existing?.source_tool_call_id;
|
|
141
|
+
const task = {
|
|
142
|
+
task_id: taskId,
|
|
143
|
+
subject,
|
|
144
|
+
...(description !== undefined ? { description } : existing?.description !== undefined ? { description: existing.description } : {}),
|
|
145
|
+
...(activeForm !== undefined ? { active_form: activeForm } : existing?.active_form !== undefined ? { active_form: existing.active_form } : {}),
|
|
146
|
+
status: status ?? existing?.status ?? "pending",
|
|
147
|
+
...(existing?.owner !== undefined ? { owner: existing.owner } : {}),
|
|
148
|
+
blocks: existing ? [...existing.blocks] : [],
|
|
149
|
+
blocked_by: existing ? [...existing.blocked_by] : [],
|
|
150
|
+
...(metadata !== undefined ? { metadata } : {}),
|
|
151
|
+
...(sourceToolCallId !== undefined ? { source_tool_call_id: sourceToolCallId } : {}),
|
|
152
|
+
};
|
|
153
|
+
tasksById.set(taskId, task);
|
|
154
|
+
updates.push({
|
|
155
|
+
type: "task_state_update",
|
|
156
|
+
source: "task_lifecycle",
|
|
157
|
+
tasks: [task],
|
|
158
|
+
removed_task_ids: [],
|
|
159
|
+
is_complete_snapshot: false,
|
|
160
|
+
});
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
function pushResumeTextChunk(updates, role, text, sourceMessageUuid) {
|
|
23
164
|
if (!text.trim()) {
|
|
24
165
|
return;
|
|
25
166
|
}
|
|
26
167
|
if (role === "assistant") {
|
|
27
|
-
updates.push({
|
|
168
|
+
updates.push({
|
|
169
|
+
type: "agent_message_chunk",
|
|
170
|
+
content: { type: "text", text },
|
|
171
|
+
...(sourceMessageUuid ? { source_message_uuid: sourceMessageUuid } : {}),
|
|
172
|
+
});
|
|
28
173
|
return;
|
|
29
174
|
}
|
|
30
|
-
updates.push({
|
|
175
|
+
updates.push({
|
|
176
|
+
type: "user_message_chunk",
|
|
177
|
+
content: { type: "text", text },
|
|
178
|
+
...(sourceMessageUuid ? { source_message_uuid: sourceMessageUuid } : {}),
|
|
179
|
+
});
|
|
31
180
|
}
|
|
32
|
-
function pushResumeToolUse(updates, toolCalls, hiddenToolUseIds, block, parentToolUseId) {
|
|
181
|
+
function pushResumeToolUse(updates, toolCalls, hiddenToolUseIds, block, parentToolUseId, sourceMessageUuid) {
|
|
33
182
|
const toolUseId = typeof block.id === "string" ? block.id : "";
|
|
34
183
|
if (!toolUseId) {
|
|
35
184
|
return;
|
|
@@ -42,10 +191,13 @@ function pushResumeToolUse(updates, toolCalls, hiddenToolUseIds, block, parentTo
|
|
|
42
191
|
}
|
|
43
192
|
const toolCall = createToolCall(toolUseId, name, input, parentToolUseId);
|
|
44
193
|
toolCall.status = "in_progress";
|
|
194
|
+
if (sourceMessageUuid) {
|
|
195
|
+
toolCall.source_message_uuid = sourceMessageUuid;
|
|
196
|
+
}
|
|
45
197
|
toolCalls.set(toolUseId, toolCall);
|
|
46
198
|
updates.push({ type: "tool_call", tool_call: toolCall });
|
|
47
199
|
}
|
|
48
|
-
function pushResumeToolResult(updates, toolCalls, hiddenToolUseIds, block) {
|
|
200
|
+
function pushResumeToolResult(updates, toolCalls, hiddenToolUseIds, block, sourceMessageUuid) {
|
|
49
201
|
const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : "";
|
|
50
202
|
if (!toolUseId) {
|
|
51
203
|
return;
|
|
@@ -58,7 +210,14 @@ function pushResumeToolResult(updates, toolCalls, hiddenToolUseIds, block) {
|
|
|
58
210
|
const isError = Boolean(block.is_error);
|
|
59
211
|
const base = toolCalls.get(toolUseId);
|
|
60
212
|
const fields = buildToolResultFields(isError, block.content, base, block);
|
|
61
|
-
updates.push({
|
|
213
|
+
updates.push({
|
|
214
|
+
type: "tool_call_update",
|
|
215
|
+
tool_call_update: {
|
|
216
|
+
tool_call_id: toolUseId,
|
|
217
|
+
...(sourceMessageUuid ? { source_message_uuid: sourceMessageUuid } : {}),
|
|
218
|
+
fields,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
62
221
|
if (!base) {
|
|
63
222
|
return;
|
|
64
223
|
}
|
|
@@ -111,9 +270,22 @@ export function mapSessionMessagesToUpdates(messages) {
|
|
|
111
270
|
const updates = [];
|
|
112
271
|
const toolCalls = new Map();
|
|
113
272
|
const hiddenToolUseIds = new Set();
|
|
273
|
+
const tasksById = new Map();
|
|
274
|
+
const taskToolUseIds = new Map();
|
|
114
275
|
for (const entry of messages) {
|
|
115
276
|
const fallbackRole = entry.type === "assistant" ? "assistant" : "user";
|
|
116
|
-
|
|
277
|
+
const entrySourceMessageUuid = typeof entry.uuid === "string" ? entry.uuid : undefined;
|
|
278
|
+
const candidates = messageCandidates(entry.message);
|
|
279
|
+
if (entry.type === "system") {
|
|
280
|
+
for (const message of candidates) {
|
|
281
|
+
if (pushResumeTaskSystemUpdate(updates, tasksById, taskToolUseIds, message)) {
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
for (const message of candidates) {
|
|
288
|
+
const sourceMessageUuid = typeof message.uuid === "string" ? message.uuid : entrySourceMessageUuid;
|
|
117
289
|
const roleCandidate = message.role;
|
|
118
290
|
const role = roleCandidate === "assistant" || roleCandidate === "user" ? roleCandidate : fallbackRole;
|
|
119
291
|
const parentToolUseId = typeof entry.parent_tool_use_id === "string"
|
|
@@ -132,19 +304,19 @@ export function mapSessionMessagesToUpdates(messages) {
|
|
|
132
304
|
continue;
|
|
133
305
|
}
|
|
134
306
|
if (blockType === "text" && typeof block.text === "string") {
|
|
135
|
-
pushResumeTextChunk(updates, role, block.text);
|
|
307
|
+
pushResumeTextChunk(updates, role, block.text, sourceMessageUuid);
|
|
136
308
|
continue;
|
|
137
309
|
}
|
|
138
310
|
if (isToolUseBlockType(blockType) && role === "assistant") {
|
|
139
|
-
pushResumeToolUse(updates, toolCalls, hiddenToolUseIds, block, parentToolUseId);
|
|
311
|
+
pushResumeToolUse(updates, toolCalls, hiddenToolUseIds, block, parentToolUseId, sourceMessageUuid);
|
|
140
312
|
continue;
|
|
141
313
|
}
|
|
142
314
|
if (TOOL_RESULT_TYPES.has(blockType)) {
|
|
143
|
-
pushResumeToolResult(updates, toolCalls, hiddenToolUseIds, block);
|
|
315
|
+
pushResumeToolResult(updates, toolCalls, hiddenToolUseIds, block, sourceMessageUuid);
|
|
144
316
|
continue;
|
|
145
317
|
}
|
|
146
318
|
if (blockType === "image") {
|
|
147
|
-
pushResumeTextChunk(updates, role, "[image]");
|
|
319
|
+
pushResumeTextChunk(updates, role, "[image]", sourceMessageUuid);
|
|
148
320
|
}
|
|
149
321
|
}
|
|
150
322
|
}
|