@oh-my-pi/subagents 1.3.376 → 1.3.377
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/package.json +1 -1
- package/tools/index.ts +98 -12
- package/tools/runtime.json +2 -1
package/package.json
CHANGED
package/tools/index.ts
CHANGED
|
@@ -32,6 +32,8 @@ import { spawn, spawnSync } from 'node:child_process'
|
|
|
32
32
|
const PI_CMD = process.platform === 'win32' ? 'pi.cmd' : 'pi'
|
|
33
33
|
/** Windows shell option for spawn/spawnSync when using PI_CMD */
|
|
34
34
|
const PI_SHELL_OPT = process.platform === 'win32'
|
|
35
|
+
/** Env var set to inhibit subagent spawning (prevents infinite recursion) */
|
|
36
|
+
const PI_NO_SUBAGENTS_ENV = 'PI_NO_SUBAGENTS'
|
|
35
37
|
|
|
36
38
|
import * as crypto from 'node:crypto'
|
|
37
39
|
import * as fs from 'node:fs'
|
|
@@ -39,7 +41,7 @@ import * as os from 'node:os'
|
|
|
39
41
|
import * as path from 'node:path'
|
|
40
42
|
import * as readline from 'node:readline'
|
|
41
43
|
import { StringEnum } from '@mariozechner/pi-ai'
|
|
42
|
-
import type { CustomAgentTool, CustomToolFactory, ToolAPI } from '@mariozechner/pi-coding-agent'
|
|
44
|
+
import type { CustomAgentTool, CustomToolFactory, ToolAPI, ToolSessionEvent } from '@mariozechner/pi-coding-agent'
|
|
43
45
|
import { Text } from '@mariozechner/pi-tui'
|
|
44
46
|
import { Type } from '@sinclair/typebox'
|
|
45
47
|
import runtime from './runtime.json'
|
|
@@ -118,6 +120,22 @@ const MAX_PARALLEL_TASKS = runtime.options.maxParallelTasks ?? 32
|
|
|
118
120
|
const MAX_CONCURRENCY = runtime.options.maxConcurrency ?? 16
|
|
119
121
|
const MAX_AGENTS_IN_DESCRIPTION = runtime.options.maxAgentsInDescription ?? 10
|
|
120
122
|
|
|
123
|
+
const PERSIST_SESSIONS = runtime.options.persistSessions ?? false
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Derive a session artifacts directory from a session file path.
|
|
127
|
+
* /path/to/sessions/project/2026-01-01T14-28-11-636Z_uuid.jsonl
|
|
128
|
+
* → /path/to/sessions/project/2026-01-01T14-28-11-636Z_uuid/
|
|
129
|
+
*/
|
|
130
|
+
function getSessionArtifactsDir(sessionFile: string | null): string | null {
|
|
131
|
+
if (!sessionFile) return null
|
|
132
|
+
// Strip .jsonl extension to get directory path
|
|
133
|
+
if (sessionFile.endsWith('.jsonl')) {
|
|
134
|
+
return sessionFile.slice(0, -6)
|
|
135
|
+
}
|
|
136
|
+
return sessionFile
|
|
137
|
+
}
|
|
138
|
+
|
|
121
139
|
type AgentScope = 'user' | 'project' | 'both'
|
|
122
140
|
|
|
123
141
|
interface AgentConfig {
|
|
@@ -126,6 +144,8 @@ interface AgentConfig {
|
|
|
126
144
|
tools?: string[]
|
|
127
145
|
model?: string
|
|
128
146
|
forkContext?: boolean
|
|
147
|
+
/** If true, this agent can spawn subagents. Default: false (subagents inhibited) */
|
|
148
|
+
recursive?: boolean
|
|
129
149
|
systemPrompt: string
|
|
130
150
|
source: 'user' | 'project'
|
|
131
151
|
filePath: string
|
|
@@ -252,12 +272,15 @@ function loadAgentsFromDir(dir: string, source: 'user' | 'project'): AgentConfig
|
|
|
252
272
|
const forkContext =
|
|
253
273
|
frontmatter.forkContext === undefined ? undefined : frontmatter.forkContext === 'true' || frontmatter.forkContext === '1'
|
|
254
274
|
|
|
275
|
+
const recursive = frontmatter.recursive === undefined ? undefined : frontmatter.recursive === 'true' || frontmatter.recursive === '1'
|
|
276
|
+
|
|
255
277
|
agents.push({
|
|
256
278
|
name: frontmatter.name,
|
|
257
279
|
description: frontmatter.description,
|
|
258
280
|
tools: tools && tools.length > 0 ? tools : undefined,
|
|
259
281
|
model: frontmatter.model,
|
|
260
282
|
forkContext,
|
|
283
|
+
recursive,
|
|
261
284
|
systemPrompt: body,
|
|
262
285
|
source,
|
|
263
286
|
filePath,
|
|
@@ -475,6 +498,10 @@ interface RunAgentOptions {
|
|
|
475
498
|
index?: number
|
|
476
499
|
signal?: AbortSignal
|
|
477
500
|
model?: string
|
|
501
|
+
/** Session file path. If provided, uses --session flag; otherwise --no-session. */
|
|
502
|
+
sessionFile?: string
|
|
503
|
+
/** Input file path. If provided, task is written here (persistent); otherwise uses temp file. */
|
|
504
|
+
inputFile?: string
|
|
478
505
|
}
|
|
479
506
|
|
|
480
507
|
async function runSingleAgent(
|
|
@@ -517,7 +544,14 @@ async function runSingleAgent(
|
|
|
517
544
|
}
|
|
518
545
|
}
|
|
519
546
|
|
|
520
|
-
const args: string[] = ['-p', '--
|
|
547
|
+
const args: string[] = ['-p', '--mode', 'json']
|
|
548
|
+
|
|
549
|
+
// Session persistence: use --session <file> if provided, otherwise --no-session
|
|
550
|
+
if (options?.sessionFile) {
|
|
551
|
+
args.push('--session', options.sessionFile)
|
|
552
|
+
} else {
|
|
553
|
+
args.push('--no-session')
|
|
554
|
+
}
|
|
521
555
|
|
|
522
556
|
// "default" means no model override - use pi's configured default
|
|
523
557
|
const modelOverride = options?.model
|
|
@@ -531,22 +565,31 @@ async function runSingleAgent(
|
|
|
531
565
|
args.push('--tools', agent.tools.join(','))
|
|
532
566
|
}
|
|
533
567
|
|
|
568
|
+
// Use provided inputFile (persistent) or create temp file
|
|
534
569
|
let tmpPromptDir: string | null = null
|
|
570
|
+
let taskFilePath: string
|
|
535
571
|
|
|
536
|
-
|
|
537
|
-
//
|
|
572
|
+
if (options?.inputFile) {
|
|
573
|
+
// Persistent: write to provided path
|
|
574
|
+
taskFilePath = options.inputFile
|
|
575
|
+
fs.writeFileSync(taskFilePath, task, { encoding: 'utf-8' })
|
|
576
|
+
} else {
|
|
577
|
+
// Ephemeral: use temp directory (cleaned up after)
|
|
538
578
|
tmpPromptDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pi-task-agent-'))
|
|
579
|
+
taskFilePath = path.join(tmpPromptDir, `task-${agent.name.replace(/[^\w.-]+/g, '_')}.md`)
|
|
580
|
+
fs.writeFileSync(taskFilePath, task, { encoding: 'utf-8', mode: 0o600 })
|
|
581
|
+
}
|
|
539
582
|
|
|
583
|
+
try {
|
|
540
584
|
if (agent.systemPrompt.trim()) {
|
|
585
|
+
// System prompt always goes to temp (not worth persisting)
|
|
586
|
+
if (!tmpPromptDir) tmpPromptDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pi-task-agent-'))
|
|
541
587
|
const systemFilePath = path.join(tmpPromptDir, `system-${agent.name.replace(/[^\w.-]+/g, '_')}.md`)
|
|
542
588
|
fs.writeFileSync(systemFilePath, agent.systemPrompt, { encoding: 'utf-8', mode: 0o600 })
|
|
543
589
|
args.push('--append-system-prompt', systemFilePath)
|
|
544
590
|
}
|
|
545
591
|
|
|
546
|
-
//
|
|
547
|
-
const taskFilePath = path.join(tmpPromptDir, `task-${agent.name.replace(/[^\w.-]+/g, '_')}.md`)
|
|
548
|
-
fs.writeFileSync(taskFilePath, task, { encoding: 'utf-8', mode: 0o600 })
|
|
549
|
-
// Use forward slashes for the @file syntax (works on all platforms)
|
|
592
|
+
// Pass task file via @file syntax
|
|
550
593
|
args.push(`@${taskFilePath.replace(/\\/g, '/')}`)
|
|
551
594
|
|
|
552
595
|
// Emit initial "Initializing" state
|
|
@@ -569,10 +612,14 @@ async function runSingleAgent(
|
|
|
569
612
|
})
|
|
570
613
|
|
|
571
614
|
return await new Promise<SingleResult>(resolve => {
|
|
615
|
+
// Unless agent has recursive: true, set env var to inhibit nested subagents
|
|
616
|
+
const spawnEnv = agent.recursive ? process.env : { ...process.env, [PI_NO_SUBAGENTS_ENV]: '1' }
|
|
617
|
+
|
|
572
618
|
const proc = spawn(PI_CMD, args, {
|
|
573
619
|
cwd,
|
|
574
620
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
575
621
|
shell: PI_SHELL_OPT,
|
|
622
|
+
env: spawnEnv,
|
|
576
623
|
})
|
|
577
624
|
|
|
578
625
|
let toolCount = 0
|
|
@@ -961,8 +1008,28 @@ function nanoid(size = 12): string {
|
|
|
961
1008
|
}
|
|
962
1009
|
|
|
963
1010
|
const factory: CustomToolFactory = pi => {
|
|
1011
|
+
// Check if subagent spawning is inhibited (we're inside a non-recursive subagent)
|
|
1012
|
+
if (process.env[PI_NO_SUBAGENTS_ENV]) {
|
|
1013
|
+
return [] // No Task tool available in this context
|
|
1014
|
+
}
|
|
1015
|
+
|
|
964
1016
|
const runId = nanoid(8)
|
|
965
|
-
|
|
1017
|
+
|
|
1018
|
+
// Session artifacts directory (sibling to .jsonl file, without extension)
|
|
1019
|
+
// e.g., /path/to/sessions/project/2026-01-01T14-28-11-636Z_uuid/
|
|
1020
|
+
let artifactsDir: string | null = null
|
|
1021
|
+
const tempDir = path.join(os.tmpdir(), `pi-task-${runId}`)
|
|
1022
|
+
|
|
1023
|
+
const updateSessionDir = (event: ToolSessionEvent) => {
|
|
1024
|
+
if (PERSIST_SESSIONS && event.sessionFile) {
|
|
1025
|
+
// Artifacts go in folder matching session file (without .jsonl)
|
|
1026
|
+
// e.g., /path/to/2026-01-01T14-28-11-636Z_uuid/
|
|
1027
|
+
artifactsDir = getSessionArtifactsDir(event.sessionFile)
|
|
1028
|
+
if (artifactsDir) fs.mkdirSync(artifactsDir, { recursive: true })
|
|
1029
|
+
} else {
|
|
1030
|
+
artifactsDir = null
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
966
1033
|
|
|
967
1034
|
const tool: CustomAgentTool<typeof TaskParams, TaskDetails> = {
|
|
968
1035
|
name: 'task',
|
|
@@ -1059,15 +1126,31 @@ const factory: CustomToolFactory = pi => {
|
|
|
1059
1126
|
model: t.model,
|
|
1060
1127
|
}))
|
|
1061
1128
|
|
|
1062
|
-
// Generate
|
|
1063
|
-
|
|
1064
|
-
|
|
1129
|
+
// Generate paths for each agent invocation
|
|
1130
|
+
// Persisted: <artifactsDir>/<agent>_<nanoid>.{jsonl,out.md,in.md}
|
|
1131
|
+
// Ephemeral: <tempDir>/task_<agent>_<idx>.md
|
|
1132
|
+
const agentIds = params.tasks.map(t => `${sanitizeAgentName(t.agent)}_${nanoid(8)}`)
|
|
1133
|
+
|
|
1134
|
+
let outputPaths: string[]
|
|
1135
|
+
let sessionFiles: string[] | undefined
|
|
1136
|
+
let inputFiles: string[] | undefined
|
|
1137
|
+
|
|
1138
|
+
if (artifactsDir) {
|
|
1139
|
+
outputPaths = agentIds.map(id => path.join(artifactsDir!, `${id}.out.md`))
|
|
1140
|
+
sessionFiles = agentIds.map(id => path.join(artifactsDir!, `${id}.jsonl`))
|
|
1141
|
+
inputFiles = agentIds.map(id => path.join(artifactsDir!, `${id}.in.md`))
|
|
1142
|
+
} else {
|
|
1143
|
+
fs.mkdirSync(tempDir, { recursive: true })
|
|
1144
|
+
outputPaths = params.tasks.map((t, i) => path.join(tempDir, `task_${sanitizeAgentName(t.agent)}_${i}.md`))
|
|
1145
|
+
}
|
|
1065
1146
|
|
|
1066
1147
|
const results = await mapWithConcurrencyLimit(tasksWithContext, MAX_CONCURRENCY, async (t, idx) => {
|
|
1067
1148
|
const result = await runSingleAgent(pi.cwd, agents, t.agent, t.task, undefined, {
|
|
1068
1149
|
index: idx,
|
|
1069
1150
|
signal,
|
|
1070
1151
|
model: t.model,
|
|
1152
|
+
sessionFile: sessionFiles?.[idx],
|
|
1153
|
+
inputFile: inputFiles?.[idx],
|
|
1071
1154
|
onProgress: progress => {
|
|
1072
1155
|
progressMap.set(idx, progress)
|
|
1073
1156
|
emitProgress()
|
|
@@ -1115,6 +1198,9 @@ const factory: CustomToolFactory = pi => {
|
|
|
1115
1198
|
}
|
|
1116
1199
|
},
|
|
1117
1200
|
|
|
1201
|
+
// Track parent session for subagent persistence
|
|
1202
|
+
onSession: updateSessionDir,
|
|
1203
|
+
|
|
1118
1204
|
renderCall(args, theme) {
|
|
1119
1205
|
// Return minimal - renderResult handles the full display
|
|
1120
1206
|
if (!args.tasks || args.tasks.length === 0) {
|