pi-cohort 7.1.1 → 7.2.0
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/CHANGELOG.md +12 -0
- package/README.md +6 -0
- package/package.json +11 -7
- package/src/execution-backend/child-host-controller.ts +10 -8
- package/src/extension/doctor.ts +6 -2
- package/src/extension/fanout-child.ts +2 -2
- package/src/extension/index.ts +8 -6
- package/src/runs/background/async-execution.ts +160 -87
- package/src/runs/background/async-job-tracker.ts +4 -1
- package/src/runs/background/async-resume.ts +93 -17
- package/src/runs/background/async-status.ts +12 -9
- package/src/runs/background/external-attempt.ts +6 -2
- package/src/runs/background/run-id-resolver.ts +15 -18
- package/src/runs/background/run-status.ts +22 -11
- package/src/runs/background/stale-run-reconciler.ts +9 -1
- package/src/runs/background/subagent-runner.ts +54 -79
- package/src/runs/foreground/chain-execution.ts +7 -0
- package/src/runs/foreground/execution.ts +30 -13
- package/src/runs/foreground/external-single-attempt.ts +3 -2
- package/src/runs/foreground/subagent-executor.ts +142 -63
- package/src/runs/shared/external-execution.ts +2 -0
- package/src/runs/shared/jiti-cli.ts +27 -27
- package/src/runs/shared/nested-events.ts +103 -30
- package/src/runs/shared/pi-spawn.ts +75 -67
- package/src/shared/agent-dir.ts +12 -0
- package/src/shared/atomic-json.ts +12 -1
- package/src/shared/fork-context.ts +15 -3
- package/src/shared/types.ts +11 -1
- package/src/shared/utils.ts +1 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [7.2.0] - 2026-10-11
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Children and detached coordinators launch with the originating parent's Node/Pi paths instead of `PATH`; revival keeps that runtime across parent installations. Run records and nested state use durable profile storage without automatic age expiry. Legacy records remain inspectable in their original paths, but revival requires a recorded original runtime and an available session file; missing installations require restoration or a new run. (#19)
|
|
8
|
+
|
|
9
|
+
## [7.1.2] - 2026-09-29
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `typebox` moves from `dependencies` to an optional `peerDependencies: "*"` entry, which silences pi's "Host-provided extension packages must be declared in peerDependencies" startup warning and removes the second typebox copy from consumer installs. The detached async runner keeps working without its own copy: `spawnRunner` now passes pi's own `typebox`, `typebox/compile`, and `typebox/value` files to the bare jiti child through `JITI_ALIAS`, the same alias map pi's extension loader applies in-process, so the `Cannot find module 'typebox/compile'` crash from [#9](https://github.com/jjuraszek/pi-cohort/issues/9) stays fixed.
|
|
14
|
+
|
|
3
15
|
## [7.1.1] - 2026-09-24
|
|
4
16
|
|
|
5
17
|
### Changed
|
package/README.md
CHANGED
|
@@ -39,6 +39,8 @@ pi install npm:pi-cohort
|
|
|
39
39
|
|
|
40
40
|
That is the only required step.
|
|
41
41
|
|
|
42
|
+
Children and detached coordinators use the originating parent's resolved Node executable and Pi entry path directly, bypassing `PATH`. Revival keeps that original runtime even when requested from a different parent installation. Custom hosts whose entry cannot be identified as the installed Pi package's declared CLI are unsupported. These paths are not a version lock: replacing Node or Pi in place changes what runs. If the original installation is removed, restore it at the recorded paths or start a new run; there is no fallback to the current parent's installation. Legacy records without an original runtime identity remain inspectable but cannot be revived - start a new run.
|
|
43
|
+
|
|
42
44
|
## Execution backend extension API
|
|
43
45
|
|
|
44
46
|
External execution backends are currently unsupported on Windows, for both foreground and background runs. Native execution remains supported.
|
|
@@ -155,6 +157,10 @@ Full reference: agent/chain authoring in [doc/agents-and-chains.md](doc/agents-a
|
|
|
155
157
|
|
|
156
158
|
Most installs need zero configuration. Full env/JSON reference: [doc/configuration.md](doc/configuration.md).
|
|
157
159
|
|
|
160
|
+
Run history lives under the pinned profile's `pi-cohort/` root and has no automatic age expiry. Status listings can grow without bound as history accumulates. To remove history manually, delete only a terminal top-level run directory after confirming its entire subtree has stopped; never delete queued or running state, individual nested records, or shared route files. This removes that directory's status and resume lookup, not separately stored session files or shared routes. Keep those sessions if you need their context; deleting a run directory is not a complete session purge. Storage details: [doc/observability.md](doc/observability.md#files-logs-and-observability).
|
|
161
|
+
|
|
162
|
+
A queued start that never publishes running status stays unresolved: pi-cohort does not automatically recover or revive it. Start a new run instead.
|
|
163
|
+
|
|
158
164
|
Parent extension CLI flags (e.g. pi-lens's `--no-autofix`) are forwarded into spawned children by default, so subagents inherit the same tooling behavior you launched with; toggle with [`forwardParentFlags`](doc/configuration.md#forwardparentflags).
|
|
159
165
|
|
|
160
166
|
Optional companions:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cohort",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.2.0",
|
|
4
4
|
"description": "Delegate Pi work to focused child agents: code review, scouting, implementation, parallel audits, saved chains, and background jobs.",
|
|
5
5
|
"author": "Jacek Juraszek",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"scripts": {
|
|
41
41
|
"check:agents-core": "node scripts/check-agents-core.mjs",
|
|
42
42
|
"test": "npm run test:unit",
|
|
43
|
-
"test:unit": "node --test test/unit/*.test.ts",
|
|
44
|
-
"test:integration": "node --import ./test/support/register-loader.mjs --test test/integration/*.test.ts",
|
|
43
|
+
"test:unit": "node --import ./test/support/isolate-test-env.mjs --test test/unit/*.test.ts",
|
|
44
|
+
"test:integration": "node --import ./test/support/isolate-test-env.mjs --import ./test/support/register-loader.mjs --test test/integration/*.test.ts",
|
|
45
45
|
"test:all": "npm run check:agents-core && npm run test:unit && npm run test:integration"
|
|
46
46
|
},
|
|
47
47
|
"pi": {
|
|
@@ -64,7 +64,8 @@
|
|
|
64
64
|
"@earendil-works/pi-agent-core": "*",
|
|
65
65
|
"@earendil-works/pi-ai": "*",
|
|
66
66
|
"@earendil-works/pi-coding-agent": ">=0.85.0",
|
|
67
|
-
"@earendil-works/pi-tui": "*"
|
|
67
|
+
"@earendil-works/pi-tui": "*",
|
|
68
|
+
"typebox": "*"
|
|
68
69
|
},
|
|
69
70
|
"peerDependenciesMeta": {
|
|
70
71
|
"@earendil-works/pi-agent-core": {
|
|
@@ -78,16 +79,19 @@
|
|
|
78
79
|
},
|
|
79
80
|
"@earendil-works/pi-tui": {
|
|
80
81
|
"optional": true
|
|
82
|
+
},
|
|
83
|
+
"typebox": {
|
|
84
|
+
"optional": true
|
|
81
85
|
}
|
|
82
86
|
},
|
|
83
87
|
"dependencies": {
|
|
84
|
-
"jiti": "^2.7.0"
|
|
85
|
-
"typebox": "^1.3.11"
|
|
88
|
+
"jiti": "^2.7.0"
|
|
86
89
|
},
|
|
87
90
|
"devDependencies": {
|
|
88
91
|
"@earendil-works/pi-agent-core": "^0.85.0",
|
|
89
92
|
"@earendil-works/pi-ai": "^0.85.0",
|
|
90
93
|
"@earendil-works/pi-coding-agent": "^0.85.0",
|
|
91
|
-
"@earendil-works/pi-tui": "^0.85.0"
|
|
94
|
+
"@earendil-works/pi-tui": "^0.85.0",
|
|
95
|
+
"typebox": "^1.3.11"
|
|
92
96
|
}
|
|
93
97
|
}
|
|
@@ -7,18 +7,20 @@ import { ChildHostFrameDecoder, encodeChildHostMessage, type ChildHostMessage }
|
|
|
7
7
|
import { PI_COHORT_CHILD_HOST_CONFIG } from "./child-host-runtime.ts";
|
|
8
8
|
import { ensureJitiCliPath } from "../runs/shared/jiti-cli.ts";
|
|
9
9
|
import type { ExecutionBackend, ExecutionBackendLease } from "./types.ts";
|
|
10
|
+
import { getPiSpawnCommand, PiRuntimeLaunchError, type PiRuntimeIdentity } from "../runs/shared/pi-spawn.ts";
|
|
10
11
|
|
|
11
12
|
export interface ChildHostAttempt { readonly attemptId: string; readonly command: string; readonly args: readonly string[]; readonly cwd: string; readonly environment: Readonly<Record<string, string>>; }
|
|
12
13
|
export interface ChildHostExit { readonly status: number | null; readonly signal: string | null; }
|
|
13
14
|
export interface ChildHostController { readonly lease: ExecutionBackendLease; readonly ready: Promise<void>; startAttempt(attempt: ChildHostAttempt): Promise<ChildHostExit>; shutdown(): Promise<void>; releaseObserver(): Promise<void>; }
|
|
14
|
-
export interface ChildHostControllerOptions { readonly backend: ExecutionBackend; readonly runId: string; readonly childId: string; readonly cwd: string; readonly title?: string; readonly signal: AbortSignal; }
|
|
15
|
-
export interface ChildHostControllerDependencies { readonly fs?: typeof fs; readonly ensureJiti?: () => string | undefined; readonly childHostCommand?: () => { command: string; args: readonly string[] }; readonly platform?: NodeJS.Platform; readonly createServer?: typeof net.createServer; }
|
|
15
|
+
export interface ChildHostControllerOptions { readonly runtime: PiRuntimeIdentity; readonly backend: ExecutionBackend; readonly runId: string; readonly childId: string; readonly cwd: string; readonly title?: string; readonly signal: AbortSignal; }
|
|
16
|
+
export interface ChildHostControllerDependencies { readonly fs?: typeof fs; readonly ensureJiti?: (root: string) => string | undefined; readonly childHostCommand?: () => { command: string; args: readonly string[] }; readonly platform?: NodeJS.Platform; readonly createServer?: typeof net.createServer; }
|
|
16
17
|
|
|
17
18
|
const HOST_ENVIRONMENT_KEYS = ["PATH", "HOME", "TMPDIR", "SystemRoot"] as const;
|
|
18
|
-
function childHostCommand(ensureJiti: () => string | undefined): { command: string; args: readonly string[] } {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
function childHostCommand(runtime: PiRuntimeIdentity, ensureJiti: (root: string) => string | undefined): { command: string; args: readonly string[] } {
|
|
20
|
+
getPiSpawnCommand([], runtime);
|
|
21
|
+
const jiti = ensureJiti(runtime.piPackageRoot);
|
|
22
|
+
if (!jiti) throw new PiRuntimeLaunchError(`jiti for selected Pi installation ${runtime.piPackageRoot} could not be found`);
|
|
23
|
+
return { command: runtime.nodeExecPath, args: [jiti, path.resolve(path.dirname(fileURLToPath(import.meta.url)), "child-host-runtime.ts")] };
|
|
22
24
|
}
|
|
23
25
|
function disconnected(phase: string): Error { return new Error(`child host disconnected ${phase}`); }
|
|
24
26
|
function aborted(): Error { return new Error("child host launch aborted"); }
|
|
@@ -90,7 +92,7 @@ export async function createChildHostController(options: ChildHostControllerOpti
|
|
|
90
92
|
}
|
|
91
93
|
if (message.kind === "attempt_started" && active?.attemptId === message.attemptId && !active.started) { active.started = true; continue; }
|
|
92
94
|
if (message.kind === "attempt_spawn_error" && active?.attemptId === message.attemptId) {
|
|
93
|
-
const pending = active; active = undefined; pending.reject(new
|
|
95
|
+
const pending = active; active = undefined; pending.reject(new PiRuntimeLaunchError(`child host attempt spawn error for ${options.runtime.nodeExecPath} ${options.runtime.piEntryPath}`)); continue;
|
|
94
96
|
}
|
|
95
97
|
if (message.kind === "attempt_exited" && active?.attemptId === message.attemptId && active.started) {
|
|
96
98
|
const pending = active; active = undefined; pending.resolve({ status: message.status, signal: message.signal }); continue;
|
|
@@ -115,9 +117,9 @@ export async function createChildHostController(options: ChildHostControllerOpti
|
|
|
115
117
|
await new Promise<void>((resolve, reject) => server!.listen(socketPath!, () => resolve()).once("error", reject));
|
|
116
118
|
fileSystem.chmodSync(socketPath, 0o600);
|
|
117
119
|
if (options.signal.aborted) throw aborted();
|
|
118
|
-
const host = dependencies.childHostCommand?.() ?? childHostCommand(dependencies.ensureJiti ?? ensureJitiCliPath);
|
|
119
120
|
const environment: Record<string, string> = { [PI_COHORT_CHILD_HOST_CONFIG]: configPath };
|
|
120
121
|
for (const key of HOST_ENVIRONMENT_KEYS) if (process.env[key]) environment[key] = process.env[key]!;
|
|
122
|
+
const host = dependencies.childHostCommand?.() ?? childHostCommand(options.runtime, dependencies.ensureJiti ?? ensureJitiCliPath);
|
|
121
123
|
lease = await options.backend.launch({
|
|
122
124
|
command: host.command,
|
|
123
125
|
args: host.args,
|
package/src/extension/doctor.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { discoverAgentsAll, type AgentSource } from "../agents/agents.ts";
|
|
4
|
+
import type { PiRuntimeIdentity } from "../runs/shared/pi-spawn.ts";
|
|
4
5
|
import { isAsyncAvailable } from "../runs/background/async-execution.ts";
|
|
5
6
|
import { discoverAvailableSkills, type SkillSource } from "../agents/skills.ts";
|
|
6
7
|
import {
|
|
@@ -20,12 +21,14 @@ interface DoctorPaths {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
interface DoctorDeps {
|
|
23
|
-
isAsyncAvailable: () => boolean;
|
|
24
|
+
isAsyncAvailable: (runtime: PiRuntimeIdentity) => boolean;
|
|
24
25
|
discoverAgentsAll: typeof discoverAgentsAll;
|
|
25
26
|
discoverAvailableSkills: typeof discoverAvailableSkills;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
interface DoctorReportInput {
|
|
30
|
+
runtime?: PiRuntimeIdentity;
|
|
31
|
+
runtimeError?: string;
|
|
29
32
|
cwd: string;
|
|
30
33
|
config: ExtensionConfig;
|
|
31
34
|
state: SubagentState;
|
|
@@ -189,7 +192,8 @@ export function buildDoctorReport(input: DoctorReportInput): string {
|
|
|
189
192
|
"",
|
|
190
193
|
"Runtime",
|
|
191
194
|
`- cwd: ${input.cwd}`,
|
|
192
|
-
|
|
195
|
+
...(input.runtimeError ? [`- runtime capture: failed - ${input.runtimeError}`] : input.runtime ? [] : ["- runtime capture: not available"]),
|
|
196
|
+
lineFromCheck("async support", () => `- async support: ${input.runtime && deps.isAsyncAvailable(input.runtime) ? "available" : "unavailable"}`),
|
|
193
197
|
...formatSessionLines(input),
|
|
194
198
|
"",
|
|
195
199
|
"Filesystem",
|
|
@@ -10,7 +10,7 @@ import { readNestedControlRequests, resolveNestedRouteFromEnv, writeNestedContro
|
|
|
10
10
|
import { SubagentParams } from "./schemas.ts";
|
|
11
11
|
import { loadConfig } from "./config.ts";
|
|
12
12
|
import { deriveForwardedFlags } from "../runs/shared/forward-flags.ts";
|
|
13
|
-
import type
|
|
13
|
+
import { RUN_STATE_ROOT_DIR, type Details, type SubagentState } from "../shared/types.ts";
|
|
14
14
|
import { emptyGrandTotal } from "./grand-total.ts";
|
|
15
15
|
|
|
16
16
|
function getSubagentSessionRoot(parentSessionFile: string | null): string {
|
|
@@ -19,7 +19,7 @@ function getSubagentSessionRoot(parentSessionFile: string | null): string {
|
|
|
19
19
|
const sessionsDir = path.dirname(parentSessionFile);
|
|
20
20
|
return path.join(sessionsDir, baseName);
|
|
21
21
|
}
|
|
22
|
-
return
|
|
22
|
+
return path.join(RUN_STATE_ROOT_DIR, "sessions");
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function expandTilde(p: string): string {
|
package/src/extension/index.ts
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
type Details,
|
|
44
44
|
type SubagentState,
|
|
45
45
|
ASYNC_DIR,
|
|
46
|
+
RUN_STATE_ROOT_DIR,
|
|
46
47
|
DEFAULT_ARTIFACT_CONFIG,
|
|
47
48
|
GRAND_TOTAL_STATUS_KEY,
|
|
48
49
|
RESULTS_DIR,
|
|
@@ -68,7 +69,7 @@ export { loadConfig } from "./config.ts";
|
|
|
68
69
|
* If parent session is ~/.pi/agent/sessions/abc123.jsonl,
|
|
69
70
|
* returns ~/.pi/agent/sessions/abc123/ as the base.
|
|
70
71
|
* Callers add runId to create the actual session root: abc123/{runId}/
|
|
71
|
-
*
|
|
72
|
+
* Without a parent, uses the durable sessions base; callers append the run UUID.
|
|
72
73
|
*/
|
|
73
74
|
function getSubagentSessionRoot(parentSessionFile: string | null): string {
|
|
74
75
|
if (parentSessionFile) {
|
|
@@ -76,19 +77,19 @@ function getSubagentSessionRoot(parentSessionFile: string | null): string {
|
|
|
76
77
|
const sessionsDir = path.dirname(parentSessionFile);
|
|
77
78
|
return path.join(sessionsDir, baseName);
|
|
78
79
|
}
|
|
79
|
-
return
|
|
80
|
+
return path.join(RUN_STATE_ROOT_DIR, "sessions");
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
function expandTilde(p: string): string {
|
|
83
84
|
return p.startsWith("~/") ? path.join(os.homedir(), p.slice(2)) : p;
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
function ensureAccessibleDir(dirPath: string): void {
|
|
87
|
+
function ensureAccessibleDir(dirPath: string, durable = false): void {
|
|
88
88
|
mkdirWithEpermRetry(dirPath);
|
|
89
89
|
try {
|
|
90
90
|
fs.accessSync(dirPath, fs.constants.R_OK | fs.constants.W_OK);
|
|
91
|
-
} catch {
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (durable) throw error;
|
|
92
93
|
try {
|
|
93
94
|
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
94
95
|
} catch {
|
|
@@ -255,8 +256,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
255
256
|
}
|
|
256
257
|
}
|
|
257
258
|
|
|
259
|
+
ensureAccessibleDir(RUN_STATE_ROOT_DIR, true);
|
|
258
260
|
ensureAccessibleDir(RESULTS_DIR);
|
|
259
|
-
ensureAccessibleDir(ASYNC_DIR);
|
|
261
|
+
ensureAccessibleDir(ASYNC_DIR, true);
|
|
260
262
|
cleanupOldChainDirs();
|
|
261
263
|
|
|
262
264
|
const config = loadConfig();
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
-
import
|
|
6
|
+
import { PINNED_AGENT_DIR_OVERRIDE } from "../../shared/agent-dir.ts";
|
|
7
|
+
import fs from "node:fs";
|
|
7
8
|
import * as os from "node:os";
|
|
8
9
|
import * as path from "node:path";
|
|
9
10
|
import { fileURLToPath } from "node:url";
|
|
@@ -12,9 +13,10 @@ import type { AgentConfig } from "../../agents/agents.ts";
|
|
|
12
13
|
import { applyThinkingSuffix } from "../shared/pi-args.ts";
|
|
13
14
|
import { injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveParallelTaskOutputPath, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
14
15
|
import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
|
|
15
|
-
import
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
16
|
+
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
17
|
+
import { isDynamicRunnerGroup, isParallelGroup, type RunnerStep } from "../shared/parallel-utils.ts";
|
|
18
|
+
import { getPiSpawnCommand, PiRuntimeLaunchError, type PiRuntimeIdentity } from "../shared/pi-spawn.ts";
|
|
19
|
+
import { ensureJitiCliPath, resolveHostModuleAliases } from "../shared/jiti-cli.ts";
|
|
18
20
|
export { createJitiCliResolver } from "../shared/jiti-cli.ts";
|
|
19
21
|
import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
|
|
20
22
|
import { resolveChildCwd } from "../../shared/utils.ts";
|
|
@@ -39,14 +41,14 @@ import {
|
|
|
39
41
|
RESULTS_DIR,
|
|
40
42
|
SUBAGENT_ASYNC_STARTED_EVENT,
|
|
41
43
|
TEMP_ROOT_DIR,
|
|
44
|
+
RUN_STATE_ROOT_DIR,
|
|
42
45
|
getAsyncConfigPath,
|
|
43
46
|
resolveChildMaxSubagentDepth,
|
|
44
47
|
} from "../../shared/types.ts";
|
|
45
48
|
import { nestedResultsPath, resolveInheritedNestedRouteFromEnv, resolveNestedParentAddressFromEnv, writeNestedEvent } from "../shared/nested-events.ts";
|
|
46
49
|
|
|
47
|
-
const piPackageRoot = resolvePiPackageRoot();
|
|
48
|
-
|
|
49
50
|
interface AsyncExecutionContext {
|
|
51
|
+
runtime: PiRuntimeIdentity;
|
|
50
52
|
pi: ExtensionAPI;
|
|
51
53
|
cwd: string;
|
|
52
54
|
currentSessionId: string;
|
|
@@ -73,6 +75,7 @@ interface AsyncChainParams {
|
|
|
73
75
|
sessionRoot?: string;
|
|
74
76
|
chainSkills?: string[];
|
|
75
77
|
sessionFilesByFlatIndex?: (string | undefined)[];
|
|
78
|
+
originSessionRoot?: string;
|
|
76
79
|
dynamicFanoutMaxItems?: number;
|
|
77
80
|
maxSubagentDepth: number;
|
|
78
81
|
worktreeSetupHook?: string;
|
|
@@ -103,6 +106,7 @@ interface AsyncSingleParams {
|
|
|
103
106
|
shareEnabled: boolean;
|
|
104
107
|
sessionRoot?: string;
|
|
105
108
|
sessionFile?: string;
|
|
109
|
+
originSessionRoot?: string;
|
|
106
110
|
skills?: string[];
|
|
107
111
|
output?: string | boolean;
|
|
108
112
|
outputMode?: "inline" | "file-only";
|
|
@@ -136,8 +140,8 @@ export function formatAsyncStartedMessage(headline: string): string {
|
|
|
136
140
|
/**
|
|
137
141
|
* Check if jiti is available for async execution
|
|
138
142
|
*/
|
|
139
|
-
export function isAsyncAvailable(): boolean {
|
|
140
|
-
return ensureJitiCliPath() !== undefined;
|
|
143
|
+
export function isAsyncAvailable(runtime: PiRuntimeIdentity): boolean {
|
|
144
|
+
return ensureJitiCliPath(runtime.piPackageRoot) !== undefined;
|
|
141
145
|
}
|
|
142
146
|
|
|
143
147
|
/**
|
|
@@ -145,7 +149,7 @@ export function isAsyncAvailable(): boolean {
|
|
|
145
149
|
* Falls back to ignored stdio if the log file cannot be opened; the parent always
|
|
146
150
|
* closes its copy of the fd, on every exit path.
|
|
147
151
|
*/
|
|
148
|
-
export function spawnDetachedWithLog(command: string, args: string[], cwd: string, asyncDir: string, spawnImpl: typeof spawn = spawn): { pid?: number; error?: string } {
|
|
152
|
+
export function spawnDetachedWithLog(command: string, args: string[], cwd: string, asyncDir: string, spawnImpl: typeof spawn = spawn, env?: NodeJS.ProcessEnv): { pid?: number; error?: string } {
|
|
149
153
|
let fd: number | undefined;
|
|
150
154
|
try {
|
|
151
155
|
try {
|
|
@@ -156,6 +160,7 @@ export function spawnDetachedWithLog(command: string, args: string[], cwd: strin
|
|
|
156
160
|
}
|
|
157
161
|
const proc = spawnImpl(command, args, {
|
|
158
162
|
cwd,
|
|
163
|
+
env,
|
|
159
164
|
detached: true,
|
|
160
165
|
stdio: fd === undefined ? "ignore" : ["ignore", fd, fd],
|
|
161
166
|
windowsHide: true,
|
|
@@ -183,18 +188,19 @@ export function spawnDetachedWithLog(command: string, args: string[], cwd: strin
|
|
|
183
188
|
* Spawn the async runner process
|
|
184
189
|
*/
|
|
185
190
|
export function spawnRunner(
|
|
186
|
-
cfg:
|
|
191
|
+
cfg: { runtime: PiRuntimeIdentity },
|
|
187
192
|
suffix: string,
|
|
188
193
|
cwd: string,
|
|
189
194
|
asyncDir: string,
|
|
190
|
-
deps: { ensureJiti?: () => string | undefined; spawnImpl?: typeof spawn } = {},
|
|
195
|
+
deps: { ensureJiti?: (root: string) => string | undefined; spawnImpl?: typeof spawn; hostAliases?: (root: string) => Record<string, string> } = {},
|
|
191
196
|
): { pid?: number; error?: string } {
|
|
192
197
|
const ensureJiti = deps.ensureJiti ?? ensureJitiCliPath;
|
|
193
|
-
const jitiCliPath = ensureJiti();
|
|
198
|
+
const jitiCliPath = ensureJiti(cfg.runtime.piPackageRoot);
|
|
194
199
|
if (!jitiCliPath) {
|
|
195
|
-
|
|
200
|
+
throw new PiRuntimeLaunchError(`Cannot resolve jiti from selected Pi installation ${cfg.runtime.piPackageRoot}`);
|
|
196
201
|
}
|
|
197
202
|
|
|
203
|
+
getPiSpawnCommand([], cfg.runtime);
|
|
198
204
|
try {
|
|
199
205
|
const cwdStats = fs.statSync(cwd);
|
|
200
206
|
if (!cwdStats.isDirectory()) {
|
|
@@ -209,7 +215,122 @@ export function spawnRunner(
|
|
|
209
215
|
fs.writeFileSync(cfgPath, JSON.stringify(cfg));
|
|
210
216
|
const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
|
|
211
217
|
|
|
212
|
-
|
|
218
|
+
const aliases = (deps.hostAliases ?? resolveHostModuleAliases)(cfg.runtime.piPackageRoot);
|
|
219
|
+
const env = Object.keys(aliases).length > 0
|
|
220
|
+
? { ...process.env, JITI_ALIAS: JSON.stringify(aliases) }
|
|
221
|
+
: { ...process.env };
|
|
222
|
+
if (PINNED_AGENT_DIR_OVERRIDE) env.PI_CODING_AGENT_DIR = PINNED_AGENT_DIR_OVERRIDE;
|
|
223
|
+
|
|
224
|
+
return spawnDetachedWithLog(cfg.runtime.nodeExecPath, [jitiCliPath, runner, cfgPath], cwd, asyncDir, deps.spawnImpl ?? spawn, env);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
type NestedStartWriter = (...args: Parameters<typeof writeNestedEvent>) => void | boolean;
|
|
228
|
+
|
|
229
|
+
function publishAndSpawnRunner(
|
|
230
|
+
cfg: { runtime: PiRuntimeIdentity; id: string; steps: RunnerStep[]; sessionId: string; sessionDir: string; resultMode: SubagentRunMode; [key: string]: unknown },
|
|
231
|
+
suffix: string,
|
|
232
|
+
cwd: string,
|
|
233
|
+
asyncDir: string,
|
|
234
|
+
origin: { route?: NestedRouteInfo; address?: ReturnType<typeof resolveNestedParentAddressFromEnv>; sessionFile?: string; sessionFiles?: string[]; sessionRoot?: string },
|
|
235
|
+
writeStart: NestedStartWriter,
|
|
236
|
+
): { pid?: number; error?: string } {
|
|
237
|
+
const parallelGroups: Array<{ start: number; count: number; stepIndex: number }> = [];
|
|
238
|
+
const steps: object[] = [];
|
|
239
|
+
for (const [stepIndex, step] of cfg.steps.entries()) {
|
|
240
|
+
const leaves = isParallelGroup(step) ? step.parallel : isDynamicRunnerGroup(step) ? [step.parallel] : [step];
|
|
241
|
+
if (isParallelGroup(step) || isDynamicRunnerGroup(step)) parallelGroups.push({ start: steps.length, count: leaves.length, stepIndex });
|
|
242
|
+
for (const leaf of leaves) {
|
|
243
|
+
steps.push({
|
|
244
|
+
agent: isDynamicRunnerGroup(step) ? `expand:${leaf.agent}` : leaf.agent,
|
|
245
|
+
phase: isDynamicRunnerGroup(step) ? step.phase ?? leaf.phase : leaf.phase,
|
|
246
|
+
label: isDynamicRunnerGroup(step) ? step.label ?? leaf.label ?? `Dynamic fanout (${step.collect.as})` : leaf.label,
|
|
247
|
+
outputName: isDynamicRunnerGroup(step) ? step.collect.as : leaf.outputName,
|
|
248
|
+
structured: isDynamicRunnerGroup(step) ? Boolean(step.collect.outputSchema) : leaf.structured,
|
|
249
|
+
status: "pending",
|
|
250
|
+
...(isDynamicRunnerGroup(step) ? {} : {
|
|
251
|
+
sessionFile: leaf.sessionFile,
|
|
252
|
+
skills: leaf.skills,
|
|
253
|
+
model: leaf.model,
|
|
254
|
+
thinking: leaf.thinking,
|
|
255
|
+
attemptedModels: leaf.modelCandidates && leaf.modelCandidates.length > 0 ? leaf.modelCandidates : leaf.model ? [leaf.model] : undefined,
|
|
256
|
+
}),
|
|
257
|
+
recentTools: [],
|
|
258
|
+
recentOutput: [],
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
let preparationError: unknown;
|
|
263
|
+
try {
|
|
264
|
+
fs.mkdirSync(cfg.sessionDir, { recursive: true });
|
|
265
|
+
cfg.sessionDir = fs.realpathSync.native(cfg.sessionDir);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
preparationError = error;
|
|
268
|
+
}
|
|
269
|
+
const statusPath = path.join(asyncDir, "status.json");
|
|
270
|
+
const now = Date.now();
|
|
271
|
+
const status = {
|
|
272
|
+
runId: cfg.id, runtime: cfg.runtime, sessionId: cfg.sessionId,
|
|
273
|
+
state: "queued", mode: cfg.resultMode, cwd, currentStep: 0, chainStepCount: cfg.steps.length,
|
|
274
|
+
startedAt: now, lastUpdate: now, lastActivityAt: now, outputFile: path.join(asyncDir, "output-0.log"), steps, parallelGroups, workflowGraph: cfg.workflowGraph,
|
|
275
|
+
artifactsDir: cfg.artifactsDir, sessionDir: cfg.sessionDir,
|
|
276
|
+
};
|
|
277
|
+
writeAtomicJson(statusPath, status);
|
|
278
|
+
try {
|
|
279
|
+
if (preparationError) throw preparationError;
|
|
280
|
+
let sessionRoot = cfg.sessionDir;
|
|
281
|
+
if (origin.sessionRoot !== undefined) {
|
|
282
|
+
if (!origin.sessionFile && !origin.sessionFiles?.length) throw new Error("Origin session root requires a reused session file.");
|
|
283
|
+
if (!path.isAbsolute(origin.sessionRoot)) throw new Error("Origin session root must be absolute.");
|
|
284
|
+
let stat: fs.Stats;
|
|
285
|
+
try {
|
|
286
|
+
stat = fs.statSync(origin.sessionRoot);
|
|
287
|
+
} catch {
|
|
288
|
+
throw new Error("Origin session root must exist.");
|
|
289
|
+
}
|
|
290
|
+
if (!stat.isDirectory()) throw new Error("Origin session root must be a directory.");
|
|
291
|
+
sessionRoot = fs.realpathSync.native(origin.sessionRoot);
|
|
292
|
+
if (sessionRoot !== origin.sessionRoot) throw new Error("Origin session root must be canonical and cannot be retargeted.");
|
|
293
|
+
}
|
|
294
|
+
const effectiveFiles = origin.sessionFiles ?? (origin.sessionFile ? [origin.sessionFile] : []);
|
|
295
|
+
if (origin.sessionRoot) {
|
|
296
|
+
const contains = (candidate: string) => {
|
|
297
|
+
const relative = path.relative(sessionRoot, candidate);
|
|
298
|
+
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
299
|
+
};
|
|
300
|
+
if (origin.sessionFiles && !contains(cfg.sessionDir)) throw new Error("Runner session directory is outside origin session root.");
|
|
301
|
+
for (const file of effectiveFiles) {
|
|
302
|
+
if (!fs.existsSync(file)) throw new Error("Reused session file must be an existing regular .jsonl file.");
|
|
303
|
+
const stat = fs.lstatSync(file);
|
|
304
|
+
if (!stat.isFile() || stat.isSymbolicLink() || path.extname(file) !== ".jsonl") throw new Error("Reused session file must be a regular .jsonl file.");
|
|
305
|
+
if (!contains(fs.realpathSync.native(file))) throw new Error("Reused session file is outside origin session root.");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (origin.route && origin.address) {
|
|
309
|
+
if (effectiveFiles.some(file => fs.existsSync(file)) && !origin.sessionRoot) throw new Error("Origin session root is required for a reused nested session.");
|
|
310
|
+
const firstLeaf = isParallelGroup(cfg.steps[0]) ? cfg.steps[0].parallel[0] : isDynamicRunnerGroup(cfg.steps[0]) ? cfg.steps[0].parallel : cfg.steps[0];
|
|
311
|
+
const agents = cfg.steps.flatMap(step => isParallelGroup(step) ? step.parallel.map(leaf => leaf.agent) : isDynamicRunnerGroup(step) ? [step.parallel.agent] : [step.agent]);
|
|
312
|
+
try {
|
|
313
|
+
if (writeStart(origin.route, {
|
|
314
|
+
type: "subagent.nested.started", ts: now,
|
|
315
|
+
parentRunId: origin.address.parentRunId, parentStepIndex: origin.address.parentStepIndex,
|
|
316
|
+
child: {
|
|
317
|
+
id: cfg.id, ...origin.address, asyncDir, sessionId: cfg.sessionId, capabilityToken: origin.route.capabilityToken,
|
|
318
|
+
runtime: cfg.runtime, sessionRoot, mode: cfg.resultMode, state: "queued",
|
|
319
|
+
agent: firstLeaf?.agent, agents, chainStepCount: cfg.steps.length, parallelGroups,
|
|
320
|
+
startedAt: now, lastUpdate: now,
|
|
321
|
+
},
|
|
322
|
+
}) === false) throw new Error("Nested origin stamp was refused.");
|
|
323
|
+
} catch (error) {
|
|
324
|
+
throw new Error(`Failed to stamp nested origin: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const result = spawnRunner(cfg, suffix, cwd, asyncDir);
|
|
328
|
+
if (result.error) throw new Error(result.error);
|
|
329
|
+
return result;
|
|
330
|
+
} catch (error) {
|
|
331
|
+
writeAtomicJson(statusPath, { ...status, state: "failed", error: error instanceof Error ? error.message : String(error), lastUpdate: Date.now() });
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
213
334
|
}
|
|
214
335
|
|
|
215
336
|
/**
|
|
@@ -239,6 +360,7 @@ class AsyncStartValidationError extends Error {}
|
|
|
239
360
|
export function executeAsyncChain(
|
|
240
361
|
id: string,
|
|
241
362
|
params: AsyncChainParams,
|
|
363
|
+
writeStart: NestedStartWriter = writeNestedEvent,
|
|
242
364
|
): AsyncExecutionResult {
|
|
243
365
|
const {
|
|
244
366
|
chain,
|
|
@@ -298,10 +420,11 @@ export function executeAsyncChain(
|
|
|
298
420
|
const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
|
|
299
421
|
const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
|
|
300
422
|
const asyncDir = inheritedNestedRoute
|
|
301
|
-
? path.join(
|
|
423
|
+
? path.join(RUN_STATE_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
|
|
302
424
|
: path.join(ASYNC_DIR, id);
|
|
303
425
|
try {
|
|
304
|
-
fs.mkdirSync(asyncDir, { recursive: true });
|
|
426
|
+
fs.mkdirSync(path.dirname(asyncDir), { recursive: true });
|
|
427
|
+
fs.mkdirSync(asyncDir);
|
|
305
428
|
} catch (error) {
|
|
306
429
|
const message = error instanceof Error ? error.message : String(error);
|
|
307
430
|
return {
|
|
@@ -480,7 +603,7 @@ export function executeAsyncChain(
|
|
|
480
603
|
|
|
481
604
|
let spawnResult: { pid?: number; error?: string } = {};
|
|
482
605
|
try {
|
|
483
|
-
spawnResult =
|
|
606
|
+
spawnResult = publishAndSpawnRunner(
|
|
484
607
|
{
|
|
485
608
|
id,
|
|
486
609
|
steps,
|
|
@@ -491,11 +614,10 @@ export function executeAsyncChain(
|
|
|
491
614
|
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
492
615
|
artifactConfig,
|
|
493
616
|
share: shareEnabled,
|
|
494
|
-
sessionDir: sessionRoot
|
|
617
|
+
sessionDir: path.join(sessionRoot ?? path.join(RUN_STATE_ROOT_DIR, "sessions"), `async-${id}`),
|
|
495
618
|
asyncDir,
|
|
496
619
|
sessionId: ctx.currentSessionId,
|
|
497
|
-
|
|
498
|
-
piArgv1: process.argv[1],
|
|
620
|
+
runtime: ctx.runtime,
|
|
499
621
|
forwardedFlags,
|
|
500
622
|
worktreeSetupHook,
|
|
501
623
|
worktreeSetupHookTimeoutMs,
|
|
@@ -515,6 +637,15 @@ export function executeAsyncChain(
|
|
|
515
637
|
id,
|
|
516
638
|
runnerCwd,
|
|
517
639
|
asyncDir,
|
|
640
|
+
{
|
|
641
|
+
route: inheritedNestedRoute,
|
|
642
|
+
address: nestedAddress,
|
|
643
|
+
sessionFiles: steps
|
|
644
|
+
.flatMap(step => isParallelGroup(step) ? step.parallel : isDynamicRunnerGroup(step) ? [step.parallel] : [step])
|
|
645
|
+
.flatMap(step => step.sessionFile ? [step.sessionFile] : []),
|
|
646
|
+
sessionRoot: params.originSessionRoot,
|
|
647
|
+
},
|
|
648
|
+
writeStart,
|
|
518
649
|
);
|
|
519
650
|
} catch (error) {
|
|
520
651
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -550,37 +681,6 @@ export function executeAsyncChain(
|
|
|
550
681
|
flatStepStart++;
|
|
551
682
|
}
|
|
552
683
|
}
|
|
553
|
-
if (inheritedNestedRoute && nestedAddress) {
|
|
554
|
-
const now = Date.now();
|
|
555
|
-
try {
|
|
556
|
-
writeNestedEvent(inheritedNestedRoute, {
|
|
557
|
-
type: "subagent.nested.started",
|
|
558
|
-
ts: now,
|
|
559
|
-
parentRunId: nestedAddress.parentRunId,
|
|
560
|
-
parentStepIndex: nestedAddress.parentStepIndex,
|
|
561
|
-
child: {
|
|
562
|
-
id,
|
|
563
|
-
parentRunId: nestedAddress.parentRunId,
|
|
564
|
-
parentStepIndex: nestedAddress.parentStepIndex,
|
|
565
|
-
depth: nestedAddress.depth,
|
|
566
|
-
path: nestedAddress.path,
|
|
567
|
-
asyncDir,
|
|
568
|
-
pid: spawnResult.pid,
|
|
569
|
-
ownerState: "live",
|
|
570
|
-
mode: resultMode,
|
|
571
|
-
state: "running",
|
|
572
|
-
agent: firstAgents[0],
|
|
573
|
-
agents: flatAgents,
|
|
574
|
-
chainStepCount: chain.length,
|
|
575
|
-
parallelGroups,
|
|
576
|
-
startedAt: now,
|
|
577
|
-
lastUpdate: now,
|
|
578
|
-
},
|
|
579
|
-
});
|
|
580
|
-
} catch (error) {
|
|
581
|
-
console.error("Failed to emit nested async start event:", error);
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
684
|
ctx.pi.events.emit(SUBAGENT_ASYNC_STARTED_EVENT, {
|
|
585
685
|
id,
|
|
586
686
|
pid: spawnResult.pid,
|
|
@@ -623,6 +723,7 @@ export function executeAsyncChain(
|
|
|
623
723
|
export function executeAsyncSingle(
|
|
624
724
|
id: string,
|
|
625
725
|
params: AsyncSingleParams,
|
|
726
|
+
writeStart: NestedStartWriter = writeNestedEvent,
|
|
626
727
|
): AsyncExecutionResult {
|
|
627
728
|
const {
|
|
628
729
|
agent,
|
|
@@ -657,10 +758,11 @@ export function executeAsyncSingle(
|
|
|
657
758
|
const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
|
|
658
759
|
const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
|
|
659
760
|
const asyncDir = inheritedNestedRoute
|
|
660
|
-
? path.join(
|
|
761
|
+
? path.join(RUN_STATE_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
|
|
661
762
|
: path.join(ASYNC_DIR, id);
|
|
662
763
|
try {
|
|
663
|
-
fs.mkdirSync(asyncDir, { recursive: true });
|
|
764
|
+
fs.mkdirSync(path.dirname(asyncDir), { recursive: true });
|
|
765
|
+
fs.mkdirSync(asyncDir);
|
|
664
766
|
} catch (error) {
|
|
665
767
|
const message = error instanceof Error ? error.message : String(error);
|
|
666
768
|
return {
|
|
@@ -681,7 +783,7 @@ export function executeAsyncSingle(
|
|
|
681
783
|
const model = applyThinkingSuffix(resolveModelCandidate(primaryRaw, availableModels, ctx.currentModelProvider), thinking);
|
|
682
784
|
let spawnResult: { pid?: number; error?: string } = {};
|
|
683
785
|
try {
|
|
684
|
-
spawnResult =
|
|
786
|
+
spawnResult = publishAndSpawnRunner(
|
|
685
787
|
{
|
|
686
788
|
id,
|
|
687
789
|
steps: [
|
|
@@ -722,11 +824,10 @@ export function executeAsyncSingle(
|
|
|
722
824
|
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
723
825
|
artifactConfig,
|
|
724
826
|
share: shareEnabled,
|
|
725
|
-
sessionDir: sessionRoot
|
|
827
|
+
sessionDir: path.join(sessionRoot ?? path.join(RUN_STATE_ROOT_DIR, "sessions"), `async-${id}`),
|
|
726
828
|
asyncDir,
|
|
727
829
|
sessionId: ctx.currentSessionId,
|
|
728
|
-
|
|
729
|
-
piArgv1: process.argv[1],
|
|
830
|
+
runtime: ctx.runtime,
|
|
730
831
|
forwardedFlags,
|
|
731
832
|
worktreeSetupHook,
|
|
732
833
|
worktreeSetupHookTimeoutMs,
|
|
@@ -744,6 +845,8 @@ export function executeAsyncSingle(
|
|
|
744
845
|
id,
|
|
745
846
|
runnerCwd,
|
|
746
847
|
asyncDir,
|
|
848
|
+
{ route: inheritedNestedRoute, address: nestedAddress, sessionFile, sessionRoot: params.originSessionRoot },
|
|
849
|
+
writeStart,
|
|
747
850
|
);
|
|
748
851
|
} catch (error) {
|
|
749
852
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -755,36 +858,6 @@ export function executeAsyncSingle(
|
|
|
755
858
|
}
|
|
756
859
|
|
|
757
860
|
if (spawnResult.pid) {
|
|
758
|
-
if (inheritedNestedRoute && nestedAddress) {
|
|
759
|
-
const now = Date.now();
|
|
760
|
-
try {
|
|
761
|
-
writeNestedEvent(inheritedNestedRoute, {
|
|
762
|
-
type: "subagent.nested.started",
|
|
763
|
-
ts: now,
|
|
764
|
-
parentRunId: nestedAddress.parentRunId,
|
|
765
|
-
parentStepIndex: nestedAddress.parentStepIndex,
|
|
766
|
-
child: {
|
|
767
|
-
id,
|
|
768
|
-
parentRunId: nestedAddress.parentRunId,
|
|
769
|
-
parentStepIndex: nestedAddress.parentStepIndex,
|
|
770
|
-
depth: nestedAddress.depth,
|
|
771
|
-
path: nestedAddress.path,
|
|
772
|
-
asyncDir,
|
|
773
|
-
pid: spawnResult.pid,
|
|
774
|
-
ownerState: "live",
|
|
775
|
-
mode: "single",
|
|
776
|
-
state: "running",
|
|
777
|
-
agent,
|
|
778
|
-
agents: [agent],
|
|
779
|
-
chainStepCount: 1,
|
|
780
|
-
startedAt: now,
|
|
781
|
-
lastUpdate: now,
|
|
782
|
-
},
|
|
783
|
-
});
|
|
784
|
-
} catch (error) {
|
|
785
|
-
console.error("Failed to emit nested async start event:", error);
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
861
|
ctx.pi.events.emit(SUBAGENT_ASYNC_STARTED_EVENT, {
|
|
789
862
|
id,
|
|
790
863
|
pid: spawnResult.pid,
|