micro-models-agent 0.8.0 → 0.10.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.
@@ -1,7 +1,56 @@
1
1
  import { t } from '../i18n/index';
2
2
  import { PipelineEngine } from '../modules/pipelines/engine';
3
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';
4
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
+ }
5
54
  export const pipelineRunTool = {
6
55
  name: 'pipeline_run',
7
56
  description: 'Run a named pipeline with YAML definition. Creates a DAG of sub-agents that execute in dependency order.',
@@ -11,26 +60,82 @@ export const pipelineRunTool = {
11
60
  properties: {
12
61
  name: { type: 'string', description: 'Pipeline name' },
13
62
  yaml: { type: 'string', description: 'Pipeline YAML definition with steps' },
63
+ params: { type: 'object', description: 'Template params for {key} placeholders' },
14
64
  },
15
65
  required: ['name', 'yaml'],
16
66
  },
17
- handler: async (_ctx, args) => {
67
+ handler: async (ctx, args) => {
18
68
  const name = String(args.name || '');
19
69
  const yaml = String(args.yaml || '');
70
+ const params = args.params || {};
20
71
  if (!yaml) {
21
72
  return { success: false, output: t('pipeline.invalid') };
22
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
+ }
23
92
  try {
24
93
  const pipeline = PipelineParser.parse(yaml);
94
+ engine.reset();
25
95
  const order = engine.resolveDependencies(pipeline.steps);
26
- const readySteps = engine.getReadySteps(pipeline.steps, new Set());
27
- const summary = [
28
- `Pipeline "${pipeline.name}" parsed successfully.`,
29
- `Steps: ${pipeline.steps.length}`,
30
- `Execution order: ${order.join(' → ')}`,
31
- `Ready to run: ${readySteps.map(s => s.id).join(', ') || '(none)'}`,
32
- ].join('\n');
33
- return { success: true, output: summary };
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
+ };
34
139
  }
35
140
  catch (e) {
36
141
  return { success: false, output: `Pipeline error: ${e.message}` };
@@ -0,0 +1,29 @@
1
+ import { processRegistry } from '../modules/processes';
2
+ import { t } from '../i18n/index';
3
+ export const processKillTool = {
4
+ name: 'process_kill',
5
+ description: 'Stop a background process started via bash (dev server, watcher). Kills the whole process tree (children included). Use the id returned by bash or process_list.',
6
+ tags: ['shell'],
7
+ parameters: {
8
+ type: 'object',
9
+ properties: {
10
+ id: { type: 'string', description: 'Process id from bash output or process_list' },
11
+ },
12
+ required: ['id'],
13
+ },
14
+ handler: async (ctx, args) => {
15
+ const id = String(args.id);
16
+ const entry = processRegistry.get(id);
17
+ if (!entry) {
18
+ return { success: false, output: t('proc.not_found', { id }) };
19
+ }
20
+ const killed = processRegistry.kill(id);
21
+ if (!killed) {
22
+ return { success: false, output: t('proc.kill_failed', { id }) };
23
+ }
24
+ return {
25
+ success: true,
26
+ output: t('proc.killed', { id, pid: entry.pid }),
27
+ };
28
+ },
29
+ };
@@ -0,0 +1,38 @@
1
+ import { processRegistry } from '../modules/processes';
2
+ import { t } from '../i18n/index';
3
+ export const processListTool = {
4
+ name: 'process_list',
5
+ description: 'List background processes started via the bash tool (dev servers, watchers, long-running commands). Shows id, pid, command, status, and recent output. Use with process_log and process_kill to inspect or stop them.',
6
+ tags: ['shell'],
7
+ parameters: {
8
+ type: 'object',
9
+ properties: {},
10
+ },
11
+ handler: async (ctx) => {
12
+ const list = processRegistry.list(ctx.sessionId);
13
+ if (list.length === 0) {
14
+ return { success: true, output: t('proc.none') };
15
+ }
16
+ const statusLabel = (status) => {
17
+ if (status === 'running')
18
+ return t('proc.status_running');
19
+ if (status === 'exited')
20
+ return t('proc.status_exited');
21
+ return t('proc.status_killed');
22
+ };
23
+ const lines = [`${t('proc.list_header')} (${list.length}):`];
24
+ for (const entry of list) {
25
+ const tail = entry.log.length > 0 ? entry.log[entry.log.length - 1] : '';
26
+ const detail = tail
27
+ ? ` — ${tail.slice(0, 80)}${tail.length > 80 ? '…' : ''}`
28
+ : '';
29
+ lines.push(` ${entry.id} PID ${entry.pid} ${statusLabel(entry.status)} ${entry.command}${detail}`);
30
+ }
31
+ lines.push(`\n${t('proc.hint', {
32
+ list: 'process_list',
33
+ log: 'process_log',
34
+ kill: 'process_kill',
35
+ })}`);
36
+ return { success: true, output: lines.join('\n') };
37
+ },
38
+ };
@@ -0,0 +1,39 @@
1
+ import { processRegistry } from '../modules/processes';
2
+ import { t } from '../i18n/index';
3
+ export const processLogTool = {
4
+ name: 'process_log',
5
+ description: 'Show the buffered output of a background process started via bash. Use after starting a dev server to verify it came up without errors, and while it runs to check its state.',
6
+ tags: ['shell'],
7
+ parameters: {
8
+ type: 'object',
9
+ properties: {
10
+ id: { type: 'string', description: 'Process id from bash output or process_list' },
11
+ tail: { type: 'number', description: 'Number of trailing lines to show (default: all buffered lines, max 300)' },
12
+ },
13
+ required: ['id'],
14
+ },
15
+ handler: async (ctx, args) => {
16
+ const id = String(args.id);
17
+ const entry = processRegistry.get(id);
18
+ if (!entry) {
19
+ return { success: false, output: t('proc.not_found', { id }) };
20
+ }
21
+ const status = entry.status === 'running'
22
+ ? t('proc.status_running')
23
+ : entry.status === 'exited'
24
+ ? t('proc.status_exited')
25
+ : t('proc.status_killed');
26
+ const tail = typeof args.tail === 'number' ? args.tail : undefined;
27
+ const log = processRegistry.getLog(id, tail);
28
+ if (!log) {
29
+ return {
30
+ success: true,
31
+ output: `${t('proc.log_header', { id, status })} ${t('proc.log_empty')}`,
32
+ };
33
+ }
34
+ return {
35
+ success: true,
36
+ output: `${t('proc.log_header', { id, status })}\n${log}`,
37
+ };
38
+ },
39
+ };
@@ -2,6 +2,8 @@ import { readFileSync, existsSync } from "fs";
2
2
  import { resolve, normalize, extname } from "path";
3
3
  import { t } from "../i18n/index";
4
4
  import { isPathInScope } from "../modules/security/path-validator";
5
+ import { DEFAULT_SECURITY_CONFIG } from "../config/security";
6
+ import { logSecurityBlock } from "../modules/security/audit-log";
5
7
  /** Default number of lines returned when the caller omits `limit`. */
6
8
  const DEFAULT_LIMIT = 300;
7
9
  export const readFileTool = {
@@ -25,8 +27,11 @@ export const readFileTool = {
25
27
  },
26
28
  handler: async (ctx, args) => {
27
29
  const path = String(args.path);
28
- const scopeCheck = isPathInScope(ctx.baseDir, path, ctx.scope, ctx.config.security?.paths);
30
+ const securityPaths = ctx.config?.security?.paths || DEFAULT_SECURITY_CONFIG.paths;
31
+ const scopeCheck = isPathInScope(ctx.baseDir, path, ctx.scope, securityPaths);
29
32
  if (!scopeCheck.allowed) {
33
+ const pathStr = path;
34
+ logSecurityBlock(ctx.sessionId || undefined, "file_read", scopeCheck.reason || "Path not allowed", pathStr);
30
35
  return {
31
36
  success: false,
32
37
  output: t("file.path_not_allowed", {
@@ -83,6 +83,18 @@ export const subagentTool = {
83
83
  output: "Sub-agent cannot run: missing llmProvider or toolExecutor in context",
84
84
  };
85
85
  }
86
+ // Defensive guard: if the caller requested specific tool tags but none of the
87
+ // registered tools match, the sub-agent would have zero tools and would
88
+ // hallucinate instead of acting. Warn the caller so it can correct the tags.
89
+ if (toolTags && toolTags.length > 0) {
90
+ const matched = ctx.toolExecutor.getToolDefinitions(toolTags);
91
+ if (matched.length === 0) {
92
+ return {
93
+ success: false,
94
+ output: `[SECURITY BLOCKED] No tools match the requested tool_tags: [${toolTags.join(", ")}]. Check the subagent tool_tags parameter and retry with valid tags (e.g. "file", "code", "shell", "research").`,
95
+ };
96
+ }
97
+ }
86
98
  try {
87
99
  const subContextManager = new ContextManager(ctx.config.contextWindow, ctx.config.contextBudget);
88
100
  const subPluginManager = new PluginManager();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "micro-models-agent",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Micro Models Agent (MMA) — LLM agent harness for small models (Qwen3.5-9B, 32K-64K context)",
5
5
  "type": "module",
6
6
  "bin": {