micro-models-agent 0.63.0 → 1.1.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 (186) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/dist/cli/cache-line.js +30 -0
  3. package/dist/cli/command-suggest.js +38 -0
  4. package/dist/cli/commands.js +285 -60
  5. package/dist/cli/completer.js +16 -16
  6. package/dist/cli/json-payload.js +32 -0
  7. package/dist/cli/main.js +165 -77
  8. package/dist/cli/plugin-commands.js +5 -4
  9. package/dist/cli/relaunch.js +37 -0
  10. package/dist/cli/repl-commands.js +441 -307
  11. package/dist/cli/repl.js +360 -83
  12. package/dist/cli/run-result.js +12 -6
  13. package/dist/cli/security-commands.js +64 -60
  14. package/dist/cli/setup-order.js +57 -0
  15. package/dist/cli/setup-prompt.js +49 -0
  16. package/dist/cli/setup.js +52 -48
  17. package/dist/config/budget.js +48 -0
  18. package/dist/config/config.js +132 -70
  19. package/dist/config/defaults.js +37 -11
  20. package/dist/config/domains.js +9 -50
  21. package/dist/config/utils.js +56 -0
  22. package/dist/core/agent/audit-gate.js +49 -0
  23. package/dist/core/agent/compaction.js +89 -0
  24. package/dist/core/agent/constants.js +61 -0
  25. package/dist/core/agent/context-renderer.js +40 -0
  26. package/dist/core/agent/hallucination-gate.js +87 -0
  27. package/dist/core/agent/loop-state.js +53 -0
  28. package/dist/core/agent/prefix-monitor.js +101 -0
  29. package/dist/core/agent/reasoning-resolver.js +56 -0
  30. package/dist/core/agent/token-tracker.js +96 -0
  31. package/dist/core/agent/tool-batch.js +237 -0
  32. package/dist/core/agent/tool-output.js +62 -0
  33. package/dist/core/agent-moe.js +214 -69
  34. package/dist/core/agent.js +506 -546
  35. package/dist/core/bootstrap.js +297 -98
  36. package/dist/core/crash-handler.js +2 -1
  37. package/dist/core/prompt-builder.js +3 -0
  38. package/dist/core/prompt-overflow.js +307 -0
  39. package/dist/core/session-logger.js +34 -2
  40. package/dist/i18n/en.json +8 -4
  41. package/dist/i18n/ru.json +8 -4
  42. package/dist/index.js +5 -1
  43. package/dist/llm/cache-usage.js +76 -0
  44. package/dist/llm/image-utils.js +20 -16
  45. package/dist/llm/llm-errors.js +41 -0
  46. package/dist/llm/model-loader.js +30 -0
  47. package/dist/llm/openai-compat.js +287 -101
  48. package/dist/llm/orchestrator.js +140 -68
  49. package/dist/llm/provider-budget.js +68 -0
  50. package/dist/llm/provider.js +0 -1
  51. package/dist/llm/stream-state.js +26 -0
  52. package/dist/llm/token-counter.js +28 -0
  53. package/dist/logger/app-logger.js +12 -15
  54. package/dist/main.js +1755 -841
  55. package/dist/migration/detect.js +3 -1
  56. package/dist/modules/browser/actions.js +0 -3
  57. package/dist/modules/browser/bridge-client.js +2 -0
  58. package/dist/modules/browser/bridge-server.mjs +37 -4
  59. package/dist/modules/browser/driver.js +46 -4
  60. package/dist/modules/certification/cli.js +85 -42
  61. package/dist/modules/certification/loader.js +15 -1
  62. package/dist/modules/certification/manifest.js +126 -15
  63. package/dist/modules/certification/runner.js +4 -26
  64. package/dist/modules/certification/scenarios.js +184 -5
  65. package/dist/modules/certification/syntax-scenarios.js +51 -0
  66. package/dist/modules/context/chunk-query.js +25 -5
  67. package/dist/modules/context/fact-extractor.js +6 -2
  68. package/dist/modules/context/manager.js +23 -7
  69. package/dist/modules/execution/audit-runners.js +7 -1
  70. package/dist/modules/execution/auditor.js +3 -3
  71. package/dist/modules/execution/execution-plugin.js +22 -15
  72. package/dist/modules/execution/input-from.js +46 -0
  73. package/dist/modules/execution/module.js +107 -18
  74. package/dist/modules/execution/moe-executor.js +166 -54
  75. package/dist/modules/execution/plan-actions.js +524 -0
  76. package/dist/modules/execution/plan-steps.js +23 -0
  77. package/dist/modules/execution/plan-store.js +15 -3
  78. package/dist/modules/execution/plan-tool.js +6 -488
  79. package/dist/modules/execution/plan-validator.js +24 -0
  80. package/dist/modules/execution/stuck-detector.js +3 -18
  81. package/dist/modules/execution/tracker.js +14 -5
  82. package/dist/modules/execution/transient-error.js +30 -0
  83. package/dist/modules/execution/verifier.js +94 -7
  84. package/dist/modules/execution/windows-commands.js +11 -0
  85. package/dist/modules/hallucination/confidence.js +36 -23
  86. package/dist/modules/hallucination/consistency.js +3 -0
  87. package/dist/modules/hallucination/detector.js +8 -3
  88. package/dist/modules/hallucination/factual.js +26 -7
  89. package/dist/modules/hallucination/llm-judge.js +12 -2
  90. package/dist/modules/indexer/map-command.js +35 -0
  91. package/dist/modules/indexer/map-select.js +87 -0
  92. package/dist/modules/indexer/module.js +34 -22
  93. package/dist/modules/indexer/symbols.js +189 -0
  94. package/dist/modules/indexer/walker.js +96 -42
  95. package/dist/modules/lsp/check-tool.js +2 -1
  96. package/dist/modules/lsp/client.js +49 -32
  97. package/dist/modules/lsp/config.js +55 -2
  98. package/dist/modules/lsp/module.js +38 -5
  99. package/dist/modules/lsp/probe.js +4 -3
  100. package/dist/modules/lsp/project-root.js +41 -1
  101. package/dist/modules/lsp/startup-check.js +12 -4
  102. package/dist/modules/mcp/client.js +153 -104
  103. package/dist/modules/mcp/module.js +165 -41
  104. package/dist/modules/memory/module.js +4 -3
  105. package/dist/modules/plugins/builtin/lint-on-write.js +36 -6
  106. package/dist/modules/plugins/manager.js +47 -84
  107. package/dist/modules/pricing/index.js +17 -7
  108. package/dist/modules/pricing/prices.js +30 -12
  109. package/dist/modules/processes/index.js +1 -0
  110. package/dist/modules/processes/kill-tree.js +56 -0
  111. package/dist/modules/processes/registry.js +2 -54
  112. package/dist/modules/providers/cache.js +23 -0
  113. package/dist/modules/providers/factory.js +28 -0
  114. package/dist/modules/providers/fallback.js +7 -5
  115. package/dist/modules/providers/health.js +2 -1
  116. package/dist/modules/providers/index.js +1 -0
  117. package/dist/modules/providers/manager.js +17 -2
  118. package/dist/modules/providers/presets.js +79 -6
  119. package/dist/modules/reasoning/policy.js +40 -0
  120. package/dist/modules/reasoning/probe.js +111 -0
  121. package/dist/modules/security/audit-notifier.js +42 -27
  122. package/dist/modules/security/command-validator.js +25 -20
  123. package/dist/modules/security/encryption.js +6 -12
  124. package/dist/modules/security/network-validator.js +76 -5
  125. package/dist/modules/security/path-validator.js +77 -34
  126. package/dist/modules/security/rate-limiter.js +11 -0
  127. package/dist/modules/security/security-policies.js +1 -1
  128. package/dist/modules/security/session-encryption.js +13 -2
  129. package/dist/modules/security/session-isolation.js +2 -9
  130. package/dist/modules/session/manager.js +11 -0
  131. package/dist/modules/session/module.js +11 -3
  132. package/dist/modules/session/store.js +41 -5
  133. package/dist/modules/skills/loader.js +7 -1
  134. package/dist/modules/skills/module.js +2 -1
  135. package/dist/modules/updater/changelog-reader.js +94 -0
  136. package/dist/modules/updater/dev-detect.js +17 -0
  137. package/dist/modules/updater/index.js +1 -0
  138. package/dist/modules/updater/module.js +14 -3
  139. package/dist/output/bus.js +32 -0
  140. package/dist/output/channel.js +233 -0
  141. package/dist/output/format.js +14 -0
  142. package/dist/output/index.js +7 -0
  143. package/dist/output/json-sink.js +22 -0
  144. package/dist/output/machine.js +8 -0
  145. package/dist/output/session-sink.js +27 -0
  146. package/dist/output/types.js +1 -0
  147. package/dist/tools/approve.js +6 -2
  148. package/dist/tools/attach-image.js +11 -11
  149. package/dist/tools/auto-fixer.js +198 -0
  150. package/dist/tools/bash.js +142 -89
  151. package/dist/tools/chunk-query.js +10 -6
  152. package/dist/tools/download-file.js +1 -1
  153. package/dist/tools/edit-file.js +20 -2
  154. package/dist/tools/executor.js +54 -9
  155. package/dist/tools/glob-tool.js +7 -0
  156. package/dist/tools/grep-tool.js +15 -1
  157. package/dist/tools/index.js +3 -1
  158. package/dist/tools/list-dir.js +3 -1
  159. package/dist/tools/load-skill.js +2 -1
  160. package/dist/tools/mcp-call.js +1 -1
  161. package/dist/tools/move-file.js +5 -4
  162. package/dist/tools/path-utils.js +7 -0
  163. package/dist/tools/pipeline-run.js +1 -1
  164. package/dist/tools/prompt-io.js +28 -0
  165. package/dist/tools/question.js +12 -12
  166. package/dist/tools/scope-request.js +91 -0
  167. package/dist/tools/session-info.js +44 -0
  168. package/dist/tools/set-thinking.js +71 -0
  169. package/dist/tools/subagent.js +50 -9
  170. package/dist/tools/syntax-validator.js +177 -0
  171. package/dist/tools/user-input.js +16 -9
  172. package/dist/tools/write-file.js +17 -1
  173. package/dist/ui/diff.js +10 -0
  174. package/dist/ui/line-editor.js +179 -26
  175. package/dist/ui/line-math.js +20 -3
  176. package/dist/ui/md-formatter.js +100 -10
  177. package/dist/ui/output.js +5 -4
  178. package/dist/ui/plan-view.js +2 -7
  179. package/dist/ui/renderer.js +89 -85
  180. package/dist/ui/spinner.js +14 -4
  181. package/dist/utils/error.js +4 -0
  182. package/dist/utils/index.js +4 -0
  183. package/dist/utils/retry.js +17 -0
  184. package/dist/utils/sleep.js +23 -0
  185. package/dist/utils/truncate.js +9 -0
  186. package/package.json +1 -1
