s-app-scoder-agent 0.6.0 → 0.8.0

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.
Files changed (49) hide show
  1. package/.scoderrc.example.json +97 -0
  2. package/LICENSE +21 -21
  3. package/README.md +600 -468
  4. package/dist/agent.js +841 -197
  5. package/dist/agents.js +70 -0
  6. package/dist/compaction.js +114 -0
  7. package/dist/config.js +178 -12
  8. package/dist/context.js +119 -14
  9. package/dist/cost.js +56 -6
  10. package/dist/guardrails.js +423 -0
  11. package/dist/index.js +129 -76
  12. package/dist/json-schema.js +58 -0
  13. package/dist/llm.js +123 -9
  14. package/dist/mcp.js +397 -70
  15. package/dist/model-catalog.js +175 -0
  16. package/dist/permissions.js +328 -35
  17. package/dist/prompt-builder.js +47 -0
  18. package/dist/prompt.js +103 -0
  19. package/dist/provider-anthropic.js +113 -35
  20. package/dist/provider-registry.js +116 -0
  21. package/dist/retry.js +3 -2
  22. package/dist/sandbox.js +96 -0
  23. package/dist/server.js +32 -2
  24. package/dist/session.js +282 -11
  25. package/dist/shell-scanner.js +112 -0
  26. package/dist/skills.js +122 -0
  27. package/dist/snapshot.js +170 -0
  28. package/dist/steering.js +35 -0
  29. package/dist/tools.js +513 -19
  30. package/dist/tui/app.js +505 -198
  31. package/dist/tui/at-complete.js +93 -0
  32. package/dist/tui/chat-views.js +146 -0
  33. package/dist/tui/chrome.js +96 -0
  34. package/dist/tui/command-registry.js +189 -0
  35. package/dist/tui/commands.js +200 -2
  36. package/dist/tui/image.js +149 -0
  37. package/dist/tui/input.js +214 -0
  38. package/dist/tui/markdown.js +229 -0
  39. package/dist/tui/mouse.js +20 -0
  40. package/dist/tui/permission-modal.js +34 -0
  41. package/dist/tui/review.js +64 -0
  42. package/dist/tui/session-picker.js +68 -0
  43. package/dist/tui/sidebar.js +99 -0
  44. package/dist/tui/state.js +192 -6
  45. package/dist/tui/theme.js +90 -0
  46. package/dist/tui/vim.js +60 -0
  47. package/dist/watch.js +52 -0
  48. package/package.json +16 -8
  49. package/dist/tui-direct.mjs +0 -4
