micro-models-agent 0.7.10 → 0.9.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 (151) hide show
  1. package/dist/cli/commands.js +173 -0
  2. package/dist/cli/completer.js +168 -0
  3. package/dist/cli/index.js +2 -0
  4. package/dist/cli/main.js +95 -0
  5. package/dist/cli/repl.js +762 -0
  6. package/dist/cli/security-commands.js +166 -0
  7. package/dist/cli/setup.js +214 -0
  8. package/dist/config/config.js +186 -0
  9. package/dist/config/defaults.js +91 -0
  10. package/dist/config/experts.js +15 -0
  11. package/dist/config/index.js +3 -0
  12. package/dist/config/security.js +187 -0
  13. package/dist/config/types.js +1 -0
  14. package/dist/core/agent.js +626 -0
  15. package/dist/core/bootstrap.js +317 -0
  16. package/dist/core/index.js +2 -0
  17. package/dist/core/prompt-builder.js +55 -0
  18. package/dist/core/types.js +1 -0
  19. package/dist/i18n/en.json +405 -0
  20. package/dist/i18n/index.js +43 -0
  21. package/dist/i18n/ru.json +405 -0
  22. package/dist/index.js +22 -0
  23. package/dist/llm/index.js +4 -0
  24. package/dist/llm/model-loader.js +78 -0
  25. package/dist/llm/openai-compat.js +277 -0
  26. package/dist/llm/orchestrator.js +194 -0
  27. package/dist/llm/provider.js +2 -0
  28. package/dist/llm/response.js +39 -0
  29. package/dist/llm/token-counter.js +37 -0
  30. package/dist/llm/types.js +1 -0
  31. package/dist/logger/app-logger.js +76 -0
  32. package/dist/logger/index.js +1 -0
  33. package/dist/main.js +15904 -0
  34. package/dist/migration/backup.js +45 -0
  35. package/dist/migration/detect.js +50 -0
  36. package/dist/migration/index.js +2 -0
  37. package/dist/modules/browser/actions.js +46 -0
  38. package/dist/modules/browser/cookie-store.js +24 -0
  39. package/dist/modules/browser/index.js +5 -0
  40. package/dist/modules/browser/module.js +28 -0
  41. package/dist/modules/browser/session.js +287 -0
  42. package/dist/modules/browser/snapshot.js +114 -0
  43. package/dist/modules/browser/types.js +9 -0
  44. package/dist/modules/context/history.js +15 -0
  45. package/dist/modules/context/index.js +1 -0
  46. package/dist/modules/context/manager.js +179 -0
  47. package/dist/modules/execution/auditor.js +72 -0
  48. package/dist/modules/execution/index.js +6 -0
  49. package/dist/modules/execution/module.js +334 -0
  50. package/dist/modules/execution/moe-executor.js +196 -0
  51. package/dist/modules/execution/plan-validator.js +153 -0
  52. package/dist/modules/execution/planner.js +35 -0
  53. package/dist/modules/execution/stuck-detector.js +113 -0
  54. package/dist/modules/execution/tracker.js +53 -0
  55. package/dist/modules/execution/types.js +1 -0
  56. package/dist/modules/execution/verifier.js +149 -0
  57. package/dist/modules/hallucination/confidence.js +47 -0
  58. package/dist/modules/hallucination/consistency.js +32 -0
  59. package/dist/modules/hallucination/detector.js +41 -0
  60. package/dist/modules/hallucination/factual.js +128 -0
  61. package/dist/modules/hallucination/index.js +4 -0
  62. package/dist/modules/index.js +5 -0
  63. package/dist/modules/indexer/cache.js +38 -0
  64. package/dist/modules/indexer/index.js +3 -0
  65. package/dist/modules/indexer/module.js +192 -0
  66. package/dist/modules/indexer/walker.js +101 -0
  67. package/dist/modules/mcp/client.js +393 -0
  68. package/dist/modules/mcp/index.js +3 -0
  69. package/dist/modules/mcp/module.js +146 -0
  70. package/dist/modules/mcp/registry.js +15 -0
  71. package/dist/modules/memory/index.js +1 -0
  72. package/dist/modules/memory/search.js +26 -0
  73. package/dist/modules/memory/store.js +38 -0
  74. package/dist/modules/pipelines/engine.js +60 -0
  75. package/dist/modules/pipelines/index.js +3 -0
  76. package/dist/modules/pipelines/parser.js +53 -0
  77. package/dist/modules/pipelines/template.js +14 -0
  78. package/dist/modules/plugins/builtin/lint-on-write.js +121 -0
  79. package/dist/modules/plugins/builtin/notify.js +8 -0
  80. package/dist/modules/plugins/index.js +1 -0
  81. package/dist/modules/plugins/loader.js +28 -0
  82. package/dist/modules/plugins/manager.js +161 -0
  83. package/dist/modules/plugins/types.js +1 -0
  84. package/dist/modules/registry.js +45 -0
  85. package/dist/modules/security/audit-log.js +116 -0
  86. package/dist/modules/security/audit-notifier.js +292 -0
  87. package/dist/modules/security/command-validator.js +104 -0
  88. package/dist/modules/security/content-scanner.js +52 -0
  89. package/dist/modules/security/data-sanitizer.js +97 -0
  90. package/dist/modules/security/encryption.js +238 -0
  91. package/dist/modules/security/index.js +14 -0
  92. package/dist/modules/security/network-validator.js +79 -0
  93. package/dist/modules/security/path-validator.js +155 -0
  94. package/dist/modules/security/rate-limiter.js +119 -0
  95. package/dist/modules/security/security-policies.js +393 -0
  96. package/dist/modules/security/session-encryption.js +193 -0
  97. package/dist/modules/security/session-isolation.js +95 -0
  98. package/dist/modules/session/index.js +3 -0
  99. package/dist/modules/session/manager.js +167 -0
  100. package/dist/modules/session/module.js +28 -0
  101. package/dist/modules/session/store.js +174 -0
  102. package/dist/modules/session/types.js +1 -0
  103. package/dist/modules/skills/index.js +3 -0
  104. package/dist/modules/skills/loader.js +72 -0
  105. package/dist/modules/skills/matcher.js +27 -0
  106. package/dist/modules/skills/module.js +180 -0
  107. package/dist/modules/types.js +1 -0
  108. package/dist/modules/updater/checker.js +32 -0
  109. package/dist/modules/updater/index.js +1 -0
  110. package/dist/modules/user-profile/compressor.js +16 -0
  111. package/dist/modules/user-profile/index.js +1 -0
  112. package/dist/modules/user-profile/profile.js +68 -0
  113. package/dist/tools/approve.js +32 -0
  114. package/dist/tools/bash.js +80 -0
  115. package/dist/tools/browser.js +97 -0
  116. package/dist/tools/create-dir.js +57 -0
  117. package/dist/tools/delete-file.js +64 -0
  118. package/dist/tools/edit-file.js +78 -0
  119. package/dist/tools/executor.js +83 -0
  120. package/dist/tools/file-info.js +46 -0
  121. package/dist/tools/filter-tools.js +10 -0
  122. package/dist/tools/glob-tool.js +19 -0
  123. package/dist/tools/grep-tool.js +57 -0
  124. package/dist/tools/index.js +44 -0
  125. package/dist/tools/list-dir.js +40 -0
  126. package/dist/tools/load-skill.js +48 -0
  127. package/dist/tools/mcp-call.js +68 -0
  128. package/dist/tools/move-file.js +84 -0
  129. package/dist/tools/pipeline-run.js +144 -0
  130. package/dist/tools/question.js +142 -0
  131. package/dist/tools/read-file.js +70 -0
  132. package/dist/tools/registry.js +36 -0
  133. package/dist/tools/scope-check.js +30 -0
  134. package/dist/tools/search-history.js +64 -0
  135. package/dist/tools/subagent.js +142 -0
  136. package/dist/tools/types.js +1 -0
  137. package/dist/tools/user-input.js +123 -0
  138. package/dist/tools/web-browse.js +51 -0
  139. package/dist/tools/web-fetch.js +62 -0
  140. package/dist/tools/web-search.js +59 -0
  141. package/dist/tools/write-file.js +80 -0
  142. package/dist/ui/box.js +81 -0
  143. package/dist/ui/colors.js +4 -0
  144. package/dist/ui/diff.js +185 -0
  145. package/dist/ui/index.js +6 -0
  146. package/dist/ui/md-formatter.js +212 -0
  147. package/dist/ui/output.js +13 -0
  148. package/dist/ui/renderer.js +141 -0
  149. package/dist/ui/spinner.js +70 -0
  150. package/dist/ui/table.js +144 -0
  151. package/package.json +1 -1
