@miphamai/cli 0.4.0 → 0.5.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.
- package/bin/mipham.ts +186 -0
- package/package.json +1 -1
- package/src/agent/agent-context.ts +56 -0
- package/src/agent/agent-registry.ts +134 -0
- package/src/agent/sub-agent.ts +73 -88
- package/src/agent/types.ts +39 -0
- package/src/agent-view/agent-view-manager.ts +217 -0
- package/src/agent-view/dashboard.tsx +218 -0
- package/src/agent-view/session-peek.tsx +72 -0
- package/src/agent-view/session-row.tsx +70 -0
- package/src/artifacts/server.ts +31 -0
- package/src/artifacts/versioning.ts +127 -0
- package/src/core/context-compact.ts +50 -0
- package/src/core/context-drain.ts +54 -0
- package/src/core/context-microcompact.ts +132 -0
- package/src/core/context-snip.ts +74 -0
- package/src/core/context-token.ts +108 -0
- package/src/core/context.ts +169 -0
- package/src/core/engine.ts +144 -3
- package/src/core/hooks-config.ts +52 -0
- package/src/core/hooks-executor.ts +113 -0
- package/src/core/hooks.ts +90 -30
- package/src/core/instructions.ts +11 -0
- package/src/core/memory/memory-loader.ts +23 -0
- package/src/core/memory/memory-manager.ts +192 -0
- package/src/core/memory/memory-writer.ts +72 -0
- package/src/core/permission-config.ts +34 -0
- package/src/core/permission-rules.ts +66 -0
- package/src/core/permission.ts +221 -36
- package/src/index.tsx +20 -0
- package/src/plugin/plugin-loader.ts +36 -0
- package/src/plugin/plugin-manager.ts +125 -0
- package/src/plugin/plugin-validator.ts +41 -0
- package/src/shared/types.ts +61 -1
- package/src/skills/fork-executor.ts +40 -0
- package/src/skills/loader.ts +46 -0
- package/src/tools/agent/agent.ts +20 -11
- package/src/tools/agent/skill.ts +32 -2
- package/src/tools/computer/app-launcher.ts +39 -0
- package/src/tools/computer/browser.ts +48 -0
- package/src/tools/computer/computer-use.ts +88 -0
- package/src/tools/computer/playwright.d.ts +7 -0
- package/src/tools/computer/screenshot.ts +57 -0
- package/src/tools/index.ts +3 -0
- package/src/ui/app.tsx +10 -1
- package/src/ui/commands.ts +99 -30
- package/src/ui/input.tsx +123 -8
- package/src/ui/vim-motions.ts +96 -0
- package/src/workflow/budget.ts +33 -0
- package/src/workflow/journal.ts +105 -0
- package/src/workflow/primitives/agent.ts +51 -0
- package/src/workflow/primitives/parallel.ts +8 -0
- package/src/workflow/primitives/phase.ts +19 -0
- package/src/workflow/primitives/pipeline.ts +24 -0
- package/src/workflow/runtime.ts +87 -0
- package/src/workflow/sandbox.ts +75 -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
|
@@ -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
|
+
}
|
package/src/agent/sub-agent.ts
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
104
|
+
return chunks.join('')
|
|
120
105
|
}
|
|
121
106
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// apps/cli/src/agent/types.ts
|
|
2
|
+
|
|
3
|
+
export type SubAgentType = 'general' | 'explore' | 'plan' | 'code-review'
|
|
4
|
+
|
|
5
|
+
export interface AgentFrontmatter {
|
|
6
|
+
name: string
|
|
7
|
+
description: string
|
|
8
|
+
tools?: string // comma-separated allowlist
|
|
9
|
+
disallowedTools?: string
|
|
10
|
+
model?: string // 'sonnet' | 'opus' | 'haiku' | 'inherit' | full model ID
|
|
11
|
+
permissionMode?: 'default' | 'acceptEdits' | 'auto' | 'bypass' | 'plan'
|
|
12
|
+
maxTurns?: number
|
|
13
|
+
skills?: string
|
|
14
|
+
background?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AgentDefinition {
|
|
18
|
+
name: string
|
|
19
|
+
description: string
|
|
20
|
+
systemPrompt: string // markdown body after frontmatter
|
|
21
|
+
tools?: string
|
|
22
|
+
disallowedTools?: string
|
|
23
|
+
model: string
|
|
24
|
+
permissionMode: string
|
|
25
|
+
maxTurns?: number
|
|
26
|
+
skills?: string[]
|
|
27
|
+
background: boolean
|
|
28
|
+
source: 'builtin' | 'project' | 'user'
|
|
29
|
+
filePath?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SubAgentOptions {
|
|
33
|
+
type?: SubAgentType
|
|
34
|
+
agentDef?: AgentDefinition
|
|
35
|
+
systemPrompt?: string
|
|
36
|
+
maxContextMessages?: number
|
|
37
|
+
allowedTools?: string[]
|
|
38
|
+
modelOverride?: string
|
|
39
|
+
}
|