min-agent 0.5.1 → 0.6.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 (169) hide show
  1. package/README.md +18 -18
  2. package/dist/cli.js +538 -12
  3. package/docs/API.md +6 -6
  4. package/package.json +6 -6
  5. package/skills/self-config/SKILL.md +4 -2
  6. package/skills/self-config/reference.md +4 -1
  7. package/dist/agent.js +0 -1388
  8. package/dist/assistant-stream.js +0 -131
  9. package/dist/cli/commands/chat.js +0 -13
  10. package/dist/cli/commands/ctx.js +0 -7
  11. package/dist/cli/commands/exec.js +0 -35
  12. package/dist/cli/commands/history.js +0 -58
  13. package/dist/cli/commands/index.js +0 -249
  14. package/dist/cli/commands/init.js +0 -18
  15. package/dist/cli/commands/mcp.js +0 -173
  16. package/dist/cli/commands/memory.js +0 -87
  17. package/dist/cli/commands/models.js +0 -21
  18. package/dist/cli/commands/permission.js +0 -12
  19. package/dist/cli/commands/rules.js +0 -33
  20. package/dist/cli/commands/sandbox.js +0 -13
  21. package/dist/cli/commands/serve.js +0 -9
  22. package/dist/cli/commands/setup.js +0 -56
  23. package/dist/cli/commands/shared.js +0 -25
  24. package/dist/cli/commands/skills.js +0 -119
  25. package/dist/cli/commands/think.js +0 -12
  26. package/dist/cli/commands/update.js +0 -7
  27. package/dist/cli/commands/write-config.js +0 -52
  28. package/dist/cli/errors.js +0 -36
  29. package/dist/cli/exec-prompt.js +0 -26
  30. package/dist/cli/option-helpers.js +0 -65
  31. package/dist/cli/program.js +0 -223
  32. package/dist/cli/setup/detect.js +0 -17
  33. package/dist/cli/setup/flags.js +0 -12
  34. package/dist/cli/setup/flow.js +0 -108
  35. package/dist/cli/setup/provider-form.js +0 -102
  36. package/dist/cli/setup/ui.js +0 -534
  37. package/dist/clipboard.js +0 -142
  38. package/dist/code-mode.js +0 -184
  39. package/dist/compaction.js +0 -644
  40. package/dist/config.js +0 -363
  41. package/dist/confirm.js +0 -172
  42. package/dist/context-window.js +0 -256
  43. package/dist/ctx-cli.js +0 -30
  44. package/dist/ctx.js +0 -80
  45. package/dist/doom-loop.js +0 -278
  46. package/dist/fetch-timeout.js +0 -152
  47. package/dist/http-approvals.js +0 -60
  48. package/dist/http.js +0 -119
  49. package/dist/instructions.js +0 -196
  50. package/dist/logger.js +0 -95
  51. package/dist/markdown.js +0 -259
  52. package/dist/mcp.js +0 -1036
  53. package/dist/memory-cli.js +0 -33
  54. package/dist/memory.js +0 -295
  55. package/dist/model-catalog.js +0 -285
  56. package/dist/ollama-model.js +0 -234
  57. package/dist/ollama-openai-bridge.js +0 -383
  58. package/dist/output.js +0 -76
  59. package/dist/paste-handler.js +0 -41
  60. package/dist/permission-cli.js +0 -40
  61. package/dist/plugins.js +0 -131
  62. package/dist/pricing.js +0 -119
  63. package/dist/provider.js +0 -63
  64. package/dist/question-format.js +0 -60
  65. package/dist/reasoning-stream.js +0 -158
  66. package/dist/sandbox-cli.js +0 -79
  67. package/dist/sandbox.js +0 -403
  68. package/dist/save-throttle.js +0 -45
  69. package/dist/scope.js +0 -23
  70. package/dist/serve/common.js +0 -425
  71. package/dist/serve/routes-chat.js +0 -367
  72. package/dist/serve/routes-mcp.js +0 -212
  73. package/dist/serve/routes-memory.js +0 -95
  74. package/dist/serve/routes-meta.js +0 -268
  75. package/dist/serve/routes-sessions.js +0 -61
  76. package/dist/serve/routes-skills.js +0 -70
  77. package/dist/serve.js +0 -138
  78. package/dist/sessions.js +0 -265
  79. package/dist/skills.js +0 -599
  80. package/dist/structured-output.js +0 -29
  81. package/dist/synthetic.js +0 -7
  82. package/dist/think-cli.js +0 -36
  83. package/dist/thinking-wire.js +0 -239
  84. package/dist/thinking.js +0 -166
  85. package/dist/title-gen.js +0 -55
  86. package/dist/token-display.js +0 -39
  87. package/dist/tool-display.js +0 -178
  88. package/dist/tool-output.js +0 -126
  89. package/dist/tools/apply_patch.js +0 -265
  90. package/dist/tools/atomic-file.js +0 -35
  91. package/dist/tools/backend.js +0 -61
  92. package/dist/tools/bash.js +0 -231
  93. package/dist/tools/code_search.js +0 -98
  94. package/dist/tools/edit.js +0 -68
  95. package/dist/tools/explore.js +0 -232
  96. package/dist/tools/glob.js +0 -40
  97. package/dist/tools/grep.js +0 -174
  98. package/dist/tools/index.js +0 -34
  99. package/dist/tools/question.js +0 -54
  100. package/dist/tools/read.js +0 -109
  101. package/dist/tools/search-searxng.js +0 -223
  102. package/dist/tools/search-serper.js +0 -189
  103. package/dist/tools/task.js +0 -165
  104. package/dist/tools/todo.js +0 -213
  105. package/dist/tools/web_fetch.js +0 -195
  106. package/dist/tools/web_search.js +0 -228
  107. package/dist/tools/write.js +0 -55
  108. package/dist/tui/App.js +0 -138
  109. package/dist/tui/ConfirmBar.js +0 -66
  110. package/dist/tui/CtxPicker.js +0 -68
  111. package/dist/tui/InputBar.js +0 -618
  112. package/dist/tui/MessageList.js +0 -718
  113. package/dist/tui/ModelPicker.js +0 -113
  114. package/dist/tui/QuestionBar.js +0 -136
  115. package/dist/tui/SessionPicker.js +0 -79
  116. package/dist/tui/Spinner.js +0 -6
  117. package/dist/tui/StatusBar.js +0 -22
  118. package/dist/tui/ThinkPicker.js +0 -75
  119. package/dist/tui/agent-runner.js +0 -223
  120. package/dist/tui/bracketed-paste.js +0 -37
  121. package/dist/tui/caret-pos.js +0 -179
  122. package/dist/tui/caret.js +0 -69
  123. package/dist/tui/click-count.js +0 -13
  124. package/dist/tui/diff-view.js +0 -61
  125. package/dist/tui/drag-state.js +0 -49
  126. package/dist/tui/hydrate.js +0 -129
  127. package/dist/tui/index.js +0 -264
  128. package/dist/tui/input-history.js +0 -125
  129. package/dist/tui/layout.js +0 -105
  130. package/dist/tui/mouse.js +0 -46
  131. package/dist/tui/overlay-input.js +0 -12
  132. package/dist/tui/paste-draft.js +0 -173
  133. package/dist/tui/prompt-queue.js +0 -24
  134. package/dist/tui/selection.js +0 -232
  135. package/dist/tui/session-switch.js +0 -28
  136. package/dist/tui/slash-commands.js +0 -129
  137. package/dist/tui/slash-handler.js +0 -615
  138. package/dist/tui/text-width.js +0 -113
  139. package/dist/tui/theme.js +0 -12
  140. package/dist/tui/token-info.js +0 -7
  141. package/dist/tui/tool-children.js +0 -19
  142. package/dist/tui/types.js +0 -1
  143. package/dist/tui/undo-stack.js +0 -14
  144. package/dist/tui/use-sgr-mouse.js +0 -29
  145. package/dist/tui-chat.js +0 -478
  146. package/dist/updater.js +0 -116
  147. package/dist/xml-search.js +0 -194
  148. package/docs/superpowers/plans/2026-08-16-batch1-tui-improvements.md +0 -1510
  149. package/docs/superpowers/plans/2026-08-16-batch2-cli-tools-api.md +0 -2105
  150. package/docs/superpowers/plans/2026-08-16-batch3-config-engineering.md +0 -1595
  151. package/docs/superpowers/plans/2026-08-16-input-caret.md +0 -782
  152. package/docs/superpowers/plans/2026-08-20-tui-completeness.md +0 -873
  153. package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +0 -631
  154. package/docs/superpowers/plans/2026-08-23-cli-setup.md +0 -501
  155. package/docs/superpowers/plans/2026-08-23-input-paste-attachments.md +0 -475
  156. package/docs/superpowers/plans/2026-08-23-thinking-wire-profile.md +0 -450
  157. package/docs/superpowers/specs/2026-08-16-batch1-tui-improvements-design.md +0 -183
  158. package/docs/superpowers/specs/2026-08-16-batch2-cli-tools-api-design.md +0 -220
  159. package/docs/superpowers/specs/2026-08-16-batch3-config-engineering-design.md +0 -196
  160. package/docs/superpowers/specs/2026-08-16-input-caret-design.md +0 -63
  161. package/docs/superpowers/specs/2026-08-17-mouse-selection-design.md +0 -116
  162. package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +0 -47
  163. package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +0 -37
  164. package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +0 -68
  165. package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +0 -273
  166. package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +0 -165
  167. package/docs/superpowers/specs/2026-08-23-cli-setup-design.md +0 -282
  168. package/docs/superpowers/specs/2026-08-23-input-paste-attachments-design.md +0 -174
  169. package/docs/superpowers/specs/2026-08-23-thinking-wire-profile-design.md +0 -140