@@ -0,0 +1,57 @@
1
+ import { execFileSync, execSync } from 'child_process';
2
+ import { resolve } from 'path';
3
+ import { t } from '../i18n/index';
4
+ import { logBashCommand } from '../modules/security/audit-log';
5
+ export const grepTool = {
6
+ name: 'grep',
7
+ description: 'Search file contents using a regular expression. Uses ripgrep (rg) if available, otherwise falls back to grep -r.',
8
+ tags: ['file', 'code', 'research'],
9
+ parameters: {
10
+ type: 'object',
11
+ properties: {
12
+ pattern: { type: 'string', description: 'Regex pattern to search' },
13
+ include: { type: 'string', description: 'File pattern to filter (e.g. *.ts)' },
14
+ path: { type: 'string', description: 'Directory to search (defaults to baseDir)' },
15
+ },
16
+ required: ['pattern'],
17
+ },
18
+ handler: async (ctx, args) => {
19
+ const pattern = String(args.pattern);
20
+ const searchPath = args.path ? resolve(ctx.baseDir, String(args.path)) : ctx.baseDir;
21
+ // Build rg arguments as an array to avoid shell interpretation of regex
22
+ // metacharacters like |, (, ) — these are regex patterns, not shell operators.
23
+ const rgArgs = ['-n', '--with-filename', pattern, searchPath];
24
+ if (args.include) {
25
+ rgArgs.push('-g', String(args.include));
26
+ }
27
+ // Log the search (sanitized) for audit purposes
28
+ logBashCommand(ctx.sessionId, `rg ${rgArgs.join(' ')}`, false, 'grep-tool');
29
+ try {
30
+ const output = execFileSync('rg', rgArgs, {
31
+ encoding: 'utf-8',
32
+ maxBuffer: 1024 * 1024,
33
+ cwd: ctx.baseDir,
34
+ });
35
+ return { success: true, output: output || t('file.no_matches') };
36
+ }
37
+ catch (e) {
38
+ if (e.status === 1)
39
+ return { success: true, output: t('file.no_matches') };
40
+ // Fall back to plain grep -r when rg is not available or fails
41
+ try {
42
+ const cmd = `grep -rn "${pattern.replace(/"/g, '\\"')}" "${searchPath}"`;
43
+ const output = execSync(cmd, {
44
+ encoding: 'utf-8',
45
+ maxBuffer: 1024 * 1024,
46
+ cwd: ctx.baseDir,
47
+ });
48
+ return { success: true, output: output || t('file.no_matches') };
49
+ }
50
+ catch (e2) {
51
+ if (e2.status === 1)
52
+ return { success: true, output: t('file.no_matches') };
53
+ return { success: false, output: t('error.grep_failed', { message: e2.message }) };
54
+ }
55
+ }
56
+ },
57
+ };
@@ -0,0 +1,44 @@
1
+ import { ToolRegistry } from './registry';
2
+ import { ToolExecutor } from './executor';
3
+ import { readFileTool } from './read-file';
4
+ import { writeFileTool } from './write-file';
5
+ import { editFileTool } from './edit-file';
6
+ import { globTool } from './glob-tool';
7
+ import { grepTool } from './grep-tool';
8
+ import { listDirTool } from './list-dir';
9
+ import { createDirTool } from './create-dir';
10
+ import { deleteFileTool } from './delete-file';
11
+ import { moveFileTool } from './move-file';
12
+ import { fileInfoTool } from './file-info';
13
+ import { bashTool } from './bash';
14
+ import { subagentTool } from './subagent';
15
+ import { webSearchTool } from './web-search';
16
+ import { webFetchTool } from './web-fetch';
17
+ import { webBrowseTool } from './web-browse';
18
+ import { questionTool } from './question';
19
+ import { approveTool } from './approve';
20
+ import { createLoadSkillTool } from './load-skill';
21
+ import { pipelineRunTool } from './pipeline-run';
22
+ import { mcpCallTool } from './mcp-call';
23
+ import { searchHistoryTool } from './search-history';
24
+ import { createBrowserTool } from './browser';
25
+ export { ToolRegistry, ToolExecutor };
26
+ export { filterToolsByTags } from './filter-tools';
27
+ export { readFileTool, writeFileTool, editFileTool, globTool, grepTool, listDirTool, createDirTool, deleteFileTool, moveFileTool, fileInfoTool, bashTool, subagentTool, webSearchTool, webFetchTool, webBrowseTool, questionTool, approveTool, createLoadSkillTool, pipelineRunTool, mcpCallTool, searchHistoryTool, createBrowserTool, };
28
+ export function registerAllTools(registry, skillsModule) {
29
+ const tools = [
30
+ readFileTool, writeFileTool, editFileTool,
31
+ globTool, grepTool, listDirTool, createDirTool,
32
+ deleteFileTool, moveFileTool, fileInfoTool,
33
+ bashTool, subagentTool,
34
+ webSearchTool, webFetchTool, webBrowseTool,
35
+ questionTool, approveTool,
36
+ pipelineRunTool, mcpCallTool, searchHistoryTool,
37
+ ];
38
+ if (skillsModule) {
39
+ tools.push(createLoadSkillTool(skillsModule));
40
+ }
41
+ for (const tool of tools) {
42
+ registry.register(tool);
43
+ }
44
+ }
@@ -0,0 +1,40 @@
1
+ import { readdirSync, statSync, existsSync } from 'fs';
2
+ import { resolve, normalize } from 'path';
3
+ import { t } from '../i18n/index';
4
+ import { isPathInScope } from '../modules/security/path-validator';
5
+ export const listDirTool = {
6
+ name: 'list_dir',
7
+ description: 'List files and directories in a given path.',
8
+ tags: ['file'],
9
+ parameters: {
10
+ type: 'object',
11
+ properties: {
12
+ path: { type: 'string', description: 'Directory path' },
13
+ },
14
+ required: ['path'],
15
+ },
16
+ handler: async (ctx, args) => {
17
+ const path = String(args.path);
18
+ const baseDir = resolve(ctx.baseDir);
19
+ const resolved = resolve(baseDir, normalize(path));
20
+ // Check path permissions
21
+ const scopeCheck = isPathInScope(ctx.baseDir, path, ctx.scope, ctx.config.security?.paths);
22
+ if (!scopeCheck.allowed) {
23
+ return {
24
+ success: false,
25
+ output: t('file.path_not_allowed', {
26
+ path: `${path} — ${scopeCheck.reason}`,
27
+ }),
28
+ };
29
+ }
30
+ if (!existsSync(resolved)) {
31
+ return { success: false, output: t('file.dir_notfound', { path }) };
32
+ }
33
+ const entries = readdirSync(resolved);
34
+ const lines = entries.map(e => {
35
+ const full = resolve(resolved, e);
36
+ return statSync(full).isDirectory() ? `${e}/` : e;
37
+ });
38
+ return { success: true, output: lines.join('\n') || t('file.empty') };
39
+ },
40
+ };
@@ -0,0 +1,48 @@
1
+ import { t } from "../i18n/index";
2
+ export function createLoadSkillTool(skillsModule) {
3
+ return {
4
+ name: "load_skill",
5
+ tags: ["core"],
6
+ description: "Load a skill by name or task description. Skills provide specialized instructions and workflows. Only use when the task clearly requires domain-specific knowledge. If loading fails because the skill is too large, continue the task without it.",
7
+ parameters: {
8
+ type: "object",
9
+ properties: {
10
+ name: { type: "string", description: "Skill name to load directly" },
11
+ task: {
12
+ type: "string",
13
+ description: "Task description to find matching skill",
14
+ },
15
+ },
16
+ },
17
+ handler: async (_ctx, args) => {
18
+ const name = args.name ? String(args.name) : undefined;
19
+ const task = args.task ? String(args.task) : undefined;
20
+ if (!name && !task) {
21
+ return { success: false, output: t("tool.name_or_task") };
22
+ }
23
+ let result;
24
+ if (name) {
25
+ result = skillsModule.loadByName(name);
26
+ }
27
+ else if (task) {
28
+ result = skillsModule.loadByMatch(task);
29
+ }
30
+ else {
31
+ return { success: false, output: t("tool.invalid_params") };
32
+ }
33
+ if (!result.success) {
34
+ const available = skillsModule.getAvailable();
35
+ const hint = available.length > 0
36
+ ? `\n\n${t("tool.skill_available_hint")}: ${available.map((s) => s.name).join(", ")}`
37
+ : "";
38
+ return { success: false, output: result.message + hint };
39
+ }
40
+ const skill = result.skill;
41
+ const budget = skillsModule.getBudget();
42
+ return {
43
+ success: true,
44
+ output: `${result.message}\n\n--- Skill Content ---\n${skill.content}\n\n${t("tool.skill_budget", { remaining: budget.remaining })}`,
45
+ };
46
+ },
47
+ };
48
+ }
@@ -0,0 +1,68 @@
1
+ import { MCPClient } from "../modules/mcp/client";
2
+ import { MCPRegistry } from "../modules/mcp/registry";
3
+ const registry = new MCPRegistry();
4
+ let initialized = false;
5
+ function ensureInitialized(ctx) {
6
+ if (initialized)
7
+ return;
8
+ initialized = true;
9
+ const servers = ctx.config.mcpServers || {};
10
+ for (const [name, cfg] of Object.entries(servers)) {
11
+ if (cfg.enabled !== false) {
12
+ registry.register({
13
+ name: cfg.name || name,
14
+ command: cfg.command,
15
+ args: cfg.args,
16
+ env: cfg.env,
17
+ transport: cfg.transport,
18
+ url: cfg.url,
19
+ headers: cfg.headers,
20
+ timeout: cfg.timeout,
21
+ });
22
+ }
23
+ }
24
+ }
25
+ export const mcpCallTool = {
26
+ name: "mcp_call",
27
+ description: "Call a tool on an MCP (Model Context Protocol) server. MCP servers provide external capabilities like databases, APIs, or specialized tools.",
28
+ tags: ["code", "research"],
29
+ parameters: {
30
+ type: "object",
31
+ properties: {
32
+ server: { type: "string", description: "MCP server name" },
33
+ tool: { type: "string", description: "Tool name on the server" },
34
+ args: { type: "object", description: "Arguments to pass to the tool" },
35
+ },
36
+ required: ["server", "tool"],
37
+ },
38
+ handler: async (ctx, args) => {
39
+ const serverName = String(args.server || "");
40
+ const toolName = String(args.tool || "");
41
+ const toolArgs = (args.args || {});
42
+ if (!serverName || !toolName) {
43
+ return { success: false, output: "Server and tool names are required" };
44
+ }
45
+ ensureInitialized(ctx);
46
+ const serverConfig = registry.get(serverName);
47
+ if (!serverConfig) {
48
+ const available = registry.list();
49
+ return {
50
+ success: false,
51
+ output: `MCP server "${serverName}" not found. Available: ${available.join(", ") || "(none)"}`,
52
+ };
53
+ }
54
+ try {
55
+ const client = new MCPClient(serverConfig);
56
+ await client.connect();
57
+ const result = await client.callTool(toolName, toolArgs);
58
+ await client.disconnect();
59
+ return {
60
+ success: true,
61
+ output: `MCP call result:\n${JSON.stringify(result, null, 2)}`,
62
+ };
63
+ }
64
+ catch (e) {
65
+ return { success: false, output: `MCP call failed: ${e.message}` };
66
+ }
67
+ },
68
+ };
@@ -0,0 +1,84 @@
1
+ import { renameSync, existsSync, mkdirSync } from "fs";
2
+ import { resolve, normalize, dirname } from "path";
3
+ import { t } from "../i18n/index";
4
+ import { isPathWritable } from "../modules/security/path-validator";
5
+ import { logFileWrite, logSecurityBlock } from "../modules/security/audit-log";
6
+ import { getSessionSecurityConfig } from "../modules/security/session-isolation";
7
+ import { generateMoveDiff } from "../ui/diff";
8
+ export const moveFileTool = {
9
+ name: "move_file",
10
+ description: "Move or rename a file or directory.",
11
+ tags: ["file"],
12
+ parameters: {
13
+ type: "object",
14
+ properties: {
15
+ from: { type: "string", description: "Source path" },
16
+ to: { type: "string", description: "Destination path" },
17
+ },
18
+ required: ["from", "to"],
19
+ },
20
+ handler: async (ctx, args) => {
21
+ const fromPath = String(args.from);
22
+ const toPath = String(args.to);
23
+ const baseDir = resolve(ctx.baseDir);
24
+ const fromResolved = resolve(baseDir, normalize(fromPath));
25
+ const toResolved = resolve(baseDir, normalize(toPath));
26
+ // Get session-specific security config
27
+ const securityConfig = ctx.sessionContext
28
+ ? getSessionSecurityConfig(ctx.config, ctx.sessionContext)
29
+ : ctx.config.security;
30
+ // Check file operations limit
31
+ const maxFileOps = securityConfig?.maxFileOperations ?? 100;
32
+ const currentCount = ctx.fileOperationsCount ?? 0;
33
+ if (currentCount >= maxFileOps) {
34
+ logSecurityBlock(ctx.sessionId, "file_write", `Maximum file operations (${maxFileOps}) exceeded`, `${fromPath} -> ${toPath}`);
35
+ return {
36
+ success: false,
37
+ output: `[SECURITY BLOCKED] Maximum file operations (${maxFileOps}) exceeded`,
38
+ };
39
+ }
40
+ // Check source path permissions
41
+ const fromCheck = isPathWritable(ctx.baseDir, fromPath, ctx.scope, ctx.config.security?.paths);
42
+ if (!fromCheck.allowed) {
43
+ logSecurityBlock(ctx.sessionId, "file_write", fromCheck.reason || "Source path not allowed", fromPath);
44
+ return {
45
+ success: false,
46
+ output: t("file.path_not_allowed", {
47
+ path: `${fromPath} — ${fromCheck.reason}`,
48
+ }),
49
+ };
50
+ }
51
+ // Check destination path permissions
52
+ const toCheck = isPathWritable(ctx.baseDir, toPath, ctx.scope, ctx.config.security?.paths);
53
+ if (!toCheck.allowed) {
54
+ logSecurityBlock(ctx.sessionId, "file_write", toCheck.reason || "Destination path not allowed", toPath);
55
+ return {
56
+ success: false,
57
+ output: t("file.path_not_allowed", {
58
+ path: `${toPath} — ${toCheck.reason}`,
59
+ }),
60
+ };
61
+ }
62
+ if (!existsSync(fromResolved)) {
63
+ return {
64
+ success: false,
65
+ output: t("file.not_found_short", { path: fromPath }),
66
+ };
67
+ }
68
+ const toDir = dirname(toResolved);
69
+ if (!existsSync(toDir)) {
70
+ mkdirSync(toDir, { recursive: true });
71
+ }
72
+ renameSync(fromResolved, toResolved);
73
+ const diff = generateMoveDiff(fromPath, toPath);
74
+ // Increment file operations counter
75
+ ctx.fileOperationsCount = currentCount + 1;
76
+ // Log file move
77
+ logFileWrite(ctx.sessionId, `${fromPath} -> ${toPath}`, true, "File moved");
78
+ return {
79
+ success: true,
80
+ output: t("file.moved", { from: fromPath, to: toPath }),
81
+ diff,
82
+ };
83
+ },
84
+ };
@@ -0,0 +1,144 @@
1
+ import { t } from '../i18n/index';
2
+ import { PipelineEngine } from '../modules/pipelines/engine';
3
+ import { PipelineParser } from '../modules/pipelines/parser';
4
+ import { TemplateEngine } from '../modules/pipelines/template';
5
+ import { Agent } from '../core/agent';
6
+ import { ContextManager } from '../modules/context/manager';
7
+ import { PluginManager } from '../modules/plugins/manager';
8
+ import { HallucinationDetector } from '../modules/hallucination/detector';
9
+ import { logSecurityBlock } from '../modules/security/audit-log';
10
+ import { getSessionSecurityConfig } from '../modules/security/session-isolation';
11
+ const engine = new PipelineEngine();
12
+ const MAX_CONCURRENT = 3;
13
+ const MAX_ATTEMPTS = 3;
14
+ async function runStep(ctx, step, params, outputs) {
15
+ const prompt = TemplateEngine.render(step.prompt, params, outputs);
16
+ let lastError = "";
17
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
18
+ try {
19
+ const subContextManager = new ContextManager(ctx.config.contextWindow, ctx.config.contextBudget);
20
+ const subPluginManager = new PluginManager();
21
+ const systemPrompt = {
22
+ content: `You are a pipeline step agent ("${step.agent}"). Complete the given task using the available tools.`,
23
+ priority: "critical",
24
+ essential: true,
25
+ estimatedTokens: 80,
26
+ };
27
+ const subDeps = {
28
+ config: ctx.config,
29
+ llmProvider: ctx.llmProvider,
30
+ toolExecutor: ctx.toolExecutor,
31
+ pluginManager: subPluginManager,
32
+ contextManager: subContextManager,
33
+ hallucinationDetector: new HallucinationDetector(),
34
+ logger: ctx.logger,
35
+ baseDir: ctx.baseDir,
36
+ scope: ctx.scope,
37
+ recursionDepth: (ctx.recursionDepth ?? 0) + 1,
38
+ promptBlocks: [systemPrompt],
39
+ };
40
+ const subAgent = new Agent(subDeps);
41
+ const result = await subAgent.run(prompt);
42
+ if (result.success) {
43
+ return { ok: true, output: result.text };
44
+ }
45
+ lastError = result.error || "no output";
46
+ }
47
+ catch (e) {
48
+ lastError = e.message;
49
+ }
50
+ await new Promise((r) => setTimeout(r, 500 * attempt));
51
+ }
52
+ return { ok: false, output: "", error: lastError };
53
+ }
54
+ export const pipelineRunTool = {
55
+ name: 'pipeline_run',
56
+ description: 'Run a named pipeline with YAML definition. Creates a DAG of sub-agents that execute in dependency order.',
57
+ tags: ['shell', 'code'],
58
+ parameters: {
59
+ type: 'object',
60
+ properties: {
61
+ name: { type: 'string', description: 'Pipeline name' },
62
+ yaml: { type: 'string', description: 'Pipeline YAML definition with steps' },
63
+ params: { type: 'object', description: 'Template params for {key} placeholders' },
64
+ },
65
+ required: ['name', 'yaml'],
66
+ },
67
+ handler: async (ctx, args) => {
68
+ const name = String(args.name || '');
69
+ const yaml = String(args.yaml || '');
70
+ const params = args.params || {};
71
+ if (!yaml) {
72
+ return { success: false, output: t('pipeline.invalid') };
73
+ }
74
+ if (!ctx.llmProvider || !ctx.toolExecutor) {
75
+ return {
76
+ success: false,
77
+ output: 'Pipeline cannot run: missing llmProvider or toolExecutor in context',
78
+ };
79
+ }
80
+ const securityConfig = ctx.sessionContext
81
+ ? getSessionSecurityConfig(ctx.config, ctx.sessionContext)
82
+ : ctx.config.security;
83
+ const maxDepth = securityConfig?.maxRecursionDepth ?? 3;
84
+ const currentDepth = ctx.recursionDepth ?? 0;
85
+ if (currentDepth >= maxDepth) {
86
+ logSecurityBlock(ctx.sessionId, "bash_command", `Maximum recursion depth (${maxDepth}) exceeded`, name);
87
+ return {
88
+ success: false,
89
+ output: `[SECURITY BLOCKED] Maximum pipeline recursion depth (${maxDepth}) exceeded`,
90
+ };
91
+ }
92
+ try {
93
+ const pipeline = PipelineParser.parse(yaml);
94
+ engine.reset();
95
+ const order = engine.resolveDependencies(pipeline.steps);
96
+ const completed = new Set();
97
+ const outputs = {};
98
+ const logs = [];
99
+ const failed = new Set();
100
+ while (completed.size < pipeline.steps.length) {
101
+ const ready = engine
102
+ .getReadySteps(pipeline.steps, completed)
103
+ .filter((s) => !failed.has(s.id));
104
+ if (ready.length === 0) {
105
+ if (failed.size > 0)
106
+ break;
107
+ throw new Error(t('pipeline.circular', { stepId: order.join(', ') }));
108
+ }
109
+ const batch = ready.slice(0, MAX_CONCURRENT);
110
+ const results = await Promise.all(batch.map((step) => runStep(ctx, step, params, outputs)));
111
+ for (let i = 0; i < batch.length; i++) {
112
+ const step = batch[i];
113
+ const result = results[i];
114
+ if (result.ok) {
115
+ completed.add(step.id);
116
+ outputs[step.id] = { output: result.output };
117
+ engine.setStepStatus(step.id, 'done');
118
+ logs.push(` ✓ ${step.id}: ${result.output.split("\n")[0]}`);
119
+ }
120
+ else {
121
+ failed.add(step.id);
122
+ engine.setStepStatus(step.id, 'failed');
123
+ logs.push(` ✗ ${step.id}: ${result.error}`);
124
+ }
125
+ }
126
+ if (failed.size > 0)
127
+ break;
128
+ }
129
+ if (failed.size > 0) {
130
+ return {
131
+ success: false,
132
+ output: `Pipeline "${pipeline.name}" failed. Executed ${completed.size}/${pipeline.steps.length} steps.\n${logs.join("\n")}`,
133
+ };
134
+ }
135
+ return {
136
+ success: true,
137
+ output: `Pipeline "${pipeline.name}" completed successfully (${completed.size} steps).\n${logs.join("\n")}`,
138
+ };
139
+ }
140
+ catch (e) {
141
+ return { success: false, output: `Pipeline error: ${e.message}` };
142
+ }
143
+ },
144
+ };
@@ -0,0 +1,142 @@
1
+ import { t } from "../i18n/index";
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
14
+ - Omit "options" for a free-text question`;
15
+ function isOption(val) {
16
+ if (!val || typeof val !== "object")
17
+ return false;
18
+ const opt = val;
19
+ return typeof opt.label === "string" && typeof opt.description === "string";
20
+ }
21
+ function normalizeQuestions(args) {
22
+ const raw = args.questions;
23
+ if (Array.isArray(raw)) {
24
+ const specs = [];
25
+ for (const item of raw) {
26
+ if (!item || typeof item !== "object")
27
+ continue;
28
+ const q = item;
29
+ if (typeof q.question !== "string" || !q.question.trim())
30
+ continue;
31
+ const options = Array.isArray(q.options)
32
+ ? q.options.filter(isOption)
33
+ : undefined;
34
+ // An explicitly provided options array with no valid entries is a
35
+ // malformed call — skip the question instead of blocking on stdin.
36
+ if (Array.isArray(q.options) && options.length === 0)
37
+ continue;
38
+ specs.push({
39
+ question: q.question,
40
+ header: typeof q.header === "string" ? q.header : undefined,
41
+ options,
42
+ multiple: q.multiple === true,
43
+ custom: q.custom === false ? false : undefined,
44
+ });
45
+ }
46
+ return specs;
47
+ }
48
+ // Legacy format: { question: "..." } — plain free-text question.
49
+ if (typeof args.question === "string" && args.question.trim()) {
50
+ return [{ question: args.question }];
51
+ }
52
+ return [];
53
+ }
54
+ export const questionTool = {
55
+ name: "question",
56
+ description: DESCRIPTION,
57
+ tags: ["core"],
58
+ interactive: true,
59
+ parameters: {
60
+ type: "object",
61
+ properties: {
62
+ questions: {
63
+ type: "array",
64
+ description: "Questions to ask",
65
+ items: {
66
+ type: "object",
67
+ properties: {
68
+ question: { type: "string", description: "Complete question" },
69
+ header: {
70
+ type: "string",
71
+ description: "Very short label (max 30 chars)",
72
+ },
73
+ options: {
74
+ type: "array",
75
+ description: "Available choices; omit for a free-text question",
76
+ items: {
77
+ type: "object",
78
+ properties: {
79
+ label: {
80
+ type: "string",
81
+ description: "Display text (1-5 words, concise)",
82
+ },
83
+ description: {
84
+ type: "string",
85
+ description: "Explanation of choice",
86
+ },
87
+ },
88
+ required: ["label", "description"],
89
+ },
90
+ },
91
+ multiple: {
92
+ type: "boolean",
93
+ description: "Allow selecting multiple choices",
94
+ },
95
+ custom: {
96
+ type: "boolean",
97
+ description: "Allow typing a custom answer (default: true)",
98
+ },
99
+ },
100
+ required: ["question"],
101
+ },
102
+ },
103
+ question: {
104
+ type: "string",
105
+ description: "Legacy single free-text question",
106
+ },
107
+ },
108
+ },
109
+ handler: async (ctx, args) => {
110
+ if (ctx.exitOnComplete) {
111
+ return { success: false, output: t("tool.interactive_disabled") };
112
+ }
113
+ const questions = normalizeQuestions(args);
114
+ if (questions.length === 0) {
115
+ return { success: false, output: t("tool.question.no_questions") };
116
+ }
117
+ const answers = [];
118
+ for (let i = 0; i < questions.length; i++) {
119
+ const q = questions[i];
120
+ const progress = questions.length > 1
121
+ ? t("tool.question.progress", {
122
+ current: i + 1,
123
+ total: questions.length,
124
+ })
125
+ : undefined;
126
+ answers.push(await askUser(q.question, {
127
+ header: q.header,
128
+ options: q.options,
129
+ multiple: q.multiple,
130
+ custom: q.custom,
131
+ progress,
132
+ }));
133
+ }
134
+ const formatted = questions
135
+ .map((q, i) => `"${q.question}"="${answers[i].length ? answers[i].join(", ") : t("tool.question.unanswered")}"`)
136
+ .join(", ");
137
+ return {
138
+ success: true,
139
+ output: t("tool.question.answered", { formatted }),
140
+ };
141
+ },
142
+ };