codemini-cli 0.4.6 → 0.4.8
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/deployment.md +5 -5
- package/package.json +1 -1
- package/src/cli.js +5 -5
- package/src/commands/chat.js +8 -2
- package/src/commands/run.js +9 -3
- package/src/commands/skill.js +1 -1
- package/src/core/agent-loop.js +53 -13
- package/src/core/chat-runtime.js +622 -83
- package/src/core/config-store.js +9 -2
- package/src/core/fff-adapter.js +1 -1
- package/src/core/session-store.js +104 -9
- package/src/core/soul.js +13 -0
- package/templates/project-requirements/report-shell.html +2 -1
package/src/core/chat-runtime.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from './provider/index.js';
|
|
13
13
|
import { isDangerousCommand, runShellCommand } from './shell.js';
|
|
14
14
|
import { getBuiltinTools } from './tools.js';
|
|
15
|
-
import { createSession, listSessions, loadSession, pruneSessions, saveSession } from './session-store.js';
|
|
15
|
+
import { createSession, deriveSessionTitle, listSessions, loadSession, pruneSessions, saveSession } from './session-store.js';
|
|
16
16
|
import { getConfigValue, loadConfig, resetConfig, setConfigValue } from './config-store.js';
|
|
17
17
|
import { evaluateCommandPolicy } from './command-policy.js';
|
|
18
18
|
import { appendInputHistory, loadInputHistory } from './input-history-store.js';
|
|
@@ -55,7 +55,7 @@ function toOpenAIMessages(sessionMessages) {
|
|
|
55
55
|
}
|
|
56
56
|
mapped.push({
|
|
57
57
|
role: msg.role,
|
|
58
|
-
content: msg.content,
|
|
58
|
+
content: typeof msg.model_content === 'string' && msg.model_content ? msg.model_content : msg.content,
|
|
59
59
|
...(typeof msg.reasoning_content === 'string' && msg.reasoning_content ? { reasoning_content: msg.reasoning_content } : {}),
|
|
60
60
|
...(Array.isArray(msg.reasoning_details) && msg.reasoning_details.length > 0 ? { reasoning_details: msg.reasoning_details } : {}),
|
|
61
61
|
...(msg.tool_calls ? { tool_calls: msg.tool_calls } : {})
|
|
@@ -102,6 +102,7 @@ function getCompletionCopy(language = 'zh') {
|
|
|
102
102
|
'gateway.timeout_ms': '网关超时时间(毫秒)',
|
|
103
103
|
'gateway.max_retries': '网关重试次数',
|
|
104
104
|
'model.name': '当前模型名称',
|
|
105
|
+
'model.fast_name': '快速模型名称',
|
|
105
106
|
'model.max_context_tokens': '模型上下文 token 上限',
|
|
106
107
|
'ui.language': '界面语言',
|
|
107
108
|
'ui.reply_language': '回复语言',
|
|
@@ -156,6 +157,7 @@ function getCompletionCopy(language = 'zh') {
|
|
|
156
157
|
exit: '退出聊天',
|
|
157
158
|
commands: '列出 slash/自定义命令',
|
|
158
159
|
status: '查看运行状态(mode/model/session)',
|
|
160
|
+
model: '查看或切换模型',
|
|
159
161
|
mode: '设置执行模式:normal|auto|plan',
|
|
160
162
|
compact: '压缩消息上下文',
|
|
161
163
|
checkpoint: '创建/查看/加载检查点',
|
|
@@ -193,6 +195,7 @@ function getCompletionCopy(language = 'zh') {
|
|
|
193
195
|
retryCommand: '重试上一条用户请求',
|
|
194
196
|
stopCommand: '中止当前回答',
|
|
195
197
|
statusCommand: '查看运行状态',
|
|
198
|
+
modelCommand: '查看或切换模型',
|
|
196
199
|
resumeSession: '恢复一个已保存的会话'
|
|
197
200
|
}
|
|
198
201
|
},
|
|
@@ -204,6 +207,7 @@ function getCompletionCopy(language = 'zh') {
|
|
|
204
207
|
'gateway.timeout_ms': 'gateway timeout in milliseconds',
|
|
205
208
|
'gateway.max_retries': 'gateway retry count',
|
|
206
209
|
'model.name': 'active model name',
|
|
210
|
+
'model.fast_name': 'fast model name',
|
|
207
211
|
'model.max_context_tokens': 'model context token limit',
|
|
208
212
|
'ui.language': 'UI language',
|
|
209
213
|
'ui.reply_language': 'reply language',
|
|
@@ -258,6 +262,7 @@ function getCompletionCopy(language = 'zh') {
|
|
|
258
262
|
exit: 'exit chat',
|
|
259
263
|
commands: 'list slash/custom commands',
|
|
260
264
|
status: 'show runtime status (mode/model/session)',
|
|
265
|
+
model: 'show or switch model',
|
|
261
266
|
mode: 'set execution mode: normal|auto|plan',
|
|
262
267
|
compact: 'compress message context',
|
|
263
268
|
checkpoint: 'create/list/load conversation checkpoints',
|
|
@@ -295,6 +300,7 @@ function getCompletionCopy(language = 'zh') {
|
|
|
295
300
|
retryCommand: 'retry the last user request',
|
|
296
301
|
stopCommand: 'stop the current response',
|
|
297
302
|
statusCommand: 'show runtime status',
|
|
303
|
+
modelCommand: 'show or switch model',
|
|
298
304
|
resumeSession: 'resume a saved session'
|
|
299
305
|
}
|
|
300
306
|
}
|
|
@@ -309,6 +315,7 @@ function describeConfigKey(key, mode = 'set', language = 'zh') {
|
|
|
309
315
|
}
|
|
310
316
|
|
|
311
317
|
const SUB_AGENT_ROLES = ['planner', 'advisor', 'coder', 'reviewer', 'tester', 'summarizer'];
|
|
318
|
+
const CODEWIKI_READ_ONLY_TOOLS = ['read', 'grep', 'list', 'glob', 'query_project_index', 'read_plan'];
|
|
312
319
|
export const ROLE_TOOL_POLICY = {
|
|
313
320
|
planner: ['read', 'grep', 'list', 'query_project_index', 'tool_search', 'glob', 'ast_query', 'read_ast_node', 'web_fetch', 'web_search', 'read_plan', 'update_plan'],
|
|
314
321
|
advisor: ['read', 'grep', 'list', 'query_project_index', 'tool_search', 'read_plan'],
|
|
@@ -2112,13 +2119,21 @@ function buildRuntimeStateSnapshot({ currentSession, config, model, executionMod
|
|
|
2112
2119
|
const currentContextTokens = parentTokens + subTokens;
|
|
2113
2120
|
const maxContextTokens = effectiveMaxContextTokens(config);
|
|
2114
2121
|
const contextUsagePct = maxContextTokens > 0 ? Math.min(100, Math.max(0, (currentContextTokens / maxContextTokens) * 100)) : 0;
|
|
2122
|
+
const planState = currentSession?.planState;
|
|
2115
2123
|
const snapshot = {
|
|
2116
2124
|
sessionId: currentSession?.id || '',
|
|
2117
|
-
|
|
2125
|
+
sessionTitle: currentSession?.title || '',
|
|
2126
|
+
messageCount: Array.isArray(currentSession?.messages) ? currentSession.messages.length : 0,
|
|
2127
|
+
mode: executionMode || config.execution?.mode || 'normal',
|
|
2118
2128
|
sdkProvider: config.sdk?.provider || 'openai-compatible',
|
|
2119
2129
|
agentRole: 'general',
|
|
2120
2130
|
model: model || config.model?.name || '',
|
|
2121
|
-
|
|
2131
|
+
mainModel: config.model?.name || '',
|
|
2132
|
+
fastModel: config.model?.fast_name || config.model?.name || '',
|
|
2133
|
+
maxContextTokens,
|
|
2134
|
+
pendingPlanApproval: planState?.status === 'pending_approval'
|
|
2135
|
+
? { goal: planState.goal, summary: planState.finalSummary || planState.summary, filePath: planState.filePath, steps: planState.steps || [] }
|
|
2136
|
+
: null
|
|
2122
2137
|
};
|
|
2123
2138
|
Object.defineProperties(snapshot, {
|
|
2124
2139
|
currentContextTokens: {
|
|
@@ -2131,11 +2146,6 @@ function buildRuntimeStateSnapshot({ currentSession, config, model, executionMod
|
|
|
2131
2146
|
enumerable: false,
|
|
2132
2147
|
writable: false
|
|
2133
2148
|
},
|
|
2134
|
-
pendingPlanApproval: {
|
|
2135
|
-
value: currentSession?.planState?.status === 'pending_approval',
|
|
2136
|
-
enumerable: false,
|
|
2137
|
-
writable: false
|
|
2138
|
-
},
|
|
2139
2149
|
pendingReflectSkill: {
|
|
2140
2150
|
value: currentSession?.planState?.status === 'pending_reflect_skill',
|
|
2141
2151
|
enumerable: false,
|
|
@@ -2145,6 +2155,70 @@ function buildRuntimeStateSnapshot({ currentSession, config, model, executionMod
|
|
|
2145
2155
|
return snapshot;
|
|
2146
2156
|
}
|
|
2147
2157
|
|
|
2158
|
+
function resolveDefaultModel(config) {
|
|
2159
|
+
return String(config?.model?.name || '').trim();
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
function resolveFastModel(config) {
|
|
2163
|
+
return String(config?.model?.fast_name || config?.model?.lite_name || config?.model?.name || '').trim();
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
function normalizeGeneratedSessionTitle(value, fallback = '') {
|
|
2167
|
+
const cleaned = String(value || '')
|
|
2168
|
+
.replace(/^[\s"'`#::「『【\[]+|[\s"'`。.!??!」』】\]]+$/g, '')
|
|
2169
|
+
.replace(/\s+/g, ' ')
|
|
2170
|
+
.trim();
|
|
2171
|
+
const title = cleaned || fallback || '';
|
|
2172
|
+
if (!title) return '';
|
|
2173
|
+
return title.length > 48 ? `${title.slice(0, 45).trimEnd()}...` : title;
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
function shouldReplaceSessionTitle(title) {
|
|
2177
|
+
const value = String(title || '').trim();
|
|
2178
|
+
return !value || value === '新会话' || value === 'New session';
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
async function generateSessionTitle({ userText, assistantText = '', config, signal }) {
|
|
2182
|
+
const fallback = normalizeGeneratedSessionTitle(deriveSessionTitle([{ role: 'user', content: userText }]));
|
|
2183
|
+
const latestConfig = await loadConfig().catch(() => config);
|
|
2184
|
+
const effectiveConfig = latestConfig || config;
|
|
2185
|
+
const fastModel = resolveFastModel(effectiveConfig);
|
|
2186
|
+
if (!fastModel) return fallback;
|
|
2187
|
+
const titleInput = [
|
|
2188
|
+
`User:\n${String(userText || '').slice(0, 1200)}`,
|
|
2189
|
+
assistantText ? `Assistant:\n${String(assistantText || '').slice(0, 1600)}` : ''
|
|
2190
|
+
].filter(Boolean).join('\n\n');
|
|
2191
|
+
try {
|
|
2192
|
+
const result = await createChatCompletion({
|
|
2193
|
+
sdkProvider: effectiveConfig.sdk?.provider,
|
|
2194
|
+
baseUrl: effectiveConfig.gateway.base_url,
|
|
2195
|
+
apiKey: effectiveConfig.gateway.api_key,
|
|
2196
|
+
model: fastModel,
|
|
2197
|
+
messages: [
|
|
2198
|
+
{
|
|
2199
|
+
role: 'system',
|
|
2200
|
+
content: [
|
|
2201
|
+
'Generate a concise chat session title.',
|
|
2202
|
+
'Base it on the completed user-assistant exchange, not only the user prompt.',
|
|
2203
|
+
'Return only the title text.',
|
|
2204
|
+
'Use the same language as the user when possible.',
|
|
2205
|
+
'No quotes, no markdown, no punctuation at the ends.',
|
|
2206
|
+
'Maximum 18 Chinese characters or 8 English words.'
|
|
2207
|
+
].join(' ')
|
|
2208
|
+
},
|
|
2209
|
+
{ role: 'user', content: titleInput }
|
|
2210
|
+
],
|
|
2211
|
+
tools: [],
|
|
2212
|
+
timeoutMs: Math.min(Number(effectiveConfig.gateway?.timeout_ms || 30000), 30000),
|
|
2213
|
+
maxRetries: 0,
|
|
2214
|
+
signal
|
|
2215
|
+
});
|
|
2216
|
+
return normalizeGeneratedSessionTitle(result?.text, fallback) || fallback;
|
|
2217
|
+
} catch {
|
|
2218
|
+
return fallback;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2148
2222
|
function estimatePromptTokensForRequest(sessionMessages, userText = '') {
|
|
2149
2223
|
const tokenMsgs = [
|
|
2150
2224
|
...(Array.isArray(sessionMessages) ? sessionMessages : []),
|
|
@@ -2330,6 +2404,7 @@ async function expandFileMentions(rawText, workspaceRoot = process.cwd()) {
|
|
|
2330
2404
|
|
|
2331
2405
|
async function askModel({
|
|
2332
2406
|
text,
|
|
2407
|
+
modelText,
|
|
2333
2408
|
session,
|
|
2334
2409
|
config,
|
|
2335
2410
|
model,
|
|
@@ -2344,13 +2419,14 @@ async function askModel({
|
|
|
2344
2419
|
maxSteps: maxStepsOverride,
|
|
2345
2420
|
skipAnalysisNudge = false
|
|
2346
2421
|
}) {
|
|
2422
|
+
const modelInputText = typeof modelText === 'string' && modelText ? modelText : text;
|
|
2347
2423
|
const maxContextTokens = effectiveMaxContextTokens(config);
|
|
2348
2424
|
const triggerPct = Number(config.context?.preflight_trigger_pct || 92);
|
|
2349
2425
|
const hardPct = Number(config.context?.hard_limit_pct || 98);
|
|
2350
|
-
const preflightTokens = estimatePromptTokensForRequest(session.messages,
|
|
2426
|
+
const preflightTokens = estimatePromptTokensForRequest(session.messages, modelInputText);
|
|
2351
2427
|
const preflightPct = (preflightTokens / maxContextTokens) * 100;
|
|
2352
2428
|
|
|
2353
|
-
if (preflightPct >= triggerPct) {
|
|
2429
|
+
if (persistSession && preflightPct >= triggerPct) {
|
|
2354
2430
|
const auto = compactMessagesLocally(session.messages, {
|
|
2355
2431
|
mode: preflightPct >= hardPct ? 'aggressive' : 'conservative'
|
|
2356
2432
|
});
|
|
@@ -2414,11 +2490,19 @@ async function askModel({
|
|
|
2414
2490
|
}
|
|
2415
2491
|
|
|
2416
2492
|
if (text) {
|
|
2417
|
-
session.messages.
|
|
2493
|
+
const shouldGenerateTitle = !session.messages.some((msg) => msg?.role === 'user');
|
|
2494
|
+
const modelExtra =
|
|
2495
|
+
typeof modelText === 'string' && modelText && modelText !== text ? { model_content: modelText } : {};
|
|
2496
|
+
session.messages.push(stampedMessage('user', text, modelExtra));
|
|
2497
|
+
if (!shouldGenerateTitle) {
|
|
2498
|
+
session.title = deriveSessionTitle(session.messages);
|
|
2499
|
+
}
|
|
2500
|
+
session.model = model || config.model.name;
|
|
2501
|
+
session.mode = executionMode || config.execution?.mode || 'normal';
|
|
2418
2502
|
if (persistSession) await saveSession(session);
|
|
2419
2503
|
}
|
|
2420
2504
|
|
|
2421
|
-
const projectContextSnippet = await buildProjectContextSnippet(process.cwd(),
|
|
2505
|
+
const projectContextSnippet = await buildProjectContextSnippet(process.cwd(), modelInputText).catch(() => '');
|
|
2422
2506
|
const projectContextGuidance =
|
|
2423
2507
|
'Use this project context as lightweight guidance and verify important details with fresh reads when needed.';
|
|
2424
2508
|
const effectiveSystemPrompt = projectContextSnippet
|
|
@@ -2478,6 +2562,22 @@ async function askModel({
|
|
|
2478
2562
|
}
|
|
2479
2563
|
|
|
2480
2564
|
let activeAssistantIndex = -1;
|
|
2565
|
+
const pendingToolMeta = new Map();
|
|
2566
|
+
const attachToolMetaToSessionCall = (toolId, meta = {}) => {
|
|
2567
|
+
if (!toolId) return false;
|
|
2568
|
+
for (let i = session.messages.length - 1; i >= 0; i -= 1) {
|
|
2569
|
+
const msg = session.messages[i];
|
|
2570
|
+
if (msg?.role !== 'assistant' || !Array.isArray(msg.tool_calls)) continue;
|
|
2571
|
+
const call = msg.tool_calls.find((tc) => String(tc?.id || '') === String(toolId));
|
|
2572
|
+
if (!call) continue;
|
|
2573
|
+
if (Number.isFinite(Number(meta.durationMs))) call.durationMs = Number(meta.durationMs);
|
|
2574
|
+
if (typeof meta.summary === 'string' && meta.summary.trim()) call.summary = meta.summary.trim();
|
|
2575
|
+
if (typeof meta.status === 'string' && meta.status.trim()) call.status = meta.status.trim();
|
|
2576
|
+
msg.at = new Date().toISOString();
|
|
2577
|
+
return true;
|
|
2578
|
+
}
|
|
2579
|
+
return false;
|
|
2580
|
+
};
|
|
2481
2581
|
const wrappedAgentEvent = (event) => {
|
|
2482
2582
|
// Always accumulate messages in session (for token tracking), only save when persisting
|
|
2483
2583
|
if (event?.type === 'assistant:start') {
|
|
@@ -2508,19 +2608,42 @@ async function askModel({
|
|
|
2508
2608
|
if (persistSession) scheduleSessionSave();
|
|
2509
2609
|
}
|
|
2510
2610
|
activeAssistantIndex = -1;
|
|
2611
|
+
} else if (event?.type === 'tool:end' || event?.type === 'tool:error' || event?.type === 'tool:blocked') {
|
|
2612
|
+
const toolId = String(event.id || '');
|
|
2613
|
+
if (toolId) {
|
|
2614
|
+
const meta = {
|
|
2615
|
+
durationMs: Number.isFinite(Number(event.durationMs)) ? Number(event.durationMs) : undefined,
|
|
2616
|
+
summary: typeof event.summary === 'string' ? event.summary : '',
|
|
2617
|
+
status:
|
|
2618
|
+
event.type === 'tool:error'
|
|
2619
|
+
? 'error'
|
|
2620
|
+
: event.type === 'tool:blocked'
|
|
2621
|
+
? 'blocked'
|
|
2622
|
+
: 'done'
|
|
2623
|
+
};
|
|
2624
|
+
pendingToolMeta.set(toolId, meta);
|
|
2625
|
+
if (attachToolMetaToSessionCall(toolId, meta) && persistSession) scheduleSessionSave();
|
|
2626
|
+
}
|
|
2511
2627
|
} else if (event?.type === 'tool:result') {
|
|
2628
|
+
const toolId = String(event.id || '');
|
|
2629
|
+
const meta = pendingToolMeta.get(toolId) || {};
|
|
2512
2630
|
session.messages.push(
|
|
2513
2631
|
stampedMessage('tool', event.content || '', {
|
|
2514
|
-
tool_call_id:
|
|
2632
|
+
tool_call_id: toolId,
|
|
2633
|
+
...(Number.isFinite(Number(meta.durationMs)) ? { tool_duration_ms: Number(meta.durationMs) } : {}),
|
|
2634
|
+
...(meta.summary ? { tool_summary: meta.summary } : {}),
|
|
2635
|
+
...(meta.status ? { tool_status: meta.status } : {})
|
|
2515
2636
|
})
|
|
2516
2637
|
);
|
|
2638
|
+
pendingToolMeta.delete(toolId);
|
|
2517
2639
|
if (persistSession) scheduleSessionSave();
|
|
2518
2640
|
}
|
|
2519
2641
|
|
|
2520
2642
|
if (onAgentEvent) onAgentEvent(event);
|
|
2521
2643
|
};
|
|
2522
2644
|
|
|
2523
|
-
const loopUserPrompt = persistSession ? '' :
|
|
2645
|
+
const loopUserPrompt = persistSession ? '' : modelInputText;
|
|
2646
|
+
const expectedModelText = typeof modelText === 'string' && modelText && modelText !== text ? modelText : '';
|
|
2524
2647
|
const loopResult = await runAgentLoop({
|
|
2525
2648
|
systemPrompt: effectiveSystemPrompt,
|
|
2526
2649
|
userPrompt: loopUserPrompt,
|
|
@@ -2530,7 +2653,7 @@ async function askModel({
|
|
|
2530
2653
|
toolHandlers: filteredHandlers,
|
|
2531
2654
|
initialMessages: toOpenAIMessages(session.messages),
|
|
2532
2655
|
onEvent: wrappedAgentEvent,
|
|
2533
|
-
executionMode: executionMode || config.execution?.mode || '
|
|
2656
|
+
executionMode: executionMode || config.execution?.mode || 'normal',
|
|
2534
2657
|
alwaysAllowTools:
|
|
2535
2658
|
alwaysAllowTools || config.execution?.always_allow_tools || ['run', 'read', 'write'],
|
|
2536
2659
|
toolResultMaxChars: config.context?.tool_result_max_chars || 12000,
|
|
@@ -2581,6 +2704,26 @@ async function askModel({
|
|
|
2581
2704
|
session.messages = loopResult.messages
|
|
2582
2705
|
.filter((m) => m.role !== 'system')
|
|
2583
2706
|
.map((m) => ({ ...m, at: new Date().toISOString() }));
|
|
2707
|
+
if (expectedModelText) {
|
|
2708
|
+
for (let i = session.messages.length - 1; i >= 0; i -= 1) {
|
|
2709
|
+
const message = session.messages[i];
|
|
2710
|
+
if (message?.role === 'user' && message.content === expectedModelText) {
|
|
2711
|
+
message.content = text;
|
|
2712
|
+
message.model_content = expectedModelText;
|
|
2713
|
+
break;
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
if (shouldReplaceSessionTitle(session.title)) {
|
|
2718
|
+
session.title = await generateSessionTitle({
|
|
2719
|
+
userText: text,
|
|
2720
|
+
assistantText: loopResult.text || '',
|
|
2721
|
+
config,
|
|
2722
|
+
signal
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
session.model = model || config.model.name;
|
|
2726
|
+
session.mode = executionMode || config.execution?.mode || 'normal';
|
|
2584
2727
|
await flushScheduledSave();
|
|
2585
2728
|
await saveSession(session);
|
|
2586
2729
|
try {
|
|
@@ -2684,7 +2827,60 @@ async function runSubAgentTask({
|
|
|
2684
2827
|
blockedCount,
|
|
2685
2828
|
toolErrorCount,
|
|
2686
2829
|
hasErrorLine,
|
|
2687
|
-
artifactPaths: artifactPaths.slice(0, SUB_AGENT_HANDOFF_MAX_ITEMS)
|
|
2830
|
+
artifactPaths: artifactPaths.slice(0, SUB_AGENT_HANDOFF_MAX_ITEMS),
|
|
2831
|
+
messages: Array.isArray(subSession.messages) ? structuredClone(subSession.messages) : []
|
|
2832
|
+
};
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
function buildPlanStepTranscript({ stepRecord, stepIndex, totalSteps, messages }) {
|
|
2836
|
+
const toolCardsById = new Map();
|
|
2837
|
+
const toolCards = [];
|
|
2838
|
+
const source = Array.isArray(messages) ? messages : [];
|
|
2839
|
+
|
|
2840
|
+
for (const msg of source) {
|
|
2841
|
+
if (msg?.role === 'assistant' && Array.isArray(msg.tool_calls)) {
|
|
2842
|
+
for (const tc of msg.tool_calls) {
|
|
2843
|
+
const id = String(tc?.id || `tool-${toolCards.length + 1}`);
|
|
2844
|
+
if (toolCardsById.has(id)) continue;
|
|
2845
|
+
const card = {
|
|
2846
|
+
id,
|
|
2847
|
+
name: tc?.function?.name || tc?.name || 'tool',
|
|
2848
|
+
arguments: tc?.function?.arguments || tc?.arguments || {},
|
|
2849
|
+
status: tc?.status || 'done',
|
|
2850
|
+
durationMs: Number.isFinite(Number(tc?.durationMs)) ? Number(tc.durationMs) : null,
|
|
2851
|
+
summary: tc?.summary || '',
|
|
2852
|
+
result: ''
|
|
2853
|
+
};
|
|
2854
|
+
toolCardsById.set(id, card);
|
|
2855
|
+
toolCards.push(card);
|
|
2856
|
+
}
|
|
2857
|
+
} else if (msg?.role === 'tool') {
|
|
2858
|
+
const id = String(msg.tool_call_id || '');
|
|
2859
|
+
const card = toolCardsById.get(id);
|
|
2860
|
+
if (!card) continue;
|
|
2861
|
+
card.result = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content || '');
|
|
2862
|
+
if (typeof msg.tool_summary === 'string' && msg.tool_summary.trim()) card.summary = msg.tool_summary.trim();
|
|
2863
|
+
if (Number.isFinite(Number(msg.tool_duration_ms))) card.durationMs = Number(msg.tool_duration_ms);
|
|
2864
|
+
if (typeof msg.tool_status === 'string' && msg.tool_status.trim()) card.status = msg.tool_status.trim();
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
const segments = [];
|
|
2869
|
+
if (toolCards.length > 0) {
|
|
2870
|
+
segments.push({ type: 'tools', cards: toolCards });
|
|
2871
|
+
}
|
|
2872
|
+
if (stepRecord.role === 'summarizer' && stepRecord.output) {
|
|
2873
|
+
segments.push({ type: 'text', text: stepRecord.output, isStreaming: false });
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
return {
|
|
2877
|
+
step: stepIndex + 1,
|
|
2878
|
+
total: totalSteps,
|
|
2879
|
+
role: stepRecord.role || 'general',
|
|
2880
|
+
title: stepRecord.title || '',
|
|
2881
|
+
status: stepRecord.failed ? 'failed' : 'done',
|
|
2882
|
+
summary: stepRecord.failed ? stepRecord.failureReason : trimInline(stepRecord.output || '', 160),
|
|
2883
|
+
segments
|
|
2688
2884
|
};
|
|
2689
2885
|
}
|
|
2690
2886
|
|
|
@@ -2712,8 +2908,11 @@ async function executePlanWithSubAgents({
|
|
|
2712
2908
|
return { text: '(no steps to execute)', aborted: false };
|
|
2713
2909
|
}
|
|
2714
2910
|
|
|
2911
|
+
emitPlanEvent({ type: 'assistant:start' });
|
|
2912
|
+
|
|
2715
2913
|
const priorSteps = [];
|
|
2716
2914
|
const results = [];
|
|
2915
|
+
const transcript = [];
|
|
2717
2916
|
|
|
2718
2917
|
// Emit structured plan steps so TUI can show all steps with real role/title
|
|
2719
2918
|
emitPlanEvent({
|
|
@@ -2725,6 +2924,15 @@ async function executePlanWithSubAgents({
|
|
|
2725
2924
|
const step = steps[i];
|
|
2726
2925
|
if (signal?.aborted) break;
|
|
2727
2926
|
|
|
2927
|
+
emitPlanEvent({
|
|
2928
|
+
type: 'plan:step_start',
|
|
2929
|
+
planFile: planFilePath,
|
|
2930
|
+
step: i + 1,
|
|
2931
|
+
total: steps.length,
|
|
2932
|
+
role: step.role,
|
|
2933
|
+
title: step.title
|
|
2934
|
+
});
|
|
2935
|
+
|
|
2728
2936
|
emitPlanEvent({
|
|
2729
2937
|
type: 'plan:progress',
|
|
2730
2938
|
planFile: planFilePath,
|
|
@@ -2772,6 +2980,7 @@ async function executePlanWithSubAgents({
|
|
|
2772
2980
|
toolErrorCount: output.toolErrorCount || 0,
|
|
2773
2981
|
hasErrorLine: output.hasErrorLine || false,
|
|
2774
2982
|
artifactPaths: output.artifactPaths || [],
|
|
2983
|
+
messages: output.messages || [],
|
|
2775
2984
|
failed:
|
|
2776
2985
|
output.hasErrorLine ||
|
|
2777
2986
|
stepOutputHasFailureSignals(step.role, output.text || ''),
|
|
@@ -2785,6 +2994,12 @@ async function executePlanWithSubAgents({
|
|
|
2785
2994
|
}
|
|
2786
2995
|
priorSteps.push(stepRecord);
|
|
2787
2996
|
results.push(stepRecord);
|
|
2997
|
+
transcript.push(buildPlanStepTranscript({
|
|
2998
|
+
stepRecord,
|
|
2999
|
+
stepIndex: i,
|
|
3000
|
+
totalSteps: steps.length,
|
|
3001
|
+
messages: output.messages || []
|
|
3002
|
+
}));
|
|
2788
3003
|
|
|
2789
3004
|
// Write step result to plan file for subsequent steps to read
|
|
2790
3005
|
if (planFilePath) {
|
|
@@ -2809,6 +3024,17 @@ async function executePlanWithSubAgents({
|
|
|
2809
3024
|
summary: stepRecord.failed ? stepRecord.failureReason : trimInline(stepRecord.output, 160)
|
|
2810
3025
|
});
|
|
2811
3026
|
|
|
3027
|
+
emitPlanEvent({
|
|
3028
|
+
type: 'plan:step_done',
|
|
3029
|
+
planFile: planFilePath,
|
|
3030
|
+
step: i + 1,
|
|
3031
|
+
total: steps.length,
|
|
3032
|
+
role: step.role,
|
|
3033
|
+
title: step.title,
|
|
3034
|
+
status: stepRecord.failed ? 'failed' : 'done',
|
|
3035
|
+
summary: stepRecord.failed ? stepRecord.failureReason : trimInline(stepRecord.output, 160)
|
|
3036
|
+
});
|
|
3037
|
+
|
|
2812
3038
|
if (stepRecord.failed && i < steps.length - 1) {
|
|
2813
3039
|
const summarizerIndex = steps.findIndex((candidate, index) => index > i && candidate.role === 'summarizer');
|
|
2814
3040
|
if (summarizerIndex > i) {
|
|
@@ -2848,7 +3074,11 @@ async function executePlanWithSubAgents({
|
|
|
2848
3074
|
return {
|
|
2849
3075
|
text: summaryLines.join('\n'),
|
|
2850
3076
|
aborted: !!signal?.aborted,
|
|
2851
|
-
results
|
|
3077
|
+
results,
|
|
3078
|
+
transcript,
|
|
3079
|
+
sessionText:
|
|
3080
|
+
[...results].reverse().find((r) => r.role === 'summarizer')?.output ||
|
|
3081
|
+
summaryLines.join('\n')
|
|
2852
3082
|
};
|
|
2853
3083
|
}
|
|
2854
3084
|
|
|
@@ -3007,10 +3237,29 @@ function renderAutoPlanMarkdown({
|
|
|
3007
3237
|
}
|
|
3008
3238
|
|
|
3009
3239
|
function parseProjectRequirementsOptions(args = []) {
|
|
3010
|
-
|
|
3240
|
+
let depth = 'standard';
|
|
3241
|
+
const focusArgs = [];
|
|
3242
|
+
for (const arg of Array.isArray(args) ? args : []) {
|
|
3243
|
+
const value = String(arg || '').trim();
|
|
3244
|
+
const normalized = value.toLowerCase();
|
|
3245
|
+
if (['--fast', '--quick', '--lite', '--light', '--快速'].includes(normalized)) {
|
|
3246
|
+
depth = 'fast';
|
|
3247
|
+
continue;
|
|
3248
|
+
}
|
|
3249
|
+
if (['--standard', '--balanced', '--默认', '--标准'].includes(normalized)) {
|
|
3250
|
+
depth = 'standard';
|
|
3251
|
+
continue;
|
|
3252
|
+
}
|
|
3253
|
+
if (['--deep', '--full', '--thorough', '--深度'].includes(normalized)) {
|
|
3254
|
+
depth = 'deep';
|
|
3255
|
+
continue;
|
|
3256
|
+
}
|
|
3257
|
+
focusArgs.push(arg);
|
|
3258
|
+
}
|
|
3259
|
+
const raw = focusArgs.join(' ').trim();
|
|
3011
3260
|
const normalized = raw.toLowerCase();
|
|
3012
3261
|
const hasIgnoreIntent = /(忽略|跳过|不生成|不要|无需|排除|exclude|skip|omit|without|no\s+)/i.test(raw);
|
|
3013
|
-
if (!hasIgnoreIntent) return { raw, ignoredSections: [] };
|
|
3262
|
+
if (!hasIgnoreIntent) return { raw, focusArgs, depth, ignoredSections: [] };
|
|
3014
3263
|
|
|
3015
3264
|
const ignored = [];
|
|
3016
3265
|
for (const section of PROJECT_REQUIREMENTS_SECTION_MARKERS) {
|
|
@@ -3023,7 +3272,7 @@ function parseProjectRequirementsOptions(args = []) {
|
|
|
3023
3272
|
});
|
|
3024
3273
|
if (matched) ignored.push(section);
|
|
3025
3274
|
}
|
|
3026
|
-
return { raw, ignoredSections: ignored };
|
|
3275
|
+
return { raw, focusArgs, depth, ignoredSections: ignored };
|
|
3027
3276
|
}
|
|
3028
3277
|
|
|
3029
3278
|
function renderProjectRequirementsSectionContract(ignoredSections = []) {
|
|
@@ -3041,7 +3290,7 @@ function renderProjectRequirementsSectionContract(ignoredSections = []) {
|
|
|
3041
3290
|
|
|
3042
3291
|
function buildProjectRequirementsSteps(renderedSkillPrompt, args = []) {
|
|
3043
3292
|
const options = parseProjectRequirementsOptions(args);
|
|
3044
|
-
const userArgs =
|
|
3293
|
+
const userArgs = options.raw;
|
|
3045
3294
|
const requestedFocus = userArgs ? `User request/focus: ${userArgs}` : 'User request/focus: full workspace requirements report.';
|
|
3046
3295
|
const reportDate = formatLocalDate();
|
|
3047
3296
|
const reportPath = `docs/requirements/${reportDate}-project-requirements.html`;
|
|
@@ -3060,6 +3309,120 @@ function buildProjectRequirementsSteps(renderedSkillPrompt, args = []) {
|
|
|
3060
3309
|
'Do not invent dates; use the report paths above.'
|
|
3061
3310
|
].join('\n');
|
|
3062
3311
|
|
|
3312
|
+
const writeReportStep = {
|
|
3313
|
+
title: '🎨 Write banking-style requirements HTML report',
|
|
3314
|
+
role: 'coder',
|
|
3315
|
+
task: [
|
|
3316
|
+
'Create the final project requirements report from the accumulated plan context.',
|
|
3317
|
+
reportContract,
|
|
3318
|
+
'Follow the project-requirements skill instructions below exactly, including chunked HTML writing for medium/large reports.',
|
|
3319
|
+
'Use the blue/white/gray banking-style shell and produce polished inline HTML/CSS/SVG diagrams. Keep the report professional, light, and conservative.',
|
|
3320
|
+
'Organize the main requirements section primarily by API/interface business requirement cards.',
|
|
3321
|
+
'The final HTML must be self-contained and directly openable from disk.',
|
|
3322
|
+
'Write the primary report to the exact primary report path above. Create the companion Markdown only if useful.',
|
|
3323
|
+
'Skill instructions:',
|
|
3324
|
+
renderedSkillPrompt
|
|
3325
|
+
].join('\n\n')
|
|
3326
|
+
};
|
|
3327
|
+
const reviewStep = {
|
|
3328
|
+
title: '🔎 Review API coverage and traceability',
|
|
3329
|
+
role: 'reviewer',
|
|
3330
|
+
task: [
|
|
3331
|
+
'Review the generated requirements report against the project-requirements contract and accumulated evidence.',
|
|
3332
|
+
reportContract,
|
|
3333
|
+
'Check that major APIs/interfaces are represented, business requirements are decomposed per API, evidence paths are present, inferred/unknown content is labeled, diagrams are visible as inline HTML/CSS/SVG without external rendering libraries, and the report path matches the required local date.',
|
|
3334
|
+
'Check that the visual style is light blue/white/gray and suitable for banking/financial review.',
|
|
3335
|
+
'Report concrete gaps and risks only. Do not rewrite the whole report.'
|
|
3336
|
+
].join('\n')
|
|
3337
|
+
};
|
|
3338
|
+
const summaryStep = {
|
|
3339
|
+
title: '🧾 Summarize final report and unresolved questions',
|
|
3340
|
+
role: 'summarizer',
|
|
3341
|
+
task: [
|
|
3342
|
+
'Synthesize the project requirements pipeline results into a concise final status for the user.',
|
|
3343
|
+
reportContract,
|
|
3344
|
+
'Mention the generated report path, API/interface coverage, strongest business requirement findings, unresolved questions, what was not verified, and the best next action.',
|
|
3345
|
+
'Do not re-analyze the codebase unless the accumulated evidence is clearly insufficient.'
|
|
3346
|
+
].join('\n')
|
|
3347
|
+
};
|
|
3348
|
+
|
|
3349
|
+
if (options.depth === 'fast') {
|
|
3350
|
+
return [
|
|
3351
|
+
{
|
|
3352
|
+
title: '⚡ Map evidence and interfaces',
|
|
3353
|
+
role: 'planner',
|
|
3354
|
+
task: [
|
|
3355
|
+
'Quickly map project evidence and build a practical API/interface inventory before report writing.',
|
|
3356
|
+
reportContract,
|
|
3357
|
+
'Inspect top-level docs, manifests, route/command entry points, obvious handlers, schemas, tests, and project index results when available.',
|
|
3358
|
+
'Produce a concise evidence map and major interface inventory with evidence paths, prioritizing broad coverage over exhaustive edge cases.',
|
|
3359
|
+
'Do not write the final report.'
|
|
3360
|
+
].join('\n')
|
|
3361
|
+
},
|
|
3362
|
+
{
|
|
3363
|
+
title: '🧩 Synthesize requirements, flows, and risks',
|
|
3364
|
+
role: 'advisor',
|
|
3365
|
+
task: [
|
|
3366
|
+
'Synthesize requirement-ready findings from the evidence map and interface inventory.',
|
|
3367
|
+
reportContract,
|
|
3368
|
+
'For major interfaces capture capability, actor, trigger, inputs, outputs, business rules, validation/permission notes, data reads/writes, core flow dependencies, risks, acceptance criteria, and open questions.',
|
|
3369
|
+
'Keep findings compact and API-centered. Preserve evidence paths and EXTRACTED/INFERRED/UNKNOWN labels. Do not write the final report.'
|
|
3370
|
+
].join('\n')
|
|
3371
|
+
},
|
|
3372
|
+
writeReportStep,
|
|
3373
|
+
{
|
|
3374
|
+
title: '🔎 Review and summarize coverage',
|
|
3375
|
+
role: 'reviewer',
|
|
3376
|
+
task: [
|
|
3377
|
+
'Review the generated report and produce the final user-facing status in one pass.',
|
|
3378
|
+
reportContract,
|
|
3379
|
+
'Check major interface coverage, evidence paths, EXTRACTED/INFERRED/UNKNOWN labels, visible diagrams, and report path.',
|
|
3380
|
+
'Do not rewrite the whole report. Report concrete gaps, unresolved questions, and the single best next action.'
|
|
3381
|
+
].join('\n')
|
|
3382
|
+
}
|
|
3383
|
+
];
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
if (options.depth === 'standard') {
|
|
3387
|
+
return [
|
|
3388
|
+
{
|
|
3389
|
+
title: '🧭 Map entry points and evidence sources',
|
|
3390
|
+
role: 'planner',
|
|
3391
|
+
task: [
|
|
3392
|
+
'Map project entry points and evidence sources before any report writing.',
|
|
3393
|
+
reportContract,
|
|
3394
|
+
'Inspect top-level docs, package manifests, route/command entry points, tests, obvious interface files, and project index results when useful.',
|
|
3395
|
+
'Produce a concise evidence map grouped by docs, routes/commands, handlers, schemas, tests, configuration, storage, and operations.',
|
|
3396
|
+
'Include evidence paths and open questions. Do not write the final report.'
|
|
3397
|
+
].join('\n')
|
|
3398
|
+
},
|
|
3399
|
+
{
|
|
3400
|
+
title: '📚 Build API and interface inventory',
|
|
3401
|
+
role: 'planner',
|
|
3402
|
+
task: [
|
|
3403
|
+
'Build the canonical API/interface inventory using the evidence map.',
|
|
3404
|
+
reportContract,
|
|
3405
|
+
'Enumerate every major HTTP endpoint, CLI command, tool call, MCP/RPC handler, queue/scheduled job, exported SDK function, and user-facing workflow entry point.',
|
|
3406
|
+
'For each item include type, route/command/function, owner module, evidence path, likely actor, and whether it is EXTRACTED, INFERRED, or UNKNOWN.',
|
|
3407
|
+
'Do not write the final report.'
|
|
3408
|
+
].join('\n')
|
|
3409
|
+
},
|
|
3410
|
+
{
|
|
3411
|
+
title: '🧩 Decompose requirements, flows, data, and risks',
|
|
3412
|
+
role: 'advisor',
|
|
3413
|
+
task: [
|
|
3414
|
+
'Decompose requirement-ready findings for each major API/interface from the inventory.',
|
|
3415
|
+
reportContract,
|
|
3416
|
+
'For each interface capture business capability, actor, user goal, trigger, inputs, outputs, preconditions, main/alternate flows, business rules, validation and permission checks, sensitive data, data reads/writes, side effects, acceptance criteria, and open questions.',
|
|
3417
|
+
'Keep findings API-centered rather than module-centered. Preserve evidence paths and EXTRACTED/INFERRED/UNKNOWN labels. Do not write the final report.'
|
|
3418
|
+
].join('\n')
|
|
3419
|
+
},
|
|
3420
|
+
writeReportStep,
|
|
3421
|
+
reviewStep,
|
|
3422
|
+
summaryStep
|
|
3423
|
+
];
|
|
3424
|
+
}
|
|
3425
|
+
|
|
3063
3426
|
return [
|
|
3064
3427
|
{
|
|
3065
3428
|
title: '🧭 Map entry points and evidence sources',
|
|
@@ -3123,42 +3486,9 @@ function buildProjectRequirementsSteps(renderedSkillPrompt, args = []) {
|
|
|
3123
3486
|
'Favor clear business process decomposition over broad architecture prose. Do not write the final report.'
|
|
3124
3487
|
].join('\n')
|
|
3125
3488
|
},
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
task: [
|
|
3130
|
-
'Create the final project requirements report from the accumulated plan context.',
|
|
3131
|
-
reportContract,
|
|
3132
|
-
'Follow the project-requirements skill instructions below exactly, including chunked HTML writing for medium/large reports.',
|
|
3133
|
-
'Use the blue/white/gray banking-style shell and produce polished inline HTML/CSS/SVG diagrams. Keep the report professional, light, and conservative.',
|
|
3134
|
-
'Organize the main requirements section primarily by API/interface business requirement cards.',
|
|
3135
|
-
'The final HTML must be self-contained and directly openable from disk.',
|
|
3136
|
-
'Write the primary report to the exact primary report path above. Create the companion Markdown only if useful.',
|
|
3137
|
-
'Skill instructions:',
|
|
3138
|
-
renderedSkillPrompt
|
|
3139
|
-
].join('\n\n')
|
|
3140
|
-
},
|
|
3141
|
-
{
|
|
3142
|
-
title: '🔎 Review API coverage and traceability',
|
|
3143
|
-
role: 'reviewer',
|
|
3144
|
-
task: [
|
|
3145
|
-
'Review the generated requirements report against the project-requirements contract and accumulated evidence.',
|
|
3146
|
-
reportContract,
|
|
3147
|
-
'Check that major APIs/interfaces are represented, business requirements are decomposed per API, evidence paths are present, inferred/unknown content is labeled, diagrams are visible as inline HTML/CSS/SVG without external rendering libraries, and the report path matches the required local date.',
|
|
3148
|
-
'Check that the visual style is light blue/white/gray and suitable for banking/financial review.',
|
|
3149
|
-
'Report concrete gaps and risks only. Do not rewrite the whole report.'
|
|
3150
|
-
].join('\n')
|
|
3151
|
-
},
|
|
3152
|
-
{
|
|
3153
|
-
title: '🧾 Summarize final report and unresolved questions',
|
|
3154
|
-
role: 'summarizer',
|
|
3155
|
-
task: [
|
|
3156
|
-
'Synthesize the project requirements pipeline results into a concise final status for the user.',
|
|
3157
|
-
reportContract,
|
|
3158
|
-
'Mention the generated report path, API/interface coverage, strongest business requirement findings, unresolved questions, what was not verified, and the best next action.',
|
|
3159
|
-
'Do not re-analyze the codebase unless the accumulated evidence is clearly insufficient.'
|
|
3160
|
-
].join('\n')
|
|
3161
|
-
}
|
|
3489
|
+
writeReportStep,
|
|
3490
|
+
reviewStep,
|
|
3491
|
+
summaryStep
|
|
3162
3492
|
];
|
|
3163
3493
|
}
|
|
3164
3494
|
|
|
@@ -3200,7 +3530,8 @@ async function createProjectRequirementsShell({
|
|
|
3200
3530
|
manifestPath,
|
|
3201
3531
|
planFile,
|
|
3202
3532
|
goal,
|
|
3203
|
-
steps
|
|
3533
|
+
steps,
|
|
3534
|
+
depth = 'standard'
|
|
3204
3535
|
}) {
|
|
3205
3536
|
const workspaceRoot = process.cwd();
|
|
3206
3537
|
const absoluteReportPath = path.resolve(workspaceRoot, reportPath);
|
|
@@ -3231,6 +3562,7 @@ async function createProjectRequirementsShell({
|
|
|
3231
3562
|
];
|
|
3232
3563
|
const manifest = {
|
|
3233
3564
|
status: 'running',
|
|
3565
|
+
depth,
|
|
3234
3566
|
goal,
|
|
3235
3567
|
html: reportPath,
|
|
3236
3568
|
markdown: companionPath,
|
|
@@ -3278,7 +3610,8 @@ async function runProjectRequirementsPipeline({
|
|
|
3278
3610
|
onSubSessionActive
|
|
3279
3611
|
}) {
|
|
3280
3612
|
const renderedSkillPrompt = await expandFileMentions(renderCommandPrompt(custom, parsedInput.args), process.cwd());
|
|
3281
|
-
const
|
|
3613
|
+
const options = parseProjectRequirementsOptions(parsedInput.args);
|
|
3614
|
+
const userFocus = options.raw;
|
|
3282
3615
|
const goal = userFocus ? `project requirements report: ${userFocus}` : 'project requirements report';
|
|
3283
3616
|
const reportDate = formatLocalDate();
|
|
3284
3617
|
const reportPath = `docs/requirements/${reportDate}-project-requirements.html`;
|
|
@@ -3298,15 +3631,17 @@ async function runProjectRequirementsPipeline({
|
|
|
3298
3631
|
manifestPath,
|
|
3299
3632
|
planFile,
|
|
3300
3633
|
goal,
|
|
3301
|
-
steps
|
|
3634
|
+
steps,
|
|
3635
|
+
depth: options.depth
|
|
3302
3636
|
});
|
|
3303
3637
|
const planState = {
|
|
3304
3638
|
status: 'approved',
|
|
3305
3639
|
source: 'project-requirements',
|
|
3640
|
+
depth: options.depth,
|
|
3306
3641
|
goal,
|
|
3307
3642
|
filePath: planFile,
|
|
3308
|
-
summary:
|
|
3309
|
-
finalSummary:
|
|
3643
|
+
summary: `Dedicated ${options.depth} sub-agent pipeline for project requirements report generation.`,
|
|
3644
|
+
finalSummary: `Executing ${options.depth} project requirements pipeline.`,
|
|
3310
3645
|
steps
|
|
3311
3646
|
};
|
|
3312
3647
|
if (onAgentEvent) {
|
|
@@ -3341,8 +3676,23 @@ async function runProjectRequirementsPipeline({
|
|
|
3341
3676
|
name: custom.name,
|
|
3342
3677
|
summary: error instanceof Error ? error.message : String(error)
|
|
3343
3678
|
});
|
|
3679
|
+
onAgentEvent({ type: 'skill:end', name: custom.name });
|
|
3680
|
+
}
|
|
3681
|
+
if (manifestPath) {
|
|
3682
|
+
await updateProjectRequirementsManifest(manifestPath, {
|
|
3683
|
+
status: 'failed',
|
|
3684
|
+
failedCount: steps.length,
|
|
3685
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3686
|
+
}).catch(() => {});
|
|
3344
3687
|
}
|
|
3345
|
-
|
|
3688
|
+
return {
|
|
3689
|
+
type: 'assistant',
|
|
3690
|
+
text: `Project requirements pipeline failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
3691
|
+
planFile,
|
|
3692
|
+
reportPath,
|
|
3693
|
+
manifestPath,
|
|
3694
|
+
aborted: true
|
|
3695
|
+
};
|
|
3346
3696
|
}
|
|
3347
3697
|
if (onAgentEvent) {
|
|
3348
3698
|
onAgentEvent({
|
|
@@ -3496,7 +3846,8 @@ function compactHistoryPreview(value, maxChars = 72) {
|
|
|
3496
3846
|
function formatHistoryList({ currentSession, sessions }) {
|
|
3497
3847
|
const currentMessages = Array.isArray(currentSession?.messages) ? currentSession.messages.length : 0;
|
|
3498
3848
|
const lines = [
|
|
3499
|
-
`Current session ${currentSession.id}`,
|
|
3849
|
+
`Current session ${currentSession.title || currentSession.id}`,
|
|
3850
|
+
`Session id ${currentSession.id}`,
|
|
3500
3851
|
`Messages ${currentMessages}`,
|
|
3501
3852
|
'',
|
|
3502
3853
|
'Recent sessions'
|
|
@@ -3505,8 +3856,9 @@ function formatHistoryList({ currentSession, sessions }) {
|
|
|
3505
3856
|
for (const [index, session] of sessions.entries()) {
|
|
3506
3857
|
const count = Number(session.messageCount || 0);
|
|
3507
3858
|
lines.push(
|
|
3508
|
-
`${index + 1}. ${session.id}`,
|
|
3509
|
-
`
|
|
3859
|
+
`${index + 1}. ${session.title || session.id}`,
|
|
3860
|
+
` id=${session.id}`,
|
|
3861
|
+
` ${count} ${count === 1 ? 'msg' : 'msgs'} | ${formatHistoryTimestamp(session.updatedAt)}${session.model ? ` | ${session.model}` : ''}`,
|
|
3510
3862
|
` ${compactHistoryPreview(session.preview)}`,
|
|
3511
3863
|
` resume: /history resume ${session.id}`
|
|
3512
3864
|
);
|
|
@@ -3523,6 +3875,9 @@ export async function createChatRuntime({
|
|
|
3523
3875
|
systemPrompt,
|
|
3524
3876
|
requestToolApproval
|
|
3525
3877
|
}) {
|
|
3878
|
+
if (session && typeof session === 'object' && !session.projectDir) {
|
|
3879
|
+
session.projectDir = process.cwd();
|
|
3880
|
+
}
|
|
3526
3881
|
let activeRequestToolApproval = typeof requestToolApproval === 'function' ? requestToolApproval : null;
|
|
3527
3882
|
const startupEvents = [];
|
|
3528
3883
|
const initialIndex = await initializeProjectIndex(process.cwd()).catch(() => null);
|
|
@@ -3558,8 +3913,12 @@ export async function createChatRuntime({
|
|
|
3558
3913
|
}
|
|
3559
3914
|
let currentSession = session;
|
|
3560
3915
|
let config = initialConfig;
|
|
3916
|
+
model = model || currentSession?.model || resolveDefaultModel(config);
|
|
3917
|
+
if (currentSession && typeof currentSession === 'object') {
|
|
3918
|
+
currentSession.model = model;
|
|
3919
|
+
}
|
|
3561
3920
|
const baseSystemPrompt = systemPrompt;
|
|
3562
|
-
let executionMode = config.execution?.mode || '
|
|
3921
|
+
let executionMode = config.execution?.mode || 'normal';
|
|
3563
3922
|
if (hasPendingPlanApproval(currentSession)) {
|
|
3564
3923
|
executionMode = 'plan';
|
|
3565
3924
|
}
|
|
@@ -3585,6 +3944,7 @@ export async function createChatRuntime({
|
|
|
3585
3944
|
let historySessionCache = [
|
|
3586
3945
|
{
|
|
3587
3946
|
id: currentSession.id,
|
|
3947
|
+
title: currentSession.title || deriveSessionTitle(currentSession.messages || []),
|
|
3588
3948
|
messageCount: Array.isArray(currentSession.messages) ? currentSession.messages.length : 0
|
|
3589
3949
|
}
|
|
3590
3950
|
];
|
|
@@ -3594,10 +3954,12 @@ export async function createChatRuntime({
|
|
|
3594
3954
|
const merged = [
|
|
3595
3955
|
{
|
|
3596
3956
|
id: currentSession.id,
|
|
3957
|
+
title: currentSession.title || deriveSessionTitle(currentSession.messages || []),
|
|
3597
3958
|
messageCount: Array.isArray(currentSession.messages) ? currentSession.messages.length : 0
|
|
3598
3959
|
},
|
|
3599
3960
|
...initialSessions.map((session) => ({
|
|
3600
3961
|
id: session.id,
|
|
3962
|
+
title: session.title || '',
|
|
3601
3963
|
messageCount: Number(session.messageCount || 0)
|
|
3602
3964
|
}))
|
|
3603
3965
|
];
|
|
@@ -3620,6 +3982,7 @@ export async function createChatRuntime({
|
|
|
3620
3982
|
'gateway.base_url',
|
|
3621
3983
|
'gateway.api_key',
|
|
3622
3984
|
'model.name',
|
|
3985
|
+
'model.fast_name',
|
|
3623
3986
|
'ui.language',
|
|
3624
3987
|
'ui.reply_language',
|
|
3625
3988
|
'execution.mode',
|
|
@@ -3648,6 +4011,7 @@ export async function createChatRuntime({
|
|
|
3648
4011
|
const commandPriorityOrder = [
|
|
3649
4012
|
'/help',
|
|
3650
4013
|
'/status',
|
|
4014
|
+
'/model',
|
|
3651
4015
|
'/config',
|
|
3652
4016
|
'/memory',
|
|
3653
4017
|
'/capture',
|
|
@@ -3672,6 +4036,7 @@ export async function createChatRuntime({
|
|
|
3672
4036
|
{ name: 'exit', description: completionCopy.commands.exit },
|
|
3673
4037
|
{ name: 'commands', description: completionCopy.commands.commands },
|
|
3674
4038
|
{ name: 'status', description: completionCopy.commands.status },
|
|
4039
|
+
{ name: 'model', description: completionCopy.commands.model },
|
|
3675
4040
|
{ name: 'mode', description: completionCopy.commands.mode },
|
|
3676
4041
|
{ name: 'compact', description: completionCopy.commands.compact },
|
|
3677
4042
|
{ name: 'checkpoint', description: completionCopy.commands.checkpoint },
|
|
@@ -3722,6 +4087,7 @@ export async function createChatRuntime({
|
|
|
3722
4087
|
const historyTemplates = ['/history list', '/history current', '/history resume <session_id>'];
|
|
3723
4088
|
const memoryTemplates = ['/memory list <scope>', '/memory search <scope> <query>', '/memory forget <scope> <id>'];
|
|
3724
4089
|
const modeTemplates = ['/mode normal', '/mode auto', '/mode plan'];
|
|
4090
|
+
const modelTemplates = ['/model current', '/model main', '/model fast', '/model set <name>'];
|
|
3725
4091
|
const checkpointTemplates = [
|
|
3726
4092
|
'/checkpoint create <name>',
|
|
3727
4093
|
'/checkpoint list',
|
|
@@ -3740,6 +4106,7 @@ export async function createChatRuntime({
|
|
|
3740
4106
|
...memoryTemplates,
|
|
3741
4107
|
...historyTemplates,
|
|
3742
4108
|
...modeTemplates,
|
|
4109
|
+
...modelTemplates,
|
|
3743
4110
|
...checkpointTemplates,
|
|
3744
4111
|
...specTemplates,
|
|
3745
4112
|
...planTemplates,
|
|
@@ -3787,6 +4154,7 @@ export async function createChatRuntime({
|
|
|
3787
4154
|
'memory',
|
|
3788
4155
|
'compact',
|
|
3789
4156
|
'mode',
|
|
4157
|
+
'model',
|
|
3790
4158
|
'checkpoint',
|
|
3791
4159
|
'plan',
|
|
3792
4160
|
'agents',
|
|
@@ -3806,6 +4174,7 @@ export async function createChatRuntime({
|
|
|
3806
4174
|
for (const template of memoryTemplates) registerSuggestion(template, completionCopy.generic.memoryCommand);
|
|
3807
4175
|
for (const template of historyTemplates) registerSuggestion(template, completionCopy.generic.historyCommand);
|
|
3808
4176
|
for (const template of modeTemplates) registerSuggestion(template, completionCopy.generic.modeCommand);
|
|
4177
|
+
for (const template of modelTemplates) registerSuggestion(template, completionCopy.generic.modelCommand || completionCopy.commands.model);
|
|
3809
4178
|
for (const template of checkpointTemplates) registerSuggestion(template, completionCopy.generic.checkpointCommand);
|
|
3810
4179
|
for (const template of specTemplates) registerSuggestion(template, completionCopy.generic.specCommand);
|
|
3811
4180
|
for (const template of planTemplates) {
|
|
@@ -3900,6 +4269,15 @@ export async function createChatRuntime({
|
|
|
3900
4269
|
if (commandPart === 'status') {
|
|
3901
4270
|
return [registerSuggestion('/status', completionCopy.generic.statusCommand)];
|
|
3902
4271
|
}
|
|
4272
|
+
if (commandPart === 'model') {
|
|
4273
|
+
if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
|
|
4274
|
+
const sub = tokens[1] || '';
|
|
4275
|
+
return ['current', 'main', 'fast', 'set']
|
|
4276
|
+
.filter((m) => m.startsWith(sub))
|
|
4277
|
+
.map((m) => registerSuggestion(`/model ${m}${m === 'set' ? ' ' : ''}`, completionCopy.generic.modelCommand));
|
|
4278
|
+
}
|
|
4279
|
+
return materializeSuggestions(modelTemplates);
|
|
4280
|
+
}
|
|
3903
4281
|
if (commandPart === 'mode') {
|
|
3904
4282
|
if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
|
|
3905
4283
|
const sub = tokens[1] || '';
|
|
@@ -3984,7 +4362,7 @@ export async function createChatRuntime({
|
|
|
3984
4362
|
.filter((session) => String(session.id || '').startsWith(''))
|
|
3985
4363
|
.map((session) => ({
|
|
3986
4364
|
value: `/history resume ${session.id}`,
|
|
3987
|
-
display: `/history resume ${session.id} · ${Number(session.messageCount || 0)} msgs`,
|
|
4365
|
+
display: `/history resume ${session.id} · ${session.title || 'untitled'} · ${Number(session.messageCount || 0)} msgs`,
|
|
3988
4366
|
description: completionCopy.generic.resumeSession
|
|
3989
4367
|
}));
|
|
3990
4368
|
if (dynamic.length > 0) return dynamic;
|
|
@@ -3999,7 +4377,7 @@ export async function createChatRuntime({
|
|
|
3999
4377
|
.filter((session) => String(session.id || '').startsWith(idPrefix))
|
|
4000
4378
|
.map((session) => ({
|
|
4001
4379
|
value: `/history resume ${session.id}`,
|
|
4002
|
-
display: `/history resume ${session.id} · ${Number(session.messageCount || 0)} msgs`,
|
|
4380
|
+
display: `/history resume ${session.id} · ${session.title || 'untitled'} · ${Number(session.messageCount || 0)} msgs`,
|
|
4003
4381
|
description: completionCopy.generic.resumeSession
|
|
4004
4382
|
}));
|
|
4005
4383
|
if (dynamic.length > 0) return dynamic;
|
|
@@ -4038,22 +4416,41 @@ export async function createChatRuntime({
|
|
|
4038
4416
|
if (systemText) {
|
|
4039
4417
|
currentSession.messages.push(stampedMessage('system', systemText));
|
|
4040
4418
|
}
|
|
4419
|
+
if (shouldReplaceSessionTitle(currentSession.title)) {
|
|
4420
|
+
currentSession.title = deriveSessionTitle(currentSession.messages);
|
|
4421
|
+
}
|
|
4422
|
+
currentSession.model = model || config.model.name;
|
|
4423
|
+
currentSession.mode = executionMode || config.execution?.mode || 'normal';
|
|
4041
4424
|
await saveSession(currentSession);
|
|
4042
4425
|
};
|
|
4043
4426
|
|
|
4044
|
-
const persistAssistantExchange = async (userText, assistantText, { includeUser = true } = {}) => {
|
|
4427
|
+
const persistAssistantExchange = async (userText, assistantText, { includeUser = true, extra = {} } = {}) => {
|
|
4045
4428
|
if (includeUser && userText) {
|
|
4046
4429
|
currentSession.messages.push(stampedMessage('user', userText));
|
|
4047
4430
|
}
|
|
4048
4431
|
if (assistantText) {
|
|
4049
|
-
currentSession.messages.push(stampedMessage('assistant', assistantText));
|
|
4432
|
+
currentSession.messages.push(stampedMessage('assistant', assistantText, extra));
|
|
4433
|
+
}
|
|
4434
|
+
if (shouldReplaceSessionTitle(currentSession.title)) {
|
|
4435
|
+
currentSession.title = await generateSessionTitle({
|
|
4436
|
+
userText,
|
|
4437
|
+
assistantText,
|
|
4438
|
+
config
|
|
4439
|
+
});
|
|
4050
4440
|
}
|
|
4441
|
+
currentSession.model = model || config.model.name;
|
|
4442
|
+
currentSession.mode = executionMode || config.execution?.mode || 'normal';
|
|
4051
4443
|
await saveSession(currentSession);
|
|
4052
4444
|
};
|
|
4053
4445
|
|
|
4054
4446
|
const persistUserExchange = async (userText) => {
|
|
4055
4447
|
if (!userText) return;
|
|
4056
4448
|
currentSession.messages.push(stampedMessage('user', userText));
|
|
4449
|
+
if (shouldReplaceSessionTitle(currentSession.title)) {
|
|
4450
|
+
currentSession.title = deriveSessionTitle(currentSession.messages);
|
|
4451
|
+
}
|
|
4452
|
+
currentSession.model = model || config.model.name;
|
|
4453
|
+
currentSession.mode = executionMode || config.execution?.mode || 'normal';
|
|
4057
4454
|
await saveSession(currentSession);
|
|
4058
4455
|
};
|
|
4059
4456
|
|
|
@@ -4187,12 +4584,13 @@ export async function createChatRuntime({
|
|
|
4187
4584
|
let activeAbortController = null;
|
|
4188
4585
|
let activeSubSession = null;
|
|
4189
4586
|
|
|
4190
|
-
const submit = async (line, onAgentEvent) => {
|
|
4587
|
+
const submit = async (line, onAgentEvent, options = {}) => {
|
|
4191
4588
|
// 每次提交创建新的 AbortController,替代旧的
|
|
4192
4589
|
activeAbortController = new AbortController();
|
|
4193
4590
|
const { signal } = activeAbortController;
|
|
4194
4591
|
const activeReplySystemPrompt = await buildActiveSystemPrompt();
|
|
4195
4592
|
const parsedInput = parseInput(line);
|
|
4593
|
+
const readOnlyCodeWiki = options?.readOnlyCodeWiki === true;
|
|
4196
4594
|
const maybeAutoDreamFromRuntime = async () => {
|
|
4197
4595
|
const threshold = Number(config?.memory?.auto_dream_threshold ?? 10);
|
|
4198
4596
|
if (!(threshold > 0)) return null;
|
|
@@ -4226,7 +4624,7 @@ export async function createChatRuntime({
|
|
|
4226
4624
|
}
|
|
4227
4625
|
};
|
|
4228
4626
|
try {
|
|
4229
|
-
if (shouldPersistInputHistory(parsedInput)) {
|
|
4627
|
+
if (!readOnlyCodeWiki && shouldPersistInputHistory(parsedInput)) {
|
|
4230
4628
|
await appendInputHistory(line);
|
|
4231
4629
|
}
|
|
4232
4630
|
} catch {
|
|
@@ -4235,6 +4633,34 @@ export async function createChatRuntime({
|
|
|
4235
4633
|
if (parsedInput.type === 'empty') {
|
|
4236
4634
|
return { type: 'noop' };
|
|
4237
4635
|
}
|
|
4636
|
+
if (readOnlyCodeWiki) {
|
|
4637
|
+
const expandedText = await expandFileMentions(line, process.cwd());
|
|
4638
|
+
const readOnlySystemPrompt = [
|
|
4639
|
+
activeReplySystemPrompt,
|
|
4640
|
+
'CodeWiki repository Q&A mode:',
|
|
4641
|
+
'- Answer questions about the current repository and generated CodeWiki/project-requirements report.',
|
|
4642
|
+
'- This is read-only. Do not modify files, update plans, run shell commands, delete anything, write memories, or ask to perform side effects.',
|
|
4643
|
+
'- Use only read-only project inspection tools when evidence is needed.',
|
|
4644
|
+
'- Be concise and cite relevant files or report sections when useful.'
|
|
4645
|
+
].join('\n\n');
|
|
4646
|
+
const transientSession = structuredClone(currentSession);
|
|
4647
|
+
const result = await askModel({
|
|
4648
|
+
text: expandedText,
|
|
4649
|
+
session: transientSession,
|
|
4650
|
+
config,
|
|
4651
|
+
model,
|
|
4652
|
+
systemPrompt: readOnlySystemPrompt,
|
|
4653
|
+
onAgentEvent,
|
|
4654
|
+
requestToolApproval: activeRequestToolApproval,
|
|
4655
|
+
executionMode: 'auto',
|
|
4656
|
+
alwaysAllowTools: CODEWIKI_READ_ONLY_TOOLS,
|
|
4657
|
+
allowedTools: CODEWIKI_READ_ONLY_TOOLS,
|
|
4658
|
+
persistSession: false,
|
|
4659
|
+
maxSteps: 32,
|
|
4660
|
+
signal
|
|
4661
|
+
});
|
|
4662
|
+
return { type: 'assistant', text: result.text, aborted: !!result.aborted };
|
|
4663
|
+
}
|
|
4238
4664
|
if (parsedInput.type === 'shell') {
|
|
4239
4665
|
const shell = await handleShellInput(parsedInput.command, config);
|
|
4240
4666
|
return { type: 'shell', text: shell.text };
|
|
@@ -4244,12 +4670,12 @@ export async function createChatRuntime({
|
|
|
4244
4670
|
if (parsedInput.command === 'new') {
|
|
4245
4671
|
const fresh = await createSession();
|
|
4246
4672
|
currentSession = fresh;
|
|
4247
|
-
executionMode = config.execution?.mode || '
|
|
4673
|
+
executionMode = config.execution?.mode || 'normal';
|
|
4248
4674
|
compactState.backupMessages = null;
|
|
4249
4675
|
setResultDir(path.join(getSessionsDir(), String(fresh.id)));
|
|
4250
4676
|
historyIdCache = [fresh.id, ...historyIdCache.filter((id) => id !== fresh.id)];
|
|
4251
4677
|
historySessionCache = [
|
|
4252
|
-
{ id: fresh.id, messageCount: 0 },
|
|
4678
|
+
{ id: fresh.id, title: fresh.title || '', messageCount: 0 },
|
|
4253
4679
|
...historySessionCache.filter((s) => s.id !== fresh.id)
|
|
4254
4680
|
];
|
|
4255
4681
|
return {
|
|
@@ -4261,7 +4687,7 @@ export async function createChatRuntime({
|
|
|
4261
4687
|
if (parsedInput.command === 'help') {
|
|
4262
4688
|
return {
|
|
4263
4689
|
type: 'system',
|
|
4264
|
-
text: 'Commands: /help /exit /new /stop /commands /status /mode /compact /checkpoint /spec /plan /yes /no /edit /reject /agents /config /memory /capture /inbox /dream /reflect /history /debug /retry /<custom> !<shell>'
|
|
4690
|
+
text: 'Commands: /help /exit /new /stop /commands /status /model /mode /compact /checkpoint /spec /plan /yes /no /edit /reject /agents /config /memory /capture /inbox /dream /reflect /history /debug /retry /<custom> !<shell>'
|
|
4265
4691
|
};
|
|
4266
4692
|
}
|
|
4267
4693
|
if (parsedInput.command === 'status') {
|
|
@@ -4271,6 +4697,31 @@ export async function createChatRuntime({
|
|
|
4271
4697
|
text: `mode=${executionMode} | role=general | model=${model || config.model.name} | max_ctx=${effectiveMaxContextTokens(config)} | session=${currentSession.id} | todos=${todoCount}`
|
|
4272
4698
|
};
|
|
4273
4699
|
}
|
|
4700
|
+
if (parsedInput.command === 'model') {
|
|
4701
|
+
const sub = String(parsedInput.args[0] || 'current').trim().toLowerCase();
|
|
4702
|
+
const mainModel = resolveDefaultModel(config);
|
|
4703
|
+
const fastModel = resolveFastModel(config);
|
|
4704
|
+
if (sub === 'current' || sub === 'status') {
|
|
4705
|
+
return {
|
|
4706
|
+
type: 'system',
|
|
4707
|
+
text: `Current model: ${model || mainModel}\nDefault model: ${mainModel}\nFast model: ${fastModel}${config.model?.fast_name ? '' : ' (fallback to default; set /config set model.fast_name <name>)'}`
|
|
4708
|
+
};
|
|
4709
|
+
}
|
|
4710
|
+
if (sub === 'main' || sub === 'default') {
|
|
4711
|
+
model = mainModel;
|
|
4712
|
+
} else if (sub === 'fast') {
|
|
4713
|
+
model = fastModel;
|
|
4714
|
+
} else if (sub === 'set') {
|
|
4715
|
+
const next = parsedInput.args.slice(1).join(' ').trim();
|
|
4716
|
+
if (!next) return { type: 'system', text: 'Usage: /model set <name>' };
|
|
4717
|
+
model = next;
|
|
4718
|
+
} else {
|
|
4719
|
+
return { type: 'system', text: 'Usage: /model current | /model main | /model fast | /model set <name>' };
|
|
4720
|
+
}
|
|
4721
|
+
currentSession.model = model;
|
|
4722
|
+
await saveSession(currentSession);
|
|
4723
|
+
return { type: 'system', text: `Model switched to: ${model}` };
|
|
4724
|
+
}
|
|
4274
4725
|
if (parsedInput.command === 'mode') {
|
|
4275
4726
|
const next = (parsedInput.args[0] || '').trim().toLowerCase();
|
|
4276
4727
|
if (!next) {
|
|
@@ -4325,9 +4776,13 @@ export async function createChatRuntime({
|
|
|
4325
4776
|
});
|
|
4326
4777
|
activeSubSession = null;
|
|
4327
4778
|
currentSession.planState = null;
|
|
4779
|
+
if (onAgentEvent) onAgentEvent({ type: 'plan:approval_cleared' });
|
|
4328
4780
|
await removePlanFileIfPresent(planState);
|
|
4329
4781
|
executionMode = 'auto';
|
|
4330
|
-
await persistAssistantExchange(line, result.text || '', {
|
|
4782
|
+
await persistAssistantExchange(line, result.sessionText || result.text || '', {
|
|
4783
|
+
includeUser: false,
|
|
4784
|
+
extra: Array.isArray(result.transcript) ? { plan_transcript: result.transcript } : {}
|
|
4785
|
+
});
|
|
4331
4786
|
return { type: 'assistant', text: result.text, aborted: !!result.aborted };
|
|
4332
4787
|
}
|
|
4333
4788
|
if (parsedInput.command === 'edit') {
|
|
@@ -4376,6 +4831,15 @@ export async function createChatRuntime({
|
|
|
4376
4831
|
});
|
|
4377
4832
|
currentSession.planState = revised;
|
|
4378
4833
|
executionMode = 'plan';
|
|
4834
|
+
if (onAgentEvent) {
|
|
4835
|
+
onAgentEvent({
|
|
4836
|
+
type: 'plan:pending_approval',
|
|
4837
|
+
goal: currentSession.planState.goal,
|
|
4838
|
+
summary: currentSession.planState.finalSummary || currentSession.planState.summary,
|
|
4839
|
+
filePath: currentSession.planState.filePath,
|
|
4840
|
+
steps: currentSession.planState.steps
|
|
4841
|
+
});
|
|
4842
|
+
}
|
|
4379
4843
|
const text = `Plan revised.\n${buildPendingPlanApprovalMessage(currentSession.planState)}`;
|
|
4380
4844
|
await persistLocalExchange(line, text);
|
|
4381
4845
|
return { type: 'system', text };
|
|
@@ -4390,6 +4854,7 @@ export async function createChatRuntime({
|
|
|
4390
4854
|
}
|
|
4391
4855
|
if (hasPendingPlanApproval(currentSession)) {
|
|
4392
4856
|
currentSession.planState = null;
|
|
4857
|
+
if (onAgentEvent) onAgentEvent({ type: 'plan:approval_cleared' });
|
|
4393
4858
|
executionMode = 'auto';
|
|
4394
4859
|
const text = 'Pending plan rejected and cleared.';
|
|
4395
4860
|
await persistLocalExchange(line, text, { includeUser: false });
|
|
@@ -4403,6 +4868,7 @@ export async function createChatRuntime({
|
|
|
4403
4868
|
}
|
|
4404
4869
|
const planState = { ...currentSession.planState };
|
|
4405
4870
|
currentSession.planState = null;
|
|
4871
|
+
if (onAgentEvent) onAgentEvent({ type: 'plan:approval_cleared' });
|
|
4406
4872
|
await removePlanFileIfPresent(planState);
|
|
4407
4873
|
executionMode = 'auto';
|
|
4408
4874
|
const text = 'Pending plan rejected and cleared.';
|
|
@@ -4518,6 +4984,15 @@ export async function createChatRuntime({
|
|
|
4518
4984
|
steps: Array.isArray(auto.steps) ? auto.steps : []
|
|
4519
4985
|
};
|
|
4520
4986
|
executionMode = 'plan';
|
|
4987
|
+
if (onAgentEvent) {
|
|
4988
|
+
onAgentEvent({
|
|
4989
|
+
type: 'plan:pending_approval',
|
|
4990
|
+
goal: currentSession.planState.goal,
|
|
4991
|
+
summary: currentSession.planState.finalSummary || currentSession.planState.summary,
|
|
4992
|
+
filePath: currentSession.planState.filePath,
|
|
4993
|
+
steps: currentSession.planState.steps
|
|
4994
|
+
});
|
|
4995
|
+
}
|
|
4521
4996
|
const text = buildAutoPlanSystemSummary(auto);
|
|
4522
4997
|
await persistLocalExchange(line, text);
|
|
4523
4998
|
return {
|
|
@@ -4543,9 +5018,13 @@ export async function createChatRuntime({
|
|
|
4543
5018
|
});
|
|
4544
5019
|
activeSubSession = null;
|
|
4545
5020
|
currentSession.planState = null;
|
|
5021
|
+
if (onAgentEvent) onAgentEvent({ type: 'plan:approval_cleared' });
|
|
4546
5022
|
await removePlanFileIfPresent(planState);
|
|
4547
5023
|
executionMode = 'auto';
|
|
4548
|
-
await persistAssistantExchange(line, result.text || '', {
|
|
5024
|
+
await persistAssistantExchange(line, result.sessionText || result.text || '', {
|
|
5025
|
+
includeUser: false,
|
|
5026
|
+
extra: Array.isArray(result.transcript) ? { plan_transcript: result.transcript } : {}
|
|
5027
|
+
});
|
|
4549
5028
|
return { type: 'assistant', text: result.text, aborted: !!result.aborted };
|
|
4550
5029
|
}
|
|
4551
5030
|
if (sub === 'stay') {
|
|
@@ -4652,6 +5131,7 @@ export async function createChatRuntime({
|
|
|
4652
5131
|
historyIdCache = sessions.map((s) => s.id);
|
|
4653
5132
|
historySessionCache = sessions.map((s) => ({
|
|
4654
5133
|
id: s.id,
|
|
5134
|
+
title: s.title || '',
|
|
4655
5135
|
messageCount: Number(s.messageCount || 0)
|
|
4656
5136
|
}));
|
|
4657
5137
|
if (sessions.length === 0) return { type: 'system', text: 'No sessions found' };
|
|
@@ -4677,7 +5157,7 @@ export async function createChatRuntime({
|
|
|
4677
5157
|
}
|
|
4678
5158
|
if (!historyIdCache.includes(targetId)) historyIdCache.unshift(targetId);
|
|
4679
5159
|
historySessionCache = [
|
|
4680
|
-
{ id: targetId, messageCount: Array.isArray(loaded.messages) ? loaded.messages.length : 0 },
|
|
5160
|
+
{ id: targetId, title: loaded.title || deriveSessionTitle(loaded.messages || []), messageCount: Array.isArray(loaded.messages) ? loaded.messages.length : 0 },
|
|
4681
5161
|
...historySessionCache.filter((s) => s.id !== targetId)
|
|
4682
5162
|
];
|
|
4683
5163
|
return {
|
|
@@ -4998,7 +5478,8 @@ export async function createChatRuntime({
|
|
|
4998
5478
|
let result;
|
|
4999
5479
|
try {
|
|
5000
5480
|
result = await askModel({
|
|
5001
|
-
text: rendered,
|
|
5481
|
+
text: custom.metadata.type === 'skill' ? line : rendered,
|
|
5482
|
+
modelText: custom.metadata.type === 'skill' ? rendered : undefined,
|
|
5002
5483
|
session: currentSession,
|
|
5003
5484
|
config,
|
|
5004
5485
|
model,
|
|
@@ -5015,8 +5496,12 @@ export async function createChatRuntime({
|
|
|
5015
5496
|
name: custom.name,
|
|
5016
5497
|
summary: error instanceof Error ? error.message : String(error)
|
|
5017
5498
|
});
|
|
5499
|
+
onAgentEvent({ type: 'skill:end', name: custom.name });
|
|
5018
5500
|
}
|
|
5019
|
-
|
|
5501
|
+
return {
|
|
5502
|
+
type: 'system',
|
|
5503
|
+
text: `Skill "${custom.name}" failed: ${error instanceof Error ? error.message : String(error)}`
|
|
5504
|
+
};
|
|
5020
5505
|
}
|
|
5021
5506
|
if (custom.metadata.type === 'skill' && onAgentEvent) {
|
|
5022
5507
|
onAgentEvent({ type: 'skill:end', name: custom.name });
|
|
@@ -5040,8 +5525,12 @@ export async function createChatRuntime({
|
|
|
5040
5525
|
});
|
|
5041
5526
|
activeSubSession = null;
|
|
5042
5527
|
currentSession.planState = null;
|
|
5528
|
+
if (onAgentEvent) onAgentEvent({ type: 'plan:approval_cleared' });
|
|
5043
5529
|
executionMode = 'auto';
|
|
5044
|
-
await persistAssistantExchange(line, result.text || '', {
|
|
5530
|
+
await persistAssistantExchange(line, result.sessionText || result.text || '', {
|
|
5531
|
+
includeUser: false,
|
|
5532
|
+
extra: Array.isArray(result.transcript) ? { plan_transcript: result.transcript } : {}
|
|
5533
|
+
});
|
|
5045
5534
|
return { type: 'assistant', text: result.text, aborted: !!result.aborted };
|
|
5046
5535
|
}
|
|
5047
5536
|
if (isStayInPlanText(parsedInput.text)) {
|
|
@@ -5051,6 +5540,7 @@ export async function createChatRuntime({
|
|
|
5051
5540
|
}
|
|
5052
5541
|
if (isRejectPlanText(parsedInput.text)) {
|
|
5053
5542
|
currentSession.planState = null;
|
|
5543
|
+
if (onAgentEvent) onAgentEvent({ type: 'plan:approval_cleared' });
|
|
5054
5544
|
executionMode = 'auto';
|
|
5055
5545
|
const text = 'Pending plan rejected and cleared.';
|
|
5056
5546
|
await persistLocalExchange(line, text);
|
|
@@ -5062,6 +5552,43 @@ export async function createChatRuntime({
|
|
|
5062
5552
|
};
|
|
5063
5553
|
}
|
|
5064
5554
|
|
|
5555
|
+
// Plan mode with no pending plan → auto-generate structured plan
|
|
5556
|
+
if (executionMode === 'plan') {
|
|
5557
|
+
const expandedPlanText = await expandFileMentions(parsedInput.text, process.cwd());
|
|
5558
|
+
await maybeAutoDreamFromRuntime();
|
|
5559
|
+
const auto = await buildAutoPlanAndRun({
|
|
5560
|
+
goal: expandedPlanText,
|
|
5561
|
+
session: currentSession,
|
|
5562
|
+
config,
|
|
5563
|
+
model,
|
|
5564
|
+
systemPrompt: activeReplySystemPrompt,
|
|
5565
|
+
onAgentEvent,
|
|
5566
|
+
sessionId: currentSession.id,
|
|
5567
|
+
taskClass: classifyPlanTaskClass(expandedPlanText)
|
|
5568
|
+
});
|
|
5569
|
+
currentSession.planState = {
|
|
5570
|
+
status: 'pending_approval',
|
|
5571
|
+
source: 'auto',
|
|
5572
|
+
goal: expandedPlanText,
|
|
5573
|
+
filePath: auto.filePath,
|
|
5574
|
+
summary: auto.summary || '',
|
|
5575
|
+
finalSummary: auto.finalSummary || auto.summary || '',
|
|
5576
|
+
steps: Array.isArray(auto.steps) ? auto.steps : []
|
|
5577
|
+
};
|
|
5578
|
+
if (onAgentEvent) {
|
|
5579
|
+
onAgentEvent({
|
|
5580
|
+
type: 'plan:pending_approval',
|
|
5581
|
+
goal: currentSession.planState.goal,
|
|
5582
|
+
summary: currentSession.planState.finalSummary || currentSession.planState.summary,
|
|
5583
|
+
filePath: currentSession.planState.filePath,
|
|
5584
|
+
steps: currentSession.planState.steps
|
|
5585
|
+
});
|
|
5586
|
+
}
|
|
5587
|
+
const text = buildAutoPlanSystemSummary(auto);
|
|
5588
|
+
await persistLocalExchange(line, text);
|
|
5589
|
+
return { type: 'system', text };
|
|
5590
|
+
}
|
|
5591
|
+
|
|
5065
5592
|
if (compactState.autoEnabled) {
|
|
5066
5593
|
const currentTokens = estimateMessagesTokens(currentSession.messages);
|
|
5067
5594
|
const maxTokens = effectiveMaxContextTokens(config);
|
|
@@ -5166,6 +5693,18 @@ export async function createChatRuntime({
|
|
|
5166
5693
|
consumeStartupEvents: () => startupEvents.splice(0, startupEvents.length),
|
|
5167
5694
|
getInputHistory: () => loadInputHistory(),
|
|
5168
5695
|
getCurrentSessionId: () => currentSession.id,
|
|
5696
|
+
getSessionMessages: () => currentSession.messages || [],
|
|
5697
|
+
reloadConfig: async () => {
|
|
5698
|
+
config = await loadConfig();
|
|
5699
|
+
return config;
|
|
5700
|
+
},
|
|
5701
|
+
setExecutionMode: async (next) => {
|
|
5702
|
+
if (!['normal', 'auto', 'plan'].includes(next)) return false;
|
|
5703
|
+
executionMode = next;
|
|
5704
|
+
await setConfigValue('execution.mode', next);
|
|
5705
|
+
config = await loadConfig();
|
|
5706
|
+
return true;
|
|
5707
|
+
},
|
|
5169
5708
|
setRequestToolApproval: (handler) => {
|
|
5170
5709
|
activeRequestToolApproval = typeof handler === 'function' ? handler : null;
|
|
5171
5710
|
return true;
|