@yeaft/webchat-agent 1.0.206 → 1.0.207
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.js +165 -7
- package/yeaft/engine.js +39 -6
- package/yeaft/sessions/ids.js +11 -0
- package/yeaft/stdio-protocol.js +299 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.0.
|
|
1
|
+
{"version":"1.0.207"}
|
package/package.json
CHANGED
package/yeaft/cli.js
CHANGED
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { createInterface } from 'readline';
|
|
27
|
+
import { randomUUID } from 'node:crypto';
|
|
28
|
+
import { resolve } from 'node:path';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
27
30
|
import { join } from 'path';
|
|
28
31
|
import { loadConfig } from './config.js';
|
|
29
32
|
import { DebugTrace } from './debug-trace.js';
|
|
@@ -33,10 +36,13 @@ import { buildSystemPrompt } from './prompts.js';
|
|
|
33
36
|
import { searchMessages } from './conversation/search.js';
|
|
34
37
|
import { ConversationStore } from './conversation/persist.js';
|
|
35
38
|
import { snapshotSessions } from './sessions/session-crud.js';
|
|
39
|
+
import { loadSessionConfig, resolveSessionConfig } from './sessions/session-config.js';
|
|
40
|
+
import { validateSessionId } from './sessions/ids.js';
|
|
41
|
+
import { createJsonlWriter, JsonlInput, runStreamTurn } from './stdio-protocol.js';
|
|
36
42
|
|
|
37
43
|
// ─── Argument parsing ──────────────────────────────────────────
|
|
38
44
|
|
|
39
|
-
function parseArgs(argv) {
|
|
45
|
+
export function parseArgs(argv) {
|
|
40
46
|
const args = {
|
|
41
47
|
debug: false,
|
|
42
48
|
interactive: false,
|
|
@@ -51,6 +57,12 @@ function parseArgs(argv) {
|
|
|
51
57
|
compactOrphans: false,
|
|
52
58
|
compactOrphansDry: false,
|
|
53
59
|
deleteSession: null,
|
|
60
|
+
sessionId: null,
|
|
61
|
+
workDir: null,
|
|
62
|
+
modelEffort: null,
|
|
63
|
+
inputFormat: 'text',
|
|
64
|
+
outputFormat: 'text',
|
|
65
|
+
print: false,
|
|
54
66
|
prompt: null,
|
|
55
67
|
};
|
|
56
68
|
|
|
@@ -75,6 +87,28 @@ function parseArgs(argv) {
|
|
|
75
87
|
case '--model':
|
|
76
88
|
args.model = rest[++i] || null;
|
|
77
89
|
break;
|
|
90
|
+
case '--effort':
|
|
91
|
+
case '--model-effort':
|
|
92
|
+
args.modelEffort = rest[++i] || null;
|
|
93
|
+
break;
|
|
94
|
+
case '--session-id':
|
|
95
|
+
case '--resume':
|
|
96
|
+
args.sessionId = rest[++i] || null;
|
|
97
|
+
break;
|
|
98
|
+
case '--cwd':
|
|
99
|
+
case '--work-dir':
|
|
100
|
+
args.workDir = rest[++i] || null;
|
|
101
|
+
break;
|
|
102
|
+
case '--input-format':
|
|
103
|
+
args.inputFormat = rest[++i] || 'text';
|
|
104
|
+
break;
|
|
105
|
+
case '--output-format':
|
|
106
|
+
args.outputFormat = rest[++i] || 'text';
|
|
107
|
+
break;
|
|
108
|
+
case '-p':
|
|
109
|
+
case '--print':
|
|
110
|
+
args.print = true;
|
|
111
|
+
break;
|
|
78
112
|
case '--language':
|
|
79
113
|
args.language = rest[++i] || null;
|
|
80
114
|
break;
|
|
@@ -693,6 +727,107 @@ async function runREPL(config, args) {
|
|
|
693
727
|
});
|
|
694
728
|
}
|
|
695
729
|
|
|
730
|
+
// ─── Structured stdio handler ──────────────────────────────────
|
|
731
|
+
|
|
732
|
+
async function runStreamJson(config, args) {
|
|
733
|
+
const sessionId = args.sessionId || `session_cli_${randomUUID()}`;
|
|
734
|
+
const validation = validateSessionId(sessionId);
|
|
735
|
+
if (!validation.ok) {
|
|
736
|
+
throw new Error(`Invalid --session-id (${validation.reason})`);
|
|
737
|
+
}
|
|
738
|
+
const workDir = resolve(args.workDir || process.cwd());
|
|
739
|
+
const persisted = args.sessionId ? loadSessionConfig(config.dir, sessionId) : {};
|
|
740
|
+
const effectiveConfig = resolveSessionConfig(config, persisted);
|
|
741
|
+
if (args.model) {
|
|
742
|
+
effectiveConfig.model = args.model;
|
|
743
|
+
effectiveConfig.primaryModel = args.model;
|
|
744
|
+
}
|
|
745
|
+
if (args.modelEffort) effectiveConfig.modelEffort = args.modelEffort;
|
|
746
|
+
|
|
747
|
+
const write = createJsonlWriter(process.stdout);
|
|
748
|
+
const input = args.inputFormat === 'stream-json' ? new JsonlInput(process.stdin) : null;
|
|
749
|
+
const originalConsole = { log: console.log, info: console.info, debug: console.debug };
|
|
750
|
+
const writeDiagnostic = (...values) => console.error(...values);
|
|
751
|
+
console.log = writeDiagnostic;
|
|
752
|
+
console.info = writeDiagnostic;
|
|
753
|
+
console.debug = writeDiagnostic;
|
|
754
|
+
|
|
755
|
+
const loaded = await loadSession({
|
|
756
|
+
dir: config.dir,
|
|
757
|
+
workDir,
|
|
758
|
+
model: effectiveConfig.model,
|
|
759
|
+
language: args.language || effectiveConfig.language,
|
|
760
|
+
debug: args.debug || effectiveConfig.debug,
|
|
761
|
+
skipMCP: args.skipMCP,
|
|
762
|
+
skipSkills: args.skipSkills,
|
|
763
|
+
configOverrides: {
|
|
764
|
+
...effectiveConfig,
|
|
765
|
+
...(effectiveConfig.modelEffort ? { modelEffort: effectiveConfig.modelEffort } : {}),
|
|
766
|
+
},
|
|
767
|
+
});
|
|
768
|
+
const { engine, conversationStore, skillManager, toolRegistry } = loaded;
|
|
769
|
+
const todoState = { value: [] };
|
|
770
|
+
|
|
771
|
+
write({
|
|
772
|
+
type: 'system',
|
|
773
|
+
subtype: 'init',
|
|
774
|
+
session_id: sessionId,
|
|
775
|
+
model: loaded.config.model,
|
|
776
|
+
model_effort: loaded.config.modelEffort || null,
|
|
777
|
+
cwd: workDir,
|
|
778
|
+
tools: toolRegistry.names,
|
|
779
|
+
skills: skillManager.list(),
|
|
780
|
+
input_format: args.inputFormat,
|
|
781
|
+
output_format: 'stream-json',
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
const runPrompt = async (prompt) => {
|
|
785
|
+
const priorMessages = conversationStore.loadRecentBySession(sessionId, 20).map(message => ({
|
|
786
|
+
role: message.role,
|
|
787
|
+
content: message.content,
|
|
788
|
+
...(message.toolCallId && { toolCallId: message.toolCallId }),
|
|
789
|
+
...(message.toolCalls && { toolCalls: message.toolCalls }),
|
|
790
|
+
}));
|
|
791
|
+
return runStreamTurn({
|
|
792
|
+
engine,
|
|
793
|
+
prompt,
|
|
794
|
+
messages: priorMessages,
|
|
795
|
+
sessionId,
|
|
796
|
+
workDir,
|
|
797
|
+
model: loaded.config.model,
|
|
798
|
+
modelEffort: loaded.config.modelEffort || null,
|
|
799
|
+
input,
|
|
800
|
+
write,
|
|
801
|
+
getCurrentTodos: () => todoState.value.slice(),
|
|
802
|
+
setCurrentTodos: todos => { todoState.value = Array.isArray(todos) ? todos.slice() : []; },
|
|
803
|
+
});
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
let lastResult = null;
|
|
807
|
+
try {
|
|
808
|
+
if (args.prompt) {
|
|
809
|
+
lastResult = await runPrompt(args.prompt);
|
|
810
|
+
} else if (input) {
|
|
811
|
+
for (;;) {
|
|
812
|
+
const item = await input.nextPrompt();
|
|
813
|
+
if (!item) break;
|
|
814
|
+
lastResult = await runPrompt(item.prompt);
|
|
815
|
+
}
|
|
816
|
+
} else {
|
|
817
|
+
let prompt = '';
|
|
818
|
+
for await (const chunk of process.stdin) prompt += chunk;
|
|
819
|
+
if (prompt.trim()) lastResult = await runPrompt(prompt.trim());
|
|
820
|
+
}
|
|
821
|
+
} finally {
|
|
822
|
+
input?.close();
|
|
823
|
+
await loaded.shutdown();
|
|
824
|
+
console.log = originalConsole.log;
|
|
825
|
+
console.info = originalConsole.info;
|
|
826
|
+
console.debug = originalConsole.debug;
|
|
827
|
+
}
|
|
828
|
+
if (lastResult?.is_error) process.exitCode = 1;
|
|
829
|
+
}
|
|
830
|
+
|
|
696
831
|
// ─── One-shot handler ──────────────────────────────────────────
|
|
697
832
|
|
|
698
833
|
async function runOnce(config, args) {
|
|
@@ -819,6 +954,20 @@ async function main() {
|
|
|
819
954
|
return;
|
|
820
955
|
}
|
|
821
956
|
|
|
957
|
+
if (args.outputFormat === 'stream-json') {
|
|
958
|
+
if (args.inputFormat !== 'text' && args.inputFormat !== 'stream-json') {
|
|
959
|
+
throw new Error('--input-format must be text or stream-json');
|
|
960
|
+
}
|
|
961
|
+
if (!args.prompt && args.inputFormat !== 'stream-json' && process.stdin.isTTY) {
|
|
962
|
+
throw new Error('stream-json output requires a prompt, piped stdin, or --input-format stream-json');
|
|
963
|
+
}
|
|
964
|
+
await runStreamJson(config, args);
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
if (args.inputFormat === 'stream-json') {
|
|
968
|
+
throw new Error('--input-format stream-json requires --output-format stream-json');
|
|
969
|
+
}
|
|
970
|
+
|
|
822
971
|
// Handle prompt (from args or stdin)
|
|
823
972
|
if (args.prompt) {
|
|
824
973
|
await runOnce(config, args);
|
|
@@ -856,15 +1005,24 @@ async function main() {
|
|
|
856
1005
|
console.log(' -d, --debug Enable debug tracing');
|
|
857
1006
|
console.log(' -i, --interactive Start REPL');
|
|
858
1007
|
console.log(' -v, --verbose Verbose output');
|
|
859
|
-
console.log(' --
|
|
860
|
-
console.log(' --
|
|
1008
|
+
console.log(' -p, --print Run a non-interactive query');
|
|
1009
|
+
console.log(' --session-id <id> Persist and resume a Yeaft Session');
|
|
1010
|
+
console.log(' --cwd <dir> Set the working directory');
|
|
1011
|
+
console.log(' --model <name> Override model');
|
|
1012
|
+
console.log(' --effort <level> Override model effort');
|
|
1013
|
+
console.log(' --input-format <fmt> text or stream-json');
|
|
1014
|
+
console.log(' --output-format <fmt> text or stream-json');
|
|
1015
|
+
console.log(' --language <code> Language: en, zh (default: en)');
|
|
861
1016
|
console.log(' --trace <cmd> Query debug trace');
|
|
862
1017
|
console.log(' --dry-run Show prompt without calling LLM');
|
|
863
1018
|
console.log(' --skip-mcp Skip MCP server connections');
|
|
864
1019
|
console.log(' --skip-skills Skip skill loading');
|
|
865
1020
|
}
|
|
866
1021
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1022
|
+
const isDirectRun = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
1023
|
+
if (isDirectRun) {
|
|
1024
|
+
main().catch(err => {
|
|
1025
|
+
console.error(err);
|
|
1026
|
+
process.exit(1);
|
|
1027
|
+
});
|
|
1028
|
+
}
|
package/yeaft/engine.js
CHANGED
|
@@ -1086,12 +1086,12 @@ export class Engine {
|
|
|
1086
1086
|
* @param {string} [args.explicitSkillName] — leading /skill:<name> command, if present
|
|
1087
1087
|
* @returns {string}
|
|
1088
1088
|
*/
|
|
1089
|
-
#buildSystemPrompt({ prompt, memoryInjection, vpPersona, activeScope, sessionAnnouncement, workCenterInstructions, projectDoc, taskCtx, activeTasks, explicitSkillName } = {}) {
|
|
1090
|
-
//
|
|
1091
|
-
//
|
|
1092
|
-
//
|
|
1093
|
-
let skillContent = '';
|
|
1094
|
-
if (this.#skillManager) {
|
|
1089
|
+
#buildSystemPrompt({ prompt, memoryInjection, vpPersona, activeScope, sessionAnnouncement, workCenterInstructions, projectDoc, taskCtx, activeTasks, explicitSkillName, resolvedSkillContent = null } = {}) {
|
|
1090
|
+
// Skill selection is normally resolved once by #runQuery so the prompt and
|
|
1091
|
+
// emitted protocol events describe the exact same skills. Keep the local
|
|
1092
|
+
// fallback for internal callers that do not need selection events.
|
|
1093
|
+
let skillContent = typeof resolvedSkillContent === 'string' ? resolvedSkillContent : '';
|
|
1094
|
+
if (resolvedSkillContent === null && this.#skillManager) {
|
|
1095
1095
|
if (explicitSkillName) {
|
|
1096
1096
|
skillContent = this.#skillManager.getPromptContent(explicitSkillName)
|
|
1097
1097
|
|| `## Skill command error\n\nRequested skill "${explicitSkillName}" was not found. Continue without that skill and tell the user it is unavailable.`;
|
|
@@ -2004,6 +2004,32 @@ export class Engine {
|
|
|
2004
2004
|
const activeTasks = this.#taskManager
|
|
2005
2005
|
? this.#taskManager.renderActiveTasksForPrompt(runtimeSessionId)
|
|
2006
2006
|
: '';
|
|
2007
|
+
let resolvedSkillContent = '';
|
|
2008
|
+
let resolvedSkills = [];
|
|
2009
|
+
let skillResolutionError = null;
|
|
2010
|
+
if (this.#skillManager) {
|
|
2011
|
+
if (explicitSkillName) {
|
|
2012
|
+
resolvedSkillContent = this.#skillManager.getPromptContent(explicitSkillName);
|
|
2013
|
+
const skill = this.#skillManager.list?.().find(item => item.name === explicitSkillName)
|
|
2014
|
+
|| (resolvedSkillContent ? { name: explicitSkillName } : null);
|
|
2015
|
+
if (resolvedSkillContent && skill) {
|
|
2016
|
+
resolvedSkills = [{ ...skill, explicit: true }];
|
|
2017
|
+
} else {
|
|
2018
|
+
skillResolutionError = `Requested skill "${explicitSkillName}" was not found.`;
|
|
2019
|
+
resolvedSkillContent = `## Skill command error\n\n${skillResolutionError} Continue without that skill and tell the user it is unavailable.`;
|
|
2020
|
+
}
|
|
2021
|
+
} else if (prompt && typeof this.#skillManager.findRelevant === 'function') {
|
|
2022
|
+
resolvedSkills = this.#skillManager.findRelevant(prompt).map(skill => ({
|
|
2023
|
+
name: skill.name,
|
|
2024
|
+
description: skill.description || '',
|
|
2025
|
+
trigger: skill.trigger || '',
|
|
2026
|
+
category: skill.category,
|
|
2027
|
+
tier: skill._tier,
|
|
2028
|
+
explicit: false,
|
|
2029
|
+
}));
|
|
2030
|
+
resolvedSkillContent = resolvedSkills.map(skill => this.#skillManager.getPromptContent(skill.name)).join('\n\n');
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2007
2033
|
|
|
2008
2034
|
const systemPrompt = this.#buildSystemPrompt({
|
|
2009
2035
|
prompt,
|
|
@@ -2015,6 +2041,7 @@ export class Engine {
|
|
|
2015
2041
|
projectDoc,
|
|
2016
2042
|
activeTasks,
|
|
2017
2043
|
explicitSkillName,
|
|
2044
|
+
resolvedSkillContent,
|
|
2018
2045
|
});
|
|
2019
2046
|
|
|
2020
2047
|
// ─── HARD INVARIANT: Compact ≠ Dream (read DESIGN-COMPACT-VS-DREAM.md) ─
|
|
@@ -2184,6 +2211,12 @@ export class Engine {
|
|
|
2184
2211
|
sessionId: sessionId || null,
|
|
2185
2212
|
at: queryStartedAt,
|
|
2186
2213
|
};
|
|
2214
|
+
for (const skill of resolvedSkills) {
|
|
2215
|
+
yield { type: 'skill_loaded', turnId: queryTurnId, skill };
|
|
2216
|
+
}
|
|
2217
|
+
if (skillResolutionError) {
|
|
2218
|
+
yield { type: 'skill_error', turnId: queryTurnId, skillName: explicitSkillName, message: skillResolutionError };
|
|
2219
|
+
}
|
|
2187
2220
|
|
|
2188
2221
|
// Surface memory recall to the debug panel right after turn_open.
|
|
2189
2222
|
// recallResult was loaded above; emit a structured `memory_used`
|
package/yeaft/sessions/ids.js
CHANGED
|
@@ -49,6 +49,17 @@ export function nextSessionId(slug = 'default') {
|
|
|
49
49
|
return `session_${safe}_${randEncoded(8)}`;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
const SESSION_ID_RE = /^session_[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
53
|
+
const SESSION_ID_MAX_LEN = 128;
|
|
54
|
+
|
|
55
|
+
/** Validate an external Session ID before it reaches any filesystem-backed subsystem. */
|
|
56
|
+
export function validateSessionId(id) {
|
|
57
|
+
if (!id || typeof id !== 'string') return { ok: false, reason: 'empty_or_non_string' };
|
|
58
|
+
if (id.length > SESSION_ID_MAX_LEN) return { ok: false, reason: 'too_long' };
|
|
59
|
+
if (!SESSION_ID_RE.test(id)) return { ok: false, reason: 'invalid_shape' };
|
|
60
|
+
return { ok: true };
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
/**
|
|
53
64
|
* Reserved vpIds that must never be used as actual VP identifiers — they
|
|
54
65
|
* collide with coordinator-level sentinels (`@all` broadcast, `user`/`system`
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
const TERMINAL_STOP_REASONS = new Set(['end_turn', 'max_tokens', 'stop_sequence', 'aborted', 'error']);
|
|
5
|
+
|
|
6
|
+
function snakeUsage(usage) {
|
|
7
|
+
const inputTokens = usage.inputTokens || 0;
|
|
8
|
+
const outputTokens = usage.outputTokens || 0;
|
|
9
|
+
const cacheReadTokens = usage.cacheReadTokens || 0;
|
|
10
|
+
const cacheWriteTokens = usage.cacheWriteTokens || 0;
|
|
11
|
+
const cacheDelta = usage.cacheTokensAreIncludedInInput ? 0 : cacheReadTokens + cacheWriteTokens;
|
|
12
|
+
return {
|
|
13
|
+
input_tokens: inputTokens,
|
|
14
|
+
output_tokens: outputTokens,
|
|
15
|
+
cache_read_input_tokens: cacheReadTokens,
|
|
16
|
+
cache_creation_input_tokens: cacheWriteTokens,
|
|
17
|
+
total_input_tokens: inputTokens + cacheDelta,
|
|
18
|
+
total_tokens: inputTokens + cacheDelta + outputTokens,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function extractPrompt(message) {
|
|
23
|
+
if (!message || typeof message !== 'object') return '';
|
|
24
|
+
if (typeof message.prompt === 'string') return message.prompt;
|
|
25
|
+
if (typeof message.text === 'string') return message.text;
|
|
26
|
+
const content = message.message?.content ?? message.content;
|
|
27
|
+
if (typeof content === 'string') return content;
|
|
28
|
+
if (!Array.isArray(content)) return '';
|
|
29
|
+
return content
|
|
30
|
+
.filter(block => block?.type === 'text' && typeof block.text === 'string')
|
|
31
|
+
.map(block => block.text)
|
|
32
|
+
.join('');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createJsonlWriter(output = process.stdout) {
|
|
36
|
+
return (event) => {
|
|
37
|
+
output.write(`${JSON.stringify(event)}\n`);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class JsonlInput {
|
|
42
|
+
#rl;
|
|
43
|
+
#prompts = [];
|
|
44
|
+
#promptWaiters = [];
|
|
45
|
+
#answers = new Map();
|
|
46
|
+
#answerWaiters = new Map();
|
|
47
|
+
#answerRequestIds = new Set();
|
|
48
|
+
#closed = false;
|
|
49
|
+
#error = null;
|
|
50
|
+
|
|
51
|
+
constructor(input = process.stdin) {
|
|
52
|
+
this.#rl = createInterface({ input, crlfDelay: Infinity, terminal: false });
|
|
53
|
+
this.#rl.on('line', line => this.#acceptLine(line));
|
|
54
|
+
this.#rl.on('close', () => this.#close());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#acceptLine(line) {
|
|
58
|
+
const trimmed = line.trim();
|
|
59
|
+
if (!trimmed) return;
|
|
60
|
+
let message;
|
|
61
|
+
try {
|
|
62
|
+
message = JSON.parse(trimmed);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
this.#error = new Error(`Invalid stream-json input: ${err.message}`);
|
|
65
|
+
this.#close();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const type = message.type || '';
|
|
69
|
+
if (type === 'ask_user_response' || type === 'user_response') {
|
|
70
|
+
const requestId = message.request_id || message.requestId;
|
|
71
|
+
if (!requestId) return;
|
|
72
|
+
const value = message.answers ?? message.answer ?? message.response ?? {};
|
|
73
|
+
const waiter = this.#answerWaiters.get(requestId);
|
|
74
|
+
if (waiter) {
|
|
75
|
+
this.#answerWaiters.delete(requestId);
|
|
76
|
+
waiter.resolve(value);
|
|
77
|
+
} else if (this.#answerRequestIds.has(requestId)) {
|
|
78
|
+
this.#error = new Error(`Duplicate AskUser response request_id: ${requestId}`);
|
|
79
|
+
this.#close();
|
|
80
|
+
} else {
|
|
81
|
+
this.#answerRequestIds.add(requestId);
|
|
82
|
+
this.#answers.set(requestId, value);
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (type === 'user' || type === 'prompt' || message.prompt !== undefined) {
|
|
87
|
+
const prompt = extractPrompt(message);
|
|
88
|
+
if (!prompt) return;
|
|
89
|
+
const item = { prompt, message };
|
|
90
|
+
const waiter = this.#promptWaiters.shift();
|
|
91
|
+
if (waiter) waiter.resolve(item);
|
|
92
|
+
else this.#prompts.push(item);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#close() {
|
|
97
|
+
if (this.#closed) return;
|
|
98
|
+
this.#closed = true;
|
|
99
|
+
for (const waiter of this.#promptWaiters.splice(0)) {
|
|
100
|
+
if (this.#error) waiter.reject(this.#error);
|
|
101
|
+
else waiter.resolve(null);
|
|
102
|
+
}
|
|
103
|
+
for (const waiter of this.#answerWaiters.values()) {
|
|
104
|
+
waiter.reject(this.#error || new Error('stdin closed before AskUser response'));
|
|
105
|
+
}
|
|
106
|
+
this.#answerWaiters.clear();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async nextPrompt() {
|
|
110
|
+
if (this.#prompts.length) return this.#prompts.shift();
|
|
111
|
+
if (this.#error) throw this.#error;
|
|
112
|
+
if (this.#closed) return null;
|
|
113
|
+
return new Promise((resolve, reject) => this.#promptWaiters.push({ resolve, reject }));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async waitForAnswer(requestId) {
|
|
117
|
+
if (!requestId || typeof requestId !== 'string') {
|
|
118
|
+
throw new Error('AskUser request_id must be a non-empty string');
|
|
119
|
+
}
|
|
120
|
+
if (this.#error) throw this.#error;
|
|
121
|
+
if (this.#answers.has(requestId)) {
|
|
122
|
+
const value = this.#answers.get(requestId);
|
|
123
|
+
this.#answers.delete(requestId);
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
if (this.#answerRequestIds.has(requestId)) {
|
|
127
|
+
throw new Error(`Duplicate AskUser request_id: ${requestId}`);
|
|
128
|
+
}
|
|
129
|
+
if (this.#error) throw this.#error;
|
|
130
|
+
if (this.#closed) throw new Error('stdin closed before AskUser response');
|
|
131
|
+
this.#answerRequestIds.add(requestId);
|
|
132
|
+
return new Promise((resolve, reject) => this.#answerWaiters.set(requestId, { resolve, reject }));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
close() {
|
|
136
|
+
this.#rl.close();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function runStreamTurn({
|
|
141
|
+
engine,
|
|
142
|
+
prompt,
|
|
143
|
+
messages = [],
|
|
144
|
+
sessionId = null,
|
|
145
|
+
workDir = process.cwd(),
|
|
146
|
+
model = null,
|
|
147
|
+
modelEffort = null,
|
|
148
|
+
input = null,
|
|
149
|
+
write,
|
|
150
|
+
getCurrentTodos = null,
|
|
151
|
+
setCurrentTodos = null,
|
|
152
|
+
}) {
|
|
153
|
+
const clientTurnId = randomUUID();
|
|
154
|
+
let engineTurnId = null;
|
|
155
|
+
let resultText = '';
|
|
156
|
+
let stopReason = 'end_turn';
|
|
157
|
+
let failed = null;
|
|
158
|
+
const usage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, cacheInputDeltaTokens: 0 };
|
|
159
|
+
|
|
160
|
+
const askUser = async ({ question, options }) => {
|
|
161
|
+
const requestId = randomUUID();
|
|
162
|
+
write({
|
|
163
|
+
type: 'ask_user',
|
|
164
|
+
subtype: 'request',
|
|
165
|
+
request_id: requestId,
|
|
166
|
+
session_id: sessionId,
|
|
167
|
+
turn_id: engineTurnId || clientTurnId,
|
|
168
|
+
question,
|
|
169
|
+
options: Array.isArray(options) ? options : [],
|
|
170
|
+
});
|
|
171
|
+
if (!input) throw new Error('AskUser requires --input-format stream-json');
|
|
172
|
+
const answers = await input.waitForAnswer(requestId);
|
|
173
|
+
write({
|
|
174
|
+
type: 'ask_user',
|
|
175
|
+
subtype: 'response',
|
|
176
|
+
request_id: requestId,
|
|
177
|
+
session_id: sessionId,
|
|
178
|
+
turn_id: engineTurnId || clientTurnId,
|
|
179
|
+
answers,
|
|
180
|
+
});
|
|
181
|
+
return answers;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
for await (const event of engine.query({
|
|
186
|
+
prompt,
|
|
187
|
+
messages,
|
|
188
|
+
sessionId,
|
|
189
|
+
workDir,
|
|
190
|
+
userEffort: modelEffort,
|
|
191
|
+
askUser,
|
|
192
|
+
getCurrentTodos,
|
|
193
|
+
setCurrentTodos,
|
|
194
|
+
})) {
|
|
195
|
+
if (event.type === 'turn_open') engineTurnId = event.turnId;
|
|
196
|
+
const turnId = engineTurnId || clientTurnId;
|
|
197
|
+
switch (event.type) {
|
|
198
|
+
case 'turn_open':
|
|
199
|
+
write({ ...event, type: 'turn', subtype: 'start', session_id: sessionId, turn_id: event.turnId, model, model_effort: modelEffort, cwd: workDir });
|
|
200
|
+
break;
|
|
201
|
+
case 'text_delta':
|
|
202
|
+
resultText += event.text || '';
|
|
203
|
+
write({ type: 'assistant', subtype: 'text_delta', session_id: sessionId, turn_id: turnId, delta: { type: 'text_delta', text: event.text || '' } });
|
|
204
|
+
break;
|
|
205
|
+
case 'thinking_delta':
|
|
206
|
+
write({ type: 'assistant', subtype: 'thinking_delta', session_id: sessionId, turn_id: turnId, delta: { type: 'thinking_delta', thinking: event.text || '' } });
|
|
207
|
+
break;
|
|
208
|
+
case 'skill_loaded':
|
|
209
|
+
write({ type: 'skill', subtype: 'loaded', session_id: sessionId, turn_id: turnId, skill: event.skill });
|
|
210
|
+
break;
|
|
211
|
+
case 'skill_error':
|
|
212
|
+
write({ type: 'skill', subtype: 'error', session_id: sessionId, turn_id: turnId, skill_name: event.skillName, error: event.message });
|
|
213
|
+
break;
|
|
214
|
+
case 'tool_call':
|
|
215
|
+
write({ type: 'assistant', subtype: 'tool_use', session_id: sessionId, turn_id: turnId, content: [{ type: 'tool_use', id: event.id, name: event.name, input: event.input }] });
|
|
216
|
+
if (event.name === 'TodoWrite') {
|
|
217
|
+
write({ type: 'todo', subtype: 'update', session_id: sessionId, turn_id: turnId, tool_use_id: event.id, todos: event.input?.todos || [] });
|
|
218
|
+
}
|
|
219
|
+
break;
|
|
220
|
+
case 'tool_start':
|
|
221
|
+
write({ type: 'tool', subtype: 'start', session_id: sessionId, turn_id: turnId, tool_use_id: event.id, name: event.name, input: event.input });
|
|
222
|
+
break;
|
|
223
|
+
case 'tool_end':
|
|
224
|
+
write({
|
|
225
|
+
type: 'tool',
|
|
226
|
+
subtype: 'result',
|
|
227
|
+
session_id: sessionId,
|
|
228
|
+
turn_id: turnId,
|
|
229
|
+
tool_use_id: event.id,
|
|
230
|
+
name: event.name,
|
|
231
|
+
content: event.output,
|
|
232
|
+
is_error: !!event.isError,
|
|
233
|
+
...(Array.isArray(event.displayImages) && event.displayImages.length > 0
|
|
234
|
+
? { display_images: event.displayImages }
|
|
235
|
+
: {}),
|
|
236
|
+
});
|
|
237
|
+
break;
|
|
238
|
+
case 'usage': {
|
|
239
|
+
const cacheDelta = event.cacheTokensAreIncludedInInput ? 0 : (event.cacheReadTokens || 0) + (event.cacheWriteTokens || 0);
|
|
240
|
+
usage.inputTokens += event.inputTokens || 0;
|
|
241
|
+
usage.outputTokens += event.outputTokens || 0;
|
|
242
|
+
usage.cacheReadTokens += event.cacheReadTokens || 0;
|
|
243
|
+
usage.cacheWriteTokens += event.cacheWriteTokens || 0;
|
|
244
|
+
usage.cacheInputDeltaTokens += cacheDelta;
|
|
245
|
+
write({ type: 'usage', session_id: sessionId, turn_id: turnId, usage: snakeUsage(event) });
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
case 'stop':
|
|
249
|
+
if (event.stopReason) stopReason = event.stopReason;
|
|
250
|
+
break;
|
|
251
|
+
case 'turn_end':
|
|
252
|
+
if (event.stopReason && TERMINAL_STOP_REASONS.has(event.stopReason)) stopReason = event.stopReason;
|
|
253
|
+
break;
|
|
254
|
+
case 'turn_close':
|
|
255
|
+
write({ type: 'turn', subtype: 'stop', session_id: sessionId, turn_id: turnId, duration_ms: event.totalMs, loop_count: event.loopCount, total_tokens: event.totalTokens });
|
|
256
|
+
break;
|
|
257
|
+
case 'error':
|
|
258
|
+
failed = event.error instanceof Error ? event.error : new Error(event.error?.message || String(event.error || 'Unknown error'));
|
|
259
|
+
write({ type: 'error', session_id: sessionId, turn_id: turnId, error: { name: failed.name, message: failed.message }, retryable: !!event.retryable });
|
|
260
|
+
break;
|
|
261
|
+
case 'fallback':
|
|
262
|
+
case 'llm_retry':
|
|
263
|
+
case 'memory_used':
|
|
264
|
+
case 'recall':
|
|
265
|
+
case 'consolidate':
|
|
266
|
+
case 'reflection':
|
|
267
|
+
write({ ...event, session_id: sessionId, turn_id: turnId });
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
} catch (err) {
|
|
272
|
+
failed = err;
|
|
273
|
+
write({ type: 'error', session_id: sessionId, turn_id: engineTurnId || clientTurnId, error: { name: err.name || 'Error', message: err.message || String(err) } });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const finalUsage = {
|
|
277
|
+
input_tokens: usage.inputTokens,
|
|
278
|
+
output_tokens: usage.outputTokens,
|
|
279
|
+
cache_read_input_tokens: usage.cacheReadTokens,
|
|
280
|
+
cache_creation_input_tokens: usage.cacheWriteTokens,
|
|
281
|
+
total_input_tokens: usage.inputTokens + usage.cacheInputDeltaTokens,
|
|
282
|
+
total_tokens: usage.inputTokens + usage.cacheInputDeltaTokens + usage.outputTokens,
|
|
283
|
+
};
|
|
284
|
+
const result = {
|
|
285
|
+
type: 'result',
|
|
286
|
+
subtype: failed ? 'error' : 'success',
|
|
287
|
+
session_id: sessionId,
|
|
288
|
+
turn_id: engineTurnId || clientTurnId,
|
|
289
|
+
model,
|
|
290
|
+
model_effort: modelEffort,
|
|
291
|
+
stop_reason: failed ? 'error' : stopReason,
|
|
292
|
+
is_error: !!failed,
|
|
293
|
+
result: resultText,
|
|
294
|
+
usage: finalUsage,
|
|
295
|
+
...(failed ? { error: failed.message || String(failed) } : {}),
|
|
296
|
+
};
|
|
297
|
+
write(result);
|
|
298
|
+
return result;
|
|
299
|
+
}
|