@love-moon/ai-sdk 0.13.0 → 0.13.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/CHANGELOG.md +35 -0
- package/dist/client.d.ts +10 -0
- package/dist/client.js +38 -1
- package/dist/providers/claude-agent-sdk-session.d.ts +25 -1
- package/dist/providers/claude-agent-sdk-session.js +114 -11
- package/dist/providers/codex-app-server-session.d.ts +44 -0
- package/dist/providers/codex-app-server-session.js +182 -12
- package/dist/providers/codex-exec-session.js +9 -2
- package/dist/providers/copilot-sdk-session.d.ts +29 -1
- package/dist/providers/copilot-sdk-session.js +115 -18
- package/dist/providers/dsh-sdk-session.d.ts +20 -1
- package/dist/providers/dsh-sdk-session.js +81 -10
- package/dist/providers/kimi-cli-session.d.ts +18 -1
- package/dist/providers/kimi-cli-session.js +40 -7
- package/dist/providers/kimi-print-session.d.ts +16 -1
- package/dist/providers/kimi-print-session.js +51 -6
- package/dist/providers/opencode-sdk-session.d.ts +44 -2
- package/dist/providers/opencode-sdk-session.js +177 -26
- package/dist/shared.d.ts +78 -0
- package/dist/shared.js +95 -1
- package/dist/worker.js +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# @love-moon/ai-sdk
|
|
2
2
|
|
|
3
|
+
## 0.13.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 8c2833d: Support `/compact [focus instructions]` in task chats. Fire detects the command
|
|
8
|
+
per message (like `/goal`), runs the backend's native context compaction
|
|
9
|
+
instead of sending the text to the model, and posts one confirmation with the
|
|
10
|
+
token savings. Sessions advertise `capabilities.compact` and implement the new
|
|
11
|
+
optional `runCompact()`: claude (native `/compact`), codex app-server
|
|
12
|
+
(`thread/compact/start`), copilot (`session.history.compact`), kimi wire and
|
|
13
|
+
legacy print (built-in `/compact`), opencode (`session.summarize`) and dsh
|
|
14
|
+
(summary turn, then a fresh session seeded with the summary). Backends without
|
|
15
|
+
the capability (chat-web, codex exec, Kimi Code prompt mode) reply with a
|
|
16
|
+
not-supported notice. Opencode compaction summaries no longer surface as chat
|
|
17
|
+
replies, and dsh clears its "compacting context" status when automatic
|
|
18
|
+
compaction ends.
|
|
19
|
+
- 8e3a4c3: Report how many model tokens each turn consumed. After every turn (or `/goal`)
|
|
20
|
+
`conductor fire` sends the turn's token count (fresh input + cache reads/writes +
|
|
21
|
+
output) over the new `task_turn_usage` websocket event, and the server adds it
|
|
22
|
+
to the task's running total shown in the task detail card. A persistent task's
|
|
23
|
+
total restarts with each round.
|
|
24
|
+
|
|
25
|
+
Failed or interrupted turns and `/compact` are counted too; when a turn's usage
|
|
26
|
+
is unknown the fire reports `null`, which clears the task's last-turn count.
|
|
27
|
+
|
|
28
|
+
- `@love-moon/conductor-sdk`: new `ConductorClient.sendTurnUsage(taskId, { tokens })`.
|
|
29
|
+
- `@love-moon/ai-sdk`: the Codex app-server provider's turn/goal `usage` now
|
|
30
|
+
carries `turnTotalTokens`, the turn's share of Codex's thread-cumulative total.
|
|
31
|
+
Claude and Codex turn errors carry the tokens spent before the failure as
|
|
32
|
+
`error.usage` (Claude sums streamed usage when an interrupted query ends
|
|
33
|
+
without a result).
|
|
34
|
+
- Claude and Codex backends are counted; other backends report nothing yet.
|
|
35
|
+
|
|
36
|
+
## 0.13.1
|
|
37
|
+
|
|
3
38
|
## 0.13.0
|
|
4
39
|
|
|
5
40
|
## 0.12.0
|
package/dist/client.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export class RemoteAiSession extends EventEmitter<[never]> {
|
|
|
18
18
|
workerReady: boolean;
|
|
19
19
|
capabilities: {
|
|
20
20
|
goal?: boolean | undefined;
|
|
21
|
+
compact?: boolean | undefined;
|
|
21
22
|
};
|
|
22
23
|
};
|
|
23
24
|
sessionMessageHandler: any;
|
|
@@ -51,11 +52,17 @@ export class RemoteAiSession extends EventEmitter<[never]> {
|
|
|
51
52
|
workerReady: boolean;
|
|
52
53
|
capabilities: {
|
|
53
54
|
goal?: boolean | undefined;
|
|
55
|
+
compact?: boolean | undefined;
|
|
54
56
|
};
|
|
55
57
|
};
|
|
56
58
|
usesSessionFileReplyStream(): boolean;
|
|
57
59
|
getSessionInfo(): any;
|
|
58
60
|
getCurrentTurnStatus(): any;
|
|
61
|
+
/**
|
|
62
|
+
* Ask the worker for the live turn status (including `active_tool`). Uses
|
|
63
|
+
* the control lane so it answers while `runTurn` holds the request queue.
|
|
64
|
+
*/
|
|
65
|
+
fetchCurrentTurnStatus(): Promise<any>;
|
|
59
66
|
setSessionMessageHandler(handler: any): void;
|
|
60
67
|
setWorkingStatusHandler(handler: any): void;
|
|
61
68
|
setSessionReplyTarget(replyTo: any): void;
|
|
@@ -64,6 +71,7 @@ export class RemoteAiSession extends EventEmitter<[never]> {
|
|
|
64
71
|
interruptCurrentTurn(): Promise<any>;
|
|
65
72
|
runTurn(promptText: any, options?: {}): Promise<any>;
|
|
66
73
|
runGoal(goal: any, options?: {}): Promise<any>;
|
|
74
|
+
runCompact(request?: {}, options?: {}): Promise<any>;
|
|
67
75
|
getGoal(): Promise<any>;
|
|
68
76
|
clearGoal(): Promise<any>;
|
|
69
77
|
close(): Promise<void>;
|
|
@@ -103,6 +111,7 @@ declare class LocalAiSessionProxy extends EventEmitter<[never]> {
|
|
|
103
111
|
workerReady: boolean;
|
|
104
112
|
capabilities: {
|
|
105
113
|
goal?: boolean | undefined;
|
|
114
|
+
compact?: boolean | undefined;
|
|
106
115
|
};
|
|
107
116
|
};
|
|
108
117
|
readyPromise: Promise<any>;
|
|
@@ -121,6 +130,7 @@ declare class LocalAiSessionProxy extends EventEmitter<[never]> {
|
|
|
121
130
|
interruptCurrentTurn(): Promise<any>;
|
|
122
131
|
runTurn(promptText: any, options?: {}): Promise<any>;
|
|
123
132
|
runGoal(goal: any, options?: {}): Promise<any>;
|
|
133
|
+
runCompact(request?: {}, options?: {}): Promise<any>;
|
|
124
134
|
getGoal(): Promise<any>;
|
|
125
135
|
clearGoal(): Promise<any>;
|
|
126
136
|
close(): Promise<void>;
|
package/dist/client.js
CHANGED
|
@@ -9,6 +9,11 @@ function createUnsupportedGoalError(name) {
|
|
|
9
9
|
err.reason = "unsupported_goal";
|
|
10
10
|
return err;
|
|
11
11
|
}
|
|
12
|
+
function createUnsupportedCompactError(name) {
|
|
13
|
+
const err = new Error(`runCompact() is not supported by backend session (${name || "unknown"}).`);
|
|
14
|
+
err.reason = "unsupported_compact";
|
|
15
|
+
return err;
|
|
16
|
+
}
|
|
12
17
|
const WORKER_PATH = fileURLToPath(new URL("./worker.js", import.meta.url));
|
|
13
18
|
function sanitizeOptionsForWorker(options = {}) {
|
|
14
19
|
const sanitized = {};
|
|
@@ -155,6 +160,17 @@ export class RemoteAiSession extends EventEmitter {
|
|
|
155
160
|
getCurrentTurnStatus() {
|
|
156
161
|
return this.currentTurnStatus ? { ...this.currentTurnStatus } : null;
|
|
157
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Ask the worker for the live turn status (including `active_tool`). Uses
|
|
165
|
+
* the control lane so it answers while `runTurn` holds the request queue.
|
|
166
|
+
*/
|
|
167
|
+
async fetchCurrentTurnStatus() {
|
|
168
|
+
const status = await this.callWorker("getCurrentTurnStatus", [], { messageType: "control" });
|
|
169
|
+
if (status && typeof status === "object") {
|
|
170
|
+
this.currentTurnStatus = { ...status };
|
|
171
|
+
}
|
|
172
|
+
return this.getCurrentTurnStatus();
|
|
173
|
+
}
|
|
158
174
|
setSessionMessageHandler(handler) {
|
|
159
175
|
this.sessionMessageHandler = typeof handler === "function" ? handler : null;
|
|
160
176
|
}
|
|
@@ -172,7 +188,9 @@ export class RemoteAiSession extends EventEmitter {
|
|
|
172
188
|
return this.callWorker("ensureSessionInfo", []);
|
|
173
189
|
}
|
|
174
190
|
async getSessionUsageSummary() {
|
|
175
|
-
|
|
191
|
+
// Control lane: fire awaits this before every runtime-status report, so
|
|
192
|
+
// queueing it behind a running `runTurn` would stall mid-turn statuses.
|
|
193
|
+
return this.callWorker("getSessionUsageSummary", [], { messageType: "control" });
|
|
176
194
|
}
|
|
177
195
|
async interruptCurrentTurn() {
|
|
178
196
|
return this.callWorker("interruptCurrentTurn", [], {
|
|
@@ -204,6 +222,16 @@ export class RemoteAiSession extends EventEmitter {
|
|
|
204
222
|
progressHandler: typeof onProgress === "function" ? onProgress : null,
|
|
205
223
|
});
|
|
206
224
|
}
|
|
225
|
+
async runCompact(request = {}, options = {}) {
|
|
226
|
+
await this.readyPromise;
|
|
227
|
+
if (!this.snapshot?.capabilities || this.snapshot.capabilities.compact !== true) {
|
|
228
|
+
throw createUnsupportedCompactError(this.snapshot?.provider || this.snapshot?.backend);
|
|
229
|
+
}
|
|
230
|
+
const { onProgress, ...restOptions } = options || {};
|
|
231
|
+
return this.callWorker("runCompact", [request, restOptions], {
|
|
232
|
+
progressHandler: typeof onProgress === "function" ? onProgress : null,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
207
235
|
async getGoal() {
|
|
208
236
|
// Propagate worker-creation errors instead of masking them as "no goal".
|
|
209
237
|
// See N3 in the review.
|
|
@@ -662,6 +690,15 @@ class LocalAiSessionProxy extends EventEmitter {
|
|
|
662
690
|
}
|
|
663
691
|
return await session.runGoal(goal, options);
|
|
664
692
|
}
|
|
693
|
+
async runCompact(request = {}, options = {}) {
|
|
694
|
+
const session = await this.readyPromise;
|
|
695
|
+
// Capabilities resolved at init (snapshot first) are authoritative; some
|
|
696
|
+
// providers only advertise them via getSnapshot().
|
|
697
|
+
if (this.snapshot?.capabilities?.compact !== true || typeof session.runCompact !== "function") {
|
|
698
|
+
throw createUnsupportedCompactError(session?.constructor?.name);
|
|
699
|
+
}
|
|
700
|
+
return await session.runCompact(request, options);
|
|
701
|
+
}
|
|
665
702
|
async getGoal() {
|
|
666
703
|
const session = await this.readyPromise;
|
|
667
704
|
const capabilities = resolveSessionCapabilities(session);
|
|
@@ -9,6 +9,7 @@ export function resolveClaudeCommandForRoot(commandLine: any, env?: NodeJS.Proce
|
|
|
9
9
|
export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
|
|
10
10
|
static capabilities: Readonly<{
|
|
11
11
|
goal: true;
|
|
12
|
+
compact: true;
|
|
12
13
|
media: Readonly<{
|
|
13
14
|
image: "native";
|
|
14
15
|
}>;
|
|
@@ -16,6 +17,7 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
|
|
|
16
17
|
constructor(backend: any, options?: {});
|
|
17
18
|
getCapabilities(): {
|
|
18
19
|
goal: true;
|
|
20
|
+
compact: true;
|
|
19
21
|
media: Readonly<{
|
|
20
22
|
image: "native";
|
|
21
23
|
}>;
|
|
@@ -42,12 +44,16 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
|
|
|
42
44
|
manualResumeReady: boolean;
|
|
43
45
|
currentTurn: {
|
|
44
46
|
abortController: AbortController;
|
|
47
|
+
suppressReply: boolean;
|
|
48
|
+
compactMetadata: null;
|
|
49
|
+
compactError: string;
|
|
45
50
|
emittedAssistantMessage: boolean;
|
|
46
51
|
fullText: string;
|
|
47
52
|
items: never[];
|
|
48
53
|
query: null;
|
|
49
54
|
resultMessage: null;
|
|
50
55
|
terminalWorkingStatusEmitted: boolean;
|
|
56
|
+
usageByMessageId: Map<any, any>;
|
|
51
57
|
} | null;
|
|
52
58
|
lastResult: any;
|
|
53
59
|
rateLimitInfo: any;
|
|
@@ -88,6 +94,7 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
|
|
|
88
94
|
currentTurnStatus: any;
|
|
89
95
|
capabilities: {
|
|
90
96
|
goal: true;
|
|
97
|
+
compact: true;
|
|
91
98
|
media: Readonly<{
|
|
92
99
|
image: "native";
|
|
93
100
|
}>;
|
|
@@ -154,10 +161,11 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
|
|
|
154
161
|
onProgress: any;
|
|
155
162
|
}): Promise<void>;
|
|
156
163
|
interruptCurrentTurn(): Promise<boolean>;
|
|
157
|
-
runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress, jsonSchema }?: {
|
|
164
|
+
runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress, jsonSchema, suppressReply }?: {
|
|
158
165
|
useInitialImages?: boolean | undefined;
|
|
159
166
|
onProgress?: null | undefined;
|
|
160
167
|
jsonSchema?: null | undefined;
|
|
168
|
+
suppressReply?: boolean | undefined;
|
|
161
169
|
}): Promise<{
|
|
162
170
|
text: string;
|
|
163
171
|
usage: null;
|
|
@@ -165,6 +173,8 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
|
|
|
165
173
|
events: never[];
|
|
166
174
|
provider?: undefined;
|
|
167
175
|
metadata?: undefined;
|
|
176
|
+
compactMetadata?: undefined;
|
|
177
|
+
compactError?: undefined;
|
|
168
178
|
} | {
|
|
169
179
|
text: any;
|
|
170
180
|
usage: any;
|
|
@@ -178,6 +188,8 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
|
|
|
178
188
|
modelUsage: any;
|
|
179
189
|
structuredOutput: any;
|
|
180
190
|
};
|
|
191
|
+
compactMetadata: undefined;
|
|
192
|
+
compactError: string | undefined;
|
|
181
193
|
}>;
|
|
182
194
|
/**
|
|
183
195
|
* Trigger Claude's native `/goal` slash command. Implemented by sending the
|
|
@@ -204,6 +216,18 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
|
|
|
204
216
|
* @returns {Promise<import("../shared.js").GoalResult>}
|
|
205
217
|
*/
|
|
206
218
|
runGoal(goal: import("../shared.js").GoalRequest, options?: object): Promise<import("../shared.js").GoalResult>;
|
|
219
|
+
/**
|
|
220
|
+
* Manually compact the conversation via Claude's native `/compact` slash
|
|
221
|
+
* command. Custom focus instructions are forwarded as the command argument.
|
|
222
|
+
* No assistant message is emitted; the caller renders the confirmation.
|
|
223
|
+
*
|
|
224
|
+
* @param {import("../shared.js").CompactRequest} [request]
|
|
225
|
+
* @param {{ onProgress?: Function }} [options]
|
|
226
|
+
* @returns {Promise<import("../shared.js").CompactResult>}
|
|
227
|
+
*/
|
|
228
|
+
runCompact(request?: import("../shared.js").CompactRequest, { onProgress }?: {
|
|
229
|
+
onProgress?: Function;
|
|
230
|
+
}): Promise<import("../shared.js").CompactResult>;
|
|
207
231
|
close(): Promise<void>;
|
|
208
232
|
}
|
|
209
233
|
import { EventEmitter } from "node:events";
|
|
@@ -3,7 +3,7 @@ import { CLAUDE_AGENT_SDK_VARIANT as CLAUDE_PROVIDER_VARIANT } from "../built-in
|
|
|
3
3
|
import { appendContextFilesToPrompt } from "../context-files.js";
|
|
4
4
|
import { PROVIDER_MEDIA_CAPABILITIES, buildClaudeContent } from "../media-adapters.js";
|
|
5
5
|
import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
|
|
6
|
-
import { emitLog, extractLongFlagFromCommandLine, getBoundedEnvInt, isGoalStatus, loadEnvConfig, normalizeLogger, proxyToEnv, sanitizeForLog, } from "../shared.js";
|
|
6
|
+
import { emitLog, extractLongFlagFromCommandLine, getBoundedEnvInt, isGoalStatus, loadEnvConfig, normalizeLogger, noteToolFinished, noteToolStarted, proxyToEnv, sanitizeForLog, withActiveTool, } from "../shared.js";
|
|
7
7
|
const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
|
|
8
8
|
const MIN_TURN_DEADLINE_MS = 30 * 1000;
|
|
9
9
|
const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
|
|
@@ -32,6 +32,22 @@ function createTurnError(message, extras = {}) {
|
|
|
32
32
|
}
|
|
33
33
|
return error;
|
|
34
34
|
}
|
|
35
|
+
/** Per-turn usage summed from streamed assistant messages (one entry per API response). */
|
|
36
|
+
function sumStreamedUsage(usageByMessageId) {
|
|
37
|
+
if (!usageByMessageId.size) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const total = {};
|
|
41
|
+
for (const usage of usageByMessageId.values()) {
|
|
42
|
+
for (const key of ["input_tokens", "cache_creation_input_tokens", "cache_read_input_tokens", "output_tokens"]) {
|
|
43
|
+
const value = Number(usage?.[key]);
|
|
44
|
+
if (Number.isFinite(value)) {
|
|
45
|
+
total[key] = (total[key] || 0) + value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return total;
|
|
50
|
+
}
|
|
35
51
|
function normalizeClaudeBackend(backend) {
|
|
36
52
|
const normalized = String(backend || "").trim().toLowerCase();
|
|
37
53
|
if (normalized === "claude-code") {
|
|
@@ -135,9 +151,11 @@ export function resolveClaudeCommandForRoot(commandLine, env = process.env) {
|
|
|
135
151
|
function normalizeText(value) {
|
|
136
152
|
return typeof value === "string" ? value : "";
|
|
137
153
|
}
|
|
154
|
+
function contentBlocks(message) {
|
|
155
|
+
return Array.isArray(message?.content) ? message.content : [];
|
|
156
|
+
}
|
|
138
157
|
function extractAssistantText(message) {
|
|
139
|
-
|
|
140
|
-
return content
|
|
158
|
+
return contentBlocks(message)
|
|
141
159
|
.map((block) => (block?.type === "text" && typeof block.text === "string" ? block.text : ""))
|
|
142
160
|
.filter(Boolean)
|
|
143
161
|
.join("");
|
|
@@ -212,9 +230,10 @@ function sanitizeSummary(value, maxLen = 180) {
|
|
|
212
230
|
export class ClaudeAgentSdkSession extends EventEmitter {
|
|
213
231
|
// Capability advertised via getSnapshot().capabilities so worker proxies
|
|
214
232
|
// can short-circuit runGoal without an IPC round trip. Claude exposes
|
|
215
|
-
// native `/goal` slash
|
|
233
|
+
// native `/goal` and `/compact` slash commands, so both are true.
|
|
216
234
|
static capabilities = Object.freeze({
|
|
217
235
|
goal: true,
|
|
236
|
+
compact: true,
|
|
218
237
|
media: PROVIDER_MEDIA_CAPABILITIES[CLAUDE_PROVIDER_VARIANT],
|
|
219
238
|
});
|
|
220
239
|
getCapabilities() {
|
|
@@ -339,7 +358,7 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
339
358
|
return this.sessionInfo ? { ...this.sessionInfo } : null;
|
|
340
359
|
}
|
|
341
360
|
getCurrentTurnStatus() {
|
|
342
|
-
return this.currentTurnStatus
|
|
361
|
+
return withActiveTool(this.currentTurnStatus, this.currentTurn);
|
|
343
362
|
}
|
|
344
363
|
async ensureSessionInfo() {
|
|
345
364
|
return this.getSessionInfo();
|
|
@@ -718,6 +737,16 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
718
737
|
this.updateSessionInfo(message.session_id || message.sessionId);
|
|
719
738
|
return;
|
|
720
739
|
}
|
|
740
|
+
if (message.subtype === "compact_boundary") {
|
|
741
|
+
currentTurn.compactMetadata =
|
|
742
|
+
message.compact_metadata && typeof message.compact_metadata === "object"
|
|
743
|
+
? { ...message.compact_metadata }
|
|
744
|
+
: {};
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (message.subtype === "status" && message.compact_result === "failed") {
|
|
748
|
+
currentTurn.compactError = normalizeText(message.compact_error) || "compaction failed";
|
|
749
|
+
}
|
|
721
750
|
if (message.subtype === "status" && message.status === "compacting") {
|
|
722
751
|
await this.emitWorkingStatus({
|
|
723
752
|
phase: "context_compaction",
|
|
@@ -773,12 +802,23 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
773
802
|
}
|
|
774
803
|
case "assistant": {
|
|
775
804
|
this.updateSessionInfo(message.session_id || message.sessionId);
|
|
805
|
+
if (message.message?.id && message.message.usage) {
|
|
806
|
+
currentTurn.usageByMessageId.set(message.message.id, message.message.usage);
|
|
807
|
+
}
|
|
808
|
+
for (const block of contentBlocks(message.message)) {
|
|
809
|
+
if (block?.type === "tool_use") {
|
|
810
|
+
noteToolStarted(currentTurn, block.id, block.name, block.input);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
776
813
|
const text = extractAssistantText(message.message);
|
|
777
814
|
if (!text) {
|
|
778
815
|
return;
|
|
779
816
|
}
|
|
780
|
-
currentTurn.emittedAssistantMessage = true;
|
|
781
817
|
currentTurn.fullText = text;
|
|
818
|
+
if (currentTurn.suppressReply) {
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
currentTurn.emittedAssistantMessage = true;
|
|
782
822
|
await this.emitWorkingStatus({
|
|
783
823
|
phase: "message_aggregation",
|
|
784
824
|
reply_in_progress: true,
|
|
@@ -788,6 +828,13 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
788
828
|
await this.emitAssistantMessage(text);
|
|
789
829
|
return;
|
|
790
830
|
}
|
|
831
|
+
case "user":
|
|
832
|
+
for (const block of contentBlocks(message.message)) {
|
|
833
|
+
if (block?.type === "tool_result") {
|
|
834
|
+
noteToolFinished(currentTurn, block.tool_use_id);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return;
|
|
791
838
|
case "rate_limit_event":
|
|
792
839
|
this.updateSessionInfo(message.session_id || message.sessionId);
|
|
793
840
|
this.rateLimitInfo =
|
|
@@ -829,7 +876,7 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
829
876
|
}
|
|
830
877
|
return true;
|
|
831
878
|
}
|
|
832
|
-
async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null, jsonSchema = null } = {}) {
|
|
879
|
+
async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null, jsonSchema = null, suppressReply = false } = {}) {
|
|
833
880
|
if (this.closeRequested) {
|
|
834
881
|
throw this.createSessionClosedError();
|
|
835
882
|
}
|
|
@@ -855,16 +902,22 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
855
902
|
if (!sdkModule || typeof sdkModule.query !== "function") {
|
|
856
903
|
throw new Error("Claude Agent SDK is unavailable");
|
|
857
904
|
}
|
|
858
|
-
|
|
905
|
+
if (!suppressReply) {
|
|
906
|
+
this.history.push({ role: "user", content: promptText });
|
|
907
|
+
}
|
|
859
908
|
const abortController = new AbortController();
|
|
860
909
|
const currentTurn = {
|
|
861
910
|
abortController,
|
|
911
|
+
suppressReply,
|
|
912
|
+
compactMetadata: null,
|
|
913
|
+
compactError: "",
|
|
862
914
|
emittedAssistantMessage: false,
|
|
863
915
|
fullText: "",
|
|
864
916
|
items: [],
|
|
865
917
|
query: null,
|
|
866
918
|
resultMessage: null,
|
|
867
919
|
terminalWorkingStatusEmitted: false,
|
|
920
|
+
usageByMessageId: new Map(),
|
|
868
921
|
};
|
|
869
922
|
this.currentTurn = currentTurn;
|
|
870
923
|
this.markTurnStartedStatus();
|
|
@@ -920,6 +973,8 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
920
973
|
permissionDenials: Array.isArray(resultMessage.permission_denials)
|
|
921
974
|
? [...resultMessage.permission_denials]
|
|
922
975
|
: [],
|
|
976
|
+
// A failed turn still spent tokens.
|
|
977
|
+
usage: resultMessage.usage ? { ...resultMessage.usage } : null,
|
|
923
978
|
});
|
|
924
979
|
}
|
|
925
980
|
const structuredOutput = jsonSchema && resultMessage.structured_output && typeof resultMessage.structured_output === "object"
|
|
@@ -930,10 +985,10 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
930
985
|
: normalizeText(resultMessage.result) ||
|
|
931
986
|
currentTurn.fullText ||
|
|
932
987
|
extractAssistantText(currentTurn.items.find((item) => item?.type === "assistant")?.message);
|
|
933
|
-
if (!currentTurn.emittedAssistantMessage && responseText) {
|
|
988
|
+
if (!suppressReply && !currentTurn.emittedAssistantMessage && responseText) {
|
|
934
989
|
await this.emitAssistantMessage(responseText);
|
|
935
990
|
}
|
|
936
|
-
if (responseText) {
|
|
991
|
+
if (!suppressReply && responseText) {
|
|
937
992
|
this.history.push({ role: "assistant", content: responseText });
|
|
938
993
|
}
|
|
939
994
|
this.manualResumeReady = Boolean(this.sessionId);
|
|
@@ -957,9 +1012,14 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
957
1012
|
modelUsage: resultMessage.modelUsage ? { ...resultMessage.modelUsage } : undefined,
|
|
958
1013
|
structuredOutput: structuredOutput !== null ? structuredOutput : undefined,
|
|
959
1014
|
},
|
|
1015
|
+
compactMetadata: currentTurn.compactMetadata || undefined,
|
|
1016
|
+
compactError: currentTurn.compactError || undefined,
|
|
960
1017
|
};
|
|
961
1018
|
}
|
|
962
1019
|
catch (error) {
|
|
1020
|
+
// A failed turn still spent tokens; an interrupted query ends without a
|
|
1021
|
+
// result, so fall back to the usage streamed so far.
|
|
1022
|
+
const usage = error?.usage ?? sumStreamedUsage(currentTurn.usageByMessageId);
|
|
963
1023
|
if (error?.reason === "turn_timeout") {
|
|
964
1024
|
await this.interruptCurrentTurn();
|
|
965
1025
|
}
|
|
@@ -971,9 +1031,12 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
971
1031
|
}, onProgress);
|
|
972
1032
|
}
|
|
973
1033
|
if (this.closeRequested && error?.reason !== "session_closed") {
|
|
974
|
-
throw this.createSessionClosedError();
|
|
1034
|
+
throw Object.assign(this.createSessionClosedError(), { usage });
|
|
975
1035
|
}
|
|
976
1036
|
this.maybeEmitAuthRequired(error?.message || "", error?.message || "");
|
|
1037
|
+
if (error && typeof error === "object") {
|
|
1038
|
+
error.usage = usage;
|
|
1039
|
+
}
|
|
977
1040
|
throw error;
|
|
978
1041
|
}
|
|
979
1042
|
finally {
|
|
@@ -1099,6 +1162,46 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
1099
1162
|
},
|
|
1100
1163
|
};
|
|
1101
1164
|
}
|
|
1165
|
+
/**
|
|
1166
|
+
* Manually compact the conversation via Claude's native `/compact` slash
|
|
1167
|
+
* command. Custom focus instructions are forwarded as the command argument.
|
|
1168
|
+
* No assistant message is emitted; the caller renders the confirmation.
|
|
1169
|
+
*
|
|
1170
|
+
* @param {import("../shared.js").CompactRequest} [request]
|
|
1171
|
+
* @param {{ onProgress?: Function }} [options]
|
|
1172
|
+
* @returns {Promise<import("../shared.js").CompactResult>}
|
|
1173
|
+
*/
|
|
1174
|
+
async runCompact(request = {}, { onProgress = null } = {}) {
|
|
1175
|
+
const instructions = normalizeText(request?.instructions).trim();
|
|
1176
|
+
if (!this.sessionId || this.pendingHistorySeed) {
|
|
1177
|
+
return { compact: { status: "noop", instructionsApplied: false }, usage: null, metadata: {} };
|
|
1178
|
+
}
|
|
1179
|
+
const turnResult = await this.runTurn(instructions ? `/compact ${instructions}` : "/compact", {
|
|
1180
|
+
onProgress,
|
|
1181
|
+
suppressReply: true,
|
|
1182
|
+
});
|
|
1183
|
+
if (/not enough messages/i.test(turnResult.compactError || "")) {
|
|
1184
|
+
return { compact: { status: "noop", instructionsApplied: false }, usage: turnResult.usage, metadata: turnResult.metadata };
|
|
1185
|
+
}
|
|
1186
|
+
if (turnResult.compactError) {
|
|
1187
|
+
throw createTurnError(`Claude compaction failed: ${turnResult.compactError}`, {
|
|
1188
|
+
reason: "compact_failed",
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
const boundary = turnResult.compactMetadata;
|
|
1192
|
+
const preTokens = Number(boundary?.pre_tokens);
|
|
1193
|
+
const postTokens = Number(boundary?.post_tokens);
|
|
1194
|
+
return {
|
|
1195
|
+
compact: {
|
|
1196
|
+
status: boundary ? "compacted" : "noop",
|
|
1197
|
+
instructionsApplied: Boolean(boundary && instructions),
|
|
1198
|
+
preTokens: Number.isFinite(preTokens) ? preTokens : undefined,
|
|
1199
|
+
postTokens: Number.isFinite(postTokens) ? postTokens : undefined,
|
|
1200
|
+
},
|
|
1201
|
+
usage: turnResult.usage,
|
|
1202
|
+
metadata: turnResult.metadata,
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1102
1205
|
async close() {
|
|
1103
1206
|
if (this.closed) {
|
|
1104
1207
|
return;
|
|
@@ -31,6 +31,8 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
|
|
|
31
31
|
nativeSessionId: string;
|
|
32
32
|
rateLimits: any;
|
|
33
33
|
tokenUsage: any;
|
|
34
|
+
turnTokenBaseline: number | null;
|
|
35
|
+
turnTokensSeen: boolean;
|
|
34
36
|
currentTurnStatus: any;
|
|
35
37
|
currentTurnActivityAt: number;
|
|
36
38
|
now: any;
|
|
@@ -42,6 +44,14 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
|
|
|
42
44
|
activeAssistantMessageText: string;
|
|
43
45
|
resolve: null;
|
|
44
46
|
reject: null;
|
|
47
|
+
} | {
|
|
48
|
+
turnId: string;
|
|
49
|
+
fullText: string;
|
|
50
|
+
activeAssistantMessageId: string;
|
|
51
|
+
activeAssistantMessageText: string;
|
|
52
|
+
resolve: null;
|
|
53
|
+
reject: null;
|
|
54
|
+
suppressReply: boolean;
|
|
45
55
|
} | {
|
|
46
56
|
turnId: string;
|
|
47
57
|
fullText: string;
|
|
@@ -127,6 +137,15 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
|
|
|
127
137
|
resumeReady?: boolean | undefined;
|
|
128
138
|
}): void;
|
|
129
139
|
applySessionConfigured(params: any): void;
|
|
140
|
+
/** Start counting a turn's tokens from the thread total known right now. */
|
|
141
|
+
beginTurnTokenAccounting(): void;
|
|
142
|
+
/**
|
|
143
|
+
* Thread token usage plus `turnTotalTokens`: tokens spent since the current
|
|
144
|
+
* turn (or goal) started. Codex only reports thread-cumulative totals, and
|
|
145
|
+
* only when a response completes, so a turn that ended before any response
|
|
146
|
+
* did has unknown usage.
|
|
147
|
+
*/
|
|
148
|
+
snapshotTokenUsage(): any;
|
|
130
149
|
resolveContextUsagePercent(): number | undefined;
|
|
131
150
|
/**
|
|
132
151
|
* Whether a turn failure was caused by the provider thread outgrowing the
|
|
@@ -175,6 +194,14 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
|
|
|
175
194
|
activeAssistantMessageText: string;
|
|
176
195
|
resolve: null;
|
|
177
196
|
reject: null;
|
|
197
|
+
} | {
|
|
198
|
+
turnId: string;
|
|
199
|
+
fullText: string;
|
|
200
|
+
activeAssistantMessageId: string;
|
|
201
|
+
activeAssistantMessageText: string;
|
|
202
|
+
resolve: null;
|
|
203
|
+
reject: null;
|
|
204
|
+
suppressReply: boolean;
|
|
178
205
|
} | {
|
|
179
206
|
turnId: string;
|
|
180
207
|
fullText: string;
|
|
@@ -207,6 +234,14 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
|
|
|
207
234
|
activeAssistantMessageText: string;
|
|
208
235
|
resolve: null;
|
|
209
236
|
reject: null;
|
|
237
|
+
} | {
|
|
238
|
+
turnId: string;
|
|
239
|
+
fullText: string;
|
|
240
|
+
activeAssistantMessageId: string;
|
|
241
|
+
activeAssistantMessageText: string;
|
|
242
|
+
resolve: null;
|
|
243
|
+
reject: null;
|
|
244
|
+
suppressReply: boolean;
|
|
210
245
|
} | {
|
|
211
246
|
turnId: string;
|
|
212
247
|
fullText: string;
|
|
@@ -244,6 +279,15 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
|
|
|
244
279
|
useInitialImages?: boolean | undefined;
|
|
245
280
|
jsonSchema?: null | undefined;
|
|
246
281
|
}): any;
|
|
282
|
+
/**
|
|
283
|
+
* Manually compact the thread via `thread/compact/start`. The app-server
|
|
284
|
+
* runs compaction as a regular turn, so completion is the usual
|
|
285
|
+
* `turn/completed`. The protocol takes no focus instructions.
|
|
286
|
+
*
|
|
287
|
+
* @param {import("../shared.js").CompactRequest} [request]
|
|
288
|
+
* @returns {Promise<import("../shared.js").CompactResult>}
|
|
289
|
+
*/
|
|
290
|
+
runCompact(request?: import("../shared.js").CompactRequest): Promise<import("../shared.js").CompactResult>;
|
|
247
291
|
/**
|
|
248
292
|
* Normalize a goal payload from a codex `thread/goal/*` notification into
|
|
249
293
|
* the cross-layer {@link GoalState} shape.
|