s-app-scoder-agent 0.9.7 → 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 +26 -6
- package/dist/agent/mcp-bridge.js +101 -5
- package/dist/agent/run-loop.js +69 -10
- package/dist/agent/tool-executor.js +13 -1
- 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
|
@@ -83,9 +83,9 @@ export function useSubmit(deps) {
|
|
|
83
83
|
setScrollOffset(0);
|
|
84
84
|
// isFirst must be computed BEFORE pushing the user item below.
|
|
85
85
|
const isFirst = stateRef.current.items.every((i) => i.kind !== "user");
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
// runTurn cũng
|
|
86
|
+
// run_start (cả run() lẫn runTurn() đều phát) dedupe với dòng user mà TUI
|
|
87
|
+
// vừa push ở dòng trên, nên câu hỏi thứ 2+ không hiển thị hai lần.
|
|
88
|
+
// runTurn cũng reset plan — cùng semantics với run_start.
|
|
89
89
|
update((s) => ({ ...s, plan: null, items: [...s.items, { kind: "user", text: task }], status: "running", summary: null }));
|
|
90
90
|
const agent = createAgent();
|
|
91
91
|
try {
|
package/dist/tui/state.js
CHANGED
|
@@ -118,9 +118,10 @@ export function applyEvent(state, ev) {
|
|
|
118
118
|
state.llmCallStartedAt = null;
|
|
119
119
|
state.lastLlmTokPerSec = null;
|
|
120
120
|
// The TUI pushes the user's message itself when submitting; avoid a
|
|
121
|
-
// duplicate when run_start carries the same task.
|
|
121
|
+
// duplicate when run_start carries the same task. `quiet` (resume) =
|
|
122
|
+
// transcript đã hiển thị rồi, không append nữa.
|
|
122
123
|
const last = lastItem(state);
|
|
123
|
-
if (!(last && last.kind === "user" && last.text === ev.task)) {
|
|
124
|
+
if (!ev.quiet && !(last && last.kind === "user" && last.text === ev.task)) {
|
|
124
125
|
appendItem(state, { kind: "user", text: ev.task });
|
|
125
126
|
}
|
|
126
127
|
// Plan của task trước không được lọt sang task mới.
|
package/dist/workspace/build.js
CHANGED
|
@@ -31,9 +31,12 @@ export function detectBuildCommand(workdir) {
|
|
|
31
31
|
* Run the build command. This is the agent's stop condition:
|
|
32
32
|
* exit code 0 == task complete.
|
|
33
33
|
* `onOutput` (optional) receives stdout/stderr chunks live while the build runs.
|
|
34
|
+
* `timeoutSecs` (P1-2) tới từ `limits.buildTimeoutSecs` — trước đây hardcode
|
|
35
|
+
* 600s, không điều chỉnh được khi verify trên máy chậm.
|
|
34
36
|
*/
|
|
35
|
-
export async function runBuild(workdir, command, onOutput) {
|
|
36
|
-
const
|
|
37
|
+
export async function runBuild(workdir, command, onOutput, timeoutSecs) {
|
|
38
|
+
const timeout = Number.isFinite(timeoutSecs ?? NaN) && (timeoutSecs ?? 0) > 0 ? Math.floor(timeoutSecs) : 600;
|
|
39
|
+
const res = await toolRunCommand(workdir, { command, timeout_secs: timeout }, undefined, onOutput);
|
|
37
40
|
return {
|
|
38
41
|
success: res.ok,
|
|
39
42
|
command,
|
|
@@ -9,9 +9,14 @@ import { join, relative } from "node:path";
|
|
|
9
9
|
import fuzzysort from "fuzzysort";
|
|
10
10
|
const SKIP = new Set(["node_modules", ".git", "dist", "build", ".next", "__pycache__", ".venv"]);
|
|
11
11
|
let cache = null;
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
/**
|
|
13
|
+
* File index for `workdir` (cached, TTL 5s; invalidateFileIndex on tool writes).
|
|
14
|
+
* `force` (P1-2) bỏ qua cache — dùng cho fingerprint, nơi một file mới được tạo
|
|
15
|
+
* bởi `run_command`/trình sửa ngoài PHẢI đổi hash, nếu không kết quả verify cũ
|
|
16
|
+
* sẽ được tái sử dụng sai.
|
|
17
|
+
*/
|
|
18
|
+
export function fileIndex(workdir, force = false) {
|
|
19
|
+
if (!force && cache && Date.now() - cache.built < 5_000)
|
|
15
20
|
return cache.files;
|
|
16
21
|
const files = [];
|
|
17
22
|
// Fast path: ripgrep --files (gitignore-aware). Bare-name globs match at any
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P1-2 (RC-5): fingerprint trạng thái workspace — dùng để KHÔNG chạy lại
|
|
3
|
+
* build/test/typecheck/lint khi không có gì thay đổi.
|
|
4
|
+
*
|
|
5
|
+
* Chi phí: 1 lần `stat` mỗi file (danh sách file lấy từ file-index —
|
|
6
|
+
* gitignore-aware, TTL 5s, đã invalidate mỗi lần tool write/edit). Không đọc
|
|
7
|
+
* nội dung file: size + mtime đổi khi agent sửa code; độ chính xác đủ cho một
|
|
8
|
+
* CACHE (kết quả cũ chỉ được tái sử dụng khi lần trước PASS), và có tắt bằng
|
|
9
|
+
* rc `"verifyCache": false`.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { statSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { fileIndex } from "./file-index.js";
|
|
15
|
+
/** Số file tối đa đưa vào fingerprint (file-index cap thực tế ~vài nghìn). */
|
|
16
|
+
const MAX_FINGERPRINT_FILES = 5_000;
|
|
17
|
+
/**
|
|
18
|
+
* Fingerprint ổn định cho một trạng thái workspace. Trả về chuỗi dài ~40 ký
|
|
19
|
+
* tự; khác 1 byte khi 1 file thêm/bỏ/đổi size hoặc mtime.
|
|
20
|
+
*
|
|
21
|
+
* `fileIndex(workdir, force)` — BỎ QUA cache TTL 5s (và cache one-slot theo
|
|
22
|
+
* workdir khác): file mới tạo bởi `run_command`/trình sửa ngoài PHẢI đổi hash,
|
|
23
|
+
* nếu không kết quả verify cũ sẽ được tái sử dụng sai. Chi phí một lần
|
|
24
|
+
* `rg --files` + stat — nhỏ hơn rất nhiều so với một lệnh build chạy thừa.
|
|
25
|
+
*/
|
|
26
|
+
export function workspaceFingerprint(workdir, cap = MAX_FINGERPRINT_FILES) {
|
|
27
|
+
const files = fileIndex(workdir, true);
|
|
28
|
+
const hash = createHash("sha1");
|
|
29
|
+
const truncated = files.length > cap;
|
|
30
|
+
const list = truncated ? files.slice(0, cap) : files;
|
|
31
|
+
hash.update(`files=${files.length}${truncated ? " (capped)" : ""}\n`);
|
|
32
|
+
for (const rel of list) {
|
|
33
|
+
let st;
|
|
34
|
+
try {
|
|
35
|
+
st = statSync(join(workdir, rel));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
hash.update(`${rel}:gone\n`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
hash.update(`${rel}:${st.size}:${Math.round(st.mtimeMs)}\n`);
|
|
42
|
+
}
|
|
43
|
+
return hash.digest("hex");
|
|
44
|
+
}
|
package/package.json
CHANGED