s-app-scoder-agent 0.9.8 → 0.9.9
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/.scoderrc.example.json +12 -1
- package/dist/agent/guardrails.js +19 -5
- package/dist/agent/mcp-bridge.js +101 -5
- package/dist/agent/run-loop.js +69 -10
- package/dist/agent/tool-executor.js +11 -5
- package/dist/agent/verification.js +28 -2
- package/dist/agent.js +224 -21
- package/dist/config/config.js +102 -4
- package/dist/context/compaction.js +39 -5
- package/dist/context/context.js +131 -28
- package/dist/index.js +12 -1
- package/dist/llm/context-probe.js +151 -0
- package/dist/llm/llm.js +21 -10
- package/dist/llm/prompt-metrics.js +78 -0
- package/dist/mcp/manager.js +3 -19
- package/dist/mcp/tool-filter.js +34 -0
- package/dist/tools/path-safety.js +22 -2
- package/dist/tools/read-file.js +24 -4
- package/dist/tui/hooks/use-submit.js +3 -3
- package/dist/tui/state.js +3 -2
- package/dist/workspace/build.js +5 -2
- package/dist/workspace/file-index.js +8 -3
- package/dist/workspace/fingerprint.js +44 -0
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -2,19 +2,21 @@ import { loadPermissionLists, loadPolicyRules, loadSavedApprovals } from "./conf
|
|
|
2
2
|
import { LlmClient, LlmError, redactSecrets } from "./llm/llm.js";
|
|
3
3
|
import { AnthropicClient } from "./llm/provider-anthropic.js";
|
|
4
4
|
import { allToolSpecs } from "./tools/registry.js";
|
|
5
|
-
import { setMaxOutputChars } from "./tools/path-safety.js";
|
|
5
|
+
import { setMaxOutputChars, setMaxToolOutputChars } from "./tools/path-safety.js";
|
|
6
6
|
import { detectBuildCommand } from "./workspace/build.js";
|
|
7
7
|
import { expandMentions } from "./context/context.js";
|
|
8
8
|
import { createGuardrailEngine } from "./agent/guardrails.js";
|
|
9
9
|
import { PROMPT_VERSION } from "./context/prompt.js";
|
|
10
10
|
import { PermissionChecker } from "./security/permissions.js";
|
|
11
11
|
import { SessionStore, EventSessionStore } from "./session/session.js";
|
|
12
|
-
import { DEFAULT_COMPACTION, estimateTokens, findCutIndex, extractSummaryInput, COMPACTION_INSTRUCTION, applyCompaction } from "./context/compaction.js";
|
|
12
|
+
import { DEFAULT_COMPACTION, estimateTokens, estimateSchemaTokens, findCutIndex, extractSummaryInput, COMPACTION_INSTRUCTION, applyCompaction } from "./context/compaction.js";
|
|
13
13
|
import { SnapshotManager } from "./workspace/snapshot.js";
|
|
14
14
|
import { contextWindowFor } from "./llm/model-catalog.js";
|
|
15
|
+
import { probeContextWindow } from "./llm/context-probe.js";
|
|
16
|
+
import { promptMetrics } from "./llm/prompt-metrics.js";
|
|
15
17
|
import { JsonlLogger } from "./session/logger.js";
|
|
16
18
|
import { createRuntime, parseCustomProviders, resolveProvider, splitProviderModel } from "./llm/provider-registry.js";
|
|
17
|
-
import { loadRcFile } from "./config/config.js";
|
|
19
|
+
import { loadRcFile, isLocalBaseUrl } from "./config/config.js";
|
|
18
20
|
import { watchWorkdir } from "./workspace/watch.js";
|
|
19
21
|
import { discoverAgents, findAgent } from "./context/agents.js";
|
|
20
22
|
import { emptyWorkingMemory, noteFileRead, noteFileChanged, noteFailure, notePlan, } from "./context/memory.js";
|
|
@@ -22,7 +24,7 @@ import { SteeringInbox } from "./session/steering.js";
|
|
|
22
24
|
import { randomUUID } from "node:crypto";
|
|
23
25
|
import { consoleReporter } from "./io/console-reporter.js";
|
|
24
26
|
import { resolveVerify as resolveVerificationCommands, verifyAll as runVerification, failureSignature as getFailureSignature, collectRunDiff as collectVerificationDiff, selfReviewGate as runSelfReviewGate, } from "./agent/verification.js";
|
|
25
|
-
import { callMcpTool as callMcpToolBridge, confirmMcpTool as confirmMcpToolBridge, connectMcp as connectMcpBridge, invokeMcpTool as invokeMcpToolBridge, mcpStatus as mcpStatusBridge, rebuildToolList as rebuildMcpToolList, shutdownMcp as shutdownMcpBridge, } from "./agent/mcp-bridge.js";
|
|
27
|
+
import { callMcpTool as callMcpToolBridge, confirmMcpTool as confirmMcpToolBridge, connectMcp as connectMcpBridge, governedMcpSpecs, invokeMcpTool as invokeMcpToolBridge, mcpStatus as mcpStatusBridge, rebuildToolList as rebuildMcpToolList, shutdownMcp as shutdownMcpBridge, } from "./agent/mcp-bridge.js";
|
|
26
28
|
import { InteractionBroker } from "./agent/interaction.js";
|
|
27
29
|
import { ToolExecutor } from "./agent/tool-executor.js";
|
|
28
30
|
import { PromptContext, buildRunMessages, memoryBlock } from "./agent/prompt-context.js";
|
|
@@ -55,6 +57,8 @@ export class Agent {
|
|
|
55
57
|
sessionCreatedAt;
|
|
56
58
|
currentTask = "";
|
|
57
59
|
lastBuildResult = null;
|
|
60
|
+
/** P1-2 (RC-5): kết quả verify thành công gần nhất theo (lệnh, fingerprint). */
|
|
61
|
+
verifyCacheStore = new Map();
|
|
58
62
|
/** Accumulated token usage across the current run (session state). */
|
|
59
63
|
usageTotals = { calls: 0, inputTokens: 0, outputTokens: 0, cachedTokens: 0 };
|
|
60
64
|
mcp = null;
|
|
@@ -174,6 +178,13 @@ export class Agent {
|
|
|
174
178
|
onToolOutput: (stream, delta) => this.handleToolOutput("build", stream, delta),
|
|
175
179
|
endToolStream: () => this.endToolStream(),
|
|
176
180
|
setLastBuildResult: (result) => { this.lastBuildResult = result; },
|
|
181
|
+
// P1-2: verify cache sống trong run (key = chuỗi lệnh verify).
|
|
182
|
+
verifyCache: {
|
|
183
|
+
read: (key) => this.verifyCacheStore.get(key),
|
|
184
|
+
write: (key, entry) => {
|
|
185
|
+
this.verifyCacheStore.set(key, entry);
|
|
186
|
+
},
|
|
187
|
+
},
|
|
177
188
|
};
|
|
178
189
|
return runVerification(runtime, buildCmd);
|
|
179
190
|
}
|
|
@@ -269,6 +280,10 @@ export class Agent {
|
|
|
269
280
|
// G7: thresholds từ rc limits.
|
|
270
281
|
if (config.limits?.maxOutputChars)
|
|
271
282
|
setMaxOutputChars(config.limits.maxOutputChars);
|
|
283
|
+
// P0-2: ngân sách ký tự của một tool result trong history (read_file cắt
|
|
284
|
+
// theo dòng theo giá trị này).
|
|
285
|
+
if (config.limits?.toolOutputChars)
|
|
286
|
+
setMaxToolOutputChars(config.limits.toolOutputChars);
|
|
272
287
|
this.session = new SessionStore(config.workdir, config.noSave !== true);
|
|
273
288
|
this.events = new EventSessionStore(config.workdir, config.noSave !== true, this.session.id);
|
|
274
289
|
this.snapshots = new SnapshotManager(config.workdir, config.snapshots !== false);
|
|
@@ -563,7 +578,9 @@ export class Agent {
|
|
|
563
578
|
emitContextEvent(compacted = false) {
|
|
564
579
|
try {
|
|
565
580
|
const window = this.compactionSettings().contextWindow;
|
|
566
|
-
|
|
581
|
+
// P1-1: token áp lực context phải tính cả tool schema (chi phí cố định
|
|
582
|
+
// mỗi lượt) — UI/`/compact` mới hiện đúng mức "còn thở" thật.
|
|
583
|
+
const tokens = estimateTokens(this.messages, this.fixedPromptTokens());
|
|
567
584
|
const pct = window > 0 ? Math.min(100, Math.round((tokens / window) * 100)) : 0;
|
|
568
585
|
this.emit({ type: "context", tokens, window, pct, ...(compacted ? { compacted: true } : {}) });
|
|
569
586
|
}
|
|
@@ -577,6 +594,19 @@ export class Agent {
|
|
|
577
594
|
const input = usage.prompt_tokens ?? 0;
|
|
578
595
|
const output = usage.completion_tokens ?? 0;
|
|
579
596
|
const cached = usage.cached_tokens ?? 0;
|
|
597
|
+
// P1-3: metrics của lượt vừa rồi (consumed one-shot — lượt summarizer
|
|
598
|
+
// của compaction đi đường khác nên không có perf).
|
|
599
|
+
const perf = this.lastPerf;
|
|
600
|
+
this.lastPerf = null;
|
|
601
|
+
// P1-1 calibration: engine vừa báo đúng số token nó thấy. Lấy số đó trừ
|
|
602
|
+
// đi phần messages mình ước lượng = chi phí cố định THẬT (tool schema +
|
|
603
|
+
// chat template + sai số bộ đếm). bộ đếm cl100k đo phần schema rất chuẩn
|
|
604
|
+
// (lệch ~1% trên Qwen3.6-35B) nhưng không biết token đóng gói của template.
|
|
605
|
+
if (perf && input > 0 && perf.messageTokens > 0) {
|
|
606
|
+
const fixed = input - perf.messageTokens;
|
|
607
|
+
if (fixed > 0)
|
|
608
|
+
this.measuredFixedTokens = fixed;
|
|
609
|
+
}
|
|
580
610
|
this.usageTotals.calls += 1;
|
|
581
611
|
this.usageTotals.inputTokens += input;
|
|
582
612
|
this.usageTotals.outputTokens += output;
|
|
@@ -595,8 +625,38 @@ export class Agent {
|
|
|
595
625
|
output_tokens: output,
|
|
596
626
|
cached_tokens: cached,
|
|
597
627
|
},
|
|
628
|
+
...(perf
|
|
629
|
+
? {
|
|
630
|
+
perf: {
|
|
631
|
+
ttftMs: perf.ttftMs,
|
|
632
|
+
callMs: perf.callMs,
|
|
633
|
+
/** Chi phí cố định/lượt mà agent đang dùng để tính ngân sách. */
|
|
634
|
+
fixedTokens: this.fixedPromptTokens(),
|
|
635
|
+
fixedSource: this.fixedPromptSource(),
|
|
636
|
+
/** token/s vùng prefill — TTFT ≈ prefill trên máy local. */
|
|
637
|
+
prefillTokPerSec: perf.ttftMs > 0 && input > 0 ? Math.round((input / perf.ttftMs) * 1000) : undefined,
|
|
638
|
+
...(perf.prompt
|
|
639
|
+
? {
|
|
640
|
+
promptChars: perf.prompt.promptChars,
|
|
641
|
+
/** phần chat (system+repo map+history+task) vs phần tool defs */
|
|
642
|
+
messageChars: perf.prompt.messageChars,
|
|
643
|
+
schemaChars: perf.prompt.schemaChars,
|
|
644
|
+
promptMessages: perf.prompt.messages,
|
|
645
|
+
promptTools: perf.prompt.tools,
|
|
646
|
+
promptHash: perf.prompt.promptHash,
|
|
647
|
+
historyHash: perf.prompt.historyHash,
|
|
648
|
+
/** RC-1: history lượt trước có còn là tiền tố của lượt này? */
|
|
649
|
+
prefixReused: perf.prompt.prefixReused,
|
|
650
|
+
commonPrefixMessages: perf.prompt.commonPrefixMessages,
|
|
651
|
+
}
|
|
652
|
+
: {}),
|
|
653
|
+
},
|
|
654
|
+
}
|
|
655
|
+
: {}),
|
|
598
656
|
});
|
|
599
657
|
}
|
|
658
|
+
/** prefixHash của lượt trước — phát hiện invalidation KV-cache (P1-3). */
|
|
659
|
+
lastPromptRef = null;
|
|
600
660
|
/** Reset per-run usage accumulation (called when a fresh run starts). */
|
|
601
661
|
resetUsageTotals() {
|
|
602
662
|
this.usageTotals = { calls: 0, inputTokens: 0, outputTokens: 0, cachedTokens: 0 };
|
|
@@ -610,7 +670,72 @@ export class Agent {
|
|
|
610
670
|
* result (flagged `incomplete`) instead of discarding it — the run loop
|
|
611
671
|
* executes the tool calls and continues with a synthetic instruction.
|
|
612
672
|
*/
|
|
673
|
+
/**
|
|
674
|
+
* P1-3: một lượt gọi LLM được đo: thời gian tới token đầu (TTFT ≈ prefill
|
|
675
|
+
* trên máy local), tổng thời gian, và hash prompt/prefix. `prefixHash` đổi
|
|
676
|
+
* giữa hai lượt = có gì đó sửa history ⇒ mất KV-cache (RC-1).
|
|
677
|
+
* Consumed one-shot bởi recordLlmUsage (lượt summarizer không có perf).
|
|
678
|
+
*/
|
|
679
|
+
pendingPerf = null;
|
|
680
|
+
lastPerf = null;
|
|
681
|
+
/**
|
|
682
|
+
* P1-1 (calibration): chi phí cố định của một lượt gọi ĐO TỪ CHÍNH ENGINE.
|
|
683
|
+
* = prompt_tokens thật − số token mình ước lượng cho messages. Phần chênh
|
|
684
|
+
* gồm tool schema + token đóng gói của chat template (ChatML) + sai số bộ
|
|
685
|
+
* đếm. Đo trên Qwen3.6-35B: schema ước lượng đúng ~1%, nhưng prompt thật
|
|
686
|
+
* lớn hơn ước lượng tổng ~19% vì chat template. null = chưa có phép đo.
|
|
687
|
+
*/
|
|
688
|
+
measuredFixedTokens = null;
|
|
689
|
+
/**
|
|
690
|
+
* Chi phí cố định mỗi lượt (không nằm trong history) để trừ khỏi window khi
|
|
691
|
+
* tính ngân sách trim/compaction. Lấy max giữa ước lượng tool schema và
|
|
692
|
+
* phép đo thật từ engine — phép đo chỉ có thể SIẾT ngân sách, không bao giờ
|
|
693
|
+
* mở rộng hơn mức ước lượng (an toàn với RC-4).
|
|
694
|
+
*/
|
|
695
|
+
fixedPromptTokens() {
|
|
696
|
+
return Math.max(estimateSchemaTokens(this.tools), this.measuredFixedTokens ?? 0);
|
|
697
|
+
}
|
|
698
|
+
/** Nguồn của con số trên — ghi vào log để giải thích ngân sách. */
|
|
699
|
+
fixedPromptSource() {
|
|
700
|
+
return this.measuredFixedTokens !== null && this.measuredFixedTokens >= estimateSchemaTokens(this.tools)
|
|
701
|
+
? "measured"
|
|
702
|
+
: "estimate";
|
|
703
|
+
}
|
|
704
|
+
/** One LLM turn (wrapper) — see chatWithStreamInner for the streaming logic. */
|
|
613
705
|
async chatWithStream() {
|
|
706
|
+
const t0 = Date.now();
|
|
707
|
+
let prompt = null;
|
|
708
|
+
// Số token mình ước lượng cho phần messages — dùng để suy ra chi phí cố
|
|
709
|
+
// định thật của engine khi usage quay về (P1-1 calibration).
|
|
710
|
+
let messageTokens = 0;
|
|
711
|
+
try {
|
|
712
|
+
messageTokens = estimateTokens(this.messages);
|
|
713
|
+
prompt = promptMetrics(this.messages, this.tools);
|
|
714
|
+
}
|
|
715
|
+
catch {
|
|
716
|
+
// metrics không được phép làm hỏng lượt gọi
|
|
717
|
+
}
|
|
718
|
+
this.pendingPerf = { t0, ttftMs: 0, messageTokens, prompt };
|
|
719
|
+
// Keep the per-message hashes for the NEXT call: `prefixReused` in the log
|
|
720
|
+
// is the direct answer to "KV-cache còn dùng được không?" (RC-1).
|
|
721
|
+
this.lastPromptRef = prompt ? { messageHashes: prompt.messageHashes } : null;
|
|
722
|
+
try {
|
|
723
|
+
return await this.chatWithStreamInner();
|
|
724
|
+
}
|
|
725
|
+
finally {
|
|
726
|
+
const p = this.pendingPerf;
|
|
727
|
+
this.pendingPerf = null;
|
|
728
|
+
if (p)
|
|
729
|
+
this.lastPerf = { ttftMs: p.ttftMs, callMs: Date.now() - t0, messageTokens: p.messageTokens, prompt: p.prompt };
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
/** Đánh dấu token đầu tiên tới (best-effort, cho TTFT). */
|
|
733
|
+
markFirstToken() {
|
|
734
|
+
const p = this.pendingPerf;
|
|
735
|
+
if (p && p.ttftMs === 0)
|
|
736
|
+
p.ttftMs = Date.now() - p.t0;
|
|
737
|
+
}
|
|
738
|
+
async chatWithStreamInner() {
|
|
614
739
|
const signal = this.runSignal.signal;
|
|
615
740
|
if (!this.config.stream) {
|
|
616
741
|
return this.client.chat(this.messages, this.tools, { signal });
|
|
@@ -621,6 +746,10 @@ export class Agent {
|
|
|
621
746
|
let sawToolCallDelta = false;
|
|
622
747
|
try {
|
|
623
748
|
for await (const chunk of this.client.streamChat(this.messages, this.tools, { signal })) {
|
|
749
|
+
// P1-3: TTFT tính từ lúc request bắt đầu tới delta đầu tiên (bất kể
|
|
750
|
+
// loại — content/reasoning/tool_call đều nghĩa là prefill đã xong).
|
|
751
|
+
if (chunk.contentDelta || chunk.reasoningDelta || chunk.toolCallDelta)
|
|
752
|
+
this.markFirstToken();
|
|
624
753
|
if (chunk.reasoningDelta) {
|
|
625
754
|
// I-05: reasoning hiển thị dim qua UI stream; không vào llm_delta.
|
|
626
755
|
this.emit({ type: "llm_reasoning", delta: chunk.reasoningDelta });
|
|
@@ -699,7 +828,11 @@ export class Agent {
|
|
|
699
828
|
pendingAllowedTools: this.promptContext.pendingTools(),
|
|
700
829
|
getMcp: () => this.mcp,
|
|
701
830
|
setMcp: (manager) => { this.mcp = manager; },
|
|
702
|
-
setTools: (tools) => {
|
|
831
|
+
setTools: (tools) => {
|
|
832
|
+
this.tools = tools;
|
|
833
|
+
// Tool list đổi ⇒ chi phí cố định đo trước không còn đúng nữa.
|
|
834
|
+
this.measuredFixedTokens = null;
|
|
835
|
+
},
|
|
703
836
|
emit: (event) => this.emit(event),
|
|
704
837
|
confirmMcpTool: (name) => this.confirmMcpTool(name),
|
|
705
838
|
};
|
|
@@ -708,6 +841,41 @@ export class Agent {
|
|
|
708
841
|
async connectMcp() {
|
|
709
842
|
await connectMcpBridge(this.mcpBridgeRuntime());
|
|
710
843
|
}
|
|
844
|
+
/**
|
|
845
|
+
* P1-1 (RC-4): hỏi server lấy context window THẬT khi user chưa đặt tường
|
|
846
|
+
* minh (rc `contextWindow` / provider def). Fallback 128k của catalog khiến
|
|
847
|
+
* compaction và trim kích hoạt muộn hơn server thật hàng chục nghìn token —
|
|
848
|
+
* server tự cắt history, và mỗi lần nó cắt là một lần mất KV-cache.
|
|
849
|
+
*/
|
|
850
|
+
async applyContextProbe() {
|
|
851
|
+
if (this.config.contextWindow)
|
|
852
|
+
return;
|
|
853
|
+
let probe = null;
|
|
854
|
+
try {
|
|
855
|
+
// Server local vẫn có thể bật --api-key: /props trả 401 nếu không gửi
|
|
856
|
+
// Authorization, và probe im lặng => agent tưởng còn 128k (RC-4 cũ).
|
|
857
|
+
probe = await probeContextWindow(this.config.baseUrl, this.client.model, 1_500, {
|
|
858
|
+
apiKey: this.config.apiKey,
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
catch {
|
|
862
|
+
// probe là best-effort — không được làm hỏng run
|
|
863
|
+
}
|
|
864
|
+
if (!probe) {
|
|
865
|
+
if (isLocalBaseUrl(this.config.baseUrl)) {
|
|
866
|
+
const catalog = contextWindowFor(this.config.provider, this.config.model);
|
|
867
|
+
this.reporter.warn(`không probe được context window của server cục bộ — đang dùng fallback ${catalog} token. ` +
|
|
868
|
+
`Nếu nhỏ hơn (vd. llama.cpp -c 16384 / Ollama num_ctx), đặt "contextWindow" trong rc để trim/compaction tính đúng.`);
|
|
869
|
+
}
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
this.config.contextWindow = probe.tokens;
|
|
873
|
+
this.logger.log({ type: "context_window", tokens: probe.tokens, source: probe.source, detail: probe.detail, model: this.client.model });
|
|
874
|
+
const catalog = contextWindowFor(this.config.provider, this.config.model);
|
|
875
|
+
if (probe.tokens !== catalog) {
|
|
876
|
+
this.reporter.info(`context window: ${probe.tokens} token (${probe.detail}) — thay fallback ${catalog} token, compaction/trim tính theo số này`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
711
879
|
/** Rebuild built-in and connected MCP tool specs for the current skill scope. */
|
|
712
880
|
rebuildToolList() {
|
|
713
881
|
rebuildMcpToolList(this.mcpBridgeRuntime());
|
|
@@ -727,6 +895,37 @@ export class Agent {
|
|
|
727
895
|
this.watcher = null;
|
|
728
896
|
this.shutdownMcp();
|
|
729
897
|
}
|
|
898
|
+
/**
|
|
899
|
+
* Dấu mốc "một lượt run bắt đầu": stamp `run.started` vào event log VÀ emit
|
|
900
|
+
* `run_start` cho UI.
|
|
901
|
+
*
|
|
902
|
+
* Trước đây chỉ `run()` làm việc này; `runTurn()` (câu hỏi thứ 2+ trong
|
|
903
|
+
* TUI/REPL) và `resume()` bỏ qua, mà reducer TUI lại reset mọi anchor
|
|
904
|
+
* per-run trong nhánh `run_start` (`startedAt`, `endedAt = null`, `usage`,
|
|
905
|
+
* `context`, `phases`, `lastLlmTokPerSec`...). Hệ quả đo được: từ câu hỏi
|
|
906
|
+
* thứ hai, `endedAt` vẫn đóng của lượt trước ⇒ thanh elapsed đứng im, áp lực
|
|
907
|
+
* ctx và tok/s không nhảy nữa — dù text vẫn stream bình thường.
|
|
908
|
+
*
|
|
909
|
+
* `taskForUi` là NGUYÊN VĂN user gõ (chưa expand mentions) để dedupe trong
|
|
910
|
+
* reducer khớp đúng item user mà TUI/REPL đã tự append. `loggedTask` (đã
|
|
911
|
+
* expand) ghi vào event log để resume/phân tích giữ đúng nội dung model thấy.
|
|
912
|
+
* `quiet` = UI đã tự hiển thị task (resume) ⇒ reducer không append thêm.
|
|
913
|
+
*/
|
|
914
|
+
emitRunBoundary(taskForUi, buildCmd, loggedTask, quiet = false) {
|
|
915
|
+
this.events.append({ type: "run.started", task: loggedTask ?? taskForUi });
|
|
916
|
+
this.emit({
|
|
917
|
+
type: "run_start",
|
|
918
|
+
task: taskForUi,
|
|
919
|
+
model: this.client.model,
|
|
920
|
+
provider: this.config.provider,
|
|
921
|
+
workdir: this.config.workdir,
|
|
922
|
+
buildCmd,
|
|
923
|
+
permission: this.config.permissionMode,
|
|
924
|
+
schemaVersion: EVENT_SCHEMA_VERSION,
|
|
925
|
+
agent: this.config.agent ?? "build",
|
|
926
|
+
...(quiet ? { quiet: true } : {}),
|
|
927
|
+
});
|
|
928
|
+
}
|
|
730
929
|
/**
|
|
731
930
|
* Autonomous closed loop:
|
|
732
931
|
* task -> LLM plans -> tool calls -> build -> on failure: read error, patch, retry.
|
|
@@ -736,6 +935,7 @@ export class Agent {
|
|
|
736
935
|
throw new Error("No API key configured. Set SCODER_API_KEY or OPENAI_API_KEY (or put apiKey in .scoderrc.json).");
|
|
737
936
|
}
|
|
738
937
|
const buildCmd = this.buildCmd;
|
|
938
|
+
const rawTask = task;
|
|
739
939
|
// @file mention expansion — task trước khi build messages.
|
|
740
940
|
task = expandMentions(task, this.config.workdir);
|
|
741
941
|
this.reporter.banner("scoder", [
|
|
@@ -746,7 +946,6 @@ export class Agent {
|
|
|
746
946
|
["steps", `max ${this.config.maxSteps} · permission ${this.config.permissionMode}`],
|
|
747
947
|
]);
|
|
748
948
|
this.currentTask = task;
|
|
749
|
-
this.events.append({ type: "run.started", task });
|
|
750
949
|
this.resetRunSignal();
|
|
751
950
|
this.resetUsageTotals();
|
|
752
951
|
this.filesModified = false;
|
|
@@ -764,7 +963,10 @@ export class Agent {
|
|
|
764
963
|
const allow = new Set(pendingAllowedTools.map((t) => t.toLowerCase()));
|
|
765
964
|
baseTools = baseTools.filter((s) => allow.has(s.function.name.toLowerCase()));
|
|
766
965
|
}
|
|
767
|
-
|
|
966
|
+
// P0-3: MCP specs đi qua đúng một cửa (allowlist "mcpTools" + trần
|
|
967
|
+
// schema). Trước đây chỗ này tự ghép raw specs → run nối tiếp trên cùng
|
|
968
|
+
// Agent (MCP đã connect, connectMcp return sớm) gửi lại đủ 53 tool.
|
|
969
|
+
const mcpSpecs = governedMcpSpecs(this.mcpBridgeRuntime());
|
|
768
970
|
if (pendingAllowedTools?.length) {
|
|
769
971
|
const allow = new Set(pendingAllowedTools.map((t) => t.toLowerCase()));
|
|
770
972
|
this.tools = [...baseTools, ...mcpSpecs.filter((s) => allow.has(s.function.name.toLowerCase()))];
|
|
@@ -783,17 +985,8 @@ export class Agent {
|
|
|
783
985
|
// Phiên bản system prompt được ghi vào log — giúp so sánh chất lượng
|
|
784
986
|
// giữa các phiên bản prompt khi phân tích JSONL.
|
|
785
987
|
this.logger.log({ type: "prompt_version", version: PROMPT_VERSION });
|
|
786
|
-
this.
|
|
787
|
-
|
|
788
|
-
task,
|
|
789
|
-
model: this.client.model,
|
|
790
|
-
provider: this.config.provider,
|
|
791
|
-
workdir: this.config.workdir,
|
|
792
|
-
buildCmd,
|
|
793
|
-
permission: this.config.permissionMode,
|
|
794
|
-
schemaVersion: EVENT_SCHEMA_VERSION,
|
|
795
|
-
agent: this.config.agent ?? "build",
|
|
796
|
-
});
|
|
988
|
+
this.emitRunBoundary(rawTask, buildCmd, task);
|
|
989
|
+
await this.applyContextProbe();
|
|
797
990
|
await this.connectMcp();
|
|
798
991
|
this.promptContext.clearPendingTools();
|
|
799
992
|
return this.runLoop(0, buildCmd, null);
|
|
@@ -833,6 +1026,7 @@ export class Agent {
|
|
|
833
1026
|
return this.run(task);
|
|
834
1027
|
}
|
|
835
1028
|
const buildCmd = this.buildCmd;
|
|
1029
|
+
const rawTask = task;
|
|
836
1030
|
task = expandMentions(task, this.config.workdir);
|
|
837
1031
|
this.currentTask = `${this.currentTask}\n${task}`.trim();
|
|
838
1032
|
this.resetRunSignal();
|
|
@@ -842,7 +1036,10 @@ export class Agent {
|
|
|
842
1036
|
for (const content of this.promptContext.takePendingContext())
|
|
843
1037
|
this.messages.push({ role: "user", content });
|
|
844
1038
|
this.messages.push({ role: "user", content: task });
|
|
1039
|
+
// Cùng một dấu mốc run_start mà run() phát — UI reset anchor per-run ở đó.
|
|
1040
|
+
this.emitRunBoundary(rawTask, buildCmd, task);
|
|
845
1041
|
this.rebuildToolList();
|
|
1042
|
+
await this.applyContextProbe();
|
|
846
1043
|
await this.connectMcp();
|
|
847
1044
|
this.promptContext.clearPendingTools();
|
|
848
1045
|
return this.runLoop(0, buildCmd, null);
|
|
@@ -888,8 +1085,11 @@ export class Agent {
|
|
|
888
1085
|
});
|
|
889
1086
|
}
|
|
890
1087
|
const lastBuild = restored.lastBuild;
|
|
891
|
-
// v2: opening a resumed run stamps a fresh run.started (event log
|
|
892
|
-
|
|
1088
|
+
// v2: opening a resumed run stamps a fresh run.started + run_start (event log
|
|
1089
|
+
// continuity VÀ UI anchor reset). quiet: transcript đã được restore, đừng
|
|
1090
|
+
// append lại task thành một dòng user mới.
|
|
1091
|
+
this.emitRunBoundary(this.currentTask, buildCmd, undefined, true);
|
|
1092
|
+
await this.applyContextProbe();
|
|
893
1093
|
await this.connectMcp();
|
|
894
1094
|
return this.runLoop(session.steps ?? 0, buildCmd, lastBuild);
|
|
895
1095
|
}
|
|
@@ -947,6 +1147,9 @@ export class Agent {
|
|
|
947
1147
|
steerCounts: () => this.steerInbox.counts(),
|
|
948
1148
|
chatWithStream: () => this.chatWithStream(),
|
|
949
1149
|
recordLlmUsage: (usage) => this.recordLlmUsage(usage),
|
|
1150
|
+
tools: () => this.tools,
|
|
1151
|
+
fixedPromptTokens: () => this.fixedPromptTokens(),
|
|
1152
|
+
fixedPromptSource: () => this.fixedPromptSource(),
|
|
950
1153
|
emit: (event) => this.emit(event),
|
|
951
1154
|
emitContextEvent: (force) => this.emitContextEvent(force),
|
|
952
1155
|
toolExecutor: () => this.toolExecutor(),
|
package/dist/config/config.js
CHANGED
|
@@ -10,24 +10,109 @@ const ANTHROPIC_DEFAULT_MODEL = "claude-sonnet-4-5";
|
|
|
10
10
|
* G7: parse + validate rc "limits" — số nguyên dương; giá trị sai bỏ qua
|
|
11
11
|
* (dùng default). Defaults là nguồn sự thật duy nhất (resolveLimits).
|
|
12
12
|
*/
|
|
13
|
-
export const LIMIT_DEFAULTS = {
|
|
13
|
+
export const LIMIT_DEFAULTS = {
|
|
14
|
+
requestTimeoutMs: 120_000,
|
|
15
|
+
maxOutputChars: 20_000,
|
|
16
|
+
toolConcurrency: 4,
|
|
17
|
+
renderCap: 200,
|
|
18
|
+
toolOutputChars: 8_000,
|
|
19
|
+
mcpSchemaTokens: 0,
|
|
20
|
+
buildTimeoutSecs: 600,
|
|
21
|
+
};
|
|
14
22
|
function parseLimits(raw) {
|
|
15
23
|
if (!raw || typeof raw !== "object")
|
|
16
24
|
return undefined;
|
|
17
25
|
const r = raw;
|
|
18
26
|
const num = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.floor(v) : undefined;
|
|
19
27
|
const out = {};
|
|
20
|
-
for (const k of [
|
|
28
|
+
for (const k of [
|
|
29
|
+
"requestTimeoutMs",
|
|
30
|
+
"maxOutputChars",
|
|
31
|
+
"toolConcurrency",
|
|
32
|
+
"renderCap",
|
|
33
|
+
"toolOutputChars",
|
|
34
|
+
"buildTimeoutSecs",
|
|
35
|
+
]) {
|
|
21
36
|
const v = num(r[k]);
|
|
22
37
|
if (v !== undefined)
|
|
23
38
|
out[k] = v;
|
|
24
39
|
}
|
|
40
|
+
// mcpSchemaTokens: 0 có nghĩa "tắt" — nhận cả 0, chỉ bỏ số âm/NaN.
|
|
41
|
+
const mcpCap = r.mcpSchemaTokens;
|
|
42
|
+
if (typeof mcpCap === "number" && Number.isFinite(mcpCap) && mcpCap >= 0)
|
|
43
|
+
out.mcpSchemaTokens = Math.floor(mcpCap);
|
|
25
44
|
return Object.keys(out).length > 0 ? out : undefined;
|
|
26
45
|
}
|
|
27
|
-
/** G7: limits đã resolve (defaults + rc override) — các module đọc từ đây. */
|
|
28
46
|
export function resolveLimits(limits) {
|
|
29
47
|
return { ...LIMIT_DEFAULTS, ...limits };
|
|
30
48
|
}
|
|
49
|
+
// ── P2 (plan/performance-issue.md): profile cho model chạy cục bộ ───────────
|
|
50
|
+
/**
|
|
51
|
+
* Model chạy trên máy của user (Ollama / LM Studio / vLLM / llama.cpp server)?
|
|
52
|
+
* Nhận diện theo host của baseUrl — loopback là tín hiệu đáng tin duy nhất mà
|
|
53
|
+
* không cần gọi network.
|
|
54
|
+
*/
|
|
55
|
+
export function isLocalBaseUrl(baseUrl) {
|
|
56
|
+
try {
|
|
57
|
+
const u = new URL(baseUrl);
|
|
58
|
+
const h = u.hostname.replace(/^\[|\]$/g, "");
|
|
59
|
+
return h === "localhost" || h === "127.0.0.1" || h === "::1" || h === "0.0.0.0" || h.endsWith(".local");
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Mặc định của profile local — chỉ áp dụng khi user KHÔNG set tường minh. */
|
|
66
|
+
export const LOCAL_PROFILE_DEFAULTS = {
|
|
67
|
+
/** P2-1: local model chậm, 40 bước là nhiều giờ; bao việc hơn trong 24 bước. */
|
|
68
|
+
maxSteps: 24,
|
|
69
|
+
/** P2-3: prefill 20–60k token trên máy cá nhân có thể vượt 120s → timeout + retry. */
|
|
70
|
+
requestTimeoutMs: 600_000,
|
|
71
|
+
/** P0-2: history của MỘT tool result nhỏ hơn mặc định cloud (8k). */
|
|
72
|
+
toolOutputChars: 6_000,
|
|
73
|
+
/** P0-3: trần token cho MCP schema; 46 tool GitHub MCP ≈ 12k token mỗi lượt. */
|
|
74
|
+
mcpSchemaTokens: 6_000,
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Áp mặc định phù hợp cho local engine. Trả về danh sách thay đổi đã áp (để
|
|
78
|
+
* log/kiểm thử); rỗng khi không áp dụng (cloud, hoặc localProfile:false).
|
|
79
|
+
*
|
|
80
|
+
* Lý do KHÔNG tắt repoMap/snapshots trong profile:
|
|
81
|
+
* - repoMap: nằm trong system prompt → prefix ổn định, không phá KV-cache, và
|
|
82
|
+
* giảm số bước khám phá (mỗi bước mới = một lượt prefill).
|
|
83
|
+
* - snapshots: git ops cục bộ, không tốn token.
|
|
84
|
+
* - selfReview: thêm MỘT lượt LLM với prompt gần đầy → tắt thật.
|
|
85
|
+
*/
|
|
86
|
+
export function applyLocalProfile(cfg, explicit = {}) {
|
|
87
|
+
if (cfg.localProfile === false)
|
|
88
|
+
return [];
|
|
89
|
+
if (!isLocalBaseUrl(cfg.baseUrl))
|
|
90
|
+
return [];
|
|
91
|
+
const applied = [];
|
|
92
|
+
const limits = { ...cfg.limits };
|
|
93
|
+
if (!explicit.maxSteps && cfg.maxSteps > LOCAL_PROFILE_DEFAULTS.maxSteps) {
|
|
94
|
+
cfg.maxSteps = LOCAL_PROFILE_DEFAULTS.maxSteps;
|
|
95
|
+
applied.push(`maxSteps=${cfg.maxSteps}`);
|
|
96
|
+
}
|
|
97
|
+
if (limits.requestTimeoutMs === undefined) {
|
|
98
|
+
limits.requestTimeoutMs = LOCAL_PROFILE_DEFAULTS.requestTimeoutMs;
|
|
99
|
+
applied.push(`requestTimeoutMs=${LOCAL_PROFILE_DEFAULTS.requestTimeoutMs}`);
|
|
100
|
+
}
|
|
101
|
+
if (limits.toolOutputChars === undefined) {
|
|
102
|
+
limits.toolOutputChars = LOCAL_PROFILE_DEFAULTS.toolOutputChars;
|
|
103
|
+
applied.push(`toolOutputChars=${LOCAL_PROFILE_DEFAULTS.toolOutputChars}`);
|
|
104
|
+
}
|
|
105
|
+
if (limits.mcpSchemaTokens === undefined) {
|
|
106
|
+
limits.mcpSchemaTokens = LOCAL_PROFILE_DEFAULTS.mcpSchemaTokens;
|
|
107
|
+
applied.push(`mcpSchemaTokens=${LOCAL_PROFILE_DEFAULTS.mcpSchemaTokens}`);
|
|
108
|
+
}
|
|
109
|
+
cfg.limits = limits;
|
|
110
|
+
if (cfg.selfReview === undefined) {
|
|
111
|
+
cfg.selfReview = false;
|
|
112
|
+
applied.push("selfReview=false");
|
|
113
|
+
}
|
|
114
|
+
return applied;
|
|
115
|
+
}
|
|
31
116
|
function readRcFile(path, label) {
|
|
32
117
|
if (!existsSync(path))
|
|
33
118
|
return {};
|
|
@@ -170,6 +255,9 @@ export function loadConfig(overrides = {}) {
|
|
|
170
255
|
// (steps < NaN is always false → the loop never runs).
|
|
171
256
|
const rawMaxSteps = Number(process.env.SCODER_MAX_STEPS ?? rc.maxSteps ?? 40);
|
|
172
257
|
const validatedMaxSteps = Number.isFinite(rawMaxSteps) && rawMaxSteps >= 1 ? Math.floor(rawMaxSteps) : 40;
|
|
258
|
+
// P2-1: profile local CHỈ hạ maxSteps khi user không đặt tường minh ở
|
|
259
|
+
// flag/env/rc — tôn trọng cấu hình rõ ràng của user.
|
|
260
|
+
const maxStepsExplicit = overrides.maxSteps !== undefined || process.env.SCODER_MAX_STEPS !== undefined || rc.maxSteps !== undefined;
|
|
173
261
|
const cfg = {
|
|
174
262
|
provider,
|
|
175
263
|
baseUrl: overrides.baseUrl ?? process.env.SCODER_BASE_URL ?? process.env.OPENAI_BASE_URL ?? rc.baseUrl ?? (isAnthropic ? ANTHROPIC_BASE_URL : DEFAULT_BASE_URL),
|
|
@@ -182,7 +270,7 @@ export function loadConfig(overrides = {}) {
|
|
|
182
270
|
"",
|
|
183
271
|
model: modelFromSlash || (overrides.model ?? process.env.SCODER_MODEL ?? process.env.OPENAI_MODEL ?? rc.model ?? (isAnthropic ? ANTHROPIC_DEFAULT_MODEL : DEFAULT_MODEL)),
|
|
184
272
|
providerDef,
|
|
185
|
-
contextWindow: providerDef?.contextWindow,
|
|
273
|
+
contextWindow: rc.contextWindow ?? providerDef?.contextWindow,
|
|
186
274
|
workdir,
|
|
187
275
|
buildCmd: overrides.buildCmd ?? process.env.SCODER_BUILD_CMD ?? rc.buildCmd ?? null,
|
|
188
276
|
maxSteps: overrides.maxSteps ?? validatedMaxSteps,
|
|
@@ -198,6 +286,7 @@ export function loadConfig(overrides = {}) {
|
|
|
198
286
|
? true
|
|
199
287
|
: process.env.SCODER_LOG_FILE || undefined),
|
|
200
288
|
mcpServers: rc.mcpServers,
|
|
289
|
+
mcpTools: rc.mcpTools,
|
|
201
290
|
guardrails: rc.guardrails,
|
|
202
291
|
stream: overrides.stream ?? (process.env.SCODER_STREAM !== "0" ? true : false),
|
|
203
292
|
reasoning: overrides.reasoning ??
|
|
@@ -218,8 +307,17 @@ export function loadConfig(overrides = {}) {
|
|
|
218
307
|
promptVersion: rc.promptVersion,
|
|
219
308
|
verifyCmds: rc.verifyCmds,
|
|
220
309
|
selfReview: rc.selfReview,
|
|
310
|
+
verifyCache: rc.verifyCache,
|
|
311
|
+
localProfile: rc.localProfile,
|
|
221
312
|
modelPricing: parsePricing(rc.modelPricing),
|
|
222
313
|
};
|
|
314
|
+
// P2: local engine → mặc định rẻ hơn (maxSteps, timeout, output cap, MCP
|
|
315
|
+
// schema budget, self-review). Danh sách thay đổi được log qua reporter để
|
|
316
|
+
// `scoder cost`/JSONL giải thích được vì sao run này khác cloud.
|
|
317
|
+
const localChanges = applyLocalProfile(cfg, { maxSteps: maxStepsExplicit });
|
|
318
|
+
if (localChanges.length > 0) {
|
|
319
|
+
overrides.reporter?.info(`local model profile (plan/performance-issue.md P2): ${localChanges.join(", ")} — ghi đè bằng rc "localProfile": false hoặc đặt từng key trong "limits"`);
|
|
320
|
+
}
|
|
223
321
|
// Secrets hygiene: warn if apiKey is set in the rc file (risk of accidental commit)
|
|
224
322
|
if (rc.apiKey && !process.env.SCODER_NO_APIKEY_WARNING) {
|
|
225
323
|
console.error(`\n⚠ WARNING: API key found in .scoderrc.json — this file may be committed to git.\n` +
|
|
@@ -5,11 +5,45 @@ export const DEFAULT_COMPACTION = {
|
|
|
5
5
|
buffer: 0.15,
|
|
6
6
|
contextWindow: 128_000,
|
|
7
7
|
};
|
|
8
|
+
/**
|
|
9
|
+
* P1-1 (RC-4): tool schema KHÔNG nằm trong messages nhưng chiếm phần lớn
|
|
10
|
+
* prompt thật. Session baseline: 25.799 input token cho một câu "chào", trong
|
|
11
|
+
* đó ~20.300 token là schema (46 tool GitHub MCP + prompt-gate + built-ins).
|
|
12
|
+
* Counting only messages made compaction trigger ~20k token quá muộn.
|
|
13
|
+
*/
|
|
14
|
+
const schemaTokenCache = new WeakMap();
|
|
15
|
+
export function estimateSchemaTokens(tools) {
|
|
16
|
+
if (!tools || tools.length === 0)
|
|
17
|
+
return 0;
|
|
18
|
+
const key = tools;
|
|
19
|
+
const hit = schemaTokenCache.get(key);
|
|
20
|
+
if (hit !== undefined)
|
|
21
|
+
return hit;
|
|
22
|
+
let text = "";
|
|
23
|
+
for (const t of tools) {
|
|
24
|
+
text += t.function.name + "\n" + (t.function.description ?? "") + "\n" + JSON.stringify(t.function.parameters ?? {}) + "\n";
|
|
25
|
+
}
|
|
26
|
+
let n;
|
|
27
|
+
try {
|
|
28
|
+
n = encode(text).length;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
n = Math.ceil(text.length / 4);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
schemaTokenCache.set(key, n);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/* readonly array from a frozen literal — cache bỏ qua */
|
|
38
|
+
}
|
|
39
|
+
return n;
|
|
40
|
+
}
|
|
8
41
|
/**
|
|
9
42
|
* Token-accurate count (I-10): gpt-tokenizer (offline, MIT) thay heuristic
|
|
10
43
|
* ~4 chars/token. Fallback char/4 nếu encode lỗi (never throw).
|
|
44
|
+
* `extraTokens` (P1-1): phần chi phí cố định ngoài messages — tool schemas.
|
|
11
45
|
*/
|
|
12
|
-
export function estimateTokens(messages) {
|
|
46
|
+
export function estimateTokens(messages, extraTokens = 0) {
|
|
13
47
|
let chars = 0;
|
|
14
48
|
const texts = [];
|
|
15
49
|
for (const m of messages) {
|
|
@@ -21,18 +55,18 @@ export function estimateTokens(messages) {
|
|
|
21
55
|
}
|
|
22
56
|
}
|
|
23
57
|
try {
|
|
24
|
-
return encode(texts.join("\n")).length + texts.length;
|
|
58
|
+
return encode(texts.join("\n")).length + texts.length + extraTokens;
|
|
25
59
|
}
|
|
26
60
|
catch {
|
|
27
61
|
for (const t of texts)
|
|
28
62
|
chars += t.length;
|
|
29
|
-
return Math.ceil(chars / 4);
|
|
63
|
+
return Math.ceil(chars / 4) + extraTokens;
|
|
30
64
|
}
|
|
31
65
|
}
|
|
32
66
|
/** True when the history should be compacted now. */
|
|
33
|
-
export function shouldCompact(messages, cfg) {
|
|
67
|
+
export function shouldCompact(messages, cfg, extraTokens = 0) {
|
|
34
68
|
const budget = cfg.contextWindow * cfg.trigger;
|
|
35
|
-
return estimateTokens(messages) >= budget;
|
|
69
|
+
return estimateTokens(messages, extraTokens) >= budget;
|
|
36
70
|
}
|
|
37
71
|
/** Index where the verbatim tail begins (never inside a tool_calls/tool pair). */
|
|
38
72
|
export function findCutIndex(messages, cfg) {
|