@datename/fastcode-termux 0.1.6 → 0.1.8-beta.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 (303) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +67 -16
  3. package/dist/agent/approval-json.d.ts +43 -0
  4. package/dist/agent/approval-json.js +69 -0
  5. package/dist/agent/approval-native.d.ts +3 -0
  6. package/dist/agent/approval-native.js +165 -0
  7. package/dist/agent/approval-store.d.ts +16 -0
  8. package/dist/agent/approval-store.js +21 -0
  9. package/dist/agent/approval.d.ts +83 -0
  10. package/dist/agent/approval.js +42 -0
  11. package/dist/agent/compaction.d.ts +41 -0
  12. package/dist/agent/compaction.js +270 -0
  13. package/dist/agent/context.d.ts +29 -0
  14. package/dist/agent/context.js +62 -0
  15. package/dist/agent/guards.d.ts +24 -16
  16. package/dist/agent/guards.js +1167 -18
  17. package/dist/agent/ignore.d.ts +11 -0
  18. package/dist/agent/ignore.js +129 -0
  19. package/dist/agent/policy/defaults.d.ts +16 -0
  20. package/dist/agent/policy/defaults.js +17 -0
  21. package/dist/agent/policy/egress.d.ts +32 -0
  22. package/dist/agent/policy/egress.js +280 -0
  23. package/dist/agent/process-tree.d.ts +22 -0
  24. package/dist/agent/process-tree.js +102 -0
  25. package/dist/agent/protocol-mode.d.ts +6 -0
  26. package/dist/agent/protocol-mode.js +51 -0
  27. package/dist/agent/protocol.d.ts +20 -1
  28. package/dist/agent/protocol.js +169 -39
  29. package/dist/agent/registry.d.ts +19 -0
  30. package/dist/agent/registry.js +247 -0
  31. package/dist/agent/runner.d.ts +127 -0
  32. package/dist/agent/runner.js +391 -0
  33. package/dist/agent/shell-ast.d.ts +11 -0
  34. package/dist/agent/shell-ast.js +204 -0
  35. package/dist/agent/shell-session.d.ts +51 -0
  36. package/dist/agent/shell-session.js +611 -0
  37. package/dist/agent/stream.d.ts +29 -0
  38. package/dist/agent/stream.js +103 -0
  39. package/dist/agent/tool-schemas.d.ts +2 -0
  40. package/dist/agent/tool-schemas.js +82 -0
  41. package/dist/agent/tools/append_file.d.ts +2 -0
  42. package/dist/agent/tools/append_file.js +28 -0
  43. package/dist/agent/tools/apply_patch.d.ts +10 -0
  44. package/dist/agent/tools/apply_patch.js +92 -0
  45. package/dist/agent/tools/copy_file.d.ts +2 -0
  46. package/dist/agent/tools/copy_file.js +53 -0
  47. package/dist/agent/tools/delete_file.d.ts +2 -0
  48. package/dist/agent/tools/delete_file.js +43 -0
  49. package/dist/agent/tools/grep.d.ts +2 -0
  50. package/dist/agent/tools/grep.js +54 -0
  51. package/dist/agent/tools/index.d.ts +2 -0
  52. package/dist/agent/tools/index.js +32 -0
  53. package/dist/agent/tools/list_files.d.ts +2 -0
  54. package/dist/agent/tools/list_files.js +17 -0
  55. package/dist/agent/tools/make_dir.d.ts +2 -0
  56. package/dist/agent/tools/make_dir.js +26 -0
  57. package/dist/agent/tools/move_path.d.ts +9 -0
  58. package/dist/agent/tools/move_path.js +73 -0
  59. package/dist/agent/tools/paths.d.ts +7 -0
  60. package/dist/agent/tools/paths.js +84 -0
  61. package/dist/agent/tools/pwd.d.ts +2 -0
  62. package/dist/agent/tools/pwd.js +10 -0
  63. package/dist/agent/tools/read_file.d.ts +2 -0
  64. package/dist/agent/tools/read_file.js +81 -0
  65. package/dist/agent/tools/read_many.d.ts +2 -0
  66. package/dist/agent/tools/read_many.js +43 -0
  67. package/dist/agent/tools/readers.d.ts +58 -0
  68. package/dist/agent/tools/readers.js +426 -0
  69. package/dist/agent/tools/replace_in_file.d.ts +2 -0
  70. package/dist/agent/tools/replace_in_file.js +63 -0
  71. package/dist/agent/tools/shell.d.ts +2 -0
  72. package/dist/agent/tools/shell.js +48 -0
  73. package/dist/agent/tools/stat_path.d.ts +2 -0
  74. package/dist/agent/tools/stat_path.js +31 -0
  75. package/dist/agent/tools/types.d.ts +65 -0
  76. package/dist/agent/tools/types.js +1 -0
  77. package/dist/agent/tools/validate.d.ts +19 -0
  78. package/dist/agent/tools/validate.js +92 -0
  79. package/dist/agent/tools/write_file.d.ts +2 -0
  80. package/dist/agent/tools/write_file.js +28 -0
  81. package/dist/agent/tools.d.ts +3 -1
  82. package/dist/agent/tools.js +2 -377
  83. package/dist/app.d.ts +2 -1
  84. package/dist/app.js +846 -173
  85. package/dist/cli-main.js +363 -0
  86. package/dist/cli.js +444 -333
  87. package/dist/commands/about.js +1 -1
  88. package/dist/commands/attach.d.ts +9 -0
  89. package/dist/commands/attach.js +15 -0
  90. package/dist/commands/bg.d.ts +7 -0
  91. package/dist/commands/bg.js +9 -0
  92. package/dist/commands/cancel.d.ts +10 -0
  93. package/dist/commands/cancel.js +10 -0
  94. package/dist/commands/clear.d.ts +1 -0
  95. package/dist/commands/clear.js +1 -1
  96. package/dist/commands/compact.d.ts +22 -0
  97. package/dist/commands/compact.js +34 -0
  98. package/dist/commands/context.d.ts +7 -0
  99. package/dist/commands/context.js +25 -0
  100. package/dist/commands/edit.d.ts +10 -0
  101. package/dist/commands/edit.js +36 -0
  102. package/dist/commands/effort.d.ts +25 -0
  103. package/dist/commands/effort.js +48 -0
  104. package/dist/commands/expand.d.ts +10 -0
  105. package/dist/commands/expand.js +8 -0
  106. package/dist/commands/index.js +24 -0
  107. package/dist/commands/jobs.d.ts +7 -0
  108. package/dist/commands/jobs.js +17 -0
  109. package/dist/commands/kill.d.ts +7 -0
  110. package/dist/commands/kill.js +13 -0
  111. package/dist/commands/login.d.ts +6 -4
  112. package/dist/commands/login.js +24 -22
  113. package/dist/commands/logout.js +8 -16
  114. package/dist/commands/model.d.ts +1 -0
  115. package/dist/commands/model.js +10 -22
  116. package/dist/commands/provider.d.ts +1 -2
  117. package/dist/commands/provider.js +4 -11
  118. package/dist/commands/skills.js +34 -4
  119. package/dist/commands/steer.d.ts +11 -0
  120. package/dist/commands/steer.js +13 -0
  121. package/dist/commands/threads.d.ts +9 -0
  122. package/dist/commands/threads.js +26 -0
  123. package/dist/commands/whoami.js +6 -5
  124. package/dist/config/store.d.ts +53 -12
  125. package/dist/config/store.js +163 -73
  126. package/dist/config/trust.d.ts +30 -8
  127. package/dist/config/trust.js +139 -14
  128. package/dist/headless.d.ts +7 -0
  129. package/dist/headless.js +213 -0
  130. package/dist/index.js +19 -3
  131. package/dist/lib/debug.d.ts +2 -0
  132. package/dist/lib/debug.js +22 -0
  133. package/dist/lib/diagnostics.d.ts +48 -4
  134. package/dist/lib/diagnostics.js +90 -8
  135. package/dist/lib/highlight-subset.d.ts +21 -0
  136. package/dist/lib/highlight-subset.js +88 -0
  137. package/dist/lib/http.d.ts +11 -1
  138. package/dist/lib/http.js +62 -4
  139. package/dist/lib/lifecycle.d.ts +5 -0
  140. package/dist/lib/lifecycle.js +62 -0
  141. package/dist/lib/markdown.js +22 -4
  142. package/dist/lib/model-meta.d.ts +19 -0
  143. package/dist/lib/model-meta.js +113 -0
  144. package/dist/lib/native-tools.d.ts +12 -0
  145. package/dist/lib/native-tools.js +21 -0
  146. package/dist/lib/net.d.ts +33 -0
  147. package/dist/lib/net.js +168 -0
  148. package/dist/patch/apply.d.ts +15 -0
  149. package/dist/patch/apply.js +172 -0
  150. package/dist/patch/parse.d.ts +29 -0
  151. package/dist/patch/parse.js +112 -0
  152. package/dist/patch/plan.d.ts +49 -0
  153. package/dist/patch/plan.js +273 -0
  154. package/dist/providers/anthropic.d.ts +1 -0
  155. package/dist/providers/anthropic.js +12 -0
  156. package/dist/providers/custom.d.ts +9 -0
  157. package/dist/providers/deepseek.d.ts +10 -0
  158. package/dist/providers/deepseek.js +3 -2
  159. package/dist/providers/google.d.ts +9 -0
  160. package/dist/providers/groq.d.ts +10 -0
  161. package/dist/providers/groq.js +2 -1
  162. package/dist/providers/index.d.ts +10 -201
  163. package/dist/providers/index.js +30 -0
  164. package/dist/providers/mistral.d.ts +10 -0
  165. package/dist/providers/mistral.js +2 -1
  166. package/dist/providers/moonshot.d.ts +10 -0
  167. package/dist/providers/moonshot.js +2 -1
  168. package/dist/providers/ollama.d.ts +9 -0
  169. package/dist/providers/openai.d.ts +10 -0
  170. package/dist/providers/openai.js +2 -1
  171. package/dist/providers/openaiCompatible.d.ts +33 -2
  172. package/dist/providers/openaiCompatible.js +78 -16
  173. package/dist/providers/openrouter.d.ts +10 -0
  174. package/dist/providers/openrouter.js +2 -1
  175. package/dist/providers/qwen.d.ts +10 -0
  176. package/dist/providers/qwen.js +2 -1
  177. package/dist/providers/spec.d.ts +12 -0
  178. package/dist/providers/spec.js +29 -0
  179. package/dist/providers/xai.d.ts +10 -0
  180. package/dist/providers/xai.js +2 -1
  181. package/dist/providers/zhipu.d.ts +10 -0
  182. package/dist/providers/zhipu.js +2 -1
  183. package/dist/session/log.d.ts +49 -14
  184. package/dist/session/log.js +236 -11
  185. package/dist/session/projection.d.ts +76 -0
  186. package/dist/session/projection.js +526 -0
  187. package/dist/skills/manager.d.ts +15 -0
  188. package/dist/skills/manager.js +135 -18
  189. package/dist/types.d.ts +75 -1
  190. package/dist/ui/Message.d.ts +1 -7
  191. package/dist/ui/Message.js +13 -2
  192. package/dist/ui/Picker.d.ts +8 -0
  193. package/dist/ui/Picker.js +24 -0
  194. package/dist/ui/Prompt.d.ts +1 -2
  195. package/dist/ui/Prompt.js +2 -2
  196. package/dist/ui/Spinner.js +11 -5
  197. package/dist/ui/StatusBar.d.ts +2 -1
  198. package/dist/ui/StatusBar.js +6 -5
  199. package/package.json +3 -2
  200. package/src/agent/approval-json.ts +103 -0
  201. package/src/agent/approval-native.ts +168 -0
  202. package/src/agent/approval-store.ts +38 -0
  203. package/src/agent/approval.ts +94 -0
  204. package/src/agent/compaction.ts +321 -0
  205. package/src/agent/context.ts +97 -0
  206. package/src/agent/guards.ts +1239 -19
  207. package/src/agent/ignore.ts +147 -0
  208. package/src/agent/policy/defaults.ts +34 -0
  209. package/src/agent/policy/egress.ts +335 -0
  210. package/src/agent/process-tree.ts +114 -0
  211. package/src/agent/protocol-mode.ts +59 -0
  212. package/src/agent/protocol.ts +191 -40
  213. package/src/agent/registry.ts +270 -0
  214. package/src/agent/runner.ts +486 -0
  215. package/src/agent/shell-ast.ts +227 -0
  216. package/src/agent/shell-session.ts +673 -0
  217. package/src/agent/stream.ts +143 -0
  218. package/src/agent/tool-schemas.ts +86 -0
  219. package/src/agent/tools/append_file.ts +30 -0
  220. package/src/agent/tools/apply_patch.ts +95 -0
  221. package/src/agent/tools/copy_file.ts +56 -0
  222. package/src/agent/tools/delete_file.ts +45 -0
  223. package/src/agent/tools/grep.ts +57 -0
  224. package/src/agent/tools/index.ts +34 -0
  225. package/src/agent/tools/list_files.ts +19 -0
  226. package/src/agent/tools/make_dir.ts +28 -0
  227. package/src/agent/tools/move_path.ts +83 -0
  228. package/src/agent/tools/paths.ts +92 -0
  229. package/src/agent/tools/pwd.ts +12 -0
  230. package/src/agent/tools/read_file.ts +84 -0
  231. package/src/agent/tools/read_many.ts +45 -0
  232. package/src/agent/tools/readers.ts +491 -0
  233. package/src/agent/tools/replace_in_file.ts +66 -0
  234. package/src/agent/tools/shell.ts +53 -0
  235. package/src/agent/tools/stat_path.ts +33 -0
  236. package/src/agent/tools/types.ts +84 -0
  237. package/src/agent/tools/validate.ts +104 -0
  238. package/src/agent/tools/write_file.ts +30 -0
  239. package/src/agent/tools.ts +3 -377
  240. package/src/app.ts +853 -170
  241. package/src/{cli.ts → cli-main.ts} +64 -9
  242. package/src/commands/about.ts +1 -1
  243. package/src/commands/attach.ts +16 -0
  244. package/src/commands/bg.ts +9 -0
  245. package/src/commands/cancel.ts +10 -0
  246. package/src/commands/clear.ts +1 -1
  247. package/src/commands/compact.ts +35 -0
  248. package/src/commands/context.ts +26 -0
  249. package/src/commands/edit.ts +37 -0
  250. package/src/commands/effort.ts +49 -0
  251. package/src/commands/expand.ts +8 -0
  252. package/src/commands/index.ts +24 -0
  253. package/src/commands/jobs.ts +18 -0
  254. package/src/commands/kill.ts +14 -0
  255. package/src/commands/login.ts +24 -22
  256. package/src/commands/logout.ts +8 -16
  257. package/src/commands/model.ts +10 -22
  258. package/src/commands/provider.ts +4 -11
  259. package/src/commands/skills.ts +34 -4
  260. package/src/commands/steer.ts +13 -0
  261. package/src/commands/threads.ts +28 -0
  262. package/src/commands/whoami.ts +6 -5
  263. package/src/config/store.ts +168 -77
  264. package/src/config/trust.ts +181 -22
  265. package/src/headless.ts +225 -0
  266. package/src/index.ts +19 -3
  267. package/src/lib/debug.ts +24 -0
  268. package/src/lib/diagnostics.ts +90 -8
  269. package/src/lib/highlight-subset.ts +93 -0
  270. package/src/lib/http.ts +66 -4
  271. package/src/lib/lifecycle.ts +69 -0
  272. package/src/lib/markdown.ts +22 -4
  273. package/src/lib/model-meta.ts +138 -0
  274. package/src/lib/native-tools.ts +32 -0
  275. package/src/lib/net.ts +204 -0
  276. package/src/patch/apply.ts +194 -0
  277. package/src/patch/parse.ts +131 -0
  278. package/src/patch/plan.ts +317 -0
  279. package/src/providers/anthropic.ts +13 -1
  280. package/src/providers/deepseek.ts +3 -2
  281. package/src/providers/groq.ts +2 -1
  282. package/src/providers/index.ts +32 -1
  283. package/src/providers/mistral.ts +2 -1
  284. package/src/providers/moonshot.ts +2 -1
  285. package/src/providers/openai.ts +2 -1
  286. package/src/providers/openaiCompatible.ts +104 -19
  287. package/src/providers/openrouter.ts +2 -1
  288. package/src/providers/qwen.ts +2 -1
  289. package/src/providers/spec.ts +42 -0
  290. package/src/providers/xai.ts +2 -1
  291. package/src/providers/zhipu.ts +2 -1
  292. package/src/session/log.ts +311 -29
  293. package/src/session/projection.ts +621 -0
  294. package/src/skills/manager.ts +140 -18
  295. package/src/types/highlight-shims.d.ts +14 -0
  296. package/src/types.ts +79 -2
  297. package/src/ui/Message.ts +13 -2
  298. package/src/ui/Picker.ts +24 -0
  299. package/src/ui/Prompt.ts +2 -2
  300. package/src/ui/Spinner.ts +11 -5
  301. package/src/ui/StatusBar.ts +6 -5
  302. package/tsconfig.json +1 -0
  303. /package/dist/{cli.d.ts → cli-main.d.ts} +0 -0
