pi-subagents 0.11.2 → 0.11.4
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 +29 -0
- package/README.md +39 -3
- package/agents.ts +1 -30
- package/async-execution.ts +60 -10
- package/async-job-tracker.ts +123 -0
- package/chain-execution.ts +4 -0
- package/chain-serializer.ts +1 -30
- package/execution.ts +28 -91
- package/fork-context.ts +56 -0
- package/frontmatter.ts +29 -0
- package/index.ts +91 -1176
- package/package.json +1 -1
- package/parallel-utils.ts +22 -5
- package/pi-args.ts +127 -0
- package/render.ts +6 -3
- package/result-watcher.ts +92 -0
- package/schemas.ts +4 -0
- package/settings.ts +2 -38
- package/slash-commands.ts +369 -0
- package/subagent-executor.ts +870 -0
- package/subagent-runner.ts +53 -74
- package/types.ts +20 -0
- package/utils.ts +1 -28
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.11.4] - 2026-03-19
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added explicit execution context mode for tool calls: `context: "fresh" | "fork"` (default: `fresh`).
|
|
9
|
+
- Added true forked-context execution for single, parallel, and chain runs. In `fork` mode each child run now starts from a real branched session file created from the parent session's current leaf.
|
|
10
|
+
- Added `--fork` slash-command flag for `/run`, `/chain`, and `/parallel` to forward `context: "fork"`.
|
|
11
|
+
- Added regression coverage for fork execution/session wiring and fork badge rendering, including slash command forwarding tests.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Session argument wiring now supports `--session <file>` in addition to `--session-dir`, enabling exact leaf-preserving forks without summary injection.
|
|
15
|
+
- Async runner step payloads now carry per-step session files so background single/chain/parallel executions can also honor `context: "fork"`.
|
|
16
|
+
- Clarified docs for foreground vs background semantics so `--bg` behavior is explicit.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- `context: "fork"` now fails fast with explicit errors when parent session state is unavailable (missing persisted session, missing current leaf, or failed branch extraction), with no silent fallback to `fresh`.
|
|
20
|
+
- Fork-session creation errors are now surfaced as tool errors instead of bubbling as uncaught exceptions during execution.
|
|
21
|
+
- Session directory preparation now fails loudly with actionable errors (instead of silently swallowing mkdir failures).
|
|
22
|
+
- Async launch now fails with explicit errors when the async run directory cannot be created.
|
|
23
|
+
- Share logs now correctly include forked session files even when no session directory exists.
|
|
24
|
+
- Tool-call and result rendering now explicitly show `[fork]` when `context: "fork"` is used, including empty-result responses.
|
|
25
|
+
- `subagent_status` now surfaces async result-file read failures instead of returning a misleading missing-status message.
|
|
26
|
+
|
|
27
|
+
## [0.11.3] - 2026-03-17
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- Decomposed `index.ts` (1,450 → ~350 lines) into focused modules: `subagent-executor.ts`, `async-job-tracker.ts`, `result-watcher.ts`, `slash-commands.ts`. Shared mutable state centralized in `SubagentState` interface. Three identical session handlers collapsed into one.
|
|
31
|
+
- Extracted shared pi CLI arg-builder (`pi-args.ts`) from duplicated logic in `execution.ts` and `subagent-runner.ts`.
|
|
32
|
+
- Consolidated `mapConcurrent` (canonical in `parallel-utils.ts`, re-exported from `utils.ts`), `aggregateParallelOutputs` (canonical in `parallel-utils.ts` with optional header formatter, re-exported from `settings.ts`), and `parseFrontmatter` (extracted to `frontmatter.ts`).
|
|
33
|
+
|
|
5
34
|
## [0.11.2] - 2026-03-11
|
|
6
35
|
|
|
7
36
|
### Fixed
|
package/README.md
CHANGED
|
@@ -172,7 +172,24 @@ Add `--bg` at the end of any slash command to run in the background:
|
|
|
172
172
|
/parallel scout "scan frontend" -> scout "scan backend" -> scout "scan infra" --bg
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
Without `--bg`, the run is foreground: the tool call stays active and streams progress until completion. With `--bg`, the run is launched asynchronously: control returns immediately, and completion arrives later via notification. In both cases subagents run as separate processes. Check status with `subagent_status`.
|
|
176
|
+
|
|
177
|
+
### Forked Context Execution
|
|
178
|
+
|
|
179
|
+
Add `--fork` at the end of `/run`, `/chain`, or `/parallel` to run with `context: "fork"`:
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
/run reviewer "review this diff" --fork
|
|
183
|
+
/chain scout "analyze this branch" -> planner "plan next steps" --fork
|
|
184
|
+
/parallel scout "audit frontend" -> reviewer "audit backend" --fork
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
You can combine `--fork` and `--bg` in any order:
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
/run reviewer "review this diff" --fork --bg
|
|
191
|
+
/run reviewer "review this diff" --bg --fork
|
|
192
|
+
```
|
|
176
193
|
|
|
177
194
|
## Agents Manager
|
|
178
195
|
|
|
@@ -285,7 +302,9 @@ Chains can be created from the Agents Manager template picker ("Blank Chain"), o
|
|
|
285
302
|
| Chain | Yes | `{ chain: [{agent, task}...] }` with `{task}`, `{previous}`, `{chain_dir}` variables |
|
|
286
303
|
| Parallel | Yes | `{ tasks: [{agent, task}...] }` - via TUI toggle or converted to chain for async |
|
|
287
304
|
|
|
288
|
-
|
|
305
|
+
Execution context defaults to `context: "fresh"`, which starts each child run from a clean session. Set `context: "fork"` to start each child from a real branched session created from the parent's current leaf.
|
|
306
|
+
|
|
307
|
+
All modes support foreground and background execution. Foreground is the default (the call waits and streams progress). For programmatic background launch, use `clarify: false, async: true`. For interactive background launch, use `clarify: true` and press `b` in the TUI before running. Chains with parallel steps (`{ parallel: [...] }`) run concurrently with configurable `concurrency` and `failFast` options.
|
|
289
308
|
|
|
290
309
|
**Clarify TUI for single/parallel:**
|
|
291
310
|
|
|
@@ -397,9 +416,15 @@ Skills are specialized instructions loaded from SKILL.md files and injected into
|
|
|
397
416
|
{ agent: "scout", task: "find todos", maxOutput: { lines: 1000 } }
|
|
398
417
|
{ agent: "scout", task: "investigate", output: false } // disable file output
|
|
399
418
|
|
|
400
|
-
//
|
|
419
|
+
// Single agent from parent-session fork (real branched session at current leaf)
|
|
420
|
+
{ agent: "worker", task: "continue this thread", context: "fork" }
|
|
421
|
+
|
|
422
|
+
// Parallel
|
|
401
423
|
{ tasks: [{ agent: "scout", task: "a" }, { agent: "scout", task: "b" }] }
|
|
402
424
|
|
|
425
|
+
// Parallel with forked context (each task gets its own isolated fork)
|
|
426
|
+
{ tasks: [{ agent: "scout", task: "audit frontend" }, { agent: "reviewer", task: "audit backend" }], context: "fork" }
|
|
427
|
+
|
|
403
428
|
// Chain with TUI clarification (default)
|
|
404
429
|
{ chain: [
|
|
405
430
|
{ agent: "scout", task: "Gather context for auth refactor" },
|
|
@@ -408,6 +433,12 @@ Skills are specialized instructions loaded from SKILL.md files and injected into
|
|
|
408
433
|
{ agent: "reviewer" }
|
|
409
434
|
]}
|
|
410
435
|
|
|
436
|
+
// Chain with forked context (each step gets its own isolated fork of the same parent leaf)
|
|
437
|
+
{ chain: [
|
|
438
|
+
{ agent: "scout", task: "Analyze current branch decisions" },
|
|
439
|
+
{ agent: "planner", task: "Plan from {previous}" }
|
|
440
|
+
], context: "fork" }
|
|
441
|
+
|
|
411
442
|
// Chain without TUI (enables async)
|
|
412
443
|
{ chain: [...], clarify: false, async: true }
|
|
413
444
|
|
|
@@ -529,6 +560,7 @@ Notes:
|
|
|
529
560
|
| `model` | string | agent default | Override model for single agent |
|
|
530
561
|
| `tasks` | `{agent, task, cwd?, skill?}[]` | - | Parallel tasks (sync only) |
|
|
531
562
|
| `chain` | ChainItem[] | - | Sequential steps with behavior overrides (see below) |
|
|
563
|
+
| `context` | `"fresh" \| "fork"` | `fresh` | Execution context mode. `fork` uses a real branched session from the parent's current leaf for each child run |
|
|
532
564
|
| `chainDir` | string | `<tmpdir>/pi-chain-runs/` | Persistent directory for chain artifacts (default auto-cleaned after 24h) |
|
|
533
565
|
| `clarify` | boolean | true (chains) | Show TUI to preview/edit chain; implies sync mode |
|
|
534
566
|
| `agentScope` | `"user" \| "project" \| "both"` | `both` | Agent discovery scope (project wins on name collisions) |
|
|
@@ -540,6 +572,8 @@ Notes:
|
|
|
540
572
|
| `share` | boolean | false | Upload session to GitHub Gist (see [Session Sharing](#session-sharing)) |
|
|
541
573
|
| `sessionDir` | string | - | Override session log directory (takes precedence over `defaultSessionDir` and parent-session-derived path) |
|
|
542
574
|
|
|
575
|
+
`context: "fork"` fails fast when the parent session is not persisted, the current leaf is missing, or a branched child session cannot be created. It never silently downgrades to `fresh`.
|
|
576
|
+
|
|
543
577
|
**ChainItem** can be either a sequential step or a parallel step:
|
|
544
578
|
|
|
545
579
|
*Sequential step fields:*
|
|
@@ -651,6 +685,8 @@ Files per task:
|
|
|
651
685
|
|
|
652
686
|
Session files (JSONL) are stored under a per-run session directory. Directory selection follows the same precedence as session root resolution: explicit `sessionDir` > `config.defaultSessionDir` > parent-session-derived path. The session file path is shown in output.
|
|
653
687
|
|
|
688
|
+
When `context: "fork"` is used, each child run starts with `--session <branched-session-file>` produced from the parent's current leaf. This is a real session fork, not injected summary text.
|
|
689
|
+
|
|
654
690
|
## Session Sharing
|
|
655
691
|
|
|
656
692
|
When `share: true` is passed, the extension will:
|
package/agents.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
import { KNOWN_FIELDS } from "./agent-serializer.js";
|
|
10
10
|
import { parseChain } from "./chain-serializer.js";
|
|
11
11
|
import { mergeAgentsForScope } from "./agent-selection.js";
|
|
12
|
+
import { parseFrontmatter } from "./frontmatter.js";
|
|
12
13
|
|
|
13
14
|
export type AgentScope = "user" | "project" | "both";
|
|
14
15
|
|
|
@@ -58,36 +59,6 @@ export interface AgentDiscoveryResult {
|
|
|
58
59
|
projectAgentsDir: string | null;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
|
|
62
|
-
const frontmatter: Record<string, string> = {};
|
|
63
|
-
const normalized = content.replace(/\r\n/g, "\n");
|
|
64
|
-
|
|
65
|
-
if (!normalized.startsWith("---")) {
|
|
66
|
-
return { frontmatter, body: normalized };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const endIndex = normalized.indexOf("\n---", 3);
|
|
70
|
-
if (endIndex === -1) {
|
|
71
|
-
return { frontmatter, body: normalized };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const frontmatterBlock = normalized.slice(4, endIndex);
|
|
75
|
-
const body = normalized.slice(endIndex + 4).trim();
|
|
76
|
-
|
|
77
|
-
for (const line of frontmatterBlock.split("\n")) {
|
|
78
|
-
const match = line.match(/^([\w-]+):\s*(.*)$/);
|
|
79
|
-
if (match) {
|
|
80
|
-
let value = match[2].trim();
|
|
81
|
-
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
82
|
-
value = value.slice(1, -1);
|
|
83
|
-
}
|
|
84
|
-
frontmatter[match[1]] = value;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return { frontmatter, body };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
62
|
function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
92
63
|
const agents: AgentConfig[] = [];
|
|
93
64
|
|
package/async-execution.ts
CHANGED
|
@@ -10,9 +10,9 @@ import { fileURLToPath } from "node:url";
|
|
|
10
10
|
import { createRequire } from "node:module";
|
|
11
11
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
12
12
|
import type { AgentConfig } from "./agents.js";
|
|
13
|
-
import { applyThinkingSuffix } from "./
|
|
13
|
+
import { applyThinkingSuffix } from "./pi-args.js";
|
|
14
14
|
import { injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.js";
|
|
15
|
-
import { isParallelStep, resolveStepBehavior, type ChainStep, type
|
|
15
|
+
import { isParallelStep, resolveStepBehavior, type ChainStep, type SequentialStep, type StepOverrides } from "./settings.js";
|
|
16
16
|
import type { RunnerStep } from "./parallel-utils.js";
|
|
17
17
|
import { resolvePiPackageRoot } from "./pi-spawn.js";
|
|
18
18
|
import { buildSkillInjection, normalizeSkillInput, resolveSkills } from "./skills.js";
|
|
@@ -40,7 +40,9 @@ const jitiCliPath: string | undefined = (() => {
|
|
|
40
40
|
try {
|
|
41
41
|
const p = candidate();
|
|
42
42
|
if (fs.existsSync(p)) return p;
|
|
43
|
-
} catch {
|
|
43
|
+
} catch {
|
|
44
|
+
// Candidate not available in this install, continue probing.
|
|
45
|
+
}
|
|
44
46
|
}
|
|
45
47
|
return undefined;
|
|
46
48
|
})();
|
|
@@ -62,6 +64,7 @@ export interface AsyncChainParams {
|
|
|
62
64
|
shareEnabled: boolean;
|
|
63
65
|
sessionRoot?: string;
|
|
64
66
|
chainSkills?: string[];
|
|
67
|
+
sessionFilesByFlatIndex?: (string | undefined)[];
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
export interface AsyncSingleParams {
|
|
@@ -75,6 +78,7 @@ export interface AsyncSingleParams {
|
|
|
75
78
|
artifactConfig: ArtifactConfig;
|
|
76
79
|
shareEnabled: boolean;
|
|
77
80
|
sessionRoot?: string;
|
|
81
|
+
sessionFile?: string;
|
|
78
82
|
skills?: string[];
|
|
79
83
|
output?: string | false;
|
|
80
84
|
}
|
|
@@ -119,7 +123,18 @@ export function executeAsyncChain(
|
|
|
119
123
|
id: string,
|
|
120
124
|
params: AsyncChainParams,
|
|
121
125
|
): AsyncExecutionResult {
|
|
122
|
-
const {
|
|
126
|
+
const {
|
|
127
|
+
chain,
|
|
128
|
+
agents,
|
|
129
|
+
ctx,
|
|
130
|
+
cwd,
|
|
131
|
+
maxOutput,
|
|
132
|
+
artifactsDir,
|
|
133
|
+
artifactConfig,
|
|
134
|
+
shareEnabled,
|
|
135
|
+
sessionRoot,
|
|
136
|
+
sessionFilesByFlatIndex,
|
|
137
|
+
} = params;
|
|
123
138
|
const chainSkills = params.chainSkills ?? [];
|
|
124
139
|
|
|
125
140
|
// Validate all agents exist before building steps
|
|
@@ -141,10 +156,17 @@ export function executeAsyncChain(
|
|
|
141
156
|
const asyncDir = path.join(ASYNC_DIR, id);
|
|
142
157
|
try {
|
|
143
158
|
fs.mkdirSync(asyncDir, { recursive: true });
|
|
144
|
-
} catch {
|
|
159
|
+
} catch (error) {
|
|
160
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
161
|
+
return {
|
|
162
|
+
content: [{ type: "text", text: `Failed to create async run directory '${asyncDir}': ${message}` }],
|
|
163
|
+
isError: true,
|
|
164
|
+
details: { mode: "chain" as const, results: [] },
|
|
165
|
+
};
|
|
166
|
+
}
|
|
145
167
|
|
|
146
168
|
/** Build a resolved runner step from a SequentialStep */
|
|
147
|
-
const buildSeqStep = (s: SequentialStep) => {
|
|
169
|
+
const buildSeqStep = (s: SequentialStep, sessionFile?: string) => {
|
|
148
170
|
const a = agents.find((x) => x.name === s.agent)!;
|
|
149
171
|
const stepSkillInput = normalizeSkillInput(s.skill);
|
|
150
172
|
const stepOverrides: StepOverrides = { skills: stepSkillInput };
|
|
@@ -174,9 +196,17 @@ export function executeAsyncChain(
|
|
|
174
196
|
systemPrompt,
|
|
175
197
|
skills: resolvedSkills.map((r) => r.name),
|
|
176
198
|
outputPath,
|
|
199
|
+
sessionFile,
|
|
177
200
|
};
|
|
178
201
|
};
|
|
179
202
|
|
|
203
|
+
let flatStepIndex = 0;
|
|
204
|
+
const nextSessionFile = (): string | undefined => {
|
|
205
|
+
const sessionFile = sessionFilesByFlatIndex?.[flatStepIndex];
|
|
206
|
+
flatStepIndex++;
|
|
207
|
+
return sessionFile;
|
|
208
|
+
};
|
|
209
|
+
|
|
180
210
|
// Build runner steps — sequential steps become flat objects,
|
|
181
211
|
// parallel steps become { parallel: [...], concurrency?, failFast? }
|
|
182
212
|
const steps: RunnerStep[] = chain.map((s) => {
|
|
@@ -189,12 +219,12 @@ export function executeAsyncChain(
|
|
|
189
219
|
skill: t.skill,
|
|
190
220
|
model: t.model,
|
|
191
221
|
output: t.output,
|
|
192
|
-
})),
|
|
222
|
+
}, nextSessionFile())),
|
|
193
223
|
concurrency: s.concurrency,
|
|
194
224
|
failFast: s.failFast,
|
|
195
225
|
};
|
|
196
226
|
}
|
|
197
|
-
return buildSeqStep(s as SequentialStep);
|
|
227
|
+
return buildSeqStep(s as SequentialStep, nextSessionFile());
|
|
198
228
|
});
|
|
199
229
|
|
|
200
230
|
const runnerCwd = cwd ?? ctx.cwd;
|
|
@@ -258,7 +288,19 @@ export function executeAsyncSingle(
|
|
|
258
288
|
id: string,
|
|
259
289
|
params: AsyncSingleParams,
|
|
260
290
|
): AsyncExecutionResult {
|
|
261
|
-
const {
|
|
291
|
+
const {
|
|
292
|
+
agent,
|
|
293
|
+
task,
|
|
294
|
+
agentConfig,
|
|
295
|
+
ctx,
|
|
296
|
+
cwd,
|
|
297
|
+
maxOutput,
|
|
298
|
+
artifactsDir,
|
|
299
|
+
artifactConfig,
|
|
300
|
+
shareEnabled,
|
|
301
|
+
sessionRoot,
|
|
302
|
+
sessionFile,
|
|
303
|
+
} = params;
|
|
262
304
|
const skillNames = params.skills ?? agentConfig.skills ?? [];
|
|
263
305
|
const { resolved: resolvedSkills } = resolveSkills(skillNames, ctx.cwd);
|
|
264
306
|
let systemPrompt = agentConfig.systemPrompt?.trim() || null;
|
|
@@ -270,7 +312,14 @@ export function executeAsyncSingle(
|
|
|
270
312
|
const asyncDir = path.join(ASYNC_DIR, id);
|
|
271
313
|
try {
|
|
272
314
|
fs.mkdirSync(asyncDir, { recursive: true });
|
|
273
|
-
} catch {
|
|
315
|
+
} catch (error) {
|
|
316
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
317
|
+
return {
|
|
318
|
+
content: [{ type: "text", text: `Failed to create async run directory '${asyncDir}': ${message}` }],
|
|
319
|
+
isError: true,
|
|
320
|
+
details: { mode: "single" as const, results: [] },
|
|
321
|
+
};
|
|
322
|
+
}
|
|
274
323
|
|
|
275
324
|
const runnerCwd = cwd ?? ctx.cwd;
|
|
276
325
|
const outputPath = resolveSingleOutputPath(params.output, ctx.cwd, cwd);
|
|
@@ -290,6 +339,7 @@ export function executeAsyncSingle(
|
|
|
290
339
|
systemPrompt,
|
|
291
340
|
skills: resolvedSkills.map((r) => r.name),
|
|
292
341
|
outputPath,
|
|
342
|
+
sessionFile,
|
|
293
343
|
},
|
|
294
344
|
],
|
|
295
345
|
resultPath: path.join(RESULTS_DIR, `${id}.json`),
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { renderWidget } from "./render.js";
|
|
4
|
+
import {
|
|
5
|
+
type SubagentState,
|
|
6
|
+
POLL_INTERVAL_MS,
|
|
7
|
+
} from "./types.js";
|
|
8
|
+
import { readStatus } from "./utils.js";
|
|
9
|
+
|
|
10
|
+
export function createAsyncJobTracker(state: SubagentState, asyncDirRoot: string): {
|
|
11
|
+
ensurePoller: () => void;
|
|
12
|
+
handleStarted: (data: unknown) => void;
|
|
13
|
+
handleComplete: (data: unknown) => void;
|
|
14
|
+
resetJobs: (ctx?: ExtensionContext) => void;
|
|
15
|
+
} {
|
|
16
|
+
const ensurePoller = () => {
|
|
17
|
+
if (state.poller) return;
|
|
18
|
+
state.poller = setInterval(() => {
|
|
19
|
+
if (!state.lastUiContext || !state.lastUiContext.hasUI) return;
|
|
20
|
+
if (state.asyncJobs.size === 0) {
|
|
21
|
+
renderWidget(state.lastUiContext, []);
|
|
22
|
+
if (state.poller) {
|
|
23
|
+
clearInterval(state.poller);
|
|
24
|
+
state.poller = null;
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const job of state.asyncJobs.values()) {
|
|
30
|
+
if (job.status === "complete" || job.status === "failed") {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const status = readStatus(job.asyncDir);
|
|
34
|
+
if (status) {
|
|
35
|
+
job.status = status.state;
|
|
36
|
+
job.mode = status.mode;
|
|
37
|
+
job.currentStep = status.currentStep ?? job.currentStep;
|
|
38
|
+
job.stepsTotal = status.steps?.length ?? job.stepsTotal;
|
|
39
|
+
job.startedAt = status.startedAt ?? job.startedAt;
|
|
40
|
+
job.updatedAt = status.lastUpdate ?? Date.now();
|
|
41
|
+
if (status.steps?.length) {
|
|
42
|
+
job.agents = status.steps.map((step) => step.agent);
|
|
43
|
+
}
|
|
44
|
+
job.sessionDir = status.sessionDir ?? job.sessionDir;
|
|
45
|
+
job.outputFile = status.outputFile ?? job.outputFile;
|
|
46
|
+
job.totalTokens = status.totalTokens ?? job.totalTokens;
|
|
47
|
+
job.sessionFile = status.sessionFile ?? job.sessionFile;
|
|
48
|
+
} else {
|
|
49
|
+
job.status = job.status === "queued" ? "running" : job.status;
|
|
50
|
+
job.updatedAt = Date.now();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
renderWidget(state.lastUiContext, Array.from(state.asyncJobs.values()));
|
|
55
|
+
}, POLL_INTERVAL_MS);
|
|
56
|
+
state.poller.unref?.();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleStarted = (data: unknown) => {
|
|
60
|
+
const info = data as {
|
|
61
|
+
id?: string;
|
|
62
|
+
asyncDir?: string;
|
|
63
|
+
agent?: string;
|
|
64
|
+
chain?: string[];
|
|
65
|
+
};
|
|
66
|
+
if (!info.id) return;
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
const asyncDir = info.asyncDir ?? path.join(asyncDirRoot, info.id);
|
|
69
|
+
const agents = info.chain && info.chain.length > 0 ? info.chain : info.agent ? [info.agent] : undefined;
|
|
70
|
+
state.asyncJobs.set(info.id, {
|
|
71
|
+
asyncId: info.id,
|
|
72
|
+
asyncDir,
|
|
73
|
+
status: "queued",
|
|
74
|
+
mode: info.chain ? "chain" : "single",
|
|
75
|
+
agents,
|
|
76
|
+
stepsTotal: agents?.length,
|
|
77
|
+
startedAt: now,
|
|
78
|
+
updatedAt: now,
|
|
79
|
+
});
|
|
80
|
+
if (state.lastUiContext) {
|
|
81
|
+
renderWidget(state.lastUiContext, Array.from(state.asyncJobs.values()));
|
|
82
|
+
ensurePoller();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const handleComplete = (data: unknown) => {
|
|
87
|
+
const result = data as { id?: string; success?: boolean; asyncDir?: string };
|
|
88
|
+
const asyncId = result.id;
|
|
89
|
+
if (!asyncId) return;
|
|
90
|
+
const job = state.asyncJobs.get(asyncId);
|
|
91
|
+
if (job) {
|
|
92
|
+
job.status = result.success ? "complete" : "failed";
|
|
93
|
+
job.updatedAt = Date.now();
|
|
94
|
+
if (result.asyncDir) job.asyncDir = result.asyncDir;
|
|
95
|
+
}
|
|
96
|
+
if (state.lastUiContext) {
|
|
97
|
+
renderWidget(state.lastUiContext, Array.from(state.asyncJobs.values()));
|
|
98
|
+
}
|
|
99
|
+
const timer = setTimeout(() => {
|
|
100
|
+
state.cleanupTimers.delete(asyncId);
|
|
101
|
+
state.asyncJobs.delete(asyncId);
|
|
102
|
+
if (state.lastUiContext) {
|
|
103
|
+
renderWidget(state.lastUiContext, Array.from(state.asyncJobs.values()));
|
|
104
|
+
}
|
|
105
|
+
}, 10000);
|
|
106
|
+
state.cleanupTimers.set(asyncId, timer);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const resetJobs = (ctx?: ExtensionContext) => {
|
|
110
|
+
for (const timer of state.cleanupTimers.values()) {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
}
|
|
113
|
+
state.cleanupTimers.clear();
|
|
114
|
+
state.asyncJobs.clear();
|
|
115
|
+
state.resultFileCoalescer.clear();
|
|
116
|
+
if (ctx?.hasUI) {
|
|
117
|
+
state.lastUiContext = ctx;
|
|
118
|
+
renderWidget(ctx, []);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return { ensurePoller, handleStarted, handleComplete, resetJobs };
|
|
123
|
+
}
|
package/chain-execution.ts
CHANGED
|
@@ -70,6 +70,7 @@ export interface ChainExecutionParams {
|
|
|
70
70
|
cwd?: string;
|
|
71
71
|
shareEnabled: boolean;
|
|
72
72
|
sessionDirForIndex: (idx?: number) => string | undefined;
|
|
73
|
+
sessionFileForIndex?: (idx?: number) => string | undefined;
|
|
73
74
|
artifactsDir: string;
|
|
74
75
|
artifactConfig: ArtifactConfig;
|
|
75
76
|
includeProgress?: boolean;
|
|
@@ -103,6 +104,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
103
104
|
cwd,
|
|
104
105
|
shareEnabled,
|
|
105
106
|
sessionDirForIndex,
|
|
107
|
+
sessionFileForIndex,
|
|
106
108
|
artifactsDir,
|
|
107
109
|
artifactConfig,
|
|
108
110
|
includeProgress,
|
|
@@ -333,6 +335,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
333
335
|
runId,
|
|
334
336
|
index: globalTaskIndex + taskIndex,
|
|
335
337
|
sessionDir: sessionDirForIndex(globalTaskIndex + taskIndex),
|
|
338
|
+
sessionFile: sessionFileForIndex?.(globalTaskIndex + taskIndex),
|
|
336
339
|
share: shareEnabled,
|
|
337
340
|
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
338
341
|
artifactConfig,
|
|
@@ -489,6 +492,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
|
|
|
489
492
|
runId,
|
|
490
493
|
index: globalTaskIndex,
|
|
491
494
|
sessionDir: sessionDirForIndex(globalTaskIndex),
|
|
495
|
+
sessionFile: sessionFileForIndex?.(globalTaskIndex),
|
|
492
496
|
share: shareEnabled,
|
|
493
497
|
artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
|
|
494
498
|
artifactConfig,
|
package/chain-serializer.ts
CHANGED
|
@@ -1,34 +1,5 @@
|
|
|
1
1
|
import type { ChainConfig, ChainStepConfig } from "./agents.js";
|
|
2
|
-
|
|
3
|
-
function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
|
|
4
|
-
const frontmatter: Record<string, string> = {};
|
|
5
|
-
const normalized = content.replace(/\r\n/g, "\n");
|
|
6
|
-
|
|
7
|
-
if (!normalized.startsWith("---")) {
|
|
8
|
-
return { frontmatter, body: normalized };
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const endIndex = normalized.indexOf("\n---", 3);
|
|
12
|
-
if (endIndex === -1) {
|
|
13
|
-
return { frontmatter, body: normalized };
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const frontmatterBlock = normalized.slice(4, endIndex);
|
|
17
|
-
const body = normalized.slice(endIndex + 4).trim();
|
|
18
|
-
|
|
19
|
-
for (const line of frontmatterBlock.split("\n")) {
|
|
20
|
-
const match = line.match(/^([\w-]+):\s*(.*)$/);
|
|
21
|
-
if (match) {
|
|
22
|
-
let value = match[2].trim();
|
|
23
|
-
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
24
|
-
value = value.slice(1, -1);
|
|
25
|
-
}
|
|
26
|
-
frontmatter[match[1]] = value;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return { frontmatter, body };
|
|
31
|
-
}
|
|
2
|
+
import { parseFrontmatter } from "./frontmatter.js";
|
|
32
3
|
|
|
33
4
|
function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
|
|
34
5
|
const lines = sectionBody.split("\n");
|