@love-moon/ai-sdk 0.2.38 → 0.2.40
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/dist/client.d.ts +4 -1
- package/dist/client.js +14 -2
- package/dist/external-provider-registry.js +38 -2
- package/dist/providers/claude-agent-sdk-session.d.ts +4 -2
- package/dist/providers/claude-agent-sdk-session.js +25 -5
- package/dist/providers/codex-app-server-session.d.ts +3 -2
- package/dist/providers/codex-app-server-session.js +22 -4
- package/dist/providers/codex-exec-session.d.ts +116 -0
- package/dist/providers/codex-exec-session.js +583 -0
- package/dist/providers/copilot-sdk-session.d.ts +193 -0
- package/dist/providers/copilot-sdk-session.js +1463 -0
- package/dist/providers/kimi-cli-session.d.ts +3 -2
- package/dist/providers/kimi-cli-session.js +17 -3
- package/dist/providers/kimi-print-session.d.ts +125 -0
- package/dist/providers/kimi-print-session.js +633 -0
- package/dist/providers/opencode-sdk-session.d.ts +3 -2
- package/dist/providers/opencode-sdk-session.js +7 -2
- package/dist/session-factory.d.ts +7 -1
- package/dist/session-factory.js +48 -6
- package/dist/shared.d.ts +1 -0
- package/dist/shared.js +39 -20
- package/dist/transports/codex-app-server-transport.d.ts +1 -0
- package/dist/transports/codex-app-server-transport.js +10 -5
- package/dist/worker.js +39 -32
- package/package.json +3 -2
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { EventEmitter } from "node:events";
|
|
6
|
+
import readline from "node:readline";
|
|
7
|
+
import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, parseCommandParts, proxyToEnv, sanitizeForLog, } from "../shared.js";
|
|
8
|
+
const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
|
|
9
|
+
const MIN_TURN_DEADLINE_MS = 30 * 1000;
|
|
10
|
+
const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
|
|
11
|
+
const KIMI_PRINT_PROVIDER_VARIANT = "kimi-cli-print";
|
|
12
|
+
const DEFAULT_KIMI_COMMAND = "kimi";
|
|
13
|
+
function createTurnError(message, extras = {}) {
|
|
14
|
+
const error = new Error(message);
|
|
15
|
+
for (const [key, value] of Object.entries(extras)) {
|
|
16
|
+
error[key] = value;
|
|
17
|
+
}
|
|
18
|
+
return error;
|
|
19
|
+
}
|
|
20
|
+
function buildEmptyTurnResult() {
|
|
21
|
+
return {
|
|
22
|
+
text: "",
|
|
23
|
+
usage: null,
|
|
24
|
+
items: [],
|
|
25
|
+
events: [],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function normalizeKimiBackend(backend) {
|
|
29
|
+
const normalized = String(backend || "").trim().toLowerCase();
|
|
30
|
+
if (normalized === "kimi-cli" || normalized === "kimi-code") {
|
|
31
|
+
return "kimi";
|
|
32
|
+
}
|
|
33
|
+
return normalized || "kimi";
|
|
34
|
+
}
|
|
35
|
+
function injectJsonSchemaPrompt(promptText, jsonSchema) {
|
|
36
|
+
const schemaText = typeof jsonSchema === "string" ? jsonSchema : JSON.stringify(jsonSchema, null, 2);
|
|
37
|
+
return `You must respond with valid JSON that strictly conforms to the following JSON Schema. Do not include any markdown formatting or explanation outside the JSON object.
|
|
38
|
+
|
|
39
|
+
JSON Schema:
|
|
40
|
+
${schemaText}
|
|
41
|
+
|
|
42
|
+
${promptText}`;
|
|
43
|
+
}
|
|
44
|
+
function buildHistoryPrompt(history, promptText) {
|
|
45
|
+
let effectivePrompt = String(promptText || "").trim();
|
|
46
|
+
if (!effectivePrompt) {
|
|
47
|
+
return "";
|
|
48
|
+
}
|
|
49
|
+
const historyText = Array.isArray(history)
|
|
50
|
+
? history
|
|
51
|
+
.map((item) => {
|
|
52
|
+
const role = String(item?.role || "").toLowerCase() === "assistant" ? "Assistant" : "User";
|
|
53
|
+
const content = String(item?.content || "").trim();
|
|
54
|
+
return content ? `${role}: ${content}` : "";
|
|
55
|
+
})
|
|
56
|
+
.filter(Boolean)
|
|
57
|
+
.join("\n\n")
|
|
58
|
+
: "";
|
|
59
|
+
if (!historyText) {
|
|
60
|
+
return effectivePrompt;
|
|
61
|
+
}
|
|
62
|
+
return [
|
|
63
|
+
"Continue the existing conversation with this history.",
|
|
64
|
+
"",
|
|
65
|
+
historyText,
|
|
66
|
+
"",
|
|
67
|
+
`User: ${effectivePrompt}`,
|
|
68
|
+
].join("\n");
|
|
69
|
+
}
|
|
70
|
+
function statusLineForPhase(phase) {
|
|
71
|
+
switch (phase) {
|
|
72
|
+
case "turn_started":
|
|
73
|
+
return "Kimi is working on it";
|
|
74
|
+
case "reasoning":
|
|
75
|
+
return "Kimi is thinking";
|
|
76
|
+
case "command_execution":
|
|
77
|
+
return "Kimi is calling a tool";
|
|
78
|
+
case "message_aggregation":
|
|
79
|
+
return "Kimi is writing the reply";
|
|
80
|
+
case "turn_completed":
|
|
81
|
+
return "Kimi finished";
|
|
82
|
+
case "turn_failed":
|
|
83
|
+
return "Kimi failed";
|
|
84
|
+
default:
|
|
85
|
+
return "Kimi is working";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function filterKimiPrintBaseArgs(args) {
|
|
89
|
+
const filtered = [];
|
|
90
|
+
let skipNext = false;
|
|
91
|
+
for (const rawArg of Array.isArray(args) ? args : []) {
|
|
92
|
+
const arg = String(rawArg || "");
|
|
93
|
+
if (!arg) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (skipNext) {
|
|
97
|
+
skipNext = false;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (arg === "--wire" || arg === "--print" || arg === "--final-message-only") {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (arg === "--input-format" ||
|
|
104
|
+
arg === "--output-format" ||
|
|
105
|
+
arg === "--prompt" ||
|
|
106
|
+
arg === "--command" ||
|
|
107
|
+
arg === "--session" ||
|
|
108
|
+
arg === "--work-dir" ||
|
|
109
|
+
arg === "--model") {
|
|
110
|
+
skipNext = true;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (arg.startsWith("--input-format=") ||
|
|
114
|
+
arg.startsWith("--output-format=") ||
|
|
115
|
+
arg.startsWith("--prompt=") ||
|
|
116
|
+
arg.startsWith("--command=") ||
|
|
117
|
+
arg.startsWith("--session=") ||
|
|
118
|
+
arg.startsWith("--work-dir=") ||
|
|
119
|
+
arg.startsWith("--model=")) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
filtered.push(arg);
|
|
123
|
+
}
|
|
124
|
+
return filtered;
|
|
125
|
+
}
|
|
126
|
+
function normalizeTextContent(content) {
|
|
127
|
+
if (typeof content === "string") {
|
|
128
|
+
return content;
|
|
129
|
+
}
|
|
130
|
+
if (Array.isArray(content)) {
|
|
131
|
+
return content
|
|
132
|
+
.map((part) => (part?.type === "text" && typeof part.text === "string" ? part.text : ""))
|
|
133
|
+
.filter(Boolean)
|
|
134
|
+
.join("");
|
|
135
|
+
}
|
|
136
|
+
return "";
|
|
137
|
+
}
|
|
138
|
+
function guessMimeType(filePath) {
|
|
139
|
+
const ext = path.extname(String(filePath || "")).toLowerCase();
|
|
140
|
+
switch (ext) {
|
|
141
|
+
case ".png":
|
|
142
|
+
return "image/png";
|
|
143
|
+
case ".jpg":
|
|
144
|
+
case ".jpeg":
|
|
145
|
+
return "image/jpeg";
|
|
146
|
+
case ".gif":
|
|
147
|
+
return "image/gif";
|
|
148
|
+
case ".webp":
|
|
149
|
+
return "image/webp";
|
|
150
|
+
case ".bmp":
|
|
151
|
+
return "image/bmp";
|
|
152
|
+
case ".svg":
|
|
153
|
+
return "image/svg+xml";
|
|
154
|
+
default:
|
|
155
|
+
return "application/octet-stream";
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function filePathToDataUri(filePath) {
|
|
159
|
+
const buffer = fs.readFileSync(filePath);
|
|
160
|
+
const mimeType = guessMimeType(filePath);
|
|
161
|
+
return `data:${mimeType};base64,${buffer.toString("base64")}`;
|
|
162
|
+
}
|
|
163
|
+
export class KimiPrintSession extends EventEmitter {
|
|
164
|
+
constructor(backend, options = {}) {
|
|
165
|
+
super();
|
|
166
|
+
this.backend = normalizeKimiBackend(backend);
|
|
167
|
+
this.options = options;
|
|
168
|
+
this.logger = normalizeLogger(options.logger);
|
|
169
|
+
this.cwd =
|
|
170
|
+
typeof options.cwd === "string" && options.cwd.trim()
|
|
171
|
+
? options.cwd.trim()
|
|
172
|
+
: process.cwd();
|
|
173
|
+
this.resumeSessionId = typeof options.resumeSessionId === "string" ? options.resumeSessionId.trim() : "";
|
|
174
|
+
this.sessionId = this.resumeSessionId || randomUUID();
|
|
175
|
+
this.sessionInfo = {
|
|
176
|
+
backend: this.backend,
|
|
177
|
+
sessionId: this.sessionId,
|
|
178
|
+
model: typeof options.model === "string" && options.model.trim()
|
|
179
|
+
? options.model.trim()
|
|
180
|
+
: this.backend,
|
|
181
|
+
};
|
|
182
|
+
this.history = Array.isArray(options.initialHistory) ? [...options.initialHistory] : [];
|
|
183
|
+
this.pendingHistorySeed = this.history.length > 0;
|
|
184
|
+
this.closeRequested = false;
|
|
185
|
+
this.closed = false;
|
|
186
|
+
this.currentTurn = null;
|
|
187
|
+
this.currentTurnStatus = null;
|
|
188
|
+
this.sessionAnnounced = false;
|
|
189
|
+
this.sessionMessageHandler = null;
|
|
190
|
+
this.workingStatusHandler = null;
|
|
191
|
+
this.activeReplyTarget = "";
|
|
192
|
+
this.lastReplyTarget = "";
|
|
193
|
+
this.turnDeadlineMs = getBoundedEnvInt("CONDUCTOR_TURN_DEADLINE_MS", DEFAULT_TURN_DEADLINE_MS, MIN_TURN_DEADLINE_MS, MAX_TURN_DEADLINE_MS);
|
|
194
|
+
const envConfig = loadEnvConfig(options.configFile);
|
|
195
|
+
const proxyEnv = proxyToEnv(envConfig);
|
|
196
|
+
this.env = {
|
|
197
|
+
...(envConfig && typeof envConfig === "object" ? envConfig : {}),
|
|
198
|
+
...proxyEnv,
|
|
199
|
+
...(options.env && typeof options.env === "object" ? options.env : {}),
|
|
200
|
+
};
|
|
201
|
+
const commandLine = process.env.CONDUCTOR_KIMI_COMMAND ||
|
|
202
|
+
options.commandLine ||
|
|
203
|
+
process.env.CONDUCTOR_CLI_COMMAND ||
|
|
204
|
+
DEFAULT_KIMI_COMMAND;
|
|
205
|
+
const { command, args } = parseCommandParts(commandLine);
|
|
206
|
+
if (!command) {
|
|
207
|
+
throw new Error("Invalid kimi print command");
|
|
208
|
+
}
|
|
209
|
+
this.command = command;
|
|
210
|
+
this.baseArgs = filterKimiPrintBaseArgs(args);
|
|
211
|
+
}
|
|
212
|
+
writeLog(message) {
|
|
213
|
+
emitLog(this.logger, message);
|
|
214
|
+
}
|
|
215
|
+
trace(message) {
|
|
216
|
+
this.writeLog(`[${this.backend}] [kimi-print] ${message}`);
|
|
217
|
+
}
|
|
218
|
+
get threadId() {
|
|
219
|
+
return this.sessionId;
|
|
220
|
+
}
|
|
221
|
+
get threadOptions() {
|
|
222
|
+
return {
|
|
223
|
+
model: this.sessionInfo?.model ||
|
|
224
|
+
(typeof this.options.model === "string" && this.options.model.trim()
|
|
225
|
+
? this.options.model.trim()
|
|
226
|
+
: this.backend),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
buildManualResumeCommand() {
|
|
230
|
+
return `kimi --work-dir ${this.cwd} --session ${this.sessionId}`;
|
|
231
|
+
}
|
|
232
|
+
getSnapshot() {
|
|
233
|
+
return {
|
|
234
|
+
backend: this.backend,
|
|
235
|
+
provider: KIMI_PRINT_PROVIDER_VARIANT,
|
|
236
|
+
cwd: this.cwd,
|
|
237
|
+
sessionId: this.sessionId || undefined,
|
|
238
|
+
sessionInfo: this.getSessionInfo(),
|
|
239
|
+
useSessionFileReplyStream: this.usesSessionFileReplyStream(),
|
|
240
|
+
resumeReady: Boolean(this.sessionId),
|
|
241
|
+
manualResume: this.sessionId
|
|
242
|
+
? {
|
|
243
|
+
ready: true,
|
|
244
|
+
command: this.buildManualResumeCommand(),
|
|
245
|
+
}
|
|
246
|
+
: null,
|
|
247
|
+
currentTurnStatus: this.getCurrentTurnStatus(),
|
|
248
|
+
pid: this.currentTurn?.child?.pid || undefined,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
getSessionInfo() {
|
|
252
|
+
return this.sessionInfo ? { ...this.sessionInfo } : null;
|
|
253
|
+
}
|
|
254
|
+
getCurrentTurnStatus() {
|
|
255
|
+
return this.currentTurnStatus ? { ...this.currentTurnStatus } : null;
|
|
256
|
+
}
|
|
257
|
+
async ensureSessionInfo() {
|
|
258
|
+
this.announceSession();
|
|
259
|
+
return this.getSessionInfo();
|
|
260
|
+
}
|
|
261
|
+
async getSessionUsageSummary() {
|
|
262
|
+
return {
|
|
263
|
+
sessionId: this.sessionId || undefined,
|
|
264
|
+
sessionFilePath: undefined,
|
|
265
|
+
tokenUsagePercent: undefined,
|
|
266
|
+
contextUsagePercent: undefined,
|
|
267
|
+
tokenUsage: null,
|
|
268
|
+
rateLimits: null,
|
|
269
|
+
manualResume: this.sessionId
|
|
270
|
+
? {
|
|
271
|
+
ready: true,
|
|
272
|
+
command: this.buildManualResumeCommand(),
|
|
273
|
+
}
|
|
274
|
+
: null,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
usesSessionFileReplyStream() {
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
setSessionMessageHandler(handler) {
|
|
281
|
+
this.sessionMessageHandler = typeof handler === "function" ? handler : null;
|
|
282
|
+
}
|
|
283
|
+
setWorkingStatusHandler(handler) {
|
|
284
|
+
this.workingStatusHandler = typeof handler === "function" ? handler : null;
|
|
285
|
+
}
|
|
286
|
+
setSessionReplyTarget(replyTo) {
|
|
287
|
+
const normalizedReplyTo = typeof replyTo === "string" ? replyTo.trim() : "";
|
|
288
|
+
this.activeReplyTarget = normalizedReplyTo;
|
|
289
|
+
if (normalizedReplyTo) {
|
|
290
|
+
this.lastReplyTarget = normalizedReplyTo;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
getCurrentReplyTarget() {
|
|
294
|
+
return this.activeReplyTarget || this.lastReplyTarget || undefined;
|
|
295
|
+
}
|
|
296
|
+
announceSession() {
|
|
297
|
+
if (this.sessionAnnounced) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
this.sessionAnnounced = true;
|
|
301
|
+
this.emit("session", this.getSessionInfo());
|
|
302
|
+
}
|
|
303
|
+
createSessionClosedError() {
|
|
304
|
+
return createTurnError("Kimi print session closed", {
|
|
305
|
+
reason: "session_closed",
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
updateCurrentTurnStatus(payload) {
|
|
309
|
+
const updatedAtMs = Date.now();
|
|
310
|
+
this.currentTurnStatus = {
|
|
311
|
+
source: KIMI_PRINT_PROVIDER_VARIANT,
|
|
312
|
+
replyTo: this.getCurrentReplyTarget(),
|
|
313
|
+
thread_id: this.sessionId || undefined,
|
|
314
|
+
session_id: this.sessionId || undefined,
|
|
315
|
+
...payload,
|
|
316
|
+
updated_at: new Date(updatedAtMs).toISOString(),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
async emitWorkingStatus(payload, onProgress = null) {
|
|
320
|
+
this.updateCurrentTurnStatus(payload);
|
|
321
|
+
const normalized = this.getCurrentTurnStatus();
|
|
322
|
+
if (typeof onProgress === "function") {
|
|
323
|
+
await onProgress(normalized);
|
|
324
|
+
}
|
|
325
|
+
if (typeof this.workingStatusHandler === "function") {
|
|
326
|
+
await this.workingStatusHandler(normalized);
|
|
327
|
+
}
|
|
328
|
+
this.emit("working_status", normalized);
|
|
329
|
+
return normalized;
|
|
330
|
+
}
|
|
331
|
+
async emitAssistantMessage(text) {
|
|
332
|
+
const normalizedText = typeof text === "string" ? text : "";
|
|
333
|
+
if (!normalizedText) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const payload = {
|
|
337
|
+
text: normalizedText,
|
|
338
|
+
preserveWhitespace: true,
|
|
339
|
+
replyTo: this.getCurrentReplyTarget(),
|
|
340
|
+
sessionId: this.sessionId || undefined,
|
|
341
|
+
backend: this.backend,
|
|
342
|
+
provider: KIMI_PRINT_PROVIDER_VARIANT,
|
|
343
|
+
};
|
|
344
|
+
if (typeof this.sessionMessageHandler === "function") {
|
|
345
|
+
await this.sessionMessageHandler(payload);
|
|
346
|
+
}
|
|
347
|
+
this.emit("assistant_message", payload);
|
|
348
|
+
}
|
|
349
|
+
buildPrompt(promptText) {
|
|
350
|
+
const normalizedPrompt = String(promptText || "").trim();
|
|
351
|
+
if (!normalizedPrompt) {
|
|
352
|
+
return "";
|
|
353
|
+
}
|
|
354
|
+
if (!this.pendingHistorySeed) {
|
|
355
|
+
return normalizedPrompt;
|
|
356
|
+
}
|
|
357
|
+
this.pendingHistorySeed = false;
|
|
358
|
+
return buildHistoryPrompt(this.history, normalizedPrompt);
|
|
359
|
+
}
|
|
360
|
+
buildPrintArgs() {
|
|
361
|
+
const args = [...this.baseArgs];
|
|
362
|
+
args.push("--print");
|
|
363
|
+
args.push("--input-format=stream-json");
|
|
364
|
+
args.push("--output-format=stream-json");
|
|
365
|
+
args.push(`--work-dir=${this.cwd}`);
|
|
366
|
+
if (this.sessionId) {
|
|
367
|
+
args.push(`--session=${this.sessionId}`);
|
|
368
|
+
}
|
|
369
|
+
if (typeof this.options.model === "string" && this.options.model.trim()) {
|
|
370
|
+
args.push(`--model=${this.options.model.trim()}`);
|
|
371
|
+
}
|
|
372
|
+
return args;
|
|
373
|
+
}
|
|
374
|
+
buildUserMessage(promptText, { useInitialImages = false } = {}) {
|
|
375
|
+
const images = useInitialImages && Array.isArray(this.options.initialImages)
|
|
376
|
+
? this.options.initialImages.filter((item) => typeof item === "string" && item.trim())
|
|
377
|
+
: [];
|
|
378
|
+
if (images.length === 0) {
|
|
379
|
+
return {
|
|
380
|
+
role: "user",
|
|
381
|
+
content: promptText,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
const content = [];
|
|
385
|
+
if (promptText) {
|
|
386
|
+
content.push({ type: "text", text: promptText });
|
|
387
|
+
}
|
|
388
|
+
for (const imagePath of images) {
|
|
389
|
+
content.push({
|
|
390
|
+
type: "image_url",
|
|
391
|
+
image_url: {
|
|
392
|
+
url: filePathToDataUri(imagePath),
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
role: "user",
|
|
398
|
+
content,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
maybeEmitAuthRequired(stderrTail) {
|
|
402
|
+
const lastMessage = Array.isArray(stderrTail) ? String(stderrTail.filter(Boolean).at(-1) || "") : "";
|
|
403
|
+
const normalized = lastMessage.toLowerCase();
|
|
404
|
+
if (!normalized.includes("login") &&
|
|
405
|
+
!normalized.includes("auth") &&
|
|
406
|
+
!normalized.includes("api key") &&
|
|
407
|
+
!normalized.includes("credential") &&
|
|
408
|
+
!normalized.includes("llm is not set")) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
this.emit("auth_required", {
|
|
412
|
+
reason: "login_required",
|
|
413
|
+
message: lastMessage || "Kimi authentication required",
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
async runTurn(promptText, { useInitialImages = false, onProgress = null, jsonSchema = null } = {}) {
|
|
417
|
+
if (this.closeRequested || this.closed) {
|
|
418
|
+
throw this.createSessionClosedError();
|
|
419
|
+
}
|
|
420
|
+
if (this.currentTurn) {
|
|
421
|
+
throw createTurnError("Kimi print turn already running", {
|
|
422
|
+
reason: "turn_already_running",
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
let effectivePrompt = this.buildPrompt(promptText);
|
|
426
|
+
const imagePaths = useInitialImages && Array.isArray(this.options.initialImages)
|
|
427
|
+
? this.options.initialImages.filter((item) => typeof item === "string" && item.trim())
|
|
428
|
+
: [];
|
|
429
|
+
if (jsonSchema && typeof jsonSchema === "object") {
|
|
430
|
+
const promptWithSchema = effectivePrompt || (imagePaths.length > 0 ? "Analyze the attached images." : "");
|
|
431
|
+
if (promptWithSchema) {
|
|
432
|
+
effectivePrompt = injectJsonSchemaPrompt(promptWithSchema, jsonSchema);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (!effectivePrompt && imagePaths.length === 0) {
|
|
436
|
+
return buildEmptyTurnResult();
|
|
437
|
+
}
|
|
438
|
+
this.announceSession();
|
|
439
|
+
this.history.push({ role: "user", content: String(promptText || "") });
|
|
440
|
+
const currentTurn = {
|
|
441
|
+
child: null,
|
|
442
|
+
fullText: "",
|
|
443
|
+
items: [],
|
|
444
|
+
stderrTail: [],
|
|
445
|
+
settled: false,
|
|
446
|
+
};
|
|
447
|
+
this.currentTurn = currentTurn;
|
|
448
|
+
try {
|
|
449
|
+
await this.emitWorkingStatus({
|
|
450
|
+
phase: "turn_started",
|
|
451
|
+
reply_in_progress: true,
|
|
452
|
+
status_line: statusLineForPhase("turn_started"),
|
|
453
|
+
}, onProgress);
|
|
454
|
+
const args = this.buildPrintArgs();
|
|
455
|
+
this.trace(`spawn ${[this.command, ...args].join(" ")}`);
|
|
456
|
+
const result = await new Promise((resolve, reject) => {
|
|
457
|
+
const child = spawn(this.command, args, {
|
|
458
|
+
cwd: this.cwd,
|
|
459
|
+
env: {
|
|
460
|
+
...process.env,
|
|
461
|
+
PWD: this.cwd,
|
|
462
|
+
...this.env,
|
|
463
|
+
},
|
|
464
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
465
|
+
});
|
|
466
|
+
currentTurn.child = child;
|
|
467
|
+
const stdoutReader = readline.createInterface({ input: child.stdout });
|
|
468
|
+
const stderrReader = readline.createInterface({ input: child.stderr });
|
|
469
|
+
let timeoutId = null;
|
|
470
|
+
const settle = (error, value = null) => {
|
|
471
|
+
if (currentTurn.settled) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
currentTurn.settled = true;
|
|
475
|
+
if (timeoutId) {
|
|
476
|
+
clearTimeout(timeoutId);
|
|
477
|
+
}
|
|
478
|
+
stdoutReader.close();
|
|
479
|
+
stderrReader.close();
|
|
480
|
+
if (error) {
|
|
481
|
+
reject(error);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
resolve(value);
|
|
485
|
+
};
|
|
486
|
+
timeoutId = setTimeout(() => {
|
|
487
|
+
try {
|
|
488
|
+
child.kill("SIGTERM");
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
// ignore
|
|
492
|
+
}
|
|
493
|
+
settle(createTurnError("Kimi print turn timed out", {
|
|
494
|
+
reason: "turn_timeout",
|
|
495
|
+
}));
|
|
496
|
+
}, this.turnDeadlineMs);
|
|
497
|
+
stdoutReader.on("line", (line) => {
|
|
498
|
+
const normalizedLine = String(line || "").trim();
|
|
499
|
+
if (!normalizedLine) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
let payload = null;
|
|
503
|
+
try {
|
|
504
|
+
payload = JSON.parse(normalizedLine);
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
payload = { role: "raw", content: normalizedLine };
|
|
508
|
+
}
|
|
509
|
+
currentTurn.items.push(payload);
|
|
510
|
+
const role = String(payload?.role || "").trim().toLowerCase();
|
|
511
|
+
if (role === "assistant") {
|
|
512
|
+
const assistantText = normalizeTextContent(payload.content);
|
|
513
|
+
if (Array.isArray(payload?.tool_calls) && payload.tool_calls.length > 0) {
|
|
514
|
+
void this.emitWorkingStatus({
|
|
515
|
+
phase: "command_execution",
|
|
516
|
+
reply_in_progress: true,
|
|
517
|
+
status_line: statusLineForPhase("command_execution"),
|
|
518
|
+
}, onProgress);
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
void this.emitWorkingStatus({
|
|
522
|
+
phase: "message_aggregation",
|
|
523
|
+
reply_in_progress: true,
|
|
524
|
+
status_line: statusLineForPhase("message_aggregation"),
|
|
525
|
+
}, onProgress);
|
|
526
|
+
}
|
|
527
|
+
if (assistantText) {
|
|
528
|
+
currentTurn.fullText += assistantText;
|
|
529
|
+
void this.emitAssistantMessage(assistantText);
|
|
530
|
+
}
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (role === "tool") {
|
|
534
|
+
void this.emitWorkingStatus({
|
|
535
|
+
phase: "command_execution",
|
|
536
|
+
reply_in_progress: true,
|
|
537
|
+
status_line: statusLineForPhase("command_execution"),
|
|
538
|
+
}, onProgress);
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
stderrReader.on("line", (line) => {
|
|
542
|
+
const normalizedLine = String(line || "");
|
|
543
|
+
if (!normalizedLine.trim()) {
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
currentTurn.stderrTail.push(normalizedLine);
|
|
547
|
+
if (currentTurn.stderrTail.length > 20) {
|
|
548
|
+
currentTurn.stderrTail.shift();
|
|
549
|
+
}
|
|
550
|
+
this.writeLog(`[kimi-print] stderr ${sanitizeForLog(normalizedLine, 300)}`);
|
|
551
|
+
});
|
|
552
|
+
child.on("error", (error) => {
|
|
553
|
+
settle(error);
|
|
554
|
+
});
|
|
555
|
+
child.on("exit", (code, signal) => {
|
|
556
|
+
if (this.closeRequested) {
|
|
557
|
+
settle(this.createSessionClosedError());
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (code !== 0) {
|
|
561
|
+
this.maybeEmitAuthRequired(currentTurn.stderrTail);
|
|
562
|
+
const stderrSummary = sanitizeForLog(currentTurn.stderrTail.filter(Boolean).at(-1), 200);
|
|
563
|
+
settle(createTurnError(stderrSummary ? `Kimi print failed: ${stderrSummary}` : "Kimi print failed", {
|
|
564
|
+
reason: code === 75 ? "retryable_turn_failed" : "turn_failed",
|
|
565
|
+
retryable: code === 75,
|
|
566
|
+
code,
|
|
567
|
+
signal,
|
|
568
|
+
stderr: [...currentTurn.stderrTail],
|
|
569
|
+
}));
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
settle(null, {
|
|
573
|
+
text: currentTurn.fullText,
|
|
574
|
+
items: [...currentTurn.items],
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
const userMessage = this.buildUserMessage(effectivePrompt, { useInitialImages });
|
|
578
|
+
child.stdin.write(`${JSON.stringify(userMessage)}\n`);
|
|
579
|
+
child.stdin.end();
|
|
580
|
+
});
|
|
581
|
+
if (result?.text) {
|
|
582
|
+
this.history.push({ role: "assistant", content: result.text });
|
|
583
|
+
}
|
|
584
|
+
this.activeReplyTarget = "";
|
|
585
|
+
await this.emitWorkingStatus({
|
|
586
|
+
phase: "turn_completed",
|
|
587
|
+
reply_in_progress: false,
|
|
588
|
+
status_line: statusLineForPhase("turn_completed"),
|
|
589
|
+
}, onProgress);
|
|
590
|
+
return {
|
|
591
|
+
text: result?.text || "",
|
|
592
|
+
usage: null,
|
|
593
|
+
items: result?.items || [],
|
|
594
|
+
events: result?.items || [],
|
|
595
|
+
provider: this.backend,
|
|
596
|
+
metadata: {
|
|
597
|
+
source: KIMI_PRINT_PROVIDER_VARIANT,
|
|
598
|
+
sessionId: this.sessionId || undefined,
|
|
599
|
+
},
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
if (!this.closeRequested && error?.reason !== "session_closed") {
|
|
604
|
+
await this.emitWorkingStatus({
|
|
605
|
+
phase: "turn_failed",
|
|
606
|
+
reply_in_progress: false,
|
|
607
|
+
status_line: statusLineForPhase("turn_failed"),
|
|
608
|
+
status_done_line: error?.message || "Kimi print failed",
|
|
609
|
+
}, onProgress);
|
|
610
|
+
}
|
|
611
|
+
throw error;
|
|
612
|
+
}
|
|
613
|
+
finally {
|
|
614
|
+
this.currentTurn = null;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
async close() {
|
|
618
|
+
if (this.closed) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
this.closeRequested = true;
|
|
622
|
+
this.closed = true;
|
|
623
|
+
const child = this.currentTurn?.child;
|
|
624
|
+
if (child) {
|
|
625
|
+
try {
|
|
626
|
+
child.kill("SIGTERM");
|
|
627
|
+
}
|
|
628
|
+
catch {
|
|
629
|
+
// ignore
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
@@ -163,10 +163,11 @@ export class OpencodeSdkSession extends EventEmitter<[never]> {
|
|
|
163
163
|
handleOpencodeEvent(event: any): Promise<void>;
|
|
164
164
|
handleTransportFailure(error: any): void;
|
|
165
165
|
handleTransportExit(payload: any): void;
|
|
166
|
-
interruptCurrentTurn(): Promise<
|
|
167
|
-
runTurn(promptText: any, { useInitialImages, onProgress }?: {
|
|
166
|
+
interruptCurrentTurn(): Promise<boolean>;
|
|
167
|
+
runTurn(promptText: any, { useInitialImages, onProgress, jsonSchema }?: {
|
|
168
168
|
useInitialImages?: boolean | undefined;
|
|
169
169
|
onProgress?: null | undefined;
|
|
170
|
+
jsonSchema?: null | undefined;
|
|
170
171
|
}): Promise<{
|
|
171
172
|
text: string;
|
|
172
173
|
usage: null;
|
|
@@ -1119,7 +1119,7 @@ export class OpencodeSdkSession extends EventEmitter {
|
|
|
1119
1119
|
async interruptCurrentTurn() {
|
|
1120
1120
|
const currentTurn = this.currentTurn;
|
|
1121
1121
|
if (!currentTurn || !this.client?.session || !this.sessionId) {
|
|
1122
|
-
return;
|
|
1122
|
+
return false;
|
|
1123
1123
|
}
|
|
1124
1124
|
try {
|
|
1125
1125
|
currentTurn.abortController?.abort?.();
|
|
@@ -1129,12 +1129,14 @@ export class OpencodeSdkSession extends EventEmitter {
|
|
|
1129
1129
|
}
|
|
1130
1130
|
try {
|
|
1131
1131
|
await this.client.session.abort({ sessionID: this.sessionId }, { throwOnError: true, responseStyle: "data" });
|
|
1132
|
+
return true;
|
|
1132
1133
|
}
|
|
1133
1134
|
catch {
|
|
1134
1135
|
// best effort
|
|
1135
1136
|
}
|
|
1137
|
+
return true;
|
|
1136
1138
|
}
|
|
1137
|
-
async runTurn(promptText, { useInitialImages = false, onProgress = null } = {}) {
|
|
1139
|
+
async runTurn(promptText, { useInitialImages = false, onProgress = null, jsonSchema = null } = {}) {
|
|
1138
1140
|
if (this.closeRequested) {
|
|
1139
1141
|
throw this.createSessionClosedError();
|
|
1140
1142
|
}
|
|
@@ -1211,6 +1213,9 @@ export class OpencodeSdkSession extends EventEmitter {
|
|
|
1211
1213
|
variant: typeof this.options.variant === "string" && this.options.variant.trim()
|
|
1212
1214
|
? this.options.variant.trim()
|
|
1213
1215
|
: undefined,
|
|
1216
|
+
format: jsonSchema && typeof jsonSchema === "object"
|
|
1217
|
+
? { type: "json_schema", schema: jsonSchema }
|
|
1218
|
+
: undefined,
|
|
1214
1219
|
parts: [
|
|
1215
1220
|
{
|
|
1216
1221
|
type: "text",
|