@@ -1,256 +0,0 @@
1
- import { readFileSync, mkdirSync, existsSync } from "fs";
2
- import path from "path";
3
- import { atomicWriteFileSync } from "./tools/atomic-file.js";
4
- import { getConfigDir, getEffectiveConfig, getActiveProvider } from "./config.js";
5
- import { getCachedModelCatalog, getModelCatalog } from "./model-catalog.js";
6
- import { getCachedOllamaModel, getOllamaModel, OLLAMA_AGENT_CONTEXT_CAP, OLLAMA_DEFAULT_CONTEXT_WINDOW, } from "./ollama-model.js";
7
- /**
8
- * Auto-detect context window size for the current model.
9
- *
10
- * Resolution order:
11
- * 1. User config: provider.contextWindow (explicit override)
12
- * 2. Ollama providers (`type: "ollama"`): native POST /api/show operational window
13
- * (Modelfile num_ctx, else architecture length capped at 32k) — never Lonae
14
- * 3. Provider-specific API (OpenRouter, vLLM, hostname-based Ollama, OpenAI-compatible /models)
15
- * 4. Lonae model catalog (context + thinking options)
16
- * 5. Fallback: 512000 (memory only — never persisted as a detected value)
17
- */
18
- export const DEFAULT_CONTEXT_WINDOW = 512000;
19
- const CACHE_TTL = 7 * 24 * 60 * 60 * 1000;
20
- const FALLBACK_MEMORY_TTL = 10 * 60 * 1000;
21
- function cacheFile() {
22
- return path.join(getConfigDir(), "context-window-cache.json");
23
- }
24
- const memoryCache = new Map();
25
- const inFlight = new Map();
26
- function loadDiskCache() {
27
- const file = cacheFile();
28
- if (!existsSync(file))
29
- return {};
30
- try {
31
- return JSON.parse(readFileSync(file, "utf-8"));
32
- }
33
- catch {
34
- return {};
35
- }
36
- }
37
- function saveCache(cache) {
38
- const file = cacheFile();
39
- mkdirSync(path.dirname(file), { recursive: true });
40
- atomicWriteFileSync(file, JSON.stringify(cache));
41
- }
42
- function cacheTtl(source) {
43
- return source === "fallback" ? FALLBACK_MEMORY_TTL : CACHE_TTL;
44
- }
45
- function getCached(modelId) {
46
- const catalog = getCachedModelCatalog(modelId);
47
- if (catalog?.contextWindow)
48
- return { tokens: catalog.contextWindow, source: "detected" };
49
- const memory = memoryCache.get(modelId);
50
- if (memory && Date.now() - memory.timestamp <= cacheTtl(memory.source)) {
51
- return { tokens: memory.contextWindow, source: memory.source };
52
- }
53
- const disk = loadDiskCache()[modelId];
54
- if (!disk || disk.source !== "detected")
55
- return null;
56
- if (Date.now() - disk.timestamp > CACHE_TTL)
57
- return null;
58
- memoryCache.set(modelId, { ...disk, source: "detected" });
59
- return { tokens: disk.contextWindow, source: "detected" };
60
- }
61
- function setCache(modelId, contextWindow, source) {
62
- const entry = { contextWindow, timestamp: Date.now(), source };
63
- memoryCache.set(modelId, entry);
64
- if (source !== "detected")
65
- return;
66
- const cache = loadDiskCache();
67
- cache[modelId] = entry;
68
- saveCache(cache);
69
- }
70
- export function extractModelContextLength(model) {
71
- if (!model || typeof model !== "object")
72
- return null;
73
- const rec = model;
74
- const nested = [rec, rec.meta, rec.limits, rec.limit, rec.model_info];
75
- const keys = [
76
- "context_length",
77
- "max_model_len",
78
- "max_model_length",
79
- "max_context_length",
80
- "context_window",
81
- "max_input_tokens",
82
- "max_total_tokens",
83
- "context",
84
- "general.context_length",
85
- ];
86
- for (const obj of nested) {
87
- if (!obj || typeof obj !== "object")
88
- continue;
89
- const bag = obj;
90
- for (const key of keys) {
91
- const n = bag[key];
92
- if (typeof n === "number" && Number.isFinite(n) && n >= 4096)
93
- return Math.floor(n);
94
- }
95
- }
96
- return null;
97
- }
98
- function matchListedModel(models, modelId) {
99
- const exact = models.find((m) => m && typeof m === "object" && m.id === modelId);
100
- if (exact)
101
- return exact;
102
- return models.find((m) => {
103
- if (!m || typeof m !== "object")
104
- return false;
105
- const id = m.id;
106
- return typeof id === "string" && (id.endsWith(`/${modelId}`) || modelId.endsWith(`/${id}`));
107
- });
108
- }
109
- /** Try OpenRouter: GET /api/v1/models returns context_length per model */
110
- async function tryOpenRouter(baseURL, apiKey, modelId) {
111
- if (!baseURL.includes("openrouter"))
112
- return null;
113
- try {
114
- const response = await fetch("https://openrouter.ai/api/v1/models", {
115
- headers: { Authorization: `Bearer ${apiKey}` },
116
- signal: AbortSignal.timeout(8000),
117
- });
118
- if (!response.ok)
119
- return null;
120
- const data = (await response.json());
121
- return extractModelContextLength(matchListedModel(data.data ?? [], modelId));
122
- }
123
- catch {
124
- return null;
125
- }
126
- }
127
- /** Try vLLM: GET /v1/models returns max_model_len */
128
- async function tryVllm(baseURL, apiKey, modelId) {
129
- if (!baseURL.includes("vllm"))
130
- return null;
131
- try {
132
- const url = `${baseURL.replace(/\/$/, "")}/models`;
133
- const response = await fetch(url, {
134
- headers: { Authorization: `Bearer ${apiKey}` },
135
- signal: AbortSignal.timeout(5000),
136
- });
137
- if (!response.ok)
138
- return null;
139
- const data = (await response.json());
140
- return extractModelContextLength(matchListedModel(data.data ?? [], modelId));
141
- }
142
- catch {
143
- return null;
144
- }
145
- }
146
- /** Any OpenAI-compatible /models that advertises context length (1M custom endpoints included). */
147
- async function tryProviderModels(baseURL, apiKey, modelId) {
148
- if (!baseURL)
149
- return null;
150
- try {
151
- const url = `${baseURL.replace(/\/$/, "")}/models`;
152
- const response = await fetch(url, {
153
- headers: { Authorization: `Bearer ${apiKey}` },
154
- signal: AbortSignal.timeout(8000),
155
- });
156
- if (!response.ok)
157
- return null;
158
- const data = (await response.json());
159
- const models = Array.isArray(data) ? data : (data.data ?? []);
160
- return extractModelContextLength(matchListedModel(models, modelId));
161
- }
162
- catch {
163
- return null;
164
- }
165
- }
166
- /** Try Ollama: POST /api/show returns model info with context length */
167
- async function tryOllama(baseURL, modelId) {
168
- if (!baseURL.includes("localhost") && !baseURL.includes("127.0.0.1") && !baseURL.includes("ollama"))
169
- return null;
170
- const info = await getOllamaModel(modelId, baseURL);
171
- return info?.contextWindow ?? null;
172
- }
173
- async function detectOllamaWindow(baseURL, id) {
174
- const shown = await getOllamaModel(id, baseURL);
175
- if (shown?.contextWindow) {
176
- setCache(id, shown.contextWindow, "detected");
177
- return { tokens: shown.contextWindow, source: "detected" };
178
- }
179
- const listed = await tryProviderModels(baseURL, "ollama", id);
180
- if (listed) {
181
- const tokens = Math.min(listed, OLLAMA_AGENT_CONTEXT_CAP);
182
- setCache(id, tokens, "detected");
183
- return { tokens, source: "detected" };
184
- }
185
- setCache(id, OLLAMA_DEFAULT_CONTEXT_WINDOW, "fallback");
186
- return { tokens: OLLAMA_DEFAULT_CONTEXT_WINDOW, source: "fallback" };
187
- }
188
- /**
189
- * Detect context window size for a model by probing all sources in parallel.
190
- * Returns the first non-null result, respecting the original precedence.
191
- */
192
- async function detectContextWindow(config, id) {
193
- const provider = getActiveProvider(config);
194
- const baseURL = provider?.baseURL ?? "";
195
- const apiKey = provider?.apiKey ?? "";
196
- if (provider?.type === "ollama")
197
- return detectOllamaWindow(baseURL, id);
198
- const catalog = await getModelCatalog(id, baseURL);
199
- if (catalog?.contextWindow)
200
- return { tokens: catalog.contextWindow, source: "detected" };
201
- const results = await Promise.all([
202
- tryOpenRouter(baseURL, apiKey, id),
203
- tryOllama(baseURL, id),
204
- tryVllm(baseURL, apiKey, id),
205
- tryProviderModels(baseURL, apiKey, id),
206
- ]);
207
- const found = results.find((v) => v !== null);
208
- if (found) {
209
- setCache(id, found, "detected");
210
- return { tokens: found, source: "detected" };
211
- }
212
- setCache(id, DEFAULT_CONTEXT_WINDOW, "fallback");
213
- return { tokens: DEFAULT_CONTEXT_WINDOW, source: "fallback" };
214
- }
215
- /**
216
- * Get context window size and whether it was configured, detected, or guessed.
217
- * Fallback 512k is never written to disk as a detected value.
218
- */
219
- export async function getContextWindowInfo(modelId) {
220
- const config = getEffectiveConfig();
221
- const provider = getActiveProvider(config);
222
- if (provider?.contextWindow)
223
- return { tokens: provider.contextWindow, source: "config" };
224
- const id = modelId ?? provider?.defaultModel;
225
- if (!id)
226
- return { tokens: DEFAULT_CONTEXT_WINDOW, source: "fallback" };
227
- if (provider?.type === "ollama") {
228
- const shown = getCachedOllamaModel(id, provider.baseURL);
229
- if (shown?.contextWindow)
230
- return { tokens: shown.contextWindow, source: "detected" };
231
- }
232
- else {
233
- const cached = getCached(id);
234
- if (cached)
235
- return cached;
236
- }
237
- const pending = inFlight.get(id);
238
- if (pending)
239
- return pending;
240
- const probing = detectContextWindow(config, id);
241
- inFlight.set(id, probing);
242
- try {
243
- return await probing;
244
- }
245
- finally {
246
- inFlight.delete(id);
247
- }
248
- }
249
- /**
250
- * Get context window size for the current model.
251
- * Tries multiple sources, caches detected results in memory and on disk.
252
- * Concurrent calls for the same model share a single probe.
253
- */
254
- export async function getContextWindow(modelId) {
255
- return (await getContextWindowInfo(modelId)).tokens;
256
- }
package/dist/ctx-cli.js DELETED
@@ -1,30 +0,0 @@
1
- import { activeProviderIsOllama, configuredCtxChoice, ctxChoiceLabel, parseCtxChoice, setOllamaContextChoice, } from "./ctx.js";
2
- export const CTX_CLI_USAGE = [
3
- "Usage: min-agent ctx [2k|4k|8k|12k|16k|32k|64k|128k|256k|auto]",
4
- " Context levels are only available for an Ollama provider.",
5
- ].join("\n");
6
- export function runCtxCli(input) {
7
- if (input.positionals[0] === "--help" || input.positionals[0] === "-h") {
8
- return { ok: true, lines: [CTX_CLI_USAGE] };
9
- }
10
- if (!activeProviderIsOllama()) {
11
- return { ok: false, lines: ["Context levels are only available for an Ollama provider.", CTX_CLI_USAGE] };
12
- }
13
- if (input.positionals.length === 0) {
14
- const current = configuredCtxChoice();
15
- const label = current.choice === "auto" && current.tokens != null ? `${current.tokens}` : current.choice;
16
- return { ok: true, lines: [`Current context window: ${label}`, CTX_CLI_USAGE] };
17
- }
18
- if (input.positionals.length > 1)
19
- return { ok: false, lines: [CTX_CLI_USAGE] };
20
- const parsed = parseCtxChoice(input.positionals[0]);
21
- if (!parsed)
22
- return { ok: false, lines: [CTX_CLI_USAGE] };
23
- const result = setOllamaContextChoice(parsed);
24
- if (!result.ok)
25
- return { ok: false, lines: ["Context levels are only available for an Ollama provider.", CTX_CLI_USAGE] };
26
- return {
27
- ok: true,
28
- lines: [`✓ Context window set to ${result.choice === "auto" ? "auto" : ctxChoiceLabel(result.choice)}`],
29
- };
30
- }
package/dist/ctx.js DELETED
@@ -1,80 +0,0 @@
1
- import { getActiveProvider, loadConfig, saveConfig } from "./config.js";
2
- export const CTX_LEVELS = ["2k", "4k", "8k", "12k", "16k", "32k", "64k", "128k", "256k"];
3
- export const CTX_AUTO = "auto";
4
- export const CTX_CHOICES = [...CTX_LEVELS, CTX_AUTO];
5
- const CTX_TOKENS = {
6
- "2k": 2048,
7
- "4k": 4096,
8
- "8k": 8192,
9
- "12k": 12288,
10
- "16k": 16384,
11
- "32k": 32768,
12
- "64k": 65536,
13
- "128k": 131072,
14
- "256k": 262144,
15
- };
16
- const TOKEN_TO_LEVEL = new Map(CTX_LEVELS.map((level) => [CTX_TOKENS[level], level]));
17
- export function isCtxLevel(value) {
18
- return CTX_LEVELS.includes(value);
19
- }
20
- export function isCtxChoice(value) {
21
- return value === CTX_AUTO || isCtxLevel(value);
22
- }
23
- export function tokensForCtxLevel(level) {
24
- return CTX_TOKENS[level];
25
- }
26
- export function matchCtxLevel(tokens) {
27
- return TOKEN_TO_LEVEL.get(Math.floor(tokens)) ?? null;
28
- }
29
- export function parseCtxChoice(raw) {
30
- if (raw == null)
31
- return null;
32
- const text = raw.trim().toLowerCase();
33
- if (!text)
34
- return null;
35
- if (text === "auto" || text === "default")
36
- return CTX_AUTO;
37
- if (isCtxLevel(text))
38
- return text;
39
- if (!/^\d+$/.test(text))
40
- return null;
41
- return matchCtxLevel(Number(text));
42
- }
43
- export function ctxChoiceLabel(choice) {
44
- return choice === CTX_AUTO ? "自动" : choice;
45
- }
46
- export function activeProviderIsOllama() {
47
- return getActiveProvider(loadConfig())?.type === "ollama";
48
- }
49
- export function configuredCtxChoice() {
50
- const tokens = getActiveProvider(loadConfig())?.contextWindow;
51
- if (typeof tokens !== "number" || !Number.isFinite(tokens) || tokens <= 0)
52
- return { choice: CTX_AUTO };
53
- const level = matchCtxLevel(tokens);
54
- if (level)
55
- return { choice: level, tokens: tokensForCtxLevel(level) };
56
- return { choice: CTX_AUTO, tokens: Math.floor(tokens) };
57
- }
58
- export function setOllamaContextChoice(choice) {
59
- const cfg = loadConfig();
60
- const provider = getActiveProvider(cfg);
61
- if (!provider)
62
- return { ok: false, reason: "no-provider" };
63
- if (provider.type !== "ollama")
64
- return { ok: false, reason: "not-ollama" };
65
- if (choice === CTX_AUTO)
66
- delete provider.contextWindow;
67
- else
68
- provider.contextWindow = tokensForCtxLevel(choice);
69
- saveConfig(cfg);
70
- return choice === CTX_AUTO ? { ok: true, choice } : { ok: true, choice, tokens: tokensForCtxLevel(choice) };
71
- }
72
- export function ctxPayload() {
73
- const provider = getActiveProvider(loadConfig());
74
- if (provider?.type !== "ollama")
75
- return { configurable: false, level: null };
76
- const tokens = provider.contextWindow;
77
- if (typeof tokens !== "number" || !Number.isFinite(tokens) || tokens <= 0)
78
- return { configurable: true, level: CTX_AUTO };
79
- return { configurable: true, level: matchCtxLevel(tokens) };
80
- }
package/dist/doom-loop.js DELETED
@@ -1,278 +0,0 @@
1
- /**
2
- * Loop / stall detection for the agent run.
3
- *
4
- * 1. Exact identity: the same tool with the same (normalized) arguments,
5
- * THRESHOLD times in a row → halt. Non-consecutive repeats of the same call
6
- * are counted too (a parallel batch used to flush the old sliding window),
7
- * with a higher threshold.
8
- * 2. Web research productivity: a research call is judged by what came back.
9
- * Rounds that bring new URLs / domains are free — gathering data about many
10
- * entities legitimately needs many searches. Rounds that bring nothing new
11
- * build an "unproductive" streak that first steers the model to write, then
12
- * removes the research tools for the rest of the turn. A hard per-turn cap
13
- * still applies.
14
- *
15
- * The detector is owned by the outer run loop so auto-continue cannot reset it.
16
- */
17
- /** Same tool + same args, back to back. */
18
- export const EXACT_LOOP_THRESHOLD = 3;
19
- /** Same tool + same args anywhere in the turn (survives parallel batches). */
20
- export const REPEAT_TOTAL_THRESHOLD = 5;
21
- /** Research rounds without new sources before steering the model to deliver. */
22
- export const RESEARCH_STEER_AFTER = 3;
23
- /** Research rounds without new sources before the research tools are dropped. */
24
- export const RESEARCH_STOP_AFTER = 5;
25
- /** Hard ceiling on research calls in a single turn. */
26
- export const RESEARCH_TOTAL_CAP = 24;
27
- export const WEB_RESEARCH_TOOLS = new Set(["search_web", "web_fetch"]);
28
- const ARTIFACT_TOOLS = new Set(["write", "edit", "apply_patch"]);
29
- export const STEER_PROMPT = "Your last few searches and page fetches returned nothing new. Stop calling search_web and web_fetch. Using only what you already have, produce the user's requested deliverable now (files, code, or a complete answer). If some fields are uncertain, mark them and proceed — do not search again.";
30
- export const DELIVER_PROMPT = "The research tools have been removed for the rest of this turn. Do not try to search again. Using the information already in this conversation, produce the user's requested deliverable now (write files with the write/edit tools). If a fact is missing, note the gap and still write the output.";
31
- export const RESEARCH_STUB_RESULT = "Web search and page fetch are disabled for the rest of this turn. Produce the user's requested deliverable from information already in this conversation. If a fact is missing, note the gap and still write the output.";
32
- export const LOOP_HALT_MESSAGE = "同一操作连续重复了多次,已停止以免空转。发送消息可继续。";
33
- function positiveInt(value, fallback) {
34
- if (typeof value !== "number" || !Number.isFinite(value) || value < 1)
35
- return fallback;
36
- return Math.floor(value);
37
- }
38
- /** Deterministic serialization so equivalent objects compare equal regardless of key order. */
39
- function serializeInput(input) {
40
- try {
41
- if (input === null || typeof input !== "object")
42
- return JSON.stringify(input);
43
- if (Array.isArray(input))
44
- return JSON.stringify(input.map(serializeInput));
45
- const sorted = {};
46
- for (const key of Object.keys(input).sort()) {
47
- sorted[key] = serializeInput(input[key]);
48
- }
49
- return JSON.stringify(sorted);
50
- }
51
- catch {
52
- return String(input);
53
- }
54
- }
55
- function asRecord(input) {
56
- if (input && typeof input === "object" && !Array.isArray(input))
57
- return input;
58
- return null;
59
- }
60
- function normalizeQuery(raw) {
61
- if (typeof raw !== "string")
62
- return "";
63
- return raw.toLowerCase().replace(/\s+/g, " ").trim();
64
- }
65
- function hostOf(raw) {
66
- try {
67
- return new URL(raw.trim()).hostname.toLowerCase().replace(/^www\./, "");
68
- }
69
- catch {
70
- return "";
71
- }
72
- }
73
- function normalizeUrl(raw) {
74
- if (typeof raw !== "string")
75
- return "";
76
- try {
77
- const u = new URL(raw.trim());
78
- const host = u.hostname.toLowerCase().replace(/^www\./, "");
79
- const path = u.pathname.replace(/\/+$/, "");
80
- return `${u.protocol}//${host}${path}${u.search}`;
81
- }
82
- catch {
83
- return raw.trim().toLowerCase();
84
- }
85
- }
86
- /** Identity used for exact-repeat detection (search queries / URLs are normalized). */
87
- export function identityPayload(toolName, input) {
88
- const rec = asRecord(input);
89
- if (toolName === "search_web" && rec)
90
- return serializeInput({ query: normalizeQuery(rec.query) });
91
- if (toolName === "web_fetch" && rec)
92
- return serializeInput({ url: normalizeUrl(rec.url) });
93
- return serializeInput(input);
94
- }
95
- const URL_IN_TEXT = /https?:\/\/[^\s"'<>)\]}]+/g;
96
- const RESULT_ERROR = /^(error|search error|fetch error)\b/i;
97
- /** New URLs / domains found in a research result, ignoring ones already seen. */
98
- export function extractSources(text) {
99
- const urls = new Set();
100
- const hosts = new Set();
101
- for (const raw of text.match(URL_IN_TEXT) ?? []) {
102
- const url = normalizeUrl(raw);
103
- if (url)
104
- urls.add(url);
105
- const host = hostOf(raw);
106
- if (host)
107
- hosts.add(host);
108
- }
109
- return { urls: [...urls], hosts: [...hosts] };
110
- }
111
- export class DoomLoopDetector {
112
- lastCall = { key: "", count: 0 };
113
- callCounts = new Map();
114
- webTotal = 0;
115
- /** Research calls issued but whose results have not been seen yet. */
116
- inFlight = 0;
117
- /** Did the current batch of research calls bring anything new? */
118
- batchProductive = false;
119
- unproductiveRounds = 0;
120
- steered = false;
121
- capped = false;
122
- delivered = false;
123
- seenUrls = new Set();
124
- seenHosts = new Set();
125
- steerAfter;
126
- stopAfter;
127
- totalCap;
128
- constructor(opts) {
129
- this.steerAfter = positiveInt(opts?.steerAfter, RESEARCH_STEER_AFTER);
130
- this.stopAfter = Math.max(positiveInt(opts?.stopAfter, RESEARCH_STOP_AFTER), this.steerAfter + 1);
131
- this.totalCap = positiveInt(opts?.totalCap, RESEARCH_TOTAL_CAP);
132
- }
133
- get webResearchCount() {
134
- return this.webTotal;
135
- }
136
- get researchCapped() {
137
- return this.capped;
138
- }
139
- get producedArtifact() {
140
- return this.delivered;
141
- }
142
- /** Research rounds in a row that returned no new sources. */
143
- get unproductiveStreak() {
144
- return this.unproductiveRounds;
145
- }
146
- /** Record a tool call. Returns true if the run should halt (exact identity loop). */
147
- record(toolName, input) {
148
- return this.observe(toolName, input) === "halt";
149
- }
150
- /** A tool call is starting. */
151
- observe(toolName, input) {
152
- const key = `${toolName}\u0000${identityPayload(toolName, input)}`;
153
- if (this.lastCall.key === key)
154
- this.lastCall.count++;
155
- else
156
- this.lastCall = { key, count: 1 };
157
- const total = (this.callCounts.get(key) ?? 0) + 1;
158
- this.callCounts.set(key, total);
159
- if (this.lastCall.count >= EXACT_LOOP_THRESHOLD || total >= REPEAT_TOTAL_THRESHOLD) {
160
- this.lastCall = { key: "", count: 0 };
161
- this.callCounts.delete(key);
162
- return "halt";
163
- }
164
- if (WEB_RESEARCH_TOOLS.has(toolName)) {
165
- this.webTotal++;
166
- if (this.inFlight === 0)
167
- this.batchProductive = false;
168
- this.inFlight++;
169
- if (!this.capped && this.webTotal >= this.totalCap) {
170
- this.markCapped();
171
- return "cap";
172
- }
173
- return "ok";
174
- }
175
- if (ARTIFACT_TOOLS.has(toolName)) {
176
- // Real progress: forgive the research history so far.
177
- this.delivered = true;
178
- this.unproductiveRounds = 0;
179
- }
180
- // Other tools neither help nor hurt the research budget.
181
- return "ok";
182
- }
183
- /**
184
- * A tool result came back. Research productivity is judged here, once the
185
- * whole parallel batch has landed, so four fresh searches in one step are not
186
- * mistaken for a loop.
187
- */
188
- observeResult(toolName, output) {
189
- if (!WEB_RESEARCH_TOOLS.has(toolName))
190
- return "ok";
191
- const text = typeof output === "string" ? output : safeString(output);
192
- if (!RESULT_ERROR.test(text.trim())) {
193
- const { urls, hosts } = extractSources(text);
194
- let fresh = 0;
195
- for (const url of urls) {
196
- if (!this.seenUrls.has(url)) {
197
- this.seenUrls.add(url);
198
- fresh++;
199
- }
200
- }
201
- for (const host of hosts) {
202
- if (!this.seenHosts.has(host)) {
203
- this.seenHosts.add(host);
204
- fresh++;
205
- }
206
- }
207
- if (fresh > 0)
208
- this.batchProductive = true;
209
- }
210
- if (this.inFlight > 0)
211
- this.inFlight--;
212
- if (this.inFlight > 0)
213
- return "ok";
214
- if (this.batchProductive) {
215
- this.unproductiveRounds = 0;
216
- return "ok";
217
- }
218
- this.unproductiveRounds++;
219
- if (this.capped)
220
- return "ok";
221
- if (this.unproductiveRounds >= this.stopAfter) {
222
- this.markCapped();
223
- return "cap";
224
- }
225
- if (!this.steered && this.unproductiveRounds >= this.steerAfter) {
226
- this.steered = true;
227
- return "steer";
228
- }
229
- return "ok";
230
- }
231
- markCapped() {
232
- this.capped = true;
233
- this.steered = true;
234
- }
235
- promptHint() {
236
- if (this.capped) {
237
- return [
238
- "## Research budget",
239
- "search_web and web_fetch have been removed for the rest of this turn.",
240
- "Produce the requested deliverable from information already in the conversation.",
241
- ].join("\n");
242
- }
243
- if (this.webTotal < 4)
244
- return "";
245
- const lines = [
246
- "## Research budget",
247
- `Research calls this turn: ${this.webTotal}/${this.totalCap}; rounds without new sources: ${this.unproductiveRounds}/${this.stopAfter}.`,
248
- ];
249
- if (this.steered || this.unproductiveRounds >= this.steerAfter) {
250
- lines.push("Recent searches added nothing new. Stop searching and produce the requested deliverable; mark uncertain fields instead of looking for more.");
251
- }
252
- else {
253
- lines.push("Keep queries targeted: fetch the best sources, then write the output.");
254
- }
255
- return lines.join("\n");
256
- }
257
- reset() {
258
- this.lastCall = { key: "", count: 0 };
259
- this.callCounts.clear();
260
- this.webTotal = 0;
261
- this.inFlight = 0;
262
- this.batchProductive = false;
263
- this.unproductiveRounds = 0;
264
- this.steered = false;
265
- this.capped = false;
266
- this.delivered = false;
267
- this.seenUrls.clear();
268
- this.seenHosts.clear();
269
- }
270
- }
271
- function safeString(value) {
272
- try {
273
- return JSON.stringify(value) ?? String(value);
274
- }
275
- catch {
276
- return String(value);
277
- }
278
- }