@miphamai/cli 0.4.0 → 0.5.1

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 (60) hide show
  1. package/bin/mipham +51 -24
  2. package/bin/mipham.ts +186 -0
  3. package/package.json +1 -1
  4. package/src/agent/agent-context.ts +56 -0
  5. package/src/agent/agent-registry.ts +134 -0
  6. package/src/agent/sub-agent.ts +73 -88
  7. package/src/agent/types.ts +39 -0
  8. package/src/agent-view/agent-view-manager.ts +217 -0
  9. package/src/agent-view/dashboard.tsx +218 -0
  10. package/src/agent-view/session-peek.tsx +72 -0
  11. package/src/agent-view/session-row.tsx +70 -0
  12. package/src/artifacts/server.ts +31 -0
  13. package/src/artifacts/versioning.ts +127 -0
  14. package/src/config/defaults.ts +2 -1
  15. package/src/core/context-compact.ts +50 -0
  16. package/src/core/context-drain.ts +54 -0
  17. package/src/core/context-microcompact.ts +132 -0
  18. package/src/core/context-snip.ts +74 -0
  19. package/src/core/context-token.ts +108 -0
  20. package/src/core/context.ts +169 -0
  21. package/src/core/engine.ts +144 -3
  22. package/src/core/hooks-config.ts +52 -0
  23. package/src/core/hooks-executor.ts +113 -0
  24. package/src/core/hooks.ts +90 -30
  25. package/src/core/instructions.ts +11 -0
  26. package/src/core/memory/memory-loader.ts +23 -0
  27. package/src/core/memory/memory-manager.ts +192 -0
  28. package/src/core/memory/memory-writer.ts +72 -0
  29. package/src/core/permission-config.ts +34 -0
  30. package/src/core/permission-rules.ts +66 -0
  31. package/src/core/permission.ts +221 -36
  32. package/src/index.tsx +20 -0
  33. package/src/plugin/plugin-loader.ts +36 -0
  34. package/src/plugin/plugin-manager.ts +125 -0
  35. package/src/plugin/plugin-validator.ts +41 -0
  36. package/src/shared/types.ts +61 -1
  37. package/src/skills/fork-executor.ts +40 -0
  38. package/src/skills/loader.ts +46 -0
  39. package/src/tools/agent/agent.ts +20 -11
  40. package/src/tools/agent/skill.ts +32 -2
  41. package/src/tools/computer/app-launcher.ts +39 -0
  42. package/src/tools/computer/browser.ts +48 -0
  43. package/src/tools/computer/computer-use.ts +88 -0
  44. package/src/tools/computer/playwright.d.ts +7 -0
  45. package/src/tools/computer/screenshot.ts +57 -0
  46. package/src/tools/index.ts +3 -0
  47. package/src/tools/network/web-fetch.ts +1 -1
  48. package/src/ui/app.tsx +14 -3
  49. package/src/ui/commands.ts +101 -32
  50. package/src/ui/input.tsx +123 -8
  51. package/src/ui/vim-motions.ts +96 -0
  52. package/src/workflow/budget.ts +33 -0
  53. package/src/workflow/journal.ts +105 -0
  54. package/src/workflow/primitives/agent.ts +51 -0
  55. package/src/workflow/primitives/parallel.ts +8 -0
  56. package/src/workflow/primitives/phase.ts +19 -0
  57. package/src/workflow/primitives/pipeline.ts +24 -0
  58. package/src/workflow/runtime.ts +87 -0
  59. package/src/workflow/sandbox.ts +79 -0
  60. package/dist/mipham +0 -0
package/bin/mipham CHANGED
@@ -1,45 +1,73 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Mipham Code v0.2.0 — @onemipham/cli
3
+ * Mipham Code — @miphamai/cli
4
4
  * Multi-model open-core intelligent coding terminal by MiphamAI
5
5
  * License: Apache-2.0 | https://mipham.ai/code
6
+ *
7
+ * Node.js wrapper: detects Bun and re-launches with it for the full
8
+ * interactive experience. Version and package name are read from
9
+ * package.json (single source of truth).
6
10
  */
7
11
 
8
- // Detect runtime and launch accordingly
9
- const runtime = typeof Bun !== 'undefined' ? 'bun' : 'node'
12
+ import { spawnSync } from 'node:child_process'
13
+ import { readFileSync } from 'node:fs'
14
+ import { fileURLToPath } from 'node:url'
15
+ import path from 'node:path'
10
16
 
