codemini-cli 0.1.12 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/core/agent-loop.js +17 -0
- package/src/core/chat-runtime.js +364 -44
- package/src/core/config-store.js +39 -3
- package/src/core/reply-language.js +25 -0
- package/src/core/shell-profile.js +1 -1
- package/src/core/soul.js +3 -1
- package/src/core/tools.js +884 -8
- package/src/tui/chat-app.js +89 -15
package/src/core/chat-runtime.js
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
estimateMessagesTokens,
|
|
27
27
|
parseCompactArgs
|
|
28
28
|
} from './context-compact.js';
|
|
29
|
+
import { buildSystemPromptWithReplyLanguage } from './reply-language.js';
|
|
29
30
|
import { buildSystemPromptWithSoul } from './soul.js';
|
|
30
31
|
|
|
31
32
|
function toOpenAIMessages(sessionMessages) {
|
|
@@ -60,6 +61,46 @@ function nowStamp() {
|
|
|
60
61
|
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
function prioritizeByPreferredOrder(items, preferredOrder) {
|
|
65
|
+
const source = Array.isArray(items) ? items : [];
|
|
66
|
+
const priorities = new Map((Array.isArray(preferredOrder) ? preferredOrder : []).map((value, index) => [value, index]));
|
|
67
|
+
return [...source].sort((left, right) => {
|
|
68
|
+
const leftRank = priorities.has(left) ? priorities.get(left) : Number.MAX_SAFE_INTEGER;
|
|
69
|
+
const rightRank = priorities.has(right) ? priorities.get(right) : Number.MAX_SAFE_INTEGER;
|
|
70
|
+
if (leftRank !== rightRank) return leftRank - rightRank;
|
|
71
|
+
return source.indexOf(left) - source.indexOf(right);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function describeConfigKey(key, mode = 'set') {
|
|
76
|
+
const labelMap = {
|
|
77
|
+
'gateway.base_url': 'gateway base URL',
|
|
78
|
+
'gateway.api_key': 'gateway API key',
|
|
79
|
+
'gateway.timeout_ms': 'gateway timeout in milliseconds',
|
|
80
|
+
'gateway.max_retries': 'gateway retry count',
|
|
81
|
+
'model.name': 'active model name',
|
|
82
|
+
'model.max_context_tokens': 'model context token limit',
|
|
83
|
+
'ui.reply_language': 'reply language',
|
|
84
|
+
'execution.mode': 'execution mode',
|
|
85
|
+
'execution.always_allow_tools': 'always-allowed tools',
|
|
86
|
+
'execution.max_steps': 'maximum tool steps',
|
|
87
|
+
'context.preflight_trigger_pct': 'preflight compact threshold',
|
|
88
|
+
'context.hard_limit_pct': 'hard compact threshold',
|
|
89
|
+
'context.tool_result_max_chars': 'tool result character limit',
|
|
90
|
+
'context.read_file_default_lines': 'default read_file line window',
|
|
91
|
+
'context.read_file_max_chars': 'read_file character limit',
|
|
92
|
+
'sessions.max_sessions': 'stored session limit',
|
|
93
|
+
'sessions.retention_days': 'session retention days',
|
|
94
|
+
'shell.default': 'default shell',
|
|
95
|
+
'shell.timeout_ms': 'shell timeout in milliseconds',
|
|
96
|
+
'context.max_tokens': 'context token budget',
|
|
97
|
+
'policy.safe_mode': 'safe mode switch',
|
|
98
|
+
'policy.allow_dangerous_commands': 'dangerous command allowance'
|
|
99
|
+
};
|
|
100
|
+
const label = labelMap[key] || key;
|
|
101
|
+
return mode === 'get' ? `show the ${label}` : `set the ${label}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
63
104
|
const SUB_AGENT_ROLES = ['planner', 'coder', 'reviewer', 'tester'];
|
|
64
105
|
const SUB_AGENT_CONTEXT_MAX_MESSAGES = 4;
|
|
65
106
|
const SUB_AGENT_CONTEXT_MAX_CHARS = 1200;
|
|
@@ -76,6 +117,8 @@ function getSubAgentRolePrompt(role) {
|
|
|
76
117
|
'You are a review sub-agent. Focus on bugs, regressions, edge cases, and missing tests.',
|
|
77
118
|
'Start with the focused files or directories handed to you. Do not roam unrelated parts of the repo unless the handed-off evidence is insufficient.',
|
|
78
119
|
'Use this exact output structure:',
|
|
120
|
+
'Acceptance Status:',
|
|
121
|
+
'- <met|unmet|unverified> :: <acceptance checklist item or "none">',
|
|
79
122
|
'Findings:',
|
|
80
123
|
'- <bug, regression, risk, or "none">',
|
|
81
124
|
'Verified:',
|
|
@@ -92,6 +135,8 @@ function getSubAgentRolePrompt(role) {
|
|
|
92
135
|
'Prefer running concrete verification commands over only suggesting them.',
|
|
93
136
|
'Start with the focused files or directories handed to you. Verify those artifacts first before scanning the wider repo.',
|
|
94
137
|
'Use this exact output structure:',
|
|
138
|
+
'Acceptance Status:',
|
|
139
|
+
'- <met|unmet|unverified> :: <acceptance checklist item or "none">',
|
|
95
140
|
'Verified:',
|
|
96
141
|
'- <commands run and evidence>',
|
|
97
142
|
'Not Verified:',
|
|
@@ -277,6 +322,114 @@ function buildFocusedTaskNote(role, focusPaths) {
|
|
|
277
322
|
return '';
|
|
278
323
|
}
|
|
279
324
|
|
|
325
|
+
function normalizeGoalClauseText(value) {
|
|
326
|
+
return String(value || '')
|
|
327
|
+
.replace(/^[\s\-*0-9.)、,,:;]+/g, '')
|
|
328
|
+
.replace(/\s+/g, ' ')
|
|
329
|
+
.trim();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function sentenceCaseRequirement(value) {
|
|
333
|
+
const text = normalizeGoalClauseText(value);
|
|
334
|
+
if (!text) return '';
|
|
335
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function deriveGoalRequirements(goal) {
|
|
339
|
+
const rawGoal = String(goal || '').trim();
|
|
340
|
+
if (!rawGoal) return [];
|
|
341
|
+
|
|
342
|
+
const normalized = rawGoal
|
|
343
|
+
.replace(/\r\n?/g, '\n')
|
|
344
|
+
.replace(/[;。]/g, ',')
|
|
345
|
+
.replace(/\band then\b/gi, ',')
|
|
346
|
+
.replace(/\bthen\b/gi, ',')
|
|
347
|
+
.replace(/\bplus\b/gi, ',')
|
|
348
|
+
.replace(/\s+(?:and|并且|而且|以及)\s+/gi, ', ')
|
|
349
|
+
.replace(/\n+/g, ', ');
|
|
350
|
+
|
|
351
|
+
const roughParts = normalized
|
|
352
|
+
.split(/\s*,\s*/)
|
|
353
|
+
.map((part) => normalizeGoalClauseText(part))
|
|
354
|
+
.filter(Boolean);
|
|
355
|
+
|
|
356
|
+
const requirements = [];
|
|
357
|
+
const seen = new Set();
|
|
358
|
+
|
|
359
|
+
for (const part of roughParts) {
|
|
360
|
+
const lowered = part.toLowerCase();
|
|
361
|
+
if (/\btrim\b/.test(lowered) && !/\bwhitespace\b/.test(lowered)) {
|
|
362
|
+
const label = 'Trim whitespace in the returned greeting';
|
|
363
|
+
if (!seen.has(label)) {
|
|
364
|
+
seen.add(label);
|
|
365
|
+
requirements.push(label);
|
|
366
|
+
}
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (/\btrim\b/.test(lowered) && /\bwhitespace\b/.test(lowered)) {
|
|
370
|
+
const label = 'Trim whitespace in the returned greeting';
|
|
371
|
+
if (!seen.has(label)) {
|
|
372
|
+
seen.add(label);
|
|
373
|
+
requirements.push(label);
|
|
374
|
+
}
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (/(exclamation mark|感叹号|!)/i.test(part)) {
|
|
378
|
+
const label = 'Preserve the exclamation mark';
|
|
379
|
+
if (!seen.has(label)) {
|
|
380
|
+
seen.add(label);
|
|
381
|
+
requirements.push(label);
|
|
382
|
+
}
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const label = sentenceCaseRequirement(part);
|
|
386
|
+
if (!label || seen.has(label)) continue;
|
|
387
|
+
seen.add(label);
|
|
388
|
+
requirements.push(label);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (requirements.length === 0) {
|
|
392
|
+
return [sentenceCaseRequirement(rawGoal)].filter(Boolean);
|
|
393
|
+
}
|
|
394
|
+
return requirements.slice(0, 6);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function isLightweightAutoPlanGoal(goal, requirements = []) {
|
|
398
|
+
const text = String(goal || '').trim();
|
|
399
|
+
if (!text) return false;
|
|
400
|
+
if (requirements.length !== 1) return false;
|
|
401
|
+
if (text.length > 140) return false;
|
|
402
|
+
if (/\b(plan|spec|design|architecture|roadmap|strategy|migration|refactor)\b/i.test(text)) return false;
|
|
403
|
+
if (/\b(ensure|verify|review|test|validate|make sure|confirm)\b/i.test(text)) return false;
|
|
404
|
+
if (/[;。]/.test(text)) return false;
|
|
405
|
+
return /\b(add|update|fix|rename|trim|export|create|remove|change|implement)\b/i.test(text);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function buildGoalRequirementPacket(goal, role) {
|
|
409
|
+
const rawGoal = trimInlineText(goal, 800);
|
|
410
|
+
if (!rawGoal) return '';
|
|
411
|
+
const requirements = deriveGoalRequirements(goal);
|
|
412
|
+
const lines = ['Original goal:', rawGoal];
|
|
413
|
+
if (requirements.length > 0) {
|
|
414
|
+
lines.push('Acceptance checklist:');
|
|
415
|
+
for (const requirement of requirements) {
|
|
416
|
+
lines.push(`- ${requirement}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (role === 'reviewer') {
|
|
420
|
+
lines.push('Review against the original goal, not just local code quality.');
|
|
421
|
+
lines.push('Check each acceptance item explicitly before deciding there are no findings.');
|
|
422
|
+
lines.push('If any requested behavior is missing, incorrect, or only partially implemented, report it in Findings.');
|
|
423
|
+
} else if (role === 'tester') {
|
|
424
|
+
lines.push('Verify the implementation against the original goal, not just syntax or smoke checks.');
|
|
425
|
+
lines.push('Check each acceptance item explicitly before calling the work verified.');
|
|
426
|
+
lines.push('If any requested behavior is unverified or contradicted by evidence, list it under Not Verified or Failures.');
|
|
427
|
+
} else if (role === 'coder') {
|
|
428
|
+
lines.push('Implement against the acceptance checklist, not only the broad wording of the goal.');
|
|
429
|
+
}
|
|
430
|
+
return lines.join('\n');
|
|
431
|
+
}
|
|
432
|
+
|
|
280
433
|
async function pathExists(targetPath) {
|
|
281
434
|
try {
|
|
282
435
|
await fs.access(targetPath);
|
|
@@ -466,7 +619,16 @@ function normalizeAutoPlan(parsed, goal) {
|
|
|
466
619
|
|
|
467
620
|
function enforceAutoPlanGuardrailSteps(plan, goal) {
|
|
468
621
|
const source = Array.isArray(plan?.steps) ? plan.steps : [];
|
|
622
|
+
const requirements = deriveGoalRequirements(goal);
|
|
623
|
+
const lightweightGoal = isLightweightAutoPlanGoal(goal, requirements);
|
|
469
624
|
const implementationSteps = source.filter((step) => step.role !== 'reviewer' && step.role !== 'tester');
|
|
625
|
+
const primaryImplementationStep =
|
|
626
|
+
implementationSteps.find((step) => step.role === 'coder') ||
|
|
627
|
+
implementationSteps[0] || {
|
|
628
|
+
title: 'Implement requested change',
|
|
629
|
+
role: 'coder',
|
|
630
|
+
task: `Implement the requested change for: ${goal}`
|
|
631
|
+
};
|
|
470
632
|
const reviewerStep = source.find((step) => step.role === 'reviewer') || {
|
|
471
633
|
title: 'Review implementation',
|
|
472
634
|
role: 'reviewer',
|
|
@@ -478,6 +640,13 @@ function enforceAutoPlanGuardrailSteps(plan, goal) {
|
|
|
478
640
|
task: `Test and verify the completed work for: ${goal}. Start with the artifacts produced by earlier implementation steps, run the most relevant checks available, report concrete evidence, and call out anything still unverified.`
|
|
479
641
|
};
|
|
480
642
|
|
|
643
|
+
if (lightweightGoal) {
|
|
644
|
+
return {
|
|
645
|
+
summary: String(plan?.summary || `Auto plan for: ${goal}`).trim(),
|
|
646
|
+
steps: [primaryImplementationStep, testerStep]
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
481
650
|
return {
|
|
482
651
|
summary: String(plan?.summary || `Auto plan for: ${goal}`).trim(),
|
|
483
652
|
steps: [...implementationSteps.slice(0, 6), reviewerStep, testerStep]
|
|
@@ -487,11 +656,67 @@ function enforceAutoPlanGuardrailSteps(plan, goal) {
|
|
|
487
656
|
function looksLikeSuccessfulStepOutput(text = '') {
|
|
488
657
|
const value = String(text || '').trim();
|
|
489
658
|
if (!value) return false;
|
|
490
|
-
|
|
491
|
-
|
|
659
|
+
const failureBullet = extractSectionFirstBullet(value, 'Failures');
|
|
660
|
+
const errorBullet = extractSectionFirstBullet(value, 'Error');
|
|
661
|
+
const nextActionBullet = extractSectionFirstBullet(value, 'Next Action');
|
|
662
|
+
const acceptanceFailures = extractAcceptanceStatusItems(value).filter((item) => item.status !== 'met');
|
|
663
|
+
if (errorBullet && !/^none\b/i.test(errorBullet)) return false;
|
|
664
|
+
if (failureBullet && !/^none\b/i.test(failureBullet)) return false;
|
|
665
|
+
if (acceptanceFailures.length > 0) return false;
|
|
666
|
+
if (nextActionBullet && /^retry\b/i.test(nextActionBullet)) return false;
|
|
492
667
|
return true;
|
|
493
668
|
}
|
|
494
669
|
|
|
670
|
+
function stepOutputHasFailureSignals(role, text = '') {
|
|
671
|
+
const value = String(text || '').trim();
|
|
672
|
+
if (!value) return true;
|
|
673
|
+
const errorBullet = extractSectionFirstBullet(value, 'Error');
|
|
674
|
+
const failureBullet = extractSectionFirstBullet(value, 'Failures');
|
|
675
|
+
const findingsBullet = extractSectionFirstBullet(value, 'Findings');
|
|
676
|
+
const nextActionBullet = extractSectionFirstBullet(value, 'Next Action');
|
|
677
|
+
const acceptanceFailures = extractAcceptanceStatusItems(value).filter((item) => item.status !== 'met');
|
|
678
|
+
if (errorBullet && !/^none\b/i.test(errorBullet)) return true;
|
|
679
|
+
if (failureBullet && !/^none\b/i.test(failureBullet)) return true;
|
|
680
|
+
if (acceptanceFailures.length > 0) return true;
|
|
681
|
+
if (role === 'reviewer' && findingsBullet && !/^none\b/i.test(findingsBullet)) return true;
|
|
682
|
+
if (nextActionBullet && /^(fix|retry|correct|repair)\b/i.test(nextActionBullet)) return true;
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function extractSectionFirstBullet(text = '', heading = '') {
|
|
687
|
+
const escaped = String(heading || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
688
|
+
const match = String(text || '').match(new RegExp(String.raw`(^|\n)\s*${escaped}\s*:\s*(?:\n|\r\n?)+\s*-\s*([^\n\r]+)`, 'i'));
|
|
689
|
+
return String(match?.[2] || '').trim();
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function extractSectionBullets(text = '', heading = '') {
|
|
693
|
+
const escaped = String(heading || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
694
|
+
const value = String(text || '');
|
|
695
|
+
const headingMatch = value.match(new RegExp(String.raw`(^|\n)\s*${escaped}\s*:\s*(?:\n|\r\n?)`, 'i'));
|
|
696
|
+
if (!headingMatch || headingMatch.index == null) return [];
|
|
697
|
+
const start = headingMatch.index + headingMatch[0].length;
|
|
698
|
+
const after = value.slice(start);
|
|
699
|
+
const nextHeading = after.search(/\n\s*[A-Za-z][A-Za-z ]+\s*:\s*(?:\n|\r\n?)/);
|
|
700
|
+
const section = nextHeading === -1 ? after : after.slice(0, nextHeading);
|
|
701
|
+
return section
|
|
702
|
+
.split(/\r?\n/)
|
|
703
|
+
.map((line) => line.match(/^\s*-\s*(.+)$/)?.[1]?.trim() || '')
|
|
704
|
+
.filter(Boolean);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function extractAcceptanceStatusItems(text = '') {
|
|
708
|
+
return extractSectionBullets(text, 'Acceptance Status')
|
|
709
|
+
.map((item) => {
|
|
710
|
+
const match = String(item).match(/^(met|unmet|unverified)\s*::\s*(.+)$/i);
|
|
711
|
+
if (!match) return null;
|
|
712
|
+
return {
|
|
713
|
+
status: String(match[1] || '').toLowerCase(),
|
|
714
|
+
label: String(match[2] || '').trim()
|
|
715
|
+
};
|
|
716
|
+
})
|
|
717
|
+
.filter(Boolean);
|
|
718
|
+
}
|
|
719
|
+
|
|
495
720
|
function buildAutoPlanSystemSummary(auto) {
|
|
496
721
|
const statusTitle =
|
|
497
722
|
auto.failedCount > 0 ? 'Auto plan finished with failures' : auto.warningCount > 0 ? 'Auto plan finished with warnings' : 'Auto plan finished';
|
|
@@ -579,6 +804,10 @@ async function buildAutoPlanFinalSummary({
|
|
|
579
804
|
}
|
|
580
805
|
const fallbackSummary = fallbackParts.join(' ');
|
|
581
806
|
|
|
807
|
+
if (runItems.some((item) => item.failed || item.error)) {
|
|
808
|
+
return fallbackSummary;
|
|
809
|
+
}
|
|
810
|
+
|
|
582
811
|
try {
|
|
583
812
|
const result = await createChatCompletion({
|
|
584
813
|
baseUrl: config.gateway.base_url,
|
|
@@ -1033,6 +1262,7 @@ async function askModel({
|
|
|
1033
1262
|
async function runSubAgentTask({
|
|
1034
1263
|
role,
|
|
1035
1264
|
task,
|
|
1265
|
+
goal = '',
|
|
1036
1266
|
priorSteps = [],
|
|
1037
1267
|
parentSession,
|
|
1038
1268
|
config,
|
|
@@ -1047,8 +1277,18 @@ async function runSubAgentTask({
|
|
|
1047
1277
|
const handoffPacket = buildStepArtifactPacket(priorSteps, role);
|
|
1048
1278
|
const handoffFocusPaths = collectStepArtifacts(priorSteps, role)?.focusPaths || [];
|
|
1049
1279
|
const focusedTaskNote = buildFocusedTaskNote(role, handoffFocusPaths);
|
|
1280
|
+
const goalRequirementPacket = buildGoalRequirementPacket(goal, role);
|
|
1050
1281
|
const verificationPacket = role === 'tester' ? await buildTesterVerificationPacket(handoffFocusPaths) : '';
|
|
1051
|
-
const scopedTask = [
|
|
1282
|
+
const scopedTask = [
|
|
1283
|
+
contextPacket,
|
|
1284
|
+
goalRequirementPacket,
|
|
1285
|
+
evidencePacket,
|
|
1286
|
+
handoffPacket,
|
|
1287
|
+
verificationPacket,
|
|
1288
|
+
focusedTaskNote,
|
|
1289
|
+
'Task:',
|
|
1290
|
+
task
|
|
1291
|
+
]
|
|
1052
1292
|
.filter(Boolean)
|
|
1053
1293
|
.join('\n\n');
|
|
1054
1294
|
let blockedCount = 0;
|
|
@@ -1105,6 +1345,7 @@ async function buildAutoPlanAndRun({
|
|
|
1105
1345
|
onAgentEvent,
|
|
1106
1346
|
sessionId
|
|
1107
1347
|
}) {
|
|
1348
|
+
const requirementPacket = buildGoalRequirementPacket(goal, 'planner');
|
|
1108
1349
|
const plannerPrompt =
|
|
1109
1350
|
'Return strict JSON only with shape {"summary":"...","steps":[{"title":"...","role":"planner|coder|reviewer|tester","task":"..."}]}. No markdown. Always include final reviewer and tester steps.';
|
|
1110
1351
|
let autoPlan = {
|
|
@@ -1127,7 +1368,13 @@ async function buildAutoPlanAndRun({
|
|
|
1127
1368
|
{ role: 'system', content: `${systemPrompt}\n${plannerPrompt}` },
|
|
1128
1369
|
{
|
|
1129
1370
|
role: 'user',
|
|
1130
|
-
content:
|
|
1371
|
+
content: [
|
|
1372
|
+
'Create an execution plan and assign best sub-agent role for each step.',
|
|
1373
|
+
requirementPacket,
|
|
1374
|
+
'The final steps must include review and testing/verification unless the goal is a tiny single-change task, in which case you may keep only one implementation step plus one testing/verification step.'
|
|
1375
|
+
]
|
|
1376
|
+
.filter(Boolean)
|
|
1377
|
+
.join('\n')
|
|
1131
1378
|
}
|
|
1132
1379
|
],
|
|
1133
1380
|
timeoutMs: config.gateway.timeout_ms || 90000,
|
|
@@ -1153,6 +1400,7 @@ async function buildAutoPlanAndRun({
|
|
|
1153
1400
|
const stepResult = await runSubAgentTask({
|
|
1154
1401
|
role: step.role,
|
|
1155
1402
|
task: step.task,
|
|
1403
|
+
goal,
|
|
1156
1404
|
priorSteps: runItems,
|
|
1157
1405
|
parentSession: session,
|
|
1158
1406
|
config,
|
|
@@ -1161,14 +1409,20 @@ async function buildAutoPlanAndRun({
|
|
|
1161
1409
|
onAgentEvent
|
|
1162
1410
|
});
|
|
1163
1411
|
const outputLooksSuccessful = looksLikeSuccessfulStepOutput(stepResult.text);
|
|
1412
|
+
const outputHasFailureSignals = stepOutputHasFailureSignals(step.role, stepResult.text);
|
|
1164
1413
|
const warningParts = [];
|
|
1165
1414
|
if (stepResult.blockedCount > 0) warningParts.push(`${stepResult.blockedCount} blocked tool call(s)`);
|
|
1166
1415
|
if (stepResult.toolErrorCount > 0) warningParts.push(`${stepResult.toolErrorCount} tool error(s)`);
|
|
1167
1416
|
const warning = warningParts.length > 0 ? `sub-agent recovered after ${warningParts.join(', ')}` : '';
|
|
1168
|
-
const failed =
|
|
1417
|
+
const failed =
|
|
1418
|
+
stepResult.hasErrorLine ||
|
|
1419
|
+
outputHasFailureSignals ||
|
|
1420
|
+
(!outputLooksSuccessful && (stepResult.blockedCount > 0 || stepResult.toolErrorCount > 0));
|
|
1169
1421
|
let error = '';
|
|
1170
1422
|
if (stepResult.hasErrorLine) {
|
|
1171
1423
|
error = 'sub-agent output contains error line(s)';
|
|
1424
|
+
} else if (outputHasFailureSignals) {
|
|
1425
|
+
error = 'sub-agent output reports unmet requirements or failed verification';
|
|
1172
1426
|
} else if (failed && stepResult.blockedCount > 0) {
|
|
1173
1427
|
error = `sub-agent ended with ${stepResult.blockedCount} blocked tool call(s)`;
|
|
1174
1428
|
} else if (failed && stepResult.toolErrorCount > 0) {
|
|
@@ -1324,11 +1578,13 @@ export async function createChatRuntime({
|
|
|
1324
1578
|
const configKeyHints = [
|
|
1325
1579
|
'gateway.base_url',
|
|
1326
1580
|
'gateway.api_key',
|
|
1581
|
+
'model.name',
|
|
1582
|
+
'ui.reply_language',
|
|
1583
|
+
'execution.mode',
|
|
1584
|
+
'shell.default',
|
|
1327
1585
|
'gateway.timeout_ms',
|
|
1328
1586
|
'gateway.max_retries',
|
|
1329
|
-
'model.name',
|
|
1330
1587
|
'model.max_context_tokens',
|
|
1331
|
-
'execution.mode',
|
|
1332
1588
|
'execution.always_allow_tools',
|
|
1333
1589
|
'execution.max_steps',
|
|
1334
1590
|
'context.preflight_trigger_pct',
|
|
@@ -1338,13 +1594,34 @@ export async function createChatRuntime({
|
|
|
1338
1594
|
'context.read_file_max_chars',
|
|
1339
1595
|
'sessions.max_sessions',
|
|
1340
1596
|
'sessions.retention_days',
|
|
1341
|
-
'shell.default',
|
|
1342
1597
|
'shell.timeout_ms',
|
|
1343
1598
|
'context.max_tokens',
|
|
1344
1599
|
'policy.safe_mode',
|
|
1345
1600
|
'policy.allow_dangerous_commands'
|
|
1346
1601
|
];
|
|
1347
1602
|
|
|
1603
|
+
const commandPriorityOrder = [
|
|
1604
|
+
'/help',
|
|
1605
|
+
'/status',
|
|
1606
|
+
'/config',
|
|
1607
|
+
'/mode',
|
|
1608
|
+
'/plan',
|
|
1609
|
+
'/tasks',
|
|
1610
|
+
'/history',
|
|
1611
|
+
'/checkpoint',
|
|
1612
|
+
'/agents',
|
|
1613
|
+
'/compact',
|
|
1614
|
+
'/debug',
|
|
1615
|
+
'/retry'
|
|
1616
|
+
];
|
|
1617
|
+
const configSubcommandPriority = ['/config set', '/config get', '/config list', '/config reset'];
|
|
1618
|
+
const configSubcommandDescriptions = {
|
|
1619
|
+
'/config set': 'update a config value',
|
|
1620
|
+
'/config get': 'show a config value',
|
|
1621
|
+
'/config list': 'print the full config',
|
|
1622
|
+
'/config reset': 'reset config to defaults'
|
|
1623
|
+
};
|
|
1624
|
+
|
|
1348
1625
|
const listCommandNames = () => {
|
|
1349
1626
|
const builtins = [
|
|
1350
1627
|
{ name: 'help', description: 'show chat help' },
|
|
@@ -1423,10 +1700,23 @@ export async function createChatRuntime({
|
|
|
1423
1700
|
'/status'
|
|
1424
1701
|
];
|
|
1425
1702
|
const compactKey = (value) => String(value || '').toLowerCase().replace(/[\/\s<>?]/g, '');
|
|
1703
|
+
const commandDescriptions = new Map();
|
|
1704
|
+
const registerSuggestion = (value, description = '') => {
|
|
1705
|
+
commandDescriptions.set(value, description);
|
|
1706
|
+
return { value, description };
|
|
1707
|
+
};
|
|
1708
|
+
const materializeSuggestions = (items) =>
|
|
1709
|
+
(Array.isArray(items) ? items : []).map((item) => {
|
|
1710
|
+
if (item && typeof item === 'object' && 'value' in item) return item;
|
|
1711
|
+
const value = String(item || '');
|
|
1712
|
+
return { value, description: commandDescriptions.get(value) || '' };
|
|
1713
|
+
});
|
|
1426
1714
|
const matchCompactTemplates = (value) => {
|
|
1427
1715
|
const needle = compactKey(value);
|
|
1428
1716
|
if (!needle) return [];
|
|
1429
|
-
return
|
|
1717
|
+
return materializeSuggestions(
|
|
1718
|
+
slashTemplates.filter((template) => compactKey(template).startsWith(needle))
|
|
1719
|
+
);
|
|
1430
1720
|
};
|
|
1431
1721
|
|
|
1432
1722
|
const getCompletionOptions = (rawInput) => {
|
|
@@ -1438,26 +1728,53 @@ export async function createChatRuntime({
|
|
|
1438
1728
|
const tokens = body.trim().split(/\s+/).filter(Boolean);
|
|
1439
1729
|
const commandPart = tokens[0] || '';
|
|
1440
1730
|
|
|
1441
|
-
const
|
|
1731
|
+
const allCommandEntries = listCommandNames();
|
|
1732
|
+
const allCommands = allCommandEntries.map((c) => c.name);
|
|
1733
|
+
for (const entry of allCommandEntries) {
|
|
1734
|
+
registerSuggestion(`/${entry.name}`, entry.description || '');
|
|
1735
|
+
}
|
|
1736
|
+
for (const template of configTemplates) {
|
|
1737
|
+
registerSuggestion(template, configSubcommandDescriptions[template] || 'config command');
|
|
1738
|
+
}
|
|
1739
|
+
for (const template of historyTemplates) registerSuggestion(template, 'history command');
|
|
1740
|
+
for (const template of modeTemplates) registerSuggestion(template, 'switch execution mode');
|
|
1741
|
+
for (const template of taskTemplates) registerSuggestion(template, 'task board command');
|
|
1742
|
+
for (const template of checkpointTemplates) registerSuggestion(template, 'checkpoint command');
|
|
1743
|
+
for (const template of specTemplates) registerSuggestion(template, 'create a spec file');
|
|
1744
|
+
for (const template of planTemplates) registerSuggestion(template, 'planning command');
|
|
1745
|
+
for (const template of agentTemplates) registerSuggestion(template, 'sub-agent command');
|
|
1746
|
+
for (const template of debugTemplates) registerSuggestion(template, 'debug command');
|
|
1747
|
+
for (const template of compactTemplates) registerSuggestion(template, 'context compaction command');
|
|
1748
|
+
registerSuggestion('/retry', 'retry the last user request');
|
|
1749
|
+
registerSuggestion('/status', 'show runtime status');
|
|
1442
1750
|
|
|
1443
1751
|
if (!commandPart) {
|
|
1444
|
-
return
|
|
1752
|
+
return materializeSuggestions(prioritizeByPreferredOrder(
|
|
1753
|
+
allCommands.map((name) => `/${name}`),
|
|
1754
|
+
commandPriorityOrder
|
|
1755
|
+
));
|
|
1445
1756
|
}
|
|
1446
1757
|
|
|
1447
1758
|
if (tokens.length === 1 && !hasTrailingSpace) {
|
|
1448
|
-
const direct =
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1759
|
+
const direct = prioritizeByPreferredOrder(
|
|
1760
|
+
allCommands
|
|
1761
|
+
.filter((name) => name.startsWith(commandPart))
|
|
1762
|
+
.map((name) => `/${name}`),
|
|
1763
|
+
commandPriorityOrder
|
|
1764
|
+
);
|
|
1765
|
+
if (direct.length > 0) return materializeSuggestions(direct);
|
|
1452
1766
|
return matchCompactTemplates(input);
|
|
1453
1767
|
}
|
|
1454
1768
|
|
|
1455
1769
|
if (commandPart === 'config') {
|
|
1456
1770
|
if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
|
|
1457
1771
|
const sub = tokens[1] || '';
|
|
1458
|
-
return
|
|
1459
|
-
|
|
1460
|
-
|
|
1772
|
+
return materializeSuggestions(prioritizeByPreferredOrder(
|
|
1773
|
+
['set', 'get', 'list', 'reset']
|
|
1774
|
+
.filter((s) => s.startsWith(sub))
|
|
1775
|
+
.map((s) => registerSuggestion(`/config ${s}`, configSubcommandDescriptions[`/config ${s}`] || 'config command').value),
|
|
1776
|
+
configSubcommandPriority
|
|
1777
|
+
));
|
|
1461
1778
|
}
|
|
1462
1779
|
|
|
1463
1780
|
const sub = tokens[1] || '';
|
|
@@ -1465,96 +1782,98 @@ export async function createChatRuntime({
|
|
|
1465
1782
|
const keyPrefix = tokens[2] || '';
|
|
1466
1783
|
return configKeyHints
|
|
1467
1784
|
.filter((k) => k.startsWith(keyPrefix))
|
|
1468
|
-
.map((k) => `/config get ${k}
|
|
1785
|
+
.map((k) => registerSuggestion(`/config get ${k}`, describeConfigKey(k, 'get')));
|
|
1469
1786
|
}
|
|
1470
1787
|
if (sub === 'set') {
|
|
1471
1788
|
const keyPrefix = tokens[2] || '';
|
|
1472
1789
|
return configKeyHints
|
|
1473
1790
|
.filter((k) => k.startsWith(keyPrefix))
|
|
1474
|
-
.map((k) => `/config set ${k}
|
|
1791
|
+
.map((k) => registerSuggestion(`/config set ${k} `, describeConfigKey(k, 'set')));
|
|
1475
1792
|
}
|
|
1476
1793
|
|
|
1477
|
-
return configTemplates;
|
|
1794
|
+
return materializeSuggestions(configTemplates);
|
|
1478
1795
|
}
|
|
1479
1796
|
|
|
1480
1797
|
if (commandPart === 'compact') {
|
|
1481
1798
|
const joined = tokens.slice(1).join(' ');
|
|
1482
1799
|
return compactOptions
|
|
1483
1800
|
.filter((opt) => opt.includes(joined) || joined === '')
|
|
1484
|
-
.map((opt) => `/compact ${opt}
|
|
1801
|
+
.map((opt) => registerSuggestion(`/compact ${opt}`, 'context compaction command'));
|
|
1485
1802
|
}
|
|
1486
1803
|
|
|
1487
1804
|
if (commandPart === 'retry') {
|
|
1488
|
-
return ['/retry'];
|
|
1805
|
+
return [registerSuggestion('/retry', 'retry the last user request')];
|
|
1489
1806
|
}
|
|
1490
1807
|
if (commandPart === 'status') {
|
|
1491
|
-
return ['/status'];
|
|
1808
|
+
return [registerSuggestion('/status', 'show runtime status')];
|
|
1492
1809
|
}
|
|
1493
1810
|
if (commandPart === 'mode') {
|
|
1494
1811
|
if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
|
|
1495
1812
|
const sub = tokens[1] || '';
|
|
1496
1813
|
return ['normal', 'auto', 'plan']
|
|
1497
1814
|
.filter((m) => m.startsWith(sub))
|
|
1498
|
-
.map((m) => `/mode ${m}
|
|
1815
|
+
.map((m) => registerSuggestion(`/mode ${m}`, 'switch execution mode'));
|
|
1499
1816
|
}
|
|
1500
|
-
return modeTemplates;
|
|
1817
|
+
return materializeSuggestions(modeTemplates);
|
|
1501
1818
|
}
|
|
1502
1819
|
if (commandPart === 'tasks') {
|
|
1503
1820
|
if (tokens.length <= 2 && !hasTrailingSpace) {
|
|
1504
1821
|
const sub = tokens[1] || '';
|
|
1505
1822
|
return ['add', 'start', 'done', 'remove', 'rm', 'clear']
|
|
1506
1823
|
.filter((s) => s.startsWith(sub))
|
|
1507
|
-
.map((s) => `/tasks ${s}
|
|
1824
|
+
.map((s) => registerSuggestion(`/tasks ${s}`, 'task board command'));
|
|
1508
1825
|
}
|
|
1509
|
-
return taskTemplates;
|
|
1826
|
+
return materializeSuggestions(taskTemplates);
|
|
1510
1827
|
}
|
|
1511
1828
|
if (commandPart === 'checkpoint') {
|
|
1512
1829
|
if (tokens.length <= 2 && !hasTrailingSpace) {
|
|
1513
1830
|
const sub = tokens[1] || '';
|
|
1514
1831
|
return ['create', 'list', 'load']
|
|
1515
1832
|
.filter((s) => s.startsWith(sub))
|
|
1516
|
-
.map((s) => `/checkpoint ${s}
|
|
1833
|
+
.map((s) => registerSuggestion(`/checkpoint ${s}`, 'checkpoint command'));
|
|
1517
1834
|
}
|
|
1518
1835
|
if (tokens[1] === 'list') {
|
|
1519
1836
|
const hint = tokens[2] || '';
|
|
1520
|
-
return ['--all']
|
|
1837
|
+
return ['--all']
|
|
1838
|
+
.filter((v) => v.startsWith(hint))
|
|
1839
|
+
.map((v) => registerSuggestion(`/checkpoint list ${v}`, 'checkpoint command'));
|
|
1521
1840
|
}
|
|
1522
1841
|
if (tokens[1] === 'load') {
|
|
1523
1842
|
if (tokens.length >= 3) {
|
|
1524
1843
|
const hint = tokens[3] || '';
|
|
1525
1844
|
return ['--all']
|
|
1526
1845
|
.filter((v) => v.startsWith(hint))
|
|
1527
|
-
.map((v) => `/checkpoint load ${tokens[2]} ${v}
|
|
1846
|
+
.map((v) => registerSuggestion(`/checkpoint load ${tokens[2]} ${v}`, 'checkpoint command'));
|
|
1528
1847
|
}
|
|
1529
1848
|
}
|
|
1530
|
-
return checkpointTemplates;
|
|
1849
|
+
return materializeSuggestions(checkpointTemplates);
|
|
1531
1850
|
}
|
|
1532
1851
|
if (commandPart === 'spec') {
|
|
1533
|
-
return specTemplates;
|
|
1852
|
+
return materializeSuggestions(specTemplates);
|
|
1534
1853
|
}
|
|
1535
1854
|
if (commandPart === 'plan') {
|
|
1536
1855
|
if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
|
|
1537
1856
|
const sub = tokens[1] || '';
|
|
1538
1857
|
return ['auto', 'from-spec']
|
|
1539
1858
|
.filter((s) => s.startsWith(sub))
|
|
1540
|
-
.map((s) => `/plan ${s}
|
|
1859
|
+
.map((s) => registerSuggestion(`/plan ${s}`, 'planning command'));
|
|
1541
1860
|
}
|
|
1542
|
-
return planTemplates;
|
|
1861
|
+
return materializeSuggestions(planTemplates);
|
|
1543
1862
|
}
|
|
1544
1863
|
if (commandPart === 'agents') {
|
|
1545
1864
|
if (tokens.length === 1 || (tokens.length === 2 && !hasTrailingSpace)) {
|
|
1546
1865
|
const sub = tokens[1] || '';
|
|
1547
1866
|
return ['list', 'run']
|
|
1548
1867
|
.filter((s) => s.startsWith(sub))
|
|
1549
|
-
.map((s) => `/agents ${s}
|
|
1868
|
+
.map((s) => registerSuggestion(`/agents ${s}`, 'sub-agent command'));
|
|
1550
1869
|
}
|
|
1551
1870
|
if (tokens[1] === 'run') {
|
|
1552
1871
|
const rolePrefix = tokens[2] || '';
|
|
1553
1872
|
return ['planner', 'coder', 'reviewer', 'tester']
|
|
1554
1873
|
.filter((r) => r.startsWith(rolePrefix))
|
|
1555
|
-
.map((r) => `/agents run ${r}
|
|
1874
|
+
.map((r) => registerSuggestion(`/agents run ${r} `, 'sub-agent command'));
|
|
1556
1875
|
}
|
|
1557
|
-
return agentTemplates;
|
|
1876
|
+
return materializeSuggestions(agentTemplates);
|
|
1558
1877
|
}
|
|
1559
1878
|
|
|
1560
1879
|
if (commandPart === 'history') {
|
|
@@ -1571,12 +1890,13 @@ export async function createChatRuntime({
|
|
|
1571
1890
|
.filter((session) => String(session.id || '').startsWith(idPrefix))
|
|
1572
1891
|
.map((session) => ({
|
|
1573
1892
|
value: `/history resume ${session.id}`,
|
|
1574
|
-
display: `/history resume ${session.id} · ${Number(session.messageCount || 0)} msgs
|
|
1893
|
+
display: `/history resume ${session.id} · ${Number(session.messageCount || 0)} msgs`,
|
|
1894
|
+
description: 'resume a saved session'
|
|
1575
1895
|
}));
|
|
1576
1896
|
if (dynamic.length > 0) return dynamic;
|
|
1577
|
-
return historyTemplates;
|
|
1897
|
+
return materializeSuggestions(historyTemplates);
|
|
1578
1898
|
}
|
|
1579
|
-
return historyTemplates;
|
|
1899
|
+
return materializeSuggestions(historyTemplates);
|
|
1580
1900
|
}
|
|
1581
1901
|
|
|
1582
1902
|
if (commandPart === 'debug') {
|
|
@@ -1586,9 +1906,9 @@ export async function createChatRuntime({
|
|
|
1586
1906
|
const action = tokens[2] || '';
|
|
1587
1907
|
return ['on', 'off', 'status']
|
|
1588
1908
|
.filter((v) => v.startsWith(action))
|
|
1589
|
-
.map((v) => `/debug keys ${v}
|
|
1909
|
+
.map((v) => registerSuggestion(`/debug keys ${v}`, 'keyboard debug command'));
|
|
1590
1910
|
}
|
|
1591
|
-
return debugTemplates;
|
|
1911
|
+
return materializeSuggestions(debugTemplates);
|
|
1592
1912
|
}
|
|
1593
1913
|
|
|
1594
1914
|
return [];
|
|
@@ -1630,7 +1950,7 @@ export async function createChatRuntime({
|
|
|
1630
1950
|
};
|
|
1631
1951
|
|
|
1632
1952
|
const submit = async (line, onAgentEvent) => {
|
|
1633
|
-
const activeBaseSystemPrompt = baseSystemPrompt;
|
|
1953
|
+
const activeBaseSystemPrompt = buildSystemPromptWithReplyLanguage(baseSystemPrompt, config);
|
|
1634
1954
|
const activeReplySystemPrompt = await buildSystemPromptWithSoul(baseSystemPrompt, config);
|
|
1635
1955
|
try {
|
|
1636
1956
|
await appendInputHistory(line);
|