@miphamai/cli 0.9.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.
- package/dist/mipham +0 -0
- package/package.json +1 -1
- package/src/agent/background-registry.ts +192 -0
- package/src/agent/message-bus.ts +113 -0
- package/src/agent/sub-agent.ts +101 -3
- package/src/agent/types.ts +4 -0
- package/src/agent-view/agent-view-manager.ts +10 -0
- package/src/agent-view/dashboard.tsx +20 -1
- package/src/core/engine.ts +52 -0
- package/src/core/hooks.ts +48 -0
- package/src/shared/types.ts +16 -1
- package/src/tools/agent/agent.ts +41 -2
- package/src/tools/agent/enter-plan.ts +92 -0
- package/src/tools/agent/exit-plan.ts +54 -0
- package/src/tools/agent/report-findings.ts +186 -0
- package/src/tools/agent/send-message.ts +67 -0
- package/src/tools/agent/workflow.ts +25 -5
- package/src/tools/exec/enter-worktree.ts +142 -0
- package/src/tools/exec/exit-worktree.ts +162 -0
- package/src/tools/exec/task.ts +215 -22
- package/src/tools/index.ts +12 -0
- package/src/ui/app.tsx +49 -19
- package/src/ui/commands.ts +10 -12
- package/src/workflow/primitives/agent.ts +77 -15
- package/src/workflow/runtime.ts +84 -7
- package/src/workflow/schema-validator.ts +162 -0
package/src/shared/types.ts
CHANGED
|
@@ -80,6 +80,7 @@ export interface ToolContext {
|
|
|
80
80
|
toolRegistry?: Map<string, ToolDefinition>
|
|
81
81
|
artifactServer?: import('../artifacts/server').ArtifactServer
|
|
82
82
|
agentRegistry?: import('../agent/agent-registry').AgentRegistry
|
|
83
|
+
backgroundAgentRegistry?: import('../agent/background-registry').BackgroundAgentRegistry
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
// ── Artifact Types ──
|
|
@@ -116,9 +117,18 @@ export interface ToolDefinition {
|
|
|
116
117
|
execute: (params: Record<string, unknown>, ctx: ToolContext) => Promise<ToolResult>
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
// ── Task Notification Types ──
|
|
121
|
+
export interface TaskNotification {
|
|
122
|
+
taskId: string
|
|
123
|
+
status: 'started' | 'completed' | 'failed'
|
|
124
|
+
description: string
|
|
125
|
+
content?: string
|
|
126
|
+
error?: string
|
|
127
|
+
}
|
|
128
|
+
|
|
119
129
|
// ── Stream Types ──
|
|
120
130
|
export interface StreamChunk {
|
|
121
|
-
type: 'text' | 'tool_use' | 'tool_result' | 'thinking' | 'stop' | 'error'
|
|
131
|
+
type: 'text' | 'tool_use' | 'tool_result' | 'thinking' | 'stop' | 'error' | 'task_notification'
|
|
122
132
|
content?: string
|
|
123
133
|
toolUse?: ToolUseContent
|
|
124
134
|
tool_use_id?: string
|
|
@@ -127,6 +137,8 @@ export interface StreamChunk {
|
|
|
127
137
|
reasoning_content?: string
|
|
128
138
|
/** Anthropic thinking block content (DeepSeek Anthropic endpoint). */
|
|
129
139
|
thinking?: string
|
|
140
|
+
/** Background task notification payload (type: 'task_notification'). */
|
|
141
|
+
taskNotification?: TaskNotification
|
|
130
142
|
}
|
|
131
143
|
|
|
132
144
|
// ── Config Types ──
|
|
@@ -173,6 +185,7 @@ export interface SkillDefinition {
|
|
|
173
185
|
export type HookEvent =
|
|
174
186
|
| 'PreToolUse'
|
|
175
187
|
| 'PostToolUse'
|
|
188
|
+
| 'PostToolUseFailure'
|
|
176
189
|
| 'SessionStart'
|
|
177
190
|
| 'SessionEnd'
|
|
178
191
|
| 'Notification'
|
|
@@ -181,6 +194,8 @@ export type HookEvent =
|
|
|
181
194
|
| 'PreCompact'
|
|
182
195
|
| 'PostCompact'
|
|
183
196
|
| 'ConfigChange'
|
|
197
|
+
| 'SubagentStart'
|
|
198
|
+
| 'SubagentStop'
|
|
184
199
|
|
|
185
200
|
export type HookType = 'command' | 'http' | 'code' | 'mcp_tool'
|
|
186
201
|
|
package/src/tools/agent/agent.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import type { ToolDefinition } from '../../shared/index.ts'
|
|
2
2
|
import { SubAgent } from '../../agent/sub-agent'
|
|
3
3
|
import type { SubAgentType } from '../../agent/types'
|
|
4
|
+
import { getBackgroundAgentRegistry } from '../../agent/background-registry'
|
|
4
5
|
|
|
5
6
|
const VALID_TYPES: SubAgentType[] = ['general', 'explore', 'plan', 'code-review']
|
|
6
7
|
|
|
7
8
|
export const agentTool: ToolDefinition = {
|
|
8
9
|
name: 'Agent',
|
|
9
10
|
description:
|
|
10
|
-
'Launch a sub-agent to handle complex, multi-step tasks independently.
|
|
11
|
+
'Launch a sub-agent to handle complex, multi-step tasks independently. ' +
|
|
12
|
+
'Available types: general (default), explore (code search), plan (design), code-review. ' +
|
|
13
|
+
'Set run_in_background: true to execute asynchronously — returns a task ID immediately; ' +
|
|
14
|
+
'results are retrievable via the Task tool (output action) or Agent View.',
|
|
11
15
|
category: 'agent',
|
|
12
16
|
permission: 'ask',
|
|
13
17
|
parameters: {
|
|
@@ -19,6 +23,11 @@ export const agentTool: ToolDefinition = {
|
|
|
19
23
|
type: 'string',
|
|
20
24
|
description: 'Type: general (default), explore (code search), plan (design), code-review',
|
|
21
25
|
},
|
|
26
|
+
run_in_background: {
|
|
27
|
+
type: 'boolean',
|
|
28
|
+
description:
|
|
29
|
+
'When true, execute asynchronously and return a task ID immediately. Use Task output to retrieve results. Default: false.',
|
|
30
|
+
},
|
|
22
31
|
},
|
|
23
32
|
required: ['description', 'prompt'],
|
|
24
33
|
},
|
|
@@ -26,6 +35,7 @@ export const agentTool: ToolDefinition = {
|
|
|
26
35
|
const description = params.description as string
|
|
27
36
|
const prompt = params.prompt as string
|
|
28
37
|
const agentType = (params.subagent_type as SubAgentType) || 'general'
|
|
38
|
+
const runInBackground = params.run_in_background === true
|
|
29
39
|
|
|
30
40
|
if (!VALID_TYPES.includes(agentType)) {
|
|
31
41
|
return {
|
|
@@ -51,7 +61,36 @@ export const agentTool: ToolDefinition = {
|
|
|
51
61
|
|
|
52
62
|
try {
|
|
53
63
|
const sub = new SubAgent(registry, toolRegistry)
|
|
54
|
-
const result = await sub.execute(prompt, description, {
|
|
64
|
+
const result = await sub.execute(prompt, description, {
|
|
65
|
+
type: agentType,
|
|
66
|
+
agentDef,
|
|
67
|
+
runInBackground,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// If background execution, also register in the task system for Task tool integration
|
|
71
|
+
if (runInBackground) {
|
|
72
|
+
const bgMatch = result.match(/\[background-task:(.+?)\]/)
|
|
73
|
+
if (bgMatch) {
|
|
74
|
+
const bgTaskId = bgMatch[1]!
|
|
75
|
+
const bgRegistry = getBackgroundAgentRegistry()
|
|
76
|
+
const bgTask = bgRegistry.get(bgTaskId)
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
success: true,
|
|
80
|
+
content:
|
|
81
|
+
`── Background Agent Started ──\n\n` +
|
|
82
|
+
`Task ID: ${bgTaskId}\n` +
|
|
83
|
+
`Type: ${agentType}\n` +
|
|
84
|
+
`Task: ${description}\n` +
|
|
85
|
+
`Status: ${bgTask?.status || 'running'}\n\n` +
|
|
86
|
+
`The agent is running in the background. You can continue working.\n` +
|
|
87
|
+
`Use Task output taskId="${bgTaskId}" to check results.\n` +
|
|
88
|
+
`Use Task stop taskId="${bgTaskId}" to cancel.\n` +
|
|
89
|
+
`Use /agents to view in Agent View dashboard.`,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
55
94
|
return { success: true, content: result }
|
|
56
95
|
} catch (err) {
|
|
57
96
|
return { success: false, content: '', error: String(err) }
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import type { ToolDefinition } from '../../shared/index.ts'
|
|
4
|
+
|
|
5
|
+
export const enterPlanModeTool: ToolDefinition = {
|
|
6
|
+
name: 'EnterPlanMode',
|
|
7
|
+
description:
|
|
8
|
+
'Enter plan mode — a read-only mode for analysis and design. ' +
|
|
9
|
+
'In plan mode, only Read/Grep/Glob tools are auto-approved; all other tools require confirmation. ' +
|
|
10
|
+
'Use this before writing code to design an implementation approach, ' +
|
|
11
|
+
'explore the codebase, and get user approval before executing changes.',
|
|
12
|
+
category: 'agent',
|
|
13
|
+
permission: 'auto',
|
|
14
|
+
parameters: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
description: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description:
|
|
20
|
+
'Brief description of what you are planning (e.g., "Add user authentication flow")',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
required: [],
|
|
24
|
+
},
|
|
25
|
+
async execute(params, ctx) {
|
|
26
|
+
const description = (params.description as string) || 'Implementation Plan'
|
|
27
|
+
|
|
28
|
+
// Create plan file
|
|
29
|
+
const planDir = join(ctx.cwd, '.mipham', 'plans')
|
|
30
|
+
mkdirSync(planDir, { recursive: true })
|
|
31
|
+
|
|
32
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
|
33
|
+
const filename = `plan-${timestamp}.md`
|
|
34
|
+
const filepath = join(planDir, filename)
|
|
35
|
+
|
|
36
|
+
const content = [
|
|
37
|
+
`# ${description}`,
|
|
38
|
+
'',
|
|
39
|
+
`> Generated: ${new Date().toISOString()}`,
|
|
40
|
+
`> Status: Draft`,
|
|
41
|
+
'',
|
|
42
|
+
'## Overview',
|
|
43
|
+
'',
|
|
44
|
+
'[Describe what this plan aims to achieve]',
|
|
45
|
+
'',
|
|
46
|
+
'## Files to Modify',
|
|
47
|
+
'',
|
|
48
|
+
'| File | Change | Reason |',
|
|
49
|
+
'|------|--------|--------|',
|
|
50
|
+
'| | | |',
|
|
51
|
+
'',
|
|
52
|
+
'## Implementation Steps',
|
|
53
|
+
'',
|
|
54
|
+
'1. ',
|
|
55
|
+
'2. ',
|
|
56
|
+
'3. ',
|
|
57
|
+
'',
|
|
58
|
+
'## Verification',
|
|
59
|
+
'',
|
|
60
|
+
'- [ ] Tests pass',
|
|
61
|
+
'- [ ] Typecheck passes',
|
|
62
|
+
'- [ ] Manual verification',
|
|
63
|
+
'',
|
|
64
|
+
'## Notes',
|
|
65
|
+
'',
|
|
66
|
+
'[Any additional context, risks, or dependencies]',
|
|
67
|
+
].join('\n')
|
|
68
|
+
|
|
69
|
+
writeFileSync(filepath, content, 'utf-8')
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
success: true,
|
|
73
|
+
content: [
|
|
74
|
+
'── Plan Mode Activated ──',
|
|
75
|
+
'',
|
|
76
|
+
`Plan file: ${filepath}`,
|
|
77
|
+
'',
|
|
78
|
+
'Plan mode is READ-ONLY:',
|
|
79
|
+
' ✅ Read, Grep, Glob — auto-approved',
|
|
80
|
+
' ⚠️ All other tools — require confirmation',
|
|
81
|
+
'',
|
|
82
|
+
'Design your approach, explore the codebase, then:',
|
|
83
|
+
' • Use ExitPlanMode to submit your plan for approval',
|
|
84
|
+
' • The user will review and approve before code changes begin',
|
|
85
|
+
'',
|
|
86
|
+
'ExitPlanMode parameters:',
|
|
87
|
+
' approved: true → switch to acceptEdits mode (code changes allowed)',
|
|
88
|
+
' approved: false → revert to default mode',
|
|
89
|
+
].join('\n'),
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
export const exitPlanModeTool: ToolDefinition = {
|
|
4
|
+
name: 'ExitPlanMode',
|
|
5
|
+
description:
|
|
6
|
+
'Exit plan mode and submit your plan for approval. ' +
|
|
7
|
+
'Set approved: true to switch to acceptEdits mode (code changes allowed). ' +
|
|
8
|
+
'Set approved: false to revert to default mode. ' +
|
|
9
|
+
'Use this after you have finished designing your approach in plan mode.',
|
|
10
|
+
category: 'agent',
|
|
11
|
+
permission: 'auto',
|
|
12
|
+
parameters: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
approved: {
|
|
16
|
+
type: 'boolean',
|
|
17
|
+
description:
|
|
18
|
+
'Whether the user approved the plan. true → switch to acceptEdits mode. false → revert to default.',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
required: ['approved'],
|
|
22
|
+
},
|
|
23
|
+
async execute(params, _ctx) {
|
|
24
|
+
const approved = params.approved === true
|
|
25
|
+
|
|
26
|
+
if (approved) {
|
|
27
|
+
return {
|
|
28
|
+
success: true,
|
|
29
|
+
content: [
|
|
30
|
+
'── Plan Approved ──',
|
|
31
|
+
'',
|
|
32
|
+
'✓ Exiting plan mode.',
|
|
33
|
+
'✓ Switching to acceptEdits mode — reads and file edits are auto-approved.',
|
|
34
|
+
'',
|
|
35
|
+
'You can now implement the plan. The plan file is in .mipham/plans/.',
|
|
36
|
+
'',
|
|
37
|
+
'Use Shift+Tab to cycle permission modes if you need to change.',
|
|
38
|
+
].join('\n'),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
success: true,
|
|
44
|
+
content: [
|
|
45
|
+
'── Plan Mode Exited ──',
|
|
46
|
+
'',
|
|
47
|
+
'✓ Returning to default permission mode.',
|
|
48
|
+
'',
|
|
49
|
+
'No code changes were made. The plan file is preserved in .mipham/plans/.',
|
|
50
|
+
'Use EnterPlanMode again when ready to resume planning.',
|
|
51
|
+
].join('\n'),
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../shared/index.ts'
|
|
2
|
+
|
|
3
|
+
export const reportFindingsTool: ToolDefinition = {
|
|
4
|
+
name: 'ReportFindings',
|
|
5
|
+
description:
|
|
6
|
+
'Report code-review findings as a typed list. ' +
|
|
7
|
+
'Use this to output structured review results with file, line, summary, ' +
|
|
8
|
+
'failure_scenario, and category. Findings are ranked most-severe first.',
|
|
9
|
+
category: 'agent',
|
|
10
|
+
permission: 'auto',
|
|
11
|
+
parameters: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
level: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
enum: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
17
|
+
description: 'Effort level the review ran at.',
|
|
18
|
+
},
|
|
19
|
+
findings: {
|
|
20
|
+
type: 'array',
|
|
21
|
+
description: 'Verified findings, most-severe first; empty if none survived.',
|
|
22
|
+
minItems: 0,
|
|
23
|
+
maxItems: 32,
|
|
24
|
+
items: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
file: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
description: 'Repo-relative path of the file the finding is in.',
|
|
30
|
+
},
|
|
31
|
+
line: {
|
|
32
|
+
type: 'integer',
|
|
33
|
+
description: '1-indexed line the finding anchors to.',
|
|
34
|
+
},
|
|
35
|
+
summary: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
description: 'One-sentence statement of the defect.',
|
|
38
|
+
},
|
|
39
|
+
short_summary: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
maxLength: 60,
|
|
42
|
+
description: 'Compressed label for compact UI (≤60 chars).',
|
|
43
|
+
},
|
|
44
|
+
failure_scenario: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
description: 'Concrete inputs/state → wrong output/crash.',
|
|
47
|
+
},
|
|
48
|
+
category: {
|
|
49
|
+
type: 'string',
|
|
50
|
+
maxLength: 40,
|
|
51
|
+
description:
|
|
52
|
+
'Short kebab-case slug: correctness, security, performance, simplification, test-coverage, etc.',
|
|
53
|
+
},
|
|
54
|
+
verdict: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
enum: ['CONFIRMED', 'PLAUSIBLE'],
|
|
57
|
+
description: 'Set when a verify pass ran; absent on inline-only reviews.',
|
|
58
|
+
},
|
|
59
|
+
outcome: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
enum: ['fixed', 'skipped', 'no_change_needed'],
|
|
62
|
+
description:
|
|
63
|
+
'Set ONLY when re-reporting after applying fixes: what happened to this finding.',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
required: ['file', 'summary', 'failure_scenario'],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
required: ['findings'],
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
async execute(params, _ctx) {
|
|
74
|
+
const findings = params.findings as Array<Record<string, unknown>> | undefined
|
|
75
|
+
const level = (params.level as string) || 'medium'
|
|
76
|
+
|
|
77
|
+
if (!Array.isArray(findings)) {
|
|
78
|
+
return {
|
|
79
|
+
success: false,
|
|
80
|
+
content: '',
|
|
81
|
+
error: 'findings must be an array',
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Validate each finding has required fields
|
|
86
|
+
const errors: string[] = []
|
|
87
|
+
for (let i = 0; i < findings.length; i++) {
|
|
88
|
+
const f = findings[i]!
|
|
89
|
+
if (!f.file || typeof f.file !== 'string') {
|
|
90
|
+
errors.push(`findings[${i}]: "file" is required and must be a string`)
|
|
91
|
+
}
|
|
92
|
+
if (!f.summary || typeof f.summary !== 'string') {
|
|
93
|
+
errors.push(`findings[${i}]: "summary" is required and must be a string`)
|
|
94
|
+
}
|
|
95
|
+
if (!f.failure_scenario || typeof f.failure_scenario !== 'string') {
|
|
96
|
+
errors.push(`findings[${i}]: "failure_scenario" is required and must be a string`)
|
|
97
|
+
}
|
|
98
|
+
if (f.line !== undefined && typeof f.line !== 'number') {
|
|
99
|
+
errors.push(`findings[${i}]: "line" must be an integer if provided`)
|
|
100
|
+
}
|
|
101
|
+
if (f.verdict && !['CONFIRMED', 'PLAUSIBLE'].includes(f.verdict as string)) {
|
|
102
|
+
errors.push(`findings[${i}]: "verdict" must be CONFIRMED or PLAUSIBLE`)
|
|
103
|
+
}
|
|
104
|
+
if (f.outcome && !['fixed', 'skipped', 'no_change_needed'].includes(f.outcome as string)) {
|
|
105
|
+
errors.push(`findings[${i}]: "outcome" must be fixed, skipped, or no_change_needed`)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (errors.length > 0) {
|
|
110
|
+
return {
|
|
111
|
+
success: false,
|
|
112
|
+
content: '',
|
|
113
|
+
error: `Validation errors:\n${errors.map((e) => ` • ${e}`).join('\n')}`,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (findings.length === 0) {
|
|
118
|
+
return {
|
|
119
|
+
success: true,
|
|
120
|
+
content: `── Review Findings (${level} effort) ──\n\n✅ No findings to report.\n\nAll checks passed at ${level} effort level.`,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Format findings as a structured report
|
|
125
|
+
const lines: string[] = [
|
|
126
|
+
`── Review Findings (${level} effort) ──`,
|
|
127
|
+
'',
|
|
128
|
+
`${findings.length} finding${findings.length === 1 ? '' : 's'}:`,
|
|
129
|
+
'',
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
// Group by category for readability
|
|
133
|
+
const byCategory = new Map<string, Array<Record<string, unknown>>>()
|
|
134
|
+
for (const f of findings) {
|
|
135
|
+
const cat = (f.category as string) || 'uncategorized'
|
|
136
|
+
const list = byCategory.get(cat) || []
|
|
137
|
+
list.push(f)
|
|
138
|
+
byCategory.set(cat, list)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const CATEGORY_ICONS: Record<string, string> = {
|
|
142
|
+
correctness: '🔴',
|
|
143
|
+
security: '🔒',
|
|
144
|
+
performance: '⚡',
|
|
145
|
+
simplification: '🧹',
|
|
146
|
+
efficiency: '⏱️',
|
|
147
|
+
'test-coverage': '🧪',
|
|
148
|
+
architecture: '🏗️',
|
|
149
|
+
maintainability: '🔧',
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const [category, items] of byCategory) {
|
|
153
|
+
const icon = CATEGORY_ICONS[category] || '📌'
|
|
154
|
+
lines.push(`${icon} ${category} (${items.length}):`)
|
|
155
|
+
for (const f of items) {
|
|
156
|
+
const verdict = f.verdict ? ` [${f.verdict}]` : ''
|
|
157
|
+
const outcome = f.outcome ? ` → ${f.outcome}` : ''
|
|
158
|
+
const lineRef = f.line ? `:${f.line}` : ''
|
|
159
|
+
lines.push(` • ${f.file}${lineRef}${verdict}${outcome}`)
|
|
160
|
+
lines.push(` ${f.summary}`)
|
|
161
|
+
if (f.failure_scenario) {
|
|
162
|
+
const scenario =
|
|
163
|
+
(f.failure_scenario as string).length > 120
|
|
164
|
+
? (f.failure_scenario as string).slice(0, 120) + '...'
|
|
165
|
+
: (f.failure_scenario as string)
|
|
166
|
+
lines.push(` 💥 ${scenario}`)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
lines.push('')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Severity summary
|
|
173
|
+
const confirmed = findings.filter((f) => f.verdict === 'CONFIRMED').length
|
|
174
|
+
const plausible = findings.filter((f) => f.verdict === 'PLAUSIBLE').length
|
|
175
|
+
if (confirmed > 0 || plausible > 0) {
|
|
176
|
+
lines.push(
|
|
177
|
+
`📊 ${confirmed} CONFIRMED · ${plausible} PLAUSIBLE · ${findings.length - confirmed - plausible} unverified`,
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
success: true,
|
|
183
|
+
content: lines.join('\n'),
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../shared/index.ts'
|
|
2
|
+
import { getMessageBus } from '../../agent/message-bus'
|
|
3
|
+
|
|
4
|
+
export const sendMessageTool: ToolDefinition = {
|
|
5
|
+
name: 'SendMessage',
|
|
6
|
+
description:
|
|
7
|
+
'Send a message to another agent or the main conversation. ' +
|
|
8
|
+
'Use "main" as the recipient to message the parent session, ' +
|
|
9
|
+
'or a background task ID (e.g., "bg-1-xxx") to message a specific agent. ' +
|
|
10
|
+
'Messages are stored in the AgentMessageBus and can be polled by the recipient.',
|
|
11
|
+
category: 'agent',
|
|
12
|
+
permission: 'auto',
|
|
13
|
+
parameters: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
to: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
description:
|
|
19
|
+
'Recipient: "main" for the parent conversation, a background task ID, or an agent name.',
|
|
20
|
+
},
|
|
21
|
+
summary: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
description: 'A 5-10 word summary shown as a one-line preview (max 200 chars).',
|
|
24
|
+
},
|
|
25
|
+
message: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
description: 'Plain text message content.',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
required: ['to', 'message'],
|
|
31
|
+
},
|
|
32
|
+
async execute(params, ctx) {
|
|
33
|
+
const to = params.to as string
|
|
34
|
+
const summary = (params.summary as string) || '(no subject)'
|
|
35
|
+
const message = params.message as string
|
|
36
|
+
|
|
37
|
+
// Determine sender: use sessionId or 'main'
|
|
38
|
+
const from =
|
|
39
|
+
ctx.sessionId === 'sub-agent'
|
|
40
|
+
? `sub-agent-${Date.now().toString(36)}`
|
|
41
|
+
: ctx.sessionId || 'main'
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const bus = getMessageBus()
|
|
45
|
+
const msgId = bus.post(from, to, summary, message)
|
|
46
|
+
|
|
47
|
+
const unreadForRecipient = bus.unreadCount(to)
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
success: true,
|
|
51
|
+
content:
|
|
52
|
+
`── Message Sent ──\n\n` +
|
|
53
|
+
`ID: ${msgId}\n` +
|
|
54
|
+
`From: ${from}\n` +
|
|
55
|
+
`To: ${to}\n` +
|
|
56
|
+
`Summary: ${summary.slice(0, 100)}\n\n` +
|
|
57
|
+
`The recipient has ${unreadForRecipient} unread message(s).`,
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
return {
|
|
61
|
+
success: false,
|
|
62
|
+
content: '',
|
|
63
|
+
error: `Failed to send message: ${String(err)}`,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
}
|
|
@@ -5,7 +5,9 @@ import type { QueryEngine } from '../../core/engine'
|
|
|
5
5
|
export const workflowTool: ToolDefinition = {
|
|
6
6
|
name: 'Workflow',
|
|
7
7
|
description:
|
|
8
|
-
'Execute a multi-agent workflow script. The script uses agent(), parallel(), pipeline(), phase(), log(), args, budget primitives.
|
|
8
|
+
'Execute a multi-agent workflow script. The script uses agent(), parallel(), pipeline(), phase(), log(), args, budget primitives. ' +
|
|
9
|
+
'Pass resumeFromRunId to resume a prior run — cached agent() calls are replayed from the journal, ' +
|
|
10
|
+
'and only new/changed calls execute live.',
|
|
9
11
|
category: 'agent',
|
|
10
12
|
permission: 'ask',
|
|
11
13
|
parameters: {
|
|
@@ -20,12 +22,20 @@ export const workflowTool: ToolDefinition = {
|
|
|
20
22
|
type: 'object',
|
|
21
23
|
description: 'Arguments to pass to the workflow script as the `args` global',
|
|
22
24
|
},
|
|
25
|
+
resumeFromRunId: {
|
|
26
|
+
type: 'string',
|
|
27
|
+
description:
|
|
28
|
+
'Run ID of a prior Workflow invocation to resume from. ' +
|
|
29
|
+
'Completed agent() calls with unchanged (prompt, opts) return cached results; ' +
|
|
30
|
+
'only edited or new calls re-run.',
|
|
31
|
+
},
|
|
23
32
|
},
|
|
24
33
|
required: ['script'],
|
|
25
34
|
},
|
|
26
35
|
async execute(params, ctx) {
|
|
27
36
|
const script = params.script as string
|
|
28
37
|
const args = (params.args as Record<string, unknown>) || {}
|
|
38
|
+
const resumeFromRunId = params.resumeFromRunId as string | undefined
|
|
29
39
|
|
|
30
40
|
if (!ctx.registry || !ctx.toolRegistry) {
|
|
31
41
|
return {
|
|
@@ -42,11 +52,21 @@ export const workflowTool: ToolDefinition = {
|
|
|
42
52
|
} as QueryEngine
|
|
43
53
|
|
|
44
54
|
try {
|
|
45
|
-
const { runId, result } = await runWorkflow(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
const { runId, result, cacheHits, cacheMisses } = await runWorkflow(
|
|
56
|
+
script,
|
|
57
|
+
engineStub,
|
|
58
|
+
args,
|
|
59
|
+
null,
|
|
60
|
+
resumeFromRunId,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
let content = `Workflow ${runId} completed.\n\n`
|
|
64
|
+
if (resumeFromRunId) {
|
|
65
|
+
content += `Cache: ${cacheHits} hits · ${cacheMisses} live\n\n`
|
|
49
66
|
}
|
|
67
|
+
content += `Result:\n${typeof result === 'string' ? result : JSON.stringify(result, null, 2)}`
|
|
68
|
+
|
|
69
|
+
return { success: true, content }
|
|
50
70
|
} catch (err) {
|
|
51
71
|
return {
|
|
52
72
|
success: false,
|