@miphamai/cli 0.3.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 +188 -0
- package/dist/mipham +0 -0
- package/package.json +9 -9
- package/skills/mipham/om-artifact.mipham-skill.md +69 -0
- package/skills/mipham/om-model-optimize.mipham-skill.md +9 -6
- package/skills/mipham/om-security.mipham-skill.md +11 -8
- package/skills/standard/doc-generator.SKILL.md +6 -1
- package/skills/standard/github-ops.SKILL.md +12 -7
- package/skills/standard/memory.SKILL.md +1 -0
- package/skills/standard/self-review.SKILL.md +1 -0
- package/skills/standard/superpower.SKILL.md +7 -7
- package/skills/standard/web-search.SKILL.md +3 -0
- package/src/agent/agent-context.ts +56 -0
- package/src/agent/agent-registry.ts +134 -0
- package/src/agent/sub-agent.ts +73 -89
- 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/manifest.ts +101 -0
- package/src/artifacts/server.ts +374 -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 +207 -58
- 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 +12 -1
- 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 +224 -34
- package/src/index.tsx +30 -2
- package/src/mcp/transport.ts +42 -5
- 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/providers/fetch-utils.ts +1 -3
- package/src/providers/openai-compat.ts +2 -11
- package/src/shared/constants.ts +8 -1
- package/src/shared/types.ts +82 -2
- package/src/skills/fork-executor.ts +40 -0
- package/src/skills/loader.ts +48 -2
- package/src/tools/agent/agent.ts +20 -11
- package/src/tools/agent/skill.ts +32 -2
- package/src/tools/artifact/artifact.ts +144 -0
- 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 +6 -0
- package/src/ui/app.tsx +20 -7
- package/src/ui/chat.tsx +9 -5
- package/src/ui/commands.ts +185 -40
- package/src/ui/input.tsx +203 -27
- package/src/ui/picker.tsx +2 -5
- 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
|
@@ -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,14 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
export type SubAgentType = 'general' | 'explore' | 'plan' | 'code-review'
|
|
6
|
-
|
|
7
|
-
interface SubAgentOptions {
|
|
8
|
-
type?: SubAgentType
|
|
9
|
-
systemPrompt?: string
|
|
10
|
-
maxContextMessages?: number
|
|
11
|
-
}
|
|
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'
|
|
12
5
|
|
|
13
6
|
const TYPE_SYSTEM_PROMPTS: Record<SubAgentType, string> = {
|
|
14
7
|
general: 'You are a focused sub-agent. Complete the assigned task thoroughly and return results.',
|
|
@@ -21,102 +14,93 @@ const TYPE_SYSTEM_PROMPTS: Record<SubAgentType, string> = {
|
|
|
21
14
|
|
|
22
15
|
/**
|
|
23
16
|
* Sub-agent engine — creates an isolated conversation context and processes
|
|
24
|
-
* a single prompt independently
|
|
25
|
-
*
|
|
26
|
-
* Phase 7: Pipeline-ready. Without a real AI provider, returns a structured
|
|
27
|
-
* 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.
|
|
28
19
|
*/
|
|
29
20
|
export class SubAgent {
|
|
30
|
-
constructor(
|
|
21
|
+
constructor(
|
|
22
|
+
private registry: ProviderRegistry,
|
|
23
|
+
private toolRegistry: Map<string, ToolDefinition>,
|
|
24
|
+
) {}
|
|
31
25
|
|
|
32
26
|
async execute(
|
|
33
27
|
prompt: string,
|
|
34
28
|
description: string,
|
|
35
29
|
options: SubAgentOptions = {},
|
|
36
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()
|
|
37
37
|
const type = options.type || 'general'
|
|
38
|
-
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
|
+
)
|
|
39
58
|
|
|
40
|
-
// Create isolated context for this sub-agent
|
|
41
|
-
const context = new ContextManager({
|
|
42
|
-
maxTokens: 100_000,
|
|
43
|
-
compactionThreshold: 0.9,
|
|
44
|
-
})
|
|
45
59
|
context.setSystemPrompt(systemPrompt)
|
|
46
60
|
context.addMessage({ role: 'user', content: prompt })
|
|
47
61
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
54
72
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
systemPrompt,
|
|
59
|
-
maxTokens: 4096,
|
|
60
|
-
})) {
|
|
61
|
-
if (chunk.type === 'text' && chunk.content) {
|
|
62
|
-
chunks.push(chunk.content)
|
|
63
|
-
}
|
|
64
|
-
if (chunk.type === 'error') {
|
|
65
|
-
// Fall through to simulation on API error
|
|
66
|
-
throw new Error(chunk.error)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
73
|
+
const modelToUse = options.modelOverride || agentDef?.model || model
|
|
74
|
+
// 'inherit' means use parent model
|
|
75
|
+
const resolvedModel = modelToUse === 'inherit' ? model : modelToUse
|
|
69
76
|
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
72
95
|
}
|
|
73
96
|
}
|
|
74
|
-
} catch {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return this.simulate(prompt, description, type)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
private simulate(prompt: string, description: string, type: SubAgentType): string {
|
|
83
|
-
const lines: string[] = [
|
|
84
|
-
`── Sub-Agent Result (${type}) ──`,
|
|
85
|
-
'',
|
|
86
|
-
`Task: ${description}`,
|
|
87
|
-
`Prompt: ${prompt.slice(0, 200)}${prompt.length > 200 ? '...' : ''}`,
|
|
88
|
-
'',
|
|
89
|
-
]
|
|
90
|
-
|
|
91
|
-
switch (type) {
|
|
92
|
-
case 'explore':
|
|
93
|
-
lines.push(
|
|
94
|
-
'Analysis: This exploration task would search the codebase for relevant',
|
|
95
|
-
'files, patterns, and structures. The sub-agent pipeline is ready — connect',
|
|
96
|
-
'a provider API key for real AI-powered exploration.',
|
|
97
|
-
)
|
|
98
|
-
break
|
|
99
|
-
case 'plan':
|
|
100
|
-
lines.push(
|
|
101
|
-
'Plan: A step-by-step implementation plan would be generated based on',
|
|
102
|
-
'the requirements. Use /plan mode for interactive planning.',
|
|
103
|
-
)
|
|
104
|
-
break
|
|
105
|
-
case 'code-review':
|
|
106
|
-
lines.push(
|
|
107
|
-
'Review: The code review sub-agent would analyze the specified files for',
|
|
108
|
-
'bugs, security issues, and code quality concerns. Use /review for',
|
|
109
|
-
'interactive code review.',
|
|
110
|
-
)
|
|
111
|
-
break
|
|
112
|
-
default:
|
|
113
|
-
lines.push(
|
|
114
|
-
'The sub-agent pipeline is operational. Connect an API key to enable',
|
|
115
|
-
'AI-powered sub-agent execution. The infrastructure for context isolation,',
|
|
116
|
-
'prompt routing, and result aggregation is in place.',
|
|
117
|
-
)
|
|
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)}`)
|
|
118
102
|
}
|
|
119
103
|
|
|
120
|
-
return
|
|
104
|
+
return chunks.join('')
|
|
121
105
|
}
|
|
122
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
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentViewManager — multi-session lifecycle manager for background agent sessions.
|
|
3
|
+
*
|
|
4
|
+
* Each session represents a background sub-agent task. The manager tracks
|
|
5
|
+
* status transitions, elapsed time, and provides grouping/peek/attach/kill
|
|
6
|
+
* operations used by the Agent View Dashboard TUI.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type SessionStatus = 'needs-input' | 'working' | 'completed' | 'failed'
|
|
10
|
+
|
|
11
|
+
export interface SessionMessage {
|
|
12
|
+
role: 'user' | 'assistant' | 'system'
|
|
13
|
+
content: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AgentSession {
|
|
17
|
+
id: string
|
|
18
|
+
title: string
|
|
19
|
+
status: SessionStatus
|
|
20
|
+
provider: string
|
|
21
|
+
model: string
|
|
22
|
+
task: string
|
|
23
|
+
createdAt: Date
|
|
24
|
+
startedAt?: Date
|
|
25
|
+
completedAt?: Date
|
|
26
|
+
elapsedMs: number
|
|
27
|
+
messages: SessionMessage[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CreateSessionOptions {
|
|
31
|
+
provider?: string
|
|
32
|
+
model?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SessionPeek {
|
|
36
|
+
session: AgentSession
|
|
37
|
+
recentMessages: SessionMessage[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type StatusGroups = Record<SessionStatus, AgentSession[]>
|
|
41
|
+
|
|
42
|
+
export class AgentViewManager {
|
|
43
|
+
private sessions: Map<string, AgentSession> = new Map()
|
|
44
|
+
private sessionOrder: string[] = []
|
|
45
|
+
private idCounter = 0
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create a new background agent session.
|
|
49
|
+
*/
|
|
50
|
+
create(title: string, task: string, options: CreateSessionOptions = {}): AgentSession {
|
|
51
|
+
const id = `agent-${++this.idCounter}-${Date.now()}`
|
|
52
|
+
const session: AgentSession = {
|
|
53
|
+
id,
|
|
54
|
+
title,
|
|
55
|
+
status: 'needs-input',
|
|
56
|
+
provider: options.provider ?? 'unknown',
|
|
57
|
+
model: options.model ?? 'unknown',
|
|
58
|
+
task,
|
|
59
|
+
createdAt: new Date(),
|
|
60
|
+
elapsedMs: 0,
|
|
61
|
+
messages: [],
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.sessions.set(id, session)
|
|
65
|
+
this.sessionOrder.push(id)
|
|
66
|
+
|
|
67
|
+
return session
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* List all sessions in creation order (newest first).
|
|
72
|
+
*/
|
|
73
|
+
list(): AgentSession[] {
|
|
74
|
+
return [...this.sessionOrder]
|
|
75
|
+
.reverse()
|
|
76
|
+
.map((id) => this.sessions.get(id)!)
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Group all sessions by their current status.
|
|
82
|
+
* Returns a record with keys for all four statuses (empty arrays if none).
|
|
83
|
+
*/
|
|
84
|
+
groupByStatus(): StatusGroups {
|
|
85
|
+
const groups: StatusGroups = {
|
|
86
|
+
'needs-input': [],
|
|
87
|
+
working: [],
|
|
88
|
+
completed: [],
|
|
89
|
+
failed: [],
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const id of this.sessionOrder) {
|
|
93
|
+
const session = this.sessions.get(id)
|
|
94
|
+
if (session) {
|
|
95
|
+
groups[session.status].push(session)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return groups
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get a session by ID.
|
|
104
|
+
*/
|
|
105
|
+
get(id: string): AgentSession | undefined {
|
|
106
|
+
return this.sessions.get(id)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Peek at a session — returns the session metadata and up to 5 recent messages.
|
|
111
|
+
*/
|
|
112
|
+
peek(id: string): SessionPeek | undefined {
|
|
113
|
+
const session = this.sessions.get(id)
|
|
114
|
+
if (!session) return undefined
|
|
115
|
+
|
|
116
|
+
const recentMessages = session.messages.slice(-5)
|
|
117
|
+
return { session, recentMessages }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Attach to a session — marks the session as working (if needs-input) and
|
|
122
|
+
* returns it so the caller can switch the main UI to this session.
|
|
123
|
+
*/
|
|
124
|
+
attach(id: string): AgentSession | undefined {
|
|
125
|
+
const session = this.sessions.get(id)
|
|
126
|
+
if (!session) return undefined
|
|
127
|
+
|
|
128
|
+
// If the session was waiting for input, transition to working
|
|
129
|
+
if (session.status === 'needs-input') {
|
|
130
|
+
session.status = 'working'
|
|
131
|
+
session.startedAt = new Date()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return session
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Kill (terminate) a session. Only kills sessions that are not already completed/failed.
|
|
139
|
+
* Returns true if the session was found and killed, false otherwise.
|
|
140
|
+
*/
|
|
141
|
+
kill(id: string): boolean {
|
|
142
|
+
const session = this.sessions.get(id)
|
|
143
|
+
if (!session) return false
|
|
144
|
+
|
|
145
|
+
// Don't re-kill already terminal sessions
|
|
146
|
+
if (session.status === 'completed' || session.status === 'failed') {
|
|
147
|
+
return false
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
session.status = 'failed'
|
|
151
|
+
session.completedAt = new Date()
|
|
152
|
+
return true
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Update a session's status and optionally add a message.
|
|
157
|
+
*/
|
|
158
|
+
updateStatus(id: string, status: SessionStatus): boolean {
|
|
159
|
+
const session = this.sessions.get(id)
|
|
160
|
+
if (!session) return false
|
|
161
|
+
|
|
162
|
+
session.status = status
|
|
163
|
+
|
|
164
|
+
if (status === 'working' && !session.startedAt) {
|
|
165
|
+
session.startedAt = new Date()
|
|
166
|
+
}
|
|
167
|
+
if ((status === 'completed' || status === 'failed') && !session.completedAt) {
|
|
168
|
+
session.completedAt = new Date()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Add a message to a session's message history.
|
|
176
|
+
*/
|
|
177
|
+
addMessage(id: string, message: SessionMessage): boolean {
|
|
178
|
+
const session = this.sessions.get(id)
|
|
179
|
+
if (!session) return false
|
|
180
|
+
|
|
181
|
+
session.messages.push(message)
|
|
182
|
+
return true
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Count sessions by status.
|
|
187
|
+
*/
|
|
188
|
+
countByStatus(): Record<SessionStatus, number> {
|
|
189
|
+
const counts: Record<SessionStatus, number> = {
|
|
190
|
+
'needs-input': 0,
|
|
191
|
+
working: 0,
|
|
192
|
+
completed: 0,
|
|
193
|
+
failed: 0,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (const [, session] of this.sessions) {
|
|
197
|
+
counts[session.status]++
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return counts
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Remove all completed and failed sessions (cleanup).
|
|
205
|
+
*/
|
|
206
|
+
prune(): number {
|
|
207
|
+
let removed = 0
|
|
208
|
+
for (const [id, session] of this.sessions) {
|
|
209
|
+
if (session.status === 'completed' || session.status === 'failed') {
|
|
210
|
+
this.sessions.delete(id)
|
|
211
|
+
this.sessionOrder = this.sessionOrder.filter((oid) => oid !== id)
|
|
212
|
+
removed++
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return removed
|
|
216
|
+
}
|
|
217
|
+
}
|