@tduyng/pi-subagent 0.0.0-stage → 0.0.2
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/LICENSE +178 -0
- package/README.md +80 -2
- package/package.json +51 -4
- package/src/bulk-read.ts +147 -0
- package/src/lib/child-runtime.ts +18 -0
- package/src/log-reducer.ts +242 -0
- package/src/redact.ts +17 -0
- package/src/subagent/agent.ts +112 -0
- package/src/subagent/drive.ts +187 -0
- package/src/subagent/durable.ts +271 -0
- package/src/subagent/format.ts +241 -0
- package/src/subagent/guards/cloak.ts +396 -0
- package/src/subagent/guards/git-interceptor.ts +73 -0
- package/src/subagent/helpers.ts +212 -0
- package/src/subagent/index.ts +853 -0
- package/src/subagent/recover.ts +268 -0
- package/src/subagent/session.ts +87 -0
- package/src/subagent/state.ts +63 -0
- package/src/subagent/structured.ts +202 -0
- package/src/subagent/types.ts +109 -0
- package/src/subagent/viewer.ts +184 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { Model } from '@earendil-works/pi-ai'
|
|
2
|
+
|
|
3
|
+
import type { JsonPayload } from './structured.js'
|
|
4
|
+
|
|
5
|
+
export const RUNS_DIR = 'subagents'
|
|
6
|
+
export const AGENTS_DIR = 'agents'
|
|
7
|
+
export const EMIT_INTERVAL_MS = 250
|
|
8
|
+
export const MAX_TASKS = 8
|
|
9
|
+
export const MAX_CONCURRENT = 4
|
|
10
|
+
export const DEFAULT_MAX_TURNS = 50
|
|
11
|
+
export const THINKING_LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'] as const
|
|
12
|
+
export const WIDGET_KEY = 'subagent-widget'
|
|
13
|
+
export const MAX_WIDGET_ROWS = 8
|
|
14
|
+
export const MAX_REGISTRY = 50
|
|
15
|
+
export const MAX_TOUCHED = 12
|
|
16
|
+
export const FINISHED_RETENTION_MS = 5 * 60_000
|
|
17
|
+
export const FAILED_RETENTION_MS = 30 * 60_000
|
|
18
|
+
|
|
19
|
+
export type ThinkingLevel = (typeof THINKING_LEVELS)[number]
|
|
20
|
+
export const DEFAULT_THINKING: ThinkingLevel = 'medium'
|
|
21
|
+
export type RunStatus = 'queued' | 'running' | 'completed' | 'failed'
|
|
22
|
+
|
|
23
|
+
export interface AgentProfile {
|
|
24
|
+
name: string
|
|
25
|
+
description: string
|
|
26
|
+
filePath: string
|
|
27
|
+
systemPrompt: string
|
|
28
|
+
systemPromptMode: 'append' | 'replace'
|
|
29
|
+
thinking?: ThinkingLevel
|
|
30
|
+
model?: string
|
|
31
|
+
tools: string[]
|
|
32
|
+
disallowedTools: string[]
|
|
33
|
+
inheritProjectContext: boolean
|
|
34
|
+
inheritSkills: boolean
|
|
35
|
+
maxExecutionTimeMs?: number
|
|
36
|
+
maxTurns?: number
|
|
37
|
+
color?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface UsageTotals {
|
|
41
|
+
input: number
|
|
42
|
+
output: number
|
|
43
|
+
cost: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface TaskDetails {
|
|
47
|
+
id: string
|
|
48
|
+
index: number
|
|
49
|
+
label: string
|
|
50
|
+
agent?: string
|
|
51
|
+
status: RunStatus
|
|
52
|
+
cwd: string
|
|
53
|
+
provider: string
|
|
54
|
+
model: string
|
|
55
|
+
thinking: string
|
|
56
|
+
activity?: string
|
|
57
|
+
turnCount?: number
|
|
58
|
+
toolCallCount?: number
|
|
59
|
+
touched?: string[]
|
|
60
|
+
usage?: UsageTotals
|
|
61
|
+
output?: string
|
|
62
|
+
partial?: boolean
|
|
63
|
+
hasSchema?: boolean
|
|
64
|
+
maxTurns?: number
|
|
65
|
+
structured?: JsonPayload
|
|
66
|
+
error?: string
|
|
67
|
+
sessionFile?: string
|
|
68
|
+
worktree?: string
|
|
69
|
+
changes?: string[]
|
|
70
|
+
color?: string
|
|
71
|
+
forkContext?: boolean
|
|
72
|
+
startedAt?: number
|
|
73
|
+
finishedAt?: number
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface RunDetails {
|
|
77
|
+
status: RunStatus
|
|
78
|
+
runId: string
|
|
79
|
+
startedAt: number
|
|
80
|
+
finishedAt?: number
|
|
81
|
+
tasks: TaskDetails[]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface TaskSpec {
|
|
85
|
+
index: number
|
|
86
|
+
id: string
|
|
87
|
+
label: string
|
|
88
|
+
task: string
|
|
89
|
+
agent?: AgentProfile
|
|
90
|
+
cwd: string
|
|
91
|
+
dir: string
|
|
92
|
+
thinking: ThinkingLevel
|
|
93
|
+
model?: Model<any>
|
|
94
|
+
timeoutMs?: number
|
|
95
|
+
maxTurns: number
|
|
96
|
+
isolation?: 'worktree'
|
|
97
|
+
forkContext?: boolean
|
|
98
|
+
schema?: JsonPayload
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface ToolCallArguments {
|
|
102
|
+
command?: string
|
|
103
|
+
path?: string
|
|
104
|
+
file_path?: string
|
|
105
|
+
url?: string
|
|
106
|
+
query?: string
|
|
107
|
+
pattern?: string
|
|
108
|
+
content?: string
|
|
109
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { AgentSession, Theme } from '@earendil-works/pi-coding-agent'
|
|
2
|
+
import {
|
|
3
|
+
isKeyRelease,
|
|
4
|
+
matchesKey,
|
|
5
|
+
truncateToWidth,
|
|
6
|
+
wrapTextWithAnsi,
|
|
7
|
+
type Component,
|
|
8
|
+
type TUI
|
|
9
|
+
} from '@earendil-works/pi-tui'
|
|
10
|
+
|
|
11
|
+
import { MAX_WIDGET_ROWS, type TaskDetails } from './types.js'
|
|
12
|
+
import { roster } from './state.js'
|
|
13
|
+
import { buildTranscriptLines, formatDuration, fmtTokens, statusIcon, truncate } from './format.js'
|
|
14
|
+
|
|
15
|
+
export function widgetLines(theme: Theme, highlightIndex: number | null): string[] {
|
|
16
|
+
const all = roster()
|
|
17
|
+
const items = all.slice(0, MAX_WIDGET_ROWS)
|
|
18
|
+
if (items.length === 0) return []
|
|
19
|
+
|
|
20
|
+
const validColors = new Set<string>([
|
|
21
|
+
'accent',
|
|
22
|
+
'success',
|
|
23
|
+
'error',
|
|
24
|
+
'warning',
|
|
25
|
+
'muted',
|
|
26
|
+
'dim',
|
|
27
|
+
'text',
|
|
28
|
+
'toolTitle',
|
|
29
|
+
'toolOutput',
|
|
30
|
+
'mdHeading',
|
|
31
|
+
'mdLink'
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
const lines: string[] = []
|
|
35
|
+
|
|
36
|
+
const running = items.filter((t) => t.status === 'running' || t.status === 'queued').length
|
|
37
|
+
const done = items.filter((t) => t.status === 'completed').length
|
|
38
|
+
const failed = items.filter((t) => t.status === 'failed').length
|
|
39
|
+
const parts = []
|
|
40
|
+
if (running > 0) parts.push(`${running} running`)
|
|
41
|
+
if (done > 0) parts.push(`${done} done`)
|
|
42
|
+
if (failed > 0) parts.push(theme.fg('error', `${failed} failed`))
|
|
43
|
+
if (all.length > MAX_WIDGET_ROWS) parts.push(`+${all.length - MAX_WIDGET_ROWS} more`)
|
|
44
|
+
lines.push(theme.fg('dim', `${all.length} agent${all.length !== 1 ? 's' : ''} (${parts.join(', ')})`))
|
|
45
|
+
|
|
46
|
+
items.forEach((task, i) => {
|
|
47
|
+
const marker = i === highlightIndex ? theme.fg('accent', theme.bold('▶ ')) : ' '
|
|
48
|
+
const duration = formatDuration(task.startedAt, task.finishedAt)
|
|
49
|
+
|
|
50
|
+
const tokenInfo = task.usage ? ` ${fmtTokens(task.usage.output)}↓` : ''
|
|
51
|
+
const costInfo = task.usage && task.usage.cost > 0 ? ` $${task.usage.cost.toFixed(3)}` : ''
|
|
52
|
+
const turnInfo = task.turnCount !== undefined && task.turnCount > 0 ? ` t${task.turnCount}` : ''
|
|
53
|
+
|
|
54
|
+
const modeTag = task.forkContext ? theme.fg('accent', ' fork') : task.worktree ? theme.fg('accent', ' wt') : ''
|
|
55
|
+
|
|
56
|
+
const wtInfo =
|
|
57
|
+
task.worktree && task.status !== 'running'
|
|
58
|
+
? task.changes?.length
|
|
59
|
+
? ` (${task.changes.length} changed)`
|
|
60
|
+
: ' (clean)'
|
|
61
|
+
: ''
|
|
62
|
+
|
|
63
|
+
const agentColor = (
|
|
64
|
+
validColors.has(task.color ?? '') ? task.color : 'dim'
|
|
65
|
+
) as import('@earendil-works/pi-coding-agent').ThemeColor
|
|
66
|
+
const name = task.agent ? `${task.label} ${theme.fg(agentColor, `· ${task.agent}`)}` : task.label
|
|
67
|
+
|
|
68
|
+
const tag = theme.fg('dim', `${duration ?? ''}${turnInfo}${tokenInfo}${costInfo}`)
|
|
69
|
+
const errSnippet =
|
|
70
|
+
task.status === 'failed' && task.error
|
|
71
|
+
? theme.fg('error', ` ${truncate(task.error.split('\n')[0]?.split('. ')[0]?.trim() ?? task.error, 60)}`)
|
|
72
|
+
: ''
|
|
73
|
+
lines.push(
|
|
74
|
+
`${marker}${statusIcon(task.status, theme)} ${name}${modeTag} ${tag}${wtInfo ? theme.fg('dim', wtInfo) : ''}${errSnippet}`
|
|
75
|
+
)
|
|
76
|
+
if (task.status === 'running' && task.activity) lines.push(` ${theme.fg('dim', task.activity)}`)
|
|
77
|
+
})
|
|
78
|
+
if (highlightIndex !== null) lines.push(theme.fg('muted', '↑/↓ navigate · enter view · d dismiss · esc close'))
|
|
79
|
+
return lines
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const CHROME_LINES = 4
|
|
83
|
+
const MIN_VIEWPORT = 5
|
|
84
|
+
|
|
85
|
+
export class SubagentViewer implements Component {
|
|
86
|
+
private raw: string[] = []
|
|
87
|
+
private scroll = 0
|
|
88
|
+
private renderedBodyLength = 0
|
|
89
|
+
private autoScroll = true
|
|
90
|
+
private unsubscribe: () => void
|
|
91
|
+
private tui: TUI
|
|
92
|
+
private theme: Theme
|
|
93
|
+
private session: AgentSession
|
|
94
|
+
private task: TaskDetails
|
|
95
|
+
private done: (result: void) => void
|
|
96
|
+
|
|
97
|
+
constructor(tui: TUI, theme: Theme, session: AgentSession, task: TaskDetails, done: (result: void) => void) {
|
|
98
|
+
this.tui = tui
|
|
99
|
+
this.theme = theme
|
|
100
|
+
this.session = session
|
|
101
|
+
this.task = task
|
|
102
|
+
this.done = done
|
|
103
|
+
this.rebuild()
|
|
104
|
+
this.unsubscribe = session.subscribe((event) => {
|
|
105
|
+
if (
|
|
106
|
+
event.type === 'message_end' ||
|
|
107
|
+
event.type === 'tool_execution_start' ||
|
|
108
|
+
event.type === 'compaction_end' ||
|
|
109
|
+
event.type === 'entry_appended'
|
|
110
|
+
) {
|
|
111
|
+
this.rebuild()
|
|
112
|
+
this.tui.requestRender()
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private rebuild(): void {
|
|
118
|
+
this.raw = buildTranscriptLines(this.session.messages, this.theme)
|
|
119
|
+
if (this.raw.length === 0 && this.task.error) this.raw = [this.theme.fg('error', this.task.error)]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private viewportHeight(): number {
|
|
123
|
+
const rows = this.tui.terminal.rows || 24
|
|
124
|
+
return Math.max(MIN_VIEWPORT, Math.floor(rows * 0.7) - CHROME_LINES)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private wrapped(width: number): string[] {
|
|
128
|
+
return this.raw.flatMap((line) => (line ? wrapTextWithAnsi(line, width) : ['']))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
render(width: number): string[] {
|
|
132
|
+
const theme = this.theme
|
|
133
|
+
const inner = Math.max(10, width - 2)
|
|
134
|
+
const body = this.wrapped(inner)
|
|
135
|
+
this.renderedBodyLength = body.length
|
|
136
|
+
const height = this.viewportHeight()
|
|
137
|
+
if (this.autoScroll) this.scroll = Math.max(0, body.length - height)
|
|
138
|
+
this.scroll = Math.min(this.scroll, Math.max(0, body.length - height))
|
|
139
|
+
|
|
140
|
+
const task = this.task
|
|
141
|
+
const duration = formatDuration(task.startedAt, task.finishedAt)
|
|
142
|
+
const header = truncateToWidth(
|
|
143
|
+
`${statusIcon(task.status, theme)} ${theme.bold(task.label)} ${theme.fg('dim', `${task.provider}/${task.model} (${task.thinking})${duration ? ` · ${duration}` : ''}`)}`,
|
|
144
|
+
width
|
|
145
|
+
)
|
|
146
|
+
const footer = theme.fg('muted', '↑/↓ scroll · pgup/pgdn page · esc close')
|
|
147
|
+
|
|
148
|
+
const visible = body.slice(this.scroll, this.scroll + height)
|
|
149
|
+
while (visible.length < height) visible.push('')
|
|
150
|
+
|
|
151
|
+
return [header, ''.padEnd(inner, '─'), ...visible, ''.padEnd(inner, '─'), footer]
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
handleInput(data: string): void {
|
|
155
|
+
if (isKeyRelease(data)) return
|
|
156
|
+
if (matchesKey(data, 'escape') || matchesKey(data, 'q')) {
|
|
157
|
+
this.unsubscribe()
|
|
158
|
+
this.done()
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
const height = this.viewportHeight()
|
|
162
|
+
const maxScroll = Math.max(0, this.renderedBodyLength - height)
|
|
163
|
+
if (matchesKey(data, 'up') || matchesKey(data, 'k')) {
|
|
164
|
+
this.autoScroll = false
|
|
165
|
+
this.scroll = Math.max(0, this.scroll - 1)
|
|
166
|
+
} else if (matchesKey(data, 'down') || matchesKey(data, 'j')) {
|
|
167
|
+
this.scroll = Math.min(maxScroll, this.scroll + 1)
|
|
168
|
+
this.autoScroll = this.scroll >= maxScroll
|
|
169
|
+
} else if (matchesKey(data, 'pageUp')) {
|
|
170
|
+
this.autoScroll = false
|
|
171
|
+
this.scroll = Math.max(0, this.scroll - height)
|
|
172
|
+
} else if (matchesKey(data, 'pageDown')) {
|
|
173
|
+
this.scroll = Math.min(maxScroll, this.scroll + height)
|
|
174
|
+
this.autoScroll = this.scroll >= maxScroll
|
|
175
|
+
} else {
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
this.tui.requestRender()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
invalidate(): void {
|
|
182
|
+
this.rebuild()
|
|
183
|
+
}
|
|
184
|
+
}
|