@@ -0,0 +1,486 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync, statSync } from "node:fs";
3
+ import type { ChatChunk, ChatMessage, NativeToolCall, Provider, ReasoningEffort, ToolSchema } from "../types.js";
4
+ import type { ActiveFile } from "../session/log.js";
5
+ import type { SideEffects, ToolResult } from "./tools/types.js";
6
+ import { estMessagesTokens, estTokens, getModelMeta } from "../lib/model-meta.js";
7
+ import { compactMessages, DEFAULT_COMPACTION_CONFIG, type CompactionConfig } from "./compaction.js";
8
+ import { isLoopExempt, isReadOnlyToolCall, toolCallFingerprint } from "./guards.js";
9
+ import { isNativeUnsupportedError, type ToolProtocol } from "./protocol-mode.js";
10
+ import type { ApprovalResolver } from "./approval.js";
11
+ import { formatToolResults, parseToolCalls, stripToolCalls } from "./protocol.js";
12
+ import { runStream } from "./stream.js";
13
+ import { runAgentTool } from "./tools.js";
14
+
15
+ export interface ToolCall {
16
+ tool: string;
17
+ [key: string]: any;
18
+ }
19
+
20
+ export interface LoopDetectionOptions {
21
+ readOnlyThreshold?: number;
22
+ sideEffectThreshold?: number;
23
+ window?: number;
24
+ exemptReadOnly?: boolean;
25
+ }
26
+
27
+ export interface RunOptions {
28
+ provider: Provider;
29
+ apiKey?: string;
30
+ model: string;
31
+ messages?: ChatMessage[];
32
+ prompt: string;
33
+ system?: string;
34
+ maxTurns?: number;
35
+ allowTools?: boolean;
36
+ tokenBudget?: number;
37
+ costBudgetUsd?: number;
38
+ signal?: AbortSignal;
39
+ cwd?: string;
40
+ idleTimeoutMs?: number;
41
+ contextBudgetChars?: number;
42
+ compaction?: CompactionConfig;
43
+ contextWindow?: number;
44
+ serverOpaqueState?: string;
45
+ loopDetection?: LoopDetectionOptions;
46
+ reasoningEffort?: ReasoningEffort;
47
+ protocol?: ToolProtocol;
48
+ tools?: ToolSchema[];
49
+ onProtocolFallback?: (message: string) => void;
50
+ onNativeOk?: () => void;
51
+ takeSteering?: () => string[];
52
+ onDelta?: (delta: string, text: string) => void;
53
+ requestApproval?: ApprovalResolver;
54
+ }
55
+
56
+ export type AgentStopReason =
57
+ | "complete"
58
+ | "max_turns"
59
+ | "token_budget"
60
+ | "cost_budget"
61
+ | "loop_detected"
62
+ | "aborted"
63
+ | "incomplete"
64
+ | "stream_error";
65
+
66
+ export type AgentErrorKind = "token_budget" | "cost_budget" | "loop_detected" | "aborted" | "stream_error";
67
+
68
+ export type AgentEvent =
69
+ | {
70
+ type: "compaction";
71
+ beforeChars: number;
72
+ afterChars: number;
73
+ beforeTokens: number;
74
+ afterTokens: number;
75
+ droppedMessages: number;
76
+ keptMessages: number;
77
+ reason: "threshold" | "heap";
78
+ source: "local" | "server";
79
+ summaryFile?: string;
80
+ }
81
+ | { type: "assistant_raw"; text: string; toolCalls?: NativeToolCall[] }
82
+ | { type: "assistant_text"; text: string }
83
+ | { type: "tool_call"; id: string; call: ToolCall; native?: boolean }
84
+ | { type: "tool_result"; id: string; call: ToolCall; ok: boolean; output: string; sideEffects?: SideEffects; code?: string; hint?: string; truncated?: boolean; totalLines?: number; returnedLines?: number; totalBytes?: number; returnedBytes?: number; filesRead?: number; filesSkipped?: number; matches?: number }
85
+ | { type: "egress_denied"; outlet: string; target?: string; rule: string; mode: "audit" | "block"; wouldBlock: boolean; code: "EGRESS_DENY"; reason: string }
86
+ | { type: "usage"; usage: NonNullable<ChatChunk["usage"]> }
87
+ | { type: "loop_detected"; fingerprint: string; occurrences: number; warned: boolean }
88
+ | { type: "notice"; text: string }
89
+ | { type: "error"; kind: AgentErrorKind; message: string; text?: string; used?: number }
90
+ | { type: "checkpoint"; turn: number; cwd: string; providerId: string; model: string; activeFiles: ActiveFile[] }
91
+ | { type: "done"; history: ChatMessage[]; finalText: string; stopReason: AgentStopReason };
92
+
93
+ export const LOOP_WARNING = "[loop detection] You appear to be repeating the same tool call. " +
94
+ "Do not repeat it again: inspect the result you already have, or change approach.";
95
+
96
+ interface LoopVerdict {
97
+ action: "ok" | "warn" | "stop";
98
+ occurrences: number;
99
+ }
100
+
101
+ /**
102
+ * Sliding-window loop detector.
103
+ *
104
+ * Semantics (fixed by FC-004, do not re-interpret):
105
+ * - hits count the last `window` tool calls, not turns;
106
+ * - read-only tools use a higher threshold than side-effecting tools (D-02);
107
+ * - the first threshold hit yields a warning and resets that fingerprint's count;
108
+ * - only a second full accumulation yields a terminal error;
109
+ * - the table lives for one run (turn), so a new user task starts clean;
110
+ * - fingerprints are normalized per tool call (absolute paths, timeoutMs removed).
111
+ */
112
+ class LoopState {
113
+ private entries = new Map<string, { warned: boolean; hits: number[] }>();
114
+ private calls = 0;
115
+ private readonly readOnlyThreshold: number;
116
+ private readonly sideEffectThreshold: number;
117
+ private readonly window: number;
118
+ private readonly exemptReadOnly: boolean;
119
+ constructor(options: LoopDetectionOptions = {}) {
120
+ this.readOnlyThreshold = positive(options.readOnlyThreshold, 6);
121
+ this.sideEffectThreshold = positive(options.sideEffectThreshold, 3);
122
+ this.window = positive(options.window, 5);
123
+ this.exemptReadOnly = options.exemptReadOnly === true;
124
+ }
125
+ check(fingerprint: string, readOnly: boolean): LoopVerdict {
126
+ if (readOnly && this.exemptReadOnly)
127
+ return { action: "ok", occurrences: 0 };
128
+ this.calls += 1;
129
+ const index = this.calls;
130
+ let entry = this.entries.get(fingerprint);
131
+ if (!entry) {
132
+ entry = { warned: false, hits: [] };
133
+ this.entries.set(fingerprint, entry);
134
+ }
135
+ entry.hits = entry.hits.filter((hit) => index - hit < this.window);
136
+ entry.hits.push(index);
137
+ const threshold = readOnly ? this.readOnlyThreshold : this.sideEffectThreshold;
138
+ if (entry.hits.length < threshold)
139
+ return { action: "ok", occurrences: entry.hits.length };
140
+ const occurrences = entry.hits.length;
141
+ if (!entry.warned) {
142
+ entry.warned = true;
143
+ entry.hits = [];
144
+ return { action: "warn", occurrences };
145
+ }
146
+ return { action: "stop", occurrences };
147
+ }
148
+ }
149
+
150
+ export async function* runAgent(options: RunOptions): AsyncGenerator<AgentEvent, void, void> {
151
+ const provider = options.provider;
152
+ const model = options.model;
153
+ const cwd = options.cwd ?? process.cwd();
154
+ const signal = options.signal;
155
+ const maxTurns = Math.max(1, Math.floor(positive(options.maxTurns, 8)));
156
+ const tokenBudget = optionalPositive(options.tokenBudget);
157
+ const costBudget = optionalPositive(options.costBudgetUsd);
158
+ const loop = new LoopState(options.loopDetection);
159
+ let history: ChatMessage[] = [...(options.messages ?? []), { role: "user", content: options.prompt }];
160
+ let currentText = "";
161
+ let lastVisible = "";
162
+ let consumedInput = 0;
163
+ let spentCost = 0;
164
+ let stopReason: AgentStopReason = "complete";
165
+ let errorMessage = "";
166
+ let naturalFinish = false;
167
+ let toolCallSeq = 0;
168
+ let protocol: ToolProtocol = options.protocol ?? "legacy";
169
+ let fellBack = false;
170
+ const activeFiles = new Map<string, ActiveFile>();
171
+ for (let turn = 0; turn < maxTurns; turn += 1) {
172
+ if (signal?.aborted) {
173
+ stopReason = "aborted";
174
+ break;
175
+ }
176
+ for (const steering of options.takeSteering?.() ?? []) {
177
+ history = [...history, { role: "system", content: "[user steering] " + steering }];
178
+ yield { type: "notice", text: "Steering applied to the next request." };
179
+ }
180
+ const contextWindow = options.contextWindow ?? getModelMeta(provider.id, model).contextWindow;
181
+ const compacted = compactMessages(history, options.compaction ?? DEFAULT_COMPACTION_CONFIG, {
182
+ contextWindow,
183
+ providerId: provider.id,
184
+ model,
185
+ serverOpaqueState: options.serverOpaqueState,
186
+ });
187
+ if (compacted.compacted) {
188
+ history = compacted.messages;
189
+ yield {
190
+ type: "compaction",
191
+ beforeChars: compacted.beforeChars,
192
+ afterChars: compacted.afterChars,
193
+ beforeTokens: compacted.beforeTokens,
194
+ afterTokens: compacted.afterTokens,
195
+ droppedMessages: compacted.droppedMessages,
196
+ keptMessages: compacted.keptMessages,
197
+ reason: compacted.reason ?? "threshold",
198
+ source: compacted.source === "server" ? "server" : "local",
199
+ summaryFile: compacted.summaryFile,
200
+ };
201
+ }
202
+ const promptTokens = estMessagesTokens(history, { providerId: provider.id, model }) +
203
+ estTokens(options.system ?? "", { providerId: provider.id, model });
204
+ if (tokenBudget !== undefined && consumedInput + promptTokens > tokenBudget) {
205
+ stopReason = "token_budget";
206
+ errorMessage = `Token budget exhausted (${consumedInput + promptTokens} > ${tokenBudget} estimated input tokens). Partial output is kept.`;
207
+ yield { type: "error", kind: "token_budget", message: errorMessage, text: lastVisible, used: consumedInput + promptTokens };
208
+ break;
209
+ }
210
+ if (costBudget !== undefined && spentCost >= costBudget) {
211
+ stopReason = "cost_budget";
212
+ errorMessage = `Cost budget exhausted ($${spentCost.toFixed(4)} of $${costBudget}). Partial output is kept.`;
213
+ yield { type: "error", kind: "cost_budget", message: errorMessage, text: lastVisible };
214
+ break;
215
+ }
216
+ currentText = "";
217
+ let resetNotice = "";
218
+ let streamed;
219
+ try {
220
+ streamed = await runStream(provider, {
221
+ model,
222
+ messages: history,
223
+ system: options.system,
224
+ reasoningEffort: options.reasoningEffort,
225
+ ...(protocol === "native" && options.tools && options.tools.length > 0 ? { tools: options.tools } : {}),
226
+ }, options.apiKey, {
227
+ signal,
228
+ idleTimeoutMs: options.idleTimeoutMs,
229
+ onDelta: (delta, text) => {
230
+ currentText = text;
231
+ options.onDelta?.(delta, text);
232
+ },
233
+ onReset: () => {
234
+ resetNotice = "Retried stream returned different content; the earlier partial output was discarded and replaced by the new stream.";
235
+ },
236
+ });
237
+ }
238
+ catch (err) {
239
+ if (err?.name === "AbortError" || signal?.aborted) {
240
+ stopReason = "aborted";
241
+ break;
242
+ }
243
+ errorMessage = String(err?.message ?? err);
244
+ if (protocol === "native" && !fellBack && isNativeUnsupportedError(errorMessage)) {
245
+ fellBack = true;
246
+ protocol = "legacy";
247
+ options.onProtocolFallback?.(`Native tools were rejected (${errorMessage.slice(0, 160)}); retrying with the pseudo protocol.`);
248
+ turn -= 1;
249
+ continue;
250
+ }
251
+ stopReason = "stream_error";
252
+ const partial = stripToolCalls(String((err as { partialText?: string })?.partialText ?? "")).trim();
253
+ if (partial)
254
+ lastVisible = partial;
255
+ yield { type: "error", kind: "stream_error", message: errorMessage, text: partial || undefined };
256
+ break;
257
+ }
258
+ if (signal?.aborted) {
259
+ stopReason = "aborted";
260
+ break;
261
+ }
262
+ if (resetNotice)
263
+ yield { type: "notice", text: resetNotice };
264
+ if (streamed.retried > 0)
265
+ yield { type: "notice", text: `Network stream was interrupted; retried ${streamed.retried} time(s).` };
266
+ if (streamed.usage) {
267
+ consumedInput += streamed.usage.inputTokens ?? promptTokens;
268
+ spentCost += streamed.usage.costUsd ?? 0;
269
+ yield { type: "usage", usage: streamed.usage };
270
+ }
271
+ else {
272
+ consumedInput += promptTokens;
273
+ }
274
+ if (streamed.incomplete) {
275
+ const partial = stripToolCalls(streamed.text).trim();
276
+ if (partial)
277
+ lastVisible = partial;
278
+ stopReason = "incomplete";
279
+ errorMessage = `Connection ended before the response finished${streamed.retried > 0 ? ` (retried ${streamed.retried}×)` : ""}. ` +
280
+ "Partial output is kept — type 继续 to continue.";
281
+ yield { type: "notice", text: errorMessage };
282
+ yield { type: "checkpoint", turn: turn + 1, cwd, providerId: provider.id, model, activeFiles: Array.from(activeFiles.values()) };
283
+ break;
284
+ }
285
+ const visibleText = stripToolCalls(streamed.text);
286
+ if (visibleText)
287
+ lastVisible = visibleText;
288
+ const nativeCalls: NativeToolCall[] = protocol === "native" && options.allowTools !== false ? (streamed.toolCalls ?? []) : [];
289
+ if (nativeCalls.length > 0) {
290
+ yield { type: "assistant_raw", text: streamed.text, toolCalls: nativeCalls };
291
+ if (visibleText)
292
+ yield { type: "assistant_text", text: visibleText };
293
+ history = [...history, { role: "assistant", content: streamed.text, toolCalls: nativeCalls }];
294
+ for (const native of nativeCalls) {
295
+ if (signal?.aborted) {
296
+ stopReason = "aborted";
297
+ break;
298
+ }
299
+ const id = native.id || `tc_${++toolCallSeq}`;
300
+ let call: ToolCall;
301
+ let parseError = "";
302
+ try {
303
+ const args = native.arguments ? JSON.parse(native.arguments) : {};
304
+ if (!args || typeof args !== "object" || Array.isArray(args))
305
+ throw new Error("arguments must be a JSON object");
306
+ call = { ...args, tool: native.name };
307
+ }
308
+ catch (err: any) {
309
+ parseError = `invalid arguments for ${native.name}: ${err?.message ?? String(err)}`;
310
+ call = { tool: native.name };
311
+ }
312
+ const fingerprint = toolCallFingerprint(call, cwd);
313
+ if (!isLoopExempt(call.tool)) {
314
+ const verdict = loop.check(fingerprint, isReadOnlyToolCall(call));
315
+ if (verdict.action === "warn") {
316
+ yield { type: "loop_detected", fingerprint, occurrences: verdict.occurrences, warned: false };
317
+ history = [...history, { role: "system", content: LOOP_WARNING }];
318
+ }
319
+ else if (verdict.action === "stop") {
320
+ stopReason = "loop_detected";
321
+ errorMessage = `Loop detected: the same tool call repeated ${verdict.occurrences} times. Stopped before executing it again.`;
322
+ yield { type: "error", kind: "loop_detected", message: errorMessage, text: lastVisible };
323
+ break;
324
+ }
325
+ }
326
+ yield { type: "tool_call", id, call, native: true };
327
+ const result: any = parseError
328
+ ? { ok: false, output: parseError }
329
+ : await runAgentTool(call, cwd, { signal, turn: turn + 1, requestApproval: options.requestApproval });
330
+ trackSideEffects(activeFiles, result.sideEffects);
331
+ if (result.egress)
332
+ yield { type: "egress_denied", ...result.egress };
333
+ yield { type: "tool_result", id, call, ok: Boolean(result.ok), output: String(result.output ?? ""), ...toolResultMeta(result) };
334
+ history = [...history, { role: "tool", toolCallId: id, content: String(result.output ?? "") }];
335
+ }
336
+ yield { type: "checkpoint", turn: turn + 1, cwd, providerId: provider.id, model, activeFiles: Array.from(activeFiles.values()) };
337
+ if (stopReason === "complete")
338
+ options.onNativeOk?.();
339
+ if (stopReason !== "complete")
340
+ break;
341
+ continue;
342
+ }
343
+ const parsed = options.allowTools === false
344
+ ? { calls: [] as ToolCall[], consumedEnd: 0, errors: [] as string[] }
345
+ : parseToolCalls(streamed.text);
346
+ const calls: ToolCall[] = parsed.calls;
347
+ yield { type: "assistant_raw", text: streamed.text };
348
+ if (calls.length === 0) {
349
+ if (parsed.errors.length > 0) {
350
+ // Never let a broken tool-call block end the turn silently: the
351
+ // model would wait forever for a tool result. Inject a visible
352
+ // tool error and continue so it can re-emit valid markup.
353
+ const id = `tc_${++toolCallSeq}`;
354
+ const call: ToolCall = { tool: "protocol_error" };
355
+ const output = "Previous output contained invalid tool-call markup. " +
356
+ "Re-emit a complete <fastcode_tool>{\"tool\":...}</fastcode_tool> block.\n" +
357
+ parsed.errors.join("\n");
358
+ yield { type: "tool_call", id, call };
359
+ yield { type: "tool_result", id, call, ok: false, output, code: "INVALID_ARGS", hint: "Re-emit a complete tool call." };
360
+ history = [...history, { role: "user", content: formatToolResults([{ call, ok: false, output }]) }];
361
+ yield { type: "checkpoint", turn: turn + 1, cwd, providerId: provider.id, model, activeFiles: Array.from(activeFiles.values()) };
362
+ continue;
363
+ }
364
+ lastVisible = visibleText || streamed.text;
365
+ naturalFinish = true;
366
+ stopReason = "complete";
367
+ yield { type: "checkpoint", turn: turn + 1, cwd, providerId: provider.id, model, activeFiles: Array.from(activeFiles.values()) };
368
+ break;
369
+ }
370
+ history = [...history, { role: "assistant", content: streamed.text }];
371
+ if (visibleText)
372
+ yield { type: "assistant_text", text: visibleText };
373
+ const results: Array<{ call: ToolCall; ok: boolean; output: string; sideEffects?: SideEffects }> = [];
374
+ for (const call of calls) {
375
+ if (signal?.aborted) {
376
+ stopReason = "aborted";
377
+ break;
378
+ }
379
+ const fingerprint = toolCallFingerprint(call, cwd);
380
+ if (!isLoopExempt(call.tool)) {
381
+ const verdict = loop.check(fingerprint, isReadOnlyToolCall(call));
382
+ if (verdict.action === "warn") {
383
+ yield { type: "loop_detected", fingerprint, occurrences: verdict.occurrences, warned: false };
384
+ history = [...history, { role: "system", content: LOOP_WARNING }];
385
+ }
386
+ else if (verdict.action === "stop") {
387
+ stopReason = "loop_detected";
388
+ errorMessage = `Loop detected: the same tool call repeated ${verdict.occurrences} times. Stopped before executing it again.`;
389
+ yield { type: "error", kind: "loop_detected", message: errorMessage, text: lastVisible };
390
+ break;
391
+ }
392
+ }
393
+ const id = `tc_${++toolCallSeq}`;
394
+ yield { type: "tool_call", id, call };
395
+ const result: any = await runAgentTool(call, cwd, { signal, turn: turn + 1, requestApproval: options.requestApproval });
396
+ results.push({ call, ok: Boolean(result.ok), output: String(result.output ?? ""), sideEffects: result.sideEffects });
397
+ trackSideEffects(activeFiles, result.sideEffects);
398
+ if (result.egress)
399
+ yield { type: "egress_denied", ...result.egress };
400
+ yield { type: "tool_result", id, call, ok: Boolean(result.ok), output: String(result.output ?? ""), ...toolResultMeta(result) };
401
+ }
402
+ if (results.length > 0)
403
+ history = [...history, { role: "user", content: formatToolResults(results) }];
404
+ yield { type: "checkpoint", turn: turn + 1, cwd, providerId: provider.id, model, activeFiles: Array.from(activeFiles.values()) };
405
+ if (stopReason !== "complete")
406
+ break;
407
+ }
408
+ if (stopReason === "stream_error") {
409
+ const partial = lastVisible;
410
+ if (partial)
411
+ history = [...history, { role: "assistant", content: partial }];
412
+ yield { type: "done", history, finalText: partial, stopReason };
413
+ return;
414
+ }
415
+ if (stopReason === "complete" && !naturalFinish)
416
+ stopReason = "max_turns";
417
+ let finalText = "";
418
+ if (stopReason === "aborted") {
419
+ const partial = stripToolCalls(currentText).trim() || lastVisible;
420
+ finalText = (partial ? partial + "\n\n" : "") + "[用户中断]";
421
+ yield { type: "error", kind: "aborted", message: "Cancelled.", text: partial };
422
+ }
423
+ else if (stopReason === "token_budget" || stopReason === "cost_budget" || stopReason === "loop_detected" || stopReason === "incomplete") {
424
+ finalText = lastVisible || errorMessage;
425
+ }
426
+ else if (stopReason === "max_turns") {
427
+ finalText = `Reached the maximum of ${maxTurns} agent tool turns. The work so far is kept in this session — type 继续 to keep going.`;
428
+ }
429
+ else {
430
+ finalText = lastVisible;
431
+ }
432
+ if (finalText)
433
+ history = [...history, { role: "assistant", content: finalText }];
434
+ yield { type: "done", history, finalText, stopReason };
435
+ }
436
+
437
+ function positive(value: number | undefined, fallback: number): number {
438
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
439
+ }
440
+
441
+ function optionalPositive(value: number | undefined): number | undefined {
442
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
443
+ }
444
+
445
+ function toolResultMeta(result: ToolResult) {
446
+ return {
447
+ sideEffects: result.sideEffects,
448
+ ...(result.code ? { code: result.code } : {}),
449
+ ...(result.hint ? { hint: result.hint } : {}),
450
+ ...(result.truncated !== undefined ? { truncated: result.truncated } : {}),
451
+ ...(result.totalLines !== undefined ? { totalLines: result.totalLines } : {}),
452
+ ...(result.returnedLines !== undefined ? { returnedLines: result.returnedLines } : {}),
453
+ ...(result.totalBytes !== undefined ? { totalBytes: result.totalBytes } : {}),
454
+ ...(result.returnedBytes !== undefined ? { returnedBytes: result.returnedBytes } : {}),
455
+ ...(result.filesRead !== undefined ? { filesRead: result.filesRead } : {}),
456
+ ...(result.filesSkipped !== undefined ? { filesSkipped: result.filesSkipped } : {}),
457
+ ...(result.matches !== undefined ? { matches: result.matches } : {}),
458
+ };
459
+ }
460
+
461
+ function trackSideEffects(activeFiles: Map<string, ActiveFile>, sideEffects: SideEffects | undefined): void {
462
+ if (!sideEffects)
463
+ return;
464
+ for (const file of [...(sideEffects.touched ?? []), ...(sideEffects.created ?? [])]) {
465
+ const meta = describeActiveFile(file);
466
+ if (meta)
467
+ activeFiles.set(file, meta);
468
+ }
469
+ for (const file of sideEffects.removed ?? [])
470
+ activeFiles.delete(file);
471
+ }
472
+
473
+ function describeActiveFile(file: string): ActiveFile | undefined {
474
+ try {
475
+ const info = statSync(file);
476
+ if (!info.isFile())
477
+ return undefined;
478
+ const sha1_8 = info.size > 8 * 1024 * 1024
479
+ ? "large"
480
+ : createHash("sha1").update(readFileSync(file)).digest("hex").slice(0, 8);
481
+ return { path: file, sha1_8, mtime: info.mtime.toISOString() };
482
+ }
483
+ catch {
484
+ return undefined;
485
+ }
486
+ }