@yeaft/webchat-agent 0.1.856 → 0.1.859
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/conversation.js +92 -28
- package/package.json +1 -1
- package/providers/base.js +34 -0
- package/providers/claude-code.js +34 -0
- package/providers/copilot.js +278 -0
- package/providers/index.js +19 -0
- package/yeaft/compact/orchestrator.js +7 -13
- package/yeaft/conversation/persist.js +94 -53
- package/yeaft/conversation/search.js +24 -6
- package/yeaft/dream-v2/apply.js +28 -11
- package/yeaft/dream-v2/prompts/index.js +8 -1
- package/yeaft/dream-v2/runner.js +12 -5
- package/yeaft/dream-v2/triage.js +13 -8
- package/yeaft/engine.js +2 -2
- package/yeaft/groups/pre-flow.js +14 -2
- package/yeaft/init.js +6 -11
- package/yeaft/memory/adjust.js +2 -4
- package/yeaft/memory/ams.js +2 -4
- package/yeaft/memory/preflow.js +2 -6
- package/yeaft/memory/seed-backfill.js +41 -1
- package/yeaft/memory/segment.js +1 -1
- package/yeaft/memory/store-v2.js +120 -73
- package/yeaft/session.js +11 -1
- package/yeaft/vp/vp-crud.js +7 -24
package/conversation.js
CHANGED
|
@@ -6,6 +6,7 @@ import { query } from './sdk/index.js';
|
|
|
6
6
|
import { loadSessionHistory } from './history.js';
|
|
7
7
|
import { startClaudeQuery } from './claude.js';
|
|
8
8
|
import { crewSessions, loadCrewIndex } from './crew.js';
|
|
9
|
+
import { getProvider, DEFAULT_PROVIDER, isValidProvider } from './providers/index.js';
|
|
9
10
|
|
|
10
11
|
// 不支持的斜杠命令(真正需要交互式 CLI 的命令)
|
|
11
12
|
const UNSUPPORTED_SLASH_COMMANDS = ['/help', '/bug', '/login', '/logout', '/terminal-setup', '/vim', '/config'];
|
|
@@ -293,7 +294,8 @@ export async function sendConversationList() {
|
|
|
293
294
|
createdAt: state.createdAt,
|
|
294
295
|
processing: !!state.turnActive,
|
|
295
296
|
userId: state.userId,
|
|
296
|
-
username: state.username
|
|
297
|
+
username: state.username,
|
|
298
|
+
provider: state.providerName || DEFAULT_PROVIDER
|
|
297
299
|
};
|
|
298
300
|
list.push(entry);
|
|
299
301
|
}
|
|
@@ -356,33 +358,41 @@ export function sendError(conversationId, message) {
|
|
|
356
358
|
export async function createConversation(msg) {
|
|
357
359
|
const { conversationId, workDir, userId, username, disallowedTools } = msg;
|
|
358
360
|
const effectiveWorkDir = workDir || ctx.CONFIG.workDir;
|
|
361
|
+
const provider = isValidProvider(msg.provider) ? msg.provider : DEFAULT_PROVIDER;
|
|
359
362
|
|
|
360
|
-
console.log(`Creating conversation: ${conversationId} in ${effectiveWorkDir} (lazy start)`);
|
|
363
|
+
console.log(`Creating conversation: ${conversationId} in ${effectiveWorkDir} (lazy start, provider=${provider})`);
|
|
361
364
|
if (username) console.log(` User: ${username} (${userId})`);
|
|
362
365
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
inputTokens: 0,
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
366
|
+
if (provider === 'claude-code') {
|
|
367
|
+
// Claude path: lazy-init state, real CLI boots on first message.
|
|
368
|
+
ctx.conversations.set(conversationId, {
|
|
369
|
+
query: null,
|
|
370
|
+
inputStream: null,
|
|
371
|
+
workDir: effectiveWorkDir,
|
|
372
|
+
claudeSessionId: null,
|
|
373
|
+
createdAt: Date.now(),
|
|
374
|
+
abortController: null,
|
|
375
|
+
tools: [],
|
|
376
|
+
slashCommands: [],
|
|
377
|
+
model: null,
|
|
378
|
+
userId,
|
|
379
|
+
username,
|
|
380
|
+
providerName: provider,
|
|
381
|
+
disallowedTools: disallowedTools || null,
|
|
382
|
+
usage: { inputTokens: 0, outputTokens: 0, cacheRead: 0, cacheCreation: 0, totalCostUsd: 0 }
|
|
383
|
+
});
|
|
384
|
+
} else {
|
|
385
|
+
// Non-Claude providers own their own state construction.
|
|
386
|
+
const driver = getProvider(provider);
|
|
387
|
+
const state = await driver.start({
|
|
388
|
+
conversationId,
|
|
389
|
+
workDir: effectiveWorkDir,
|
|
390
|
+
resumeSessionId: null,
|
|
391
|
+
userId,
|
|
392
|
+
username,
|
|
393
|
+
});
|
|
394
|
+
state.disallowedTools = disallowedTools || null;
|
|
395
|
+
}
|
|
386
396
|
|
|
387
397
|
ctx.sendToServer({
|
|
388
398
|
type: 'conversation_created',
|
|
@@ -390,6 +400,7 @@ export async function createConversation(msg) {
|
|
|
390
400
|
workDir: effectiveWorkDir,
|
|
391
401
|
userId,
|
|
392
402
|
username,
|
|
403
|
+
provider,
|
|
393
404
|
disallowedTools: disallowedTools || null
|
|
394
405
|
});
|
|
395
406
|
|
|
@@ -414,13 +425,16 @@ export async function createConversation(msg) {
|
|
|
414
425
|
|
|
415
426
|
// ★ Prestart Claude CLI in background to eagerly fetch skills/tools/model
|
|
416
427
|
// Fire-and-forget: failure just degrades to lazy-start behavior
|
|
417
|
-
|
|
428
|
+
if (provider === 'claude-code') {
|
|
429
|
+
prestartClaude(conversationId, effectiveWorkDir, null);
|
|
430
|
+
}
|
|
418
431
|
}
|
|
419
432
|
|
|
420
433
|
// Resume 历史 conversation (延迟启动 Claude,等待用户发送第一条消息)
|
|
421
434
|
export async function resumeConversation(msg) {
|
|
422
435
|
const { conversationId, claudeSessionId, workDir, userId, username, disallowedTools } = msg;
|
|
423
436
|
const effectiveWorkDir = workDir || ctx.CONFIG.workDir;
|
|
437
|
+
const provider = isValidProvider(msg.provider) ? msg.provider : DEFAULT_PROVIDER;
|
|
424
438
|
|
|
425
439
|
console.log(`[Resume] conversationId: ${conversationId}`);
|
|
426
440
|
console.log(`[Resume] claudeSessionId: ${claudeSessionId}`);
|
|
@@ -458,6 +472,7 @@ export async function resumeConversation(msg) {
|
|
|
458
472
|
model: null,
|
|
459
473
|
userId,
|
|
460
474
|
username,
|
|
475
|
+
providerName: provider,
|
|
461
476
|
disallowedTools: disallowedTools || null, // null = 使用全局默认
|
|
462
477
|
usage: {
|
|
463
478
|
inputTokens: 0,
|
|
@@ -468,6 +483,19 @@ export async function resumeConversation(msg) {
|
|
|
468
483
|
}
|
|
469
484
|
});
|
|
470
485
|
|
|
486
|
+
// Non-Claude providers: re-init state via driver so sessionId/providerName are set correctly.
|
|
487
|
+
if (provider !== 'claude-code') {
|
|
488
|
+
const driver = getProvider(provider);
|
|
489
|
+
const state = await driver.start({
|
|
490
|
+
conversationId,
|
|
491
|
+
workDir: effectiveWorkDir,
|
|
492
|
+
resumeSessionId: claudeSessionId || null,
|
|
493
|
+
userId,
|
|
494
|
+
username,
|
|
495
|
+
});
|
|
496
|
+
state.disallowedTools = disallowedTools || null;
|
|
497
|
+
}
|
|
498
|
+
|
|
471
499
|
ctx.sendToServer({
|
|
472
500
|
type: 'conversation_resumed',
|
|
473
501
|
conversationId,
|
|
@@ -475,7 +503,8 @@ export async function resumeConversation(msg) {
|
|
|
475
503
|
workDir: effectiveWorkDir,
|
|
476
504
|
historyMessages,
|
|
477
505
|
userId,
|
|
478
|
-
username
|
|
506
|
+
username,
|
|
507
|
+
provider
|
|
479
508
|
});
|
|
480
509
|
|
|
481
510
|
// 立即发送 agent 级别的 MCP servers 列表
|
|
@@ -498,7 +527,7 @@ export async function resumeConversation(msg) {
|
|
|
498
527
|
// ★ Prestart Claude CLI in background to eagerly fetch skills/tools/model
|
|
499
528
|
// Skip if conversation already has an active query (shouldn't happen, but safety check)
|
|
500
529
|
const resumeState = ctx.conversations.get(conversationId);
|
|
501
|
-
if (!resumeState?.query) {
|
|
530
|
+
if (provider === 'claude-code' && !resumeState?.query) {
|
|
502
531
|
prestartClaude(conversationId, effectiveWorkDir, claudeSessionId);
|
|
503
532
|
}
|
|
504
533
|
}
|
|
@@ -704,6 +733,41 @@ export async function handleUserInput(msg) {
|
|
|
704
733
|
|
|
705
734
|
let state = ctx.conversations.get(conversationId);
|
|
706
735
|
|
|
736
|
+
// ★ Non-Claude providers: dispatch to driver and return
|
|
737
|
+
const providerName = state?.providerName || DEFAULT_PROVIDER;
|
|
738
|
+
if (providerName !== 'claude-code') {
|
|
739
|
+
const driver = getProvider(providerName);
|
|
740
|
+
const effectiveWorkDir = workDir || state?.workDir || ctx.CONFIG.workDir;
|
|
741
|
+
if (!state) {
|
|
742
|
+
state = await driver.start({
|
|
743
|
+
conversationId,
|
|
744
|
+
workDir: effectiveWorkDir,
|
|
745
|
+
resumeSessionId: claudeSessionId || null,
|
|
746
|
+
userId: msg.userId,
|
|
747
|
+
username: msg.username,
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
if (workDir) state.workDir = workDir;
|
|
751
|
+
sendOutput(conversationId, { type: 'user', message: { role: 'user', content: prompt } });
|
|
752
|
+
state.turnActive = true;
|
|
753
|
+
sendConversationList();
|
|
754
|
+
try {
|
|
755
|
+
await driver.sendInput(state, prompt, { conversationId, raw: msg });
|
|
756
|
+
} catch (err) {
|
|
757
|
+
sendOutput(conversationId, {
|
|
758
|
+
type: 'result',
|
|
759
|
+
subtype: 'error',
|
|
760
|
+
session_id: state.sessionId || null,
|
|
761
|
+
is_error: true,
|
|
762
|
+
error: `${providerName} error: ${err?.message || err}`,
|
|
763
|
+
});
|
|
764
|
+
} finally {
|
|
765
|
+
state.turnActive = false;
|
|
766
|
+
sendConversationList();
|
|
767
|
+
}
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
|
|
707
771
|
// 如果没有活跃的查询,启动新的
|
|
708
772
|
if (!state || !state.query || !state.inputStream) {
|
|
709
773
|
const resumeSessionId = claudeSessionId || state?.claudeSessionId || null;
|
package/package.json
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat provider abstraction.
|
|
3
|
+
*
|
|
4
|
+
* Wire-protocol note: `claude_output` is now a PROTOCOL name, not a
|
|
5
|
+
* vendor name. Every driver MUST emit events on
|
|
6
|
+
* `ctx.sendToServer({ type: 'claude_output', conversationId, data })`,
|
|
7
|
+
* where `data` follows the Claude stream-json envelope:
|
|
8
|
+
* - { type: 'assistant', message: { role, content: [...] } }
|
|
9
|
+
* - { type: 'user', message: { role, content: [...] } }
|
|
10
|
+
* - { type: 'result', subtype, session_id, is_error, ... }
|
|
11
|
+
* - { type: 'system', subtype, ... }
|
|
12
|
+
*
|
|
13
|
+
* Non-Claude drivers (e.g. Copilot) MUST translate their native event
|
|
14
|
+
* streams into this envelope so the existing renderer needs no changes.
|
|
15
|
+
*
|
|
16
|
+
* @typedef {Object} ChatProvider
|
|
17
|
+
* @property {string} name
|
|
18
|
+
* @property {(opts: StartOpts) => Promise<Object>} start
|
|
19
|
+
* @property {(state: Object, prompt: string, opts?: Object) => Promise<void>} sendInput
|
|
20
|
+
* @property {(state: Object) => void} abort
|
|
21
|
+
*
|
|
22
|
+
* @typedef {Object} StartOpts
|
|
23
|
+
* @property {string} conversationId
|
|
24
|
+
* @property {string} workDir
|
|
25
|
+
* @property {string|null} [resumeSessionId]
|
|
26
|
+
* @property {string} [userId]
|
|
27
|
+
* @property {string} [username]
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export const PROVIDER_NAMES = Object.freeze(['claude-code', 'copilot']);
|
|
31
|
+
export const DEFAULT_PROVIDER = 'claude-code';
|
|
32
|
+
export function isValidProvider(name) {
|
|
33
|
+
return typeof name === 'string' && PROVIDER_NAMES.includes(name);
|
|
34
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { startClaudeQuery } from '../claude.js';
|
|
2
|
+
|
|
3
|
+
export const name = 'claude-code';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Start (or resume) a Claude Code CLI session.
|
|
7
|
+
* Returns the same state object that startClaudeQuery stores in ctx.conversations.
|
|
8
|
+
*/
|
|
9
|
+
export async function start(opts) {
|
|
10
|
+
const state = await startClaudeQuery(
|
|
11
|
+
opts.conversationId,
|
|
12
|
+
opts.workDir,
|
|
13
|
+
opts.resumeSessionId || null
|
|
14
|
+
);
|
|
15
|
+
state.providerName = name;
|
|
16
|
+
return state;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Claude CLI handles input via the persistent stdin Stream that
|
|
21
|
+
* conversation.js manages directly, so this driver's sendInput is a no-op.
|
|
22
|
+
* conversation.js's existing branch keeps owning the Claude path.
|
|
23
|
+
*/
|
|
24
|
+
export async function sendInput(_state, _prompt, _opts) {
|
|
25
|
+
/* handled inline by conversation.js for the Claude branch */
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function abort(state) {
|
|
29
|
+
if (state?.abortController) {
|
|
30
|
+
try { state.abortController.abort(); } catch { /* noop */ }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default { name, start, sendInput, abort };
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import ctx from '../context.js';
|
|
4
|
+
|
|
5
|
+
export const name = 'copilot';
|
|
6
|
+
|
|
7
|
+
const COPILOT_BIN = process.env.COPILOT_BIN || 'copilot';
|
|
8
|
+
// Opt-in only: --allow-all-tools is a destructive footgun by default in a
|
|
9
|
+
// multi-tenant agent. Set COPILOT_YOLO=1 (and only if you know what you're
|
|
10
|
+
// doing) to skip Copilot's tool prompts.
|
|
11
|
+
const YOLO = process.env.COPILOT_YOLO === '1';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Start (or resume) a Copilot session.
|
|
15
|
+
* Copilot's `-p` mode is one-shot per turn, so "start" just prepares state.
|
|
16
|
+
* Each sendInput() spawns one `copilot -p ...` child with the same
|
|
17
|
+
* --session-id for continuity.
|
|
18
|
+
*/
|
|
19
|
+
export async function start(opts) {
|
|
20
|
+
const conversationId = opts.conversationId;
|
|
21
|
+
// Tear down any prior entry so we don't leak children.
|
|
22
|
+
const prior = ctx.conversations.get(conversationId);
|
|
23
|
+
if (prior?.copilotChild) {
|
|
24
|
+
try { prior.copilotChild.kill('SIGTERM'); } catch { /* noop */ }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const sessionId = opts.resumeSessionId || randomUUID();
|
|
28
|
+
const state = {
|
|
29
|
+
providerName: name,
|
|
30
|
+
conversationId: opts.conversationId,
|
|
31
|
+
query: null,
|
|
32
|
+
inputStream: null,
|
|
33
|
+
workDir: opts.workDir,
|
|
34
|
+
claudeSessionId: sessionId,
|
|
35
|
+
sessionId,
|
|
36
|
+
createdAt: prior?.createdAt || Date.now(),
|
|
37
|
+
abortController: null,
|
|
38
|
+
tools: [],
|
|
39
|
+
slashCommands: [],
|
|
40
|
+
model: 'copilot',
|
|
41
|
+
userId: opts.userId,
|
|
42
|
+
username: opts.username,
|
|
43
|
+
disallowedTools: prior?.disallowedTools || null,
|
|
44
|
+
copilotChild: null,
|
|
45
|
+
usage: { inputTokens: 0, outputTokens: 0, cacheRead: 0, cacheCreation: 0, totalCostUsd: 0 },
|
|
46
|
+
};
|
|
47
|
+
ctx.conversations.set(conversationId, state);
|
|
48
|
+
return state;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function sendInput(state, prompt, opts = {}) {
|
|
52
|
+
const conversationId = opts.conversationId || state.conversationId;
|
|
53
|
+
if (!conversationId) throw new Error('copilot: conversationId required');
|
|
54
|
+
if (!state.sessionId) state.sessionId = randomUUID();
|
|
55
|
+
|
|
56
|
+
// Abort any in-flight turn.
|
|
57
|
+
if (state.copilotChild) {
|
|
58
|
+
try { state.copilotChild.kill('SIGTERM'); } catch { /* noop */ }
|
|
59
|
+
state.copilotChild = null;
|
|
60
|
+
}
|
|
61
|
+
const abortController = new AbortController();
|
|
62
|
+
state.abortController = abortController;
|
|
63
|
+
state.turnActive = true;
|
|
64
|
+
state.turnResultReceived = false;
|
|
65
|
+
|
|
66
|
+
const args = ['-p', prompt, '--output-format', 'json', '-C', state.workDir, '--session-id', state.sessionId];
|
|
67
|
+
if (YOLO) args.push('--allow-all-tools');
|
|
68
|
+
|
|
69
|
+
let child;
|
|
70
|
+
try {
|
|
71
|
+
child = spawn(COPILOT_BIN, args, {
|
|
72
|
+
cwd: state.workDir,
|
|
73
|
+
env: process.env,
|
|
74
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
75
|
+
});
|
|
76
|
+
} catch (err) {
|
|
77
|
+
sendOutput(conversationId, {
|
|
78
|
+
type: 'result',
|
|
79
|
+
subtype: 'error',
|
|
80
|
+
session_id: state.sessionId,
|
|
81
|
+
is_error: true,
|
|
82
|
+
error: `copilot spawn failed: ${err?.message || err}`,
|
|
83
|
+
});
|
|
84
|
+
state.turnActive = false;
|
|
85
|
+
ctx.sendToServer({ type: 'turn_completed', conversationId, claudeSessionId: state.sessionId, workDir: state.workDir });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
state.copilotChild = child;
|
|
89
|
+
|
|
90
|
+
// Pre-register error handler so async ENOENT from spawn is never unhandled.
|
|
91
|
+
child.on('error', (err) => {
|
|
92
|
+
sendOutput(conversationId, {
|
|
93
|
+
type: 'result',
|
|
94
|
+
subtype: 'error',
|
|
95
|
+
session_id: state.sessionId,
|
|
96
|
+
is_error: true,
|
|
97
|
+
error: `copilot process error: ${err?.message || err}`,
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
let killTimer = null;
|
|
102
|
+
abortController.signal.addEventListener('abort', () => {
|
|
103
|
+
try { child.kill('SIGTERM'); } catch { /* noop */ }
|
|
104
|
+
// Escalate to SIGKILL if the child ignores SIGTERM, so the awaited
|
|
105
|
+
// close promise resolves and the next turn isn't blocked forever.
|
|
106
|
+
killTimer = setTimeout(() => {
|
|
107
|
+
try { child.kill('SIGKILL'); } catch { /* noop */ }
|
|
108
|
+
}, 5000);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
let stderrBuf = '';
|
|
112
|
+
const STDERR_CAP = 64 * 1024;
|
|
113
|
+
let sawResult = false;
|
|
114
|
+
|
|
115
|
+
const parser = createNdjsonParser((evt) => {
|
|
116
|
+
const envelopes = translateCopilotEvent(evt, state);
|
|
117
|
+
for (const e of envelopes) {
|
|
118
|
+
sendOutput(conversationId, e);
|
|
119
|
+
if (e?.type === 'result') sawResult = true;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
child.stdout.on('data', (chunk) => parser.push(chunk));
|
|
124
|
+
child.stderr.on('data', (chunk) => {
|
|
125
|
+
if (stderrBuf.length < STDERR_CAP) {
|
|
126
|
+
stderrBuf += chunk.toString('utf8').slice(0, STDERR_CAP - stderrBuf.length);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await new Promise((resolve) => {
|
|
131
|
+
child.on('close', (code) => {
|
|
132
|
+
if (killTimer) clearTimeout(killTimer);
|
|
133
|
+
parser.flush();
|
|
134
|
+
if (!sawResult) {
|
|
135
|
+
const ok = code === 0;
|
|
136
|
+
sendOutput(conversationId, {
|
|
137
|
+
type: 'result',
|
|
138
|
+
subtype: ok ? 'success' : 'error',
|
|
139
|
+
session_id: state.sessionId,
|
|
140
|
+
is_error: !ok,
|
|
141
|
+
error: ok ? undefined : (stderrBuf.trim().slice(0, 2000) || `copilot exited with code ${code}`),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
state.copilotChild = null;
|
|
145
|
+
state.turnActive = false;
|
|
146
|
+
ctx.sendToServer({
|
|
147
|
+
type: 'turn_completed',
|
|
148
|
+
conversationId,
|
|
149
|
+
claudeSessionId: state.sessionId,
|
|
150
|
+
workDir: state.workDir,
|
|
151
|
+
});
|
|
152
|
+
resolve();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function abort(state) {
|
|
158
|
+
if (state?.abortController) {
|
|
159
|
+
try { state.abortController.abort(); } catch { /* noop */ }
|
|
160
|
+
}
|
|
161
|
+
if (state?.copilotChild) {
|
|
162
|
+
try { state.copilotChild.kill('SIGTERM'); } catch { /* noop */ }
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---------- internals ----------
|
|
167
|
+
|
|
168
|
+
function sendOutput(conversationId, data) {
|
|
169
|
+
ctx.sendToServer({ type: 'claude_output', conversationId, data });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function createNdjsonParser(onEvent) {
|
|
173
|
+
let buf = '';
|
|
174
|
+
return {
|
|
175
|
+
push(chunk) {
|
|
176
|
+
buf += chunk.toString('utf8');
|
|
177
|
+
let idx;
|
|
178
|
+
while ((idx = buf.indexOf('\n')) >= 0) {
|
|
179
|
+
const line = buf.slice(0, idx).trim();
|
|
180
|
+
buf = buf.slice(idx + 1);
|
|
181
|
+
if (!line) continue;
|
|
182
|
+
let evt;
|
|
183
|
+
try { evt = JSON.parse(line); }
|
|
184
|
+
catch (err) {
|
|
185
|
+
if (ctx?.CONFIG?.debug) console.warn('[copilot] dropping unparsable line:', line.slice(0, 200));
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
try { onEvent(evt); }
|
|
189
|
+
catch (err) { console.warn('[copilot] event handler error:', err?.message || err); }
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
flush() {
|
|
193
|
+
const line = buf.trim();
|
|
194
|
+
buf = '';
|
|
195
|
+
if (!line) return;
|
|
196
|
+
try {
|
|
197
|
+
const evt = JSON.parse(line);
|
|
198
|
+
onEvent(evt);
|
|
199
|
+
} catch { /* discard trailing junk */ }
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Map a Copilot NDJSON event to zero-or-more claude_output envelopes.
|
|
206
|
+
* Defensive: unknown shapes are logged and dropped.
|
|
207
|
+
*
|
|
208
|
+
* Recognized loose schemas (Copilot CLI JSON output is not yet stable, so
|
|
209
|
+
* we accept several aliases and forward only what we understand):
|
|
210
|
+
* - text: { type: 'text'|'text_delta'|'assistant_text', text|delta }
|
|
211
|
+
* - message: { type: 'message', role, content }
|
|
212
|
+
* - tool_call: { type: 'tool_call'|'tool_use', id, name|tool, input|arguments }
|
|
213
|
+
* - tool_result: { type: 'tool_result', tool_use_id|id, content|output }
|
|
214
|
+
* - done: { type: 'result'|'done'|'complete', session_id?, error? }
|
|
215
|
+
* - error: { type: 'error', message|error }
|
|
216
|
+
*/
|
|
217
|
+
export function translateCopilotEvent(evt, state) {
|
|
218
|
+
if (!evt || typeof evt !== 'object') return [];
|
|
219
|
+
const t = evt.type;
|
|
220
|
+
|
|
221
|
+
if (t === 'text' || t === 'text_delta' || t === 'assistant_text') {
|
|
222
|
+
const text = typeof evt.text === 'string' ? evt.text : (typeof evt.delta === 'string' ? evt.delta : '');
|
|
223
|
+
if (!text) return [];
|
|
224
|
+
return [{ type: 'assistant', message: { role: 'assistant', content: [{ type: 'text', text }] } }];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (t === 'message' && evt.role === 'assistant') {
|
|
228
|
+
const content = normalizeContent(evt.content);
|
|
229
|
+
return [{ type: 'assistant', message: { role: 'assistant', content } }];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (t === 'tool_call' || t === 'tool_use') {
|
|
233
|
+
const id = evt.id || evt.call_id || randomUUID();
|
|
234
|
+
const toolName = evt.name || evt.tool || 'unknown';
|
|
235
|
+
const input = evt.input ?? evt.arguments ?? {};
|
|
236
|
+
return [{ type: 'assistant', message: { role: 'assistant', content: [{ type: 'tool_use', id, name: toolName, input }] } }];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (t === 'tool_result') {
|
|
240
|
+
const tool_use_id = evt.tool_use_id || evt.id || 'unknown';
|
|
241
|
+
const content = typeof evt.content === 'string'
|
|
242
|
+
? evt.content
|
|
243
|
+
: (typeof evt.output === 'string' ? evt.output : JSON.stringify(evt.content ?? evt.output ?? ''));
|
|
244
|
+
return [{ type: 'user', message: { role: 'user', content: [{ type: 'tool_result', tool_use_id, content }] } }];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (t === 'result' || t === 'done' || t === 'complete') {
|
|
248
|
+
const isErr = !!evt.error || evt.is_error === true;
|
|
249
|
+
return [{
|
|
250
|
+
type: 'result',
|
|
251
|
+
subtype: isErr ? 'error' : 'success',
|
|
252
|
+
session_id: evt.session_id || state?.sessionId || null,
|
|
253
|
+
is_error: isErr,
|
|
254
|
+
error: isErr ? (evt.error || evt.message || 'copilot error') : undefined,
|
|
255
|
+
}];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (t === 'error') {
|
|
259
|
+
return [{
|
|
260
|
+
type: 'result',
|
|
261
|
+
subtype: 'error',
|
|
262
|
+
session_id: state?.sessionId || null,
|
|
263
|
+
is_error: true,
|
|
264
|
+
error: evt.message || evt.error || 'copilot error',
|
|
265
|
+
}];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (ctx?.CONFIG?.debug) console.warn('[copilot] dropping unknown event type:', t);
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function normalizeContent(content) {
|
|
273
|
+
if (typeof content === 'string') return [{ type: 'text', text: content }];
|
|
274
|
+
if (Array.isArray(content)) return content;
|
|
275
|
+
return [{ type: 'text', text: String(content ?? '') }];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export default { name, start, sendInput, abort };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { PROVIDER_NAMES, DEFAULT_PROVIDER, isValidProvider } from './base.js';
|
|
2
|
+
import * as claudeCode from './claude-code.js';
|
|
3
|
+
import * as copilot from './copilot.js';
|
|
4
|
+
|
|
5
|
+
const REGISTRY = Object.freeze({
|
|
6
|
+
'claude-code': claudeCode,
|
|
7
|
+
'copilot': copilot,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export function getProvider(nameOrUndef) {
|
|
11
|
+
const key = nameOrUndef || DEFAULT_PROVIDER;
|
|
12
|
+
const driver = REGISTRY[key];
|
|
13
|
+
if (!driver) {
|
|
14
|
+
throw new Error(`Unknown chat provider: ${nameOrUndef} (known: ${PROVIDER_NAMES.join(', ')})`);
|
|
15
|
+
}
|
|
16
|
+
return driver;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { PROVIDER_NAMES, DEFAULT_PROVIDER, isValidProvider };
|
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import { groupTurns, pickCoolingGroups, indicesFromGroups } from './turn-group.js';
|
|
31
|
-
import { writeSummary } from '../memory/store-v2.js';
|
|
32
31
|
|
|
33
32
|
/**
|
|
34
33
|
* @typedef {{
|
|
@@ -57,7 +56,7 @@ import { writeSummary } from '../memory/store-v2.js';
|
|
|
57
56
|
* nextMessages: object[],
|
|
58
57
|
* }>}
|
|
59
58
|
*/
|
|
60
|
-
export async function runCompact({ messages, keepHot = 10,
|
|
59
|
+
export async function runCompact({ messages, keepHot = 10, hooks }) {
|
|
61
60
|
if (!Array.isArray(messages)) {
|
|
62
61
|
throw new Error('runCompact: messages array required');
|
|
63
62
|
}
|
|
@@ -104,17 +103,12 @@ export async function runCompact({ messages, keepHot = 10, taskId = null, root,
|
|
|
104
103
|
archiveResults.push({ ...g, turnId: r?.turnId });
|
|
105
104
|
}
|
|
106
105
|
|
|
107
|
-
// Track 2 —
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (typeof next === 'string' && next.trim()) {
|
|
114
|
-
await writeSummary({ kind: 'feature', id: taskId }, next, { root });
|
|
115
|
-
taskSummaryRefreshed = true;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
106
|
+
// Track 2 — task summary refresh: removed. The legacy `feature/<id>` root
|
|
107
|
+
// scope was dropped along with the Feature system (2026-05-13); under the
|
|
108
|
+
// group-isolated layout feature summaries would live at
|
|
109
|
+
// `group/<g>/feature/<id>/` and are written by dream, not by post-turn
|
|
110
|
+
// compact. Engine no longer passes `taskId`/`root` to this orchestrator.
|
|
111
|
+
const taskSummaryRefreshed = false;
|
|
118
112
|
|
|
119
113
|
// Track 3 — memory extraction.
|
|
120
114
|
let extractedCount = 0;
|