u-foo 2.3.9 → 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 +15 -129
- package/src/agent/publisherRouting.js +5 -0
- 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
|
@@ -9,6 +9,7 @@ const { ActivityDetector } = require("./activityDetector");
|
|
|
9
9
|
const { createActivityStatePublisher } = require("./activityStatePublisher");
|
|
10
10
|
const {
|
|
11
11
|
parseStreamEnvelope,
|
|
12
|
+
shouldAutoReplyFromPtyToPublisher,
|
|
12
13
|
shouldForwardStreamToPublisher,
|
|
13
14
|
} = require("./publisherRouting");
|
|
14
15
|
|
|
@@ -115,11 +116,6 @@ function computeInjectedSubmitDelayMs(agentType, text) {
|
|
|
115
116
|
return delayMs;
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
function buildPrompt(text, marker) {
|
|
119
|
-
if (!marker) return text;
|
|
120
|
-
return `${text}\n\n请在完成后输出以下标记(单独一行):\n${marker}\n`;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
119
|
function resolveCommand(agentType, extraArgs = []) {
|
|
124
120
|
const normalizedAgent = String(agentType || "").trim().toLowerCase();
|
|
125
121
|
const extra = Array.isArray(extraArgs) ? extraArgs : [];
|
|
@@ -171,9 +167,11 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
171
167
|
UFOO_INTERNAL_PTY: "1",
|
|
172
168
|
};
|
|
173
169
|
|
|
170
|
+
const idleMs = Number.parseInt(process.env.UFOO_INTERNAL_PTY_IDLE_MS || "", 10) || 30000;
|
|
174
171
|
const eventBus = new EventBus(projectRoot);
|
|
175
172
|
const activityDetector = new ActivityDetector(agentType, {
|
|
176
173
|
mode: "internal-pty",
|
|
174
|
+
quietWindowMs: idleMs,
|
|
177
175
|
});
|
|
178
176
|
const agentsFilePath = getUfooPaths(projectRoot).agentsFile;
|
|
179
177
|
|
|
@@ -183,16 +181,11 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
183
181
|
let ptyReady = false;
|
|
184
182
|
let readyTimer = null;
|
|
185
183
|
let currentPublisher = "";
|
|
186
|
-
let currentMarker = "";
|
|
187
184
|
let pendingOutput = [];
|
|
188
185
|
let outputBuffer = "";
|
|
189
186
|
let flushTimer = null;
|
|
190
187
|
let idleTimer = null;
|
|
191
188
|
let watchdogTimer = null;
|
|
192
|
-
let suppressEcho = false;
|
|
193
|
-
let echoMarker = "";
|
|
194
|
-
let suppressTimer = null;
|
|
195
|
-
let managedReplyBuffer = "";
|
|
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,
|
|
@@ -254,45 +244,23 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
254
244
|
return result;
|
|
255
245
|
}
|
|
256
246
|
|
|
257
|
-
function appendManagedReply(chunk) {
|
|
258
|
-
const text = String(chunk || "");
|
|
259
|
-
if (!text) return;
|
|
260
|
-
managedReplyBuffer += text;
|
|
261
|
-
if (managedReplyBuffer.length > 40000) {
|
|
262
|
-
managedReplyBuffer = managedReplyBuffer.slice(-40000);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function takeManagedReply() {
|
|
267
|
-
const reply = sanitizeChunk(managedReplyBuffer)
|
|
268
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
269
|
-
.trim();
|
|
270
|
-
managedReplyBuffer = "";
|
|
271
|
-
return reply;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
247
|
function completePublisherResponse(reason, fallbackNote = "") {
|
|
275
248
|
if (!currentPublisher) return;
|
|
276
249
|
if (flushTimer) {
|
|
277
250
|
clearTimeout(flushTimer);
|
|
278
251
|
flushTimer = null;
|
|
279
252
|
}
|
|
253
|
+
if (!shouldAutoReplyFromPtyToPublisher(projectRoot, currentPublisher)) {
|
|
254
|
+
outputBuffer = "";
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
280
257
|
if (outputBuffer) {
|
|
281
258
|
const remaining = outputBuffer;
|
|
282
259
|
outputBuffer = "";
|
|
283
260
|
deliverChunk(remaining);
|
|
284
261
|
}
|
|
285
|
-
if (
|
|
286
|
-
|
|
287
|
-
enqueueSend(currentPublisher, JSON.stringify({ stream: true, done: true, reason }));
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
const reply = takeManagedReply();
|
|
291
|
-
if (reply) {
|
|
292
|
-
enqueueSend(currentPublisher, reply);
|
|
293
|
-
} else if (fallbackNote) {
|
|
294
|
-
enqueueSend(currentPublisher, fallbackNote);
|
|
295
|
-
}
|
|
262
|
+
if (fallbackNote) enqueueSend(currentPublisher, fallbackNote);
|
|
263
|
+
enqueueSend(currentPublisher, JSON.stringify({ stream: true, done: true, reason }));
|
|
296
264
|
}
|
|
297
265
|
|
|
298
266
|
// TTY view subscribers (same protocol as launcher inject.sock)
|
|
@@ -623,8 +591,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
623
591
|
if (currentPublisher) {
|
|
624
592
|
if (canStreamToPublisher(currentPublisher)) {
|
|
625
593
|
enqueueSend(currentPublisher, payload);
|
|
626
|
-
} else {
|
|
627
|
-
appendManagedReply(cleaned);
|
|
628
594
|
}
|
|
629
595
|
} else {
|
|
630
596
|
pendingOutput.push(payload);
|
|
@@ -684,8 +650,7 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
684
650
|
detail: snap.detail,
|
|
685
651
|
});
|
|
686
652
|
// Quiet-window detector may classify IDLE sooner than stream fallback timer.
|
|
687
|
-
|
|
688
|
-
if (newState === "idle" && busy && !currentMarker && !suppressEcho) {
|
|
653
|
+
if (newState === "idle" && busy) {
|
|
689
654
|
if (idleTimer) {
|
|
690
655
|
clearTimeout(idleTimer);
|
|
691
656
|
idleTimer = null;
|
|
@@ -699,7 +664,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
699
664
|
}
|
|
700
665
|
busy = false;
|
|
701
666
|
currentPublisher = "";
|
|
702
|
-
managedReplyBuffer = "";
|
|
703
667
|
processQueue();
|
|
704
668
|
}
|
|
705
669
|
});
|
|
@@ -721,50 +685,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
721
685
|
if (!clean) return;
|
|
722
686
|
outputBuffer += clean;
|
|
723
687
|
activityDetector.processOutput(clean);
|
|
724
|
-
if (suppressEcho) {
|
|
725
|
-
if (echoMarker && outputBuffer.includes(echoMarker)) {
|
|
726
|
-
const idx = outputBuffer.indexOf(echoMarker);
|
|
727
|
-
outputBuffer = outputBuffer.slice(idx + echoMarker.length);
|
|
728
|
-
outputBuffer = outputBuffer.replace(/^\n+/, "");
|
|
729
|
-
suppressEcho = false;
|
|
730
|
-
currentMarker = echoMarker;
|
|
731
|
-
echoMarker = "";
|
|
732
|
-
if (suppressTimer) {
|
|
733
|
-
clearTimeout(suppressTimer);
|
|
734
|
-
suppressTimer = null;
|
|
735
|
-
}
|
|
736
|
-
} else {
|
|
737
|
-
return;
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
if (currentMarker) {
|
|
741
|
-
const idx = outputBuffer.indexOf(currentMarker);
|
|
742
|
-
if (idx !== -1) {
|
|
743
|
-
const before = outputBuffer.slice(0, idx);
|
|
744
|
-
outputBuffer = "";
|
|
745
|
-
if (before) {
|
|
746
|
-
deliverChunk(before);
|
|
747
|
-
}
|
|
748
|
-
if (currentPublisher) {
|
|
749
|
-
completePublisherResponse("marker");
|
|
750
|
-
}
|
|
751
|
-
currentMarker = "";
|
|
752
|
-
busy = false;
|
|
753
|
-
activityDetector.markIdle();
|
|
754
|
-
currentPublisher = "";
|
|
755
|
-
managedReplyBuffer = "";
|
|
756
|
-
if (watchdogTimer) {
|
|
757
|
-
clearTimeout(watchdogTimer);
|
|
758
|
-
watchdogTimer = null;
|
|
759
|
-
}
|
|
760
|
-
if (idleTimer) {
|
|
761
|
-
clearTimeout(idleTimer);
|
|
762
|
-
idleTimer = null;
|
|
763
|
-
}
|
|
764
|
-
processQueue();
|
|
765
|
-
return;
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
688
|
scheduleFlush();
|
|
769
689
|
// Ready detection: during TUI startup, reset the quiet timer on each output.
|
|
770
690
|
// Once output stops for READY_QUIET_MS, the TUI is considered initialized.
|
|
@@ -793,7 +713,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
793
713
|
busy = false;
|
|
794
714
|
activityDetector.markIdle();
|
|
795
715
|
currentPublisher = "";
|
|
796
|
-
managedReplyBuffer = "";
|
|
797
716
|
processQueue();
|
|
798
717
|
}, idleMs);
|
|
799
718
|
}
|
|
@@ -832,8 +751,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
832
751
|
busy = false;
|
|
833
752
|
activityDetector.markIdle();
|
|
834
753
|
currentPublisher = "";
|
|
835
|
-
currentMarker = "";
|
|
836
|
-
managedReplyBuffer = "";
|
|
837
754
|
|
|
838
755
|
// If stop() was called, let the runner exit
|
|
839
756
|
if (!running) return;
|
|
@@ -968,12 +885,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
968
885
|
busy = true;
|
|
969
886
|
activityDetector.markWorking();
|
|
970
887
|
currentPublisher = next.publisher;
|
|
971
|
-
currentMarker = next.marker || "";
|
|
972
|
-
managedReplyBuffer = "";
|
|
973
|
-
if (suppressTimer) {
|
|
974
|
-
clearTimeout(suppressTimer);
|
|
975
|
-
suppressTimer = null;
|
|
976
|
-
}
|
|
977
888
|
flushPending();
|
|
978
889
|
if (next.text) {
|
|
979
890
|
if (next.raw) {
|
|
@@ -981,30 +892,16 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
981
892
|
} else {
|
|
982
893
|
// Write text first, then send Enter separately.
|
|
983
894
|
// Codex Ink TUI requires text and submit key as separate writes.
|
|
984
|
-
|
|
985
|
-
// because the prompt echo (TextInput display) contains the marker text.
|
|
986
|
-
const prompt = buildPrompt(next.text, currentMarker);
|
|
987
|
-
const savedMarker = currentMarker;
|
|
988
|
-
suppressEcho = true;
|
|
989
|
-
echoMarker = savedMarker;
|
|
990
|
-
currentMarker = ""; // Disable marker detection during prompt echo & formatted display
|
|
991
|
-
ptyProcess.write(prompt);
|
|
895
|
+
ptyProcess.write(next.text);
|
|
992
896
|
setTimeout(() => {
|
|
993
897
|
if (ptyProcess && ptyAlive) {
|
|
898
|
+
// Drop the local TUI input echo from any forwarded stream output.
|
|
994
899
|
outputBuffer = "";
|
|
995
900
|
const isClaude = agentType === "claude-code";
|
|
996
901
|
if (isClaude) {
|
|
997
902
|
// Claude Code: send CR directly without ESC.
|
|
998
903
|
// ESC before CR is interpreted as Alt+Enter (newline).
|
|
999
904
|
ptyProcess.write("\r");
|
|
1000
|
-
suppressTimer = setTimeout(() => {
|
|
1001
|
-
suppressTimer = null;
|
|
1002
|
-
if (!suppressEcho) return;
|
|
1003
|
-
suppressEcho = false;
|
|
1004
|
-
echoMarker = "";
|
|
1005
|
-
currentMarker = savedMarker;
|
|
1006
|
-
outputBuffer = "";
|
|
1007
|
-
}, 1500);
|
|
1008
905
|
} else {
|
|
1009
906
|
// Codex/others: ESC dismisses autocomplete, then CR submits.
|
|
1010
907
|
ptyProcess.write("\x1b");
|
|
@@ -1012,14 +909,6 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
1012
909
|
if (ptyProcess && ptyAlive) {
|
|
1013
910
|
ptyProcess.write("\r");
|
|
1014
911
|
}
|
|
1015
|
-
suppressTimer = setTimeout(() => {
|
|
1016
|
-
suppressTimer = null;
|
|
1017
|
-
if (!suppressEcho) return;
|
|
1018
|
-
suppressEcho = false;
|
|
1019
|
-
echoMarker = "";
|
|
1020
|
-
currentMarker = savedMarker;
|
|
1021
|
-
outputBuffer = "";
|
|
1022
|
-
}, 1500);
|
|
1023
912
|
}, 100);
|
|
1024
913
|
}
|
|
1025
914
|
}
|
|
@@ -1030,17 +919,15 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
1030
919
|
watchdogTimer = setTimeout(() => {
|
|
1031
920
|
watchdogTimer = null;
|
|
1032
921
|
if (!busy) return;
|
|
1033
|
-
const timeoutNote = `[internal-pty]
|
|
922
|
+
const timeoutNote = `[internal-pty] task timeout; restarting PTY`;
|
|
1034
923
|
if (currentPublisher) {
|
|
1035
924
|
completePublisherResponse("timeout", timeoutNote);
|
|
1036
925
|
}
|
|
1037
926
|
logNote(timeoutNote);
|
|
1038
|
-
restartPty("
|
|
1039
|
-
currentMarker = "";
|
|
927
|
+
restartPty("task timeout");
|
|
1040
928
|
busy = false;
|
|
1041
929
|
activityDetector.markIdle();
|
|
1042
930
|
currentPublisher = "";
|
|
1043
|
-
managedReplyBuffer = "";
|
|
1044
931
|
processQueue();
|
|
1045
932
|
}, watchdogMs);
|
|
1046
933
|
}
|
|
@@ -1088,11 +975,10 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
|
|
|
1088
975
|
if (messageQueue.length >= maxQueue) {
|
|
1089
976
|
messageQueue.shift();
|
|
1090
977
|
}
|
|
1091
|
-
const marker = raw ? "" : `__UFOO_DONE_${Date.now()}_${Math.random().toString(16).slice(2)}__`;
|
|
1092
978
|
const publisher = typeof evt.publisher === "object" && evt.publisher
|
|
1093
979
|
? (evt.publisher.subscriber || evt.publisher.nickname || "unknown")
|
|
1094
980
|
: (evt.publisher || "unknown");
|
|
1095
|
-
messageQueue.push({ publisher, raw, text
|
|
981
|
+
messageQueue.push({ publisher, raw, text });
|
|
1096
982
|
}
|
|
1097
983
|
}
|
|
1098
984
|
processQueue();
|
|
@@ -33,6 +33,10 @@ function shouldForwardStreamToPublisher(projectRoot, publisher) {
|
|
|
33
33
|
return !isManagedAgentPublisher(projectRoot, id);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function shouldAutoReplyFromPtyToPublisher(projectRoot, publisher) {
|
|
37
|
+
return shouldForwardStreamToPublisher(projectRoot, publisher);
|
|
38
|
+
}
|
|
39
|
+
|
|
36
40
|
function parseStreamEnvelope(message) {
|
|
37
41
|
if (typeof message !== "string" || !message.trim()) return null;
|
|
38
42
|
try {
|
|
@@ -50,5 +54,6 @@ module.exports = {
|
|
|
50
54
|
isManagedAgentPublisher,
|
|
51
55
|
normalizePublisher,
|
|
52
56
|
parseStreamEnvelope,
|
|
57
|
+
shouldAutoReplyFromPtyToPublisher,
|
|
53
58
|
shouldForwardStreamToPublisher,
|
|
54
59
|
};
|
|
@@ -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);
|