11
- if (runtime === 'bun') {
12
- // Bun runtime import TS source directly
13
- import('../src/index.tsx').then((m) => m.runApp({}))
14
- } else {
15
- // Node.js runtime — prompt to use Bun for best experience
16
- const nodeVersion = process.versions.node
17
- const [major] = nodeVersion.split('.').map(Number)
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
18
+ const pkg = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'))
19
+ const VERSION = pkg.version
20
+ const PKG_NAME = pkg.name
18
21
 
19
- if (!major || major < 22) {
20
- console.error(`
22
+ // ── Check whether Bun is available ──────────────────────────────────────────
23
+ function hasBun() {
24
+ try {
25
+ const result = spawnSync('bun', ['--version'], { stdio: 'pipe', timeout: 5000 })
26
+ return result.status === 0
27
+ } catch {
28
+ return false
29
+ }
30
+ }
31
+
32
+ // ── Bun available: re-launch with Bun for the full TUI ──────────────────────
33
+ if (hasBun()) {
34
+ const tsEntry = path.join(__dirname, 'mipham.ts')
35
+ const result = spawnSync('bun', ['run', tsEntry, ...process.argv.slice(2)], {
36
+ stdio: 'inherit',
37
+ env: process.env,
38
+ })
39
+ process.exit(result.status ?? 0)
40
+ }
41
+
42
+ // ── No Bun: show version info and install guidance ─────────────────────────
43
+ const nodeVersion = process.versions.node
44
+ const [major] = nodeVersion.split('.').map(Number)
45
+
46
+ if (!major || major < 22) {
47
+ process.stderr.write(`
21
48
  \`mipham\` requires Node.js 22+ or Bun 1.2+.
22
49
 
23
50
  Current Node.js: ${nodeVersion}
24
51
 
25
- Quick install:
26
- npm install -g bun
52
+ Quick install Bun:
53
+ curl -fsSL https://bun.sh/install | bash
54
+
55
+ Then run:
27
56
  mipham
28
57
 
29
- Or:
30
- curl -fsSL https://mipham.ai/install.sh | bash
58
+ Docs: https://mipham.ai/code/docs
31
59
  `)
32
- process.exit(1)
33
- }
60
+ process.exit(1)
61
+ }
34
62
 
