@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/subagents",
3
- "version": "1.3.376",
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
@@ -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', '--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
+ }
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
- try {
537
- // Create temp dir for task file (avoids Windows shell escaping issues with long/complex CLI args)
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
- // Write task to file and pass as @file to avoid shell escaping issues on Windows
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
- 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
+ }
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 output paths
1063
- fs.mkdirSync(outputDir, { recursive: true })
1064
- 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
+ }
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) {
@@ -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
  }