pi-subagents 0.83.0 → 0.83.1
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 +22 -2
- package/bundle/agents.js +4 -4
- package/bundle/capability-ceiling.js +1 -1
- package/bundle/child-tool-plan.js +3 -3
- package/bundle/{chunk-P2NCCTZP.js → chunk-2D52JFLF.js} +1 -1
- package/bundle/{chunk-WG2OTTAK.js → chunk-2DHZTCOL.js} +2 -2
- package/bundle/{chunk-MBDOYZ2V.js → chunk-2S4K53MV.js} +1 -1
- package/bundle/{chunk-HJJAVHFB.js → chunk-5YIITCPD.js} +23 -23
- package/bundle/{chunk-CNLDYEH4.js → chunk-AHIFYGR3.js} +11 -11
- package/bundle/{chunk-MHX22USZ.js → chunk-C5P5CHYK.js} +1 -1
- package/bundle/{chunk-XXKG3P2W.js → chunk-C7JMPU5F.js} +4 -4
- package/bundle/{chunk-VTMNBPCQ.js → chunk-D3JAFGMO.js} +1 -1
- package/bundle/{chunk-BKFWQ642.js → chunk-F4ASL6OV.js} +1 -1
- package/bundle/{chunk-CDI3GMNV.js → chunk-F5DBXJ2G.js} +15 -15
- package/bundle/{chunk-JHZ6DZ5M.js → chunk-G2Z6ZH26.js} +1 -1
- package/bundle/{chunk-TJQJ3MEA.js → chunk-IPGZMQXV.js} +10 -10
- package/bundle/{chunk-Y3I6H6DV.js → chunk-LFDBD4PW.js} +2 -2
- package/bundle/{chunk-7F5SK2U3.js → chunk-LNRJCP2Y.js} +17 -14
- package/bundle/{chunk-6B4ITO2W.js → chunk-MABL3FUM.js} +1 -1
- package/bundle/{chunk-CGXWSNPA.js → chunk-NPIYWVOD.js} +4 -4
- package/bundle/{chunk-RZJN63GU.js → chunk-QP5ZWNIS.js} +2 -2
- package/bundle/{chunk-Z7KVHQHL.js → chunk-QXASAHIA.js} +2 -2
- package/bundle/{chunk-5ESXK62X.js → chunk-RNIZLSEA.js} +416 -389
- package/bundle/{chunk-HLE22FSH.js → chunk-SSG7IEP5.js} +2 -2
- package/bundle/{chunk-CZTMUBUI.js → chunk-UFL7IYCW.js} +4 -4
- package/bundle/{chunk-LDMXJIA6.js → chunk-UUP372DR.js} +44 -67
- package/bundle/{chunk-OOFW75PV.js → chunk-UYK5MZWE.js} +15 -11
- package/bundle/{chunk-CSSZRTKY.js → chunk-V2BZYLEP.js} +2 -2
- package/bundle/{chunk-4BW55BTL.js → chunk-VLFZIN6D.js} +4 -4
- package/bundle/{chunk-527E3D2J.js → chunk-YXFJVCOL.js} +5 -5
- package/bundle/{chunk-HXVKGS33.js → chunk-ZVOAMTUQ.js} +4 -4
- package/bundle/control-channel.js +3 -3
- package/bundle/index.js +1 -1
- package/bundle/intercom-bridge.js +3 -3
- package/bundle/preflight.js +9 -9
- package/bundle/project-panes.js +4 -4
- package/docs/configuration.md +12 -1
- package/docs/extension-api.md +2 -0
- package/docs/missions.md +2 -0
- package/docs/tool-reference.md +1 -1
- package/package.json +1 -1
- package/src/agents/agent-management.d.ts +1 -1
- package/src/agents/agent-management.js +2 -2
- package/src/agents/agents.js +1 -2
- package/src/agents/frontmatter.js +22 -18
- package/src/extension/index.js +2 -2
- package/src/missions/actions.js +7 -2
- package/src/missions/lifecycle.d.ts +11 -0
- package/src/missions/lifecycle.js +37 -6
- package/src/missions/store.js +1 -0
- package/src/missions/types.d.ts +2 -0
- package/src/runs/background/async-execution.d.ts +1 -1
- package/src/runs/background/async-execution.js +3 -3
- package/src/runs/background/async-resume.js +3 -2
- package/src/runs/background/retained-children.js +1 -1
- package/src/runs/foreground/subagent-executor.js +24 -51
- package/src/runs/shared/capability-ceiling.d.ts +2 -1
- package/src/runs/shared/capability-ceiling.js +3 -2
- package/src/runs/shared/parallel-handoff.d.ts +1 -1
- package/src/runs/shared/parallel-handoff.js +10 -6
- package/src/shared/types.d.ts +1 -1
- package/src/workflows/scripted-workflow.js +4 -1
- package/workflow-script-worker.cjs +2 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
resolveIntercomSessionTarget
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-C5P5CHYK.js";
|
|
4
4
|
import "./chunk-7F6RKT6W.js";
|
|
5
|
-
import "./chunk-
|
|
5
|
+
import "./chunk-LNRJCP2Y.js";
|
|
6
6
|
import "./chunk-JEZOSEWG.js";
|
|
7
7
|
import "./chunk-YFCK7H2S.js";
|
|
8
|
-
import "./chunk-
|
|
8
|
+
import "./chunk-QXASAHIA.js";
|
|
9
9
|
export {
|
|
10
10
|
resolveIntercomSessionTarget
|
|
11
11
|
};
|
package/bundle/preflight.js
CHANGED
|
@@ -2,22 +2,22 @@ import {
|
|
|
2
2
|
buildEffectiveSystemPrompt,
|
|
3
3
|
canPreferForkFromSnapshot,
|
|
4
4
|
resolveSubagentLaunchContext
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-SSG7IEP5.js";
|
|
6
|
+
import "./chunk-LFDBD4PW.js";
|
|
7
7
|
import {
|
|
8
8
|
loadConfig
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-QP5ZWNIS.js";
|
|
10
10
|
import {
|
|
11
11
|
nestedResultsPath,
|
|
12
12
|
processTerminalCandidatePath,
|
|
13
13
|
processTerminalPath,
|
|
14
14
|
resultFilePath
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-RNIZLSEA.js";
|
|
16
16
|
import {
|
|
17
17
|
applyIntercomBridgeToAgent,
|
|
18
18
|
resolveIntercomBridge,
|
|
19
19
|
validateIntercomBridgeConfig
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-C5P5CHYK.js";
|
|
21
21
|
import {
|
|
22
22
|
AGENT_DEFINITION_PROJECTION_VERSION,
|
|
23
23
|
resolveLaunchBinding,
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
import {
|
|
27
27
|
applyThinkingSuffix,
|
|
28
28
|
resolvePiLaunchToolPlan
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-V2BZYLEP.js";
|
|
30
30
|
import {
|
|
31
31
|
assertRequiredChildExtensionsAdmitted,
|
|
32
32
|
resolveRequiredChildExtensions
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
import {
|
|
35
35
|
getArtifactPaths,
|
|
36
36
|
getArtifactsDir
|
|
37
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-D3JAFGMO.js";
|
|
38
38
|
import {
|
|
39
39
|
assertClaudeCodeModelScope,
|
|
40
40
|
assertThinkingWithinCeiling,
|
|
@@ -61,7 +61,7 @@ import {
|
|
|
61
61
|
resolveSkillsWithFallback,
|
|
62
62
|
resolveStepBehavior,
|
|
63
63
|
unknownAgentDiagnosticContext
|
|
64
|
-
} from "./chunk-
|
|
64
|
+
} from "./chunk-LNRJCP2Y.js";
|
|
65
65
|
import {
|
|
66
66
|
DIRS,
|
|
67
67
|
SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
@@ -71,7 +71,7 @@ import "./chunk-YFCK7H2S.js";
|
|
|
71
71
|
import {
|
|
72
72
|
capabilityCeilingAgentRestrictionMessage,
|
|
73
73
|
intersectSubagentCapabilityCeilings
|
|
74
|
-
} from "./chunk-
|
|
74
|
+
} from "./chunk-QXASAHIA.js";
|
|
75
75
|
|
|
76
76
|
// src/api/preflight.ts
|
|
77
77
|
import * as fs from "node:fs";
|
package/bundle/project-panes.js
CHANGED
|
@@ -8,13 +8,13 @@ import {
|
|
|
8
8
|
openProjectPane,
|
|
9
9
|
projectPaneBindingPath,
|
|
10
10
|
readProjectPaneBinding
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-2DHZTCOL.js";
|
|
12
12
|
import "./chunk-7J4ZXMY7.js";
|
|
13
|
-
import "./chunk-
|
|
14
|
-
import "./chunk-
|
|
13
|
+
import "./chunk-D3JAFGMO.js";
|
|
14
|
+
import "./chunk-LNRJCP2Y.js";
|
|
15
15
|
import "./chunk-JEZOSEWG.js";
|
|
16
16
|
import "./chunk-YFCK7H2S.js";
|
|
17
|
-
import "./chunk-
|
|
17
|
+
import "./chunk-QXASAHIA.js";
|
|
18
18
|
export {
|
|
19
19
|
PROJECT_PANES_API_VERSION,
|
|
20
20
|
PROJECT_PANE_TRUST_STATUS,
|
package/docs/configuration.md
CHANGED
|
@@ -202,7 +202,18 @@ With `"summary"`, a tool result looks like this:
|
|
|
202
202
|
|
|
203
203
|
Set to `false` to register none of the pi-subagents slash commands (`/run`, `/subagents`, `/subagents-fleet`, `/subagents-watchdog`, `/prompt-workflow`, and the rest) and no [`foregroundDetachShortcut`](#foregrounddetachshortcut). The `subagent` tool, RPC, the async widget, and the extension's other features are unchanged. Prompt templates whose frontmatter has `chain:` launch through `/prompt-workflow`, so they stop working too. Defaults to `true`. Restart Pi or run `/reload` after changing it.
|
|
204
204
|
|
|
205
|
-
The package's prompt shortcuts, such as `/parallel-review` and `/review-loop`, are Pi prompt templates, not commands, so this setting leaves them alone.
|
|
205
|
+
The package's prompt shortcuts, such as `/parallel-review` and `/review-loop`, are Pi prompt templates, not commands, so this setting leaves them alone. The same goes for its skills (`pi-subagents` and `council-mode`) and built-in agents. To turn them off, use Pi's package filter for prompts and skills and `disableBuiltins` for the agents, in Pi's `settings.json`. This turns off every prompt, the `council-mode` skill and every built-in agent:
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"packages": [
|
|
210
|
+
{ "source": "npm:pi-subagents", "prompts": [], "skills": ["!council-mode"] }
|
|
211
|
+
],
|
|
212
|
+
"subagents": { "disableBuiltins": true }
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
A `!` entry such as `"!review-loop.md"` drops one file, and `[]` drops all of that type. `subagents.agentOverrides.<name>.disabled: true` turns off one built-in agent instead of all of them ([agents.md](agents.md)). `pi config` toggles the package's prompts and skills interactively.
|
|
206
217
|
|
|
207
218
|
## `foregroundDetachShortcut`
|
|
208
219
|
|
package/docs/extension-api.md
CHANGED
|
@@ -430,6 +430,8 @@ const restriction = registerSubagentCapabilityCeiling({
|
|
|
430
430
|
// restriction.dispose() removes only this provider's registration.
|
|
431
431
|
```
|
|
432
432
|
|
|
433
|
+
Register with **`ctx.sessionManager.getSessionId()`**; a ceiling registered by session file path does not apply.
|
|
434
|
+
|
|
433
435
|
Semantics:
|
|
434
436
|
|
|
435
437
|
- Active registrations intersect their `allowedTools` and `allowedAgents` sets and OR `denyExtensions`.
|
package/docs/missions.md
CHANGED
|
@@ -13,6 +13,8 @@ Missions are durable wrappers around runs. The noun map:
|
|
|
13
13
|
|
|
14
14
|
Ordinary workflow launches create one enclosing mission by default, with detailed JSON records under `~/.pi/agent/missions/projects/<project-hash>/` linking objectives, run ids, lifecycle status, decisions, artifact paths, and delivery receipts. Workflow children do not create separate missions. Each workflow child attempt is stored in the enclosing mission with its stable workflow key, run id when known, agent, task metadata, timestamps, session and artifact paths, and latest status heartbeat.
|
|
15
15
|
|
|
16
|
+
A finished run completes a mission you asked for with `mission` or `missionId` only when its result was accepted. If the run, or any workflow child in the mission, ends with acceptance `review-required`, `rejected`, `claimed`, or `pending`, the mission stays `waiting` and `mission.show` lists each child's acceptance status. Check the result and close the mission with `mission.close`. A `gate` or `acceptance.verify` command lets the runtime accept the result, except for writer runs: they require a review by default and stay `review-required` without one. When the command is enough proof, use `acceptance: { level: "verified", verify: [{ id: "check", command: "..." }], review: { required: false } }` instead of `gate`. Missions made by default for ordinary launches still complete when their run finishes.
|
|
17
|
+
|
|
16
18
|
Records created under the old default `<project>/.pi/subagents/missions` stay on disk. Continue them by setting `missions.directory` to that path for the project or by copying the record into the new agent-dir project store. There is no automatic migration.
|
|
17
19
|
|
|
18
20
|
Behavior:
|
package/docs/tool-reference.md
CHANGED
|
@@ -344,7 +344,7 @@ subagent({ action: "doctor" })
|
|
|
344
344
|
- `view: "transcript"` tails the selected run's live `output-<index>.log` or persisted session transcript, with `lines` capped at 500.
|
|
345
345
|
- Nested status shows the root/parent path, nested children, session/artifact paths when known, and nested control commands.
|
|
346
346
|
- Inside child-safe fanout mode, bare `status` requires an id when no local foreground run is active, so children cannot enumerate unrelated top-level async runs.
|
|
347
|
-
-
|
|
347
|
+
- `interrupt` requires an id, like `stop`, `steer`, and `resume`; for a nested run, pass its nested id.
|
|
348
348
|
|
|
349
349
|
### Command controls
|
|
350
350
|
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@ import { type RuntimeAgentOwner } from "./runtime-agent-registry.ts";
|
|
|
6
6
|
type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry"> & {
|
|
7
7
|
model?: ExtensionContext["model"];
|
|
8
8
|
config?: ExtensionConfig;
|
|
9
|
-
|
|
9
|
+
piSessionId?: string;
|
|
10
10
|
runtimeAgentOwner?: RuntimeAgentOwner;
|
|
11
11
|
onAgentsChanged?: () => void;
|
|
12
12
|
discoverAgentsAll?: typeof discoverAgentsAll;
|
|
@@ -1023,7 +1023,7 @@ export function handleList(params, ctx) {
|
|
|
1023
1023
|
let scopedAgents = effectiveAgentsForScope(scope, d, ctx.runtimeAgentOwner, ctx.model?.provider);
|
|
1024
1024
|
scopedAgents = scopedAgents
|
|
1025
1025
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
1026
|
-
const capabilityCeiling = resolveCurrentSubagentCapabilityCeiling(ctx.
|
|
1026
|
+
const capabilityCeiling = resolveCurrentSubagentCapabilityCeiling(ctx.piSessionId);
|
|
1027
1027
|
const visibleAgents = scopedAgents.filter((a) => !a.disabled);
|
|
1028
1028
|
const agents = visibleAgents.filter((a) => isAgentAllowedByCapabilityCeiling(a.name, capabilityCeiling));
|
|
1029
1029
|
const restrictedAgents = visibleAgents.filter((a) => !isAgentAllowedByCapabilityCeiling(a.name, capabilityCeiling));
|
|
@@ -1082,7 +1082,7 @@ function handleModels(params, ctx) {
|
|
|
1082
1082
|
const availableModels = ctx.modelRegistry.getAvailable().map(toModelInfo);
|
|
1083
1083
|
const currentModel = ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined;
|
|
1084
1084
|
const preferredProvider = ctx.model?.provider;
|
|
1085
|
-
const capabilityCeiling = resolveCurrentSubagentCapabilityCeiling(ctx.
|
|
1085
|
+
const capabilityCeiling = resolveCurrentSubagentCapabilityCeiling(ctx.piSessionId);
|
|
1086
1086
|
let selectedAgents = effectiveAgents;
|
|
1087
1087
|
if (requestedAgent) {
|
|
1088
1088
|
const matches = findAgentsInDiscovery(requestedAgent, discovered, scope, ctx.runtimeAgentOwner, ctx.model?.provider);
|
package/src/agents/agents.js
CHANGED
|
@@ -2432,8 +2432,7 @@ function buildAgentDiscoverySources(cwd, preferredModelProvider, globalNpmRoot,
|
|
|
2432
2432
|
const watchPaths = new Set([
|
|
2433
2433
|
userSettingsPath,
|
|
2434
2434
|
...(projectSettingsPath ? [projectSettingsPath] : []),
|
|
2435
|
-
...projectDiscoveryWatchPaths(effectiveCwd),
|
|
2436
|
-
...findProjectRootCandidates(effectiveCwd).map((root) => path.join(getProjectConfigDir(root), "settings.json")),
|
|
2435
|
+
...(projectTrusted ? [...projectDiscoveryWatchPaths(effectiveCwd), ...findProjectRootCandidates(effectiveCwd).map((root) => path.join(getProjectConfigDir(root), "settings.json"))] : []),
|
|
2437
2436
|
...userScanDirs.watchPaths,
|
|
2438
2437
|
...projectScanDirs.watchPaths,
|
|
2439
2438
|
...packageSubagentPaths.watchPaths,
|
|
@@ -97,22 +97,15 @@ export function parseFrontmatter(content) {
|
|
|
97
97
|
// neither end a pending list block nor become part of its value.
|
|
98
98
|
if (trimmed.startsWith("#") && !currentFolded && !currentLiteral)
|
|
99
99
|
continue;
|
|
100
|
-
|
|
100
|
+
// Exactly-empty lines belong to any pending block, not only to | and > scalars.
|
|
101
|
+
if (currentKey !== null && currentBlockLines !== null && (indent > (currentIndent ?? 0) || trimmed === "")) {
|
|
101
102
|
// This line is part of the current block value
|
|
102
103
|
currentBlockLines.push(line);
|
|
103
104
|
continue;
|
|
104
105
|
}
|
|
105
106
|
// Flush any pending block value
|
|
106
107
|
if (currentKey !== null && currentBlockLines !== null) {
|
|
107
|
-
|
|
108
|
-
// serializer can add its own indentation level.
|
|
109
|
-
const rawBlock = currentBlockLines.join("\n");
|
|
110
|
-
const leadingSpaces = rawBlock.match(/^[ \t]+(?=\S)/m);
|
|
111
|
-
const prefix = leadingSpaces?.[0] ?? "";
|
|
112
|
-
const stripped = prefix
|
|
113
|
-
? rawBlock.replace(new RegExp(`^${escapeRegex(prefix)}`, "gm"), "").replace(/^\n/, "")
|
|
114
|
-
: rawBlock;
|
|
115
|
-
frontmatter[currentKey] = currentFolded ? foldBlock(stripped) : stripped;
|
|
108
|
+
frontmatter[currentKey] = flushBlockLines(currentBlockLines, currentFolded, currentLiteral);
|
|
116
109
|
currentKey = null;
|
|
117
110
|
currentBlockLines = null;
|
|
118
111
|
currentIndent = null;
|
|
@@ -146,17 +139,28 @@ export function parseFrontmatter(content) {
|
|
|
146
139
|
frontmatter[key] = value;
|
|
147
140
|
}
|
|
148
141
|
}
|
|
149
|
-
// Lines that don't match a key pattern (e.g., comments, empty lines) are ignored
|
|
150
142
|
}
|
|
151
143
|
// Flush final block value
|
|
152
144
|
if (currentKey !== null && currentBlockLines !== null) {
|
|
153
|
-
|
|
154
|
-
const leadingSpaces = rawBlock.match(/^[ \t]+(?=\S)/m);
|
|
155
|
-
const prefix = leadingSpaces?.[0] ?? "";
|
|
156
|
-
const stripped = prefix
|
|
157
|
-
? rawBlock.replace(new RegExp(`^${escapeRegex(prefix)}`, "gm"), "").replace(/^\n/, "")
|
|
158
|
-
: rawBlock;
|
|
159
|
-
frontmatter[currentKey] = currentFolded ? foldBlock(stripped) : stripped;
|
|
145
|
+
frontmatter[currentKey] = flushBlockLines(currentBlockLines, currentFolded, currentLiteral);
|
|
160
146
|
}
|
|
161
147
|
return { frontmatter, body };
|
|
162
148
|
}
|
|
149
|
+
function flushBlockLines(blockLines, folded, literal) {
|
|
150
|
+
let lines = blockLines;
|
|
151
|
+
if (!folded && !literal) {
|
|
152
|
+
// Drop exactly-empty edge lines, and every line from the first exactly-empty one after
|
|
153
|
+
// the last content line, so nested values without inner blank lines parse as before.
|
|
154
|
+
const end = lines.indexOf("", lines.findLastIndex((line) => line.trim() !== "") + 1);
|
|
155
|
+
lines = lines.slice(lines.findIndex((line) => line !== ""), end < 0 ? lines.length : end);
|
|
156
|
+
}
|
|
157
|
+
// Strip the common leading whitespace from the block so the
|
|
158
|
+
// serializer can add its own indentation level.
|
|
159
|
+
const rawBlock = lines.join("\n");
|
|
160
|
+
const leadingSpaces = rawBlock.match(/^[ \t]+(?=\S)/m);
|
|
161
|
+
const prefix = leadingSpaces?.[0] ?? "";
|
|
162
|
+
const stripped = prefix
|
|
163
|
+
? rawBlock.replace(new RegExp(`^${escapeRegex(prefix)}`, "gm"), "").replace(/^\n/, "")
|
|
164
|
+
: rawBlock;
|
|
165
|
+
return folded ? foldBlock(stripped) : stripped;
|
|
166
|
+
}
|
package/src/extension/index.js
CHANGED
|
@@ -735,7 +735,7 @@ export default function registerSubagentExtension(pi) {
|
|
|
735
735
|
event.systemPromptOptions.sections.active_subagents = activeRuns;
|
|
736
736
|
if (!event.systemPromptOptions.selectedTools.includes("subagent"))
|
|
737
737
|
return;
|
|
738
|
-
const catalog = buildAdvertisedAgentCatalog(advertisedAgents, resolveCurrentSubagentCapabilityCeiling(
|
|
738
|
+
const catalog = buildAdvertisedAgentCatalog(advertisedAgents, resolveCurrentSubagentCapabilityCeiling(ctx.sessionManager.getSessionId() ?? undefined), recordedPromptSection(pi, "advertised_subagents"));
|
|
739
739
|
if (catalog)
|
|
740
740
|
event.systemPromptOptions.sections.advertised_subagents = catalog;
|
|
741
741
|
});
|
|
@@ -1207,7 +1207,7 @@ export default function registerSubagentExtension(pi) {
|
|
|
1207
1207
|
hasPendingSupervisorRequest: () => [...supervisorChannel.pending.values()].some((request) => request.expectsReply),
|
|
1208
1208
|
advertisedPrompt: async () => {
|
|
1209
1209
|
await waitForAdvertisement();
|
|
1210
|
-
return buildAdvertisedAgentPrompt(advertisedAgents, resolveCurrentSubagentCapabilityCeiling(state.
|
|
1210
|
+
return buildAdvertisedAgentPrompt(advertisedAgents, resolveCurrentSubagentCapabilityCeiling(state.supervisorOwnerSessionId ?? undefined));
|
|
1211
1211
|
},
|
|
1212
1212
|
});
|
|
1213
1213
|
}
|
package/src/missions/actions.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { MISSION_STATUSES, } from "./types.js";
|
|
4
4
|
import { missionStatePath } from "./workflow-state.js";
|
|
5
|
+
import { finishedMissionStatus, missionAcceptanceStatus, readMissionBinding } from "./lifecycle.js";
|
|
5
6
|
import { createMission, listGlobalMissions, listMissions, missionRecordPath, readMission, resolveMissionStoreLocation, updateMission, validateMissionId, } from "./store.js";
|
|
6
7
|
export const MISSION_ACTIONS = [
|
|
7
8
|
"mission.create",
|
|
@@ -184,6 +185,7 @@ function validateMissionUpdate(value) {
|
|
|
184
185
|
}
|
|
185
186
|
function refreshLinkedRunStatus(location, record) {
|
|
186
187
|
const warnings = [];
|
|
188
|
+
const runAcceptance = [];
|
|
187
189
|
const updates = record.runs.flatMap((run) => {
|
|
188
190
|
if (!run.asyncDir)
|
|
189
191
|
return [];
|
|
@@ -207,6 +209,7 @@ function refreshLinkedRunStatus(location, record) {
|
|
|
207
209
|
warnings.push(`Linked run status '${statusPath}' is missing state`);
|
|
208
210
|
return [];
|
|
209
211
|
}
|
|
212
|
+
runAcceptance.push({ runId: run.runId, status: missionAcceptanceStatus(status.steps) });
|
|
210
213
|
if (state === run.status)
|
|
211
214
|
return [];
|
|
212
215
|
return [{
|
|
@@ -232,7 +235,8 @@ function refreshLinkedRunStatus(location, record) {
|
|
|
232
235
|
: states.some((state) => state === "stopped" || state === "rejected" || state === "cancelled")
|
|
233
236
|
? "cancelled"
|
|
234
237
|
: states.length > 0 && states.every((state) => state === "complete" || state === "completed")
|
|
235
|
-
|
|
238
|
+
// A run without a binding counts as auto-created.
|
|
239
|
+
? finishedMissionStatus(record, !merged.some((run) => run.asyncDir && readMissionBinding(run.asyncDir)?.autoCreated === false), runAcceptance)
|
|
236
240
|
: record.status;
|
|
237
241
|
return { record: updateMission(location, record.id, { status, addRuns: updates }), warnings };
|
|
238
242
|
}
|
|
@@ -261,7 +265,8 @@ function formatMission(record) {
|
|
|
261
265
|
const identity = child.runId ? `${child.key} (${child.runId})` : child.key;
|
|
262
266
|
const heartbeat = child.heartbeat ? `; heartbeat ${child.heartbeat.status ?? child.status}${child.heartbeat.phase ? `/${child.heartbeat.phase}` : ""} at ${child.heartbeat.updatedAt}` : "";
|
|
263
267
|
const recovery = child.sessionPath ? `; session ${child.sessionPath}` : child.artifactPaths.length ? `; artifacts ${child.artifactPaths.length}` : "";
|
|
264
|
-
|
|
268
|
+
const acceptance = child.acceptanceStatus ? `; acceptance ${child.acceptanceStatus}` : "";
|
|
269
|
+
lines.push(` ${identity}: ${child.status}${child.agent ? ` — ${child.agent}` : ""}${child.phase ? ` [${child.phase}]` : ""}${acceptance}; updated ${child.updatedAt}${heartbeat}${recovery}`);
|
|
265
270
|
}
|
|
266
271
|
}
|
|
267
272
|
if (record.decisions.length) {
|
|
@@ -21,6 +21,7 @@ export interface MissionLaunchParams {
|
|
|
21
21
|
export interface MissionLaunchBinding {
|
|
22
22
|
missionId: string;
|
|
23
23
|
location: MissionStoreLocation;
|
|
24
|
+
/** True when the launch made the mission without a `mission` or `missionId` request. */
|
|
24
25
|
autoCreated: boolean;
|
|
25
26
|
announceInContent?: boolean;
|
|
26
27
|
}
|
|
@@ -30,6 +31,16 @@ export declare function prepareMissionLaunch(input: {
|
|
|
30
31
|
config?: MissionStoreConfig;
|
|
31
32
|
ownerSessionId?: string;
|
|
32
33
|
}): MissionLaunchBinding | undefined;
|
|
34
|
+
/** The first unaccepted acceptance status among child results, else the first status found. */
|
|
35
|
+
export declare function missionAcceptanceStatus(results: unknown): string | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* The status of a mission whose runs all finished successfully. Every path that
|
|
38
|
+
* completes a mission from run state goes through here.
|
|
39
|
+
*/
|
|
40
|
+
export declare function finishedMissionStatus(record: MissionRecord, autoCreated: boolean, runAcceptance: ReadonlyArray<{
|
|
41
|
+
runId: string;
|
|
42
|
+
status: string | undefined;
|
|
43
|
+
}>): "completed" | "waiting";
|
|
33
44
|
export declare function writeMissionAsyncBinding(asyncDir: string, binding: MissionLaunchBinding): void;
|
|
34
45
|
export declare function attachMissionToLaunchResult(input: {
|
|
35
46
|
binding: MissionLaunchBinding;
|
|
@@ -50,7 +50,7 @@ export function prepareMissionLaunch(input) {
|
|
|
50
50
|
...(mission?.labels ? { labels: mission.labels } : {}),
|
|
51
51
|
...(input.ownerSessionId ? { ownerSessionId: input.ownerSessionId } : {}),
|
|
52
52
|
}, new Date(), input.config?.retainTerminal);
|
|
53
|
-
return { missionId: record.id, location, autoCreated:
|
|
53
|
+
return { missionId: record.id, location, autoCreated: input.params.mission === undefined, announceInContent: true };
|
|
54
54
|
}
|
|
55
55
|
function toolResultIsError(result) {
|
|
56
56
|
return "isError" in result && result.isError === true;
|
|
@@ -67,7 +67,30 @@ function runStatusForResult(result) {
|
|
|
67
67
|
return "failed";
|
|
68
68
|
return "completed";
|
|
69
69
|
}
|
|
70
|
-
|
|
70
|
+
/** Acceptance statuses that mean nobody accepted the child's result. */
|
|
71
|
+
const UNACCEPTED_ACCEPTANCE_STATUSES = new Set(["pending", "claimed", "review-required", "rejected"]);
|
|
72
|
+
/** The first unaccepted acceptance status among child results, else the first status found. */
|
|
73
|
+
export function missionAcceptanceStatus(results) {
|
|
74
|
+
const statuses = (Array.isArray(results) ? results : [])
|
|
75
|
+
.map((result) => result?.acceptance?.status)
|
|
76
|
+
.filter((status) => typeof status === "string" && status.trim() !== "");
|
|
77
|
+
return statuses.find((status) => UNACCEPTED_ACCEPTANCE_STATUSES.has(status)) ?? statuses[0];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The status of a mission whose runs all finished successfully. Every path that
|
|
81
|
+
* completes a mission from run state goes through here.
|
|
82
|
+
*/
|
|
83
|
+
export function finishedMissionStatus(record, autoCreated, runAcceptance) {
|
|
84
|
+
// Nobody is expected to close a mission made by default for an ordinary launch.
|
|
85
|
+
if (autoCreated)
|
|
86
|
+
return "completed";
|
|
87
|
+
if (runAcceptance.some((run) => UNACCEPTED_ACCEPTANCE_STATUSES.has(run.status)))
|
|
88
|
+
return "waiting";
|
|
89
|
+
// A run reported here supersedes its older workflow child record.
|
|
90
|
+
const reported = new Set(runAcceptance.map((run) => run.runId));
|
|
91
|
+
return record.workflowChildren.some((child) => !reported.has(child.runId) && UNACCEPTED_ACCEPTANCE_STATUSES.has(child.acceptanceStatus)) ? "waiting" : "completed";
|
|
92
|
+
}
|
|
93
|
+
function missionStatusForRun(record, runId, runStatus, acceptanceStatus, autoCreated) {
|
|
71
94
|
if (record.status === "completed" || record.status === "failed" || record.status === "cancelled")
|
|
72
95
|
return record.status;
|
|
73
96
|
if (runStatus === "active" || runStatus === "queued" || runStatus === "running")
|
|
@@ -80,7 +103,7 @@ function missionStatusForRun(record, runId, runStatus) {
|
|
|
80
103
|
if (otherActive)
|
|
81
104
|
return "active";
|
|
82
105
|
if (runStatus === "completed" || runStatus === "complete")
|
|
83
|
-
return
|
|
106
|
+
return finishedMissionStatus(record, autoCreated, [{ runId, status: acceptanceStatus }]);
|
|
84
107
|
if (runStatus === "stopped" || runStatus === "rejected" || runStatus === "cancelled")
|
|
85
108
|
return "cancelled";
|
|
86
109
|
return "failed";
|
|
@@ -129,6 +152,7 @@ function persistedBinding(binding) {
|
|
|
129
152
|
globalIndexDir: binding.location.globalIndexDir,
|
|
130
153
|
writeGlobalIndex: binding.location.writeGlobalIndex,
|
|
131
154
|
...(binding.location.retainTerminal !== undefined ? { retainTerminal: binding.location.retainTerminal } : {}),
|
|
155
|
+
autoCreated: binding.autoCreated,
|
|
132
156
|
};
|
|
133
157
|
}
|
|
134
158
|
export function writeMissionAsyncBinding(asyncDir, binding) {
|
|
@@ -170,7 +194,7 @@ export function attachMissionToLaunchResult(input) {
|
|
|
170
194
|
...(usage ? { usage } : {}),
|
|
171
195
|
};
|
|
172
196
|
let mission = updateMission(input.binding.location, input.binding.missionId, {
|
|
173
|
-
status: missionStatusForRun(current, runId, runStatus),
|
|
197
|
+
status: missionStatusForRun(current, runId, runStatus, missionAcceptanceStatus(input.result.details.results), input.binding.autoCreated),
|
|
174
198
|
addRuns: [run],
|
|
175
199
|
addArtifacts: artifactsForResult(input.result),
|
|
176
200
|
...(firstText(input.result) && runStatus !== "active" ? { summary: firstText(input.result) } : {}),
|
|
@@ -188,6 +212,8 @@ export function attachMissionToLaunchResult(input) {
|
|
|
188
212
|
asyncDir: input.result.details.asyncDir,
|
|
189
213
|
mode: input.result.details.mode,
|
|
190
214
|
state: status.state,
|
|
215
|
+
// A run that finished before this launch result was attached keeps its acceptance in the status steps.
|
|
216
|
+
...(Array.isArray(status.steps) ? { results: status.steps.map((step) => ({ acceptance: step?.acceptance })) } : {}),
|
|
191
217
|
...(firstText(input.result) ? { summary: firstText(input.result) } : {}),
|
|
192
218
|
}) ?? mission;
|
|
193
219
|
}
|
|
@@ -237,9 +263,12 @@ function parsePersistedBinding(value, source) {
|
|
|
237
263
|
throw new Error(`${source}.writeGlobalIndex must be boolean`);
|
|
238
264
|
if (input.retainTerminal !== undefined && (!Number.isInteger(input.retainTerminal) || input.retainTerminal < 1))
|
|
239
265
|
throw new Error(`${source}.retainTerminal must be a positive integer`);
|
|
266
|
+
if (input.autoCreated !== undefined && typeof input.autoCreated !== "boolean")
|
|
267
|
+
throw new Error(`${source}.autoCreated must be boolean`);
|
|
240
268
|
return {
|
|
241
269
|
missionId: validateMissionId(input.missionId, `${source}.missionId`),
|
|
242
|
-
|
|
270
|
+
// Bindings saved before this field existed belong to runs that completed their mission without an acceptance check.
|
|
271
|
+
autoCreated: input.autoCreated !== false,
|
|
243
272
|
location: {
|
|
244
273
|
projectRoot: input.projectRoot,
|
|
245
274
|
missionDir: input.missionDir,
|
|
@@ -339,8 +368,9 @@ export function syncMissionFromAsyncCompletion(value) {
|
|
|
339
368
|
? "stopped"
|
|
340
369
|
: "failed";
|
|
341
370
|
const workflowChildTerminal = !["running", "queued", "active", "paused"].includes(workflowChildStatus);
|
|
371
|
+
const acceptanceStatus = missionAcceptanceStatus(event.results);
|
|
342
372
|
return updateMission(binding.location, binding.missionId, {
|
|
343
|
-
status: missionStatusForRun(current, runId, runStatus),
|
|
373
|
+
status: missionStatusForRun(current, runId, runStatus, acceptanceStatus, binding.autoCreated),
|
|
344
374
|
addRuns: [{ runId, mode: typeof event.mode === "string" && ["single", "parallel", "chain", "workflow"].includes(event.mode) ? event.mode : "external", asyncDir: event.asyncDir, status: runStatus, completedAt, ...(usage && usage.tokens > 0 ? { usage } : {}) }],
|
|
345
375
|
addArtifacts: artifacts,
|
|
346
376
|
...(workflowRunId && workflowKey ? { upsertWorkflowChildren: [{
|
|
@@ -350,6 +380,7 @@ export function syncMissionFromAsyncCompletion(value) {
|
|
|
350
380
|
status: workflowChildStatus,
|
|
351
381
|
artifactPaths: artifacts.map((artifact) => artifact.path),
|
|
352
382
|
...(workflowChildTerminal ? { completedAt } : {}),
|
|
383
|
+
...(acceptanceStatus ? { acceptanceStatus } : {}),
|
|
353
384
|
heartbeat: { status: workflowChildStatus, ...(summary ? { message: summary } : {}) },
|
|
354
385
|
}] } : {}),
|
|
355
386
|
...(summary ? { summary } : {}),
|
package/src/missions/store.js
CHANGED
|
@@ -133,6 +133,7 @@ function parseWorkflowChild(value, label) {
|
|
|
133
133
|
...(optionalString(input.phase, `${label}.phase`) ? { phase: input.phase } : {}),
|
|
134
134
|
...(input.completedAt !== undefined ? { completedAt: timestamp(input.completedAt, `${label}.completedAt`) } : {}),
|
|
135
135
|
...(optionalString(input.sessionPath, `${label}.sessionPath`) ? { sessionPath: input.sessionPath } : {}),
|
|
136
|
+
...(optionalString(input.acceptanceStatus, `${label}.acceptanceStatus`) ? { acceptanceStatus: input.acceptanceStatus } : {}),
|
|
136
137
|
...(heartbeat ? { heartbeat: {
|
|
137
138
|
updatedAt: timestamp(heartbeat.updatedAt, `${label}.heartbeat.updatedAt`),
|
|
138
139
|
...(optionalString(heartbeat.status, `${label}.heartbeat.status`) ? { status: heartbeat.status } : {}),
|
package/src/missions/types.d.ts
CHANGED
|
@@ -57,6 +57,8 @@ export interface MissionWorkflowChild {
|
|
|
57
57
|
sessionPath?: string;
|
|
58
58
|
artifactPaths: string[];
|
|
59
59
|
heartbeat?: MissionChildHeartbeat;
|
|
60
|
+
/** Latest acceptance ledger status reported for the child. */
|
|
61
|
+
acceptanceStatus?: string;
|
|
60
62
|
}
|
|
61
63
|
export type MissionWorkflowChildUpdate = Pick<MissionWorkflowChild, "workflowRunId" | "key" | "status"> & Partial<Omit<MissionWorkflowChild, "workflowRunId" | "key" | "status" | "startedAt" | "updatedAt" | "artifactPaths" | "heartbeat">> & {
|
|
62
64
|
startedAt?: string;
|
|
@@ -25,7 +25,7 @@ interface AsyncExecutionContext {
|
|
|
25
25
|
cwd: string;
|
|
26
26
|
currentSessionId: string;
|
|
27
27
|
completionOwnerId?: string;
|
|
28
|
-
/** Parent session id
|
|
28
|
+
/** Parent Pi session id (`getSessionId()`): keys permission-system ask forwarding and capability ceilings. */
|
|
29
29
|
parentSessionId?: string;
|
|
30
30
|
/** Parent session file, recorded as each new child session's `parentSession` header. */
|
|
31
31
|
parentSessionFile?: string;
|
|
@@ -1233,7 +1233,8 @@ export function executeAsyncChain(id, params) {
|
|
|
1233
1233
|
.find((name) => name !== undefined && !lookupRunnerLauncher(ctx.runnerLaunchers, name));
|
|
1234
1234
|
if (undefinedLauncher !== undefined)
|
|
1235
1235
|
return formatAsyncStartError(resultMode, `Launcher '${undefinedLauncher}' used by this ${resultMode} is not defined in runnerLaunchers in the user subagent config.`);
|
|
1236
|
-
const
|
|
1236
|
+
const launchParentSessionId = ctx.parentSessionId ?? ctx.currentSessionId;
|
|
1237
|
+
const capabilityCeiling = params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(launchParentSessionId);
|
|
1237
1238
|
const inheritedNestedRoute = inheritedNestedRouteOf(ctx.childRuntime);
|
|
1238
1239
|
const nestedAddress = inheritedNestedRoute ? inheritedNestedParentAddressOf(ctx.childRuntime) : undefined;
|
|
1239
1240
|
const asyncDir = inheritedNestedRoute
|
|
@@ -1336,7 +1337,6 @@ export function executeAsyncChain(id, params) {
|
|
|
1336
1337
|
const initialStatusAt = Date.now();
|
|
1337
1338
|
const initialCompletionOwnerId = ctx.completionOwnerId ?? currentCompletionOwnerId();
|
|
1338
1339
|
const completionOwnerProcess = currentRecordedPid();
|
|
1339
|
-
const launchParentSessionId = ctx.parentSessionId ?? ctx.currentSessionId;
|
|
1340
1340
|
try {
|
|
1341
1341
|
writeRetainedRequiredChildExtensions(asyncDir, params.requiredExtensions ?? ctx.childRuntime?.requiredExtensions ?? resolveRequiredChildExtensions(launchParentSessionId ?? undefined));
|
|
1342
1342
|
}
|
|
@@ -1600,7 +1600,7 @@ export function executeAsyncSingle(id, params) {
|
|
|
1600
1600
|
if (unsupported.length > 0)
|
|
1601
1601
|
return formatAsyncStartError("single", `Agent '${agentConfig.name}' uses runner.type='${externalRunnerType}' and does not support: ${unsupported.join(", ")}.`);
|
|
1602
1602
|
}
|
|
1603
|
-
const capabilityCeiling = intersectSubagentCapabilityCeilings(params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.currentSessionId), ctx.childRuntime?.capabilityCeiling);
|
|
1603
|
+
const capabilityCeiling = intersectSubagentCapabilityCeilings(params.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(ctx.parentSessionId ?? ctx.currentSessionId), ctx.childRuntime?.capabilityCeiling);
|
|
1604
1604
|
try {
|
|
1605
1605
|
assertAgentAllowedByCapabilityCeiling(agentConfig.name, capabilityCeiling);
|
|
1606
1606
|
}
|
|
@@ -484,7 +484,8 @@ export function resolveAsyncResumeTarget(params, deps = {}, options = {}) {
|
|
|
484
484
|
const requireSessionFile = options.requireSessionFile ?? true;
|
|
485
485
|
const location = resolveAsyncRunLocation(params, asyncDirRoot, resultsDir);
|
|
486
486
|
if (!location.asyncDir && !location.resultPath) {
|
|
487
|
-
|
|
487
|
+
const requested = params.id ?? params.runId;
|
|
488
|
+
throw new Error(requested ? `Async run not found for '${requested}'.` : "Async run not found. Provide id or dir.");
|
|
488
489
|
}
|
|
489
490
|
const reconciliation = location.asyncDir
|
|
490
491
|
? reconcileAsyncRun(location.asyncDir, { resultsDir, kill: deps.kill, now: deps.now })
|
|
@@ -606,7 +607,7 @@ export function resolveAsyncResumeTarget(params, deps = {}, options = {}) {
|
|
|
606
607
|
? intersectSubagentCapabilityCeilings(status?.admissionCapabilityCeiling, result?.admissionCapabilityCeiling)
|
|
607
608
|
: intersectSubagentCapabilityCeilings(status?.capabilityCeiling, statusSteps[index]?.capabilityCeiling, result?.capabilityCeiling, resultSteps[index]?.capabilityCeiling);
|
|
608
609
|
const managedWorktreeCwd = location.asyncDir
|
|
609
|
-
? resolveRetainedWorktreeCwd(parallelHandoffPath(location.asyncDir), runId, index)
|
|
610
|
+
? resolveRetainedWorktreeCwd(parallelHandoffPath(location.asyncDir), runId, index, statusSteps[index]?.worktreePath)
|
|
610
611
|
: undefined;
|
|
611
612
|
const resumeCwd = validateResumeCwd(runId, managedWorktreeCwd ?? status?.cwd ?? result?.cwd ?? recoveryDescriptor?.cwd);
|
|
612
613
|
return {
|
|
@@ -52,7 +52,7 @@ function childResumability(run, step) {
|
|
|
52
52
|
}
|
|
53
53
|
let requiredCwd;
|
|
54
54
|
try {
|
|
55
|
-
requiredCwd = resolveRetainedWorktreeCwd(parallelHandoffPath(run.asyncDir), run.id, step.index) ?? recoveryDescriptor.cwd ?? run.cwd;
|
|
55
|
+
requiredCwd = resolveRetainedWorktreeCwd(parallelHandoffPath(run.asyncDir), run.id, step.index, step.worktreePath) ?? recoveryDescriptor.cwd ?? run.cwd;
|
|
56
56
|
if (!requiredCwd || !fs.statSync(requiredCwd).isDirectory())
|
|
57
57
|
return { state: "not-resumable", reason: `required cwd is missing: ${requiredCwd ?? "unknown"}` };
|
|
58
58
|
}
|