@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
|
@@ -4,10 +4,15 @@ import { appendContextFilesToPrompt } from "../context-files.js";
|
|
|
4
4
|
import { PROVIDER_MEDIA_CAPABILITIES, buildOpencodeParts } from "../media-adapters.js";
|
|
5
5
|
import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
|
|
6
6
|
import { OpencodeServerTransport } from "../transports/opencode-server-transport.js";
|
|
7
|
-
import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, proxyToEnv, sanitizeForLog, } from "../shared.js";
|
|
7
|
+
import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, noteToolFinished, noteToolStarted, proxyToEnv, sanitizeForLog, withActiveTool, } from "../shared.js";
|
|
8
8
|
const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
|
|
9
9
|
const MIN_TURN_DEADLINE_MS = 30 * 1000;
|
|
10
10
|
const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
|
|
11
|
+
const SUMMARY_EVENT_GRACE_MS = 2000;
|
|
12
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
13
|
+
function isHeadersTimeoutError(error) {
|
|
14
|
+
return [error?.code, error?.cause?.code].includes("UND_ERR_HEADERS_TIMEOUT");
|
|
15
|
+
}
|
|
11
16
|
function waitForever() {
|
|
12
17
|
return new Promise(() => { });
|
|
13
18
|
}
|
|
@@ -226,7 +231,7 @@ export class OpencodeSdkSession extends EventEmitter {
|
|
|
226
231
|
resumeReady: Boolean(this.sessionId),
|
|
227
232
|
manualResume: null,
|
|
228
233
|
currentTurnStatus: this.getCurrentTurnStatus(),
|
|
229
|
-
capabilities: { media: PROVIDER_MEDIA_CAPABILITIES[OPENCODE_PROVIDER_VARIANT] },
|
|
234
|
+
capabilities: { compact: true, media: PROVIDER_MEDIA_CAPABILITIES[OPENCODE_PROVIDER_VARIANT] },
|
|
230
235
|
pid: this.transport.pid || undefined,
|
|
231
236
|
};
|
|
232
237
|
}
|
|
@@ -234,7 +239,7 @@ export class OpencodeSdkSession extends EventEmitter {
|
|
|
234
239
|
return this.sessionInfo ? { ...this.sessionInfo } : null;
|
|
235
240
|
}
|
|
236
241
|
getCurrentTurnStatus() {
|
|
237
|
-
return this.currentTurnStatus
|
|
242
|
+
return withActiveTool(this.currentTurnStatus, this.currentTurn);
|
|
238
243
|
}
|
|
239
244
|
async ensureSessionInfo() {
|
|
240
245
|
await this.boot();
|
|
@@ -729,7 +734,9 @@ export class OpencodeSdkSession extends EventEmitter {
|
|
|
729
734
|
phase: "message_aggregation",
|
|
730
735
|
reply_in_progress: true,
|
|
731
736
|
status_line: statusLineForPhase("message_aggregation"),
|
|
732
|
-
reply_preview:
|
|
737
|
+
reply_preview: currentTurn.suppressReply || messageState.info?.summary === true
|
|
738
|
+
? undefined
|
|
739
|
+
: sanitizeSummary(this.collectAssistantText(messageState), 120),
|
|
733
740
|
}, onProgress);
|
|
734
741
|
return;
|
|
735
742
|
}
|
|
@@ -742,6 +749,12 @@ export class OpencodeSdkSession extends EventEmitter {
|
|
|
742
749
|
return;
|
|
743
750
|
}
|
|
744
751
|
if (part.type === "tool") {
|
|
752
|
+
if (part.state?.status === "completed" || part.state?.status === "error") {
|
|
753
|
+
noteToolFinished(currentTurn, part.callID || part.id);
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
noteToolStarted(currentTurn, part.callID || part.id, part.tool, part.state?.input);
|
|
757
|
+
}
|
|
745
758
|
const phase = toolPhaseForName(part.tool);
|
|
746
759
|
const toolStatus = part.state?.status === "completed"
|
|
747
760
|
? part.state?.title || part.state?.output || ""
|
|
@@ -791,7 +804,9 @@ export class OpencodeSdkSession extends EventEmitter {
|
|
|
791
804
|
phase: "message_aggregation",
|
|
792
805
|
reply_in_progress: true,
|
|
793
806
|
status_line: statusLineForPhase("message_aggregation"),
|
|
794
|
-
reply_preview:
|
|
807
|
+
reply_preview: currentTurn.suppressReply || messageState.info?.summary === true
|
|
808
|
+
? undefined
|
|
809
|
+
: sanitizeSummary(this.collectAssistantText(messageState), 120),
|
|
795
810
|
}, onProgress);
|
|
796
811
|
return;
|
|
797
812
|
}
|
|
@@ -832,7 +847,8 @@ export class OpencodeSdkSession extends EventEmitter {
|
|
|
832
847
|
if (currentTurn.activeAssistantMessageId === targetId) {
|
|
833
848
|
currentTurn.activeAssistantMessageId = "";
|
|
834
849
|
}
|
|
835
|
-
|
|
850
|
+
// Compaction summaries (manual or automatic) are context, not a reply.
|
|
851
|
+
if (!text || currentTurn.suppressReply || messageState.info?.summary === true) {
|
|
836
852
|
return false;
|
|
837
853
|
}
|
|
838
854
|
currentTurn.fullText += text;
|
|
@@ -1140,6 +1156,160 @@ export class OpencodeSdkSession extends EventEmitter {
|
|
|
1140
1156
|
}
|
|
1141
1157
|
return true;
|
|
1142
1158
|
}
|
|
1159
|
+
createTurnState(abortController, onProgress) {
|
|
1160
|
+
const currentTurn = {
|
|
1161
|
+
abortController,
|
|
1162
|
+
assistantMessages: new Map(),
|
|
1163
|
+
assistantMessageOrder: [],
|
|
1164
|
+
activeAssistantMessageId: "",
|
|
1165
|
+
messageRoles: new Map(),
|
|
1166
|
+
pendingMessageEvents: new Map(),
|
|
1167
|
+
fullText: "",
|
|
1168
|
+
items: [],
|
|
1169
|
+
lastAssistantInfo: null,
|
|
1170
|
+
onProgress,
|
|
1171
|
+
resolve: null,
|
|
1172
|
+
reject: null,
|
|
1173
|
+
settled: false,
|
|
1174
|
+
terminalWorkingStatusEmitted: false,
|
|
1175
|
+
};
|
|
1176
|
+
const completionPromise = new Promise((resolve, reject) => {
|
|
1177
|
+
currentTurn.resolve = resolve;
|
|
1178
|
+
currentTurn.reject = reject;
|
|
1179
|
+
});
|
|
1180
|
+
return { currentTurn, completionPromise };
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Model to summarize with: `session.summarize` requires one explicitly.
|
|
1184
|
+
* Prefer what the session actually ran on (last turn, then its recent
|
|
1185
|
+
* messages); the configured model is only a fallback.
|
|
1186
|
+
*/
|
|
1187
|
+
async resolveCompactionModel() {
|
|
1188
|
+
const fromInfo = (info) => {
|
|
1189
|
+
const providerID = info?.providerID || info?.model?.providerID;
|
|
1190
|
+
const modelID = info?.modelID || info?.model?.modelID;
|
|
1191
|
+
return typeof providerID === "string" && providerID && typeof modelID === "string" && modelID
|
|
1192
|
+
? { providerID, modelID }
|
|
1193
|
+
: null;
|
|
1194
|
+
};
|
|
1195
|
+
const lastModel = fromInfo(this.lastAssistantInfo);
|
|
1196
|
+
if (lastModel) {
|
|
1197
|
+
return lastModel;
|
|
1198
|
+
}
|
|
1199
|
+
if (typeof this.client?.session?.messages === "function") {
|
|
1200
|
+
const messages = await this.requestOrThrow(this.client.session.messages({ sessionID: this.sessionId, limit: 20 }, { throwOnError: true, responseStyle: "data" })).catch((error) => {
|
|
1201
|
+
this.trace(`compaction model lookup failed: ${extractErrorMessage(error)}`);
|
|
1202
|
+
return [];
|
|
1203
|
+
});
|
|
1204
|
+
for (const entry of [...(Array.isArray(messages) ? messages : [])].reverse()) {
|
|
1205
|
+
const model = fromInfo(entry?.info);
|
|
1206
|
+
if (model) {
|
|
1207
|
+
return model;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
const configured = typeof this.options.model === "string" ? this.options.model.trim() : "";
|
|
1212
|
+
const slash = configured.indexOf("/");
|
|
1213
|
+
if (slash > 0 && slash < configured.length - 1) {
|
|
1214
|
+
return { providerID: configured.slice(0, slash), modelID: configured.slice(slash + 1) };
|
|
1215
|
+
}
|
|
1216
|
+
throw createTurnError("Opencode compaction needs a model, but none could be resolved for this session", {
|
|
1217
|
+
reason: "compact_failed",
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Manually compact via the server's `session.summarize` (what the TUI's
|
|
1222
|
+
* `/compact` calls). It takes no focus instructions and answers `true`
|
|
1223
|
+
* even when summarizing fails, so success is judged from the summary
|
|
1224
|
+
* message itself.
|
|
1225
|
+
*
|
|
1226
|
+
* @param {import("../shared.js").CompactRequest} [request]
|
|
1227
|
+
* @param {{ onProgress?: Function }} [options]
|
|
1228
|
+
* @returns {Promise<import("../shared.js").CompactResult>}
|
|
1229
|
+
*/
|
|
1230
|
+
async runCompact(request = {}, { onProgress = null } = {}) {
|
|
1231
|
+
if (this.closeRequested) {
|
|
1232
|
+
throw this.createSessionClosedError();
|
|
1233
|
+
}
|
|
1234
|
+
if (this.currentTurn) {
|
|
1235
|
+
throw createTurnError("Opencode turn already running", {
|
|
1236
|
+
reason: "turn_already_running",
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
if (this.pendingHistorySeed || (!this.resumeSessionId && this.history.length === 0)) {
|
|
1240
|
+
return { compact: { status: "noop", instructionsApplied: false }, usage: null, metadata: {} };
|
|
1241
|
+
}
|
|
1242
|
+
this.markTurnStartedStatus();
|
|
1243
|
+
let model;
|
|
1244
|
+
try {
|
|
1245
|
+
await this.boot();
|
|
1246
|
+
if (typeof this.client?.session?.summarize !== "function") {
|
|
1247
|
+
throw new Error("Opencode summarize API is unavailable");
|
|
1248
|
+
}
|
|
1249
|
+
model = await this.resolveCompactionModel();
|
|
1250
|
+
}
|
|
1251
|
+
catch (error) {
|
|
1252
|
+
await this.failPendingTurnStart(error, onProgress);
|
|
1253
|
+
throw error;
|
|
1254
|
+
}
|
|
1255
|
+
const abortController = new AbortController();
|
|
1256
|
+
const { currentTurn, completionPromise } = this.createTurnState(abortController, onProgress);
|
|
1257
|
+
currentTurn.suppressReply = true;
|
|
1258
|
+
this.currentTurn = currentTurn;
|
|
1259
|
+
const closeGuard = this.createCloseGuard(() => {
|
|
1260
|
+
abortController.abort();
|
|
1261
|
+
void this.interruptCurrentTurn();
|
|
1262
|
+
});
|
|
1263
|
+
const turnTimeoutGuard = this.createTurnTimeoutGuard(() => {
|
|
1264
|
+
abortController.abort();
|
|
1265
|
+
void this.interruptCurrentTurn();
|
|
1266
|
+
});
|
|
1267
|
+
try {
|
|
1268
|
+
await this.emitWorkingStatus({
|
|
1269
|
+
phase: "context_compaction",
|
|
1270
|
+
reply_in_progress: true,
|
|
1271
|
+
status_line: statusLineForPhase("context_compaction"),
|
|
1272
|
+
}, onProgress);
|
|
1273
|
+
// The response only arrives once summarizing finishes, which can outlive
|
|
1274
|
+
// fetch's header timeout; `session.idle` marks completion either way.
|
|
1275
|
+
const summarize = this.requestOrThrow(this.client.session.summarize({ sessionID: this.sessionId, ...model, auto: false }, { throwOnError: true, responseStyle: "data", signal: abortController.signal })).catch((error) => (isHeadersTimeoutError(error) ? completionPromise : Promise.reject(error)));
|
|
1276
|
+
await Promise.race([Promise.race([summarize, completionPromise]), closeGuard.promise, turnTimeoutGuard.promise]);
|
|
1277
|
+
// Events trail the HTTP response on a separate stream; let idle land.
|
|
1278
|
+
await Promise.race([completionPromise, delay(SUMMARY_EVENT_GRACE_MS)]);
|
|
1279
|
+
const summaryInfo = currentTurn.lastAssistantInfo?.summary === true ? currentTurn.lastAssistantInfo : null;
|
|
1280
|
+
if (summaryInfo?.error || summaryInfo?.finish === "error") {
|
|
1281
|
+
throw createTurnError(`Opencode compaction failed: ${extractErrorMessage(summaryInfo.error?.data || summaryInfo.error) || "unknown error"}`, { reason: "compact_failed" });
|
|
1282
|
+
}
|
|
1283
|
+
await this.emitTerminalWorkingStatus(currentTurn, { phase: "turn_completed", status_done_line: "opencode compacted context" }, onProgress);
|
|
1284
|
+
return {
|
|
1285
|
+
// An empty session never gets here, so a summary whose events are
|
|
1286
|
+
// still in flight counts as compacted.
|
|
1287
|
+
compact: { status: "compacted", instructionsApplied: false },
|
|
1288
|
+
usage: this.buildUsageFromAssistantInfo(summaryInfo),
|
|
1289
|
+
metadata: { source: OPENCODE_PROVIDER_VARIANT, sessionId: this.sessionId || undefined, ...model },
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
catch (error) {
|
|
1293
|
+
if (error?.reason === "turn_timeout") {
|
|
1294
|
+
await this.interruptCurrentTurn();
|
|
1295
|
+
}
|
|
1296
|
+
if (!this.closeRequested && error?.reason !== "session_closed") {
|
|
1297
|
+
await this.emitTerminalWorkingStatus(currentTurn, { phase: "turn_failed", status_done_line: extractErrorMessage(error) }, onProgress);
|
|
1298
|
+
}
|
|
1299
|
+
if (this.closeRequested && error?.reason !== "session_closed") {
|
|
1300
|
+
throw this.createSessionClosedError();
|
|
1301
|
+
}
|
|
1302
|
+
throw error;
|
|
1303
|
+
}
|
|
1304
|
+
finally {
|
|
1305
|
+
this.activeReplyTarget = "";
|
|
1306
|
+
if (this.currentTurn === currentTurn) {
|
|
1307
|
+
this.currentTurn = null;
|
|
1308
|
+
}
|
|
1309
|
+
closeGuard.cleanup();
|
|
1310
|
+
turnTimeoutGuard.cleanup();
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1143
1313
|
async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null, jsonSchema = null } = {}) {
|
|
1144
1314
|
if (this.closeRequested) {
|
|
1145
1315
|
throw this.createSessionClosedError();
|
|
@@ -1169,26 +1339,7 @@ export class OpencodeSdkSession extends EventEmitter {
|
|
|
1169
1339
|
}
|
|
1170
1340
|
this.history.push({ role: "user", content: promptText });
|
|
1171
1341
|
const abortController = new AbortController();
|
|
1172
|
-
const currentTurn =
|
|
1173
|
-
abortController,
|
|
1174
|
-
assistantMessages: new Map(),
|
|
1175
|
-
assistantMessageOrder: [],
|
|
1176
|
-
activeAssistantMessageId: "",
|
|
1177
|
-
messageRoles: new Map(),
|
|
1178
|
-
pendingMessageEvents: new Map(),
|
|
1179
|
-
fullText: "",
|
|
1180
|
-
items: [],
|
|
1181
|
-
lastAssistantInfo: null,
|
|
1182
|
-
onProgress,
|
|
1183
|
-
resolve: null,
|
|
1184
|
-
reject: null,
|
|
1185
|
-
settled: false,
|
|
1186
|
-
terminalWorkingStatusEmitted: false,
|
|
1187
|
-
};
|
|
1188
|
-
const completionPromise = new Promise((resolve, reject) => {
|
|
1189
|
-
currentTurn.resolve = resolve;
|
|
1190
|
-
currentTurn.reject = reject;
|
|
1191
|
-
});
|
|
1342
|
+
const { currentTurn, completionPromise } = this.createTurnState(abortController, onProgress);
|
|
1192
1343
|
this.currentTurn = currentTurn;
|
|
1193
1344
|
const closeGuard = this.createCloseGuard(() => {
|
|
1194
1345
|
abortController.abort();
|
package/dist/shared.d.ts
CHANGED
|
@@ -20,6 +20,18 @@ export function normalizeLogger(logger: any): any;
|
|
|
20
20
|
export function emitLog(logger: any, message: any): void;
|
|
21
21
|
export function truncateText(value: any, maxLen?: number): string;
|
|
22
22
|
export function sanitizeForLog(value: any, maxLen?: number): string;
|
|
23
|
+
/**
|
|
24
|
+
* Track tool calls that started but have not finished on a turn object, so
|
|
25
|
+
* `getCurrentTurnStatus()` can report what a long silent turn is running.
|
|
26
|
+
*/
|
|
27
|
+
export function noteToolStarted(turn: any, id: any, name: any, input: any): void;
|
|
28
|
+
export function noteToolFinished(turn: any, id: any): void;
|
|
29
|
+
/** Map a codex app-server (camelCase) or exec (snake_case) tool item to `{ name, input }`. */
|
|
30
|
+
export function describeCodexToolItem(item: any): {
|
|
31
|
+
name: any;
|
|
32
|
+
input: any;
|
|
33
|
+
} | null;
|
|
34
|
+
export function withActiveTool(status: any, turn: any): any;
|
|
23
35
|
export function isTruthyEnv(value: any): boolean;
|
|
24
36
|
export function getBoundedEnvInt(envName: any, fallback: any, min: any, max: any): any;
|
|
25
37
|
export function parseCommandParts(commandLine: any): {
|
|
@@ -87,6 +99,30 @@ export const TERMINAL_GOAL_STATUSES: ReadonlyArray<GoalStatus>;
|
|
|
87
99
|
*
|
|
88
100
|
* @typedef {Object} SessionCapabilities
|
|
89
101
|
* @property {boolean} [goal]
|
|
102
|
+
* @property {boolean} [compact]
|
|
103
|
+
*/
|
|
104
|
+
/**
|
|
105
|
+
* Manual context compaction (`/compact`) contract for providers that
|
|
106
|
+
* implement the optional `runCompact` method and advertise
|
|
107
|
+
* `capabilities.compact === true`. Providers must NOT emit assistant messages
|
|
108
|
+
* while compacting; the caller renders the user-facing confirmation.
|
|
109
|
+
*
|
|
110
|
+
* @typedef {Object} CompactRequest
|
|
111
|
+
* @property {string} [instructions] Optional focus instructions; providers
|
|
112
|
+
* whose backend cannot take them ignore them and report
|
|
113
|
+
* `instructionsApplied: false`.
|
|
114
|
+
*
|
|
115
|
+
* @typedef {Object} CompactState
|
|
116
|
+
* @property {"compacted"|"noop"} status `noop` = nothing to compact.
|
|
117
|
+
* @property {boolean} instructionsApplied
|
|
118
|
+
* @property {number} [preTokens]
|
|
119
|
+
* @property {number} [postTokens]
|
|
120
|
+
* @property {number} [tokensRemoved]
|
|
121
|
+
*
|
|
122
|
+
* @typedef {Object} CompactResult
|
|
123
|
+
* @property {CompactState} compact
|
|
124
|
+
* @property {unknown} [usage]
|
|
125
|
+
* @property {Record<string, unknown>} [metadata]
|
|
90
126
|
*/
|
|
91
127
|
/**
|
|
92
128
|
* Default capabilities assigned to providers that do not opt into any optional
|
|
@@ -175,4 +211,46 @@ export type GoalResult = {
|
|
|
175
211
|
*/
|
|
176
212
|
export type SessionCapabilities = {
|
|
177
213
|
goal?: boolean | undefined;
|
|
214
|
+
compact?: boolean | undefined;
|
|
215
|
+
};
|
|
216
|
+
/**
|
|
217
|
+
* Manual context compaction (`/compact`) contract for providers that
|
|
218
|
+
* implement the optional `runCompact` method and advertise
|
|
219
|
+
* `capabilities.compact === true`. Providers must NOT emit assistant messages
|
|
220
|
+
* while compacting; the caller renders the user-facing confirmation.
|
|
221
|
+
*/
|
|
222
|
+
export type CompactRequest = {
|
|
223
|
+
/**
|
|
224
|
+
* Optional focus instructions; providers
|
|
225
|
+
* whose backend cannot take them ignore them and report
|
|
226
|
+
* `instructionsApplied: false`.
|
|
227
|
+
*/
|
|
228
|
+
instructions?: string | undefined;
|
|
229
|
+
};
|
|
230
|
+
/**
|
|
231
|
+
* Manual context compaction (`/compact`) contract for providers that
|
|
232
|
+
* implement the optional `runCompact` method and advertise
|
|
233
|
+
* `capabilities.compact === true`. Providers must NOT emit assistant messages
|
|
234
|
+
* while compacting; the caller renders the user-facing confirmation.
|
|
235
|
+
*/
|
|
236
|
+
export type CompactState = {
|
|
237
|
+
/**
|
|
238
|
+
* `noop` = nothing to compact.
|
|
239
|
+
*/
|
|
240
|
+
status: "compacted" | "noop";
|
|
241
|
+
instructionsApplied: boolean;
|
|
242
|
+
preTokens?: number | undefined;
|
|
243
|
+
postTokens?: number | undefined;
|
|
244
|
+
tokensRemoved?: number | undefined;
|
|
245
|
+
};
|
|
246
|
+
/**
|
|
247
|
+
* Manual context compaction (`/compact`) contract for providers that
|
|
248
|
+
* implement the optional `runCompact` method and advertise
|
|
249
|
+
* `capabilities.compact === true`. Providers must NOT emit assistant messages
|
|
250
|
+
* while compacting; the caller renders the user-facing confirmation.
|
|
251
|
+
*/
|
|
252
|
+
export type CompactResult = {
|
|
253
|
+
compact: CompactState;
|
|
254
|
+
usage?: unknown;
|
|
255
|
+
metadata?: Record<string, unknown> | undefined;
|
|
178
256
|
};
|
package/dist/shared.js
CHANGED
|
@@ -80,6 +80,30 @@ export function isTerminalGoalStatus(value) {
|
|
|
80
80
|
*
|
|
81
81
|
* @typedef {Object} SessionCapabilities
|
|
82
82
|
* @property {boolean} [goal]
|
|
83
|
+
* @property {boolean} [compact]
|
|
84
|
+
*/
|
|
85
|
+
/**
|
|
86
|
+
* Manual context compaction (`/compact`) contract for providers that
|
|
87
|
+
* implement the optional `runCompact` method and advertise
|
|
88
|
+
* `capabilities.compact === true`. Providers must NOT emit assistant messages
|
|
89
|
+
* while compacting; the caller renders the user-facing confirmation.
|
|
90
|
+
*
|
|
91
|
+
* @typedef {Object} CompactRequest
|
|
92
|
+
* @property {string} [instructions] Optional focus instructions; providers
|
|
93
|
+
* whose backend cannot take them ignore them and report
|
|
94
|
+
* `instructionsApplied: false`.
|
|
95
|
+
*
|
|
96
|
+
* @typedef {Object} CompactState
|
|
97
|
+
* @property {"compacted"|"noop"} status `noop` = nothing to compact.
|
|
98
|
+
* @property {boolean} instructionsApplied
|
|
99
|
+
* @property {number} [preTokens]
|
|
100
|
+
* @property {number} [postTokens]
|
|
101
|
+
* @property {number} [tokensRemoved]
|
|
102
|
+
*
|
|
103
|
+
* @typedef {Object} CompactResult
|
|
104
|
+
* @property {CompactState} compact
|
|
105
|
+
* @property {unknown} [usage]
|
|
106
|
+
* @property {Record<string, unknown>} [metadata]
|
|
83
107
|
*/
|
|
84
108
|
/**
|
|
85
109
|
* Default capabilities assigned to providers that do not opt into any optional
|
|
@@ -87,7 +111,7 @@ export function isTerminalGoalStatus(value) {
|
|
|
87
111
|
*
|
|
88
112
|
* @type {Readonly<SessionCapabilities>}
|
|
89
113
|
*/
|
|
90
|
-
export const DEFAULT_SESSION_CAPABILITIES = Object.freeze({ goal: false });
|
|
114
|
+
export const DEFAULT_SESSION_CAPABILITIES = Object.freeze({ goal: false, compact: false });
|
|
91
115
|
/**
|
|
92
116
|
* Resolve a session's capability snapshot, falling back to
|
|
93
117
|
* {@link DEFAULT_SESSION_CAPABILITIES}.
|
|
@@ -146,6 +170,76 @@ export function sanitizeForLog(value, maxLen = 180) {
|
|
|
146
170
|
return "";
|
|
147
171
|
return truncateText(String(value).replace(/\s+/g, " ").trim(), maxLen);
|
|
148
172
|
}
|
|
173
|
+
const TOOL_INPUT_SUMMARY_KEYS = ["command", "cmd", "file_path", "filePath", "path", "pattern", "query", "url", "description", "prompt"];
|
|
174
|
+
function summarizeToolInput(input) {
|
|
175
|
+
let value = input;
|
|
176
|
+
if (typeof value === "string" && /^\s*[[{]/.test(value)) {
|
|
177
|
+
try {
|
|
178
|
+
value = JSON.parse(value);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// keep the raw string
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
185
|
+
const key = TOOL_INPUT_SUMMARY_KEYS.find((candidate) => value[candidate]);
|
|
186
|
+
value = key ? value[key] : JSON.stringify(value);
|
|
187
|
+
}
|
|
188
|
+
if (Array.isArray(value)) {
|
|
189
|
+
value = value.join(" ");
|
|
190
|
+
}
|
|
191
|
+
return sanitizeForLog(value, 200) || undefined;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Track tool calls that started but have not finished on a turn object, so
|
|
195
|
+
* `getCurrentTurnStatus()` can report what a long silent turn is running.
|
|
196
|
+
*/
|
|
197
|
+
export function noteToolStarted(turn, id, name, input) {
|
|
198
|
+
if (!turn || !id || !name) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
turn.activeTools ||= new Map();
|
|
202
|
+
const key = String(id);
|
|
203
|
+
if (turn.activeTools.has(key)) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
turn.activeTools.set(key, {
|
|
207
|
+
name: String(name),
|
|
208
|
+
summary: summarizeToolInput(input),
|
|
209
|
+
started_at: new Date().toISOString(),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
export function noteToolFinished(turn, id) {
|
|
213
|
+
if (id) {
|
|
214
|
+
turn?.activeTools?.delete(String(id));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/** Map a codex app-server (camelCase) or exec (snake_case) tool item to `{ name, input }`. */
|
|
218
|
+
export function describeCodexToolItem(item) {
|
|
219
|
+
switch (String(item?.type || "").replace(/_/g, "").toLowerCase()) {
|
|
220
|
+
case "commandexecution":
|
|
221
|
+
return { name: "command", input: item.command };
|
|
222
|
+
case "filechange":
|
|
223
|
+
return { name: "apply_patch", input: (item.changes || []).map((change) => change?.path).join(" ") };
|
|
224
|
+
case "mcptoolcall":
|
|
225
|
+
return { name: [item.server, item.tool].filter(Boolean).join("."), input: item.arguments };
|
|
226
|
+
case "dynamictoolcall":
|
|
227
|
+
case "collabagenttoolcall":
|
|
228
|
+
return { name: item.tool, input: item.arguments ?? item.prompt };
|
|
229
|
+
case "websearch":
|
|
230
|
+
return { name: "web_search", input: item.query };
|
|
231
|
+
default:
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
export function withActiveTool(status, turn) {
|
|
236
|
+
if (!status) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
const tools = turn?.activeTools ? [...turn.activeTools.values()] : [];
|
|
240
|
+
const activeTool = tools[tools.length - 1];
|
|
241
|
+
return activeTool ? { ...status, active_tool: { ...activeTool } } : { ...status };
|
|
242
|
+
}
|
|
149
243
|
export function isTruthyEnv(value) {
|
|
150
244
|
if (value === undefined || value === null)
|
|
151
245
|
return false;
|
package/dist/worker.js
CHANGED
|
@@ -76,7 +76,7 @@ async function handleRequest(message) {
|
|
|
76
76
|
throw new Error(`Unsupported worker method: ${method}`);
|
|
77
77
|
}
|
|
78
78
|
const args = Array.isArray(message.args) ? [...message.args] : [];
|
|
79
|
-
if (method === "runTurn" || method === "runGoal") {
|
|
79
|
+
if (method === "runTurn" || method === "runGoal" || method === "runCompact") {
|
|
80
80
|
const firstArg = args[0];
|
|
81
81
|
const options = args[1] && typeof args[1] === "object" ? { ...args[1] } : {};
|
|
82
82
|
options.onProgress = (payload) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/ai-sdk",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.2",
|
|
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.13.
|
|
56
|
+
"@love-moon/chat-web": "^0.13.2"
|
|
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": "d60a6a2"
|
|
64
64
|
}
|