u-foo 2.3.10 → 2.3.11
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 +3 -1
- package/src/agent/claudeThreadProvider.js +208 -39
- package/src/agent/codexThreadProvider.js +125 -64
- package/src/agent/internalRunner.js +181 -22
- package/src/agent/ptyRunner.js +8 -92
- package/src/chat/agentViewController.js +4 -1
- package/src/chat/dashboardKeyController.js +17 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "u-foo",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.11",
|
|
4
4
|
"description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://ufoo.dev",
|
|
@@ -45,6 +45,8 @@
|
|
|
45
45
|
"bench:global-switch": "node scripts/global-chat-switch-benchmark.js"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.138",
|
|
49
|
+
"@openai/codex-sdk": "^0.130.0",
|
|
48
50
|
"blessed": "^0.1.81",
|
|
49
51
|
"chalk": "^4.1.2",
|
|
50
52
|
"commander": "^13.1.0",
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
const {
|
|
4
4
|
createClaudeEventState,
|
|
5
5
|
normalizeClaudeEvent,
|
|
6
|
+
normalizeClaudeMessage,
|
|
7
|
+
normalizeClaudeUsage,
|
|
6
8
|
} = require("./claudeEventTranslator");
|
|
7
9
|
const { redactUfooEvent } = require("../providerapi/redactor");
|
|
8
|
-
const { sendUpstreamRequest } = require("./upstreamTransport");
|
|
9
10
|
|
|
10
11
|
const CACHE_CONTROL = Object.freeze({ type: "ephemeral" });
|
|
11
12
|
|
|
@@ -26,6 +27,25 @@ function resolveAnthropicSdk() {
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
async function resolveClaudeAgentSdk() {
|
|
31
|
+
try {
|
|
32
|
+
return await import("@anthropic-ai/claude-agent-sdk");
|
|
33
|
+
} catch (err) {
|
|
34
|
+
const error = new Error("Claude Agent SDK mode requires @anthropic-ai/claude-agent-sdk");
|
|
35
|
+
error.code = "CLAUDE_AGENT_SDK_UNAVAILABLE";
|
|
36
|
+
error.cause = err;
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveClaudeAgentQuery(sdk) {
|
|
42
|
+
const query = sdk && (sdk.query || (sdk.default && sdk.default.query));
|
|
43
|
+
if (typeof query !== "function") {
|
|
44
|
+
throw new Error("Claude Agent SDK module does not export query");
|
|
45
|
+
}
|
|
46
|
+
return query;
|
|
47
|
+
}
|
|
48
|
+
|
|
29
49
|
async function defaultClaudeAuthProvider() {
|
|
30
50
|
const apiKey = String(process.env.ANTHROPIC_API_KEY || "").trim();
|
|
31
51
|
if (!apiKey) {
|
|
@@ -61,43 +81,12 @@ function defaultClaudeStreamFactory({ client, request }) {
|
|
|
61
81
|
});
|
|
62
82
|
}
|
|
63
83
|
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
baseUrl: String(auth.baseUrl || process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com/v1").trim(),
|
|
70
|
-
auth,
|
|
71
|
-
credentialSource: auth.apiKey ? "thread-auth" : "thread-headers",
|
|
72
|
-
};
|
|
73
|
-
const result = await sendUpstreamRequest({
|
|
74
|
-
runtime,
|
|
75
|
-
request,
|
|
76
|
-
timeoutMs: Number.isFinite(Number(request.timeout_ms)) ? Number(request.timeout_ms) : 120000,
|
|
84
|
+
function defaultClaudeAgentStreamFactory({ sdk, input, options }) {
|
|
85
|
+
const query = resolveClaudeAgentQuery(sdk);
|
|
86
|
+
return query({
|
|
87
|
+
prompt: String(input || ""),
|
|
88
|
+
options,
|
|
77
89
|
});
|
|
78
|
-
if (!result.ok) {
|
|
79
|
-
const err = new Error(result.error || "Claude upstream request failed");
|
|
80
|
-
err.code = "CLAUDE_UPSTREAM_FAILED";
|
|
81
|
-
err.attempt = attempt;
|
|
82
|
-
throw err;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
yield {
|
|
86
|
-
type: "message_start",
|
|
87
|
-
message: {
|
|
88
|
-
id: `msg-${Date.now().toString(36)}`,
|
|
89
|
-
usage: result.usage || undefined,
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
yield {
|
|
93
|
-
type: "content_block_delta",
|
|
94
|
-
index: 0,
|
|
95
|
-
delta: {
|
|
96
|
-
type: "text_delta",
|
|
97
|
-
text: String(result.output || ""),
|
|
98
|
-
},
|
|
99
|
-
};
|
|
100
|
-
yield { type: "message_stop" };
|
|
101
90
|
}
|
|
102
91
|
|
|
103
92
|
function normalizeToolDefinition(tool = {}) {
|
|
@@ -217,6 +206,74 @@ function createAssistantMessageFromState(state) {
|
|
|
217
206
|
};
|
|
218
207
|
}
|
|
219
208
|
|
|
209
|
+
function extraArgsToObject(extraArgs = []) {
|
|
210
|
+
const result = {};
|
|
211
|
+
if (!Array.isArray(extraArgs)) return result;
|
|
212
|
+
for (let i = 0; i < extraArgs.length; i += 1) {
|
|
213
|
+
const raw = String(extraArgs[i] || "");
|
|
214
|
+
if (!raw.startsWith("--")) continue;
|
|
215
|
+
const key = raw.replace(/^--+/, "");
|
|
216
|
+
if (!key) continue;
|
|
217
|
+
const next = i + 1 < extraArgs.length ? String(extraArgs[i + 1] || "") : "";
|
|
218
|
+
if (!next || next.startsWith("--")) {
|
|
219
|
+
result[key] = null;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
result[key] = next;
|
|
223
|
+
i += 1;
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function buildClaudeAgentOptions({
|
|
229
|
+
model = "",
|
|
230
|
+
cwd = "",
|
|
231
|
+
threadId = "",
|
|
232
|
+
extraArgs = [],
|
|
233
|
+
agentOptions = {},
|
|
234
|
+
opts = {},
|
|
235
|
+
} = {}) {
|
|
236
|
+
const options = {
|
|
237
|
+
...agentOptions,
|
|
238
|
+
...(opts && typeof opts.agentOptions === "object" ? opts.agentOptions : {}),
|
|
239
|
+
};
|
|
240
|
+
if (model && options.model === undefined) options.model = model;
|
|
241
|
+
if (cwd && options.cwd === undefined) options.cwd = cwd;
|
|
242
|
+
if (options.includePartialMessages === undefined) options.includePartialMessages = true;
|
|
243
|
+
if (options.extraArgs === undefined) {
|
|
244
|
+
const converted = extraArgsToObject(extraArgs);
|
|
245
|
+
if (Object.keys(converted).length > 0) options.extraArgs = converted;
|
|
246
|
+
}
|
|
247
|
+
if (threadId && !options.resume && !options.continue && !options.sessionId) {
|
|
248
|
+
options.resume = threadId;
|
|
249
|
+
}
|
|
250
|
+
if (opts && opts.abortController && options.abortController === undefined) {
|
|
251
|
+
options.abortController = opts.abortController;
|
|
252
|
+
}
|
|
253
|
+
if (opts && opts.signal && options.abortController === undefined) {
|
|
254
|
+
const controller = new AbortController();
|
|
255
|
+
opts.signal.addEventListener("abort", () => controller.abort(opts.signal.reason), { once: true });
|
|
256
|
+
options.abortController = controller;
|
|
257
|
+
}
|
|
258
|
+
return options;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function messageTextFromContent(content = []) {
|
|
262
|
+
if (!Array.isArray(content)) return "";
|
|
263
|
+
return content
|
|
264
|
+
.map((block) => (block && block.type === "text" ? String(block.text || "") : ""))
|
|
265
|
+
.join("");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function resultErrorMessage(message = {}) {
|
|
269
|
+
if (Array.isArray(message.errors) && message.errors.length > 0) {
|
|
270
|
+
return message.errors.map((item) => String(item || "")).filter(Boolean).join("\n");
|
|
271
|
+
}
|
|
272
|
+
if (typeof message.result === "string" && message.result) return message.result;
|
|
273
|
+
if (message.subtype) return String(message.subtype);
|
|
274
|
+
return "Claude Agent SDK query failed";
|
|
275
|
+
}
|
|
276
|
+
|
|
220
277
|
function shouldRetryClaudeStream(err, attempt) {
|
|
221
278
|
if (attempt >= 1) return false;
|
|
222
279
|
const code = String((err && err.code) || "").trim().toUpperCase();
|
|
@@ -228,23 +285,132 @@ function shouldRetryClaudeStream(err, attempt) {
|
|
|
228
285
|
class ClaudeApiThread {
|
|
229
286
|
constructor({
|
|
230
287
|
model = "",
|
|
288
|
+
cwd = "",
|
|
289
|
+
extraArgs = [],
|
|
231
290
|
authProvider = defaultClaudeAuthProvider,
|
|
232
291
|
clientFactory = defaultClaudeClientFactory,
|
|
233
|
-
streamFactory =
|
|
292
|
+
streamFactory = defaultClaudeAgentStreamFactory,
|
|
234
293
|
sdk,
|
|
294
|
+
agentOptions = {},
|
|
235
295
|
maxTokens = 4096,
|
|
236
296
|
} = {}) {
|
|
237
297
|
this.id = "";
|
|
238
298
|
this.model = model;
|
|
299
|
+
this.cwd = cwd;
|
|
300
|
+
this.extraArgs = Array.isArray(extraArgs) ? extraArgs.slice() : [];
|
|
239
301
|
this.authProvider = authProvider;
|
|
240
302
|
this.clientFactory = clientFactory;
|
|
241
303
|
this.streamFactory = streamFactory;
|
|
242
304
|
this.sdk = sdk;
|
|
305
|
+
this.agentOptions = { ...agentOptions };
|
|
243
306
|
this.maxTokens = maxTokens;
|
|
244
307
|
this.messages = [];
|
|
245
308
|
}
|
|
246
309
|
|
|
247
310
|
async *runStreamed(input, opts = {}) {
|
|
311
|
+
if (this.streamFactory === defaultClaudeAgentStreamFactory) {
|
|
312
|
+
yield* this.runAgentSdkStreamed(input, opts);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
yield* this.runMessagesStreamed(input, opts);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async *runAgentSdkStreamed(input, opts = {}) {
|
|
319
|
+
if (!this.sdk) {
|
|
320
|
+
this.sdk = await resolveClaudeAgentSdk();
|
|
321
|
+
}
|
|
322
|
+
const options = buildClaudeAgentOptions({
|
|
323
|
+
model: this.model,
|
|
324
|
+
cwd: this.cwd,
|
|
325
|
+
threadId: this.id,
|
|
326
|
+
extraArgs: this.extraArgs,
|
|
327
|
+
agentOptions: this.agentOptions,
|
|
328
|
+
opts,
|
|
329
|
+
});
|
|
330
|
+
const stream = await this.streamFactory({
|
|
331
|
+
sdk: this.sdk,
|
|
332
|
+
input,
|
|
333
|
+
options,
|
|
334
|
+
model: this.model,
|
|
335
|
+
cwd: this.cwd,
|
|
336
|
+
threadId: this.id,
|
|
337
|
+
opts,
|
|
338
|
+
});
|
|
339
|
+
const state = createClaudeEventState({ threadId: this.id });
|
|
340
|
+
let threadStarted = false;
|
|
341
|
+
let sawStreamEvents = false;
|
|
342
|
+
let sawText = false;
|
|
343
|
+
let sawTurnCompleted = false;
|
|
344
|
+
|
|
345
|
+
const emitThreadStarted = function* emitThreadStarted(self, sessionId = "") {
|
|
346
|
+
const nextId = String(sessionId || self.id || "").trim();
|
|
347
|
+
if (nextId) self.id = nextId;
|
|
348
|
+
if (!threadStarted && self.id) {
|
|
349
|
+
threadStarted = true;
|
|
350
|
+
yield redactUfooEvent({ type: "thread_started", threadId: self.id });
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
for await (const message of stream) {
|
|
355
|
+
if (!message || typeof message !== "object") continue;
|
|
356
|
+
const sessionId = String(message.session_id || message.sessionId || "").trim();
|
|
357
|
+
for (const event of emitThreadStarted(this, sessionId)) yield event;
|
|
358
|
+
|
|
359
|
+
if (message.type === "stream_event") {
|
|
360
|
+
sawStreamEvents = true;
|
|
361
|
+
const events = normalizeClaudeEvent(message.event || {}, state);
|
|
362
|
+
for (const event of events) {
|
|
363
|
+
if (!event || typeof event !== "object") continue;
|
|
364
|
+
if (event.type === "text_delta" && event.delta) sawText = true;
|
|
365
|
+
if (event.type === "turn_completed") sawTurnCompleted = true;
|
|
366
|
+
yield redactUfooEvent(event);
|
|
367
|
+
}
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (message.type === "assistant") {
|
|
372
|
+
if (sawStreamEvents) continue;
|
|
373
|
+
const events = normalizeClaudeMessage(message.message || {});
|
|
374
|
+
for (const event of events) {
|
|
375
|
+
if (!event || typeof event !== "object") continue;
|
|
376
|
+
if (event.type === "text_delta" && event.delta) sawText = true;
|
|
377
|
+
if (event.type === "turn_completed") sawTurnCompleted = true;
|
|
378
|
+
yield redactUfooEvent(event);
|
|
379
|
+
}
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (message.type === "result") {
|
|
384
|
+
if (message.is_error) {
|
|
385
|
+
yield redactUfooEvent({
|
|
386
|
+
type: "turn_failed",
|
|
387
|
+
turnId: state.turnId || message.uuid || "",
|
|
388
|
+
error: resultErrorMessage(message),
|
|
389
|
+
});
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (!sawText && typeof message.result === "string" && message.result) {
|
|
393
|
+
sawText = true;
|
|
394
|
+
yield redactUfooEvent({
|
|
395
|
+
type: "text_delta",
|
|
396
|
+
delta: message.result,
|
|
397
|
+
itemType: "text",
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
if (!sawTurnCompleted) {
|
|
401
|
+
sawTurnCompleted = true;
|
|
402
|
+
yield redactUfooEvent({
|
|
403
|
+
type: "turn_completed",
|
|
404
|
+
turnId: state.turnId || message.uuid || "",
|
|
405
|
+
usage: normalizeClaudeUsage(message.usage || null),
|
|
406
|
+
stopReason: String(message.stop_reason || ""),
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async *runMessagesStreamed(input, opts = {}) {
|
|
248
414
|
if (!this.id) this.id = createThreadId();
|
|
249
415
|
const userMessage = normalizeMessageInput(input);
|
|
250
416
|
const requestMessages = this.messages.slice();
|
|
@@ -332,12 +498,15 @@ module.exports = {
|
|
|
332
498
|
ClaudeApiThread,
|
|
333
499
|
ClaudeThreadProvider,
|
|
334
500
|
createClaudeThreadProvider,
|
|
501
|
+
buildClaudeAgentOptions,
|
|
335
502
|
defaultClaudeAuthProvider,
|
|
503
|
+
defaultClaudeAgentStreamFactory,
|
|
336
504
|
defaultClaudeClientFactory,
|
|
337
505
|
defaultClaudeStreamFactory,
|
|
338
|
-
|
|
506
|
+
extraArgsToObject,
|
|
339
507
|
normalizeMessageInput,
|
|
340
508
|
normalizeToolDefinition,
|
|
509
|
+
resolveClaudeAgentSdk,
|
|
341
510
|
resolveAnthropicSdk,
|
|
342
511
|
withCacheControlOnLastBlock,
|
|
343
512
|
};
|
|
@@ -1,80 +1,79 @@
|
|
|
1
1
|
const { normalizeCodexEvent } = require("./codexEventTranslator");
|
|
2
2
|
const { redactUfooEvent } = require("../providerapi/redactor");
|
|
3
|
-
const { sendUpstreamPrompt } = require("./upstreamTransport");
|
|
4
3
|
|
|
5
|
-
function resolveCodexSdk() {
|
|
4
|
+
async function resolveCodexSdk() {
|
|
6
5
|
try {
|
|
7
|
-
|
|
8
|
-
// eslint-disable-next-line global-require, import/no-extraneous-dependencies
|
|
9
|
-
return require("@openai/codex-sdk");
|
|
6
|
+
return await import("@openai/codex-sdk");
|
|
10
7
|
} catch (err) {
|
|
11
|
-
const error = new Error("Codex SDK
|
|
8
|
+
const error = new Error("Codex SDK mode requires @openai/codex-sdk");
|
|
12
9
|
error.code = "CODEX_SDK_UNAVAILABLE";
|
|
13
10
|
error.cause = err;
|
|
14
11
|
throw error;
|
|
15
12
|
}
|
|
16
13
|
}
|
|
17
14
|
|
|
18
|
-
function
|
|
19
|
-
sdk
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
extraArgs = [],
|
|
23
|
-
threadId = "",
|
|
24
|
-
input,
|
|
25
|
-
opts = {},
|
|
26
|
-
}) {
|
|
27
|
-
if (!sdk || typeof sdk.runStreamed !== "function") {
|
|
28
|
-
throw new Error("Codex SDK seam requires runStreamed support");
|
|
15
|
+
function resolveCodexConstructor(sdk) {
|
|
16
|
+
const Codex = sdk && (sdk.Codex || (sdk.default && sdk.default.Codex) || sdk.default);
|
|
17
|
+
if (typeof Codex !== "function") {
|
|
18
|
+
throw new Error("Codex SDK module does not export Codex");
|
|
29
19
|
}
|
|
30
|
-
|
|
31
|
-
void history;
|
|
32
|
-
return sdk.runStreamed({
|
|
33
|
-
model,
|
|
34
|
-
cwd,
|
|
35
|
-
extraArgs,
|
|
36
|
-
threadId,
|
|
37
|
-
input,
|
|
38
|
-
...sdkOpts,
|
|
39
|
-
});
|
|
20
|
+
return Codex;
|
|
40
21
|
}
|
|
41
22
|
|
|
42
|
-
function
|
|
43
|
-
return
|
|
23
|
+
function buildCodexOptions({ codexOptions = {} } = {}) {
|
|
24
|
+
return { ...codexOptions };
|
|
44
25
|
}
|
|
45
26
|
|
|
46
|
-
|
|
47
|
-
model,
|
|
48
|
-
cwd,
|
|
49
|
-
|
|
27
|
+
function buildThreadOptions({
|
|
28
|
+
model = "",
|
|
29
|
+
cwd = "",
|
|
30
|
+
threadOptions = {},
|
|
31
|
+
} = {}) {
|
|
32
|
+
const options = { ...threadOptions };
|
|
33
|
+
if (model && options.model === undefined) {
|
|
34
|
+
options.model = model;
|
|
35
|
+
}
|
|
36
|
+
if (cwd && options.workingDirectory === undefined) {
|
|
37
|
+
options.workingDirectory = cwd;
|
|
38
|
+
}
|
|
39
|
+
if (options.skipGitRepoCheck === undefined) {
|
|
40
|
+
options.skipGitRepoCheck = true;
|
|
41
|
+
}
|
|
42
|
+
if (options.sandboxMode === undefined) {
|
|
43
|
+
options.sandboxMode = "workspace-write";
|
|
44
|
+
}
|
|
45
|
+
return options;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function buildTurnOptions(opts = {}) {
|
|
49
|
+
const {
|
|
50
|
+
history,
|
|
51
|
+
tools,
|
|
52
|
+
timeoutMs,
|
|
53
|
+
...turnOptions
|
|
54
|
+
} = opts || {};
|
|
55
|
+
void history;
|
|
56
|
+
void tools;
|
|
57
|
+
void timeoutMs;
|
|
58
|
+
return turnOptions;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function* defaultCodexStreamFactory({
|
|
62
|
+
thread,
|
|
50
63
|
input,
|
|
51
64
|
opts = {},
|
|
52
65
|
}) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
err.code = result.errorCode || "CODEX_UPSTREAM_FAILED";
|
|
65
|
-
throw err;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
yield { type: "thread.started", thread_id: nextThreadId };
|
|
69
|
-
yield {
|
|
70
|
-
type: "item.completed",
|
|
71
|
-
item: { type: "message", text: String(result.output || "") },
|
|
72
|
-
};
|
|
73
|
-
yield {
|
|
74
|
-
type: "turn.completed",
|
|
75
|
-
turn_id: `turn-${Date.now().toString(36)}`,
|
|
76
|
-
usage: result.usage || null,
|
|
77
|
-
};
|
|
66
|
+
if (!thread || typeof thread.runStreamed !== "function") {
|
|
67
|
+
throw new Error("Codex SDK thread requires runStreamed support");
|
|
68
|
+
}
|
|
69
|
+
const streamed = await thread.runStreamed(String(input || ""), buildTurnOptions(opts));
|
|
70
|
+
const events = streamed && streamed.events ? streamed.events : streamed;
|
|
71
|
+
if (!events || typeof events[Symbol.asyncIterator] !== "function") {
|
|
72
|
+
throw new Error("Codex SDK runStreamed did not return an async event stream");
|
|
73
|
+
}
|
|
74
|
+
for await (const event of events) {
|
|
75
|
+
yield event;
|
|
76
|
+
}
|
|
78
77
|
}
|
|
79
78
|
|
|
80
79
|
class CodexSdkThread {
|
|
@@ -84,26 +83,67 @@ class CodexSdkThread {
|
|
|
84
83
|
extraArgs = [],
|
|
85
84
|
streamFactory = defaultCodexStreamFactory,
|
|
86
85
|
sdk,
|
|
86
|
+
codexClient,
|
|
87
|
+
sdkThread,
|
|
88
|
+
threadId = "",
|
|
87
89
|
tools = [],
|
|
90
|
+
codexOptions = {},
|
|
91
|
+
threadOptions = {},
|
|
88
92
|
} = {}) {
|
|
89
|
-
this.id = "";
|
|
93
|
+
this.id = String(threadId || "").trim();
|
|
90
94
|
this.model = model;
|
|
91
95
|
this.cwd = cwd;
|
|
92
96
|
this.extraArgs = Array.isArray(extraArgs) ? extraArgs.slice() : [];
|
|
93
97
|
this.streamFactory = streamFactory;
|
|
94
98
|
this.sdk = sdk;
|
|
99
|
+
this.codexClient = codexClient || null;
|
|
100
|
+
this.sdkThread = sdkThread || null;
|
|
95
101
|
this.tools = Array.isArray(tools) ? tools.slice() : [];
|
|
102
|
+
this.codexOptions = buildCodexOptions({ codexOptions });
|
|
103
|
+
this.threadOptions = { ...threadOptions };
|
|
96
104
|
this.messages = [];
|
|
97
105
|
}
|
|
98
106
|
|
|
107
|
+
async getCodexClient() {
|
|
108
|
+
if (this.codexClient) return this.codexClient;
|
|
109
|
+
if (!this.sdk) {
|
|
110
|
+
this.sdk = await resolveCodexSdk();
|
|
111
|
+
}
|
|
112
|
+
const Codex = resolveCodexConstructor(this.sdk);
|
|
113
|
+
this.codexClient = new Codex(this.codexOptions);
|
|
114
|
+
return this.codexClient;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async getSdkThread() {
|
|
118
|
+
if (this.sdkThread) return this.sdkThread;
|
|
119
|
+
const client = await this.getCodexClient();
|
|
120
|
+
const options = buildThreadOptions({
|
|
121
|
+
model: this.model,
|
|
122
|
+
cwd: this.cwd,
|
|
123
|
+
threadOptions: this.threadOptions,
|
|
124
|
+
});
|
|
125
|
+
this.sdkThread = this.id && typeof client.resumeThread === "function"
|
|
126
|
+
? client.resumeThread(this.id, options)
|
|
127
|
+
: client.startThread(options);
|
|
128
|
+
if (this.sdkThread && this.sdkThread.id) {
|
|
129
|
+
this.id = this.sdkThread.id;
|
|
130
|
+
}
|
|
131
|
+
return this.sdkThread;
|
|
132
|
+
}
|
|
133
|
+
|
|
99
134
|
async *runStreamed(input, opts = {}) {
|
|
100
135
|
const mergedOpts = { ...opts };
|
|
101
136
|
if (this.tools.length > 0 && !Array.isArray(mergedOpts.tools)) {
|
|
102
137
|
mergedOpts.tools = this.tools.slice();
|
|
103
138
|
}
|
|
104
139
|
mergedOpts.history = this.messages.slice();
|
|
140
|
+
const sdkThread = this.streamFactory === defaultCodexStreamFactory
|
|
141
|
+
? await this.getSdkThread()
|
|
142
|
+
: this.sdkThread;
|
|
105
143
|
const stream = this.streamFactory({
|
|
106
144
|
sdk: this.sdk,
|
|
145
|
+
thread: sdkThread,
|
|
146
|
+
codexClient: this.codexClient,
|
|
107
147
|
model: this.model,
|
|
108
148
|
cwd: this.cwd,
|
|
109
149
|
extraArgs: this.extraArgs,
|
|
@@ -123,6 +163,9 @@ class CodexSdkThread {
|
|
|
123
163
|
}
|
|
124
164
|
yield redactUfooEvent(normalized);
|
|
125
165
|
}
|
|
166
|
+
if (sdkThread && sdkThread.id) {
|
|
167
|
+
this.id = sdkThread.id;
|
|
168
|
+
}
|
|
126
169
|
this.messages.push({ role: "user", content: String(input || "") });
|
|
127
170
|
this.messages.push({ role: "assistant", content: outputText });
|
|
128
171
|
}
|
|
@@ -139,14 +182,20 @@ class CodexThreadProvider {
|
|
|
139
182
|
extraArgs = [],
|
|
140
183
|
streamFactory = defaultCodexStreamFactory,
|
|
141
184
|
sdk,
|
|
185
|
+
codexClient,
|
|
142
186
|
tools = [],
|
|
187
|
+
codexOptions = {},
|
|
188
|
+
threadOptions = {},
|
|
143
189
|
} = {}) {
|
|
144
190
|
this.model = model;
|
|
145
191
|
this.cwd = cwd;
|
|
146
192
|
this.extraArgs = Array.isArray(extraArgs) ? extraArgs.slice() : [];
|
|
147
193
|
this.streamFactory = streamFactory;
|
|
148
|
-
this.sdk = sdk ||
|
|
194
|
+
this.sdk = sdk || null;
|
|
195
|
+
this.codexClient = codexClient || null;
|
|
149
196
|
this.tools = Array.isArray(tools) ? tools.slice() : [];
|
|
197
|
+
this.codexOptions = buildCodexOptions({ codexOptions });
|
|
198
|
+
this.threadOptions = { ...threadOptions };
|
|
150
199
|
}
|
|
151
200
|
|
|
152
201
|
startThread() {
|
|
@@ -156,14 +205,26 @@ class CodexThreadProvider {
|
|
|
156
205
|
extraArgs: this.extraArgs,
|
|
157
206
|
streamFactory: this.streamFactory,
|
|
158
207
|
sdk: this.sdk,
|
|
208
|
+
codexClient: this.codexClient,
|
|
159
209
|
tools: this.tools,
|
|
210
|
+
codexOptions: this.codexOptions,
|
|
211
|
+
threadOptions: this.threadOptions,
|
|
160
212
|
});
|
|
161
213
|
}
|
|
162
214
|
|
|
163
215
|
resumeThread(threadId = "") {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
216
|
+
return new CodexSdkThread({
|
|
217
|
+
model: this.model,
|
|
218
|
+
cwd: this.cwd,
|
|
219
|
+
extraArgs: this.extraArgs,
|
|
220
|
+
streamFactory: this.streamFactory,
|
|
221
|
+
sdk: this.sdk,
|
|
222
|
+
codexClient: this.codexClient,
|
|
223
|
+
threadId,
|
|
224
|
+
tools: this.tools,
|
|
225
|
+
codexOptions: this.codexOptions,
|
|
226
|
+
threadOptions: this.threadOptions,
|
|
227
|
+
});
|
|
167
228
|
}
|
|
168
229
|
}
|
|
169
230
|
|
|
@@ -175,7 +236,7 @@ module.exports = {
|
|
|
175
236
|
CodexSdkThread,
|
|
176
237
|
CodexThreadProvider,
|
|
177
238
|
createCodexThreadProvider,
|
|
239
|
+
buildThreadOptions,
|
|
178
240
|
defaultCodexStreamFactory,
|
|
179
|
-
defaultCodexTransportStreamFactory,
|
|
180
241
|
resolveCodexSdk,
|
|
181
242
|
};
|
|
@@ -7,14 +7,8 @@ const { runCliAgent } = require("./cliRunner");
|
|
|
7
7
|
const { normalizeCliOutput } = require("./normalizeOutput");
|
|
8
8
|
const { createActivityStatePublisher } = require("./activityStatePublisher");
|
|
9
9
|
const { loadConfig, normalizeCodexInternalThreadMode } = require("../config");
|
|
10
|
-
const {
|
|
11
|
-
|
|
12
|
-
defaultCodexTransportStreamFactory,
|
|
13
|
-
} = require("./codexThreadProvider");
|
|
14
|
-
const {
|
|
15
|
-
createClaudeThreadProvider,
|
|
16
|
-
defaultClaudeTransportStreamFactory,
|
|
17
|
-
} = require("./claudeThreadProvider");
|
|
10
|
+
const { createCodexThreadProvider } = require("./codexThreadProvider");
|
|
11
|
+
const { createClaudeThreadProvider } = require("./claudeThreadProvider");
|
|
18
12
|
const { resolveClaudeUpstreamCredentials } = require("./credentials/claude");
|
|
19
13
|
const { buildUpstreamAuthFromCredential } = require("./credentials");
|
|
20
14
|
const { listToolsForCallerTier, CALLER_TIERS } = require("../tools");
|
|
@@ -131,6 +125,73 @@ function drainQueue(queueFile) {
|
|
|
131
125
|
return content.split(/\r?\n/).filter(Boolean);
|
|
132
126
|
}
|
|
133
127
|
|
|
128
|
+
function parseAgentViewRawInput(message) {
|
|
129
|
+
if (typeof message !== "string" || !message.trim()) return null;
|
|
130
|
+
try {
|
|
131
|
+
const parsed = JSON.parse(message);
|
|
132
|
+
if (parsed && parsed.raw === true && typeof parsed.data === "string") {
|
|
133
|
+
return parsed.data;
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
// Not a raw agent-view envelope.
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function createInteractiveInputSession({ write = () => {} } = {}) {
|
|
142
|
+
let buffer = "";
|
|
143
|
+
|
|
144
|
+
function writePrompt() {
|
|
145
|
+
write("> ");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function handleRaw(data) {
|
|
149
|
+
const submissions = [];
|
|
150
|
+
const text = String(data || "");
|
|
151
|
+
for (const char of text) {
|
|
152
|
+
if (char === "\r" || char === "\n") {
|
|
153
|
+
const submitted = buffer.trim();
|
|
154
|
+
buffer = "";
|
|
155
|
+
write("\r\n");
|
|
156
|
+
if (submitted) submissions.push(submitted);
|
|
157
|
+
else writePrompt();
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (char === "\u0003") {
|
|
162
|
+
buffer = "";
|
|
163
|
+
write("^C\r\n");
|
|
164
|
+
writePrompt();
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (char === "\u007f" || char === "\b") {
|
|
169
|
+
if (buffer.length > 0) {
|
|
170
|
+
buffer = buffer.slice(0, -1);
|
|
171
|
+
write("\b \b");
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (char >= " " && char !== "\u007f") {
|
|
177
|
+
buffer += char;
|
|
178
|
+
write(char);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return submissions;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
handleRaw,
|
|
186
|
+
writePrompt,
|
|
187
|
+
writeResponsePrompt: () => {
|
|
188
|
+
write("\r\n");
|
|
189
|
+
writePrompt();
|
|
190
|
+
},
|
|
191
|
+
getBuffer: () => buffer,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
134
195
|
async function handleEvent(
|
|
135
196
|
projectRoot,
|
|
136
197
|
agentType,
|
|
@@ -259,6 +320,9 @@ async function handleThreadedEvent({
|
|
|
259
320
|
const plainReplyParts = [];
|
|
260
321
|
for await (const event of threadRuntime.thread.runStreamed(prompt, {})) {
|
|
261
322
|
if (!event || typeof event !== "object") continue;
|
|
323
|
+
if (typeof threadRuntime.syncProviderSessionId === "function") {
|
|
324
|
+
threadRuntime.syncProviderSessionId();
|
|
325
|
+
}
|
|
262
326
|
if (event.type === "text_delta" && event.delta) {
|
|
263
327
|
if (streamToPublisher) {
|
|
264
328
|
emitStreamDelta(event.delta);
|
|
@@ -269,6 +333,9 @@ async function handleThreadedEvent({
|
|
|
269
333
|
throw new Error(event.error || `thread turn failed for ${agentType}`);
|
|
270
334
|
}
|
|
271
335
|
}
|
|
336
|
+
if (typeof threadRuntime.syncProviderSessionId === "function") {
|
|
337
|
+
threadRuntime.syncProviderSessionId();
|
|
338
|
+
}
|
|
272
339
|
|
|
273
340
|
if (streamToPublisher) {
|
|
274
341
|
busSender.enqueue(
|
|
@@ -420,15 +487,47 @@ function buildClaudeAuthProvider(projectRoot) {
|
|
|
420
487
|
};
|
|
421
488
|
}
|
|
422
489
|
|
|
423
|
-
function
|
|
490
|
+
function persistProviderSessionId(projectRoot, subscriber, providerSessionId) {
|
|
491
|
+
const id = String(providerSessionId || "").trim();
|
|
492
|
+
if (!projectRoot || !subscriber || !id) return false;
|
|
493
|
+
try {
|
|
494
|
+
const agentsFile = getUfooPaths(projectRoot).agentsFile;
|
|
495
|
+
const parsed = fs.existsSync(agentsFile)
|
|
496
|
+
? JSON.parse(fs.readFileSync(agentsFile, "utf8"))
|
|
497
|
+
: {};
|
|
498
|
+
if (!parsed.agents || typeof parsed.agents !== "object") return false;
|
|
499
|
+
if (!parsed.agents[subscriber] || typeof parsed.agents[subscriber] !== "object") return false;
|
|
500
|
+
if (parsed.agents[subscriber].provider_session_id === id) return false;
|
|
501
|
+
parsed.agents[subscriber].provider_session_id = id;
|
|
502
|
+
parsed.agents[subscriber].provider_session_updated_at = new Date().toISOString();
|
|
503
|
+
fs.writeFileSync(agentsFile, `${JSON.stringify(parsed, null, 2)}\n`);
|
|
504
|
+
return true;
|
|
505
|
+
} catch {
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function createThreadRuntime({ projectRoot, provider, model, extraArgs = [], subscriber = "", providerSessionId = "" }) {
|
|
424
511
|
const disabledRuntime = {
|
|
425
512
|
enabled: false,
|
|
426
513
|
thread: null,
|
|
427
514
|
toolRuntime: { enabled: false, mode: "disabled", tools: [] },
|
|
428
515
|
close: async () => {},
|
|
429
516
|
rebuildThread: async () => {},
|
|
517
|
+
syncProviderSessionId: () => false,
|
|
430
518
|
};
|
|
431
519
|
|
|
520
|
+
const initialProviderSessionId = String(providerSessionId || "").trim();
|
|
521
|
+
let savedProviderSessionId = initialProviderSessionId;
|
|
522
|
+
|
|
523
|
+
function rememberProviderSessionId(thread) {
|
|
524
|
+
const id = String(thread && thread.id ? thread.id : "").trim();
|
|
525
|
+
if (!id || id === savedProviderSessionId) return false;
|
|
526
|
+
const changed = persistProviderSessionId(projectRoot, subscriber, id);
|
|
527
|
+
if (changed) savedProviderSessionId = id;
|
|
528
|
+
return changed;
|
|
529
|
+
}
|
|
530
|
+
|
|
432
531
|
if (provider === "codex-cli") {
|
|
433
532
|
if (getCodexThreadMode(projectRoot) !== "api") {
|
|
434
533
|
return disabledRuntime;
|
|
@@ -444,9 +543,10 @@ function createThreadRuntime({ projectRoot, provider, model, extraArgs = [], sub
|
|
|
444
543
|
cwd: projectRoot,
|
|
445
544
|
extraArgs,
|
|
446
545
|
tools: toolRuntime.tools,
|
|
447
|
-
streamFactory: defaultCodexTransportStreamFactory,
|
|
448
546
|
});
|
|
449
|
-
let thread =
|
|
547
|
+
let thread = initialProviderSessionId
|
|
548
|
+
? providerInstance.resumeThread(initialProviderSessionId)
|
|
549
|
+
: providerInstance.startThread();
|
|
450
550
|
|
|
451
551
|
return {
|
|
452
552
|
enabled: true,
|
|
@@ -454,6 +554,9 @@ function createThreadRuntime({ projectRoot, provider, model, extraArgs = [], sub
|
|
|
454
554
|
get thread() {
|
|
455
555
|
return thread;
|
|
456
556
|
},
|
|
557
|
+
syncProviderSessionId() {
|
|
558
|
+
return rememberProviderSessionId(thread);
|
|
559
|
+
},
|
|
457
560
|
async rebuildThread() {
|
|
458
561
|
if (thread && typeof thread.close === "function") {
|
|
459
562
|
await thread.close();
|
|
@@ -463,9 +566,10 @@ function createThreadRuntime({ projectRoot, provider, model, extraArgs = [], sub
|
|
|
463
566
|
cwd: projectRoot,
|
|
464
567
|
extraArgs,
|
|
465
568
|
tools: toolRuntime.tools,
|
|
466
|
-
streamFactory: defaultCodexTransportStreamFactory,
|
|
467
569
|
});
|
|
468
|
-
thread =
|
|
570
|
+
thread = savedProviderSessionId
|
|
571
|
+
? providerInstance.resumeThread(savedProviderSessionId)
|
|
572
|
+
: providerInstance.startThread();
|
|
469
573
|
},
|
|
470
574
|
async close() {
|
|
471
575
|
if (thread && typeof thread.close === "function") {
|
|
@@ -482,33 +586,40 @@ function createThreadRuntime({ projectRoot, provider, model, extraArgs = [], sub
|
|
|
482
586
|
if (getClaudeThreadMode() !== "api") {
|
|
483
587
|
return disabledRuntime;
|
|
484
588
|
}
|
|
485
|
-
if (typeof createClaudeThreadProvider !== "function"
|
|
589
|
+
if (typeof createClaudeThreadProvider !== "function") {
|
|
486
590
|
return disabledRuntime;
|
|
487
591
|
}
|
|
488
592
|
|
|
489
593
|
try {
|
|
490
594
|
let providerInstance = createClaudeThreadProvider({
|
|
491
595
|
model,
|
|
492
|
-
|
|
493
|
-
|
|
596
|
+
cwd: projectRoot,
|
|
597
|
+
extraArgs,
|
|
494
598
|
});
|
|
495
|
-
let thread =
|
|
599
|
+
let thread = initialProviderSessionId
|
|
600
|
+
? providerInstance.resumeThread(initialProviderSessionId)
|
|
601
|
+
: providerInstance.startThread();
|
|
496
602
|
|
|
497
603
|
return {
|
|
498
604
|
enabled: true,
|
|
499
605
|
get thread() {
|
|
500
606
|
return thread;
|
|
501
607
|
},
|
|
608
|
+
syncProviderSessionId() {
|
|
609
|
+
return rememberProviderSessionId(thread);
|
|
610
|
+
},
|
|
502
611
|
async rebuildThread() {
|
|
503
612
|
if (thread && typeof thread.close === "function") {
|
|
504
613
|
await thread.close();
|
|
505
614
|
}
|
|
506
615
|
providerInstance = createClaudeThreadProvider({
|
|
507
616
|
model,
|
|
508
|
-
|
|
509
|
-
|
|
617
|
+
cwd: projectRoot,
|
|
618
|
+
extraArgs,
|
|
510
619
|
});
|
|
511
|
-
thread =
|
|
620
|
+
thread = savedProviderSessionId
|
|
621
|
+
? providerInstance.resumeThread(savedProviderSessionId)
|
|
622
|
+
: providerInstance.startThread();
|
|
512
623
|
},
|
|
513
624
|
async close() {
|
|
514
625
|
if (thread && typeof thread.close === "function") {
|
|
@@ -538,12 +649,14 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
|
|
|
538
649
|
const provider = normalizedAgentType === "codex" ? "codex-cli" : "claude-cli";
|
|
539
650
|
const model = process.env.UFOO_AGENT_MODEL || "";
|
|
540
651
|
const busSender = createBusSender(projectRoot, subscriber);
|
|
652
|
+
const interactiveSessions = new Map();
|
|
541
653
|
const threadRuntime = createThreadRuntime({
|
|
542
654
|
projectRoot,
|
|
543
655
|
provider,
|
|
544
656
|
model,
|
|
545
657
|
extraArgs,
|
|
546
658
|
subscriber,
|
|
659
|
+
providerSessionId: process.env.UFOO_PROVIDER_SESSION_ID || "",
|
|
547
660
|
});
|
|
548
661
|
|
|
549
662
|
// Session state management for CLI continuity
|
|
@@ -586,6 +699,18 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
|
|
|
586
699
|
activityPublisher.publish(state);
|
|
587
700
|
}
|
|
588
701
|
|
|
702
|
+
function getInteractiveSession(publisher) {
|
|
703
|
+
const key = String(publisher || "unknown");
|
|
704
|
+
if (interactiveSessions.has(key)) return interactiveSessions.get(key);
|
|
705
|
+
const session = createInteractiveInputSession({
|
|
706
|
+
write: (delta) => {
|
|
707
|
+
busSender.enqueue(key, JSON.stringify({ stream: true, delta: String(delta || "") }));
|
|
708
|
+
},
|
|
709
|
+
});
|
|
710
|
+
interactiveSessions.set(key, session);
|
|
711
|
+
return session;
|
|
712
|
+
}
|
|
713
|
+
|
|
589
714
|
setActivityState("ready");
|
|
590
715
|
|
|
591
716
|
// 心跳更新函数
|
|
@@ -615,7 +740,6 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
|
|
|
615
740
|
try {
|
|
616
741
|
const lines = drainQueue(queueFile);
|
|
617
742
|
if (lines.length > 0) {
|
|
618
|
-
setActivityState("working");
|
|
619
743
|
const events = [];
|
|
620
744
|
for (const line of lines) {
|
|
621
745
|
try {
|
|
@@ -625,7 +749,33 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
|
|
|
625
749
|
}
|
|
626
750
|
}
|
|
627
751
|
|
|
752
|
+
const runnableEvents = [];
|
|
628
753
|
for (const evt of events) {
|
|
754
|
+
const rawInput = parseAgentViewRawInput(evt && evt.data ? evt.data.message : "");
|
|
755
|
+
if (rawInput === null) {
|
|
756
|
+
runnableEvents.push(evt);
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const session = getInteractiveSession(evt.publisher || "unknown");
|
|
761
|
+
const submissions = session.handleRaw(rawInput);
|
|
762
|
+
for (const message of submissions) {
|
|
763
|
+
runnableEvents.push({
|
|
764
|
+
...evt,
|
|
765
|
+
__agentViewRaw: true,
|
|
766
|
+
data: {
|
|
767
|
+
...(evt.data || {}),
|
|
768
|
+
message,
|
|
769
|
+
},
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (runnableEvents.length > 0) {
|
|
775
|
+
setActivityState("working");
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
for (const evt of runnableEvents) {
|
|
629
779
|
// eslint-disable-next-line no-await-in-loop
|
|
630
780
|
await handleEvent(
|
|
631
781
|
projectRoot,
|
|
@@ -640,6 +790,9 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
|
|
|
640
790
|
extraArgs,
|
|
641
791
|
threadRuntime
|
|
642
792
|
);
|
|
793
|
+
if (evt.__agentViewRaw) {
|
|
794
|
+
getInteractiveSession(evt.publisher || "unknown").writeResponsePrompt();
|
|
795
|
+
}
|
|
643
796
|
}
|
|
644
797
|
|
|
645
798
|
// Persist CLI session state after processing (only if changed and for claude)
|
|
@@ -659,7 +812,10 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
|
|
|
659
812
|
// 处理消息后更新心跳
|
|
660
813
|
updateHeartbeat();
|
|
661
814
|
lastHeartbeat = now;
|
|
662
|
-
|
|
815
|
+
if (runnableEvents.length > 0) {
|
|
816
|
+
setActivityState("idle");
|
|
817
|
+
}
|
|
818
|
+
await busSender.flush();
|
|
663
819
|
}
|
|
664
820
|
} finally {
|
|
665
821
|
processing = false;
|
|
@@ -684,4 +840,7 @@ module.exports = {
|
|
|
684
840
|
getClaudeThreadMode,
|
|
685
841
|
buildClaudeAuthProvider,
|
|
686
842
|
shouldFallbackToLegacyThreadProvider,
|
|
843
|
+
parseAgentViewRawInput,
|
|
844
|
+
createInteractiveInputSession,
|
|
845
|
+
persistProviderSessionId,
|
|
687
846
|
};
|
package/src/agent/ptyRunner.js
CHANGED
|
@@ -116,11 +116,6 @@ function computeInjectedSubmitDelayMs(agentType, text) {
|
|
|
116
116
|
return delayMs;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
function buildPrompt(text, marker) {
|
|
120
|
-
if (!marker) return text;
|
|
121
|
-
return `${text}\n\n请在完成后输出以下标记(单独一行):\n${marker}\n`;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
119
|
function resolveCommand(agentType, extraArgs = []) {
|
|
125
120
|
const normalizedAgent = String(agentType || "").trim().toLowerCase();
|
|
126
121
|
const extra = Array.isArray(extraArgs) ? extraArgs : [];
|
|
@@ -172,9 +167,11 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
172
167
|
UFOO_INTERNAL_PTY: "1",
|
|
173
168
|
};
|
|
174
169
|
|
|
170
|
+
const idleMs = Number.parseInt(process.env.UFOO_INTERNAL_PTY_IDLE_MS || "", 10) || 30000;
|
|
175
171
|
const eventBus = new EventBus(projectRoot);
|
|
176
172
|
const activityDetector = new ActivityDetector(agentType, {
|
|
177
173
|
mode: "internal-pty",
|
|
174
|
+
quietWindowMs: idleMs,
|
|
178
175
|
});
|
|
179
176
|
const agentsFilePath = getUfooPaths(projectRoot).agentsFile;
|
|
180
177
|
|
|
@@ -184,15 +181,11 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
184
181
|
let ptyReady = false;
|
|
185
182
|
let readyTimer = null;
|
|
186
183
|
let currentPublisher = "";
|
|
187
|
-
let currentMarker = "";
|
|
188
184
|
let pendingOutput = [];
|
|
189
185
|
let outputBuffer = "";
|
|
190
186
|
let flushTimer = null;
|
|
191
187
|
let idleTimer = null;
|
|
192
188
|
let watchdogTimer = null;
|
|
193
|
-
let suppressEcho = false;
|
|
194
|
-
let echoMarker = "";
|
|
195
|
-
let suppressTimer = null;
|
|
196
189
|
let ptyProcess = null;
|
|
197
190
|
let restartCount = 0;
|
|
198
191
|
let lastSpawnTime = 0;
|
|
@@ -205,14 +198,11 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
205
198
|
const injectServer = setupInjectServer();
|
|
206
199
|
initScreenBuffer(80, 24);
|
|
207
200
|
const maxChunk = 2000;
|
|
208
|
-
const idleMs = 30000;
|
|
209
201
|
const watchdogMs = 120000;
|
|
210
202
|
const maxQueue = 200;
|
|
211
203
|
let sendQueue = Promise.resolve();
|
|
212
204
|
const streamPublisherCache = new Map();
|
|
213
205
|
const DROP_LINE_PATTERNS = [
|
|
214
|
-
/__UFOO_DONE_/,
|
|
215
|
-
/请在完成后输出以下标记/,
|
|
216
206
|
/context left/i,
|
|
217
207
|
/esc to interrupt/i,
|
|
218
208
|
/for shortcuts/i,
|
|
@@ -660,8 +650,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
660
650
|
detail: snap.detail,
|
|
661
651
|
});
|
|
662
652
|
// Quiet-window detector may classify IDLE sooner than stream fallback timer.
|
|
663
|
-
|
|
664
|
-
if (newState === "idle" && busy && !currentMarker && !suppressEcho) {
|
|
653
|
+
if (newState === "idle" && busy) {
|
|
665
654
|
if (idleTimer) {
|
|
666
655
|
clearTimeout(idleTimer);
|
|
667
656
|
idleTimer = null;
|
|
@@ -696,49 +685,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
696
685
|
if (!clean) return;
|
|
697
686
|
outputBuffer += clean;
|
|
698
687
|
activityDetector.processOutput(clean);
|
|
699
|
-
if (suppressEcho) {
|
|
700
|
-
if (echoMarker && outputBuffer.includes(echoMarker)) {
|
|
701
|
-
const idx = outputBuffer.indexOf(echoMarker);
|
|
702
|
-
outputBuffer = outputBuffer.slice(idx + echoMarker.length);
|
|
703
|
-
outputBuffer = outputBuffer.replace(/^\n+/, "");
|
|
704
|
-
suppressEcho = false;
|
|
705
|
-
currentMarker = echoMarker;
|
|
706
|
-
echoMarker = "";
|
|
707
|
-
if (suppressTimer) {
|
|
708
|
-
clearTimeout(suppressTimer);
|
|
709
|
-
suppressTimer = null;
|
|
710
|
-
}
|
|
711
|
-
} else {
|
|
712
|
-
return;
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
if (currentMarker) {
|
|
716
|
-
const idx = outputBuffer.indexOf(currentMarker);
|
|
717
|
-
if (idx !== -1) {
|
|
718
|
-
const before = outputBuffer.slice(0, idx);
|
|
719
|
-
outputBuffer = "";
|
|
720
|
-
if (before) {
|
|
721
|
-
deliverChunk(before);
|
|
722
|
-
}
|
|
723
|
-
if (currentPublisher) {
|
|
724
|
-
completePublisherResponse("marker");
|
|
725
|
-
}
|
|
726
|
-
currentMarker = "";
|
|
727
|
-
busy = false;
|
|
728
|
-
activityDetector.markIdle();
|
|
729
|
-
currentPublisher = "";
|
|
730
|
-
if (watchdogTimer) {
|
|
731
|
-
clearTimeout(watchdogTimer);
|
|
732
|
-
watchdogTimer = null;
|
|
733
|
-
}
|
|
734
|
-
if (idleTimer) {
|
|
735
|
-
clearTimeout(idleTimer);
|
|
736
|
-
idleTimer = null;
|
|
737
|
-
}
|
|
738
|
-
processQueue();
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
688
|
scheduleFlush();
|
|
743
689
|
// Ready detection: during TUI startup, reset the quiet timer on each output.
|
|
744
690
|
// Once output stops for READY_QUIET_MS, the TUI is considered initialized.
|
|
@@ -805,7 +751,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
805
751
|
busy = false;
|
|
806
752
|
activityDetector.markIdle();
|
|
807
753
|
currentPublisher = "";
|
|
808
|
-
currentMarker = "";
|
|
809
754
|
|
|
810
755
|
// If stop() was called, let the runner exit
|
|
811
756
|
if (!running) return;
|
|
@@ -940,11 +885,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
940
885
|
busy = true;
|
|
941
886
|
activityDetector.markWorking();
|
|
942
887
|
currentPublisher = next.publisher;
|
|
943
|
-
currentMarker = next.marker || "";
|
|
944
|
-
if (suppressTimer) {
|
|
945
|
-
clearTimeout(suppressTimer);
|
|
946
|
-
suppressTimer = null;
|
|
947
|
-
}
|
|
948
888
|
flushPending();
|
|
949
889
|
if (next.text) {
|
|
950
890
|
if (next.raw) {
|
|
@@ -952,30 +892,16 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
952
892
|
} else {
|
|
953
893
|
// Write text first, then send Enter separately.
|
|
954
894
|
// Codex Ink TUI requires text and submit key as separate writes.
|
|
955
|
-
|
|
956
|
-
// because the prompt echo (TextInput display) contains the marker text.
|
|
957
|
-
const prompt = buildPrompt(next.text, currentMarker);
|
|
958
|
-
const savedMarker = currentMarker;
|
|
959
|
-
suppressEcho = true;
|
|
960
|
-
echoMarker = savedMarker;
|
|
961
|
-
currentMarker = ""; // Disable marker detection during prompt echo & formatted display
|
|
962
|
-
ptyProcess.write(prompt);
|
|
895
|
+
ptyProcess.write(next.text);
|
|
963
896
|
setTimeout(() => {
|
|
964
897
|
if (ptyProcess && ptyAlive) {
|
|
898
|
+
// Drop the local TUI input echo from any forwarded stream output.
|
|
965
899
|
outputBuffer = "";
|
|
966
900
|
const isClaude = agentType === "claude-code";
|
|
967
901
|
if (isClaude) {
|
|
968
902
|
// Claude Code: send CR directly without ESC.
|
|
969
903
|
// ESC before CR is interpreted as Alt+Enter (newline).
|
|
970
904
|
ptyProcess.write("\r");
|
|
971
|
-
suppressTimer = setTimeout(() => {
|
|
972
|
-
suppressTimer = null;
|
|
973
|
-
if (!suppressEcho) return;
|
|
974
|
-
suppressEcho = false;
|
|
975
|
-
echoMarker = "";
|
|
976
|
-
currentMarker = savedMarker;
|
|
977
|
-
outputBuffer = "";
|
|
978
|
-
}, 1500);
|
|
979
905
|
} else {
|
|
980
906
|
// Codex/others: ESC dismisses autocomplete, then CR submits.
|
|
981
907
|
ptyProcess.write("\x1b");
|
|
@@ -983,14 +909,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
983
909
|
if (ptyProcess && ptyAlive) {
|
|
984
910
|
ptyProcess.write("\r");
|
|
985
911
|
}
|
|
986
|
-
suppressTimer = setTimeout(() => {
|
|
987
|
-
suppressTimer = null;
|
|
988
|
-
if (!suppressEcho) return;
|
|
989
|
-
suppressEcho = false;
|
|
990
|
-
echoMarker = "";
|
|
991
|
-
currentMarker = savedMarker;
|
|
992
|
-
outputBuffer = "";
|
|
993
|
-
}, 1500);
|
|
994
912
|
}, 100);
|
|
995
913
|
}
|
|
996
914
|
}
|
|
@@ -1001,13 +919,12 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
1001
919
|
watchdogTimer = setTimeout(() => {
|
|
1002
920
|
watchdogTimer = null;
|
|
1003
921
|
if (!busy) return;
|
|
1004
|
-
const timeoutNote = `[internal-pty]
|
|
922
|
+
const timeoutNote = `[internal-pty] task timeout; restarting PTY`;
|
|
1005
923
|
if (currentPublisher) {
|
|
1006
924
|
completePublisherResponse("timeout", timeoutNote);
|
|
1007
925
|
}
|
|
1008
926
|
logNote(timeoutNote);
|
|
1009
|
-
restartPty("
|
|
1010
|
-
currentMarker = "";
|
|
927
|
+
restartPty("task timeout");
|
|
1011
928
|
busy = false;
|
|
1012
929
|
activityDetector.markIdle();
|
|
1013
930
|
currentPublisher = "";
|
|
@@ -1058,11 +975,10 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
1058
975
|
if (messageQueue.length >= maxQueue) {
|
|
1059
976
|
messageQueue.shift();
|
|
1060
977
|
}
|
|
1061
|
-
const marker = raw ? "" : `__UFOO_DONE_${Date.now()}_${Math.random().toString(16).slice(2)}__`;
|
|
1062
978
|
const publisher = typeof evt.publisher === "object" && evt.publisher
|
|
1063
979
|
? (evt.publisher.subscriber || evt.publisher.nickname || "unknown")
|
|
1064
980
|
: (evt.publisher || "unknown");
|
|
1065
|
-
messageQueue.push({ publisher, raw, text
|
|
981
|
+
messageQueue.push({ publisher, raw, text });
|
|
1066
982
|
}
|
|
1067
983
|
}
|
|
1068
984
|
processQueue();
|
|
@@ -126,7 +126,10 @@ function createAgentViewController(options = {}) {
|
|
|
126
126
|
|
|
127
127
|
agentInputSuppressUntil = now() + 300;
|
|
128
128
|
agentViewUsesBus = Boolean(options.useBus);
|
|
129
|
-
if (
|
|
129
|
+
if (agentViewUsesBus) {
|
|
130
|
+
const label = getAgentLabel(agentId);
|
|
131
|
+
processStdout.write(`ufoo internal · ${label}\r\n\r\n> `);
|
|
132
|
+
} else {
|
|
130
133
|
const sockPath = getInjectSockPath(agentId);
|
|
131
134
|
connectAgentOutput(sockPath);
|
|
132
135
|
connectAgentInput(sockPath);
|
|
@@ -57,6 +57,11 @@ function createDashboardKeyController(options = {}) {
|
|
|
57
57
|
return Boolean(caps && caps.supportsSocketProtocol);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function supportsInternalQueue(agentId) {
|
|
61
|
+
const caps = getAgentCapabilities(agentId);
|
|
62
|
+
return Boolean(caps && caps.supportsInternalQueueLoop);
|
|
63
|
+
}
|
|
64
|
+
|
|
60
65
|
function withAgentInputFocus() {
|
|
61
66
|
state.focusMode = "input";
|
|
62
67
|
state.agentOutputSuppressed = false;
|
|
@@ -73,7 +78,7 @@ function createDashboardKeyController(options = {}) {
|
|
|
73
78
|
|
|
74
79
|
function switchAgentView(agentId) {
|
|
75
80
|
withAgentInputFocus();
|
|
76
|
-
enterAgentView(agentId);
|
|
81
|
+
enterAgentView(agentId, { useBus: supportsInternalQueue(agentId) && !supportsSocket(agentId) });
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
function exitAgentDashboardToInput() {
|
|
@@ -154,7 +159,7 @@ function createDashboardKeyController(options = {}) {
|
|
|
154
159
|
} else {
|
|
155
160
|
withAgentInputFocus();
|
|
156
161
|
state.selectedAgentIndex = nextIndex + 1;
|
|
157
|
-
enterAgentView(nextAgent);
|
|
162
|
+
enterAgentView(nextAgent, { useBus: supportsInternalQueue(nextAgent) && !supportsSocket(nextAgent) });
|
|
158
163
|
}
|
|
159
164
|
} else {
|
|
160
165
|
exitAgentView();
|
|
@@ -511,6 +516,16 @@ function createDashboardKeyController(options = {}) {
|
|
|
511
516
|
enterAgentView(agentId);
|
|
512
517
|
return true;
|
|
513
518
|
}
|
|
519
|
+
|
|
520
|
+
if (supportsInternalQueue(agentId)) {
|
|
521
|
+
clearTargetAgent();
|
|
522
|
+
state.focusMode = "input";
|
|
523
|
+
state.dashboardView = "agents";
|
|
524
|
+
state.selectedAgentIndex = -1;
|
|
525
|
+
setScreenGrabKeys(false);
|
|
526
|
+
enterAgentView(agentId, { useBus: true });
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
514
529
|
}
|
|
515
530
|
|
|
516
531
|
exitDashboardMode(false);
|