@love-moon/ai-sdk 0.16.0 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# @love-moon/ai-sdk
|
|
2
2
|
|
|
3
|
+
## 0.17.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7e92c0f: When a task's model quota runs out ("You've hit your session limit · resets
|
|
8
|
+
1:20am"), fire no longer stops there. It reads the reset time, schedules a
|
|
9
|
+
one-off Conductor scheduled message to continue a minute or few after the
|
|
10
|
+
reset, and posts a single notice. Messages that arrive while waiting are queued
|
|
11
|
+
and handed to the resume turn together instead of each failing with another
|
|
12
|
+
"处理失败". The resume shows up in the task's scheduled-message list, where it
|
|
13
|
+
can be canceled. This also covers a quota hit while Claude works on its own
|
|
14
|
+
between user turns: ai-sdk now reports such a background turn that ends in an
|
|
15
|
+
error result (or a process exit after it) as a failed turn.
|
|
16
|
+
|
|
17
|
+
## 0.17.0
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- d088b31: Claude tasks keep one Claude process per session, with its input left open
|
|
22
|
+
between turns, instead of starting a new one-shot process for every turn.
|
|
23
|
+
Background subagents now run to completion: before, claude killed them 10
|
|
24
|
+
minutes after the main agent's turn ended, so long multi-agent tasks went quiet
|
|
25
|
+
until the user sent another message. When they finish, Claude's follow-up reply
|
|
26
|
+
is posted to the chat without waiting for a user message. Subagent narration is
|
|
27
|
+
no longer posted as a task reply.
|
|
28
|
+
- 79ca552: Claude no longer switches to `acceptEdits` mode when the daemon runs as root.
|
|
29
|
+
That fallback looked like it worked, but in a headless task it silently refused
|
|
30
|
+
every shell command (ssh, rsync, ...) and every read outside the project. Claude
|
|
31
|
+
now keeps `bypassPermissions` everywhere. As root without `IS_SANDBOX=1`, the
|
|
32
|
+
chat shows a warning right after "session started" telling you to add
|
|
33
|
+
`envs: { IS_SANDBOX: "1" }` to the conductor config (or run the daemon as a
|
|
34
|
+
regular user). Terminal tasks print the same hint. An `IS_SANDBOX` set in config
|
|
35
|
+
`envs` now also reaches terminal (PTY) claude tasks. `conductor config` always
|
|
36
|
+
writes `--dangerously-skip-permissions` for claude.
|
|
37
|
+
- ccbfeeb: Kimi Code tasks no longer kill background subagents after 12 minutes. `kimi -p`
|
|
38
|
+
stays silent while it waits for a task it launched with `run_in_background`,
|
|
39
|
+
and the session's idle deadline used to treat that silence as a stuck turn and
|
|
40
|
+
terminate kimi together with the task. Once a turn launches a background task,
|
|
41
|
+
the deadline now waits for kimi to exit on its own.
|
|
42
|
+
|
|
3
43
|
## 0.16.0
|
|
4
44
|
|
|
5
45
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { BUILT_IN_BACKENDS } from "./built-in-backends.js";
|
|
|
2
2
|
export { PROVIDER_MEDIA_CAPABILITIES } from "./media-adapters.js";
|
|
3
3
|
export { summarizeTurnUsage } from "./turn-usage.js";
|
|
4
4
|
export { createAiSession, RemoteAiSession } from "./client.js";
|
|
5
|
-
export { isClaudeRootPermissionRestricted,
|
|
5
|
+
export { isClaudeRootPermissionRestricted, buildClaudeRootSandboxNotice, claudeCommandNeedsRootSandbox, resolveClaudePermissionPolicy } from "./providers/claude-agent-sdk-session.js";
|
|
6
6
|
export { appendContextFilesToPrompt, normalizeContextFiles } from "./context-files.js";
|
|
7
7
|
export { assertMediaCapabilities, fileMediaToBase64, fileMediaToDataUri, normalizeMediaInputs, resolveTurnMedia } from "./media-input.js";
|
|
8
8
|
export { GOAL_STATUSES, TERMINAL_GOAL_STATUSES, isGoalStatus, isTerminalGoalStatus } from "./shared.js";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { createAiSession, RemoteAiSession } from "./client.js";
|
|
2
2
|
export { BUILT_IN_BACKENDS } from "./built-in-backends.js";
|
|
3
3
|
export { PROVIDER_MEDIA_CAPABILITIES } from "./media-adapters.js";
|
|
4
|
-
export { isClaudeRootPermissionRestricted,
|
|
4
|
+
export { isClaudeRootPermissionRestricted, buildClaudeRootSandboxNotice, claudeCommandNeedsRootSandbox, resolveClaudePermissionPolicy, } from "./providers/claude-agent-sdk-session.js";
|
|
5
5
|
export { appendContextFilesToPrompt, normalizeContextFiles } from "./context-files.js";
|
|
6
6
|
export { summarizeTurnUsage } from "./turn-usage.js";
|
|
7
7
|
export { assertMediaCapabilities, fileMediaToBase64, fileMediaToDataUri, normalizeMediaInputs, resolveTurnMedia, } from "./media-input.js";
|
|
@@ -2,10 +2,11 @@ export function isClaudeRootPermissionRestricted(env?: NodeJS.ProcessEnv): boole
|
|
|
2
2
|
export function resolveClaudePermissionPolicy(options?: {}, env?: NodeJS.ProcessEnv): {
|
|
3
3
|
permissionMode: string;
|
|
4
4
|
allowDangerouslySkipPermissions: boolean;
|
|
5
|
-
|
|
5
|
+
rootSandboxRequired: boolean;
|
|
6
6
|
invalidMode: any;
|
|
7
7
|
};
|
|
8
|
-
export function
|
|
8
|
+
export function claudeCommandNeedsRootSandbox(commandLine: any, env?: NodeJS.ProcessEnv): boolean;
|
|
9
|
+
export function buildClaudeRootSandboxNotice({ configFile }?: {}): string;
|
|
9
10
|
export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
|
|
10
11
|
static capabilities: Readonly<{
|
|
11
12
|
goal: true;
|
|
@@ -45,7 +46,10 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
|
|
|
45
46
|
lastReplyTarget: string;
|
|
46
47
|
manualResumeReady: boolean;
|
|
47
48
|
currentTurn: {
|
|
48
|
-
abortController:
|
|
49
|
+
abortController: null;
|
|
50
|
+
liveQuery: null;
|
|
51
|
+
onProgress: null;
|
|
52
|
+
settle: null;
|
|
49
53
|
suppressReply: boolean;
|
|
50
54
|
compactMetadata: null;
|
|
51
55
|
compactError: string;
|
|
@@ -56,7 +60,27 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
|
|
|
56
60
|
query: null;
|
|
57
61
|
resultMessage: null;
|
|
58
62
|
terminalWorkingStatusEmitted: boolean;
|
|
63
|
+
usageBase: null;
|
|
59
64
|
usageByMessageId: Map<any, any>;
|
|
65
|
+
uuid: string;
|
|
66
|
+
} | null;
|
|
67
|
+
liveQuery: {
|
|
68
|
+
abortController: AbortController;
|
|
69
|
+
backgroundTurn: null;
|
|
70
|
+
ended: boolean;
|
|
71
|
+
input: {
|
|
72
|
+
push(message: any): boolean;
|
|
73
|
+
close: () => void;
|
|
74
|
+
[Symbol.asyncIterator](): {
|
|
75
|
+
next: () => Promise<any>;
|
|
76
|
+
return: () => Promise<{
|
|
77
|
+
value: undefined;
|
|
78
|
+
done: boolean;
|
|
79
|
+
}>;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
query: null;
|
|
83
|
+
usageBase: null;
|
|
60
84
|
} | null;
|
|
61
85
|
lastResult: any;
|
|
62
86
|
rateLimitInfo: any;
|
|
@@ -69,9 +93,10 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
|
|
|
69
93
|
permissionPolicy: {
|
|
70
94
|
permissionMode: string;
|
|
71
95
|
allowDangerouslySkipPermissions: boolean;
|
|
72
|
-
|
|
96
|
+
rootSandboxRequired: boolean;
|
|
73
97
|
invalidMode: any;
|
|
74
98
|
};
|
|
99
|
+
notices: string[];
|
|
75
100
|
writeLog(message: any): void;
|
|
76
101
|
trace(message: any): void;
|
|
77
102
|
get threadId(): any;
|
|
@@ -103,6 +128,7 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
|
|
|
103
128
|
image: "native";
|
|
104
129
|
}>;
|
|
105
130
|
};
|
|
131
|
+
notices: string[];
|
|
106
132
|
};
|
|
107
133
|
getSessionInfo(): {
|
|
108
134
|
backend: string;
|
|
@@ -165,6 +191,65 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
|
|
|
165
191
|
onProgress: any;
|
|
166
192
|
}): Promise<void>;
|
|
167
193
|
interruptCurrentTurn(): Promise<boolean>;
|
|
194
|
+
createTurnState({ suppressReply, onProgress }?: {
|
|
195
|
+
suppressReply?: boolean | undefined;
|
|
196
|
+
onProgress?: null | undefined;
|
|
197
|
+
}): {
|
|
198
|
+
abortController: null;
|
|
199
|
+
liveQuery: null;
|
|
200
|
+
onProgress: null;
|
|
201
|
+
settle: null;
|
|
202
|
+
suppressReply: boolean;
|
|
203
|
+
compactMetadata: null;
|
|
204
|
+
compactError: string;
|
|
205
|
+
conversationReset: string;
|
|
206
|
+
emittedAssistantMessage: boolean;
|
|
207
|
+
fullText: string;
|
|
208
|
+
items: never[];
|
|
209
|
+
query: null;
|
|
210
|
+
resultMessage: null;
|
|
211
|
+
terminalWorkingStatusEmitted: boolean;
|
|
212
|
+
usageBase: null;
|
|
213
|
+
usageByMessageId: Map<any, any>;
|
|
214
|
+
uuid: string;
|
|
215
|
+
};
|
|
216
|
+
/**
|
|
217
|
+
* One claude process per session, fed through a prompt stream that stays
|
|
218
|
+
* open between turns. A one-shot string prompt closes claude's input at once,
|
|
219
|
+
* and claude then kills background subagents 10 minutes after the main
|
|
220
|
+
* agent's turn ends (CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS). Kept open, the
|
|
221
|
+
* subagents run to completion and claude starts its own follow-up turn on
|
|
222
|
+
* their notifications.
|
|
223
|
+
*/
|
|
224
|
+
ensureLiveQuery(sdkModule: any): {
|
|
225
|
+
abortController: AbortController;
|
|
226
|
+
backgroundTurn: null;
|
|
227
|
+
ended: boolean;
|
|
228
|
+
input: {
|
|
229
|
+
push(message: any): boolean;
|
|
230
|
+
close: () => void;
|
|
231
|
+
[Symbol.asyncIterator](): {
|
|
232
|
+
next: () => Promise<any>;
|
|
233
|
+
return: () => Promise<{
|
|
234
|
+
value: undefined;
|
|
235
|
+
done: boolean;
|
|
236
|
+
}>;
|
|
237
|
+
};
|
|
238
|
+
};
|
|
239
|
+
query: null;
|
|
240
|
+
usageBase: null;
|
|
241
|
+
};
|
|
242
|
+
closeLiveQuery(liveQuery: any): void;
|
|
243
|
+
settleTurn(turn: any, error: any, resultMessage?: null): void;
|
|
244
|
+
pumpLiveQuery(liveQuery: any): Promise<void>;
|
|
245
|
+
/**
|
|
246
|
+
* Routes the long-lived query's output. A result ends the pending turn when it
|
|
247
|
+
* echoes that turn's user message uuid (or echoes none while no follow-up turn
|
|
248
|
+
* of claude's own is running); everything else is a background turn, e.g.
|
|
249
|
+
* claude answering a finished subagent's notification between user turns.
|
|
250
|
+
*/
|
|
251
|
+
dispatchLiveMessage(liveQuery: any, message: any): Promise<void>;
|
|
252
|
+
finishBackgroundTurn(backgroundTurn: any, resultMessage: any, failure?: null): Promise<void>;
|
|
168
253
|
runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress, jsonSchema, suppressReply }?: {
|
|
169
254
|
useInitialImages?: boolean | undefined;
|
|
170
255
|
onProgress?: null | undefined;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { EventEmitter } from "node:events";
|
|
2
3
|
import { CLAUDE_AGENT_SDK_VARIANT as CLAUDE_PROVIDER_VARIANT } from "../built-in-backends.js";
|
|
3
4
|
import { appendContextFilesToPrompt } from "../context-files.js";
|
|
@@ -7,24 +8,89 @@ import { emitLog, extractLongFlagFromCommandLine, getBoundedEnvInt, isGoalStatus
|
|
|
7
8
|
const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
|
|
8
9
|
const MIN_TURN_DEADLINE_MS = 30 * 1000;
|
|
9
10
|
const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
|
|
11
|
+
// How long an interrupted turn of the long-lived query may take to report its
|
|
12
|
+
// result before the process is torn down instead.
|
|
13
|
+
const INTERRUPT_GRACE_MS = 10 * 1000;
|
|
10
14
|
const DEFAULT_SETTING_SOURCES = ["user", "project", "local"];
|
|
11
15
|
const PERMISSION_MODES = new Set(["default", "acceptEdits", "bypassPermissions", "plan", "dontAsk"]);
|
|
12
16
|
const DEFAULT_PERMISSION_MODE = "bypassPermissions";
|
|
13
|
-
// Auto ("accept edits") mode: the strongest mode claude allows as root.
|
|
14
|
-
const ROOT_PERMISSION_MODE = "acceptEdits";
|
|
15
17
|
function waitForever() {
|
|
16
18
|
return new Promise(() => { });
|
|
17
19
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
function buildClaudeUserMessage(promptText, media, uuid) {
|
|
21
|
+
return {
|
|
20
22
|
type: "user",
|
|
21
23
|
message: {
|
|
22
24
|
role: "user",
|
|
23
|
-
content: buildClaudeContent(promptText, media),
|
|
25
|
+
content: media.length ? buildClaudeContent(promptText, media) : promptText,
|
|
24
26
|
},
|
|
25
27
|
parent_tool_use_id: null,
|
|
28
|
+
...(uuid ? { uuid } : {}),
|
|
26
29
|
};
|
|
27
30
|
}
|
|
31
|
+
async function* buildClaudeInput(promptText, media) {
|
|
32
|
+
yield buildClaudeUserMessage(promptText, media);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The prompt stream of a long-lived query. It stays open between turns: claude
|
|
36
|
+
* only winds a print-mode process down (killing background subagents after its
|
|
37
|
+
* wait ceiling) once its input is closed.
|
|
38
|
+
*/
|
|
39
|
+
function createInputQueue() {
|
|
40
|
+
const buffer = [];
|
|
41
|
+
let waiter = null;
|
|
42
|
+
let closed = false;
|
|
43
|
+
const finish = () => {
|
|
44
|
+
closed = true;
|
|
45
|
+
if (waiter) {
|
|
46
|
+
const resolve = waiter;
|
|
47
|
+
waiter = null;
|
|
48
|
+
resolve({ value: undefined, done: true });
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
push(message) {
|
|
53
|
+
if (closed) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
if (waiter) {
|
|
57
|
+
const resolve = waiter;
|
|
58
|
+
waiter = null;
|
|
59
|
+
resolve({ value: message, done: false });
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
buffer.push(message);
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
},
|
|
66
|
+
close: finish,
|
|
67
|
+
[Symbol.asyncIterator]() {
|
|
68
|
+
return {
|
|
69
|
+
next: () => {
|
|
70
|
+
if (buffer.length) {
|
|
71
|
+
return Promise.resolve({ value: buffer.shift(), done: false });
|
|
72
|
+
}
|
|
73
|
+
if (closed) {
|
|
74
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
75
|
+
}
|
|
76
|
+
return new Promise((resolve) => {
|
|
77
|
+
waiter = resolve;
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
return: () => {
|
|
81
|
+
finish();
|
|
82
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function resultUserMessageUuids(resultMessage) {
|
|
89
|
+
if (Array.isArray(resultMessage?.user_message_uuids)) {
|
|
90
|
+
return resultMessage.user_message_uuids;
|
|
91
|
+
}
|
|
92
|
+
return resultMessage?.user_message_uuid ? [resultMessage.user_message_uuid] : [];
|
|
93
|
+
}
|
|
28
94
|
function createTurnError(message, extras = {}) {
|
|
29
95
|
const error = new Error(message);
|
|
30
96
|
for (const [key, value] of Object.entries(extras)) {
|
|
@@ -52,11 +118,27 @@ function sumStreamedUsage(usageByMessageId, base = null) {
|
|
|
52
118
|
return total;
|
|
53
119
|
}
|
|
54
120
|
/**
|
|
55
|
-
* A finished turn's usage.
|
|
56
|
-
*
|
|
57
|
-
*
|
|
121
|
+
* A finished turn's usage. `usage` covers only the main loop's last segment, while
|
|
122
|
+
* `modelUsage` accumulates every API call the query's process made (all segments,
|
|
123
|
+
* subagents and earlier turns of a long-lived query), so prefer its sum minus `base`,
|
|
124
|
+
* the total at the end of the previous turn.
|
|
58
125
|
*/
|
|
59
|
-
function resultUsage(resultMessage) {
|
|
126
|
+
function resultUsage(resultMessage, base = null) {
|
|
127
|
+
const total = modelUsageTotal(resultMessage);
|
|
128
|
+
if (!total) {
|
|
129
|
+
return resultMessage.usage ? { ...resultMessage.usage } : null;
|
|
130
|
+
}
|
|
131
|
+
// /clear resets the running total; a total below its base starts over.
|
|
132
|
+
if (!base || Object.keys(base).some((key) => !(total[key] >= base[key]))) {
|
|
133
|
+
return total;
|
|
134
|
+
}
|
|
135
|
+
const delta = {};
|
|
136
|
+
for (const [key, value] of Object.entries(total)) {
|
|
137
|
+
delta[key] = value - base[key];
|
|
138
|
+
}
|
|
139
|
+
return delta;
|
|
140
|
+
}
|
|
141
|
+
function modelUsageTotal(resultMessage) {
|
|
60
142
|
const total = {};
|
|
61
143
|
for (const entry of Object.values(resultMessage.modelUsage || {})) {
|
|
62
144
|
for (const [from, to] of [
|
|
@@ -71,10 +153,7 @@ function resultUsage(resultMessage) {
|
|
|
71
153
|
}
|
|
72
154
|
}
|
|
73
155
|
}
|
|
74
|
-
|
|
75
|
-
return total;
|
|
76
|
-
}
|
|
77
|
-
return resultMessage.usage ? { ...resultMessage.usage } : null;
|
|
156
|
+
return Object.keys(total).length ? total : null;
|
|
78
157
|
}
|
|
79
158
|
function normalizeClaudeBackend(backend) {
|
|
80
159
|
const normalized = String(backend || "").trim().toLowerCase();
|
|
@@ -110,71 +189,76 @@ function normalizePermissionMode(value) {
|
|
|
110
189
|
// Two different env idioms in that one line, and both are load-bearing:
|
|
111
190
|
// IS_SANDBOX is a *strict* `=== "1"` compare, while CLAUDE_CODE_BUBBLEWRAP goes
|
|
112
191
|
// through claude's loose truthy parser (`1|true|yes|on`, trimmed+lowercased).
|
|
113
|
-
// Do not "helpfully" accept IS_SANDBOX=true here —
|
|
114
|
-
// the
|
|
115
|
-
//
|
|
192
|
+
// Do not "helpfully" accept IS_SANDBOX=true here — claude would still refuse,
|
|
193
|
+
// and the user would get no hint telling them why.
|
|
194
|
+
//
|
|
195
|
+
// Values are compared the way claude will see them: spawn stringifies env, so
|
|
196
|
+
// YAML `IS_SANDBOX: 1` (a number from config `envs:`) reaches claude as "1"
|
|
197
|
+
// and passes its gate. `true` becomes "true" and still does not.
|
|
116
198
|
export function isClaudeRootPermissionRestricted(env = process.env) {
|
|
117
199
|
if (typeof process.getuid !== "function" || process.getuid() !== 0) {
|
|
118
200
|
return false;
|
|
119
201
|
}
|
|
120
|
-
if (env?.IS_SANDBOX === "1") {
|
|
202
|
+
if (String(env?.IS_SANDBOX ?? "") === "1") {
|
|
121
203
|
return false;
|
|
122
204
|
}
|
|
123
205
|
const bubblewrap = String(env?.CLAUDE_CODE_BUBBLEWRAP ?? "").trim().toLowerCase();
|
|
124
206
|
return !["1", "true", "yes", "on"].includes(bubblewrap);
|
|
125
207
|
}
|
|
126
|
-
//
|
|
127
|
-
//
|
|
208
|
+
// The configured mode is used as-is, including on root. We used to downgrade
|
|
209
|
+
// bypassPermissions -> acceptEdits for root, but a headless session has no one
|
|
210
|
+
// to approve prompts, so acceptEdits silently denied every Bash command and
|
|
211
|
+
// every read outside cwd: the task "ran" and could do nothing. Instead we keep
|
|
212
|
+
// the mode and flag `rootSandboxRequired` so the caller can tell the user to
|
|
213
|
+
// opt in with IS_SANDBOX=1 (or run as a regular user).
|
|
128
214
|
export function resolveClaudePermissionPolicy(options = {}, env = process.env) {
|
|
129
|
-
const
|
|
215
|
+
const permissionMode = normalizePermissionMode(options.permissionMode);
|
|
130
216
|
// A configured value we don't recognize still falls back to the default (a
|
|
131
217
|
// typo must never take the session down), but the caller reports it so the
|
|
132
218
|
// user finds out their config line is doing nothing.
|
|
133
219
|
const rawMode = typeof options.permissionMode === "string" ? options.permissionMode.trim() : "";
|
|
134
220
|
const invalidMode = rawMode && !PERMISSION_MODES.has(rawMode) ? rawMode : "";
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
permissionMode: requested,
|
|
138
|
-
allowDangerouslySkipPermissions: requested === DEFAULT_PERMISSION_MODE || options.allowDangerouslySkipPermissions === true,
|
|
139
|
-
downgradedFrom: "",
|
|
140
|
-
invalidMode,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
const permissionMode = requested === DEFAULT_PERMISSION_MODE ? ROOT_PERMISSION_MODE : requested;
|
|
221
|
+
const rootRestricted = isClaudeRootPermissionRestricted(env);
|
|
222
|
+
const bypass = permissionMode === DEFAULT_PERMISSION_MODE;
|
|
144
223
|
return {
|
|
145
224
|
permissionMode,
|
|
146
|
-
|
|
147
|
-
|
|
225
|
+
// A non-bypass mode the user picked on purpose never needs the escape
|
|
226
|
+
// flag, and as root it would only trip claude's gate.
|
|
227
|
+
allowDangerouslySkipPermissions: bypass || (options.allowDangerouslySkipPermissions === true && !rootRestricted),
|
|
228
|
+
rootSandboxRequired: bypass && rootRestricted,
|
|
148
229
|
invalidMode,
|
|
149
230
|
};
|
|
150
231
|
}
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
export function resolveClaudeCommandForRoot(commandLine, env = process.env) {
|
|
232
|
+
// True when a configured claude command line would hit claude's root gate:
|
|
233
|
+
// it asks for bypass (the flag, or `--permission-mode bypassPermissions`) and
|
|
234
|
+
// the process env doesn't opt out.
|
|
235
|
+
export function claudeCommandNeedsRootSandbox(commandLine, env = process.env) {
|
|
156
236
|
const command = typeof commandLine === "string" ? commandLine : "";
|
|
157
|
-
if (!command.trim()
|
|
158
|
-
return
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
.trim()
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
237
|
+
if (!command.trim()) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
const bypass = /(^|\s)--dangerously-skip-permissions(?=\s|$)/.test(command)
|
|
241
|
+
|| extractLongFlagFromCommandLine(command, "permission-mode") === DEFAULT_PERMISSION_MODE;
|
|
242
|
+
return bypass && isClaudeRootPermissionRestricted(env);
|
|
243
|
+
}
|
|
244
|
+
// User-facing hint shown in the task chat (and the PTY terminal) when claude
|
|
245
|
+
// is about to be started as root in bypass mode without IS_SANDBOX=1.
|
|
246
|
+
export function buildClaudeRootSandboxNotice({ configFile } = {}) {
|
|
247
|
+
const configPath = typeof configFile === "string" && configFile.trim()
|
|
248
|
+
? configFile.trim()
|
|
249
|
+
: "~/.conductor/config.yaml";
|
|
250
|
+
return [
|
|
251
|
+
"⚠️ 当前 conductor 以 root 用户运行 claude。Claude Code 禁止 root 使用 bypassPermissions(--dangerously-skip-permissions),本会话的请求会直接失败。",
|
|
252
|
+
"",
|
|
253
|
+
`如果这台机器是隔离环境(容器 / 虚机),请在 ${configPath} 中加入下面的配置,然后重启 daemon:`,
|
|
254
|
+
"",
|
|
255
|
+
"```yaml",
|
|
256
|
+
"envs:",
|
|
257
|
+
" IS_SANDBOX: \"1\"",
|
|
258
|
+
"```",
|
|
259
|
+
"",
|
|
260
|
+
"注意值必须是 1(写成 1 或 \"1\" 都可以),写成 true / yes 不生效。设置后 claude 会以 root 身份直接执行命令、不再请求确认;如果不是隔离环境,建议改用普通用户运行 daemon。",
|
|
261
|
+
].join("\n");
|
|
178
262
|
}
|
|
179
263
|
function normalizeText(value) {
|
|
180
264
|
return typeof value === "string" ? value : "";
|
|
@@ -315,6 +399,7 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
315
399
|
this.lastReplyTarget = "";
|
|
316
400
|
this.manualResumeReady = Boolean(this.sessionId);
|
|
317
401
|
this.currentTurn = null;
|
|
402
|
+
this.liveQuery = null;
|
|
318
403
|
this.lastResult = null;
|
|
319
404
|
this.rateLimitInfo = null;
|
|
320
405
|
this.currentTurnStatus = null;
|
|
@@ -340,9 +425,15 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
340
425
|
+ `using ${this.permissionPolicy.permissionMode} instead `
|
|
341
426
|
+ `(valid: ${[...PERMISSION_MODES].join(", ")})`);
|
|
342
427
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
428
|
+
// Shown to the user by fire as a chat message (see getSnapshot().notices):
|
|
429
|
+
// claude will refuse to start, and a daemon log line alone is invisible.
|
|
430
|
+
this.notices = [];
|
|
431
|
+
if (this.permissionPolicy.rootSandboxRequired) {
|
|
432
|
+
this.trace("WARN running as root without IS_SANDBOX=1; claude will refuse bypassPermissions "
|
|
433
|
+
+ "(set envs.IS_SANDBOX: \"1\" in the conductor config, or run as a regular user)");
|
|
434
|
+
this.notices.push(buildClaudeRootSandboxNotice({
|
|
435
|
+
configFile: options.configFile || this.env.CONDUCTOR_CONFIG || process.env.CONDUCTOR_CONFIG,
|
|
436
|
+
}));
|
|
346
437
|
}
|
|
347
438
|
}
|
|
348
439
|
writeLog(message) {
|
|
@@ -381,6 +472,7 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
381
472
|
: null,
|
|
382
473
|
currentTurnStatus: this.getCurrentTurnStatus(),
|
|
383
474
|
capabilities: this.getCapabilities(),
|
|
475
|
+
notices: [...this.notices],
|
|
384
476
|
};
|
|
385
477
|
}
|
|
386
478
|
getSessionInfo() {
|
|
@@ -461,6 +553,8 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
461
553
|
status_done_line: payload?.status_done_line,
|
|
462
554
|
reply_preview: payload?.reply_preview,
|
|
463
555
|
thread_id: this.sessionId || undefined,
|
|
556
|
+
// A turn claude ran on its own, between user turns (see dispatchLiveMessage).
|
|
557
|
+
...(payload?.background === true ? { background: true } : {}),
|
|
464
558
|
};
|
|
465
559
|
this.updateCurrentTurnStatus(normalized);
|
|
466
560
|
const snapshot = this.getCurrentTurnStatus();
|
|
@@ -839,7 +933,8 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
839
933
|
noteToolStarted(currentTurn, block.id, block.name, block.input);
|
|
840
934
|
}
|
|
841
935
|
}
|
|
842
|
-
|
|
936
|
+
// Subagent narration (parent_tool_use_id set) is not a reply to the user.
|
|
937
|
+
const text = message.parent_tool_use_id ? "" : extractAssistantText(message.message);
|
|
843
938
|
if (!text) {
|
|
844
939
|
return;
|
|
845
940
|
}
|
|
@@ -893,6 +988,26 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
893
988
|
if (!currentTurn) {
|
|
894
989
|
return false;
|
|
895
990
|
}
|
|
991
|
+
const { liveQuery } = currentTurn;
|
|
992
|
+
if (liveQuery) {
|
|
993
|
+
// The interrupted turn reports its own result; tear the process down only
|
|
994
|
+
// if it does not, so a stuck CLI cannot hold the turn open.
|
|
995
|
+
try {
|
|
996
|
+
await liveQuery.query.interrupt?.();
|
|
997
|
+
}
|
|
998
|
+
catch {
|
|
999
|
+
// best effort
|
|
1000
|
+
}
|
|
1001
|
+
if (this.currentTurn === currentTurn) {
|
|
1002
|
+
const timer = setTimeout(() => {
|
|
1003
|
+
if (this.currentTurn === currentTurn) {
|
|
1004
|
+
this.closeLiveQuery(liveQuery);
|
|
1005
|
+
}
|
|
1006
|
+
}, INTERRUPT_GRACE_MS);
|
|
1007
|
+
timer.unref?.();
|
|
1008
|
+
}
|
|
1009
|
+
return true;
|
|
1010
|
+
}
|
|
896
1011
|
try {
|
|
897
1012
|
await currentTurn.query?.interrupt?.();
|
|
898
1013
|
}
|
|
@@ -913,6 +1028,168 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
913
1028
|
}
|
|
914
1029
|
return true;
|
|
915
1030
|
}
|
|
1031
|
+
createTurnState({ suppressReply = false, onProgress = null } = {}) {
|
|
1032
|
+
return {
|
|
1033
|
+
abortController: null,
|
|
1034
|
+
liveQuery: null,
|
|
1035
|
+
onProgress,
|
|
1036
|
+
settle: null,
|
|
1037
|
+
suppressReply,
|
|
1038
|
+
compactMetadata: null,
|
|
1039
|
+
compactError: "",
|
|
1040
|
+
conversationReset: "",
|
|
1041
|
+
emittedAssistantMessage: false,
|
|
1042
|
+
fullText: "",
|
|
1043
|
+
items: [],
|
|
1044
|
+
query: null,
|
|
1045
|
+
resultMessage: null,
|
|
1046
|
+
terminalWorkingStatusEmitted: false,
|
|
1047
|
+
usageBase: null,
|
|
1048
|
+
usageByMessageId: new Map(),
|
|
1049
|
+
uuid: "",
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* One claude process per session, fed through a prompt stream that stays
|
|
1054
|
+
* open between turns. A one-shot string prompt closes claude's input at once,
|
|
1055
|
+
* and claude then kills background subagents 10 minutes after the main
|
|
1056
|
+
* agent's turn ends (CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS). Kept open, the
|
|
1057
|
+
* subagents run to completion and claude starts its own follow-up turn on
|
|
1058
|
+
* their notifications.
|
|
1059
|
+
*/
|
|
1060
|
+
ensureLiveQuery(sdkModule) {
|
|
1061
|
+
if (this.liveQuery && !this.liveQuery.ended) {
|
|
1062
|
+
return this.liveQuery;
|
|
1063
|
+
}
|
|
1064
|
+
const abortController = new AbortController();
|
|
1065
|
+
const liveQuery = {
|
|
1066
|
+
abortController,
|
|
1067
|
+
backgroundTurn: null,
|
|
1068
|
+
ended: false,
|
|
1069
|
+
input: createInputQueue(),
|
|
1070
|
+
query: null,
|
|
1071
|
+
usageBase: null,
|
|
1072
|
+
};
|
|
1073
|
+
liveQuery.query = sdkModule.query({
|
|
1074
|
+
prompt: liveQuery.input,
|
|
1075
|
+
options: this.buildSdkOptions(abortController),
|
|
1076
|
+
});
|
|
1077
|
+
this.liveQuery = liveQuery;
|
|
1078
|
+
void this.pumpLiveQuery(liveQuery);
|
|
1079
|
+
return liveQuery;
|
|
1080
|
+
}
|
|
1081
|
+
closeLiveQuery(liveQuery) {
|
|
1082
|
+
liveQuery.ended = true;
|
|
1083
|
+
if (this.liveQuery === liveQuery) {
|
|
1084
|
+
this.liveQuery = null;
|
|
1085
|
+
}
|
|
1086
|
+
liveQuery.input.close();
|
|
1087
|
+
try {
|
|
1088
|
+
liveQuery.abortController.abort();
|
|
1089
|
+
}
|
|
1090
|
+
catch {
|
|
1091
|
+
// best effort
|
|
1092
|
+
}
|
|
1093
|
+
try {
|
|
1094
|
+
liveQuery.query?.close?.();
|
|
1095
|
+
}
|
|
1096
|
+
catch {
|
|
1097
|
+
// best effort
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
settleTurn(turn, error, resultMessage = null) {
|
|
1101
|
+
const settle = turn.settle;
|
|
1102
|
+
turn.settle = null;
|
|
1103
|
+
settle?.(error, resultMessage);
|
|
1104
|
+
}
|
|
1105
|
+
async pumpLiveQuery(liveQuery) {
|
|
1106
|
+
let failure = null;
|
|
1107
|
+
try {
|
|
1108
|
+
for await (const message of liveQuery.query) {
|
|
1109
|
+
await this.dispatchLiveMessage(liveQuery, message);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
catch (error) {
|
|
1113
|
+
failure = error;
|
|
1114
|
+
}
|
|
1115
|
+
this.closeLiveQuery(liveQuery);
|
|
1116
|
+
const turn = this.currentTurn;
|
|
1117
|
+
if (turn?.liveQuery === liveQuery && turn.settle) {
|
|
1118
|
+
this.settleTurn(turn, failure, turn.resultMessage);
|
|
1119
|
+
}
|
|
1120
|
+
else if (failure && !this.closeRequested) {
|
|
1121
|
+
this.trace(`claude process ended between turns: ${failure?.message || failure}`);
|
|
1122
|
+
}
|
|
1123
|
+
const backgroundTurn = liveQuery.backgroundTurn;
|
|
1124
|
+
liveQuery.backgroundTurn = null;
|
|
1125
|
+
if (backgroundTurn) {
|
|
1126
|
+
await this.finishBackgroundTurn(backgroundTurn, null, failure);
|
|
1127
|
+
}
|
|
1128
|
+
else if (failure && !this.closeRequested && !this.currentTurn) {
|
|
1129
|
+
// The process died after a background turn's result, e.g. an error
|
|
1130
|
+
// result (quota hit) makes claude exit right after reporting it. No turn
|
|
1131
|
+
// is left to own the failure, so report it as a background one.
|
|
1132
|
+
await this.emitWorkingStatus({
|
|
1133
|
+
phase: "turn_failed",
|
|
1134
|
+
status_done_line: failure?.message || String(failure),
|
|
1135
|
+
reply_in_progress: false,
|
|
1136
|
+
background: true,
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Routes the long-lived query's output. A result ends the pending turn when it
|
|
1142
|
+
* echoes that turn's user message uuid (or echoes none while no follow-up turn
|
|
1143
|
+
* of claude's own is running); everything else is a background turn, e.g.
|
|
1144
|
+
* claude answering a finished subagent's notification between user turns.
|
|
1145
|
+
*/
|
|
1146
|
+
async dispatchLiveMessage(liveQuery, message) {
|
|
1147
|
+
const turn = this.currentTurn?.liveQuery === liveQuery && this.currentTurn.settle ? this.currentTurn : null;
|
|
1148
|
+
const backgroundTurn = liveQuery.backgroundTurn;
|
|
1149
|
+
if (message?.type !== "result") {
|
|
1150
|
+
const target = backgroundTurn || turn || (liveQuery.backgroundTurn = this.createTurnState());
|
|
1151
|
+
await this.handleSdkMessage(message, target, { onProgress: target.onProgress });
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
liveQuery.backgroundTurn = null;
|
|
1155
|
+
const uuids = resultUserMessageUuids(message);
|
|
1156
|
+
if (turn && (uuids.length ? uuids.includes(turn.uuid) : !backgroundTurn)) {
|
|
1157
|
+
// A background turn that folded the user's message in ends as this turn.
|
|
1158
|
+
if (backgroundTurn?.emittedAssistantMessage) {
|
|
1159
|
+
turn.emittedAssistantMessage = true;
|
|
1160
|
+
}
|
|
1161
|
+
await this.handleSdkMessage(message, turn, { onProgress: turn.onProgress });
|
|
1162
|
+
liveQuery.usageBase = modelUsageTotal(message) || liveQuery.usageBase;
|
|
1163
|
+
this.settleTurn(turn, null, message);
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
const target = backgroundTurn || this.createTurnState();
|
|
1167
|
+
await this.handleSdkMessage(message, target, {});
|
|
1168
|
+
await this.finishBackgroundTurn(target, message);
|
|
1169
|
+
}
|
|
1170
|
+
async finishBackgroundTurn(backgroundTurn, resultMessage, failure = null) {
|
|
1171
|
+
// An error result (e.g. "You've hit your session limit") can carry subtype
|
|
1172
|
+
// "success"; `is_error` is what marks it.
|
|
1173
|
+
const errorResult = Boolean(resultMessage && (resultMessage.subtype !== "success" || resultMessage.is_error === true));
|
|
1174
|
+
const failed = Boolean(failure) || errorResult;
|
|
1175
|
+
const text = normalizeText(resultMessage?.result);
|
|
1176
|
+
// An error result's text still reaches the chat, as it always has.
|
|
1177
|
+
if (!failure && resultMessage?.subtype === "success" && text && !backgroundTurn.emittedAssistantMessage) {
|
|
1178
|
+
await this.emitAssistantMessage(text);
|
|
1179
|
+
}
|
|
1180
|
+
// A pending user turn owns the working status.
|
|
1181
|
+
if (this.currentTurn) {
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
await this.emitTerminalWorkingStatus(backgroundTurn, failed
|
|
1185
|
+
? {
|
|
1186
|
+
phase: "turn_failed",
|
|
1187
|
+
status_done_line: failure?.message ||
|
|
1188
|
+
(resultMessage?.is_error === true && text ? text : extractResultErrorMessage(resultMessage)),
|
|
1189
|
+
background: true,
|
|
1190
|
+
}
|
|
1191
|
+
: { phase: "turn_completed", status_done_line: "claude finished", background: true });
|
|
1192
|
+
}
|
|
916
1193
|
async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null, jsonSchema = null, suppressReply = false } = {}) {
|
|
917
1194
|
if (this.closeRequested) {
|
|
918
1195
|
throw this.createSessionClosedError();
|
|
@@ -942,31 +1219,24 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
942
1219
|
if (!suppressReply) {
|
|
943
1220
|
this.history.push({ role: "user", content: promptText });
|
|
944
1221
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
conversationReset: "",
|
|
952
|
-
emittedAssistantMessage: false,
|
|
953
|
-
fullText: "",
|
|
954
|
-
items: [],
|
|
955
|
-
query: null,
|
|
956
|
-
resultMessage: null,
|
|
957
|
-
terminalWorkingStatusEmitted: false,
|
|
958
|
-
usageByMessageId: new Map(),
|
|
959
|
-
};
|
|
1222
|
+
// Structured output is fixed when the process starts, so a jsonSchema turn
|
|
1223
|
+
// runs on a one-shot query of its own.
|
|
1224
|
+
const oneShot = Boolean(jsonSchema && typeof jsonSchema === "object");
|
|
1225
|
+
const abortController = oneShot ? new AbortController() : null;
|
|
1226
|
+
const currentTurn = this.createTurnState({ suppressReply, onProgress });
|
|
1227
|
+
currentTurn.abortController = abortController;
|
|
960
1228
|
this.currentTurn = currentTurn;
|
|
961
1229
|
this.markTurnStartedStatus();
|
|
962
|
-
const
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
abortController
|
|
1230
|
+
const stopTurn = () => {
|
|
1231
|
+
if (currentTurn.liveQuery) {
|
|
1232
|
+
this.closeLiveQuery(currentTurn.liveQuery);
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
abortController?.abort();
|
|
968
1236
|
currentTurn.query?.close?.();
|
|
969
|
-
}
|
|
1237
|
+
};
|
|
1238
|
+
const closeGuard = this.createCloseGuard(stopTurn);
|
|
1239
|
+
const turnTimeoutGuard = this.createTurnTimeoutGuard(stopTurn);
|
|
970
1240
|
const previousOutputFormat = this.options.outputFormat;
|
|
971
1241
|
if (jsonSchema && typeof jsonSchema === "object") {
|
|
972
1242
|
this.options.outputFormat = { type: "json_schema", schema: jsonSchema };
|
|
@@ -977,28 +1247,45 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
977
1247
|
reply_in_progress: true,
|
|
978
1248
|
status_line: "claude is working",
|
|
979
1249
|
}, onProgress);
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1250
|
+
let turnResult;
|
|
1251
|
+
if (oneShot) {
|
|
1252
|
+
const query = sdkModule.query({
|
|
1253
|
+
prompt: media.length ? buildClaudeInput(effectivePrompt, media) : effectivePrompt,
|
|
1254
|
+
options: this.buildSdkOptions(abortController),
|
|
1255
|
+
});
|
|
1256
|
+
currentTurn.query = query;
|
|
1257
|
+
turnResult = (async () => {
|
|
987
1258
|
for await (const message of query) {
|
|
988
1259
|
await this.handleSdkMessage(message, currentTurn, { onProgress });
|
|
989
1260
|
}
|
|
990
1261
|
return currentTurn.resultMessage;
|
|
991
|
-
})()
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1262
|
+
})();
|
|
1263
|
+
}
|
|
1264
|
+
else {
|
|
1265
|
+
turnResult = new Promise((resolve, reject) => {
|
|
1266
|
+
currentTurn.settle = (error, message) => (error ? reject(error) : resolve(message));
|
|
1267
|
+
});
|
|
1268
|
+
const liveQuery = this.ensureLiveQuery(sdkModule);
|
|
1269
|
+
currentTurn.liveQuery = liveQuery;
|
|
1270
|
+
currentTurn.query = liveQuery.query;
|
|
1271
|
+
currentTurn.usageBase = liveQuery.usageBase;
|
|
1272
|
+
currentTurn.uuid = randomUUID();
|
|
1273
|
+
if (!liveQuery.input.push(buildClaudeUserMessage(effectivePrompt, media, currentTurn.uuid))) {
|
|
1274
|
+
this.settleTurn(currentTurn, createTurnError("Claude process input is closed", { reason: "process_exited" }));
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
turnResult.catch(() => { });
|
|
1278
|
+
const resultMessage = await Promise.race([turnResult, closeGuard.promise, turnTimeoutGuard.promise]);
|
|
995
1279
|
if (!resultMessage) {
|
|
996
1280
|
throw createTurnError("Claude query ended without a result message", {
|
|
997
1281
|
reason: "missing_result",
|
|
998
1282
|
});
|
|
999
1283
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1284
|
+
// An error result (e.g. "You've hit your session limit") can carry subtype
|
|
1285
|
+
// "success"; `is_error` is what marks it.
|
|
1286
|
+
if (resultMessage.subtype !== "success" || resultMessage.is_error === true) {
|
|
1287
|
+
const errorText = resultMessage.is_error === true ? normalizeText(resultMessage.result) : "";
|
|
1288
|
+
const errorMessage = errorText || extractResultErrorMessage(resultMessage);
|
|
1002
1289
|
this.maybeEmitAuthRequired(errorMessage, errorMessage);
|
|
1003
1290
|
await this.emitTerminalWorkingStatus(currentTurn, {
|
|
1004
1291
|
phase: "turn_failed",
|
|
@@ -1012,7 +1299,7 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
1012
1299
|
? [...resultMessage.permission_denials]
|
|
1013
1300
|
: [],
|
|
1014
1301
|
// A failed turn still spent tokens.
|
|
1015
|
-
usage: resultUsage(resultMessage),
|
|
1302
|
+
usage: resultUsage(resultMessage, currentTurn.usageBase),
|
|
1016
1303
|
});
|
|
1017
1304
|
}
|
|
1018
1305
|
const structuredOutput = jsonSchema && resultMessage.structured_output && typeof resultMessage.structured_output === "object"
|
|
@@ -1037,7 +1324,7 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
1037
1324
|
}, onProgress);
|
|
1038
1325
|
return {
|
|
1039
1326
|
text: responseText,
|
|
1040
|
-
usage: resultUsage(resultMessage),
|
|
1327
|
+
usage: resultUsage(resultMessage, currentTurn.usageBase),
|
|
1041
1328
|
items: currentTurn.items,
|
|
1042
1329
|
events: [],
|
|
1043
1330
|
provider: this.backend,
|
|
@@ -1059,7 +1346,7 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
1059
1346
|
// A failed turn still spent tokens; an interrupted query ends without a
|
|
1060
1347
|
// (final) result, so fall back to the last result plus what streamed since.
|
|
1061
1348
|
const usage = error?.usage ??
|
|
1062
|
-
sumStreamedUsage(currentTurn.usageByMessageId, currentTurn.resultMessage ? resultUsage(currentTurn.resultMessage) : null);
|
|
1349
|
+
sumStreamedUsage(currentTurn.usageByMessageId, currentTurn.resultMessage ? resultUsage(currentTurn.resultMessage, currentTurn.usageBase) : null);
|
|
1063
1350
|
if (error?.reason === "turn_timeout") {
|
|
1064
1351
|
await this.interruptCurrentTurn();
|
|
1065
1352
|
}
|
|
@@ -1094,11 +1381,14 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
1094
1381
|
}
|
|
1095
1382
|
closeGuard.cleanup();
|
|
1096
1383
|
turnTimeoutGuard.cleanup();
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1384
|
+
currentTurn.settle = null;
|
|
1385
|
+
if (oneShot) {
|
|
1386
|
+
try {
|
|
1387
|
+
currentTurn.query?.close?.();
|
|
1388
|
+
}
|
|
1389
|
+
catch {
|
|
1390
|
+
// best effort
|
|
1391
|
+
}
|
|
1102
1392
|
}
|
|
1103
1393
|
}
|
|
1104
1394
|
}
|
|
@@ -1272,7 +1562,12 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
1272
1562
|
}
|
|
1273
1563
|
this.closeRequested = true;
|
|
1274
1564
|
this.flushCloseWaiters();
|
|
1275
|
-
|
|
1565
|
+
if (!this.currentTurn?.liveQuery) {
|
|
1566
|
+
await this.interruptCurrentTurn();
|
|
1567
|
+
}
|
|
1568
|
+
if (this.liveQuery) {
|
|
1569
|
+
this.closeLiveQuery(this.liveQuery);
|
|
1570
|
+
}
|
|
1276
1571
|
this.closed = true;
|
|
1277
1572
|
}
|
|
1278
1573
|
}
|
|
@@ -157,6 +157,13 @@ function quoteShellArg(value) {
|
|
|
157
157
|
}
|
|
158
158
|
return `'${normalized.replace(/'/g, `'\\''`)}'`;
|
|
159
159
|
}
|
|
160
|
+
// Kimi Code reports a detached launch (`Agent`/`Bash` with run_in_background)
|
|
161
|
+
// with these metadata lines; the task's completion later wakes the main agent
|
|
162
|
+
// inside the same `kimi -p` process.
|
|
163
|
+
function isBackgroundTaskLaunch(content) {
|
|
164
|
+
const text = normalizeTextContent(content);
|
|
165
|
+
return /^task_id: /m.test(text) && /^automatic_notification: true$/m.test(text);
|
|
166
|
+
}
|
|
160
167
|
function normalizeTextContent(content) {
|
|
161
168
|
if (typeof content === "string") {
|
|
162
169
|
return content;
|
|
@@ -633,7 +640,9 @@ export class KimiPrintSession extends EventEmitter {
|
|
|
633
640
|
if (currentTurn.settled) {
|
|
634
641
|
return;
|
|
635
642
|
}
|
|
636
|
-
|
|
643
|
+
// `kimi -p` stays silent while it waits for background tasks it
|
|
644
|
+
// launched; killing it on idle would kill those tasks too.
|
|
645
|
+
if (Date.now() - lastActivityAt < this.turnDeadlineMs || currentTurn.backgroundTaskLaunched) {
|
|
637
646
|
scheduleTurnDeadline();
|
|
638
647
|
return;
|
|
639
648
|
}
|
|
@@ -700,6 +709,10 @@ export class KimiPrintSession extends EventEmitter {
|
|
|
700
709
|
}
|
|
701
710
|
if (role === "tool") {
|
|
702
711
|
noteToolFinished(currentTurn, payload.tool_call_id);
|
|
712
|
+
if (!currentTurn.backgroundTaskLaunched && isBackgroundTaskLaunch(payload.content)) {
|
|
713
|
+
currentTurn.backgroundTaskLaunched = true;
|
|
714
|
+
this.trace("background task launched; turn idle deadline suspended until kimi exits");
|
|
715
|
+
}
|
|
703
716
|
void this.emitWorkingStatus({
|
|
704
717
|
phase: "command_execution",
|
|
705
718
|
reply_in_progress: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/ai-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/lovemoon-ai/conductor.git"
|
|
@@ -53,12 +53,12 @@
|
|
|
53
53
|
"zod": "^4.1.5"
|
|
54
54
|
},
|
|
55
55
|
"optionalDependencies": {
|
|
56
|
-
"@love-moon/chat-web": "^0.
|
|
56
|
+
"@love-moon/chat-web": "^0.17.1"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/node": "^22.10.2",
|
|
60
60
|
"tsx": "^4.20.6",
|
|
61
61
|
"typescript": "^5.6.3"
|
|
62
62
|
},
|
|
63
|
-
"gitCommitId": "
|
|
63
|
+
"gitCommitId": "e3dbbad"
|
|
64
64
|
}
|