pi-code 1.0.31 → 1.0.32
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.
|
@@ -340,7 +340,9 @@ export default function hooksExtension(pi: ExtensionAPI) {
|
|
|
340
340
|
if (!isSubagentPhaseEvent(data) || !sessionCtx) return
|
|
341
341
|
const ctx = sessionCtx
|
|
342
342
|
const eventName = data.phase === 'start' ? 'SubagentStart' : 'SubagentStop'
|
|
343
|
-
|
|
343
|
+
// Claude's SubagentStop carries the subagent's final text; agent_transcript_path
|
|
344
|
+
// stays absent (a --no-session child writes no transcript, see docs/hooks.md).
|
|
345
|
+
const payload = { hook_event_name: eventName, agent_type: data.agentType, agent_id: data.agentId, ...(data.phase === 'stop' && data.lastAssistantMessage !== undefined ? { last_assistant_message: data.lastAssistantMessage } : {}) }
|
|
344
346
|
try {
|
|
345
347
|
const results = await runNotifyHooks(matchingCommands(config[eventName], data.agentType), payload, boundRunner(ctx))
|
|
346
348
|
surfaceSystemMessages(results, (message) => ctx.ui.notify(message, 'warning'))
|
|
@@ -10,6 +10,9 @@ export interface SubagentPhaseEvent {
|
|
|
10
10
|
phase: 'start' | 'stop'
|
|
11
11
|
agentType: string
|
|
12
12
|
agentId: string
|
|
13
|
+
/** The run's final assistant text, on stop: Claude's SubagentStop delivers it
|
|
14
|
+
* as last_assistant_message so hooks need not parse a transcript. */
|
|
15
|
+
lastAssistantMessage?: string
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
export function isSubagentPhaseEvent(data: unknown): data is SubagentPhaseEvent {
|
|
@@ -22,6 +22,8 @@ export interface BackgroundRun {
|
|
|
22
22
|
turns: number
|
|
23
23
|
/** Last stderr bytes of a failed child; the only diagnostics a boot failure leaves. */
|
|
24
24
|
stderr?: string
|
|
25
|
+
/** Claude's partial marker: the run stopped at its maxTurns limit. */
|
|
26
|
+
partial?: boolean
|
|
25
27
|
/** Set while running so the run can be cancelled; cleared on completion. */
|
|
26
28
|
kill?: () => void
|
|
27
29
|
/** True until the child process actually closes: a cancelled child that ignores
|
|
@@ -41,7 +43,7 @@ export interface BackgroundSpawn {
|
|
|
41
43
|
command: string
|
|
42
44
|
args: string[]
|
|
43
45
|
cwd: string
|
|
44
|
-
/** The --
|
|
46
|
+
/** The --system-prompt body, kept so a resume can rebuild the file the
|
|
45
47
|
* completing run deleted. Without it the resumed child is handed a path that no
|
|
46
48
|
* longer exists, and pi falls back to using that path as the prompt text. */
|
|
47
49
|
promptBody?: string
|
|
@@ -191,9 +193,9 @@ export function resumeBackgroundRun(id: string, task: string, onComplete: (run:
|
|
|
191
193
|
return 'resumed'
|
|
192
194
|
}
|
|
193
195
|
|
|
194
|
-
/** Re-point --
|
|
196
|
+
/** Re-point --system-prompt at a fresh file when the original is gone. */
|
|
195
197
|
function withRebuiltPrompt(spawnSpec: BackgroundSpawn): string[] {
|
|
196
|
-
const flag = spawnSpec.args.indexOf('--
|
|
198
|
+
const flag = spawnSpec.args.indexOf('--system-prompt')
|
|
197
199
|
if (flag === -1 || !spawnSpec.promptBody) return spawnSpec.args
|
|
198
200
|
const current = spawnSpec.args[flag + 1]
|
|
199
201
|
if (current && fs.existsSync(current)) return spawnSpec.args
|
|
@@ -314,6 +316,8 @@ function driveRun(run: BackgroundRun, invocation: BackgroundSpawn, onComplete: (
|
|
|
314
316
|
// maxTurns cap ends cleanly with output preserved, so it counts as done, not failed.
|
|
315
317
|
if (run.state !== 'cancelled') run.state = code === 0 || cappedByMaxTurns ? 'done' : 'failed'
|
|
316
318
|
run.exitCode = cappedByMaxTurns ? 0 : (code ?? 0)
|
|
319
|
+
// Claude marks a maxTurns-capped run's output as partial and offers a resume.
|
|
320
|
+
if (cappedByMaxTurns) run.partial = true
|
|
317
321
|
run.output = text
|
|
318
322
|
run.turns = turns
|
|
319
323
|
run.stderr = stderrTail.trim() || undefined
|
|
@@ -67,6 +67,8 @@ interface SingleResult {
|
|
|
67
67
|
stopReason?: string
|
|
68
68
|
errorMessage?: string
|
|
69
69
|
step?: number
|
|
70
|
+
/** Claude's partial marker: the run stopped at its maxTurns limit. */
|
|
71
|
+
partial?: boolean
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
interface SubagentDetails {
|
|
@@ -157,13 +159,14 @@ interface RunAgentOptions {
|
|
|
157
159
|
projectApproved?: boolean
|
|
158
160
|
}
|
|
159
161
|
|
|
160
|
-
/** Publishes a child run's start/stop for the hooks extension's SubagentStart/Stop.
|
|
161
|
-
|
|
162
|
+
/** Publishes a child run's start/stop for the hooks extension's SubagentStart/Stop.
|
|
163
|
+
* The stop carries the run's final assistant text, which Claude's SubagentStop
|
|
164
|
+
* delivers as last_assistant_message. */
|
|
165
|
+
type SubagentPhaseSink = (phase: 'start' | 'stop', agentType: string, agentId: string, lastAssistantMessage?: string) => void
|
|
162
166
|
|
|
163
|
-
/**
|
|
164
|
-
*
|
|
165
|
-
function
|
|
166
|
-
const note = `[isolation: worktree kept at ${worktree.dir} (branch ${worktree.branch}); the agent's changes live there]`
|
|
167
|
+
/** Append a note to the final assistant message so it rides the run's normal
|
|
168
|
+
* output; stderr when there is none. */
|
|
169
|
+
function appendResultNote(result: SingleResult, note: string): void {
|
|
167
170
|
for (let i = result.messages.length - 1; i >= 0; i--) {
|
|
168
171
|
const msg = result.messages[i]
|
|
169
172
|
if (msg.role === 'assistant') {
|
|
@@ -174,15 +177,44 @@ function appendWorktreeNote(result: SingleResult, worktree: AgentWorktree): void
|
|
|
174
177
|
result.stderr = result.stderr ? `${result.stderr}\n${note}` : note
|
|
175
178
|
}
|
|
176
179
|
|
|
180
|
+
/** Tell the parent where a kept worktree lives. */
|
|
181
|
+
function appendWorktreeNote(result: SingleResult, worktree: AgentWorktree): void {
|
|
182
|
+
appendResultNote(result, `[isolation: worktree kept at ${worktree.dir} (branch ${worktree.branch}); the agent's changes live there]`)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Claude marks a maxTurns-capped run's output as partial; the note rides the
|
|
186
|
+
* final assistant message like the worktree note, so the parent model sees it
|
|
187
|
+
* with the output. A no-op for uncapped runs. */
|
|
188
|
+
function appendPartialNote(result: SingleResult): void {
|
|
189
|
+
if (result.partial) appendResultNote(result, '[Output is partial: the subagent stopped at its maxTurns limit.]')
|
|
190
|
+
}
|
|
191
|
+
|
|
177
192
|
async function runSingleAgent(options: RunAgentOptions): Promise<SingleResult> {
|
|
178
193
|
const agent = options.agents.find((a) => a.name === options.agentName)
|
|
179
194
|
if (!agent) return runSingleAgentInner(options)
|
|
195
|
+
// A refused launch (Claude's zero-tools error) never starts, so no
|
|
196
|
+
// SubagentStart/Stop pair fires for it.
|
|
197
|
+
const toolsError = unresolvedToolsError(agent)
|
|
198
|
+
if (toolsError) {
|
|
199
|
+
return {
|
|
200
|
+
agent: agent.name,
|
|
201
|
+
agentSource: agent.source,
|
|
202
|
+
task: options.task,
|
|
203
|
+
exitCode: 1,
|
|
204
|
+
messages: [],
|
|
205
|
+
stderr: toolsError,
|
|
206
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
207
|
+
step: options.step,
|
|
208
|
+
}
|
|
209
|
+
}
|
|
180
210
|
const agentId = `fg-${randomUUID().slice(0, 8)}`
|
|
181
211
|
options.onPhase?.('start', agent.name, agentId)
|
|
212
|
+
let result: SingleResult | undefined
|
|
182
213
|
try {
|
|
183
|
-
|
|
214
|
+
result = await runSingleAgentInner(options)
|
|
215
|
+
return result
|
|
184
216
|
} finally {
|
|
185
|
-
options.onPhase?.('stop', agent.name, agentId)
|
|
217
|
+
options.onPhase?.('stop', agent.name, agentId, result ? getFinalOutput(result.messages) || undefined : undefined)
|
|
186
218
|
}
|
|
187
219
|
}
|
|
188
220
|
|
|
@@ -265,7 +297,9 @@ async function runSingleAgentInner(options: RunAgentOptions): Promise<SingleResu
|
|
|
265
297
|
const tmp = await writePromptToTempFile(agent.name, promptBody)
|
|
266
298
|
tmpPromptDir = tmp.dir
|
|
267
299
|
tmpPromptPath = tmp.filePath
|
|
268
|
-
|
|
300
|
+
// Claude: the agent body IS the subagent's system prompt, replacing the
|
|
301
|
+
// default, not an addition to it (--system-prompt reads a file path too).
|
|
302
|
+
args.push('--system-prompt', tmpPromptPath)
|
|
269
303
|
}
|
|
270
304
|
|
|
271
305
|
args.push(`Task: ${task}`)
|
|
@@ -304,8 +338,12 @@ async function runSingleAgentInner(options: RunAgentOptions): Promise<SingleResu
|
|
|
304
338
|
accumulateAssistantMessage(currentResult, msg)
|
|
305
339
|
assistantTurns++
|
|
306
340
|
// Claude's maxTurns cap: end the child at the turn boundary once it has
|
|
307
|
-
// produced its Nth turn, so the collected output is kept and no turn is
|
|
308
|
-
|
|
341
|
+
// produced its Nth turn, so the collected output is kept and no turn is
|
|
342
|
+
// cut; the returned output is marked partial, as Claude documents.
|
|
343
|
+
if (agent.maxTurns && assistantTurns >= agent.maxTurns) {
|
|
344
|
+
currentResult.partial = true
|
|
345
|
+
killGroup('SIGTERM')
|
|
346
|
+
}
|
|
309
347
|
}
|
|
310
348
|
emitUpdate()
|
|
311
349
|
} else if (event.type === 'tool_result_end') {
|
|
@@ -371,6 +409,7 @@ async function runSingleAgentInner(options: RunAgentOptions): Promise<SingleResu
|
|
|
371
409
|
|
|
372
410
|
currentResult.exitCode = exitCode
|
|
373
411
|
if (wasAborted) throw new Error('Subagent was aborted')
|
|
412
|
+
appendPartialNote(currentResult)
|
|
374
413
|
return currentResult
|
|
375
414
|
} finally {
|
|
376
415
|
// Cleanup runs on abort too: it only removes a pristine worktree, so an
|
|
@@ -452,12 +491,15 @@ type ChainStepParam = Static<typeof ChainItem>
|
|
|
452
491
|
type TaskItemParam = Static<typeof TaskItem>
|
|
453
492
|
|
|
454
493
|
/** The completion notice a background run sends when it finishes. */
|
|
455
|
-
export function backgroundCompletionText(run: { id: string; agent: string; state: string; turns: number; output?: string; stderr?: string }): string {
|
|
494
|
+
export function backgroundCompletionText(run: { id: string; agent: string; state: string; turns: number; output?: string; stderr?: string; partial?: boolean }): string {
|
|
456
495
|
const output = capForContext(run.output ?? '') || '(no output)'
|
|
457
496
|
// A child that dies at boot writes its reason only to stderr; without this the
|
|
458
497
|
// notice reads "failed after 0 turns ... (no output)" with nothing to act on.
|
|
459
498
|
const diagnostics = run.state === 'failed' && run.stderr ? `\n\nstderr tail:\n${capForContext(run.stderr)}` : ''
|
|
460
|
-
|
|
499
|
+
// Claude marks maxTurns-capped output as partial and notes the run can be
|
|
500
|
+
// resumed to continue from where it stopped.
|
|
501
|
+
const partialNote = run.partial ? `\n\n[Output is partial: the run stopped at its maxTurns limit. Resume it with {resume: "${run.id}", task: "..."} to continue.]` : ''
|
|
502
|
+
return `Background subagent run ${run.id} (${run.agent}) ${run.state} after ${run.turns} turns.\n\n${output}${diagnostics}${partialNote}`
|
|
461
503
|
}
|
|
462
504
|
|
|
463
505
|
/** What to tell the model about a resume request. */
|
|
@@ -690,13 +732,31 @@ export function setKnownMcpAliases(aliases: ReadonlyArray<{ pi: string; claude:
|
|
|
690
732
|
knownMcpAliases = aliases
|
|
691
733
|
}
|
|
692
734
|
|
|
735
|
+
/** pi's built-in ToolName union (core/tools/index.d.ts; the package's export map
|
|
736
|
+
* does not expose allToolNames, so this mirrors it) plus the tools pi-code's own
|
|
737
|
+
* extensions register in a child. Claude's capitalized spellings fold onto these. */
|
|
738
|
+
const CHILD_TOOL_NAMES = new Set(['read', 'bash', 'edit', 'write', 'grep', 'find', 'ls', 'web_fetch', 'web_search', 'list_mcp_resources', 'read_mcp_resource'])
|
|
739
|
+
|
|
740
|
+
/** Claude: when no entry in a `tools` list resolves to a tool, the subagent fails
|
|
741
|
+
* to launch with an error naming the entries, instead of running tool-less. */
|
|
742
|
+
function unresolvedToolsError(agent: AgentConfig): string | undefined {
|
|
743
|
+
if (!agent.tools || agent.tools.length === 0) return undefined
|
|
744
|
+
const fold = (name: string): string => name.toLowerCase().replaceAll('-', '_')
|
|
745
|
+
const known = new Set(knownMcpAliases.map((alias) => fold(alias.pi)))
|
|
746
|
+
const resolves = expandMcpToolPatterns(agent.tools, knownMcpAliases).some((entry) => CHILD_TOOL_NAMES.has(fold(entry)) || known.has(fold(entry)))
|
|
747
|
+
if (resolves) return undefined
|
|
748
|
+
return `Agent "${agent.name}" would launch with zero tools: no entry in [${agent.tools.join(', ')}] resolves to a tool.`
|
|
749
|
+
}
|
|
750
|
+
|
|
693
751
|
/** CLI args shared by foreground and background children, from the agent's config. */
|
|
694
752
|
function agentInvocationArgs(agent: AgentConfig, aliasModel?: string): string[] {
|
|
695
753
|
const args: string[] = ['--mode', 'json', '-p', '--no-session']
|
|
696
754
|
// A concrete model wins; otherwise a Claude tier alias resolved against the models
|
|
697
|
-
// this user can actually run
|
|
698
|
-
//
|
|
699
|
-
|
|
755
|
+
// this user can actually run; then CLAUDE_CODE_SUBAGENT_MODEL, per Claude's model
|
|
756
|
+
// order (invocation model, frontmatter model, this variable, the session model).
|
|
757
|
+
// pi reads a thinking level from the model pattern's :suffix when a model is
|
|
758
|
+
// pinned, and from --thinking otherwise.
|
|
759
|
+
const model = agent.model ?? aliasModel ?? process.env.CLAUDE_CODE_SUBAGENT_MODEL
|
|
700
760
|
if (model) args.push('--model', agent.effort ? `${model}:${agent.effort}` : model)
|
|
701
761
|
else if (agent.effort) args.push('--thinking', agent.effort)
|
|
702
762
|
// Claude's mcp__<server> / mcp__* patterns expand against the parent's MCP roster;
|
|
@@ -758,6 +818,13 @@ async function runBackgroundMode(params: SubagentParamsStatic, context: Backgrou
|
|
|
758
818
|
details: makeDetails('single')([]),
|
|
759
819
|
}
|
|
760
820
|
}
|
|
821
|
+
const toolsError = unresolvedToolsError(agent)
|
|
822
|
+
if (toolsError) {
|
|
823
|
+
return {
|
|
824
|
+
content: [{ type: 'text', text: toolsError }],
|
|
825
|
+
details: makeDetails('single')([]),
|
|
826
|
+
}
|
|
827
|
+
}
|
|
761
828
|
if (activeBackgroundRuns() >= MAX_BACKGROUND_RUNS) {
|
|
762
829
|
return backgroundCapResult(makeDetails)
|
|
763
830
|
}
|
|
@@ -782,7 +849,9 @@ async function runBackgroundMode(params: SubagentParamsStatic, context: Backgrou
|
|
|
782
849
|
const promptBody = childPromptBody(agent, skillRoots, memorySection)
|
|
783
850
|
if (promptBody.trim()) {
|
|
784
851
|
tmpPrompt = await writePromptToTempFile(agent.name, promptBody)
|
|
785
|
-
|
|
852
|
+
// Claude: the agent body replaces the default system prompt (see the
|
|
853
|
+
// foreground path).
|
|
854
|
+
args.push('--system-prompt', tmpPrompt.filePath)
|
|
786
855
|
}
|
|
787
856
|
args.push(`Task: ${task}`)
|
|
788
857
|
const invocation = getPiInvocation(args)
|
|
@@ -793,7 +862,7 @@ async function runBackgroundMode(params: SubagentParamsStatic, context: Backgrou
|
|
|
793
862
|
// catch covers the synchronous path, but the worktree branch reaches here from an
|
|
794
863
|
// async continuation outside it, so the guard must live in finish itself.
|
|
795
864
|
try {
|
|
796
|
-
pi.events.emit(SUBAGENT_CHANNEL, { phase: 'stop', agentType: run.agent, agentId: run.id })
|
|
865
|
+
pi.events.emit(SUBAGENT_CHANNEL, { phase: 'stop', agentType: run.agent, agentId: run.id, ...(run.output?.trim() ? { lastAssistantMessage: run.output.trim() } : {}) })
|
|
797
866
|
pi.sendMessage({ customType: 'subagent-background', content: backgroundCompletionText(run), display: true }, { triggerTurn: true })
|
|
798
867
|
} catch {
|
|
799
868
|
// Session disposed after the run outlived it; nothing to notify.
|
|
@@ -1486,7 +1555,17 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
1486
1555
|
|
|
1487
1556
|
if (params.background) return runBackgroundMode(params, { agents, defaultCwd: ctx.cwd, pi, makeDetails, skillRoots, availableModels, projectApproved }, (id) => rememberBackgroundRun(id))
|
|
1488
1557
|
|
|
1489
|
-
const mode: ModeContext = {
|
|
1558
|
+
const mode: ModeContext = {
|
|
1559
|
+
agents,
|
|
1560
|
+
defaultCwd: ctx.cwd,
|
|
1561
|
+
signal,
|
|
1562
|
+
onUpdate,
|
|
1563
|
+
makeDetails,
|
|
1564
|
+
skillRoots,
|
|
1565
|
+
availableModels,
|
|
1566
|
+
projectApproved,
|
|
1567
|
+
onPhase: (phase, agentType, agentId, lastAssistantMessage) => pi.events.emit(SUBAGENT_CHANNEL, { phase, agentType, agentId, ...(lastAssistantMessage === undefined ? {} : { lastAssistantMessage }) }),
|
|
1568
|
+
}
|
|
1490
1569
|
|
|
1491
1570
|
if (params.chain?.length) return runChainMode(params.chain, mode)
|
|
1492
1571
|
if (params.tasks?.length) return runParallelMode(params.tasks, mode)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.32",
|
|
4
4
|
"description": "Claude Code experience for the pi coding agent: reads your .claude config (rules, commands, skills, hooks, output styles, MCP servers, agents) and adds todo, checkpoints, memory, web, and subagents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|