@@ -15,6 +15,11 @@ export class ToolExecutor {
15
15
  * wiring). Used internally — e.g. bash redirects a mistaken tool-call-as-
16
16
  * command into the real tool. Runs through the same registry and plugin
17
17
  * hooks as a normal call.
18
+ *
19
+ * **Reentrancy warning:** swaps `this.ctx` temporarily (line 30-39). Safe
20
+ * for sequential callers but NOT for concurrent calls on the same executor
21
+ * instance — they would race on the ctx swap. Today all callers are
22
+ * sequential (bash redirect, single subagent tool); keep it that way.
18
23
  */
19
24
  async executeByName(name, args, ctx) {
20
25
  const prevCtx = this.ctx;
@@ -53,6 +58,7 @@ export class ToolExecutor {
53
58
  const reason = typeof proceed === "string" ? proceed : undefined;
54
59
  return {
55
60
  success: false,
61
+ blocked: true,
56
62
  output: reason
57
63
  ? t("tool.blocked_reason", { plugin: pluginName, reason })
58
64
  : t("tool.blocked", { plugin: pluginName }),
@@ -74,11 +80,25 @@ export class ToolExecutor {
74
80
  }
75
81
  else {
76
82
  const timeoutMs = tool.timeoutMs ?? TOOL_EXECUTION_TIMEOUT_MS;
83
+ const failFast = (message) => {
84
+ // Kill any long-running child process registered for this call.
85
+ killByCallId(call.id);
86
+ // Run tool-specific cleanup (e.g. subagent scope restore) before
87
+ // rejecting so shared state is restored even on abandonment.
88
+ const cleanup = this.ctx.cleanup;
89
+ if (cleanup) {
90
+ cleanup();
91
+ // Clear only if no newer call has replaced it meanwhile.
92
+ if (this.ctx.cleanup === cleanup)
93
+ this.ctx.cleanup = undefined;
94
+ }
95
+ return new Error(message);
96
+ };
97
+ let timer;
98
+ let onAbort;
77
99
  const timeoutPromise = new Promise((_, reject) => {
78
- setTimeout(() => {
79
- // Kill any long-running child process registered for this call.
80
- killByCallId(call.id);
81
- reject(new Error(t("tool.timeout", {
100
+ timer = setTimeout(() => {
101
+ reject(failFast(t("tool.timeout", {
82
102
  name: call.name,
83
103
  seconds: Math.round(timeoutMs / 1000),
84
104
  })));
@@ -90,10 +110,7 @@ export class ToolExecutor {
90
110
  ];
91
111
  if (signal) {
92
112
  const abortPromise = new Promise((_, reject) => {
93
- const onAbort = () => {
94
- killByCallId(call.id);
95
- reject(new Error(t("tool.aborted", { name: call.name })));
96
- };
113
+ onAbort = () => reject(failFast(t("tool.aborted", { name: call.name })));
97
114
  if (signal.aborted) {
98
115
  onAbort();
99
116
  }
@@ -103,7 +120,26 @@ export class ToolExecutor {
103
120
  });
104
121
  racePromises.push(abortPromise);
105
122
  }
106
- result = await Promise.race(racePromises);
123
+ try {
124
+ result = await Promise.race(racePromises);
125
+ }
126
+ finally {
127
+ // Stop the timer and detach the abort listener once settled so they
128
+ // never fire against a DIFFERENT call's context/cleanup later.
129
+ if (timer !== undefined)
130
+ clearTimeout(timer);
131
+ if (onAbort && signal)
132
+ signal.removeEventListener("abort", onAbort);
133
+ // Losing promises have no consumer after Promise.race settled
134
+ // (their kill/cleanup effects were already applied in failFast).
135
+ // Log the rejection, then detach it so it never surfaces as an
136
+ // unhandled rejection.
137
+ for (const p of racePromises.slice(1)) {
138
+ p.catch((e) => {
139
+ this.ctx.logger.debug(`Tool ${call.name}: discarded race loser: ${e.message}`);
140
+ });
141
+ }
142
+ }
107
143
  }
108
144
  result.toolCallId = call.id;
109
145
  }
@@ -148,6 +184,15 @@ export class ToolExecutor {
148
184
  setScope(scope) {
149
185
  this.ctx.scope = scope;
150
186
  }
187
+ setPromptIO(io) {
188
+ this.ctx.promptIO = io;
189
+ }
190
+ getRecursionDepth() {
191
+ return this.ctx.recursionDepth ?? 0;
192
+ }
193
+ setRecursionDepth(depth) {
194
+ this.ctx.recursionDepth = depth;
195
+ }
151
196
  updateProvider(provider) {
152
197
  this.ctx.llmProvider = provider;
153
198
  }
@@ -15,6 +15,13 @@ export const globTool = {
15
15
  },
16
16
  handler: async (ctx, args) => {
17
17
  const pattern = String(args.pattern);
18
+ // Security: absolute patterns (/etc/*) and ".." segments escape baseDir.
19
+ if (/^([a-zA-Z]:)?[\\/]/.test(pattern) || pattern.split(/[\\/]/).includes("..")) {
20
+ return {
21
+ success: false,
22
+ output: t("file.path_not_allowed", { path: pattern }),
23
+ };
24
+ }
18
25
  const results = globSync(pattern, { cwd: ctx.baseDir });
19
26
  if (results.length === 0) {
20
27
  return { success: true, output: t("file.no_matches") };
@@ -1,7 +1,9 @@
1
1
  import { spawn } from "child_process";
2
2
  import { resolve } from "path";
3
3
  import { t } from "../i18n/index";
4
- import { logBashCommand } from "../modules/security/audit-log";
4
+ import { logBashCommand, logSecurityBlock } from "../modules/security/audit-log";
5
+ import { isPathInScope } from "../modules/security/path-validator";
6
+ import { DEFAULT_SECURITY_CONFIG } from "../config/security";
5
7
  import { MAX_PREVIEW_LINES } from "./preview";
6
8
  function truncateLines(output) {
7
9
  const lines = output.split("\n");
@@ -74,6 +76,18 @@ export const grepTool = {
74
76
  handler: async (ctx, args) => {
75
77
  const pattern = String(args.pattern);
76
78
  const searchPath = args.path ? resolve(ctx.baseDir, String(args.path)) : ctx.baseDir;
79
+ // Security: the search directory must stay inside baseDir / scope /
80
+ // allowed paths — `path: "../../.."` must not read outside the sandbox.
81
+ const scopeCheck = isPathInScope(ctx.baseDir, searchPath, ctx.scope, ctx.config?.security?.paths || DEFAULT_SECURITY_CONFIG.paths);
82
+ if (!scopeCheck.allowed) {
83
+ logSecurityBlock(ctx.sessionId || undefined, "file_read", scopeCheck.reason || "grep path not allowed", String(args.path ?? ctx.baseDir));
84
+ return {
85
+ success: false,
86
+ output: t("file.path_not_allowed", {
87
+ path: `${args.path ?? "."} — ${scopeCheck.reason}`,
88
+ }),
89
+ };
90
+ }
77
91
  // Build rg arguments as an array to avoid shell interpretation of regex
78
92
  // metacharacters like |, (, ) — these are regex patterns, not shell operators.
79
93
  const rgArgs = ["-n", "--with-filename", pattern, searchPath];
@@ -33,9 +33,10 @@ import { recallTool } from "./recall";
33
33
  import { createBrowserTool } from "./browser";
34
34
  import { attachImageTool } from "./attach-image";
35
35
  import { enableToolsTool } from "./enable-tools";
36
+ import { sessionInfoTool } from "./session-info";
36
37
  export { ToolRegistry, ToolExecutor };
37
38
  export { filterToolsByTags } from "./filter-tools";
38
- export { readFileTool, writeFileTool, editFileTool, globTool, grepTool, listDirTool, createDirTool, deleteFileTool, moveFileTool, fileInfoTool, bashTool, subagentTool, chunkQueryTool, processListTool, processLogTool, processKillTool, webSearchTool, webFetchTool, webBrowseTool, downloadFileTool, createLoadSkillTool, pipelineRunTool, mcpCallTool, searchHistoryTool, rememberTool, recallTool, createBrowserTool, attachImageTool, enableToolsTool, };
39
+ export { readFileTool, writeFileTool, editFileTool, globTool, grepTool, listDirTool, createDirTool, deleteFileTool, moveFileTool, fileInfoTool, bashTool, subagentTool, chunkQueryTool, processListTool, processLogTool, processKillTool, webSearchTool, webFetchTool, webBrowseTool, downloadFileTool, createLoadSkillTool, pipelineRunTool, mcpCallTool, searchHistoryTool, rememberTool, recallTool, createBrowserTool, attachImageTool, enableToolsTool, sessionInfoTool, };
39
40
  export function registerAllTools(registry, skillsModule, opts) {
40
41
  const tools = [
41
42
  readFileTool,
@@ -62,6 +63,7 @@ export function registerAllTools(registry, skillsModule, opts) {
62
63
  mcpCallTool,
63
64
  searchHistoryTool,
64
65
  attachImageTool,
66
+ sessionInfoTool,
65
67
  ];
66
68
  // On-demand tool activation is the default. When explicitly disabled
67
69
  // (enableOnDemand: false), the tool is not registered at all — combined
@@ -4,6 +4,8 @@ import { t } from "../i18n/index";
4
4
  import { isPathInScope } from "../modules/security/path-validator";
5
5
  import { safeResolvePath } from "./path-utils";
6
6
  import { MAX_PREVIEW_LINES } from "./preview";
7
+ /** Directories hidden from list_dir output (consistent with indexer walker). */
8
+ const IGNORED_DIRS = new Set(["node_modules", ".git", "dist", "build", "coverage", ".mma"]);
7
9
  export const listDirTool = {
8
10
  name: "list_dir",
9
11
  icon: "📂",
@@ -33,7 +35,7 @@ export const listDirTool = {
33
35
  if (!existsSync(resolved)) {
34
36
  return { success: false, output: t("file.dir_notfound", { path }) };
35
37
  }
36
- const entries = readdirSync(resolved);
38
+ const entries = readdirSync(resolved).filter((e) => !IGNORED_DIRS.has(e));
37
39
  const lines = entries.map((e) => {
38
40
  const full = resolve(resolved, e);
39
41
  return statSync(full).isDirectory() ? `${e}/` : e;
@@ -1,4 +1,5 @@
1
1
  import { t } from "../i18n/index";
2
+ import { estimateTokens } from "../llm/token-counter";
2
3
  export function createLoadSkillTool(skillsModule) {
3
4
  return {
4
5
  name: "load_skill",
@@ -29,7 +30,7 @@ export function createLoadSkillTool(skillsModule) {
29
30
  return { success: false, output: result.message + hint };
30
31
  }
31
32
  const budget = skillsModule.getBudget();
32
- const tokens = result.skill ? Math.ceil(result.skill.content.length / 4) : 0;
33
+ const tokens = result.skill ? estimateTokens(result.skill.content) : 0;
33
34
  return {
34
35
  success: true,
35
36
  output: t("tool.skill_budget", {
@@ -26,7 +26,7 @@ export const mcpCallTool = {
26
26
  name: "mcp_call",
27
27
  icon: "🔗",
28
28
  description: "Call a tool on an MCP (Model Context Protocol) server. MCP servers provide external capabilities like databases, APIs, or specialized tools.",
29
- tags: ["code", "research"],
29
+ tags: ["research"],
30
30
  parameters: {
31
31
  type: "object",
32
32
  properties: {
@@ -39,8 +39,9 @@ export const moveFileTool = {
39
39
  output: `[SECURITY BLOCKED] Maximum file operations (${maxFileOps}) exceeded`,
40
40
  };
41
41
  }
42
- // Check source path permissions
43
- const fromCheck = isPathWritable(ctx.baseDir, fromPath, ctx.scope, ctx.config.security?.paths);
42
+ // Check source path permissions — against the RESOLVED path (the same
43
+ // one renameSync will act on), never the raw argument.
44
+ const fromCheck = isPathWritable(ctx.baseDir, fromResolved, ctx.scope, ctx.config.security?.paths);
44
45
  if (!fromCheck.allowed) {
45
46
  logSecurityBlock(ctx.sessionId, "file_write", fromCheck.reason || "Source path not allowed", fromPath);
46
47
  return {
@@ -50,8 +51,8 @@ export const moveFileTool = {
50
51
  }),
51
52
  };
52
53
  }
53
- // Check destination path permissions
54
- const toCheck = isPathWritable(ctx.baseDir, toPath, ctx.scope, ctx.config.security?.paths);
54
+ // Check destination path permissions — resolved, same as above.
55
+ const toCheck = isPathWritable(ctx.baseDir, toResolved, ctx.scope, ctx.config.security?.paths);
55
56
  if (!toCheck.allowed) {
56
57
  logSecurityBlock(ctx.sessionId, "file_write", toCheck.reason || "Destination path not allowed", toPath);
57
58
  return {
@@ -1,5 +1,12 @@
1
1
  import { resolve, normalize, dirname, basename, sep, relative, isAbsolute } from "path";
2
2
  import { existsSync } from "fs";
3
+ /**
4
+ * Normalize backslashes to forward slashes for display and comparison.
5
+ * On POSIX this is a no-op; on Windows it converts `src\foo\bar.ts` → `src/foo/bar.ts`.
6
+ */
7
+ export function toForwardSlash(p) {
8
+ return p.replace(/\\/g, "/");
9
+ }
3
10
  /**
4
11
  * True when targetPath equals dirPath or lies inside it.
5
12
  *
@@ -55,7 +55,7 @@ export const pipelineRunTool = {
55
55
  name: "pipeline_run",
56
56
  icon: "▶️",
57
57
  description: "Run a named pipeline with YAML definition. Creates a DAG of sub-agents that execute in dependency order.",
58
- tags: ["shell", "code"],
58
+ tags: ["shell"],
59
59
  parameters: {
60
60
  type: "object",
61
61
  properties: {
@@ -0,0 +1,28 @@
1
+ import { formatMenu, choicePrompt, parseSelection, CUSTOM_INDEX, } from "./user-input";
2
+ import { t } from "../i18n/index";
3
+ /** Build a PromptIO from injectable ask/print primitives (testable, terminal-agnostic). */
4
+ export function createPromptIO(deps) {
5
+ return {
6
+ async askText(question) {
7
+ return (await deps.ask(`${question} `)).trim();
8
+ },
9
+ async askChoice(question, options, opts = {}) {
10
+ const multiple = opts.multiple ?? false;
11
+ const entryCount = options.length + (opts.allowCustom ? 1 : 0);
12
+ // No selectable entries (and no custom entry) — parseSelection can never
13
+ // succeed, so return without printing a dead menu or looping forever.
14
+ if (entryCount === 0)
15
+ return [];
16
+ const customEntry = options.length;
17
+ deps.print(formatMenu(question, options, opts));
18
+ for (;;) {
19
+ const answer = await deps.ask(choicePrompt(entryCount, multiple));
20
+ const parsed = parseSelection(answer, entryCount, multiple);
21
+ if (parsed) {
22
+ return parsed.map((i) => (opts.allowCustom && i === customEntry ? CUSTOM_INDEX : i));
23
+ }
24
+ deps.print(t("tool.user_input.invalid"));
25
+ }
26
+ },
27
+ };
28
+ }
@@ -1,16 +1,16 @@
1
1
  import { t } from "../i18n/index";
2
2
  import { askUser } from "./user-input";
3
- const DESCRIPTION = `Use this tool when you need to ask the user questions during execution. This allows you to:
4
- 1. Gather user preferences or requirements
5
- 2. Clarify ambiguous instructions
6
- 3. Get decisions on implementation choices as you work
7
- 4. Offer choices to the user about what direction to take.
8
-
9
- Usage notes:
10
- - When "custom" is enabled (default), a "Type your own answer" option is added automatically; don't include "Other" or catch-all options
11
- - Answers are returned as arrays of labels; set "multiple": true to allow selecting more than one
12
- - If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label
13
- - Execution blocks until the user answers every question
3
+ const DESCRIPTION = `Use this tool when you need to ask the user questions during execution. This allows you to:
4
+ 1. Gather user preferences or requirements
5
+ 2. Clarify ambiguous instructions
6
+ 3. Get decisions on implementation choices as you work
7
+ 4. Offer choices to the user about what direction to take.
8
+
9
+ Usage notes:
10
+ - When "custom" is enabled (default), a "Type your own answer" option is added automatically; don't include "Other" or catch-all options
11
+ - Answers are returned as arrays of labels; set "multiple": true to allow selecting more than one
12
+ - If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label
13
+ - Execution blocks until the user answers every question
14
14
  - Omit "options" for a free-text question`;
15
15
  function isOption(val) {
16
16
  if (!val || typeof val !== "object")
@@ -128,7 +128,7 @@ export const questionTool = {
128
128
  multiple: q.multiple,
129
129
  custom: q.custom,
130
130
  progress,
131
- }));
131
+ }, ctx.promptIO));
132
132
  }
133
133
  const formatted = questions
134
134
  .map((q, i) => `"${q.question}"="${answers[i].length ? answers[i].join(", ") : t("tool.question.unanswered")}"`)
@@ -0,0 +1,91 @@
1
+ const WRITE_TOOLS = new Set([
2
+ "write_file",
3
+ "edit_file",
4
+ "delete_file",
5
+ "create_dir",
6
+ "move_file",
7
+ "download_file",
8
+ ]);
9
+ /**
10
+ * Scope denials funnel through i18n keys file.path_not_allowed(_short)
11
+ * ("Path not allowed" / "Путь недопустим") and chunk-query's "[SCOPE]" prefix.
12
+ */
13
+ export function isPathDenialOutput(output) {
14
+ if (!output)
15
+ return false;
16
+ return (output.includes("Path not allowed") ||
17
+ output.includes("Путь недопустим") ||
18
+ output.startsWith("[SCOPE]"));
19
+ }
20
+ function pathArgOf(args) {
21
+ if (!args)
22
+ return "";
23
+ for (const key of ["path", "file", "input_path", "target", "source", "destination", "dir"]) {
24
+ const v = args[key];
25
+ if (typeof v === "string" && v.trim())
26
+ return v.trim();
27
+ }
28
+ for (const v of Object.values(args)) {
29
+ if (typeof v === "string" && /\.[\w-]+$/.test(v))
30
+ return v.trim();
31
+ }
32
+ return "";
33
+ }
34
+ /** Observes the sub-agent's tool results and records scope-denied paths. */
35
+ export class ScopeDenialCollector {
36
+ denied = new Map();
37
+ plugin = {
38
+ name: "scope-denial-collector",
39
+ isBuiltin: true,
40
+ onAfterTool: (_ctx, call, result) => {
41
+ const name = call?.name;
42
+ if (!name || !result || result.success)
43
+ return;
44
+ if (!isPathDenialOutput(typeof result.output === "string" ? result.output : undefined)) {
45
+ return;
46
+ }
47
+ const path = pathArgOf(call?.arguments);
48
+ if (!path)
49
+ return;
50
+ const write = WRITE_TOOLS.has(name);
51
+ this.denied.set(path, { path, write });
52
+ },
53
+ };
54
+ getDenied() {
55
+ return Array.from(this.denied.values());
56
+ }
57
+ /** read_only=true only when every denial was a pure read denial. */
58
+ toScopeRequest(reason = "sub-agent was denied access to files needed for the task") {
59
+ const all = this.getDenied();
60
+ if (all.length === 0)
61
+ return null;
62
+ return {
63
+ files: all.map((d) => d.path),
64
+ read_only: !all.some((d) => d.write),
65
+ reason,
66
+ };
67
+ }
68
+ }
69
+ export function formatScopeRequest(req) {
70
+ return `<scope-request>${JSON.stringify(req)}</scope-request>`;
71
+ }
72
+ export function parseScopeRequest(text) {
73
+ if (!text)
74
+ return null;
75
+ const match = text.match(/<scope-request>([\s\S]*?)<\/scope-request>/);
76
+ if (!match)
77
+ return null;
78
+ try {
79
+ const parsed = JSON.parse(match[1]);
80
+ if (!Array.isArray(parsed.files) || parsed.files.length === 0)
81
+ return null;
82
+ return {
83
+ files: parsed.files.map(String),
84
+ read_only: !!parsed.read_only,
85
+ reason: String(parsed.reason || "access denied"),
86
+ };
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ }
@@ -0,0 +1,44 @@
1
+ import { t } from "../i18n/index";
2
+ export const sessionInfoTool = {
3
+ name: "session_info",
4
+ icon: "ℹ️",
5
+ description: "Show metadata about the current session: id, name, model, provider, context window, message count, and how much of the context budget is used. Use when the user asks which session this is or how much context is left.",
6
+ alwaysOn: true,
7
+ boundedOutput: true,
8
+ parameters: {
9
+ type: "object",
10
+ properties: {},
11
+ },
12
+ handler: async (ctx) => {
13
+ const meta = ctx.sessionManager?.getActiveMeta();
14
+ if (!meta) {
15
+ return { success: true, output: t("tool.session_info.no_active") };
16
+ }
17
+ const provider = ctx.config.provider;
18
+ const providerName = provider.active ?? provider.type ?? "unknown";
19
+ const lines = [
20
+ t("tool.session_info.result", {
21
+ id: meta.id,
22
+ name: meta.name,
23
+ model: meta.model ?? ctx.config.model ?? "unknown",
24
+ provider: providerName,
25
+ contextWindow: String(meta.contextWindow ?? ctx.config.contextWindow),
26
+ messages: String(meta.messageCount),
27
+ createdAt: meta.createdAt,
28
+ updatedAt: meta.updatedAt,
29
+ }),
30
+ ];
31
+ const snapshot = ctx.contextManager?.getSnapshot();
32
+ if (snapshot) {
33
+ const used = snapshot.tokens;
34
+ const budget = snapshot.budget.history;
35
+ const percent = budget > 0 ? Math.round((used / budget) * 100) : 0;
36
+ lines.push(t("tool.session_info.context", {
37
+ used: String(used),
38
+ budget: String(budget),
39
+ percent: String(percent),
40
+ }));
41
+ }
42
+ return { success: true, output: lines.join("\n") };
43
+ },
44
+ };
@@ -0,0 +1,71 @@
1
+ // src/tools/set-thinking.ts
2
+ import { t } from "../i18n/index";
3
+ const VALID_LEVELS = ["default", "none", "low", "medium", "high", "max"];
4
+ export function createSetThinkingTool(state, config, getCurrentIteration) {
5
+ return {
6
+ name: "set_thinking",
7
+ description: "Set reasoning effort level for the next LLM call. Normally managed automatically by the harness. Use only to force deeper thinking for genuinely hard subproblems or shallower for mechanical batches.",
8
+ icon: "🧠",
9
+ tags: [],
10
+ alwaysOn: true,
11
+ parameters: {
12
+ type: "object",
13
+ properties: {
14
+ level: {
15
+ type: "string",
16
+ enum: ["none", "low", "high", "max"],
17
+ description: "Reasoning effort level to set",
18
+ },
19
+ reason: {
20
+ type: "string",
21
+ description: "Why changing the level",
22
+ },
23
+ },
24
+ required: ["level"],
25
+ },
26
+ handler: async (_ctx, args) => {
27
+ const requested = args.level;
28
+ const reason = args.reason || "agent override";
29
+ if (!VALID_LEVELS.includes(requested) && requested !== "auto") {
30
+ return {
31
+ success: false,
32
+ output: t("tool.thinking_invalid_level", { level: requested }),
33
+ };
34
+ }
35
+ // Cooldown check
36
+ const currentIter = getCurrentIteration();
37
+ const elapsed = currentIter - state.overrideIteration;
38
+ if (elapsed > 0 && elapsed < config.overrideCooldown) {
39
+ return {
40
+ success: true,
41
+ output: t("tool.thinking_cooldown", {
42
+ remaining: config.overrideCooldown - elapsed,
43
+ current: state.level,
44
+ }),
45
+ };
46
+ }
47
+ // Clamp to configured bounds
48
+ let level = requested;
49
+ const levelOrder = ["none", "low", "medium", "high", "max"];
50
+ const minIdx = levelOrder.indexOf(config.min);
51
+ const maxIdx = levelOrder.indexOf(config.max);
52
+ const reqIdx = levelOrder.indexOf(level);
53
+ let clamped = false;
54
+ if (reqIdx >= 0 && reqIdx < minIdx) {
55
+ level = config.min;
56
+ clamped = true;
57
+ }
58
+ else if (reqIdx > maxIdx) {
59
+ level = config.max;
60
+ clamped = true;
61
+ }
62
+ // Apply
63
+ state.level = level;
64
+ state.overrideIteration = currentIter;
65
+ const msg = clamped
66
+ ? t("tool.thinking_set_clamped", { level, reason })
67
+ : t("tool.thinking_set", { level, reason });
68
+ return { success: true, output: msg };
69
+ },
70
+ };
71
+ }