@@ -0,0 +1,423 @@
1
+ import { claimsCompletion } from "./context.js";
2
+ /** Marker do system prompt quy ước: lượt chat thuần, không cần build. */
3
+ export const CHAT_DONE_MARKER = "<<CHAT_DONE>>";
4
+ export const DEFAULT_GUARDRAIL_OPTIONS = {
5
+ textOnlyStreakLimit: 2,
6
+ doomLoopRepeats: 2,
7
+ wrapUpStepsBeforeEnd: 2,
8
+ };
9
+ /**
10
+ * Wrap-up prefill: gần hết step budget → ép model dừng mở việc mới, chạy build
11
+ * và tổng kết. Nếu không, model thường đốt nốt các bước cuối để khám phá và
12
+ * run thất bại mà không có summary.
13
+ */
14
+ export function wrapUpRule(opts) {
15
+ return {
16
+ name: "wrap-up",
17
+ description: `inject reminder ${opts.wrapUpStepsBeforeEnd} steps before budget end`,
18
+ evaluate(ctx) {
19
+ const t = ctx;
20
+ if (t.wrapUpInjected)
21
+ return null;
22
+ if (ctx.maxSteps - ctx.step !== opts.wrapUpStepsBeforeEnd)
23
+ return null;
24
+ return {
25
+ action: {
26
+ kind: "inject",
27
+ reminder: "<system-reminder>Step budget nearly exhausted (2 steps left). Stop starting new work: finish the current change, run the build command, and reply with a final summary. If the build cannot pass in the remaining steps, report exactly what remains in your final message.</system-reminder>",
28
+ },
29
+ reason: `${opts.wrapUpStepsBeforeEnd} steps left in budget`,
30
+ };
31
+ },
32
+ };
33
+ }
34
+ /** Lượt chat thuần (marker CHAT_DONE) → kết thúc, không cần build. */
35
+ const chatDoneRule = {
36
+ name: "chat-done",
37
+ description: "assistant emitted the chat-done marker — end the conversational turn",
38
+ evaluate(ctx) {
39
+ if (!ctx.chatDone)
40
+ return null;
41
+ // Phòng vệ: làm sạch marker dù loop đã làm trước — summary không bao giờ
42
+ // lộ marker nội bộ ra UI.
43
+ const clean = ctx.text.split(CHAT_DONE_MARKER)[0].trim();
44
+ return {
45
+ action: {
46
+ kind: "finish",
47
+ success: true,
48
+ summary: clean || "Conversation turn complete.",
49
+ chatOnly: true,
50
+ },
51
+ reason: "chat-done marker present",
52
+ };
53
+ },
54
+ };
55
+ /**
56
+ * Completion claim: model tuyên bố xong việc → không tin lời nói, ép build xác
57
+ * minh. Trường hợp không sửa file nào (chỉ trả lời câu hỏi) thì bỏ qua build.
58
+ */
59
+ const completionClaimRule = {
60
+ name: "completion-claim",
61
+ description: "assistant claims completion — force a build verification",
62
+ evaluate(ctx) {
63
+ if (ctx.hasToolCalls)
64
+ return null;
65
+ if (!claimsCompletion(ctx.text))
66
+ return null;
67
+ if (ctx.step >= ctx.maxSteps)
68
+ return null;
69
+ if (!ctx.filesModified) {
70
+ return {
71
+ action: { kind: "finish", success: true, summary: ctx.text },
72
+ reason: "claims completion but no files were modified — skip build",
73
+ };
74
+ }
75
+ return {
76
+ action: {
77
+ kind: "verify-build",
78
+ successSummary: ctx.text,
79
+ // Loop sẽ ghép thêm "\nOutput:\n<build output>\nFix and continue."
80
+ failReminder: "Build still FAILED after your claimed completion.",
81
+ },
82
+ reason: "claims completion with modified files — verifying build",
83
+ };
84
+ },
85
+ };
86
+ /**
87
+ * Text-only streak breaker: model chỉ nói không làm (không gọi tool, không sửa
88
+ * file) đủ N bước liên tiếp → dừng run thay vì đốt step budget bằng vòng lặp
89
+ * "nudge" vô sản (failure mode "chào" nổi tiếng).
90
+ */
91
+ export function textOnlyStreakRule(opts) {
92
+ return {
93
+ name: "text-only-streak",
94
+ description: `end the run after ${opts.textOnlyStreakLimit} consecutive tool-less steps with no file changes`,
95
+ evaluate(ctx) {
96
+ const t = ctx;
97
+ if (ctx.hasToolCalls)
98
+ return null;
99
+ if (t.textOnlyStreak < opts.textOnlyStreakLimit)
100
+ return null;
101
+ if (ctx.filesModified)
102
+ return null;
103
+ return {
104
+ action: { kind: "finish", success: true, summary: ctx.text || "(agent replied without acting)" },
105
+ reason: `${t.textOnlyStreak} consecutive text-only steps with no file changes`,
106
+ };
107
+ },
108
+ };
109
+ }
110
+ /**
111
+ * Error-loop breaker (H-12): cùng tool + cùng error lặp lại 3 lần ⇒ inject
112
+ * "change approach" — doom-loop mở rộng bằng signature bao gồm error hash.
113
+ */
114
+ export function errorLoopRule(_opts) {
115
+ return {
116
+ name: "error-loop",
117
+ description: "same tool + same error 3x in a row — force a strategy change",
118
+ evaluate(ctx) {
119
+ const t = ctx;
120
+ if (t.toolErrorRepeat < 2)
121
+ return null; // 3rd occurrence = repeat 2
122
+ return {
123
+ action: {
124
+ kind: "inject",
125
+ reminder: "<system-reminder>The same tool call has failed with the same error 3 times. Stop retrying: the approach itself is wrong. Diagnose the root cause, try a fundamentally different method, or report the blocker in your final message.</system-reminder>",
126
+ },
127
+ reason: `same tool+error repeated ${t.toolErrorRepeat + 1}x`,
128
+ };
129
+ },
130
+ };
131
+ }
132
+ /**
133
+ * Token-no-progress (H-12): outputTokens < 20 trong 3 step liên tiếp ⇒ finish
134
+ * (model degenerate — tránh treo run không tiến triển).
135
+ */
136
+ export function tokenNoProgressRule(opts) {
137
+ return {
138
+ name: "token-no-progress",
139
+ description: "output tokens < 20 for 3 consecutive steps — end a degenerate run",
140
+ evaluate(ctx) {
141
+ const t = ctx;
142
+ void opts;
143
+ if (ctx.outputTokensThisStep === undefined)
144
+ return null;
145
+ if (ctx.outputTokensThisStep >= 20)
146
+ return null;
147
+ if (t.lowTokenStreak < 3)
148
+ return null;
149
+ return {
150
+ action: {
151
+ kind: "finish",
152
+ success: false,
153
+ summary: "Run stopped: 3 consecutive near-empty LLM responses (no progress).",
154
+ },
155
+ reason: `output tokens ${ctx.outputTokensThisStep} < 20 for ${t.lowTokenStreak} steps`,
156
+ };
157
+ },
158
+ };
159
+ }
160
+ /** Nudge: trả lời text thường (không claim, không hết streak) → nhắc tiếp tục làm việc. */
161
+ const continueNudgeRule = { name: "continue-nudge",
162
+ description: "tool-less step that neither claims completion nor ends the run — nudge the agent to act",
163
+ evaluate(ctx) {
164
+ if (ctx.hasToolCalls)
165
+ return null;
166
+ if (ctx.chatDone)
167
+ return null;
168
+ if (claimsCompletion(ctx.text))
169
+ return null; // completion-claim rule phụ trách
170
+ // No file changed yet → never demand a build for what may be a pure Q&A or
171
+ // an explanation-only answer. Give the model the explicit conversational
172
+ // exit instead; text-only-streak still ends the run if it keeps stalling.
173
+ if (!ctx.filesModified) {
174
+ return {
175
+ action: {
176
+ kind: "inject",
177
+ reminder: `Continue working on the task. Use tools only if code changes are needed. ` +
178
+ `If the user only asked a question and no code change is needed, do NOT run the build — ` +
179
+ `end your reply with ${CHAT_DONE_MARKER} on its own last line instead.`,
180
+ },
181
+ reason: "plain text answer without tool calls (no files modified)",
182
+ };
183
+ }
184
+ return {
185
+ action: {
186
+ kind: "inject",
187
+ reminder: "Continue working on the task. Use tools. Build must pass before you finish.",
188
+ },
189
+ reason: "plain text answer without tool calls",
190
+ };
191
+ },
192
+ };
193
+ /**
194
+ * Doom-loop breaker: chuỗi tool call giống hệt nhau lặp lại quá ngưỡng →
195
+ * can thiệp bằng lời nhắc đổi chiến lược (model hiếm khi tự thoát khỏi loop;
196
+ * run không người trông sẽ đốt sạch step budget).
197
+ */
198
+ export function doomLoopRule(opts) {
199
+ return {
200
+ name: "doom-loop",
201
+ description: `intervene after ${opts.doomLoopRepeats + 1} identical consecutive tool-call batches`,
202
+ evaluate(ctx) {
203
+ const t = ctx;
204
+ if (!ctx.hasToolCalls)
205
+ return null;
206
+ if (t.sigRepeat < opts.doomLoopRepeats)
207
+ return null;
208
+ return {
209
+ action: {
210
+ kind: "inject",
211
+ reminder: "<system-reminder>You have made the identical tool call 3 times in a row. This is a loop: read the last output carefully, change your approach, or report the blocker in your final message. Do NOT repeat the same call again.</system-reminder>",
212
+ },
213
+ reason: `identical tool-call batch repeated ${t.sigRepeat + 1}x`,
214
+ };
215
+ },
216
+ };
217
+ }
218
+ /** Context management: luôn cắt gọn lịch sử sau mỗi bước. */
219
+ const historyTrimRule = {
220
+ name: "history-trim",
221
+ description: "trim message history after every step (token budget)",
222
+ evaluate() {
223
+ return { action: { kind: "trim" }, reason: "routine context management" };
224
+ },
225
+ };
226
+ /**
227
+ * G6: token budget per run — tổng (input+output) vượt cap → kết thúc run
228
+ * (pause thay vì đốt tiền). Đọc usage từ ctx (loop truyền usageTotals).
229
+ */
230
+ export function tokenBudgetRule(capTokens) {
231
+ return {
232
+ name: "token-budget",
233
+ description: `end the run when total session tokens exceed ${capTokens}`,
234
+ evaluate(ctx) {
235
+ const total = (ctx.totalTokensIn ?? 0) + (ctx.totalTokensOut ?? 0);
236
+ if (total < capTokens)
237
+ return null;
238
+ return {
239
+ action: {
240
+ kind: "finish",
241
+ success: false,
242
+ summary: `Token budget exhausted: ${total} tokens used (cap ${capTokens}). ` +
243
+ "Report current progress in the summary.",
244
+ },
245
+ reason: `token budget ${total}/${capTokens}`,
246
+ };
247
+ },
248
+ };
249
+ }
250
+ /**
251
+ * Build stop condition: bước này có tool call chạy ĐÚNG build command →
252
+ * thành công là điều kiện kết thúc run (dùng lại kết quả tool, không build
253
+ * lần 2); thất bại thì tự xác minh lại rồi nudge fix.
254
+ */
255
+ const buildStopRule = {
256
+ name: "build-stop",
257
+ description: "a tool call ran the exact build command — success ends the run, failure re-verifies",
258
+ evaluate(ctx) {
259
+ if (!ctx.hasToolCalls || !ctx.buildCallPresent)
260
+ return null;
261
+ const summary = ctx.text || "Build passed.";
262
+ if (ctx.buildPassedThisStep) {
263
+ return {
264
+ action: { kind: "finish", success: true, summary },
265
+ reason: "build command succeeded this step",
266
+ };
267
+ }
268
+ return {
269
+ action: {
270
+ kind: "verify-build",
271
+ successSummary: summary,
272
+ // Loop sẽ ghép thêm output build thật vào sau.
273
+ failReminder: "Build FAILED (stop condition not met).",
274
+ },
275
+ reason: "build command ran and failed — re-verifying independently",
276
+ };
277
+ },
278
+ };
279
+ /** Bộ rule mặc định, theo thứ tự ưu tiên (finish đầu tiên thắng). */
280
+ export function defaultGuardrailRules(opts = DEFAULT_GUARDRAIL_OPTIONS) {
281
+ return [
282
+ wrapUpRule(opts),
283
+ chatDoneRule,
284
+ // G6: budget rule đứng sớm — vượt cap là finish ngay, trước mọi rule khác
285
+ // (trừ wrap-up, vốn vô hại khi budget đã cạn).
286
+ ...(opts.tokenBudget ? [tokenBudgetRule(opts.tokenBudget)] : []),
287
+ completionClaimRule,
288
+ textOnlyStreakRule(opts),
289
+ continueNudgeRule,
290
+ doomLoopRule(opts),
291
+ errorLoopRule(opts),
292
+ tokenNoProgressRule(opts),
293
+ buildStopRule,
294
+ historyTrimRule,
295
+ ];
296
+ }
297
+ // ── Engine ──────────────────────────────────────────────────────────────────
298
+ /**
299
+ * Engine chạy toàn bộ rule mỗi bước và thu thập các decision đã kích hoạt.
300
+ * runLoop áp dụng: finish đầu tiên thắng; inject/trim gom lại áp một lần.
301
+ * G1: toàn bộ tracker state (textOnlyStreak, sigRepeat, wrapUpInjected,
302
+ * toolError*, lowTokenStreak) sống ở đây — runLoop chỉ emit sự kiện thô.
303
+ */
304
+ export class GuardrailEngine {
305
+ onDecision;
306
+ rules;
307
+ /** G1: per-run tracker — reset() khi bắt đầu run mới. */
308
+ tracker = {
309
+ textOnlyStreak: 0,
310
+ lastCallSig: "",
311
+ sigRepeat: 0,
312
+ wrapUpInjected: false,
313
+ toolErrorSig: "",
314
+ toolErrorRepeat: 0,
315
+ lowTokenStreak: 0,
316
+ };
317
+ constructor(rules, onDecision) {
318
+ this.onDecision = onDecision;
319
+ this.rules = rules ?? defaultGuardrailRules();
320
+ }
321
+ /** Reset tracker — gọi ở đầu mỗi run (per-run state, không rò rỉ giữa các run). */
322
+ reset() {
323
+ this.tracker = {
324
+ textOnlyStreak: 0,
325
+ lastCallSig: "",
326
+ sigRepeat: 0,
327
+ wrapUpInjected: false,
328
+ toolErrorSig: "",
329
+ toolErrorRepeat: 0,
330
+ lowTokenStreak: 0,
331
+ };
332
+ }
333
+ /** Steer batch được deliver — reset text-only allowance một lần cho cả batch. */
334
+ notifySteered() {
335
+ this.tracker.textOnlyStreak = 0;
336
+ }
337
+ /** Wrap-up reminder vừa được apply — chỉ inject một lần mỗi run. */
338
+ markWrapUpInjected() {
339
+ this.tracker.wrapUpInjected = true;
340
+ }
341
+ /** Đăng ký thêm rule (chạy sau các rule mặc định nếu thêm sau khi tạo). */
342
+ register(rule) {
343
+ this.rules.push(rule);
344
+ }
345
+ listRules() {
346
+ return this.rules.map((r) => ({ name: r.name, description: r.description }));
347
+ }
348
+ /**
349
+ * Chạy tất cả rule; trả về các decision ĐÃ kích hoạt (theo thứ tự ưu tiên).
350
+ * Tracker được cập nhật từ ctx thô TRƯỚC khi rule chạy (rule thấy giá trị
351
+ * của bước này — đúng semantics cũ của runLoop).
352
+ */
353
+ evaluate(ctx) {
354
+ this.updateTrackers(ctx);
355
+ // Rules nhìn thấy tracker qua ctx mở rộng (internal — không thuộc API public).
356
+ const full = { ...ctx, ...this.tracker };
357
+ // G6: rule có thể async — chạy tuần tự (thứ tự ưu tiên giữ nguyên).
358
+ return (async () => {
359
+ const out = [];
360
+ for (const rule of this.rules) {
361
+ let r;
362
+ try {
363
+ r = await rule.evaluate(full);
364
+ }
365
+ catch (err) {
366
+ // Một rule lỗi không được phép làm gãy vòng lặp agent.
367
+ r = { action: { kind: "none" }, reason: `rule threw: ${err.message}` };
368
+ }
369
+ if (!r)
370
+ continue;
371
+ const d = { rule: rule.name, action: r.action, reason: r.reason };
372
+ out.push(d);
373
+ try {
374
+ this.onDecision?.(d);
375
+ }
376
+ catch {
377
+ /* logging không được phép giết loop */
378
+ }
379
+ }
380
+ return out;
381
+ })();
382
+ }
383
+ /**
384
+ * G1: cập nhật tracker từ sự kiện thô của bước này (thay cho state cục bộ
385
+ * của runLoop cũ). Semantics giữ nguyên 100% so với code inline trước đây.
386
+ */
387
+ updateTrackers(ctx) {
388
+ const t = this.tracker;
389
+ if (ctx.hasToolCalls) {
390
+ t.textOnlyStreak = 0;
391
+ // Doom-loop: chữ ký chuỗi tool call so với bước trước.
392
+ const callSig = (ctx.message?.tool_calls ?? [])
393
+ .map((tc) => `${tc.name}:${tc.arguments}`)
394
+ .sort()
395
+ .join("|");
396
+ t.sigRepeat = callSig === t.lastCallSig ? t.sigRepeat + 1 : 0;
397
+ t.lastCallSig = callSig;
398
+ }
399
+ else {
400
+ t.textOnlyStreak++;
401
+ }
402
+ // Error-loop (H-12): signature tool+error từ kết quả bước này.
403
+ const errSig = (ctx.message?.tool_calls ?? [])
404
+ .map((tc) => {
405
+ const r = ctx.toolResults?.get(tc.id);
406
+ return r && !r.ok ? `${tc.name}:${r.output.slice(0, 80)}` : null;
407
+ })
408
+ .filter(Boolean)
409
+ .sort()
410
+ .join("|");
411
+ t.toolErrorRepeat = errSig && errSig === t.toolErrorSig ? t.toolErrorRepeat + 1 : 0;
412
+ if (errSig)
413
+ t.toolErrorSig = errSig;
414
+ // Token-no-progress (H-12).
415
+ const outTok = ctx.outputTokensThisStep;
416
+ t.lowTokenStreak = outTok !== undefined && outTok < 20 ? t.lowTokenStreak + 1 : 0;
417
+ }
418
+ }
419
+ /** Engine với bộ rule mặc định + ngưỡng tùy chọn. */
420
+ export function createGuardrailEngine(opts = {}, onDecision) {
421
+ const merged = { ...DEFAULT_GUARDRAIL_OPTIONS, ...opts };
422
+ return new GuardrailEngine(defaultGuardrailRules(merged), onDecision);
423
+ }