codemini-cli 0.3.5 → 0.3.7
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/README.md +20 -18
- package/package.json +6 -6
- package/souls/anime.md +5 -2
- package/src/cli.js +3 -1
- package/src/commands/run.js +229 -16
- package/src/core/agent-loop.js +159 -47
- package/src/core/ast.js +40 -0
- package/src/core/chat-runtime.js +712 -126
- package/src/core/command-policy.js +56 -0
- package/src/core/config-store.js +0 -3
- package/src/core/crypto-utils.js +6 -2
- package/src/core/memory-store.js +3 -3
- package/src/core/project-index.js +4 -18
- package/src/core/provider/anthropic.js +15 -2
- package/src/core/provider/openai-compatible.js +15 -2
- package/src/core/session-store.js +82 -25
- package/src/core/shell-profile.js +17 -1
- package/src/core/string-utils.js +37 -0
- package/src/core/tools.js +152 -393
- package/src/tui/chat-app.js +461 -147
- package/src/tui/tool-activity/presenters/files.js +2 -2
- package/src/tui/tool-narration.js +0 -3
- package/src/tui/tool-narration/presenters/patch.js +0 -3
package/src/core/agent-loop.js
CHANGED
|
@@ -2,15 +2,22 @@ import os from 'node:os';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
4
|
import { BoundedCache } from './bounded-cache.js';
|
|
5
|
+
import { trimInline as _trimInline, normalizePath } from './string-utils.js';
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* 安全解析 JSON 字符串。
|
|
9
|
+
* 解析失败时返回带 _raw 和 _invalid_json 标记的对象,
|
|
10
|
+
* 调用方可据此决定是回退到原始文本还是报告错误。
|
|
11
|
+
*/
|
|
6
12
|
function safeJsonParse(raw) {
|
|
7
13
|
if (!raw || typeof raw !== 'string') return {};
|
|
8
14
|
try {
|
|
9
15
|
return JSON.parse(raw);
|
|
10
|
-
} catch {
|
|
16
|
+
} catch (parseError) {
|
|
11
17
|
return {
|
|
12
18
|
_raw: String(raw),
|
|
13
|
-
_invalid_json: true
|
|
19
|
+
_invalid_json: true,
|
|
20
|
+
_parseError: parseError.message
|
|
14
21
|
};
|
|
15
22
|
}
|
|
16
23
|
}
|
|
@@ -29,6 +36,41 @@ function parseInlineRangePath(value) {
|
|
|
29
36
|
return { path: maybePath, start_line: start, end_line: end };
|
|
30
37
|
}
|
|
31
38
|
|
|
39
|
+
function buildDeleteApprovalDetails(source, rawPath) {
|
|
40
|
+
const existing =
|
|
41
|
+
source?.approval && typeof source.approval === 'object' && !Array.isArray(source.approval)
|
|
42
|
+
? source.approval
|
|
43
|
+
: {};
|
|
44
|
+
const approvalPath = String(existing.path || rawPath || '').trim();
|
|
45
|
+
const approvalName = String(existing.name || (approvalPath ? path.basename(approvalPath) : '') || '').trim();
|
|
46
|
+
const approvalType = String(existing.type || '').trim();
|
|
47
|
+
|
|
48
|
+
const approval = {};
|
|
49
|
+
if (approvalPath) approval.path = approvalPath;
|
|
50
|
+
if (approvalName) approval.name = approvalName;
|
|
51
|
+
if (approvalType) approval.type = approvalType;
|
|
52
|
+
return Object.keys(approval).length > 0 ? approval : undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function buildDeleteCancellationResult(args) {
|
|
56
|
+
const approval =
|
|
57
|
+
args?.approval && typeof args.approval === 'object' && !Array.isArray(args.approval)
|
|
58
|
+
? args.approval
|
|
59
|
+
: undefined;
|
|
60
|
+
const pathValue = String(approval?.path || args?.path || '').trim();
|
|
61
|
+
const nameValue = String(approval?.name || (pathValue ? path.basename(pathValue) : '') || '').trim();
|
|
62
|
+
const typeValue = String(approval?.type || '').trim();
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
...(pathValue ? { path: pathValue } : {}),
|
|
66
|
+
...(nameValue ? { name: nameValue } : {}),
|
|
67
|
+
...(typeValue ? { type: typeValue } : {}),
|
|
68
|
+
deleted: false,
|
|
69
|
+
cancelled: true,
|
|
70
|
+
reason: 'User denied deletion approval'
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
32
74
|
function normalizeToolArguments(toolName, args, rawArguments) {
|
|
33
75
|
const rawText = typeof rawArguments === 'string' ? rawArguments.trim() : '';
|
|
34
76
|
const primitive =
|
|
@@ -102,6 +144,14 @@ function normalizeToolArguments(toolName, args, rawArguments) {
|
|
|
102
144
|
return source;
|
|
103
145
|
}
|
|
104
146
|
|
|
147
|
+
if (toolName === 'delete') {
|
|
148
|
+
const value = String(source.path || source.file_path || source.file || source.target || source.directory || source.dir || stringValue || '').trim();
|
|
149
|
+
if (value) source.path = value;
|
|
150
|
+
const approval = buildDeleteApprovalDetails(source, source.path);
|
|
151
|
+
if (approval) source.approval = approval;
|
|
152
|
+
return source;
|
|
153
|
+
}
|
|
154
|
+
|
|
105
155
|
return source;
|
|
106
156
|
}
|
|
107
157
|
|
|
@@ -307,7 +357,7 @@ export function checkReadDedup(filePath, startLine, endLine, mtimeMs) {
|
|
|
307
357
|
|
|
308
358
|
const READ_ONLY_TOOLS = new Set([
|
|
309
359
|
'read', 'grep', 'glob', 'list',
|
|
310
|
-
'ast_query', 'read_ast_node',
|
|
360
|
+
'ast_query', 'read_ast_node',
|
|
311
361
|
'list_background_tasks', 'get_background_task'
|
|
312
362
|
]);
|
|
313
363
|
|
|
@@ -322,6 +372,12 @@ export function summarizeToolResult(result) {
|
|
|
322
372
|
if (typeof result === 'object') {
|
|
323
373
|
const obj = result;
|
|
324
374
|
if (Array.isArray(obj)) return `array(${obj.length})`;
|
|
375
|
+
if ('deleted' in obj && 'path' in obj) {
|
|
376
|
+
const kind = trimInline(obj.type || 'item', 16);
|
|
377
|
+
const target = trimInline(obj.path || '', 96);
|
|
378
|
+
if (obj.deleted) return target ? `deleted ${kind} ${target}` : `deleted ${kind}`;
|
|
379
|
+
if (obj.cancelled) return target ? `cancelled delete ${target}` : 'cancelled delete';
|
|
380
|
+
}
|
|
325
381
|
if ('path' in obj && 'action' in obj) {
|
|
326
382
|
const p = String(obj.path || '');
|
|
327
383
|
const action = String(obj.action || 'write');
|
|
@@ -354,11 +410,12 @@ export function summarizeToolResult(result) {
|
|
|
354
410
|
: start;
|
|
355
411
|
const rangeText = start > 0 && end >= start ? ` lines ${start}-${end}` : '';
|
|
356
412
|
const totalText = total > 0 ? ` of ${total}` : '';
|
|
413
|
+
const enclosingText = obj.enclosing_symbol ? ` in ${obj.enclosing_symbol}` : '';
|
|
357
414
|
const errorText = obj.error ? ` (${trimInline(obj.error, 64)})` : '';
|
|
358
415
|
const truncatedText = obj.truncated ? ' [truncated]' : '';
|
|
359
416
|
return phase === 'metadata'
|
|
360
417
|
? `metadata for ${p}${rangeText}${totalText}${errorText}`
|
|
361
|
-
: `content from ${p}${rangeText}${totalText}${truncatedText}`;
|
|
418
|
+
: `content from ${p}${rangeText}${totalText}${enclosingText}${truncatedText}`;
|
|
362
419
|
}
|
|
363
420
|
if ('stdout' in obj || 'stderr' in obj || 'code' in obj) {
|
|
364
421
|
const stdout = trimInline(obj.stdout || '', 96);
|
|
@@ -405,12 +462,7 @@ export function summarizeToolResult(result) {
|
|
|
405
462
|
return String(result);
|
|
406
463
|
}
|
|
407
464
|
|
|
408
|
-
export
|
|
409
|
-
const s = String(value || '').replace(/\s+/g, ' ').trim();
|
|
410
|
-
if (!s) return '';
|
|
411
|
-
if (s.length <= maxLen) return s;
|
|
412
|
-
return `${s.slice(0, maxLen - 3)}...`;
|
|
413
|
-
}
|
|
465
|
+
export const trimInline = _trimInline;
|
|
414
466
|
|
|
415
467
|
function normalizeAssistantText(value) {
|
|
416
468
|
return String(value || '').trim();
|
|
@@ -503,12 +555,12 @@ function createAnalysisGuardState(userPrompt) {
|
|
|
503
555
|
}
|
|
504
556
|
|
|
505
557
|
function topLevelPath(value) {
|
|
506
|
-
const normalized =
|
|
558
|
+
const normalized = normalizePath(value).trim();
|
|
507
559
|
return normalized.split('/')[0] || '';
|
|
508
560
|
}
|
|
509
561
|
|
|
510
562
|
function isRelevantSourcePath(filePath, state) {
|
|
511
|
-
const normalized =
|
|
563
|
+
const normalized = normalizePath(filePath).trim();
|
|
512
564
|
if (!normalized) return false;
|
|
513
565
|
if (state.candidateFiles.has(normalized) || state.entryCandidates.has(normalized)) return true;
|
|
514
566
|
for (const root of state.sourceRoots) {
|
|
@@ -519,20 +571,17 @@ function isRelevantSourcePath(filePath, state) {
|
|
|
519
571
|
|
|
520
572
|
function blockedExplorationReason(toolName, args, state) {
|
|
521
573
|
if (!state.active) return '';
|
|
522
|
-
if (!state.indexQueried && toolName !== 'query_project_index') {
|
|
523
|
-
return 'Use query_project_index before broad repository exploration so the next reads stay focused on relevant source files.';
|
|
524
|
-
}
|
|
525
574
|
|
|
526
|
-
|
|
575
|
+
// Always note when query_project_index is used, but never force it
|
|
576
|
+
if (toolName === 'query_project_index') return '';
|
|
577
|
+
|
|
578
|
+
const target = normalizePath(String(args?.path || args?.pattern || args?.query || '')).trim();
|
|
527
579
|
const top = topLevelPath(target);
|
|
528
580
|
if (!top) return '';
|
|
529
581
|
|
|
530
582
|
if (['skills', 'souls', 'templates', '.codemini', '.codemini-project'].includes(top)) {
|
|
531
583
|
return `Skip ${top}/ for broad repository analysis unless the user explicitly asks for it. Inspect relevant source files first.`;
|
|
532
584
|
}
|
|
533
|
-
if (top === 'tests' && state.relevantSourceReads.size < 2) {
|
|
534
|
-
return 'Inspect the next relevant source files before reading tests. Broad analysis should be grounded in production code first.';
|
|
535
|
-
}
|
|
536
585
|
return '';
|
|
537
586
|
}
|
|
538
587
|
|
|
@@ -603,13 +652,13 @@ function formatToolDisplayName(name, args) {
|
|
|
603
652
|
const target = trimInline(args?.path || args?.file || '.', 96) || '.';
|
|
604
653
|
return `edit(${target})`;
|
|
605
654
|
}
|
|
655
|
+
if (name === 'delete') {
|
|
656
|
+
const target = trimInline(args?.path || args?.target || '.', 96) || '.';
|
|
657
|
+
return `delete(${target})`;
|
|
658
|
+
}
|
|
606
659
|
if (name === 'update_todos') {
|
|
607
660
|
return 'update_todos';
|
|
608
661
|
}
|
|
609
|
-
if (name === 'patch') {
|
|
610
|
-
const target = trimInline(args?.path || args?.file || args?.patch || '', 96) || '.';
|
|
611
|
-
return `patch(${target})`;
|
|
612
|
-
}
|
|
613
662
|
if (name === 'list_background_tasks') {
|
|
614
663
|
return name;
|
|
615
664
|
}
|
|
@@ -617,10 +666,6 @@ function formatToolDisplayName(name, args) {
|
|
|
617
666
|
const taskId = trimInline(args?.task_id || args?.taskId || '', 96);
|
|
618
667
|
return taskId ? `${name}(${taskId})` : name;
|
|
619
668
|
}
|
|
620
|
-
if (name === 'read' || name === 'write' || name === 'run' || name === 'grep' || name === 'glob' || name === 'list' || name === 'edit' || name === 'patch' || name === 'generate_diff') {
|
|
621
|
-
const target = trimInline(args?.path || args?.query || args?.symbol || '', 96);
|
|
622
|
-
return target ? `${name}(${target})` : name;
|
|
623
|
-
}
|
|
624
669
|
return name;
|
|
625
670
|
}
|
|
626
671
|
|
|
@@ -654,7 +699,9 @@ export async function runAgentLoop({
|
|
|
654
699
|
requestToolApproval,
|
|
655
700
|
toolResultMaxChars = 12000,
|
|
656
701
|
toolFormatters = {},
|
|
657
|
-
deferredDefinitions = {}
|
|
702
|
+
deferredDefinitions = {},
|
|
703
|
+
signal,
|
|
704
|
+
skipAnalysisNudge = false
|
|
658
705
|
}) {
|
|
659
706
|
const messages = [];
|
|
660
707
|
if (systemPrompt) {
|
|
@@ -677,13 +724,25 @@ export async function runAgentLoop({
|
|
|
677
724
|
const activeTools = [...toolDefinitions];
|
|
678
725
|
|
|
679
726
|
for (let step = 0; step < maxSteps; step += 1) {
|
|
727
|
+
// 检查是否已被用户中止
|
|
728
|
+
if (signal?.aborted) {
|
|
729
|
+
if (onEvent) onEvent({ type: 'aborted', step: step + 1 });
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
680
732
|
if (onEvent) onEvent({ type: 'step:start', step: step + 1 });
|
|
681
733
|
const completion = await requestCompletion({
|
|
682
734
|
model,
|
|
683
735
|
messages,
|
|
684
|
-
tools: activeTools
|
|
736
|
+
tools: activeTools,
|
|
737
|
+
signal
|
|
685
738
|
});
|
|
686
739
|
|
|
740
|
+
// 流式请求完成后再次检查中止状态
|
|
741
|
+
if (signal?.aborted) {
|
|
742
|
+
if (onEvent) onEvent({ type: 'aborted', step: step + 1 });
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
|
|
687
746
|
if (completion?.incomplete) {
|
|
688
747
|
continue;
|
|
689
748
|
}
|
|
@@ -718,7 +777,7 @@ export async function runAgentLoop({
|
|
|
718
777
|
}
|
|
719
778
|
|
|
720
779
|
if (toolCalls.length === 0) {
|
|
721
|
-
if (needsMoreAnalysisEvidence(analysisGuard) && pendingSummaryNudges < 2) {
|
|
780
|
+
if (!skipAnalysisNudge && needsMoreAnalysisEvidence(analysisGuard) && pendingSummaryNudges < 2) {
|
|
722
781
|
pendingSummaryNudges += 1;
|
|
723
782
|
messages.push({
|
|
724
783
|
role: 'user',
|
|
@@ -727,7 +786,7 @@ export async function runAgentLoop({
|
|
|
727
786
|
});
|
|
728
787
|
continue;
|
|
729
788
|
}
|
|
730
|
-
if (shouldAskForConcreteFinalAnswer(assistantText, messages.slice(0, -1)) && pendingSummaryNudges < 2) {
|
|
789
|
+
if (!skipAnalysisNudge && shouldAskForConcreteFinalAnswer(assistantText, messages.slice(0, -1)) && pendingSummaryNudges < 2) {
|
|
731
790
|
pendingSummaryNudges += 1;
|
|
732
791
|
messages.push({
|
|
733
792
|
role: 'user',
|
|
@@ -770,19 +829,44 @@ export async function runAgentLoop({
|
|
|
770
829
|
const approvalResults = new Map();
|
|
771
830
|
for (const { call, toolName, displayName, args } of callsWithMeta) {
|
|
772
831
|
let approved = true;
|
|
773
|
-
|
|
832
|
+
let approvalArgs = args;
|
|
833
|
+
let preflightErrorContent = '';
|
|
834
|
+
const needsApproval = toolName === 'delete' || (executionMode === 'normal' && !alwaysAllowSet.has(toolName));
|
|
835
|
+
if (needsApproval) {
|
|
774
836
|
approved = false;
|
|
837
|
+
const handler = toolHandlers[toolName];
|
|
838
|
+
if (toolName === 'delete' && typeof handler?.prepareApproval === 'function') {
|
|
839
|
+
try {
|
|
840
|
+
const approval = await handler.prepareApproval(args);
|
|
841
|
+
const normalizedApproval = buildDeleteApprovalDetails({ approval }, args?.path);
|
|
842
|
+
if (normalizedApproval) {
|
|
843
|
+
approvalArgs = { ...args, approval: normalizedApproval };
|
|
844
|
+
}
|
|
845
|
+
} catch (error) {
|
|
846
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
847
|
+
preflightErrorContent = clipToolResult({ error: message }, toolResultMaxChars);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (preflightErrorContent) {
|
|
851
|
+
approvalResults.set(call.id, {
|
|
852
|
+
approved: false,
|
|
853
|
+
args: approvalArgs,
|
|
854
|
+
errorContent: preflightErrorContent
|
|
855
|
+
});
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
775
858
|
if (typeof requestToolApproval === 'function') {
|
|
776
859
|
const decision = await requestToolApproval({
|
|
777
860
|
id: call.id,
|
|
778
861
|
name: toolName,
|
|
779
862
|
displayName,
|
|
780
|
-
arguments:
|
|
863
|
+
arguments: approvalArgs,
|
|
864
|
+
approvalDetails: toolName === 'delete' ? approvalArgs.approval : undefined
|
|
781
865
|
});
|
|
782
866
|
approved = Boolean(decision?.approved);
|
|
783
867
|
}
|
|
784
868
|
}
|
|
785
|
-
approvalResults.set(call.id, approved);
|
|
869
|
+
approvalResults.set(call.id, { approved, args: approvalArgs });
|
|
786
870
|
}
|
|
787
871
|
|
|
788
872
|
// Collect results keyed by call.id, then write to messages in original order
|
|
@@ -791,28 +875,45 @@ export async function runAgentLoop({
|
|
|
791
875
|
// Helper to execute a single tool call
|
|
792
876
|
async function executeOne({ call, args, toolName, displayName, isReadOnly }) {
|
|
793
877
|
const startedAt = Date.now();
|
|
878
|
+
const approvalState = approvalResults.get(call.id) || { approved: true, args };
|
|
879
|
+
const effectiveArgs = approvalState.args || args;
|
|
880
|
+
|
|
881
|
+
if (approvalState.errorContent) {
|
|
882
|
+
if (onEvent) {
|
|
883
|
+
onEvent({ type: 'tool:error', name: displayName, id: call.id, arguments: effectiveArgs, durationMs: 0, summary: trimInline(approvalState.errorContent, 120) });
|
|
884
|
+
}
|
|
885
|
+
return {
|
|
886
|
+
callId: call.id,
|
|
887
|
+
content: approvalState.errorContent,
|
|
888
|
+
error: true
|
|
889
|
+
};
|
|
890
|
+
}
|
|
794
891
|
|
|
795
|
-
if (!
|
|
796
|
-
if (onEvent) onEvent({ type: 'tool:blocked', name: displayName, id: call.id, arguments:
|
|
892
|
+
if (!approvalState.approved) {
|
|
893
|
+
if (onEvent) onEvent({ type: 'tool:blocked', name: displayName, id: call.id, arguments: effectiveArgs });
|
|
894
|
+
const blockedPayload =
|
|
895
|
+
toolName === 'delete'
|
|
896
|
+
? buildDeleteCancellationResult(effectiveArgs)
|
|
897
|
+
: { blocked: true, reason: 'Tool call requires approval in normal mode' };
|
|
797
898
|
return {
|
|
798
899
|
callId: call.id,
|
|
799
|
-
content: JSON.stringify(
|
|
900
|
+
content: JSON.stringify(blockedPayload),
|
|
800
901
|
blocked: true
|
|
801
902
|
};
|
|
802
903
|
}
|
|
803
904
|
|
|
804
|
-
if (onEvent) onEvent({ type: 'tool:start', name: displayName, id: call.id, arguments:
|
|
905
|
+
if (onEvent) onEvent({ type: 'tool:start', name: displayName, id: call.id, arguments: effectiveArgs });
|
|
805
906
|
const handler = toolHandlers[toolName];
|
|
806
907
|
if (!handler) {
|
|
807
908
|
throw new Error(`Unknown tool: ${call.name}`);
|
|
808
909
|
}
|
|
809
910
|
|
|
810
|
-
const blockedReason = blockedExplorationReason(toolName,
|
|
911
|
+
const blockedReason = blockedExplorationReason(toolName, effectiveArgs, analysisGuard);
|
|
811
912
|
if (blockedReason) {
|
|
812
913
|
analysisGuard.blockedExplorations += 1;
|
|
813
914
|
const content = clipToolResult({ error: blockedReason }, toolResultMaxChars);
|
|
814
915
|
if (onEvent) {
|
|
815
|
-
onEvent({ type: 'tool:error', name: displayName, id: call.id, arguments:
|
|
916
|
+
onEvent({ type: 'tool:error', name: displayName, id: call.id, arguments: effectiveArgs, durationMs: 0, summary: trimInline(blockedReason, 120) });
|
|
816
917
|
}
|
|
817
918
|
return {
|
|
818
919
|
callId: call.id,
|
|
@@ -823,12 +924,12 @@ export async function runAgentLoop({
|
|
|
823
924
|
|
|
824
925
|
let toolResult;
|
|
825
926
|
try {
|
|
826
|
-
toolResult = await handler(
|
|
927
|
+
toolResult = await handler(effectiveArgs);
|
|
827
928
|
} catch (error) {
|
|
828
929
|
const durationMs = Date.now() - startedAt;
|
|
829
930
|
const message = error instanceof Error ? error.message : String(error);
|
|
830
931
|
if (onEvent) {
|
|
831
|
-
onEvent({ type: 'tool:error', name: displayName, id: call.id, arguments:
|
|
932
|
+
onEvent({ type: 'tool:error', name: displayName, id: call.id, arguments: effectiveArgs, durationMs, summary: trimInline(message, 120) });
|
|
832
933
|
}
|
|
833
934
|
return {
|
|
834
935
|
callId: call.id,
|
|
@@ -839,12 +940,12 @@ export async function runAgentLoop({
|
|
|
839
940
|
|
|
840
941
|
const durationMs = Date.now() - startedAt;
|
|
841
942
|
if (onEvent) {
|
|
842
|
-
onEvent({ type: 'tool:end', name: displayName, id: call.id, arguments:
|
|
943
|
+
onEvent({ type: 'tool:end', name: displayName, id: call.id, arguments: effectiveArgs, durationMs, summary: summarizeToolResult(toolResult) });
|
|
843
944
|
}
|
|
844
945
|
|
|
845
946
|
// P1b: Use per-tool formatter if available, else fallback
|
|
846
|
-
let formatted = formatToolResult(toolResult, toolName,
|
|
847
|
-
noteAnalysisEvidence(analysisGuard, toolName,
|
|
947
|
+
let formatted = formatToolResult(toolResult, toolName, effectiveArgs, toolFormatters, toolResultMaxChars);
|
|
948
|
+
noteAnalysisEvidence(analysisGuard, toolName, effectiveArgs, toolResult);
|
|
848
949
|
|
|
849
950
|
// P2: If tool_search loaded deferred tools, inject their schemas into activeTools
|
|
850
951
|
if (toolName === 'tool_search' && toolResult && Array.isArray(toolResult.schemas)) {
|
|
@@ -863,8 +964,8 @@ export async function runAgentLoop({
|
|
|
863
964
|
}
|
|
864
965
|
|
|
865
966
|
// Separate read-only and write calls, preserving order
|
|
866
|
-
const readOnlyCalls = callsWithMeta.filter((c) => c.isReadOnly && approvalResults.get(c.call.id));
|
|
867
|
-
const writeCalls = callsWithMeta.filter((c) => !c.isReadOnly || !approvalResults.get(c.call.id));
|
|
967
|
+
const readOnlyCalls = callsWithMeta.filter((c) => c.isReadOnly && approvalResults.get(c.call.id)?.approved);
|
|
968
|
+
const writeCalls = callsWithMeta.filter((c) => !c.isReadOnly || !approvalResults.get(c.call.id)?.approved);
|
|
868
969
|
|
|
869
970
|
// Execute read-only calls in parallel
|
|
870
971
|
if (readOnlyCalls.length > 0) {
|
|
@@ -908,6 +1009,17 @@ export async function runAgentLoop({
|
|
|
908
1009
|
}
|
|
909
1010
|
}
|
|
910
1011
|
|
|
1012
|
+
// 如果被用户中止,返回已有内容并标记
|
|
1013
|
+
if (signal?.aborted) {
|
|
1014
|
+
const fallback = lastAssistantText || '';
|
|
1015
|
+
return {
|
|
1016
|
+
text: fallback,
|
|
1017
|
+
messages,
|
|
1018
|
+
steps: maxSteps,
|
|
1019
|
+
aborted: true
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
|
|
911
1023
|
const fallback = lastAssistantText || 'Stopped before final response.';
|
|
912
1024
|
return {
|
|
913
1025
|
text: `${fallback}\n\n[stopped] Reached max tool steps (${maxSteps}). Try a narrower prompt or increase execution.max_steps.`,
|
package/src/core/ast.js
CHANGED
|
@@ -153,6 +153,46 @@ function exactNodeForTarget(rootNode, target) {
|
|
|
153
153
|
return null;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Find the enclosing named structural symbol (function, class, method, etc.)
|
|
158
|
+
* for a given line range in already-parsed content. Returns null if not found
|
|
159
|
+
* or if the language is unsupported.
|
|
160
|
+
*/
|
|
161
|
+
export async function findEnclosingSymbol(content, filePath, line) {
|
|
162
|
+
const ext = path.extname(String(filePath || '')).toLowerCase();
|
|
163
|
+
const language = EXTENSION_LANGUAGE_MAP[ext];
|
|
164
|
+
if (!language) return null;
|
|
165
|
+
let parser = null;
|
|
166
|
+
let tree = null;
|
|
167
|
+
try {
|
|
168
|
+
const parsed = await parseContent(content, language);
|
|
169
|
+
parser = parsed.parser;
|
|
170
|
+
tree = parsed.tree;
|
|
171
|
+
const row = Math.max(0, Number(line || 1) - 1);
|
|
172
|
+
const node = tree.rootNode.descendantForPosition({ row, column: 0 });
|
|
173
|
+
let current = node;
|
|
174
|
+
while (current) {
|
|
175
|
+
if (current.type === 'program' || !current.parent) break;
|
|
176
|
+
const nameChild = current.childForFieldName('name');
|
|
177
|
+
if (nameChild) {
|
|
178
|
+
return {
|
|
179
|
+
name: nameChild.text,
|
|
180
|
+
kind: current.type,
|
|
181
|
+
start_line: current.startPosition.row + 1,
|
|
182
|
+
end_line: current.endPosition.row + 1
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
current = current.parent;
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
} catch {
|
|
189
|
+
return null;
|
|
190
|
+
} finally {
|
|
191
|
+
if (tree) tree.delete();
|
|
192
|
+
if (parser) parser.delete();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
156
196
|
export async function queryAst(root, args) {
|
|
157
197
|
const relativePath = String(args?.path || '').trim();
|
|
158
198
|
const querySource = String(args?.query || '').trim();
|