@oh-my-pi/subagents 1.3.375 → 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 +115 -36
- package/tools/runtime.json +2 -1
package/package.json
CHANGED
package/tools/index.ts
CHANGED
|
@@ -27,13 +27,21 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import { spawn, spawnSync } from 'node:child_process'
|
|
30
|
+
|
|
31
|
+
/** pi command: 'pi.cmd' on Windows, 'pi' elsewhere */
|
|
32
|
+
const PI_CMD = process.platform === 'win32' ? 'pi.cmd' : 'pi'
|
|
33
|
+
/** Windows shell option for spawn/spawnSync when using PI_CMD */
|
|
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'
|
|
37
|
+
|
|
30
38
|
import * as crypto from 'node:crypto'
|
|
31
39
|
import * as fs from 'node:fs'
|
|
32
40
|
import * as os from 'node:os'
|
|
33
41
|
import * as path from 'node:path'
|
|
34
42
|
import * as readline from 'node:readline'
|
|
35
43
|
import { StringEnum } from '@mariozechner/pi-ai'
|
|
36
|
-
import type { CustomAgentTool, CustomToolFactory, ToolAPI } from '@mariozechner/pi-coding-agent'
|
|
44
|
+
import type { CustomAgentTool, CustomToolFactory, ToolAPI, ToolSessionEvent } from '@mariozechner/pi-coding-agent'
|
|
37
45
|
import { Text } from '@mariozechner/pi-tui'
|
|
38
46
|
import { Type } from '@sinclair/typebox'
|
|
39
47
|
import runtime from './runtime.json'
|
|
@@ -49,9 +57,10 @@ function getAvailableModels(): string[] {
|
|
|
49
57
|
if (cachedModels !== null) return cachedModels
|
|
50
58
|
|
|
51
59
|
try {
|
|
52
|
-
const result = spawnSync(
|
|
60
|
+
const result = spawnSync(PI_CMD, ['--list-models'], {
|
|
53
61
|
encoding: 'utf-8',
|
|
54
62
|
timeout: 5000,
|
|
63
|
+
shell: PI_SHELL_OPT,
|
|
55
64
|
})
|
|
56
65
|
|
|
57
66
|
if (result.status !== 0 || !result.stdout) {
|
|
@@ -111,6 +120,22 @@ const MAX_PARALLEL_TASKS = runtime.options.maxParallelTasks ?? 32
|
|
|
111
120
|
const MAX_CONCURRENCY = runtime.options.maxConcurrency ?? 16
|
|
112
121
|
const MAX_AGENTS_IN_DESCRIPTION = runtime.options.maxAgentsInDescription ?? 10
|
|
113
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
|
+
|
|
114
139
|
type AgentScope = 'user' | 'project' | 'both'
|
|
115
140
|
|
|
116
141
|
interface AgentConfig {
|
|
@@ -119,6 +144,8 @@ interface AgentConfig {
|
|
|
119
144
|
tools?: string[]
|
|
120
145
|
model?: string
|
|
121
146
|
forkContext?: boolean
|
|
147
|
+
/** If true, this agent can spawn subagents. Default: false (subagents inhibited) */
|
|
148
|
+
recursive?: boolean
|
|
122
149
|
systemPrompt: string
|
|
123
150
|
source: 'user' | 'project'
|
|
124
151
|
filePath: string
|
|
@@ -245,12 +272,15 @@ function loadAgentsFromDir(dir: string, source: 'user' | 'project'): AgentConfig
|
|
|
245
272
|
const forkContext =
|
|
246
273
|
frontmatter.forkContext === undefined ? undefined : frontmatter.forkContext === 'true' || frontmatter.forkContext === '1'
|
|
247
274
|
|
|
275
|
+
const recursive = frontmatter.recursive === undefined ? undefined : frontmatter.recursive === 'true' || frontmatter.recursive === '1'
|
|
276
|
+
|
|
248
277
|
agents.push({
|
|
249
278
|
name: frontmatter.name,
|
|
250
279
|
description: frontmatter.description,
|
|
251
280
|
tools: tools && tools.length > 0 ? tools : undefined,
|
|
252
281
|
model: frontmatter.model,
|
|
253
282
|
forkContext,
|
|
283
|
+
recursive,
|
|
254
284
|
systemPrompt: body,
|
|
255
285
|
source,
|
|
256
286
|
filePath,
|
|
@@ -463,25 +493,15 @@ async function mapWithConcurrencyLimit<TIn, TOut>(
|
|
|
463
493
|
return results
|
|
464
494
|
}
|
|
465
495
|
|
|
466
|
-
function writePromptToTempFile(agentName: string, prompt: string): { dir: string; filePath: string } {
|
|
467
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pi-task-agent-'))
|
|
468
|
-
const safeName = agentName.replace(/[^\w.-]+/g, '_')
|
|
469
|
-
const filePath = path.join(tmpDir, `prompt-${safeName}.md`)
|
|
470
|
-
try {
|
|
471
|
-
// mode: 0o600 restricts file permissions on Unix; ignored on Windows
|
|
472
|
-
fs.writeFileSync(filePath, prompt, { encoding: 'utf-8', mode: 0o600 })
|
|
473
|
-
} catch (err) {
|
|
474
|
-
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
475
|
-
throw err
|
|
476
|
-
}
|
|
477
|
-
return { dir: tmpDir, filePath }
|
|
478
|
-
}
|
|
479
|
-
|
|
480
496
|
interface RunAgentOptions {
|
|
481
497
|
onProgress?: (progress: AgentProgress) => void
|
|
482
498
|
index?: number
|
|
483
499
|
signal?: AbortSignal
|
|
484
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
|
|
485
505
|
}
|
|
486
506
|
|
|
487
507
|
async function runSingleAgent(
|
|
@@ -524,7 +544,14 @@ async function runSingleAgent(
|
|
|
524
544
|
}
|
|
525
545
|
}
|
|
526
546
|
|
|
527
|
-
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
|
+
}
|
|
528
555
|
|
|
529
556
|
// "default" means no model override - use pi's configured default
|
|
530
557
|
const modelOverride = options?.model
|
|
@@ -538,18 +565,32 @@ async function runSingleAgent(
|
|
|
538
565
|
args.push('--tools', agent.tools.join(','))
|
|
539
566
|
}
|
|
540
567
|
|
|
568
|
+
// Use provided inputFile (persistent) or create temp file
|
|
541
569
|
let tmpPromptDir: string | null = null
|
|
542
|
-
let
|
|
570
|
+
let taskFilePath: string
|
|
571
|
+
|
|
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)
|
|
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
|
+
}
|
|
543
582
|
|
|
544
583
|
try {
|
|
545
584
|
if (agent.systemPrompt.trim()) {
|
|
546
|
-
|
|
547
|
-
tmpPromptDir =
|
|
548
|
-
|
|
549
|
-
|
|
585
|
+
// System prompt always goes to temp (not worth persisting)
|
|
586
|
+
if (!tmpPromptDir) tmpPromptDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pi-task-agent-'))
|
|
587
|
+
const systemFilePath = path.join(tmpPromptDir, `system-${agent.name.replace(/[^\w.-]+/g, '_')}.md`)
|
|
588
|
+
fs.writeFileSync(systemFilePath, agent.systemPrompt, { encoding: 'utf-8', mode: 0o600 })
|
|
589
|
+
args.push('--append-system-prompt', systemFilePath)
|
|
550
590
|
}
|
|
551
591
|
|
|
552
|
-
|
|
592
|
+
// Pass task file via @file syntax
|
|
593
|
+
args.push(`@${taskFilePath.replace(/\\/g, '/')}`)
|
|
553
594
|
|
|
554
595
|
// Emit initial "Initializing" state
|
|
555
596
|
options?.onProgress?.({
|
|
@@ -571,9 +612,14 @@ async function runSingleAgent(
|
|
|
571
612
|
})
|
|
572
613
|
|
|
573
614
|
return await new Promise<SingleResult>(resolve => {
|
|
574
|
-
|
|
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
|
+
|
|
618
|
+
const proc = spawn(PI_CMD, args, {
|
|
575
619
|
cwd,
|
|
576
620
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
621
|
+
shell: PI_SHELL_OPT,
|
|
622
|
+
env: spawnEnv,
|
|
577
623
|
})
|
|
578
624
|
|
|
579
625
|
let toolCount = 0
|
|
@@ -782,16 +828,10 @@ async function runSingleAgent(
|
|
|
782
828
|
})
|
|
783
829
|
})
|
|
784
830
|
} finally {
|
|
785
|
-
|
|
786
|
-
try {
|
|
787
|
-
fs.unlinkSync(tmpPromptPath)
|
|
788
|
-
} catch {
|
|
789
|
-
/* ignore */
|
|
790
|
-
}
|
|
791
|
-
}
|
|
831
|
+
// Clean up temp directory (contains system prompt and task files)
|
|
792
832
|
if (tmpPromptDir) {
|
|
793
833
|
try {
|
|
794
|
-
fs.
|
|
834
|
+
fs.rmSync(tmpPromptDir, { recursive: true, force: true })
|
|
795
835
|
} catch {
|
|
796
836
|
/* ignore */
|
|
797
837
|
}
|
|
@@ -968,8 +1008,28 @@ function nanoid(size = 12): string {
|
|
|
968
1008
|
}
|
|
969
1009
|
|
|
970
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
|
+
|
|
971
1016
|
const runId = nanoid(8)
|
|
972
|
-
|
|
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
|
+
}
|
|
973
1033
|
|
|
974
1034
|
const tool: CustomAgentTool<typeof TaskParams, TaskDetails> = {
|
|
975
1035
|
name: 'task',
|
|
@@ -1066,15 +1126,31 @@ const factory: CustomToolFactory = pi => {
|
|
|
1066
1126
|
model: t.model,
|
|
1067
1127
|
}))
|
|
1068
1128
|
|
|
1069
|
-
// Generate
|
|
1070
|
-
|
|
1071
|
-
|
|
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
|
+
}
|
|
1072
1146
|
|
|
1073
1147
|
const results = await mapWithConcurrencyLimit(tasksWithContext, MAX_CONCURRENCY, async (t, idx) => {
|
|
1074
1148
|
const result = await runSingleAgent(pi.cwd, agents, t.agent, t.task, undefined, {
|
|
1075
1149
|
index: idx,
|
|
1076
1150
|
signal,
|
|
1077
1151
|
model: t.model,
|
|
1152
|
+
sessionFile: sessionFiles?.[idx],
|
|
1153
|
+
inputFile: inputFiles?.[idx],
|
|
1078
1154
|
onProgress: progress => {
|
|
1079
1155
|
progressMap.set(idx, progress)
|
|
1080
1156
|
emitProgress()
|
|
@@ -1122,6 +1198,9 @@ const factory: CustomToolFactory = pi => {
|
|
|
1122
1198
|
}
|
|
1123
1199
|
},
|
|
1124
1200
|
|
|
1201
|
+
// Track parent session for subagent persistence
|
|
1202
|
+
onSession: updateSessionDir,
|
|
1203
|
+
|
|
1125
1204
|
renderCall(args, theme) {
|
|
1126
1205
|
// Return minimal - renderResult handles the full display
|
|
1127
1206
|
if (!args.tasks || args.tasks.length === 0) {
|