@love-moon/ai-sdk 0.2.20 → 0.2.21
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/package.json +2 -2
- package/dist/tui-session.d.ts +0 -154
- package/dist/tui-session.js +0 -954
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/ai-sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -25,5 +25,5 @@
|
|
|
25
25
|
"@types/node": "^22.10.2",
|
|
26
26
|
"typescript": "^5.6.3"
|
|
27
27
|
},
|
|
28
|
-
"gitCommitId": "
|
|
28
|
+
"gitCommitId": "fa11085"
|
|
29
29
|
}
|
package/dist/tui-session.d.ts
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
export function profileNameForBackend(backend: any): any;
|
|
2
|
-
export function parseCommandParts(commandLine: any): {
|
|
3
|
-
command: string;
|
|
4
|
-
args: string[];
|
|
5
|
-
};
|
|
6
|
-
export function buildResumeArgsForBackend(backend: any, sessionId: any): string[];
|
|
7
|
-
export function resumeProviderForBackend(backend: any): "codex" | "copilot" | "claude" | null;
|
|
8
|
-
export function createAiSession(backend: any, options?: {}): TuiAiSession;
|
|
9
|
-
export class TuiAiSession extends EventEmitter<[never]> {
|
|
10
|
-
constructor(backend: any, options?: {});
|
|
11
|
-
backend: any;
|
|
12
|
-
options: {};
|
|
13
|
-
logger: any;
|
|
14
|
-
cwd: any;
|
|
15
|
-
sessionId: any;
|
|
16
|
-
history: any[];
|
|
17
|
-
pendingHistorySeed: boolean;
|
|
18
|
-
sessionInfo: {
|
|
19
|
-
backend: any;
|
|
20
|
-
sessionId: any;
|
|
21
|
-
sessionFilePath: any;
|
|
22
|
-
} | null;
|
|
23
|
-
command: string;
|
|
24
|
-
args: string[];
|
|
25
|
-
tuiDebug: boolean;
|
|
26
|
-
tuiTrace: boolean;
|
|
27
|
-
tuiTraceLines: number;
|
|
28
|
-
lastSignalSignature: string;
|
|
29
|
-
lastPollSignature: string;
|
|
30
|
-
lastSnapshotHash: string;
|
|
31
|
-
closeRequested: boolean;
|
|
32
|
-
closed: boolean;
|
|
33
|
-
closeWaiters: Set<any>;
|
|
34
|
-
sessionMessageHandler: any;
|
|
35
|
-
sessionMonitorPromise: Promise<void> | null;
|
|
36
|
-
sessionMonitorStopRequested: boolean;
|
|
37
|
-
sessionMonitorCursor: number;
|
|
38
|
-
sessionMonitorSessionId: string;
|
|
39
|
-
sessionMonitorSessionFilePath: string;
|
|
40
|
-
sessionMonitorActiveReplyTo: string;
|
|
41
|
-
sessionMonitorHasActiveReplyTarget: boolean;
|
|
42
|
-
sessionMonitorLastReplyTo: string;
|
|
43
|
-
sessionMonitorAwaitingFirstReply: boolean;
|
|
44
|
-
workingStatusHandler: any;
|
|
45
|
-
workingStatusMonitorPromise: Promise<void> | null;
|
|
46
|
-
workingStatusMonitorStopRequested: boolean;
|
|
47
|
-
lastReportedWorkingStatusLine: string;
|
|
48
|
-
turnDeadlineMs: any;
|
|
49
|
-
useSessionFileReplyStream: boolean;
|
|
50
|
-
sessionMonitorFastPollMs: any;
|
|
51
|
-
sessionMonitorSlowPollMs: any;
|
|
52
|
-
workingStatusPollMs: any;
|
|
53
|
-
driver: TuiDriver;
|
|
54
|
-
writeLog(message: any): void;
|
|
55
|
-
get threadId(): any;
|
|
56
|
-
get threadOptions(): {
|
|
57
|
-
model: any;
|
|
58
|
-
};
|
|
59
|
-
getSnapshot(): {
|
|
60
|
-
backend: any;
|
|
61
|
-
command: string;
|
|
62
|
-
args: string[];
|
|
63
|
-
cwd: any;
|
|
64
|
-
sessionId: any;
|
|
65
|
-
sessionInfo: {
|
|
66
|
-
backend: any;
|
|
67
|
-
sessionId: any;
|
|
68
|
-
sessionFilePath: any;
|
|
69
|
-
} | null;
|
|
70
|
-
useSessionFileReplyStream: boolean;
|
|
71
|
-
};
|
|
72
|
-
applySessionInfo(session: any): void;
|
|
73
|
-
getSessionInfo(): {
|
|
74
|
-
backend: any;
|
|
75
|
-
sessionId: any;
|
|
76
|
-
sessionFilePath: any;
|
|
77
|
-
} | null;
|
|
78
|
-
ensureSessionInfo(): Promise<{
|
|
79
|
-
backend: any;
|
|
80
|
-
sessionId: any;
|
|
81
|
-
sessionFilePath: any;
|
|
82
|
-
} | null>;
|
|
83
|
-
getSessionUsageSummary(): Promise<import("@love-moon/tui-driver").TuiSessionUsageSummary | null>;
|
|
84
|
-
usesSessionFileReplyStream(): boolean;
|
|
85
|
-
setSessionMessageHandler(handler: any): void;
|
|
86
|
-
setWorkingStatusHandler(handler: any): void;
|
|
87
|
-
setSessionReplyTarget(replyTo: any): void;
|
|
88
|
-
ensureSessionFileMonitor(): Promise<void>;
|
|
89
|
-
ensureWorkingStatusMonitor(): Promise<void>;
|
|
90
|
-
runSessionFileMonitor(): Promise<void>;
|
|
91
|
-
runWorkingStatusMonitor(): Promise<void>;
|
|
92
|
-
resolveSessionMonitorPollMs(): any;
|
|
93
|
-
normalizeCodexWorkingStatusLine(statusLine: any): string;
|
|
94
|
-
normalizeCopilotWorkingStatusLine(statusLine: any): string;
|
|
95
|
-
normalizeWorkingStatusLine(statusLine: any): string;
|
|
96
|
-
getCurrentReplyTarget(): string | undefined;
|
|
97
|
-
pollWorkingStatus(): Promise<void>;
|
|
98
|
-
pollSessionFileMessages(): Promise<void>;
|
|
99
|
-
createSessionClosedError(): Error;
|
|
100
|
-
createTurnTimeoutError(timeoutMs: any): Error;
|
|
101
|
-
createCloseGuard(): {
|
|
102
|
-
promise: Promise<any>;
|
|
103
|
-
cleanup: () => void;
|
|
104
|
-
};
|
|
105
|
-
createTurnTimeoutGuard(): {
|
|
106
|
-
promise: Promise<any>;
|
|
107
|
-
cleanup: () => void;
|
|
108
|
-
};
|
|
109
|
-
flushCloseWaiters(): void;
|
|
110
|
-
close(): Promise<void>;
|
|
111
|
-
getHealthStatus(): import("@love-moon/tui-driver").HealthStatus | {
|
|
112
|
-
healthy: boolean;
|
|
113
|
-
reason: string;
|
|
114
|
-
message: string;
|
|
115
|
-
};
|
|
116
|
-
buildPrompt(promptText: any, { useInitialImages }?: {
|
|
117
|
-
useInitialImages?: boolean | undefined;
|
|
118
|
-
}): string;
|
|
119
|
-
emitProgress(onProgress: any, payload: any): void;
|
|
120
|
-
trace(message: any): void;
|
|
121
|
-
formatSignalSummary(signals?: {}): {
|
|
122
|
-
prompt: string | undefined;
|
|
123
|
-
replyInProgress: boolean;
|
|
124
|
-
status: string | undefined;
|
|
125
|
-
done: string | undefined;
|
|
126
|
-
replyPreview: string | undefined;
|
|
127
|
-
blocks: any;
|
|
128
|
-
};
|
|
129
|
-
logSignals(state: any, signals: any, snapshot: any): void;
|
|
130
|
-
logSnapshot(state: any, snapshot: any): void;
|
|
131
|
-
runTurn(promptText: any, { useInitialImages, onProgress }?: {
|
|
132
|
-
useInitialImages?: boolean | undefined;
|
|
133
|
-
}): Promise<{
|
|
134
|
-
text: string;
|
|
135
|
-
usage: null;
|
|
136
|
-
items: never[];
|
|
137
|
-
events: never[];
|
|
138
|
-
provider?: undefined;
|
|
139
|
-
metadata?: undefined;
|
|
140
|
-
} | {
|
|
141
|
-
text: string;
|
|
142
|
-
usage: null;
|
|
143
|
-
items: never[];
|
|
144
|
-
events: never[];
|
|
145
|
-
provider: any;
|
|
146
|
-
metadata: {
|
|
147
|
-
source: string;
|
|
148
|
-
elapsed_ms: any;
|
|
149
|
-
signals: any;
|
|
150
|
-
};
|
|
151
|
-
}>;
|
|
152
|
-
}
|
|
153
|
-
import { EventEmitter } from "node:events";
|
|
154
|
-
import { TuiDriver } from "@love-moon/tui-driver";
|
package/dist/tui-session.js
DELETED
|
@@ -1,954 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { EventEmitter } from "node:events";
|
|
5
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
6
|
-
import yaml from "js-yaml";
|
|
7
|
-
import { TuiDriver, claudeCodeProfile, codexProfile, copilotProfile } from "@love-moon/tui-driver";
|
|
8
|
-
const CUSTOM_CLI_COMMAND = process.env.CONDUCTOR_CLI_COMMAND;
|
|
9
|
-
const DEFAULT_ALLOW_CLI_LIST = loadAllowCliList();
|
|
10
|
-
const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
|
|
11
|
-
const MIN_TURN_DEADLINE_MS = 30 * 1000;
|
|
12
|
-
const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
|
|
13
|
-
const BACKEND_PROFILE_MAP = {
|
|
14
|
-
codex: "codex",
|
|
15
|
-
code: "codex",
|
|
16
|
-
claude: "claude-code",
|
|
17
|
-
"claude-code": "claude-code",
|
|
18
|
-
copilot: "copilot",
|
|
19
|
-
};
|
|
20
|
-
function normalizeLogger(logger) {
|
|
21
|
-
if (typeof logger === "function") {
|
|
22
|
-
return { log: logger };
|
|
23
|
-
}
|
|
24
|
-
if (logger && typeof logger === "object") {
|
|
25
|
-
return logger;
|
|
26
|
-
}
|
|
27
|
-
return {};
|
|
28
|
-
}
|
|
29
|
-
function emitLog(logger, message) {
|
|
30
|
-
if (typeof logger?.log !== "function") {
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
try {
|
|
34
|
-
logger.log(message);
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
// best effort
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
function tailLines(value, count = 6) {
|
|
41
|
-
if (!value)
|
|
42
|
-
return "";
|
|
43
|
-
const lines = String(value).split(/\r?\n/);
|
|
44
|
-
return lines.slice(Math.max(0, lines.length - Math.max(1, count))).join("\n");
|
|
45
|
-
}
|
|
46
|
-
export function profileNameForBackend(backend) {
|
|
47
|
-
const normalized = String(backend || "").trim().toLowerCase();
|
|
48
|
-
return BACKEND_PROFILE_MAP[normalized] || null;
|
|
49
|
-
}
|
|
50
|
-
export function parseCommandParts(commandLine) {
|
|
51
|
-
const normalized = String(commandLine || "").trim();
|
|
52
|
-
if (!normalized) {
|
|
53
|
-
return { command: "", args: [] };
|
|
54
|
-
}
|
|
55
|
-
const parts = normalized.split(/\s+/);
|
|
56
|
-
return {
|
|
57
|
-
command: parts[0],
|
|
58
|
-
args: parts.slice(1),
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
export function buildResumeArgsForBackend(backend, sessionId) {
|
|
62
|
-
const resumeSessionId = typeof sessionId === "string" ? sessionId.trim() : "";
|
|
63
|
-
if (!resumeSessionId) {
|
|
64
|
-
return [];
|
|
65
|
-
}
|
|
66
|
-
const normalizedBackend = String(backend || "").trim().toLowerCase();
|
|
67
|
-
if (normalizedBackend === "codex" || normalizedBackend === "code") {
|
|
68
|
-
return ["resume", resumeSessionId];
|
|
69
|
-
}
|
|
70
|
-
if (normalizedBackend === "claude" || normalizedBackend === "claude-code") {
|
|
71
|
-
return ["--resume", resumeSessionId];
|
|
72
|
-
}
|
|
73
|
-
if (normalizedBackend === "copilot") {
|
|
74
|
-
return [`--resume=${resumeSessionId}`];
|
|
75
|
-
}
|
|
76
|
-
throw new Error(`--resume is not supported for backend "${backend}"`);
|
|
77
|
-
}
|
|
78
|
-
export function resumeProviderForBackend(backend) {
|
|
79
|
-
const normalizedBackend = String(backend || "").trim().toLowerCase();
|
|
80
|
-
if (normalizedBackend === "codex" || normalizedBackend === "code") {
|
|
81
|
-
return "codex";
|
|
82
|
-
}
|
|
83
|
-
if (normalizedBackend === "claude" || normalizedBackend === "claude-code") {
|
|
84
|
-
return "claude";
|
|
85
|
-
}
|
|
86
|
-
if (normalizedBackend === "copilot") {
|
|
87
|
-
return "copilot";
|
|
88
|
-
}
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
function loadAllowCliList(configFilePath) {
|
|
92
|
-
try {
|
|
93
|
-
const home = os.homedir();
|
|
94
|
-
const configPath = configFilePath || process.env.CONDUCTOR_CONFIG || path.join(home, ".conductor", "config.yaml");
|
|
95
|
-
if (fs.existsSync(configPath)) {
|
|
96
|
-
const content = fs.readFileSync(configPath, "utf8");
|
|
97
|
-
const parsed = yaml.load(content);
|
|
98
|
-
if (parsed && typeof parsed === "object" && parsed.allow_cli_list) {
|
|
99
|
-
return parsed.allow_cli_list;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
// ignore error
|
|
105
|
-
}
|
|
106
|
-
return {};
|
|
107
|
-
}
|
|
108
|
-
function loadEnvConfig(configFilePath) {
|
|
109
|
-
try {
|
|
110
|
-
const home = os.homedir();
|
|
111
|
-
const configPath = configFilePath || process.env.CONDUCTOR_CONFIG || path.join(home, ".conductor", "config.yaml");
|
|
112
|
-
if (fs.existsSync(configPath)) {
|
|
113
|
-
const content = fs.readFileSync(configPath, "utf8");
|
|
114
|
-
const parsed = yaml.load(content);
|
|
115
|
-
if (parsed && typeof parsed === "object" && parsed.envs) {
|
|
116
|
-
return parsed.envs;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
// ignore error
|
|
122
|
-
}
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
function proxyToEnv(envConfig) {
|
|
126
|
-
if (!envConfig || typeof envConfig !== "object") {
|
|
127
|
-
return {};
|
|
128
|
-
}
|
|
129
|
-
const env = {};
|
|
130
|
-
const mappings = {
|
|
131
|
-
http_proxy: ["HTTP_PROXY", "http_proxy"],
|
|
132
|
-
https_proxy: ["HTTPS_PROXY", "https_proxy"],
|
|
133
|
-
all_proxy: ["ALL_PROXY", "all_proxy"],
|
|
134
|
-
no_proxy: ["NO_PROXY", "no_proxy"],
|
|
135
|
-
};
|
|
136
|
-
for (const [key, envKeys] of Object.entries(mappings)) {
|
|
137
|
-
const value = envConfig[key] || envConfig[key.toUpperCase()];
|
|
138
|
-
if (!value) {
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
for (const envKey of envKeys) {
|
|
142
|
-
env[envKey] = value;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return env;
|
|
146
|
-
}
|
|
147
|
-
function truncateText(value, maxLen = 240) {
|
|
148
|
-
if (!value)
|
|
149
|
-
return "";
|
|
150
|
-
const text = String(value).trim();
|
|
151
|
-
if (text.length <= maxLen)
|
|
152
|
-
return text;
|
|
153
|
-
return `${text.slice(0, maxLen)}...`;
|
|
154
|
-
}
|
|
155
|
-
function isTruthyEnv(value) {
|
|
156
|
-
if (value === undefined || value === null)
|
|
157
|
-
return false;
|
|
158
|
-
const normalized = String(value).trim().toLowerCase();
|
|
159
|
-
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
160
|
-
}
|
|
161
|
-
function sanitizeForLog(value, maxLen = 180) {
|
|
162
|
-
if (!value)
|
|
163
|
-
return "";
|
|
164
|
-
return truncateText(String(value).replace(/\s+/g, " ").trim(), maxLen);
|
|
165
|
-
}
|
|
166
|
-
function getBoundedEnvInt(envName, fallback, min, max) {
|
|
167
|
-
const fallbackNumber = Number(fallback);
|
|
168
|
-
const normalizedFallback = Number.isFinite(fallbackNumber)
|
|
169
|
-
? Math.min(Math.max(Math.round(fallbackNumber), min), max)
|
|
170
|
-
: min;
|
|
171
|
-
const raw = process.env[envName];
|
|
172
|
-
const parsed = Number.parseInt(String(raw ?? ""), 10);
|
|
173
|
-
if (!Number.isFinite(parsed)) {
|
|
174
|
-
return normalizedFallback;
|
|
175
|
-
}
|
|
176
|
-
return Math.min(Math.max(parsed, min), max);
|
|
177
|
-
}
|
|
178
|
-
export function createAiSession(backend, options = {}) {
|
|
179
|
-
return new TuiAiSession(backend, options);
|
|
180
|
-
}
|
|
181
|
-
export class TuiAiSession extends EventEmitter {
|
|
182
|
-
constructor(backend, options = {}) {
|
|
183
|
-
super();
|
|
184
|
-
this.backend = backend;
|
|
185
|
-
this.options = options;
|
|
186
|
-
this.logger = normalizeLogger(options.logger);
|
|
187
|
-
this.cwd =
|
|
188
|
-
typeof options.cwd === "string" && options.cwd.trim()
|
|
189
|
-
? options.cwd.trim()
|
|
190
|
-
: process.cwd();
|
|
191
|
-
const resumeSessionId = typeof options.resumeSessionId === "string" ? options.resumeSessionId.trim() : "";
|
|
192
|
-
this.sessionId = resumeSessionId || `${backend}-${Date.now()}`;
|
|
193
|
-
this.history = Array.isArray(options.initialHistory) ? [...options.initialHistory] : [];
|
|
194
|
-
this.pendingHistorySeed = this.history.length > 0;
|
|
195
|
-
this.sessionInfo = null;
|
|
196
|
-
const allowCliList = options.configFile ? loadAllowCliList(options.configFile) : DEFAULT_ALLOW_CLI_LIST;
|
|
197
|
-
const cliCommand = CUSTOM_CLI_COMMAND || allowCliList[backend] || backend;
|
|
198
|
-
const { command, args } = parseCommandParts(cliCommand);
|
|
199
|
-
if (!command) {
|
|
200
|
-
throw new Error(`Invalid command for backend "${backend}"`);
|
|
201
|
-
}
|
|
202
|
-
const resumeArgs = buildResumeArgsForBackend(backend, resumeSessionId);
|
|
203
|
-
this.command = command;
|
|
204
|
-
this.args = [...args, ...resumeArgs];
|
|
205
|
-
this.tuiDebug = isTruthyEnv(process.env.CONDUCTOR_TUI_DEBUG);
|
|
206
|
-
this.tuiTrace = this.tuiDebug || isTruthyEnv(process.env.CONDUCTOR_TUI_TRACE);
|
|
207
|
-
this.tuiTraceLines = Number.isFinite(Number.parseInt(process.env.CONDUCTOR_TUI_TRACE_LINES || "", 10))
|
|
208
|
-
? Math.max(2, Number.parseInt(process.env.CONDUCTOR_TUI_TRACE_LINES, 10))
|
|
209
|
-
: 8;
|
|
210
|
-
this.lastSignalSignature = "";
|
|
211
|
-
this.lastPollSignature = "";
|
|
212
|
-
this.lastSnapshotHash = "";
|
|
213
|
-
this.closeRequested = false;
|
|
214
|
-
this.closed = false;
|
|
215
|
-
this.closeWaiters = new Set();
|
|
216
|
-
this.sessionMessageHandler = null;
|
|
217
|
-
this.sessionMonitorPromise = null;
|
|
218
|
-
this.sessionMonitorStopRequested = false;
|
|
219
|
-
this.sessionMonitorCursor = 0;
|
|
220
|
-
this.sessionMonitorSessionId = "";
|
|
221
|
-
this.sessionMonitorSessionFilePath = "";
|
|
222
|
-
this.sessionMonitorActiveReplyTo = "";
|
|
223
|
-
this.sessionMonitorHasActiveReplyTarget = false;
|
|
224
|
-
this.sessionMonitorLastReplyTo = "";
|
|
225
|
-
this.sessionMonitorAwaitingFirstReply = false;
|
|
226
|
-
this.workingStatusHandler = null;
|
|
227
|
-
this.workingStatusMonitorPromise = null;
|
|
228
|
-
this.workingStatusMonitorStopRequested = false;
|
|
229
|
-
this.lastReportedWorkingStatusLine = "";
|
|
230
|
-
this.turnDeadlineMs = getBoundedEnvInt("CONDUCTOR_TURN_DEADLINE_MS", DEFAULT_TURN_DEADLINE_MS, MIN_TURN_DEADLINE_MS, MAX_TURN_DEADLINE_MS);
|
|
231
|
-
const profileName = profileNameForBackend(backend);
|
|
232
|
-
if (!profileName) {
|
|
233
|
-
throw new Error(`Backend "${backend}" is not supported by tui-driver`);
|
|
234
|
-
}
|
|
235
|
-
this.useSessionFileReplyStream = profileName === "codex" || profileName === "copilot";
|
|
236
|
-
this.sessionMonitorFastPollMs = getBoundedEnvInt("CONDUCTOR_CODEX_SESSION_FAST_POLL_MS", 700, 100, 10 * 1000);
|
|
237
|
-
this.sessionMonitorSlowPollMs = getBoundedEnvInt("CONDUCTOR_CODEX_SESSION_SLOW_POLL_MS", 2500, 300, 60 * 1000);
|
|
238
|
-
this.workingStatusPollMs = getBoundedEnvInt("CONDUCTOR_CODEX_TUI_STATUS_POLL_MS", 700, 100, 10 * 1000);
|
|
239
|
-
const profileMap = {
|
|
240
|
-
codex: codexProfile,
|
|
241
|
-
"claude-code": claudeCodeProfile,
|
|
242
|
-
copilot: copilotProfile,
|
|
243
|
-
};
|
|
244
|
-
const baseProfile = profileMap[profileName];
|
|
245
|
-
const effectiveDriverArgs = [...(baseProfile.args || []), ...this.args];
|
|
246
|
-
const envConfig = loadEnvConfig(options.configFile);
|
|
247
|
-
const proxyEnv = proxyToEnv(envConfig);
|
|
248
|
-
const cliEnv = envConfig && typeof envConfig === "object" ? { ...envConfig, ...proxyEnv } : proxyEnv;
|
|
249
|
-
if (Object.keys(proxyEnv).length > 0) {
|
|
250
|
-
this.writeLog(`Using proxy: ${proxyEnv.http_proxy || proxyEnv.https_proxy || proxyEnv.all_proxy}`);
|
|
251
|
-
}
|
|
252
|
-
this.writeLog(`Using TUI command for ${backend}: ${[this.command, ...effectiveDriverArgs].join(" ")} (cwd: ${this.cwd})`);
|
|
253
|
-
if (this.tuiTrace) {
|
|
254
|
-
this.writeLog(`[${this.backend}] [tui-trace] profile=${baseProfile.name} command=${this.command} args=${JSON.stringify(effectiveDriverArgs)}`);
|
|
255
|
-
this.writeLog(`[${this.backend}] [tui-trace] timeouts=${JSON.stringify(baseProfile.timeouts || {})} size=${baseProfile.cols || 120}x${baseProfile.rows || 40}`);
|
|
256
|
-
}
|
|
257
|
-
this.driver = new TuiDriver({
|
|
258
|
-
profile: {
|
|
259
|
-
...baseProfile,
|
|
260
|
-
command: this.command,
|
|
261
|
-
args: effectiveDriverArgs,
|
|
262
|
-
env: {
|
|
263
|
-
...process.env,
|
|
264
|
-
...(baseProfile.env || {}),
|
|
265
|
-
...cliEnv,
|
|
266
|
-
},
|
|
267
|
-
},
|
|
268
|
-
cwd: this.cwd,
|
|
269
|
-
expectedSessionId: resumeSessionId || undefined,
|
|
270
|
-
debug: this.tuiDebug,
|
|
271
|
-
onSnapshot: this.tuiTrace
|
|
272
|
-
? (snapshot, state) => {
|
|
273
|
-
this.logSnapshot(state, snapshot);
|
|
274
|
-
}
|
|
275
|
-
: undefined,
|
|
276
|
-
onSignals: this.tuiTrace
|
|
277
|
-
? (signals, snapshot, state) => {
|
|
278
|
-
this.logSignals(state, signals, snapshot);
|
|
279
|
-
}
|
|
280
|
-
: undefined,
|
|
281
|
-
});
|
|
282
|
-
this.driver.on("login_required", (health) => {
|
|
283
|
-
this.writeLog(`[${this.backend}] [WARN] Login required detected: ${health.message || health.reason}`);
|
|
284
|
-
if (health.matchedPattern) {
|
|
285
|
-
this.writeLog(`[${this.backend}] [WARN] Matched pattern: "${health.matchedPattern}"`);
|
|
286
|
-
}
|
|
287
|
-
this.writeLog(`[${this.backend}] [WARN] Please run "${this.command} login" or authenticate manually.`);
|
|
288
|
-
this.emit("auth_required", health);
|
|
289
|
-
});
|
|
290
|
-
this.driver.on("session", (session) => {
|
|
291
|
-
this.applySessionInfo(session);
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
writeLog(message) {
|
|
295
|
-
emitLog(this.logger, message);
|
|
296
|
-
}
|
|
297
|
-
get threadId() {
|
|
298
|
-
return this.sessionId;
|
|
299
|
-
}
|
|
300
|
-
get threadOptions() {
|
|
301
|
-
return { model: this.backend };
|
|
302
|
-
}
|
|
303
|
-
getSnapshot() {
|
|
304
|
-
return {
|
|
305
|
-
backend: this.backend,
|
|
306
|
-
command: this.command,
|
|
307
|
-
args: [...this.args],
|
|
308
|
-
cwd: this.cwd,
|
|
309
|
-
sessionId: this.sessionId,
|
|
310
|
-
sessionInfo: this.getSessionInfo(),
|
|
311
|
-
useSessionFileReplyStream: this.usesSessionFileReplyStream(),
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
applySessionInfo(session) {
|
|
315
|
-
if (!session || typeof session !== "object") {
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
const sessionId = typeof session.sessionId === "string" ? session.sessionId.trim() : "";
|
|
319
|
-
const sessionFilePath = typeof session.sessionFilePath === "string" ? session.sessionFilePath.trim() : "";
|
|
320
|
-
if (!sessionId) {
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
this.sessionId = sessionId;
|
|
324
|
-
this.sessionInfo = {
|
|
325
|
-
backend: this.backend,
|
|
326
|
-
sessionId,
|
|
327
|
-
sessionFilePath: sessionFilePath || undefined,
|
|
328
|
-
};
|
|
329
|
-
this.trace(`session id=${sessionId} file="${sanitizeForLog(sessionFilePath, 180)}"`);
|
|
330
|
-
this.emit("session", this.getSessionInfo());
|
|
331
|
-
if (this.useSessionFileReplyStream) {
|
|
332
|
-
void this.ensureSessionFileMonitor();
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
getSessionInfo() {
|
|
336
|
-
if (this.sessionInfo) {
|
|
337
|
-
return { ...this.sessionInfo };
|
|
338
|
-
}
|
|
339
|
-
return null;
|
|
340
|
-
}
|
|
341
|
-
async ensureSessionInfo() {
|
|
342
|
-
if (!this.driver) {
|
|
343
|
-
return null;
|
|
344
|
-
}
|
|
345
|
-
try {
|
|
346
|
-
await this.driver.boot();
|
|
347
|
-
}
|
|
348
|
-
catch (error) {
|
|
349
|
-
this.trace(`session boot failed: ${sanitizeForLog(error?.message || error, 180)}`);
|
|
350
|
-
return this.getSessionInfo();
|
|
351
|
-
}
|
|
352
|
-
try {
|
|
353
|
-
if (typeof this.driver.ensureSessionInfo === "function") {
|
|
354
|
-
const detected = await this.driver.ensureSessionInfo();
|
|
355
|
-
this.applySessionInfo(detected);
|
|
356
|
-
}
|
|
357
|
-
else if (typeof this.driver.getSessionInfo === "function") {
|
|
358
|
-
this.applySessionInfo(this.driver.getSessionInfo());
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
catch (error) {
|
|
362
|
-
this.trace(`session detect failed: ${sanitizeForLog(error?.message || error, 180)}`);
|
|
363
|
-
}
|
|
364
|
-
return this.getSessionInfo();
|
|
365
|
-
}
|
|
366
|
-
async getSessionUsageSummary() {
|
|
367
|
-
if (!this.driver || typeof this.driver.getSessionUsageSummary !== "function") {
|
|
368
|
-
return null;
|
|
369
|
-
}
|
|
370
|
-
try {
|
|
371
|
-
const summary = await this.driver.getSessionUsageSummary();
|
|
372
|
-
return summary && typeof summary === "object" ? summary : null;
|
|
373
|
-
}
|
|
374
|
-
catch (error) {
|
|
375
|
-
this.trace(`session usage detect failed: ${sanitizeForLog(error?.message || error, 180)}`);
|
|
376
|
-
return null;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
usesSessionFileReplyStream() {
|
|
380
|
-
return Boolean(this.useSessionFileReplyStream);
|
|
381
|
-
}
|
|
382
|
-
setSessionMessageHandler(handler) {
|
|
383
|
-
this.sessionMessageHandler = typeof handler === "function" ? handler : null;
|
|
384
|
-
if (this.useSessionFileReplyStream) {
|
|
385
|
-
void this.ensureSessionFileMonitor();
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
setWorkingStatusHandler(handler) {
|
|
389
|
-
this.workingStatusHandler = typeof handler === "function" ? handler : null;
|
|
390
|
-
if (this.useSessionFileReplyStream) {
|
|
391
|
-
void this.ensureWorkingStatusMonitor();
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
setSessionReplyTarget(replyTo) {
|
|
395
|
-
if (!this.useSessionFileReplyStream) {
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
const normalizedReplyTo = typeof replyTo === "string" ? replyTo.trim() : "";
|
|
399
|
-
this.sessionMonitorActiveReplyTo = normalizedReplyTo;
|
|
400
|
-
this.sessionMonitorHasActiveReplyTarget = true;
|
|
401
|
-
if (normalizedReplyTo) {
|
|
402
|
-
this.sessionMonitorLastReplyTo = normalizedReplyTo;
|
|
403
|
-
}
|
|
404
|
-
this.sessionMonitorAwaitingFirstReply = true;
|
|
405
|
-
void this.ensureSessionFileMonitor();
|
|
406
|
-
}
|
|
407
|
-
async ensureSessionFileMonitor() {
|
|
408
|
-
if (!this.useSessionFileReplyStream || this.sessionMonitorPromise) {
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
this.sessionMonitorStopRequested = false;
|
|
412
|
-
const monitorPromise = this.runSessionFileMonitor();
|
|
413
|
-
this.sessionMonitorPromise = monitorPromise;
|
|
414
|
-
monitorPromise.catch((error) => {
|
|
415
|
-
this.trace(`session monitor exited: ${sanitizeForLog(error?.message || error, 180)}`);
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
async ensureWorkingStatusMonitor() {
|
|
419
|
-
if (!this.useSessionFileReplyStream || this.workingStatusMonitorPromise) {
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
this.workingStatusMonitorStopRequested = false;
|
|
423
|
-
const monitorPromise = this.runWorkingStatusMonitor();
|
|
424
|
-
this.workingStatusMonitorPromise = monitorPromise;
|
|
425
|
-
monitorPromise.catch((error) => {
|
|
426
|
-
this.trace(`working status monitor exited: ${sanitizeForLog(error?.message || error, 180)}`);
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
async runSessionFileMonitor() {
|
|
430
|
-
try {
|
|
431
|
-
while (!this.closeRequested && !this.sessionMonitorStopRequested) {
|
|
432
|
-
try {
|
|
433
|
-
await this.pollSessionFileMessages();
|
|
434
|
-
}
|
|
435
|
-
catch (error) {
|
|
436
|
-
this.trace(`session monitor poll failed: ${sanitizeForLog(error?.message || error, 180)}`);
|
|
437
|
-
}
|
|
438
|
-
await delay(this.resolveSessionMonitorPollMs());
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
finally {
|
|
442
|
-
this.sessionMonitorPromise = null;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
async runWorkingStatusMonitor() {
|
|
446
|
-
try {
|
|
447
|
-
while (!this.closeRequested && !this.workingStatusMonitorStopRequested) {
|
|
448
|
-
try {
|
|
449
|
-
await this.pollWorkingStatus();
|
|
450
|
-
}
|
|
451
|
-
catch (error) {
|
|
452
|
-
this.trace(`working status monitor poll failed: ${sanitizeForLog(error?.message || error, 180)}`);
|
|
453
|
-
}
|
|
454
|
-
await delay(this.workingStatusPollMs);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
finally {
|
|
458
|
-
this.workingStatusMonitorPromise = null;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
resolveSessionMonitorPollMs() {
|
|
462
|
-
return this.sessionMonitorAwaitingFirstReply
|
|
463
|
-
? this.sessionMonitorFastPollMs
|
|
464
|
-
: this.sessionMonitorSlowPollMs;
|
|
465
|
-
}
|
|
466
|
-
normalizeCodexWorkingStatusLine(statusLine) {
|
|
467
|
-
const normalized = typeof statusLine === "string" ? statusLine.trim() : "";
|
|
468
|
-
if (!normalized) {
|
|
469
|
-
return "";
|
|
470
|
-
}
|
|
471
|
-
if (/^\s*[•◦]\s*Working\b/i.test(normalized)) {
|
|
472
|
-
return normalized;
|
|
473
|
-
}
|
|
474
|
-
if (/\bWorking\s*\([^)]*\)/i.test(normalized)) {
|
|
475
|
-
return normalized;
|
|
476
|
-
}
|
|
477
|
-
return "";
|
|
478
|
-
}
|
|
479
|
-
normalizeCopilotWorkingStatusLine(statusLine) {
|
|
480
|
-
const normalized = typeof statusLine === "string" ? statusLine.trim() : "";
|
|
481
|
-
if (!normalized) {
|
|
482
|
-
return "";
|
|
483
|
-
}
|
|
484
|
-
if (/^\s*[∙◉◎◐◑◒◓◔◕●○•◦]\s*.+Esc to cancel/i.test(normalized)) {
|
|
485
|
-
return normalized;
|
|
486
|
-
}
|
|
487
|
-
if (/^\s*.+Esc to cancel/i.test(normalized)) {
|
|
488
|
-
return normalized;
|
|
489
|
-
}
|
|
490
|
-
return "";
|
|
491
|
-
}
|
|
492
|
-
normalizeWorkingStatusLine(statusLine) {
|
|
493
|
-
if (this.backend === "copilot") {
|
|
494
|
-
return this.normalizeCopilotWorkingStatusLine(statusLine);
|
|
495
|
-
}
|
|
496
|
-
return this.normalizeCodexWorkingStatusLine(statusLine);
|
|
497
|
-
}
|
|
498
|
-
getCurrentReplyTarget() {
|
|
499
|
-
if (this.sessionMonitorHasActiveReplyTarget) {
|
|
500
|
-
return this.sessionMonitorActiveReplyTo || undefined;
|
|
501
|
-
}
|
|
502
|
-
return this.sessionMonitorLastReplyTo || undefined;
|
|
503
|
-
}
|
|
504
|
-
async pollWorkingStatus() {
|
|
505
|
-
if (!this.useSessionFileReplyStream || !this.driver || !this.driver.running) {
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
const signals = this.driver.getSignals();
|
|
509
|
-
const workingStatusLine = this.normalizeWorkingStatusLine(signals.statusLine);
|
|
510
|
-
if (workingStatusLine === this.lastReportedWorkingStatusLine) {
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
this.lastReportedWorkingStatusLine = workingStatusLine;
|
|
514
|
-
if (typeof this.workingStatusHandler !== "function") {
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
const payload = workingStatusLine
|
|
518
|
-
? {
|
|
519
|
-
phase: "working_status_monitor",
|
|
520
|
-
source: "tui-driver",
|
|
521
|
-
reply_in_progress: true,
|
|
522
|
-
status_line: workingStatusLine,
|
|
523
|
-
replyTo: this.getCurrentReplyTarget(),
|
|
524
|
-
}
|
|
525
|
-
: {
|
|
526
|
-
phase: "working_status_clear",
|
|
527
|
-
source: "tui-driver",
|
|
528
|
-
reply_in_progress: false,
|
|
529
|
-
replyTo: this.getCurrentReplyTarget(),
|
|
530
|
-
};
|
|
531
|
-
await this.workingStatusHandler(payload);
|
|
532
|
-
this.emit("working_status", payload);
|
|
533
|
-
}
|
|
534
|
-
async pollSessionFileMessages() {
|
|
535
|
-
if (!this.useSessionFileReplyStream || !this.driver) {
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
const sessionInfo = this.getSessionInfo();
|
|
539
|
-
if (!sessionInfo?.sessionId || !sessionInfo?.sessionFilePath) {
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
const sessionId = String(sessionInfo.sessionId).trim();
|
|
543
|
-
const sessionFilePath = String(sessionInfo.sessionFilePath).trim();
|
|
544
|
-
if (!sessionId || !sessionFilePath) {
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
const sessionChanged = sessionId !== this.sessionMonitorSessionId ||
|
|
548
|
-
sessionFilePath !== this.sessionMonitorSessionFilePath;
|
|
549
|
-
if (sessionChanged) {
|
|
550
|
-
this.sessionMonitorSessionId = sessionId;
|
|
551
|
-
this.sessionMonitorSessionFilePath = sessionFilePath;
|
|
552
|
-
this.sessionMonitorCursor = this.sessionMonitorAwaitingFirstReply
|
|
553
|
-
? 0
|
|
554
|
-
: await this.driver.getSessionFileSize(sessionInfo);
|
|
555
|
-
this.trace(`session monitor bound id=${sessionId} file="${sanitizeForLog(sessionFilePath, 180)}" cursor=${this.sessionMonitorCursor}`);
|
|
556
|
-
}
|
|
557
|
-
const batch = await this.driver.readSessionAssistantMessagesSince(sessionInfo, this.sessionMonitorCursor);
|
|
558
|
-
this.sessionMonitorCursor = Number.isFinite(batch?.nextOffset)
|
|
559
|
-
? batch.nextOffset
|
|
560
|
-
: this.sessionMonitorCursor;
|
|
561
|
-
const messages = Array.isArray(batch?.messages) ? batch.messages : [];
|
|
562
|
-
if (messages.length === 0) {
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
for (const message of messages) {
|
|
566
|
-
const text = typeof message?.text === "string" ? message.text.trim() : "";
|
|
567
|
-
if (!text) {
|
|
568
|
-
continue;
|
|
569
|
-
}
|
|
570
|
-
this.history.push({ role: "assistant", content: text });
|
|
571
|
-
this.sessionMonitorAwaitingFirstReply = false;
|
|
572
|
-
const payload = {
|
|
573
|
-
...message,
|
|
574
|
-
replyTo: this.sessionMonitorHasActiveReplyTarget
|
|
575
|
-
? this.sessionMonitorActiveReplyTo || undefined
|
|
576
|
-
: this.sessionMonitorLastReplyTo || undefined,
|
|
577
|
-
};
|
|
578
|
-
if (typeof this.sessionMessageHandler === "function") {
|
|
579
|
-
await this.sessionMessageHandler(payload);
|
|
580
|
-
}
|
|
581
|
-
this.emit("assistant_message", payload);
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
createSessionClosedError() {
|
|
585
|
-
const error = new Error("TUI session closed");
|
|
586
|
-
error.reason = "session_closed";
|
|
587
|
-
return error;
|
|
588
|
-
}
|
|
589
|
-
createTurnTimeoutError(timeoutMs) {
|
|
590
|
-
const seconds = Math.max(1, Math.round(timeoutMs / 1000));
|
|
591
|
-
const error = new Error(`Turn exceeded hard deadline (${seconds}s)`);
|
|
592
|
-
error.reason = "turn_timeout";
|
|
593
|
-
error.timeoutMs = timeoutMs;
|
|
594
|
-
return error;
|
|
595
|
-
}
|
|
596
|
-
createCloseGuard() {
|
|
597
|
-
if (this.closeRequested) {
|
|
598
|
-
return {
|
|
599
|
-
promise: Promise.reject(this.createSessionClosedError()),
|
|
600
|
-
cleanup: () => { },
|
|
601
|
-
};
|
|
602
|
-
}
|
|
603
|
-
let waiter = null;
|
|
604
|
-
const promise = new Promise((_, reject) => {
|
|
605
|
-
waiter = () => {
|
|
606
|
-
reject(this.createSessionClosedError());
|
|
607
|
-
};
|
|
608
|
-
this.closeWaiters.add(waiter);
|
|
609
|
-
});
|
|
610
|
-
return {
|
|
611
|
-
promise,
|
|
612
|
-
cleanup: () => {
|
|
613
|
-
if (waiter) {
|
|
614
|
-
this.closeWaiters.delete(waiter);
|
|
615
|
-
}
|
|
616
|
-
},
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
createTurnTimeoutGuard() {
|
|
620
|
-
if (!Number.isFinite(this.turnDeadlineMs) || this.turnDeadlineMs <= 0) {
|
|
621
|
-
return {
|
|
622
|
-
promise: new Promise(() => { }),
|
|
623
|
-
cleanup: () => { },
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
let timer = null;
|
|
627
|
-
const promise = new Promise((_, reject) => {
|
|
628
|
-
timer = setTimeout(() => {
|
|
629
|
-
reject(this.createTurnTimeoutError(this.turnDeadlineMs));
|
|
630
|
-
}, this.turnDeadlineMs);
|
|
631
|
-
if (typeof timer.unref === "function") {
|
|
632
|
-
timer.unref();
|
|
633
|
-
}
|
|
634
|
-
});
|
|
635
|
-
return {
|
|
636
|
-
promise,
|
|
637
|
-
cleanup: () => {
|
|
638
|
-
if (timer) {
|
|
639
|
-
clearTimeout(timer);
|
|
640
|
-
}
|
|
641
|
-
},
|
|
642
|
-
};
|
|
643
|
-
}
|
|
644
|
-
flushCloseWaiters() {
|
|
645
|
-
if (!this.closeWaiters || this.closeWaiters.size === 0) {
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
for (const waiter of this.closeWaiters) {
|
|
649
|
-
try {
|
|
650
|
-
waiter();
|
|
651
|
-
}
|
|
652
|
-
catch {
|
|
653
|
-
// best effort
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
this.closeWaiters.clear();
|
|
657
|
-
}
|
|
658
|
-
async close() {
|
|
659
|
-
if (this.closed) {
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
this.closed = true;
|
|
663
|
-
this.closeRequested = true;
|
|
664
|
-
this.sessionMonitorStopRequested = true;
|
|
665
|
-
this.workingStatusMonitorStopRequested = true;
|
|
666
|
-
this.flushCloseWaiters();
|
|
667
|
-
if (this.driver) {
|
|
668
|
-
this.driver.kill();
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
getHealthStatus() {
|
|
672
|
-
if (!this.driver) {
|
|
673
|
-
return { healthy: false, reason: "not_initialized", message: "Driver not initialized" };
|
|
674
|
-
}
|
|
675
|
-
return this.driver.healthCheck();
|
|
676
|
-
}
|
|
677
|
-
buildPrompt(promptText, { useInitialImages = false } = {}) {
|
|
678
|
-
let effectivePrompt = String(promptText || "").trim();
|
|
679
|
-
if (!effectivePrompt) {
|
|
680
|
-
return "";
|
|
681
|
-
}
|
|
682
|
-
if (this.pendingHistorySeed) {
|
|
683
|
-
const historyText = this.history
|
|
684
|
-
.map((item) => {
|
|
685
|
-
const role = String(item?.role || "").toLowerCase() === "assistant" ? "Assistant" : "User";
|
|
686
|
-
return `${role}: ${String(item?.content || "").trim()}`;
|
|
687
|
-
})
|
|
688
|
-
.filter(Boolean)
|
|
689
|
-
.join("\n\n");
|
|
690
|
-
if (historyText) {
|
|
691
|
-
effectivePrompt = [
|
|
692
|
-
"Continue the existing conversation with this history.",
|
|
693
|
-
"",
|
|
694
|
-
historyText,
|
|
695
|
-
"",
|
|
696
|
-
`User: ${effectivePrompt}`,
|
|
697
|
-
].join("\n");
|
|
698
|
-
}
|
|
699
|
-
this.pendingHistorySeed = false;
|
|
700
|
-
}
|
|
701
|
-
const images = Array.isArray(this.options.initialImages) ? this.options.initialImages : [];
|
|
702
|
-
if (useInitialImages && images.length > 0) {
|
|
703
|
-
const imageContext = images.map((item, idx) => `${idx + 1}. ${item}`).join("\n");
|
|
704
|
-
effectivePrompt = `${effectivePrompt}\n\nAttached image files:\n${imageContext}`;
|
|
705
|
-
}
|
|
706
|
-
return effectivePrompt;
|
|
707
|
-
}
|
|
708
|
-
emitProgress(onProgress, payload) {
|
|
709
|
-
if (typeof onProgress !== "function") {
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
try {
|
|
713
|
-
onProgress(payload);
|
|
714
|
-
}
|
|
715
|
-
catch {
|
|
716
|
-
// best effort
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
trace(message) {
|
|
720
|
-
if (!this.tuiTrace) {
|
|
721
|
-
return;
|
|
722
|
-
}
|
|
723
|
-
this.writeLog(`[${this.backend}] [tui-trace] ${message}`);
|
|
724
|
-
}
|
|
725
|
-
formatSignalSummary(signals = {}) {
|
|
726
|
-
return {
|
|
727
|
-
prompt: sanitizeForLog(signals.promptLine || "", 100) || undefined,
|
|
728
|
-
replyInProgress: Boolean(signals.replyInProgress),
|
|
729
|
-
status: sanitizeForLog(signals.statusLine || "", 140) || undefined,
|
|
730
|
-
done: sanitizeForLog(signals.statusDoneLine || "", 140) || undefined,
|
|
731
|
-
replyPreview: sanitizeForLog(signals.replyText || "", 180) || undefined,
|
|
732
|
-
blocks: Array.isArray(signals.replyBlocks) ? signals.replyBlocks.length : 0,
|
|
733
|
-
};
|
|
734
|
-
}
|
|
735
|
-
logSignals(state, signals, snapshot) {
|
|
736
|
-
const summary = this.formatSignalSummary(signals);
|
|
737
|
-
const signature = JSON.stringify(summary);
|
|
738
|
-
if (signature === this.lastSignalSignature) {
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
this.lastSignalSignature = signature;
|
|
742
|
-
this.trace(`signals state=${state} hash=${snapshot?.hash || "n/a"} prompt="${summary.prompt || ""}" status="${summary.status || ""}" done="${summary.done || ""}" replyInProgress=${summary.replyInProgress} blocks=${summary.blocks} preview="${summary.replyPreview || ""}"`);
|
|
743
|
-
}
|
|
744
|
-
logSnapshot(state, snapshot) {
|
|
745
|
-
if (!snapshot || snapshot.hash === this.lastSnapshotHash) {
|
|
746
|
-
return;
|
|
747
|
-
}
|
|
748
|
-
this.lastSnapshotHash = snapshot.hash;
|
|
749
|
-
const viewportTail = sanitizeForLog(tailLines(snapshot.viewportText, this.tuiTraceLines), 400);
|
|
750
|
-
this.trace(`snapshot state=${state} hash=${snapshot.hash} cursor=${snapshot.cursor?.x || 0},${snapshot.cursor?.y || 0} tail="${viewportTail}"`);
|
|
751
|
-
}
|
|
752
|
-
async runTurn(promptText, { useInitialImages = false, onProgress } = {}) {
|
|
753
|
-
if (this.closeRequested) {
|
|
754
|
-
throw this.createSessionClosedError();
|
|
755
|
-
}
|
|
756
|
-
const effectivePrompt = this.buildPrompt(promptText, { useInitialImages });
|
|
757
|
-
if (!effectivePrompt) {
|
|
758
|
-
return {
|
|
759
|
-
text: "",
|
|
760
|
-
usage: null,
|
|
761
|
-
items: [],
|
|
762
|
-
events: [],
|
|
763
|
-
};
|
|
764
|
-
}
|
|
765
|
-
this.writeLog(`[${this.backend}] Running prompt: ${truncateText(effectivePrompt, 100)}...`);
|
|
766
|
-
this.trace(`runTurn start promptLen=${effectivePrompt.length} useInitialImages=${Boolean(useInitialImages)}`);
|
|
767
|
-
const useSessionFileReplyStream = this.usesSessionFileReplyStream();
|
|
768
|
-
const handleStateChange = (transition) => {
|
|
769
|
-
this.trace(`state ${transition.from} -> ${transition.to}`);
|
|
770
|
-
if (useSessionFileReplyStream) {
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
this.emitProgress(onProgress, {
|
|
774
|
-
state: transition.to,
|
|
775
|
-
phase: "state_change",
|
|
776
|
-
source: "tui-driver",
|
|
777
|
-
});
|
|
778
|
-
};
|
|
779
|
-
this.driver.on("stateChange", handleStateChange);
|
|
780
|
-
const signalTimer = setInterval(() => {
|
|
781
|
-
const signals = this.driver.getSignals();
|
|
782
|
-
if (this.tuiTrace) {
|
|
783
|
-
const pollSummary = this.formatSignalSummary(signals);
|
|
784
|
-
const pollSignature = JSON.stringify({
|
|
785
|
-
state: this.driver.state,
|
|
786
|
-
replyInProgress: pollSummary.replyInProgress,
|
|
787
|
-
status: pollSummary.status,
|
|
788
|
-
done: pollSummary.done,
|
|
789
|
-
preview: pollSummary.replyPreview,
|
|
790
|
-
});
|
|
791
|
-
if (pollSignature !== this.lastPollSignature) {
|
|
792
|
-
this.lastPollSignature = pollSignature;
|
|
793
|
-
this.trace(`poll state=${this.driver.state} replyInProgress=${pollSummary.replyInProgress} status="${pollSummary.status || ""}" done="${pollSummary.done || ""}" preview="${pollSummary.replyPreview || ""}"`);
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
if (useSessionFileReplyStream) {
|
|
797
|
-
return;
|
|
798
|
-
}
|
|
799
|
-
this.emitProgress(onProgress, {
|
|
800
|
-
state: this.driver.state,
|
|
801
|
-
phase: "signal_poll",
|
|
802
|
-
source: "tui-driver",
|
|
803
|
-
reply_in_progress: Boolean(signals.replyInProgress),
|
|
804
|
-
status_line: signals.statusLine || undefined,
|
|
805
|
-
status_done_line: signals.statusDoneLine || undefined,
|
|
806
|
-
reply_preview: truncateText(signals.replyText || "", 240) || undefined,
|
|
807
|
-
});
|
|
808
|
-
}, 700);
|
|
809
|
-
if (typeof signalTimer.unref === "function") {
|
|
810
|
-
signalTimer.unref();
|
|
811
|
-
}
|
|
812
|
-
const previousCwd = process.cwd();
|
|
813
|
-
const shouldSwitchCwd = this.cwd && this.cwd !== previousCwd;
|
|
814
|
-
if (shouldSwitchCwd) {
|
|
815
|
-
try {
|
|
816
|
-
process.chdir(this.cwd);
|
|
817
|
-
}
|
|
818
|
-
catch (error) {
|
|
819
|
-
throw new Error(`Failed to switch backend cwd to ${this.cwd}: ${error?.message || error}`);
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
const closeGuard = this.createCloseGuard();
|
|
823
|
-
const turnTimeoutGuard = this.createTurnTimeoutGuard();
|
|
824
|
-
if (useSessionFileReplyStream) {
|
|
825
|
-
this.history.push({ role: "user", content: promptText });
|
|
826
|
-
void this.ensureSessionFileMonitor();
|
|
827
|
-
void this.ensureWorkingStatusMonitor();
|
|
828
|
-
}
|
|
829
|
-
try {
|
|
830
|
-
const result = await Promise.race([this.driver.ask(effectivePrompt), closeGuard.promise, turnTimeoutGuard.promise]);
|
|
831
|
-
const answer = String(result.answer || result.replyText || "").trim();
|
|
832
|
-
this.trace(`runTurn finished success=${Boolean(result.success)} elapsedMs=${result.elapsedMs} answerLen=${answer.length} state=${this.driver.state}`);
|
|
833
|
-
if (!useSessionFileReplyStream) {
|
|
834
|
-
this.history.push({ role: "user", content: promptText });
|
|
835
|
-
}
|
|
836
|
-
if (answer && !useSessionFileReplyStream) {
|
|
837
|
-
this.history.push({ role: "assistant", content: answer });
|
|
838
|
-
}
|
|
839
|
-
if (!useSessionFileReplyStream) {
|
|
840
|
-
this.emitProgress(onProgress, {
|
|
841
|
-
state: result.success ? "DONE" : "ERROR",
|
|
842
|
-
phase: "turn_result",
|
|
843
|
-
source: "tui-driver",
|
|
844
|
-
reply_in_progress: false,
|
|
845
|
-
status_line: result.statusLine || undefined,
|
|
846
|
-
status_done_line: result.statusDoneLine || undefined,
|
|
847
|
-
reply_preview: truncateText(result.replyText || answer, 240) || undefined,
|
|
848
|
-
});
|
|
849
|
-
}
|
|
850
|
-
if (!result.success) {
|
|
851
|
-
const error = result.error || new Error("tui-driver failed to complete this turn");
|
|
852
|
-
throw error;
|
|
853
|
-
}
|
|
854
|
-
this.writeLog(`[${this.backend}] Response received: ${truncateText(answer, 100)}...`);
|
|
855
|
-
this.trace(`runTurn reply preview="${sanitizeForLog(answer, 220)}"`);
|
|
856
|
-
return {
|
|
857
|
-
text: answer,
|
|
858
|
-
usage: null,
|
|
859
|
-
items: [],
|
|
860
|
-
events: [],
|
|
861
|
-
provider: this.backend,
|
|
862
|
-
metadata: {
|
|
863
|
-
source: "tui-driver",
|
|
864
|
-
elapsed_ms: result.elapsedMs,
|
|
865
|
-
signals: result.signals ?? null,
|
|
866
|
-
},
|
|
867
|
-
};
|
|
868
|
-
}
|
|
869
|
-
catch (error) {
|
|
870
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
871
|
-
const errorReason = error?.reason || "unknown";
|
|
872
|
-
if (errorReason === "session_closed") {
|
|
873
|
-
this.trace("runTurn interrupted because backend session is closing");
|
|
874
|
-
throw error instanceof Error ? error : new Error(errorMessage);
|
|
875
|
-
}
|
|
876
|
-
if (errorReason === "turn_timeout") {
|
|
877
|
-
this.emitProgress(onProgress, {
|
|
878
|
-
state: "ERROR",
|
|
879
|
-
phase: "timeout_recovered",
|
|
880
|
-
source: "tui-driver",
|
|
881
|
-
error: errorMessage,
|
|
882
|
-
reason: errorReason,
|
|
883
|
-
timeout_ms: error?.timeoutMs,
|
|
884
|
-
});
|
|
885
|
-
this.writeLog(`[${this.backend}] Turn timed out (${error?.timeoutMs || this.turnDeadlineMs}ms), restarting TUI session`);
|
|
886
|
-
try {
|
|
887
|
-
await this.driver.forceRestart();
|
|
888
|
-
}
|
|
889
|
-
catch (restartError) {
|
|
890
|
-
this.writeLog(`[${this.backend}] Failed to restart TUI after timeout: ${restartError?.message || restartError}`);
|
|
891
|
-
}
|
|
892
|
-
this.writeLog(`[${this.backend}] Error: ${errorMessage}`);
|
|
893
|
-
}
|
|
894
|
-
else if (errorReason === "login_required") {
|
|
895
|
-
this.emitProgress(onProgress, {
|
|
896
|
-
state: "ERROR",
|
|
897
|
-
phase: "login_required",
|
|
898
|
-
source: "tui-driver",
|
|
899
|
-
error: errorMessage,
|
|
900
|
-
reason: errorReason,
|
|
901
|
-
matched_pattern: error?.matchedPattern,
|
|
902
|
-
});
|
|
903
|
-
this.writeLog(`[${this.backend}] Login required: ${errorMessage}`);
|
|
904
|
-
this.writeLog(`[${this.backend}] Please run "${this.command} login" or authenticate manually.`);
|
|
905
|
-
}
|
|
906
|
-
else if (errorReason === "permission_required") {
|
|
907
|
-
this.emitProgress(onProgress, {
|
|
908
|
-
state: "ERROR",
|
|
909
|
-
phase: "permission_required",
|
|
910
|
-
source: "tui-driver",
|
|
911
|
-
error: errorMessage,
|
|
912
|
-
reason: errorReason,
|
|
913
|
-
matched_pattern: error?.matchedPattern,
|
|
914
|
-
});
|
|
915
|
-
this.writeLog(`[${this.backend}] Permission required: ${errorMessage}`);
|
|
916
|
-
}
|
|
917
|
-
else {
|
|
918
|
-
this.emitProgress(onProgress, {
|
|
919
|
-
state: "ERROR",
|
|
920
|
-
phase: "exception",
|
|
921
|
-
source: "tui-driver",
|
|
922
|
-
error: errorMessage,
|
|
923
|
-
reason: errorReason,
|
|
924
|
-
});
|
|
925
|
-
this.writeLog(`[${this.backend}] Error: ${errorMessage}`);
|
|
926
|
-
}
|
|
927
|
-
let latestSignals = {};
|
|
928
|
-
try {
|
|
929
|
-
latestSignals = this.driver.getSignals();
|
|
930
|
-
}
|
|
931
|
-
catch {
|
|
932
|
-
// driver may already be disposed while closing
|
|
933
|
-
}
|
|
934
|
-
const summary = this.formatSignalSummary(latestSignals);
|
|
935
|
-
this.trace(`runTurn exception state=${this.driver.state} error="${sanitizeForLog(errorMessage, 220)}" status="${summary.status || ""}" done="${summary.done || ""}" preview="${summary.replyPreview || ""}"`);
|
|
936
|
-
throw error instanceof Error ? error : new Error(errorMessage);
|
|
937
|
-
}
|
|
938
|
-
finally {
|
|
939
|
-
if (shouldSwitchCwd) {
|
|
940
|
-
try {
|
|
941
|
-
process.chdir(previousCwd);
|
|
942
|
-
}
|
|
943
|
-
catch (error) {
|
|
944
|
-
this.writeLog(`Failed to restore cwd to ${previousCwd}: ${error?.message || error}`);
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
clearInterval(signalTimer);
|
|
948
|
-
this.driver.off("stateChange", handleStateChange);
|
|
949
|
-
turnTimeoutGuard.cleanup();
|
|
950
|
-
closeGuard.cleanup();
|
|
951
|
-
this.trace(`runTurn cleanup state=${this.driver.state}`);
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
}
|