@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/subagents",
3
- "version": "1.3.375",
3
+ "version": "1.3.377",
4
4
  "description": "Task delegation system with specialized subagents (task, planner, explore, reviewer, browser)",
5
5
  "keywords": [
6
6
  "omp-plugin",
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('pi', ['--list-models'], {
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', '--no-session', '--mode', 'json']
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 tmpPromptPath: string | null = null
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
- const tmp = writePromptToTempFile(agent.name, agent.systemPrompt)
547
- tmpPromptDir = tmp.dir
548
- tmpPromptPath = tmp.filePath
549
- args.push('--append-system-prompt', tmpPromptPath)
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
- args.push(`Task: ${task}`)
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
- const proc = spawn('pi', args, {
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
- if (tmpPromptPath) {
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.rmdirSync(tmpPromptDir)
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
- const outputDir = path.join(os.tmpdir(), `pi-task-${runId}`)
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 output paths
1070
- fs.mkdirSync(outputDir, { recursive: true })
1071
- const outputPaths = params.tasks.map((t, i) => path.join(outputDir, `task_${sanitizeAgentName(t.agent)}_${i}.md`))
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) {
@@ -4,6 +4,7 @@
4
4
  "maxOutputBytes": 500000,
5
5
  "maxParallelTasks": 32,
6
6
  "maxConcurrency": 16,
7
- "maxAgentsInDescription": 10
7
+ "maxAgentsInDescription": 10,
8
+ "persistSessions": true
8
9
  }
9
10
  }