35
- console.log(`
36
- \x1b[0;36m\x1b[1m✦ Mipham Code v0.2.0\x1b[0m
37
- \x1b[0;36m @onemipham/cli — Multi-model coding agent by MiphamAI\x1b[0m
63
+ process.stdout.write(`
64
+ \x1b[0;36m\x1b[1m✦ Mipham Code v${VERSION}\x1b[0m
65
+ \x1b[0;36m ${PKG_NAME} — Multi-model coding agent by MiphamAI\x1b[0m
38
66
 
39
67
  \x1b[0;33m⚠ Bun runtime is recommended for the best Mipham Code experience.\x1b[0m
40
68
 
41
69
  Quick install Bun:
42
- \x1b[0;32mnpm install -g bun\x1b[0m
70
+ \x1b[0;32mcurl -fsSL https://bun.sh/install | bash\x1b[0m
43
71
 
44
72
  Then run:
45
73
  \x1b[0;32mmipham\x1b[0m
@@ -47,5 +75,4 @@ Then run:
47
75
  Install: \x1b[0;34mhttps://mipham.ai/code/install\x1b[0m
48
76
  Docs: \x1b[0;34mhttps://mipham.ai/code/docs\x1b[0m
49
77
  `)
50
- process.exit(0)
51
- }
78
+ process.exit(0)
package/bin/mipham.ts CHANGED
@@ -6,7 +6,193 @@
6
6
 
7
7
  export {} // ensure module scope (prevents global name collisions)
8
8
 
9
+ async function runWorkflowCLI(): Promise<boolean> {
10
+ const args = process.argv.slice(2)
11
+ if (args[0] !== 'workflow') return false
12
+
13
+ const { Command } = await import('commander')
14
+ const program = new Command()
15
+
16
+ program.name('mipham workflow').description('Workflow orchestration commands')
17
+
18
+ program
19
+ .command('run <script>')
20
+ .description('Run a workflow script')
21
+ .option('--args <json>', 'JSON arguments for the workflow')
22
+ .action(async (scriptPath: string, opts: { args?: string }) => {
23
+ const { readFileSync } = await import('node:fs')
24
+ const { runWorkflow } = await import('../src/workflow/runtime')
25
+ const { loadConfig } = await import('../src/config/loader')
26
+ const { bootstrapProviders } = await import('../src/providers/bootstrap')
27
+ const { ContextManager } = await import('../src/core/context')
28
+ const { QueryEngine } = await import('../src/core/engine')
29
+ const { createToolRegistry } = await import('../src/tools')
30
+
31
+ const script = readFileSync(scriptPath, 'utf-8')
32
+ const workflowArgs = opts.args ? JSON.parse(opts.args) : {}
33
+
34
+ // Bootstrap minimal engine
35
+ const config = loadConfig()
36
+ const registry = bootstrapProviders(
37
+ config.providers,
38
+ config.defaultProvider,
39
+ config.defaultModel,
40
+ )
41
+ const context = new ContextManager({ maxTokens: 200_000, compactionThreshold: 0.9 })
42
+ const tools = createToolRegistry()
43
+ const engine = new QueryEngine(registry, context, tools)
44
+ engine.setupContextSummarizer()
45
+
46
+ const result = await runWorkflow(script, engine, workflowArgs)
47
+ console.log(JSON.stringify(result, null, 2))
48
+ process.exit(0)
49
+ })
50
+
51
+ program
52
+ .command('list')
53
+ .description('List all workflow runs')
54
+ .action(async () => {
55
+ const { listRuns } = await import('../src/workflow/journal')
56
+ const runs = listRuns()
57
+ if (runs.length === 0) {
58
+ console.log('No workflow runs found.')
59
+ } else {
60
+ runs.forEach((r) => console.log(r))
61
+ }
62
+ process.exit(0)
63
+ })
64
+
65
+ program
66
+ .command('resume <runId>')
67
+ .description('Resume a paused workflow')
68
+ .action(async (runId: string) => {
69
+ const { loadJournal } = await import('../src/workflow/journal')
70
+ const entries = loadJournal(runId)
71
+ if (entries.length === 0) {
72
+ console.log(`No journal found for run: ${runId}`)
73
+ } else {
74
+ console.log(`Resuming workflow ${runId} with ${entries.length} journal entries...`)
75
+ // Replay completed agents, continue from last state
76
+ }
77
+ process.exit(0)
78
+ })
79
+
80
+ program
81
+ .command('stop <runId>')
82
+ .description('Stop a running workflow')
83
+ .action(async (runId: string) => {
84
+ console.log(`Stopping workflow ${runId}...`)
85
+ // Mark workflow as stopped in state
86
+ process.exit(0)
87
+ })
88
+
89
+ // Commander will handle --help, unknown commands, etc.
90
+ await program.parseAsync(process.argv)
91
+ return true
92
+ }
93
+
94
+ async function runPluginCLI(): Promise<boolean> {
95
+ const args = process.argv.slice(2)
96
+ if (args[0] !== 'plugin') return false
97
+
98
+ const { Command } = await import('commander')
99
+ const program = new Command()
100
+
101
+ program.name('mipham plugin').description('Plugin management commands')
102
+
103
+ program
104
+ .command('install <path>')
105
+ .description('Install a plugin from a directory path')
106
+ .action(async (sourcePath: string) => {
107
+ const { PluginManager } = await import('../src/plugin/plugin-manager')
108
+ const manager = new PluginManager()
109
+ const result = manager.install(sourcePath)
110
+ console.log(result.message)
111
+ process.exit(result.success ? 0 : 1)
112
+ })
113
+
114
+ program
115
+ .command('list')
116
+ .description('List installed plugins')
117
+ .action(async () => {
118
+ const { PluginManager } = await import('../src/plugin/plugin-manager')
119
+ const manager = new PluginManager()
120
+ const plugins = manager.list()
121
+ if (plugins.length === 0) {
122
+ console.log('No plugins installed.')
123
+ } else {
124
+ for (const p of plugins) {
125
+ const status = p.enabled ? 'enabled' : 'disabled'
126
+ console.log(`${p.name} v${p.version} [${status}] — ${p.installedAt}`)
127
+ }
128
+ }
129
+ process.exit(0)
130
+ })
131
+
132
+ program
133
+ .command('remove <name>')
134
+ .description('Remove an installed plugin')
135
+ .action(async (name: string) => {
136
+ const { PluginManager } = await import('../src/plugin/plugin-manager')
137
+ const manager = new PluginManager()
138
+ const removed = manager.remove(name)
139
+ if (removed) {
140
+ console.log(`Plugin "${name}" removed.`)
141
+ } else {
142
+ console.log(`Plugin "${name}" not found.`)
143
+ }
144
+ process.exit(removed ? 0 : 1)
145
+ })
146
+
147
+ program
148
+ .command('enable <name>')
149
+ .description('Enable a disabled plugin')
150
+ .action(async (name: string) => {
151
+ const { PluginManager } = await import('../src/plugin/plugin-manager')
152
+ const manager = new PluginManager()
153
+ const enabled = manager.enable(name)
154
+ if (enabled) {
155
+ console.log(`Plugin "${name}" enabled.`)
156
+ } else {
157
+ console.log(`Plugin "${name}" not found.`)
158
+ }
159
+ process.exit(enabled ? 0 : 1)
160
+ })
161
+
162
+ program
163
+ .command('disable <name>')
164
+ .description('Disable an enabled plugin')
165
+ .action(async (name: string) => {
166
+ const { PluginManager } = await import('../src/plugin/plugin-manager')
167
+ const manager = new PluginManager()
168
+ const disabled = manager.disable(name)
169
+ if (disabled) {
170
+ console.log(`Plugin "${name}" disabled.`)
171
+ } else {
172
+ console.log(`Plugin "${name}" not found.`)
173
+ }
174
+ process.exit(disabled ? 0 : 1)
175
+ })
176
+
177
+ await program.parseAsync(process.argv)
178
+ return true
179
+ }
180
+
9
181
  async function main() {
182
+ // Check for plugin subcommands first
183
+ const handledPlugin = await runPluginCLI()
184
+ if (handledPlugin) return
185
+
186
+ // Check for workflow subcommands next
187
+ const handled = await runWorkflowCLI()
188
+ if (handled) return
189
+
190
+ // Parse --safe-mode flag: skip custom agents, skills, hooks, plugins
191
+ const safeModeFlag = process.argv.includes('--safe-mode')
192
+ if (safeModeFlag) {
193
+ process.env.MIPHAM_SAFE_MODE = '1'
194
+ }
195
+
10
196
  try {
11
197
  const { runApp } = await import('../src/index')
12
198
  await runApp({})
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miphamai/cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
@@ -0,0 +1,56 @@
1
+ // apps/cli/src/agent/agent-context.ts
2
+ import { ContextManager } from '../core/context'
3
+ import type { ToolDefinition } from '../shared/index.ts'
4
+ import type { AgentDefinition } from './types'
5
+
6
+ export interface AgentContextResult {
7
+ context: ContextManager
8
+ allowedTools: ToolDefinition[]
9
+ }
10
+
11
+ /**
12
+ * Create an isolated context and tool set for a sub-agent.
13
+ *
14
+ * Tool scoping rules (first match wins):
15
+ * 1. If `tools` is set, only those tools are allowed.
16
+ * 2. If `disallowedTools` is set, those are removed from the full set.
17
+ * 3. If neither is set, all tools are available.
18
+ */
19
+ export function createAgentContext(
20
+ agentDef: AgentDefinition,
21
+ toolRegistry: Map<string, ToolDefinition>,
22
+ contextWindow?: number,
23
+ ): AgentContextResult {
24
+ // Create isolated context
25
+ const context = new ContextManager({
26
+ maxTokens: contextWindow || 100_000,
27
+ compactionThreshold: 0.85,
28
+ })
29
+
30
+ context.setSystemPrompt(agentDef.systemPrompt)
31
+
32
+ // Scope tools
33
+ let allowedTools = Array.from(toolRegistry.values())
34
+
35
+ if (agentDef.tools) {
36
+ const allowSet = new Set(
37
+ agentDef.tools
38
+ .split(',')
39
+ .map((s) => s.trim())
40
+ .filter(Boolean),
41
+ )
42
+ allowedTools = allowedTools.filter((t) => allowSet.has(t.name))
43
+ }
44
+
45
+ if (agentDef.disallowedTools) {
46
+ const denySet = new Set(
47
+ agentDef.disallowedTools
48
+ .split(',')
49
+ .map((s) => s.trim())
50
+ .filter(Boolean),
51
+ )
52
+ allowedTools = allowedTools.filter((t) => !denySet.has(t.name))
53
+ }
54
+
55
+ return { context, allowedTools }
56
+ }
@@ -0,0 +1,134 @@
1
+ import { readFileSync, existsSync, readdirSync } from 'node:fs'
2
+ import { homedir } from 'node:os'
3
+ import { join, extname } from 'node:path'
4
+ import { parse as parseYaml } from 'yaml'
5
+ import type { AgentDefinition, SubAgentType } from './types'
6
+
7
+ interface FrontmatterResult {
8
+ data: Record<string, unknown>
9
+ content: string
10
+ }
11
+
12
+ function parseFrontmatter(raw: string): FrontmatterResult {
13
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/)
14
+ if (!match) {
15
+ return { data: {}, content: raw }
16
+ }
17
+ return {
18
+ data: parseYaml(match[1] || '') as Record<string, unknown>,
19
+ content: (match[2] || '').trim(),
20
+ }
21
+ }
22
+
23
+ const BUILTIN_SYSTEM_PROMPTS: Record<SubAgentType, string> = {
24
+ general: 'You are a focused sub-agent. Complete the assigned task thoroughly and return results.',
25
+ explore:
26
+ 'You are an exploration sub-agent. Search, read, and analyze code. Return structured findings with file paths and line numbers.',
27
+ plan: 'You are a planning sub-agent. Design implementation approaches. Return a step-by-step plan with files to modify.',
28
+ 'code-review':
29
+ 'You are a code review sub-agent. Find bugs, security issues, and code quality problems. Return findings by severity.',
30
+ }
31
+
32
+ const BUILTIN_DESCRIPTIONS: Record<SubAgentType, string> = {
33
+ general: 'General-purpose agent for complex multi-step tasks.',
34
+ explore: 'Read-only search agent for broad fan-out searches across files.',
35
+ plan: 'Software architect agent for designing implementation plans.',
36
+ 'code-review': 'Code review agent for finding bugs and quality issues.',
37
+ }
38
+
39
+ export class AgentRegistry {
40
+ private agents = new Map<string, AgentDefinition>()
41
+
42
+ /** Load agents from a directory. Later loads override earlier ones for same name. */
43
+ loadDirectory(dir: string, source: 'project' | 'user'): void {
44
+ if (!existsSync(dir)) return
45
+
46
+ let entries: string[] = []
47
+ try {
48
+ entries = readdirSync(dir)
49
+ } catch {
50
+ // Permission errors, missing dir, etc.
51
+ return
52
+ }
53
+
54
+ for (const entry of entries) {
55
+ if (extname(entry) !== '.md') continue
56
+ const fullPath = join(dir, entry)
57
+ try {
58
+ const raw = readFileSync(fullPath, 'utf-8')
59
+ const { data, content } = parseFrontmatter(raw)
60
+
61
+ const name = (data.name as string) || entry.replace(/\.md$/, '')
62
+ const def: AgentDefinition = {
63
+ name,
64
+ description: (data.description as string) || '',
65
+ systemPrompt: content,
66
+ tools: data.tools as string | undefined,
67
+ disallowedTools: data.disallowedTools as string | undefined,
68
+ model: (data.model as string) || 'inherit',
69
+ permissionMode: (data.permissionMode as string) || 'inherit',
70
+ maxTurns: data.maxTurns as number | undefined,
71
+ skills: data.skills
72
+ ? String(data.skills)
73
+ .split(',')
74
+ .map((s) => s.trim())
75
+ : undefined,
76
+ background: (data.background as boolean) || false,
77
+ source,
78
+ filePath: fullPath,
79
+ }
80
+
81
+ this.agents.set(name, def)
82
+ } catch {
83
+ // Skip unparseable files
84
+ }
85
+ }
86
+ }
87
+
88
+ /** Load project-level agents from .mipham/agents/ */
89
+ loadProjectAgents(cwd: string): void {
90
+ this.loadDirectory(join(cwd, '.mipham', 'agents'), 'project')
91
+ }
92
+
93
+ /** Load user-level agents from ~/.mipham/agents/ */
94
+ loadUserAgents(): void {
95
+ const home = homedir()
96
+ this.loadDirectory(join(home, '.mipham', 'agents'), 'user')
97
+ }
98
+
99
+ /** Get a custom agent by name. Returns undefined for builtins. */
100
+ get(name: string): AgentDefinition | undefined {
101
+ return this.agents.get(name)
102
+ }
103
+
104
+ /** List all custom agents. */
105
+ list(): AgentDefinition[] {
106
+ return Array.from(this.agents.values())
107
+ }
108
+
109
+ /**
110
+ * Resolve an agent name to its definition.
111
+ * Priority: custom (project) > custom (user) > builtin.
112
+ * Builtins are always available and never return undefined.
113
+ */
114
+ resolve(name: string): AgentDefinition | undefined {
115
+ const custom = this.agents.get(name)
116
+ if (custom) return custom
117
+
118
+ // Check if it's a builtin type
119
+ const builtinType = name as SubAgentType
120
+ if (BUILTIN_SYSTEM_PROMPTS[builtinType]) {
121
+ return {
122
+ name: builtinType,
123
+ description: BUILTIN_DESCRIPTIONS[builtinType],
124
+ systemPrompt: BUILTIN_SYSTEM_PROMPTS[builtinType],
125
+ model: 'inherit',
126
+ permissionMode: 'inherit',
127
+ background: false,
128
+ source: 'builtin',
129
+ }
130
+ }
131
+
132
+ return undefined
133
+ }
134
+ }
@@ -1,13 +1,7 @@
1
- import { ContextManager } from '../core/context'
2
- import type { ProviderRegistry } from '../providers/registry'
3
-
4
- export type SubAgentType = 'general' | 'explore' | 'plan' | 'code-review'
5
-
6
- interface SubAgentOptions {
7
- type?: SubAgentType
8
- systemPrompt?: string
9
- maxContextMessages?: number
10
- }
1
+ import type { ProviderRegistry, ProviderInstance } from '../providers/registry'
2
+ import type { ToolDefinition } from '../shared/index.ts'
3
+ import type { SubAgentType, SubAgentOptions, AgentDefinition } from './types'
4
+ import { createAgentContext } from './agent-context'
11
5
 
12
6
  const TYPE_SYSTEM_PROMPTS: Record<SubAgentType, string> = {
13
7
  general: 'You are a focused sub-agent. Complete the assigned task thoroughly and return results.',
@@ -20,102 +14,93 @@ const TYPE_SYSTEM_PROMPTS: Record<SubAgentType, string> = {
20
14
 
21
15
  /**
22
16
  * Sub-agent engine — creates an isolated conversation context and processes
23
- * a single prompt independently. Returns the consolidated result.
24
- *
25
- * Phase 7: Pipeline-ready. Without a real AI provider, returns a structured
26
- * analysis of the prompt. Full AI integration in M3.
17
+ * a single prompt independently via the active AI provider. Returns the
18
+ * consolidated result text.
27
19
  */
28
20
  export class SubAgent {
29
- constructor(private registry: ProviderRegistry) {}
21
+ constructor(
22
+ private registry: ProviderRegistry,
23
+ private toolRegistry: Map<string, ToolDefinition>,
24
+ ) {}
30
25
 
31
26
  async execute(
32
27
  prompt: string,
33
28
  description: string,
34
29
  options: SubAgentOptions = {},
35
30
  ): Promise<string> {
31
+ const provider = this.registry.getActive()
32
+ if (!provider) {
33
+ throw new Error('No active provider available for sub-agent execution')
34
+ }
35
+
36
+ const model = this.registry.getActiveModel()
36
37
  const type = options.type || 'general'
37
- const systemPrompt = options.systemPrompt || TYPE_SYSTEM_PROMPTS[type]
38
+ const agentDef = options.agentDef
39
+
40
+ // Resolve system prompt: agentDef > options.systemPrompt > builtin type
41
+ const systemPrompt = agentDef?.systemPrompt || options.systemPrompt || TYPE_SYSTEM_PROMPTS[type]
42
+
43
+ // Create isolated context with tool scoping
44
+ const resolvedDef: AgentDefinition = agentDef || {
45
+ name: type,
46
+ description: '',
47
+ systemPrompt,
48
+ model: options.modelOverride || 'inherit',
49
+ permissionMode: 'inherit',
50
+ background: false,
51
+ source: 'builtin',
52
+ }
53
+ const { context, allowedTools } = createAgentContext(
54
+ resolvedDef,
55
+ this.toolRegistry,
56
+ options.maxContextMessages,
57
+ )
38
58
 
39
- // Create isolated context for this sub-agent
40
- const context = new ContextManager({
41
- maxTokens: 100_000,
42
- compactionThreshold: 0.9,
43
- })
44
59
  context.setSystemPrompt(systemPrompt)
45
60
  context.addMessage({ role: 'user', content: prompt })
46
61
 
47
- // Phase 7 pipeline: attempt real AI, fall back to structured simulation
48
- try {
49
- const provider = this.registry.getActive()
50
- if (provider) {
51
- const messages = context.getMessages()
52
- const chunks: string[] = []
62
+ const messages = context.getMessages()
63
+ const toolDefs =
64
+ allowedTools.length > 0
65
+ ? allowedTools.map((t) => ({
66
+ name: t.name,
67
+ description: t.description,
68
+ parameters: t.parameters,
69
+ input_schema: t.parameters,
70
+ }))
71
+ : undefined
53
72
 
54
- for await (const chunk of provider.chat({
55
- model: this.registry.getActiveModel(),
56
- messages,
57
- systemPrompt,
58
- maxTokens: 4096,
59
- })) {
60
- if (chunk.type === 'text' && chunk.content) {
61
- chunks.push(chunk.content)
62
- }
63
- if (chunk.type === 'error') {
64
- // Fall through to simulation on API error
65
- throw new Error(chunk.error)
66
- }
67
- }
73
+ const modelToUse = options.modelOverride || agentDef?.model || model
74
+ // 'inherit' means use parent model
75
+ const resolvedModel = modelToUse === 'inherit' ? model : modelToUse
68
76
 
69
- if (chunks.length > 0) {
70
- return chunks.join('')
77
+ const chunks: string[] = []
78
+
79
+ try {
80
+ for await (const chunk of provider.chat({
81
+ model: resolvedModel,
82
+ messages,
83
+ systemPrompt,
84
+ tools: toolDefs,
85
+ maxTokens: 4096,
86
+ })) {
87
+ if (chunk.type === 'text' && chunk.content) {
88
+ chunks.push(chunk.content)
89
+ }
90
+ if (chunk.type === 'error') {
91
+ throw new Error(`Sub-agent execution failed: ${chunk.error}`)
92
+ }
93
+ if (chunk.type === 'stop') {
94
+ break
71
95
  }
72
96
  }
73
- } catch {
74
- // API unavailable use simulation mode
75
- }
76
-
77
- // Simulation mode: return structured task analysis
78
- return this.simulate(prompt, description, type)
79
- }
80
-
81
- private simulate(prompt: string, description: string, type: SubAgentType): string {
82
- const lines: string[] = [
83
- `── Sub-Agent Result (${type}) ──`,
84
- '',
85
- `Task: ${description}`,
86
- `Prompt: ${prompt.slice(0, 200)}${prompt.length > 200 ? '...' : ''}`,
87
- '',
88
- ]
89
-
90
- switch (type) {
91
- case 'explore':
92
- lines.push(
93
- 'Analysis: This exploration task would search the codebase for relevant',
94
- 'files, patterns, and structures. The sub-agent pipeline is ready — connect',
95
- 'a provider API key for real AI-powered exploration.',
96
- )
97
- break
98
- case 'plan':
99
- lines.push(
100
- 'Plan: A step-by-step implementation plan would be generated based on',
101
- 'the requirements. Use /plan mode for interactive planning.',
102
- )
103
- break
104
- case 'code-review':
105
- lines.push(
106
- 'Review: The code review sub-agent would analyze the specified files for',
107
- 'bugs, security issues, and code quality concerns. Use /review for',
108
- 'interactive code review.',
109
- )
110
- break
111
- default:
112
- lines.push(
113
- 'The sub-agent pipeline is operational. Connect an API key to enable',
114
- 'AI-powered sub-agent execution. The infrastructure for context isolation,',
115
- 'prompt routing, and result aggregation is in place.',
116
- )
97
+ } catch (err) {
98
+ if (err instanceof Error && err.message.startsWith('Sub-agent')) {
99
+ throw err
100
+ }
101
+ throw new Error(`Sub-agent execution failed: ${String(err)}`)
117
102
  }
118
103
 
119
- return lines.join('\n')
104
+ return chunks.join('')
120
105
  }
121
106
  }