@love-moon/ai-sdk 0.17.0 → 0.17.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
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# @love-moon/ai-sdk
|
|
2
2
|
|
|
3
|
+
## 0.17.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- b124565: Add QueuedChatSession with a shared local browser broker and a FIFO turn queue
|
|
8
|
+
across processes. Each task retains its own conversation tab; idle tasks no
|
|
9
|
+
longer hold exclusive access to the provider profile. The AI SDK uses queued
|
|
10
|
+
sessions when available and reports waiting positions through working status.
|
|
11
|
+
|
|
12
|
+
Cancel pending turns on disconnect, stop/close failed conversation tabs before
|
|
13
|
+
advancing the queue, and release the browser after its last client exits.
|
|
14
|
+
Keep the exclusive ChatSession API available for direct Playwright access.
|
|
15
|
+
|
|
16
|
+
Keep logical sessions usable after an active stop, timeout, or provider failure.
|
|
17
|
+
Restore the original provider conversation for the next explicit user operation;
|
|
18
|
+
require explicit clear when safe restoration is unavailable, and never replay a
|
|
19
|
+
failed prompt. Allow clear after a failed first submission. Correct the Windows
|
|
20
|
+
named-pipe address escaping.
|
|
21
|
+
|
|
22
|
+
- 0a9488e: Add Kimi website text conversations with persistent login, multi-turn context,
|
|
23
|
+
reply progress, new chats and browser diagnostics. Expose the provider through
|
|
24
|
+
ai-sdk's chat-web backend and the web-kimi Conductor configuration alias.
|
|
25
|
+
|
|
26
|
+
Explicit unknown chat-web providers now fail with a clear error instead of
|
|
27
|
+
silently opening ChatGPT. The existing kimi CLI backend is unchanged.
|
|
28
|
+
|
|
29
|
+
## 0.17.1
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- 7e92c0f: When a task's model quota runs out ("You've hit your session limit · resets
|
|
34
|
+
1:20am"), fire no longer stops there. It reads the reset time, schedules a
|
|
35
|
+
one-off Conductor scheduled message to continue a minute or few after the
|
|
36
|
+
reset, and posts a single notice. Messages that arrive while waiting are queued
|
|
37
|
+
and handed to the resume turn together instead of each failing with another
|
|
38
|
+
"处理失败". The resume shows up in the task's scheduled-message list, where it
|
|
39
|
+
can be canceled. This also covers a quota hit while Claude works on its own
|
|
40
|
+
between user turns: ai-sdk now reports such a background turn that ends in an
|
|
41
|
+
error result (or a process exit after it) as a failed turn.
|
|
42
|
+
|
|
3
43
|
## 0.17.0
|
|
4
44
|
|
|
5
45
|
### Patch Changes
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AI SDK provider that delegates to the chat-web runtime
|
|
3
3
|
* (`@love-moon/chat-web`), which automates a real Chromium browser against
|
|
4
|
-
* ChatGPT / Gemini /
|
|
4
|
+
* ChatGPT / Gemini / Kimi and ferries conversations through their web
|
|
5
5
|
* UIs. Choose the underlying chat-web provider via:
|
|
6
6
|
*
|
|
7
|
-
* - `options.chatWebProvider`: "chatgpt" | "gemini" (preferred)
|
|
7
|
+
* - `options.chatWebProvider`: "chatgpt" | "gemini" | "kimi" (preferred)
|
|
8
8
|
* - `options.provider`: same surface
|
|
9
9
|
* - `options.model`: same surface (fallback for ergonomic
|
|
10
10
|
* `createAiSession("chat-web", { model: "gemini" })`)
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Lifecycle:
|
|
15
15
|
* - `boot()` lazily imports `@love-moon/chat-web`, registers its built-in
|
|
16
|
-
* providers, and opens a
|
|
16
|
+
* providers, and opens a queued conversation tab (headed by default).
|
|
17
17
|
* - `runTurn(prompt)` calls `session.send(prompt)` and emits a single
|
|
18
18
|
* `assistant_message` with the model's reply.
|
|
19
|
-
* - `close()`
|
|
19
|
+
* - `close()` releases this task's tab; the broker closes an idle browser.
|
|
20
20
|
*
|
|
21
21
|
* Resume: chat-web's "session" is a Chromium browser context, not a
|
|
22
22
|
* conversation ID — there is no native cross-process resume. We synthesise
|
|
@@ -50,6 +50,7 @@ export class ChatWebSession extends EventEmitter<[never]> {
|
|
|
50
50
|
closed: boolean;
|
|
51
51
|
currentTurn: {
|
|
52
52
|
aborted: boolean;
|
|
53
|
+
controller: AbortController;
|
|
53
54
|
} | null;
|
|
54
55
|
currentTurnStatus: {
|
|
55
56
|
source: string;
|
|
@@ -65,6 +66,7 @@ export class ChatWebSession extends EventEmitter<[never]> {
|
|
|
65
66
|
session_file_path: undefined;
|
|
66
67
|
updated_at: string;
|
|
67
68
|
} | null;
|
|
69
|
+
turnAttempted: boolean;
|
|
68
70
|
sessionMessageHandler: any;
|
|
69
71
|
workingStatusHandler: any;
|
|
70
72
|
activeReplyTarget: string;
|
|
@@ -4,7 +4,7 @@ import { PROVIDER_MEDIA_CAPABILITIES } from "../media-adapters.js";
|
|
|
4
4
|
import { assertMediaCapabilities, resolveTurnMedia } from "../media-input.js";
|
|
5
5
|
import { normalizeContextFiles } from "../context-files.js";
|
|
6
6
|
import { emitLog, normalizeLogger } from "../shared.js";
|
|
7
|
-
const SUPPORTED_CHAT_WEB_PROVIDERS = new Set(["chatgpt", "gemini"]);
|
|
7
|
+
const SUPPORTED_CHAT_WEB_PROVIDERS = new Set(["chatgpt", "gemini", "kimi"]);
|
|
8
8
|
const DEFAULT_CHAT_WEB_PROVIDER = "chatgpt";
|
|
9
9
|
const DEFAULT_TURN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
10
10
|
function normalizeChatWebProvider(value) {
|
|
@@ -22,9 +22,14 @@ function resolveChatWebProvider(options = {}) {
|
|
|
22
22
|
const candidates = [options.chatWebProvider, options.provider, options.model];
|
|
23
23
|
for (const candidate of candidates) {
|
|
24
24
|
const normalized = normalizeChatWebProvider(candidate);
|
|
25
|
-
if (
|
|
26
|
-
|
|
25
|
+
if (!normalized)
|
|
26
|
+
continue;
|
|
27
|
+
if (!SUPPORTED_CHAT_WEB_PROVIDERS.has(normalized)) {
|
|
28
|
+
const error = new Error(`Unknown chat-web provider "${normalized}". Supported: ${[...SUPPORTED_CHAT_WEB_PROVIDERS].join(", ")}`);
|
|
29
|
+
error.reason = "unknown_chat_web_provider";
|
|
30
|
+
throw error;
|
|
27
31
|
}
|
|
32
|
+
return normalized;
|
|
28
33
|
}
|
|
29
34
|
return DEFAULT_CHAT_WEB_PROVIDER;
|
|
30
35
|
}
|
|
@@ -93,10 +98,10 @@ function isPlaywrightMissingError(error) {
|
|
|
93
98
|
/**
|
|
94
99
|
* AI SDK provider that delegates to the chat-web runtime
|
|
95
100
|
* (`@love-moon/chat-web`), which automates a real Chromium browser against
|
|
96
|
-
* ChatGPT / Gemini /
|
|
101
|
+
* ChatGPT / Gemini / Kimi and ferries conversations through their web
|
|
97
102
|
* UIs. Choose the underlying chat-web provider via:
|
|
98
103
|
*
|
|
99
|
-
* - `options.chatWebProvider`: "chatgpt" | "gemini" (preferred)
|
|
104
|
+
* - `options.chatWebProvider`: "chatgpt" | "gemini" | "kimi" (preferred)
|
|
100
105
|
* - `options.provider`: same surface
|
|
101
106
|
* - `options.model`: same surface (fallback for ergonomic
|
|
102
107
|
* `createAiSession("chat-web", { model: "gemini" })`)
|
|
@@ -105,10 +110,10 @@ function isPlaywrightMissingError(error) {
|
|
|
105
110
|
*
|
|
106
111
|
* Lifecycle:
|
|
107
112
|
* - `boot()` lazily imports `@love-moon/chat-web`, registers its built-in
|
|
108
|
-
* providers, and opens a
|
|
113
|
+
* providers, and opens a queued conversation tab (headed by default).
|
|
109
114
|
* - `runTurn(prompt)` calls `session.send(prompt)` and emits a single
|
|
110
115
|
* `assistant_message` with the model's reply.
|
|
111
|
-
* - `close()`
|
|
116
|
+
* - `close()` releases this task's tab; the broker closes an idle browser.
|
|
112
117
|
*
|
|
113
118
|
* Resume: chat-web's "session" is a Chromium browser context, not a
|
|
114
119
|
* conversation ID — there is no native cross-process resume. We synthesise
|
|
@@ -176,6 +181,7 @@ export class ChatWebSession extends EventEmitter {
|
|
|
176
181
|
this.closed = false;
|
|
177
182
|
this.currentTurn = null;
|
|
178
183
|
this.currentTurnStatus = null;
|
|
184
|
+
this.turnAttempted = false;
|
|
179
185
|
this.sessionMessageHandler = null;
|
|
180
186
|
this.workingStatusHandler = null;
|
|
181
187
|
this.activeReplyTarget = "";
|
|
@@ -316,7 +322,10 @@ export class ChatWebSession extends EventEmitter {
|
|
|
316
322
|
if (typeof mod.registerBuiltinProviders === "function") {
|
|
317
323
|
mod.registerBuiltinProviders();
|
|
318
324
|
}
|
|
319
|
-
|
|
325
|
+
// New runtimes share a browser and schedule complete turns across workers.
|
|
326
|
+
// Keep the existing session surface for older optional chat-web versions.
|
|
327
|
+
const Session = mod.QueuedChatSession ?? mod.ChatSession;
|
|
328
|
+
if (typeof Session?.open !== "function") {
|
|
320
329
|
throw new Error("Loaded @love-moon/chat-web is missing ChatSession.open");
|
|
321
330
|
}
|
|
322
331
|
// Forward optional browser-binary overrides to chat-web. The env vars
|
|
@@ -328,7 +337,7 @@ export class ChatWebSession extends EventEmitter {
|
|
|
328
337
|
launch.channel = this.browserChannel;
|
|
329
338
|
if (this.browserExecutablePath)
|
|
330
339
|
launch.executablePath = this.browserExecutablePath;
|
|
331
|
-
this.chatSession = await
|
|
340
|
+
this.chatSession = await Session.open(this.chatWebProvider, {
|
|
332
341
|
headless: this.headless,
|
|
333
342
|
// IMPORTANT: chat-web's logger contract is `{ error, warn, info, debug }`;
|
|
334
343
|
// ai-sdk's normalised logger is `{ log }`. Passing the ai-sdk logger
|
|
@@ -376,7 +385,7 @@ export class ChatWebSession extends EventEmitter {
|
|
|
376
385
|
}
|
|
377
386
|
// Nothing has been said yet — fire boots the browser at announce time, so
|
|
378
387
|
// "booted" alone does not mean there is a conversation to walk away from.
|
|
379
|
-
if (!this.providerConversationId && this.history.length === 0) {
|
|
388
|
+
if (!this.providerConversationId && this.history.length === 0 && !this.turnAttempted) {
|
|
380
389
|
return { clear: { status: "noop" }, usage: null, metadata: {} };
|
|
381
390
|
}
|
|
382
391
|
await this.boot();
|
|
@@ -394,6 +403,7 @@ export class ChatWebSession extends EventEmitter {
|
|
|
394
403
|
providerUrl: undefined,
|
|
395
404
|
};
|
|
396
405
|
this.history = [];
|
|
406
|
+
this.turnAttempted = false;
|
|
397
407
|
this.emit("session", this.getSessionInfo());
|
|
398
408
|
// No id to report: the provider mints one on the next reply.
|
|
399
409
|
return { clear: { status: "cleared" }, usage: null, metadata: {} };
|
|
@@ -438,6 +448,8 @@ export class ChatWebSession extends EventEmitter {
|
|
|
438
448
|
switch (this.chatWebProvider) {
|
|
439
449
|
case "chatgpt":
|
|
440
450
|
return `https://chatgpt.com/c/${this.providerConversationId}`;
|
|
451
|
+
case "kimi":
|
|
452
|
+
return `https://www.kimi.com/chat/${encodeURIComponent(this.providerConversationId)}`;
|
|
441
453
|
case "gemini":
|
|
442
454
|
// chat-web's `gemini` provider targets AI Studio
|
|
443
455
|
// (aistudio.google.com/prompts/new_chat) — that's the free web
|
|
@@ -476,7 +488,7 @@ export class ChatWebSession extends EventEmitter {
|
|
|
476
488
|
error.reason = "turn_already_running";
|
|
477
489
|
throw error;
|
|
478
490
|
}
|
|
479
|
-
this.currentTurn = { aborted: false };
|
|
491
|
+
this.currentTurn = { aborted: false, controller: new AbortController() };
|
|
480
492
|
await this.emitWorkingStatus({
|
|
481
493
|
phase: "turn_started",
|
|
482
494
|
reply_in_progress: true,
|
|
@@ -486,8 +498,19 @@ export class ChatWebSession extends EventEmitter {
|
|
|
486
498
|
await this.boot();
|
|
487
499
|
if (this.closeRequested)
|
|
488
500
|
throw this.createSessionClosedError();
|
|
501
|
+
this.turnAttempted = true;
|
|
489
502
|
const result = await this.chatSession.send(prompt, {
|
|
490
503
|
timeoutMs: this.turnTimeoutMs,
|
|
504
|
+
signal: this.currentTurn.controller.signal,
|
|
505
|
+
onQueue: (position) => {
|
|
506
|
+
void this.emitWorkingStatus({
|
|
507
|
+
phase: position > 0 ? "queued" : "turn_started",
|
|
508
|
+
reply_in_progress: true,
|
|
509
|
+
status_line: position > 0
|
|
510
|
+
? `chat-web (${this.chatWebProvider}) queued: ${position} ahead`
|
|
511
|
+
: `chat-web (${this.chatWebProvider}) is working`,
|
|
512
|
+
}, onProgress);
|
|
513
|
+
},
|
|
491
514
|
onProgress: (text) => {
|
|
492
515
|
// chat-web's onProgress fires while streaming text grows; we
|
|
493
516
|
// forward those as working_status updates with a short preview.
|
|
@@ -567,12 +590,11 @@ export class ChatWebSession extends EventEmitter {
|
|
|
567
590
|
}
|
|
568
591
|
}
|
|
569
592
|
async interruptCurrentTurn() {
|
|
570
|
-
// chat-web's ChatSession doesn't expose a turn abort — the underlying
|
|
571
|
-
// Chromium tab is still busy with the model. We mark the turn as
|
|
572
|
-
// aborted so the next status emission is "interrupted", but the
|
|
573
|
-
// assistant_message may still arrive once the model finishes.
|
|
574
593
|
if (this.currentTurn) {
|
|
575
594
|
this.currentTurn.aborted = true;
|
|
595
|
+
// The shared broker removes a pending turn, or stops/closes the active
|
|
596
|
+
// task's tab before moving to the next task. No queued prompt is sent.
|
|
597
|
+
this.currentTurn.controller.abort();
|
|
576
598
|
}
|
|
577
599
|
}
|
|
578
600
|
async close() {
|
|
@@ -553,6 +553,8 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
553
553
|
status_done_line: payload?.status_done_line,
|
|
554
554
|
reply_preview: payload?.reply_preview,
|
|
555
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 } : {}),
|
|
556
558
|
};
|
|
557
559
|
this.updateCurrentTurnStatus(normalized);
|
|
558
560
|
const snapshot = this.getCurrentTurnStatus();
|
|
@@ -1123,6 +1125,17 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
1123
1125
|
if (backgroundTurn) {
|
|
1124
1126
|
await this.finishBackgroundTurn(backgroundTurn, null, failure);
|
|
1125
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
|
+
}
|
|
1126
1139
|
}
|
|
1127
1140
|
/**
|
|
1128
1141
|
* Routes the long-lived query's output. A result ends the pending turn when it
|
|
@@ -1155,9 +1168,13 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
1155
1168
|
await this.finishBackgroundTurn(target, message);
|
|
1156
1169
|
}
|
|
1157
1170
|
async finishBackgroundTurn(backgroundTurn, resultMessage, failure = null) {
|
|
1158
|
-
|
|
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;
|
|
1159
1175
|
const text = normalizeText(resultMessage?.result);
|
|
1160
|
-
|
|
1176
|
+
// An error result's text still reaches the chat, as it always has.
|
|
1177
|
+
if (!failure && resultMessage?.subtype === "success" && text && !backgroundTurn.emittedAssistantMessage) {
|
|
1161
1178
|
await this.emitAssistantMessage(text);
|
|
1162
1179
|
}
|
|
1163
1180
|
// A pending user turn owns the working status.
|
|
@@ -1167,9 +1184,11 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
1167
1184
|
await this.emitTerminalWorkingStatus(backgroundTurn, failed
|
|
1168
1185
|
? {
|
|
1169
1186
|
phase: "turn_failed",
|
|
1170
|
-
status_done_line: failure?.message ||
|
|
1187
|
+
status_done_line: failure?.message ||
|
|
1188
|
+
(resultMessage?.is_error === true && text ? text : extractResultErrorMessage(resultMessage)),
|
|
1189
|
+
background: true,
|
|
1171
1190
|
}
|
|
1172
|
-
: { phase: "turn_completed", status_done_line: "claude finished" });
|
|
1191
|
+
: { phase: "turn_completed", status_done_line: "claude finished", background: true });
|
|
1173
1192
|
}
|
|
1174
1193
|
async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null, jsonSchema = null, suppressReply = false } = {}) {
|
|
1175
1194
|
if (this.closeRequested) {
|
|
@@ -1262,8 +1281,11 @@ export class ClaudeAgentSdkSession extends EventEmitter {
|
|
|
1262
1281
|
reason: "missing_result",
|
|
1263
1282
|
});
|
|
1264
1283
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
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);
|
|
1267
1289
|
this.maybeEmitAuthRequired(errorMessage, errorMessage);
|
|
1268
1290
|
await this.emitTerminalWorkingStatus(currentTurn, {
|
|
1269
1291
|
phase: "turn_failed",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/ai-sdk",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.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.17.
|
|
56
|
+
"@love-moon/chat-web": "^0.17.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": "1671ee1"
|
|
64
64
|
}
|