@yeaft/webchat-agent 1.0.289 → 1.0.290
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/local-runtime/version.json +1 -1
- package/package.json +1 -1
- package/yeaft/cli-session-runner.js +213 -0
- package/yeaft/cli.js +75 -13
- package/yeaft/stdio-protocol.js +164 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.0.
|
|
1
|
+
{"version":"1.0.290"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { Engine } from './engine.js';
|
|
4
|
+
import { createRouter } from './routing/router.js';
|
|
5
|
+
import { createCoordinator } from './sessions/coordinator.js';
|
|
6
|
+
import { sessionsRoot } from './sessions/session-crud.js';
|
|
7
|
+
import { openSession, loadSessionMeta } from './sessions/session-store.js';
|
|
8
|
+
import { loadSessionConfig, resolveSessionConfig } from './sessions/session-config.js';
|
|
9
|
+
import { readVp } from './vp/vp-crud.js';
|
|
10
|
+
import { COLLAB_TOOL_POLICY } from './tools/registry.js';
|
|
11
|
+
|
|
12
|
+
function buildVpPersona(vpId, loaded) {
|
|
13
|
+
const vp = readVp(vpId, { libDir: join(loaded.yeaftDir, 'virtual-persons') });
|
|
14
|
+
if (!vp) return null;
|
|
15
|
+
return {
|
|
16
|
+
vpId,
|
|
17
|
+
displayName: vp.displayName || vpId,
|
|
18
|
+
displayNameZh: vp.displayNameZh || '',
|
|
19
|
+
role: vp.role || '',
|
|
20
|
+
roleZh: vp.roleZh || '',
|
|
21
|
+
persona: vp.persona || '',
|
|
22
|
+
planInstruction: vp.planInstruction || '',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createEngine(loaded, sessionId, vpId) {
|
|
27
|
+
const effectiveConfig = resolveSessionConfig(
|
|
28
|
+
loaded.config,
|
|
29
|
+
loadSessionConfig(loaded.yeaftDir, sessionId),
|
|
30
|
+
);
|
|
31
|
+
return new Engine({
|
|
32
|
+
adapter: loaded.adapter,
|
|
33
|
+
trace: loaded.trace,
|
|
34
|
+
config: effectiveConfig,
|
|
35
|
+
conversationStore: loaded.conversationStore,
|
|
36
|
+
memoryIndex: loaded.memoryIndex || null,
|
|
37
|
+
amsRegistry: loaded.amsRegistry || null,
|
|
38
|
+
toolRegistry: loaded.toolRegistry,
|
|
39
|
+
skillManager: loaded.skillManager,
|
|
40
|
+
mcpManager: loaded.mcpManager,
|
|
41
|
+
yeaftDir: loaded.yeaftDir,
|
|
42
|
+
toolStats: loaded.toolStats || null,
|
|
43
|
+
taskManager: loaded.taskManager || null,
|
|
44
|
+
sessionId,
|
|
45
|
+
vpId,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Open a transport-neutral multi-VP runtime for an existing Yeaft Session.
|
|
51
|
+
* Different VPs run concurrently; each VP owns one serial promise tail and
|
|
52
|
+
* one Engine, matching the WebSocket runtime's state-isolation rule.
|
|
53
|
+
*
|
|
54
|
+
* Returns null when session metadata does not exist so legacy one-engine CLI
|
|
55
|
+
* invocations can keep their existing behavior.
|
|
56
|
+
*/
|
|
57
|
+
export function createCliSessionRunner({
|
|
58
|
+
loaded,
|
|
59
|
+
sessionId,
|
|
60
|
+
workDir = process.cwd(),
|
|
61
|
+
engineFactory = createEngine,
|
|
62
|
+
personaFactory = buildVpPersona,
|
|
63
|
+
} = {}) {
|
|
64
|
+
if (!loaded || !sessionId) return null;
|
|
65
|
+
const sessionDir = join(sessionsRoot(loaded.yeaftDir), sessionId);
|
|
66
|
+
if (!loadSessionMeta(sessionDir)) return null;
|
|
67
|
+
|
|
68
|
+
const handle = openSession(sessionsRoot(loaded.yeaftDir), sessionId);
|
|
69
|
+
const engines = new Map();
|
|
70
|
+
const tails = new Map();
|
|
71
|
+
const pending = new Set();
|
|
72
|
+
let closed = false;
|
|
73
|
+
|
|
74
|
+
const engineFor = (vpId) => {
|
|
75
|
+
let engine = engines.get(vpId);
|
|
76
|
+
if (!engine) {
|
|
77
|
+
engine = engineFactory(loaded, sessionId, vpId);
|
|
78
|
+
engines.set(vpId, engine);
|
|
79
|
+
}
|
|
80
|
+
return engine;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
let coordinator;
|
|
84
|
+
|
|
85
|
+
const runEnvelope = async (vpId, envelope, options) => {
|
|
86
|
+
const meta = handle.getMeta();
|
|
87
|
+
const engine = engineFor(vpId);
|
|
88
|
+
const promptText = envelope?.msg?.text || '';
|
|
89
|
+
const prompt = `@vp-${vpId} ${promptText}`;
|
|
90
|
+
const messages = loaded.conversationStore.loadSessionHistoryForVp(sessionId, vpId);
|
|
91
|
+
const todos = [];
|
|
92
|
+
let resultText = '';
|
|
93
|
+
let failed = null;
|
|
94
|
+
const scopedCoordinator = {
|
|
95
|
+
group: coordinator.group,
|
|
96
|
+
ingest(input, opts) {
|
|
97
|
+
return coordinator.ingest({ ...input, _cliTurnContext: envelope._cliTurnContext }, opts);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
const queryOptions = {
|
|
101
|
+
prompt,
|
|
102
|
+
messages,
|
|
103
|
+
sessionId,
|
|
104
|
+
workDir: meta.workDir || workDir,
|
|
105
|
+
senderVpId: vpId,
|
|
106
|
+
sessionMembers: meta.roster.slice(),
|
|
107
|
+
sessionAnnouncement: meta.announcement || '',
|
|
108
|
+
vpPersona: personaFactory(vpId, loaded),
|
|
109
|
+
router: createRouter({ coordinator: scopedCoordinator }),
|
|
110
|
+
inboundEnvelope: envelope,
|
|
111
|
+
userAlreadyPersisted: true,
|
|
112
|
+
threadId: 'main',
|
|
113
|
+
vpTurnId: envelope?.msg?.id || randomUUID(),
|
|
114
|
+
collabToolPolicy: meta.roster.length > 1
|
|
115
|
+
? COLLAB_TOOL_POLICY.MULTI_VP
|
|
116
|
+
: COLLAB_TOOL_POLICY.SINGLE_VP,
|
|
117
|
+
getCurrentTodos: () => todos.slice(),
|
|
118
|
+
setCurrentTodos: (next) => {
|
|
119
|
+
todos.splice(0, todos.length, ...(Array.isArray(next) ? next : []));
|
|
120
|
+
},
|
|
121
|
+
askUser: options.askUser
|
|
122
|
+
? request => options.askUser(request, vpId, queryOptions.vpTurnId)
|
|
123
|
+
: null,
|
|
124
|
+
userEffort: options.modelEffort || null,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
for await (const event of engine.query(queryOptions)) {
|
|
129
|
+
if (event.type === 'text_delta') resultText += event.text || '';
|
|
130
|
+
await options.onEvent?.({ vpId, event, sessionId, turnId: queryOptions.vpTurnId });
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
failed = error;
|
|
134
|
+
await options.onEvent?.({
|
|
135
|
+
vpId,
|
|
136
|
+
sessionId,
|
|
137
|
+
turnId: queryOptions.vpTurnId,
|
|
138
|
+
event: { type: 'error', error, retryable: false },
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return { vpId, result: resultText, error: failed };
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const enqueue = (vpId, envelope) => {
|
|
145
|
+
if (closed) throw new Error('CLI Session runner is closed');
|
|
146
|
+
const turnContext = envelope?._cliTurnContext;
|
|
147
|
+
if (!turnContext) throw new Error('CLI Session envelope is missing its turn context');
|
|
148
|
+
const previous = tails.get(vpId) || Promise.resolve();
|
|
149
|
+
const task = previous.catch(() => {}).then(() => runEnvelope(vpId, envelope, turnContext.options));
|
|
150
|
+
tails.set(vpId, task);
|
|
151
|
+
pending.add(task);
|
|
152
|
+
turnContext.pending.add(task);
|
|
153
|
+
task.finally(() => {
|
|
154
|
+
pending.delete(task);
|
|
155
|
+
turnContext.pending.delete(task);
|
|
156
|
+
if (tails.get(vpId) === task) tails.delete(vpId);
|
|
157
|
+
}).catch(() => {});
|
|
158
|
+
return task;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
coordinator = createCoordinator(handle, { deliver: enqueue });
|
|
162
|
+
|
|
163
|
+
async function drain(tasks = pending) {
|
|
164
|
+
const results = [];
|
|
165
|
+
while (tasks.size > 0) {
|
|
166
|
+
const batch = Array.from(tasks);
|
|
167
|
+
results.push(...await Promise.all(batch));
|
|
168
|
+
await Promise.resolve();
|
|
169
|
+
}
|
|
170
|
+
return results;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
sessionId,
|
|
175
|
+
get meta() { return handle.getMeta(); },
|
|
176
|
+
async run(prompt, options = {}) {
|
|
177
|
+
if (closed) throw new Error('CLI Session runner is closed');
|
|
178
|
+
const turnContext = Object.freeze({ options: Object.freeze({ ...options }), pending: new Set() });
|
|
179
|
+
const messageId = randomUUID();
|
|
180
|
+
// The shared user row is the durability boundary. Every VP Engine skips
|
|
181
|
+
// its own user append, preventing @all from duplicating the prompt N times.
|
|
182
|
+
loaded.conversationStore.append({
|
|
183
|
+
role: 'user',
|
|
184
|
+
content: prompt,
|
|
185
|
+
sessionId,
|
|
186
|
+
threadId: 'main',
|
|
187
|
+
clientMessageId: messageId,
|
|
188
|
+
userAuthored: true,
|
|
189
|
+
});
|
|
190
|
+
const report = coordinator.ingest({
|
|
191
|
+
id: messageId,
|
|
192
|
+
from: 'user',
|
|
193
|
+
role: 'user',
|
|
194
|
+
text: prompt,
|
|
195
|
+
_cliTurnContext: turnContext,
|
|
196
|
+
});
|
|
197
|
+
const results = await drain(turnContext.pending);
|
|
198
|
+
return { report, results };
|
|
199
|
+
},
|
|
200
|
+
abort(reason = 'user') {
|
|
201
|
+
let count = 0;
|
|
202
|
+
for (const engine of engines.values()) {
|
|
203
|
+
if (engine.abort?.(reason)) count += 1;
|
|
204
|
+
}
|
|
205
|
+
return count;
|
|
206
|
+
},
|
|
207
|
+
async close() {
|
|
208
|
+
closed = true;
|
|
209
|
+
await drain();
|
|
210
|
+
handle.close();
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
package/yeaft/cli.js
CHANGED
|
@@ -38,7 +38,8 @@ import { ConversationStore } from './conversation/persist.js';
|
|
|
38
38
|
import { snapshotSessions } from './sessions/session-crud.js';
|
|
39
39
|
import { loadSessionConfig, resolveSessionConfig } from './sessions/session-config.js';
|
|
40
40
|
import { validateSessionId } from './sessions/ids.js';
|
|
41
|
-
import { createJsonlWriter, JsonlInput, runStreamTurn } from './stdio-protocol.js';
|
|
41
|
+
import { createJsonlWriter, JsonlInput, runStreamTurn, runStreamSessionTurn } from './stdio-protocol.js';
|
|
42
|
+
import { createCliSessionRunner } from './cli-session-runner.js';
|
|
42
43
|
|
|
43
44
|
// ─── Argument parsing ──────────────────────────────────────────
|
|
44
45
|
|
|
@@ -333,16 +334,28 @@ function handleDeleteGroup(config, sessionId) {
|
|
|
333
334
|
// ─── REPL ──────────────────────────────────────────────────────
|
|
334
335
|
|
|
335
336
|
async function runREPL(config, args) {
|
|
336
|
-
|
|
337
|
+
const workDir = resolve(args.workDir || process.cwd());
|
|
338
|
+
const persisted = args.sessionId ? loadSessionConfig(config.dir, args.sessionId) : {};
|
|
339
|
+
const effectiveConfig = resolveSessionConfig(config, persisted);
|
|
337
340
|
const session = await loadSession({
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
+
dir: config.dir,
|
|
342
|
+
workDir,
|
|
343
|
+
model: args.model || effectiveConfig.model,
|
|
344
|
+
language: args.language || effectiveConfig.language,
|
|
345
|
+
debug: args.debug || effectiveConfig.debug,
|
|
341
346
|
skipMCP: args.skipMCP,
|
|
342
347
|
skipSkills: args.skipSkills,
|
|
348
|
+
configOverrides: effectiveConfig,
|
|
343
349
|
});
|
|
344
350
|
|
|
345
351
|
const { engine, conversationStore, trace, skillManager, mcpManager, toolRegistry } = session;
|
|
352
|
+
const sessionRunner = args.sessionId
|
|
353
|
+
? createCliSessionRunner({ loaded: session, sessionId: args.sessionId, workDir })
|
|
354
|
+
: null;
|
|
355
|
+
if (args.sessionId && !sessionRunner) {
|
|
356
|
+
await session.shutdown();
|
|
357
|
+
throw new Error(`Session not found: ${args.sessionId}`);
|
|
358
|
+
}
|
|
346
359
|
|
|
347
360
|
// Load persisted conversation as initial messages. `loadRecent` is now
|
|
348
361
|
// turn-based (one user round-trip = one turn; multi-VP fan-out collapses
|
|
@@ -655,9 +668,28 @@ async function runREPL(config, args) {
|
|
|
655
668
|
return;
|
|
656
669
|
}
|
|
657
670
|
|
|
658
|
-
// Regular input →
|
|
671
|
+
// Regular input → Session fan-out or legacy single Engine.
|
|
659
672
|
try {
|
|
660
673
|
let responseText = '';
|
|
674
|
+
if (sessionRunner) {
|
|
675
|
+
const outcome = await sessionRunner.run(input, {
|
|
676
|
+
modelEffort: args.modelEffort || session.config.modelEffort || null,
|
|
677
|
+
onEvent: ({ vpId, event }) => {
|
|
678
|
+
if (event.type === 'text_delta') {
|
|
679
|
+
responseText += event.text || '';
|
|
680
|
+
process.stdout.write(`\n[${vpId}] ${event.text || ''}`);
|
|
681
|
+
} else if (event.type === 'tool_start') {
|
|
682
|
+
process.stderr.write(`\n[${vpId}] ${event.name}...\n`);
|
|
683
|
+
} else if (event.type === 'error') {
|
|
684
|
+
process.stderr.write(`\n[${vpId}] Error: ${event.error?.message || event.error}\n`);
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
});
|
|
688
|
+
console.log();
|
|
689
|
+
if (outcome.results.some(result => result.error)) process.exitCode = 1;
|
|
690
|
+
rl.prompt();
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
661
693
|
|
|
662
694
|
for await (const event of engine.query({
|
|
663
695
|
prompt: input,
|
|
@@ -721,6 +753,7 @@ async function runREPL(config, args) {
|
|
|
721
753
|
});
|
|
722
754
|
|
|
723
755
|
rl.on('close', async () => {
|
|
756
|
+
await sessionRunner?.close();
|
|
724
757
|
await session.shutdown();
|
|
725
758
|
console.log('\nBye!');
|
|
726
759
|
process.exit(0);
|
|
@@ -766,6 +799,7 @@ async function runStreamJson(config, args) {
|
|
|
766
799
|
},
|
|
767
800
|
});
|
|
768
801
|
const { engine, conversationStore, skillManager, toolRegistry } = loaded;
|
|
802
|
+
const sessionRunner = createCliSessionRunner({ loaded, sessionId, workDir });
|
|
769
803
|
const todoState = { value: [] };
|
|
770
804
|
|
|
771
805
|
write({
|
|
@@ -788,8 +822,7 @@ async function runStreamJson(config, args) {
|
|
|
788
822
|
...(message.toolCallId && { toolCallId: message.toolCallId }),
|
|
789
823
|
...(message.toolCalls && { toolCalls: message.toolCalls }),
|
|
790
824
|
}));
|
|
791
|
-
|
|
792
|
-
engine,
|
|
825
|
+
const turnOptions = {
|
|
793
826
|
prompt,
|
|
794
827
|
messages: priorMessages,
|
|
795
828
|
sessionId,
|
|
@@ -800,7 +833,10 @@ async function runStreamJson(config, args) {
|
|
|
800
833
|
write,
|
|
801
834
|
getCurrentTodos: () => todoState.value.slice(),
|
|
802
835
|
setCurrentTodos: todos => { todoState.value = Array.isArray(todos) ? todos.slice() : []; },
|
|
803
|
-
}
|
|
836
|
+
};
|
|
837
|
+
return sessionRunner
|
|
838
|
+
? runStreamSessionTurn({ runner: sessionRunner, ...turnOptions })
|
|
839
|
+
: runStreamTurn({ engine, ...turnOptions });
|
|
804
840
|
};
|
|
805
841
|
|
|
806
842
|
let lastResult = null;
|
|
@@ -820,6 +856,7 @@ async function runStreamJson(config, args) {
|
|
|
820
856
|
}
|
|
821
857
|
} finally {
|
|
822
858
|
input?.close();
|
|
859
|
+
await sessionRunner?.close();
|
|
823
860
|
await loaded.shutdown();
|
|
824
861
|
console.log = originalConsole.log;
|
|
825
862
|
console.info = originalConsole.info;
|
|
@@ -836,18 +873,42 @@ async function runOnce(config, args) {
|
|
|
836
873
|
return;
|
|
837
874
|
}
|
|
838
875
|
|
|
839
|
-
|
|
876
|
+
const workDir = resolve(args.workDir || process.cwd());
|
|
877
|
+
const persisted = args.sessionId ? loadSessionConfig(config.dir, args.sessionId) : {};
|
|
878
|
+
const effectiveConfig = resolveSessionConfig(config, persisted);
|
|
840
879
|
const session = await loadSession({
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
880
|
+
dir: config.dir,
|
|
881
|
+
workDir,
|
|
882
|
+
model: args.model || effectiveConfig.model,
|
|
883
|
+
language: args.language || effectiveConfig.language,
|
|
884
|
+
debug: args.debug || effectiveConfig.debug,
|
|
844
885
|
skipMCP: args.skipMCP,
|
|
845
886
|
skipSkills: args.skipSkills,
|
|
887
|
+
configOverrides: effectiveConfig,
|
|
846
888
|
});
|
|
847
889
|
|
|
848
890
|
const { engine, conversationStore } = session;
|
|
891
|
+
const sessionRunner = args.sessionId
|
|
892
|
+
? createCliSessionRunner({ loaded: session, sessionId: args.sessionId, workDir })
|
|
893
|
+
: null;
|
|
849
894
|
|
|
850
895
|
try {
|
|
896
|
+
if (args.sessionId && !sessionRunner) {
|
|
897
|
+
throw new Error(`Session not found: ${args.sessionId}`);
|
|
898
|
+
}
|
|
899
|
+
if (sessionRunner) {
|
|
900
|
+
const outcome = await sessionRunner.run(args.prompt, {
|
|
901
|
+
modelEffort: args.modelEffort || session.config.modelEffort || null,
|
|
902
|
+
onEvent: ({ vpId, event }) => {
|
|
903
|
+
if (event.type === 'text_delta') process.stdout.write(`\n[${vpId}] ${event.text || ''}`);
|
|
904
|
+
else if (event.type === 'tool_start') process.stderr.write(`\n[${vpId}] ${event.name}...\n`);
|
|
905
|
+
else if (event.type === 'error') process.stderr.write(`\n[${vpId}] Error: ${event.error?.message || event.error}\n`);
|
|
906
|
+
},
|
|
907
|
+
});
|
|
908
|
+
console.log();
|
|
909
|
+
if (outcome.results.some(result => result.error)) process.exitCode = 1;
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
851
912
|
// Load recent conversation as context
|
|
852
913
|
const priorMessages = conversationStore.loadRecent(20).map(m => ({
|
|
853
914
|
role: m.role,
|
|
@@ -903,6 +964,7 @@ async function runOnce(config, args) {
|
|
|
903
964
|
// Final newline after streaming text
|
|
904
965
|
console.log();
|
|
905
966
|
} finally {
|
|
967
|
+
await sessionRunner?.close();
|
|
906
968
|
await session.shutdown();
|
|
907
969
|
}
|
|
908
970
|
}
|
package/yeaft/stdio-protocol.js
CHANGED
|
@@ -297,3 +297,167 @@ export async function runStreamTurn({
|
|
|
297
297
|
write(result);
|
|
298
298
|
return result;
|
|
299
299
|
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Run one prompt through the multi-VP CLI Session runner. Engine events keep
|
|
303
|
+
* the stream-json shape used by runStreamTurn and add `vp_id`, allowing
|
|
304
|
+
* concurrent VP streams to be demultiplexed without imposing output order.
|
|
305
|
+
*/
|
|
306
|
+
export async function runStreamSessionTurn({
|
|
307
|
+
runner,
|
|
308
|
+
prompt,
|
|
309
|
+
sessionId,
|
|
310
|
+
workDir = process.cwd(),
|
|
311
|
+
model = null,
|
|
312
|
+
modelEffort = null,
|
|
313
|
+
input = null,
|
|
314
|
+
write,
|
|
315
|
+
}) {
|
|
316
|
+
const clientTurnId = randomUUID();
|
|
317
|
+
const states = new Map();
|
|
318
|
+
let failed = null;
|
|
319
|
+
|
|
320
|
+
const stateFor = (vpId) => {
|
|
321
|
+
let state = states.get(vpId);
|
|
322
|
+
if (!state) {
|
|
323
|
+
state = {
|
|
324
|
+
turnId: clientTurnId,
|
|
325
|
+
resultText: '',
|
|
326
|
+
stopReason: 'end_turn',
|
|
327
|
+
usage: { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, cacheInputDeltaTokens: 0 },
|
|
328
|
+
};
|
|
329
|
+
states.set(vpId, state);
|
|
330
|
+
}
|
|
331
|
+
return state;
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const askUser = async ({ question, options }, vpId = null, turnId = clientTurnId) => {
|
|
335
|
+
const requestId = randomUUID();
|
|
336
|
+
write({
|
|
337
|
+
type: 'ask_user', subtype: 'request', request_id: requestId,
|
|
338
|
+
session_id: sessionId, turn_id: turnId, vp_id: vpId,
|
|
339
|
+
question, options: Array.isArray(options) ? options : [],
|
|
340
|
+
});
|
|
341
|
+
if (!input) throw new Error('AskUser requires --input-format stream-json');
|
|
342
|
+
const answers = await input.waitForAnswer(requestId);
|
|
343
|
+
write({
|
|
344
|
+
type: 'ask_user', subtype: 'response', request_id: requestId,
|
|
345
|
+
session_id: sessionId, turn_id: turnId, vp_id: vpId, answers,
|
|
346
|
+
});
|
|
347
|
+
return answers;
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const onEvent = async ({ vpId, event, turnId: fallbackTurnId }) => {
|
|
351
|
+
const state = stateFor(vpId);
|
|
352
|
+
if (event.type === 'turn_open' && event.turnId) state.turnId = event.turnId;
|
|
353
|
+
const turnId = state.turnId || fallbackTurnId || clientTurnId;
|
|
354
|
+
const base = { session_id: sessionId, turn_id: turnId, vp_id: vpId };
|
|
355
|
+
switch (event.type) {
|
|
356
|
+
case 'turn_open':
|
|
357
|
+
write({ ...event, type: 'turn', subtype: 'start', ...base, model, model_effort: modelEffort, cwd: workDir });
|
|
358
|
+
break;
|
|
359
|
+
case 'text_delta':
|
|
360
|
+
state.resultText += event.text || '';
|
|
361
|
+
write({ type: 'assistant', subtype: 'text_delta', ...base, delta: { type: 'text_delta', text: event.text || '' } });
|
|
362
|
+
break;
|
|
363
|
+
case 'thinking_delta':
|
|
364
|
+
write({ type: 'assistant', subtype: 'thinking_delta', ...base, delta: { type: 'thinking_delta', thinking: event.text || '' } });
|
|
365
|
+
break;
|
|
366
|
+
case 'skill_loaded':
|
|
367
|
+
write({ type: 'skill', subtype: 'loaded', ...base, skill: event.skill });
|
|
368
|
+
break;
|
|
369
|
+
case 'skill_error':
|
|
370
|
+
write({ type: 'skill', subtype: 'error', ...base, skill_name: event.skillName, error: event.message });
|
|
371
|
+
break;
|
|
372
|
+
case 'tool_call':
|
|
373
|
+
write({ type: 'assistant', subtype: 'tool_use', ...base, content: [{ type: 'tool_use', id: event.id, name: event.name, input: event.input }] });
|
|
374
|
+
if (event.name === 'TodoWrite') {
|
|
375
|
+
write({ type: 'todo', subtype: 'update', ...base, tool_use_id: event.id, todos: event.input?.todos || [] });
|
|
376
|
+
}
|
|
377
|
+
break;
|
|
378
|
+
case 'tool_start':
|
|
379
|
+
write({ type: 'tool', subtype: 'start', ...base, tool_use_id: event.id, name: event.name, input: event.input });
|
|
380
|
+
break;
|
|
381
|
+
case 'tool_end':
|
|
382
|
+
write({ type: 'tool', subtype: 'result', ...base, tool_use_id: event.id, name: event.name, content: event.output, is_error: !!event.isError });
|
|
383
|
+
break;
|
|
384
|
+
case 'usage': {
|
|
385
|
+
const cacheDelta = event.cacheTokensAreIncludedInInput ? 0 : (event.cacheReadTokens || 0) + (event.cacheWriteTokens || 0);
|
|
386
|
+
state.usage.inputTokens += event.inputTokens || 0;
|
|
387
|
+
state.usage.outputTokens += event.outputTokens || 0;
|
|
388
|
+
state.usage.cacheReadTokens += event.cacheReadTokens || 0;
|
|
389
|
+
state.usage.cacheWriteTokens += event.cacheWriteTokens || 0;
|
|
390
|
+
state.usage.cacheInputDeltaTokens += cacheDelta;
|
|
391
|
+
write({ type: 'usage', ...base, usage: snakeUsage(event) });
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
case 'stop':
|
|
395
|
+
case 'turn_end':
|
|
396
|
+
if (event.stopReason && TERMINAL_STOP_REASONS.has(event.stopReason)) state.stopReason = event.stopReason;
|
|
397
|
+
break;
|
|
398
|
+
case 'turn_close':
|
|
399
|
+
write({ type: 'turn', subtype: 'stop', ...base, duration_ms: event.totalMs, loop_count: event.loopCount, total_tokens: event.totalTokens });
|
|
400
|
+
break;
|
|
401
|
+
case 'error':
|
|
402
|
+
failed ||= event.error instanceof Error ? event.error : new Error(event.error?.message || String(event.error || 'Unknown error'));
|
|
403
|
+
write({ type: 'error', ...base, error: { name: failed.name, message: failed.message }, retryable: !!event.retryable });
|
|
404
|
+
break;
|
|
405
|
+
case 'fallback':
|
|
406
|
+
case 'llm_retry':
|
|
407
|
+
case 'memory_used':
|
|
408
|
+
case 'recall':
|
|
409
|
+
case 'consolidate':
|
|
410
|
+
case 'reflection':
|
|
411
|
+
write({ ...event, ...base });
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
let outcome;
|
|
417
|
+
try {
|
|
418
|
+
outcome = await runner.run(prompt, {
|
|
419
|
+
modelEffort,
|
|
420
|
+
onEvent,
|
|
421
|
+
askUser,
|
|
422
|
+
});
|
|
423
|
+
} catch (error) {
|
|
424
|
+
failed = error;
|
|
425
|
+
write({ type: 'error', session_id: sessionId, turn_id: clientTurnId, error: { name: error.name || 'Error', message: error.message || String(error) } });
|
|
426
|
+
outcome = { report: { dispatched: [] }, results: [] };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const perVp = Array.from(states, ([vpId, state]) => ({
|
|
430
|
+
vp_id: vpId,
|
|
431
|
+
turn_id: state.turnId,
|
|
432
|
+
stop_reason: state.stopReason,
|
|
433
|
+
result: state.resultText,
|
|
434
|
+
usage: {
|
|
435
|
+
input_tokens: state.usage.inputTokens,
|
|
436
|
+
output_tokens: state.usage.outputTokens,
|
|
437
|
+
cache_read_input_tokens: state.usage.cacheReadTokens,
|
|
438
|
+
cache_creation_input_tokens: state.usage.cacheWriteTokens,
|
|
439
|
+
total_input_tokens: state.usage.inputTokens + state.usage.cacheInputDeltaTokens,
|
|
440
|
+
total_tokens: state.usage.inputTokens + state.usage.cacheInputDeltaTokens + state.usage.outputTokens,
|
|
441
|
+
},
|
|
442
|
+
}));
|
|
443
|
+
const stopReasons = perVp.map(item => item.stop_reason);
|
|
444
|
+
const aggregateStopReason = failed || stopReasons.includes('error')
|
|
445
|
+
? 'error'
|
|
446
|
+
: stopReasons.length > 0 && stopReasons.every(reason => reason === 'aborted')
|
|
447
|
+
? 'aborted'
|
|
448
|
+
: 'end_turn';
|
|
449
|
+
const isError = !!failed || aggregateStopReason === 'error';
|
|
450
|
+
const result = {
|
|
451
|
+
type: 'result', subtype: isError ? 'error' : 'success',
|
|
452
|
+
session_id: sessionId, turn_id: clientTurnId,
|
|
453
|
+
model, model_effort: modelEffort,
|
|
454
|
+
stop_reason: aggregateStopReason,
|
|
455
|
+
is_error: isError,
|
|
456
|
+
result: perVp.map(item => item.result).filter(Boolean).join('\n'),
|
|
457
|
+
dispatched_vp_ids: outcome?.report?.dispatched || [],
|
|
458
|
+
vp_results: perVp,
|
|
459
|
+
...(failed ? { error: failed.message || String(failed) } : {}),
|
|
460
|
+
};
|
|
461
|
+
write(result);
|
|
462
|
+
return result;
|
|
463
|
+
}
|