pi-subagents 0.82.1 → 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 +49 -0
- package/bundle/agents.js +5 -5
- package/bundle/background-work.js +1 -1
- package/bundle/capability-ceiling.js +1 -1
- package/bundle/child-tool-plan.js +4 -4
- package/bundle/{chunk-RMQFMMAT.js → chunk-2D52JFLF.js} +231 -10
- package/bundle/{chunk-52UBALJL.js → chunk-2DHZTCOL.js} +2 -2
- package/bundle/{chunk-43SCHIMD.js → chunk-2S4K53MV.js} +1 -1
- package/bundle/{chunk-IVAE5Z6Q.js → chunk-5YIITCPD.js} +31 -27
- package/bundle/{chunk-JYUKW2SY.js → chunk-AHIFYGR3.js} +13 -13
- package/bundle/{chunk-XSWOTIA3.js → chunk-C5P5CHYK.js} +1 -1
- package/bundle/{chunk-PDZWJCHD.js → chunk-C7JMPU5F.js} +10 -7
- package/bundle/{chunk-5TYMSZX3.js → chunk-D3JAFGMO.js} +2 -2
- package/bundle/{chunk-OYF6AFDY.js → chunk-F4ASL6OV.js} +2 -2
- package/bundle/{chunk-ZIV4J4HG.js → chunk-F5DBXJ2G.js} +51 -34
- package/bundle/{chunk-YKEWIB4M.js → chunk-G2Z6ZH26.js} +1 -1
- package/bundle/{chunk-G4GZD3OJ.js → chunk-IPGZMQXV.js} +11 -11
- package/bundle/{chunk-5LFLVSTM.js → chunk-JEZOSEWG.js} +2 -0
- package/bundle/{chunk-PLCCBLPY.js → chunk-LFDBD4PW.js} +2 -2
- package/bundle/{chunk-CIZQXJ5Q.js → chunk-LNRJCP2Y.js} +56 -20
- package/bundle/{chunk-UTT254JE.js → chunk-MABL3FUM.js} +8 -4
- package/bundle/{chunk-3JCDWF4V.js → chunk-NPIYWVOD.js} +12 -18
- package/bundle/{chunk-UCPP4GNU.js → chunk-QP5ZWNIS.js} +12 -5
- package/bundle/{chunk-Z7KVHQHL.js → chunk-QXASAHIA.js} +2 -2
- package/bundle/{chunk-46XKCPJR.js → chunk-RNIZLSEA.js} +2221 -3260
- package/bundle/{chunk-GJECHSAR.js → chunk-SSG7IEP5.js} +2 -2
- package/bundle/{chunk-VSUSLDNW.js → chunk-UFL7IYCW.js} +10 -8
- package/bundle/{chunk-QE4AGIMG.js → chunk-UUP372DR.js} +482 -705
- package/bundle/{chunk-6XJB7PIX.js → chunk-UYK5MZWE.js} +16 -12
- package/bundle/{chunk-4SDU2BZX.js → chunk-V2BZYLEP.js} +3 -3
- package/bundle/{chunk-FXUXCX2K.js → chunk-VLFZIN6D.js} +6 -6
- package/bundle/{chunk-N2BN7BF5.js → chunk-XA3DY6UG.js} +7 -5
- package/bundle/{chunk-PBK2I7WY.js → chunk-YXFJVCOL.js} +6 -6
- package/bundle/{chunk-4Y2ROR7L.js → chunk-ZVOAMTUQ.js} +5 -5
- package/bundle/control-channel.js +4 -4
- package/bundle/index.js +1 -1
- package/bundle/intercom-bridge.js +4 -4
- package/bundle/preflight.js +10 -10
- package/bundle/project-panes.js +5 -5
- package/bundle/shared-types.js +1 -1
- package/docs/configuration.md +13 -2
- package/docs/extension-api.md +21 -4
- package/docs/missions.md +2 -0
- package/docs/observability.md +1 -0
- package/docs/tool-reference.md +2 -2
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +6 -6
- package/src/agents/agent-management.d.ts +1 -1
- package/src/agents/agent-management.js +10 -16
- package/src/agents/agent-serializer.js +1 -2
- package/src/agents/agents.js +20 -5
- package/src/agents/frontmatter.js +22 -18
- package/src/api/background-work.d.ts +9 -6
- package/src/api/background-work.js +14 -7
- package/src/extension/index.js +6 -3
- package/src/extension/public-execution.d.ts +2 -2
- package/src/extension/public-execution.js +3 -3
- package/src/extension/reply-workflow-script.js +4 -5
- package/src/extension/rpc.js +3 -1
- package/src/extension/subagent-options.js +17 -4
- 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 +7 -4
- package/src/runs/background/async-job-tracker.js +2 -1
- package/src/runs/background/async-resume.js +3 -2
- package/src/runs/background/auto-drain.js +2 -3
- package/src/runs/background/fleet-view.js +1 -1
- package/src/runs/background/retained-children.js +1 -1
- package/src/runs/background/run-child-session.js +22 -6
- package/src/runs/background/subagent-wait.d.ts +2 -1
- package/src/runs/background/subagent-wait.js +5 -2
- package/src/runs/foreground/execution.js +22 -6
- package/src/runs/foreground/subagent-executor.js +26 -56
- package/src/runs/shared/capability-ceiling.d.ts +2 -1
- package/src/runs/shared/capability-ceiling.js +3 -2
- package/src/runs/shared/child-launch.js +5 -2
- package/src/runs/shared/child-session.d.ts +6 -0
- package/src/runs/shared/child-session.js +20 -4
- package/src/runs/shared/herdr-machine.d.ts +2 -0
- package/src/runs/shared/herdr-machine.js +2 -0
- package/src/runs/shared/herdr-placed-run.js +11 -4
- package/src/runs/shared/parallel-handoff.d.ts +1 -1
- package/src/runs/shared/parallel-handoff.js +10 -6
- package/src/runs/shared/subagent-prompt-runtime.js +11 -17
- package/src/shared/disabled-features.d.ts +3 -1
- package/src/shared/disabled-features.js +11 -3
- package/src/shared/types.d.ts +4 -2
- package/src/shared/types.js +2 -0
- package/src/workflows/scripted-workflow.js +13 -1071
- package/workflow-script-worker.cjs +1068 -0
|
@@ -6,7 +6,7 @@ import { mergeAgentsForScope } from "./agent-selection.js";
|
|
|
6
6
|
import { discoverAvailableSkills, resolveSkills } from "./skills.js";
|
|
7
7
|
import { buildProactiveSkillSubagentRecommendationLines, } from "./proactive-skills.js";
|
|
8
8
|
import { parseFrontmatter, parseUnquotedList } from "./frontmatter.js";
|
|
9
|
-
import { resolveEffectiveThinking, toModelInfo } from "../shared/model-info.js";
|
|
9
|
+
import { resolveEffectiveThinking, THINKING_LEVELS, toModelInfo } from "../shared/model-info.js";
|
|
10
10
|
import { resolveSubagentModelOverride } from "../runs/shared/model-resolution.js";
|
|
11
11
|
import { validateToolBudgetConfig } from "../runs/shared/tool-budget.js";
|
|
12
12
|
import { formatReviewGateLabel, validateAcceptanceInput } from "../runs/shared/acceptance.js";
|
|
@@ -279,11 +279,8 @@ export function preservedAgentFrontmatterFields(agent, cfg) {
|
|
|
279
279
|
changed("subagentOnlyExtensions");
|
|
280
280
|
if (hasKey(cfg, "mutationTools"))
|
|
281
281
|
changed("mutationTools");
|
|
282
|
-
if (hasKey(cfg, "thinking"))
|
|
282
|
+
if (hasKey(cfg, "thinking"))
|
|
283
283
|
changed("thinking");
|
|
284
|
-
if (cfg.thinking === "off")
|
|
285
|
-
fields.add("thinking");
|
|
286
|
-
}
|
|
287
284
|
if (hasKey(cfg, "systemPromptMode")) {
|
|
288
285
|
changed("systemPromptMode");
|
|
289
286
|
fields.add("systemPromptMode");
|
|
@@ -528,17 +525,14 @@ function applyAgentConfig(target, cfg) {
|
|
|
528
525
|
return "config.mutationTools must be a comma-separated string, empty string, or false when provided.";
|
|
529
526
|
}
|
|
530
527
|
if (hasKey(cfg, "thinking")) {
|
|
531
|
-
|
|
528
|
+
const thinking = typeof cfg.thinking === "string" ? cfg.thinking.trim() : cfg.thinking;
|
|
529
|
+
const level = THINKING_LEVELS.find((candidate) => candidate === thinking);
|
|
530
|
+
if (thinking === false || thinking === "")
|
|
532
531
|
delete target.thinking;
|
|
533
|
-
else if (
|
|
534
|
-
|
|
535
|
-
if (thinking)
|
|
536
|
-
target.thinking = thinking;
|
|
537
|
-
else
|
|
538
|
-
delete target.thinking;
|
|
539
|
-
}
|
|
532
|
+
else if (level)
|
|
533
|
+
target.thinking = level;
|
|
540
534
|
else
|
|
541
|
-
return
|
|
535
|
+
return `config.thinking must be one of ${THINKING_LEVELS.join(", ")}, or false when provided.`;
|
|
542
536
|
}
|
|
543
537
|
if (hasKey(cfg, "systemPromptMode")) {
|
|
544
538
|
if (cfg.systemPromptMode === "append" || cfg.systemPromptMode === "replace")
|
|
@@ -1029,7 +1023,7 @@ export function handleList(params, ctx) {
|
|
|
1029
1023
|
let scopedAgents = effectiveAgentsForScope(scope, d, ctx.runtimeAgentOwner, ctx.model?.provider);
|
|
1030
1024
|
scopedAgents = scopedAgents
|
|
1031
1025
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
1032
|
-
const capabilityCeiling = resolveCurrentSubagentCapabilityCeiling(ctx.
|
|
1026
|
+
const capabilityCeiling = resolveCurrentSubagentCapabilityCeiling(ctx.piSessionId);
|
|
1033
1027
|
const visibleAgents = scopedAgents.filter((a) => !a.disabled);
|
|
1034
1028
|
const agents = visibleAgents.filter((a) => isAgentAllowedByCapabilityCeiling(a.name, capabilityCeiling));
|
|
1035
1029
|
const restrictedAgents = visibleAgents.filter((a) => !isAgentAllowedByCapabilityCeiling(a.name, capabilityCeiling));
|
|
@@ -1088,7 +1082,7 @@ function handleModels(params, ctx) {
|
|
|
1088
1082
|
const availableModels = ctx.modelRegistry.getAvailable().map(toModelInfo);
|
|
1089
1083
|
const currentModel = ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined;
|
|
1090
1084
|
const preferredProvider = ctx.model?.provider;
|
|
1091
|
-
const capabilityCeiling = resolveCurrentSubagentCapabilityCeiling(ctx.
|
|
1085
|
+
const capabilityCeiling = resolveCurrentSubagentCapabilityCeiling(ctx.piSessionId);
|
|
1092
1086
|
let selectedAgents = effectiveAgents;
|
|
1093
1087
|
if (requestedAgent) {
|
|
1094
1088
|
const matches = findAgentsInDiscovery(requestedAgent, discovered, scope, ctx.runtimeAgentOwner, ctx.model?.provider);
|
|
@@ -91,9 +91,8 @@ export function serializeAgent(config, options = {}) {
|
|
|
91
91
|
lines.push(`model: ${config.model ?? ""}`);
|
|
92
92
|
if (config.fast === true || preserve("fast"))
|
|
93
93
|
lines.push(`fast: ${config.fast === undefined ? "" : config.fast ? "true" : "false"}`);
|
|
94
|
-
if (
|
|
94
|
+
if (config.thinking || preserve("thinking"))
|
|
95
95
|
lines.push(`thinking: ${config.thinking ?? ""}`);
|
|
96
|
-
}
|
|
97
96
|
if (!preservingExistingFrontmatter || preserve("systemPromptMode"))
|
|
98
97
|
lines.push(`systemPromptMode: ${config.systemPromptMode}`);
|
|
99
98
|
if (!preservingExistingFrontmatter || preserve("inheritProjectContext"))
|
package/src/agents/agents.js
CHANGED
|
@@ -28,6 +28,7 @@ import { parseMemoryFrontmatter } from "./agent-memory.js";
|
|
|
28
28
|
import { validateAcceptanceInput } from "../runs/shared/acceptance.js";
|
|
29
29
|
import { validatePermissionRules } from "../runs/shared/permissions.js";
|
|
30
30
|
import { parseThinkingLevel } from "../shared/thinking-ceiling.js";
|
|
31
|
+
import { THINKING_LEVELS } from "../shared/model-info.js";
|
|
31
32
|
import { assertJsonSchemaObject } from "../runs/shared/structured-output.js";
|
|
32
33
|
import { normalizeCapabilityCeilingAllowedAgents } from "../runs/shared/capability-ceiling.js";
|
|
33
34
|
export function defaultSystemPromptMode(name) {
|
|
@@ -39,6 +40,14 @@ export function defaultInheritProjectContext(name) {
|
|
|
39
40
|
export function defaultInheritSkills() {
|
|
40
41
|
return false;
|
|
41
42
|
}
|
|
43
|
+
/** Every field an `agentOverrides` entry may set; the Record type keeps it equal to BuiltinAgentOverrideConfig. */
|
|
44
|
+
const BUILTIN_OVERRIDE_FIELDS = {
|
|
45
|
+
description: true, advertise: true, machine: true, output: true, outputMode: true, defaultReads: true, model: true,
|
|
46
|
+
defaultProvider: true, fast: true, thinking: true, systemPromptMode: true, inheritProjectContext: true,
|
|
47
|
+
inheritGlobalContext: true, inheritSkills: true, defaultContext: true, acceptanceRole: true, disabled: true,
|
|
48
|
+
systemPrompt: true, skills: true, tools: true, excludeTools: true, allowNestedSubagents: true, allowedAgents: true,
|
|
49
|
+
extensions: true, subagentOnlyExtensions: true, mutationTools: true, toolBudget: true,
|
|
50
|
+
};
|
|
42
51
|
const EMPTY_SUBAGENT_SETTINGS = { overrides: {}, providerOverrides: {} };
|
|
43
52
|
const agentFrontmatterFields = new WeakMap();
|
|
44
53
|
const AGENT_SOURCE_PRIORITY = {
|
|
@@ -763,6 +772,12 @@ function parseBuiltinOverrideEntry(name, value, filePath) {
|
|
|
763
772
|
if (Object.hasOwn(input, "launcher")) {
|
|
764
773
|
throw new Error(`Builtin override '${name}' in '${filePath}' sets 'launcher', which settings overrides do not support; set 'launcher' in a user agent file instead.`);
|
|
765
774
|
}
|
|
775
|
+
for (const key of Object.keys(input)) {
|
|
776
|
+
if (Object.hasOwn(BUILTIN_OVERRIDE_FIELDS, key))
|
|
777
|
+
continue;
|
|
778
|
+
const suggestion = closestMatch(key, Object.keys(BUILTIN_OVERRIDE_FIELDS));
|
|
779
|
+
throw new Error(`Builtin override '${name}' in '${filePath}' has unknown field '${key}'.${suggestion ? ` Did you mean '${suggestion}'?` : ""}`);
|
|
780
|
+
}
|
|
766
781
|
const override = {};
|
|
767
782
|
if ("description" in input) {
|
|
768
783
|
if (typeof input.description === "string" && input.description.trim()) {
|
|
@@ -805,10 +820,11 @@ function parseBuiltinOverrideEntry(name, value, filePath) {
|
|
|
805
820
|
throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'fast'; expected a boolean.`);
|
|
806
821
|
}
|
|
807
822
|
if ("thinking" in input) {
|
|
808
|
-
|
|
809
|
-
|
|
823
|
+
const level = THINKING_LEVELS.find((candidate) => candidate === input.thinking);
|
|
824
|
+
if (level !== undefined || input.thinking === false)
|
|
825
|
+
override.thinking = level ?? false;
|
|
810
826
|
else
|
|
811
|
-
throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'thinking'; expected
|
|
827
|
+
throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'thinking'; expected one of ${THINKING_LEVELS.join(", ")}, or false.`);
|
|
812
828
|
}
|
|
813
829
|
if ("systemPromptMode" in input) {
|
|
814
830
|
if (input.systemPromptMode === "append" || input.systemPromptMode === "replace") {
|
|
@@ -2416,8 +2432,7 @@ function buildAgentDiscoverySources(cwd, preferredModelProvider, globalNpmRoot,
|
|
|
2416
2432
|
const watchPaths = new Set([
|
|
2417
2433
|
userSettingsPath,
|
|
2418
2434
|
...(projectSettingsPath ? [projectSettingsPath] : []),
|
|
2419
|
-
...projectDiscoveryWatchPaths(effectiveCwd),
|
|
2420
|
-
...findProjectRootCandidates(effectiveCwd).map((root) => path.join(getProjectConfigDir(root), "settings.json")),
|
|
2435
|
+
...(projectTrusted ? [...projectDiscoveryWatchPaths(effectiveCwd), ...findProjectRootCandidates(effectiveCwd).map((root) => path.join(getProjectConfigDir(root), "settings.json"))] : []),
|
|
2421
2436
|
...userScanDirs.watchPaths,
|
|
2422
2437
|
...projectScanDirs.watchPaths,
|
|
2423
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
|
+
}
|
|
@@ -4,14 +4,13 @@ export interface BackgroundWorkItem {
|
|
|
4
4
|
id: string;
|
|
5
5
|
sessionId: string;
|
|
6
6
|
}
|
|
7
|
-
export interface BackgroundWorkReconcileContext {
|
|
8
|
-
sessionId: string;
|
|
9
|
-
nowMs: number;
|
|
10
|
-
}
|
|
11
7
|
export interface BackgroundWorkListContext {
|
|
12
8
|
sessionId: string;
|
|
9
|
+
/** Every form the current session is known by: its file path when it has one, and `getSessionId()`. Includes `sessionId`. */
|
|
10
|
+
sessionIds: readonly string[];
|
|
13
11
|
nowMs: number;
|
|
14
12
|
}
|
|
13
|
+
export type BackgroundWorkReconcileContext = BackgroundWorkListContext;
|
|
15
14
|
export interface BackgroundWorkProvider {
|
|
16
15
|
name: string;
|
|
17
16
|
listActiveWork(context?: BackgroundWorkListContext): readonly BackgroundWorkItem[];
|
|
@@ -34,5 +33,9 @@ export declare function registerBackgroundWorkProvider(provider: BackgroundWorkP
|
|
|
34
33
|
export declare function listBackgroundWorkProviders(): readonly BackgroundWorkProvider[];
|
|
35
34
|
/** Read validated provider wake channels without reconciling or listing work. */
|
|
36
35
|
export declare function listBackgroundWorkWakeChannels(): readonly string[];
|
|
37
|
-
/**
|
|
38
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Reconcile and snapshot active provider work owned by one exact Pi session.
|
|
38
|
+
* `sessionIds` lists that session's identity forms; the first is passed to
|
|
39
|
+
* providers as `sessionId`, and items keyed by any of them are kept.
|
|
40
|
+
*/
|
|
41
|
+
export declare function snapshotBackgroundWork(sessionIds: readonly string[], nowMs?: number): BackgroundWorkSnapshot;
|
|
@@ -4,7 +4,7 @@ const MAX_PROVIDER_NAME_LENGTH = 128;
|
|
|
4
4
|
const MAX_PROVIDERS = 100;
|
|
5
5
|
const MAX_ITEM_ID_LENGTH = 256;
|
|
6
6
|
/**
|
|
7
|
-
* A
|
|
7
|
+
* A session can be identified by its file path, which routinely exceeds a short
|
|
8
8
|
* identity budget in nested worktrees, so it is bounded like the other paths.
|
|
9
9
|
*/
|
|
10
10
|
const MAX_SESSION_ID_LENGTH = 4_096;
|
|
@@ -120,22 +120,29 @@ export function listBackgroundWorkWakeChannels() {
|
|
|
120
120
|
}
|
|
121
121
|
return [...channels];
|
|
122
122
|
}
|
|
123
|
-
/**
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Reconcile and snapshot active provider work owned by one exact Pi session.
|
|
125
|
+
* `sessionIds` lists that session's identity forms; the first is passed to
|
|
126
|
+
* providers as `sessionId`, and items keyed by any of them are kept.
|
|
127
|
+
*/
|
|
128
|
+
export function snapshotBackgroundWork(sessionIds, nowMs = Date.now()) {
|
|
129
|
+
const accepted = [...new Set(sessionIds.map((value) => validateString(value, "Background-work snapshot sessionId", MAX_SESSION_ID_LENGTH)))];
|
|
130
|
+
const sessionId = accepted[0];
|
|
131
|
+
if (sessionId === undefined)
|
|
132
|
+
throw new Error("Background-work snapshot needs at least one session id.");
|
|
126
133
|
const providers = listBackgroundWorkProviders();
|
|
127
134
|
const items = [];
|
|
128
135
|
const identities = new Set();
|
|
129
136
|
for (const provider of providers) {
|
|
130
137
|
try {
|
|
131
|
-
provider.reconcile?.({ sessionId, nowMs });
|
|
138
|
+
provider.reconcile?.({ sessionId, sessionIds: [...accepted], nowMs });
|
|
132
139
|
}
|
|
133
140
|
catch (error) {
|
|
134
141
|
throw new Error(`Background-work provider '${provider.name}' reconcile failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
135
142
|
}
|
|
136
143
|
let active;
|
|
137
144
|
try {
|
|
138
|
-
active = provider.listActiveWork({ sessionId, nowMs });
|
|
145
|
+
active = provider.listActiveWork({ sessionId, sessionIds: [...accepted], nowMs });
|
|
139
146
|
}
|
|
140
147
|
catch (error) {
|
|
141
148
|
throw new Error(`Background-work provider '${provider.name}' listActiveWork failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
@@ -153,7 +160,7 @@ export function snapshotBackgroundWork(sessionId, nowMs = Date.now()) {
|
|
|
153
160
|
throw new Error(`Background-work provider '${provider.name}' returned duplicate item '${item.id}' for session '${item.sessionId}'.`);
|
|
154
161
|
}
|
|
155
162
|
identities.add(identity);
|
|
156
|
-
if (item.sessionId
|
|
163
|
+
if (accepted.includes(item.sessionId))
|
|
157
164
|
items.push({ provider: provider.name, ...item });
|
|
158
165
|
});
|
|
159
166
|
}
|
package/src/extension/index.js
CHANGED
|
@@ -566,8 +566,11 @@ export default function registerSubagentExtension(pi) {
|
|
|
566
566
|
// Pi sends custom messages to the model as plain user messages, so every model-visible
|
|
567
567
|
// pi-subagents message is wrapped in a marker naming its source. The marker depends only on
|
|
568
568
|
// the message, so history renders the same bytes every turn and cached prefixes stay valid.
|
|
569
|
+
// It runs on context_with_system, not context: Pi treats any change a context handler makes
|
|
570
|
+
// to the message list as a rewritten conversation and folds every mid-conversation system
|
|
571
|
+
// message into the leading one, which misses the prompt cache on every later turn.
|
|
569
572
|
const markedMessageTypes = new Set([...SUBAGENT_MESSAGE_TYPES, "subagent-compaction-resume", SLASH_RESULT_TYPE, SLASH_TEXT_RESULT_TYPE, ADMIN_MESSAGE_TYPE]);
|
|
570
|
-
pi.on("
|
|
573
|
+
pi.on("context_with_system", (event) => {
|
|
571
574
|
const messages = [];
|
|
572
575
|
for (const message of event.messages) {
|
|
573
576
|
if (message.role !== "custom" || !markedMessageTypes.has(message.customType)) {
|
|
@@ -732,7 +735,7 @@ export default function registerSubagentExtension(pi) {
|
|
|
732
735
|
event.systemPromptOptions.sections.active_subagents = activeRuns;
|
|
733
736
|
if (!event.systemPromptOptions.selectedTools.includes("subagent"))
|
|
734
737
|
return;
|
|
735
|
-
const catalog = buildAdvertisedAgentCatalog(advertisedAgents, resolveCurrentSubagentCapabilityCeiling(
|
|
738
|
+
const catalog = buildAdvertisedAgentCatalog(advertisedAgents, resolveCurrentSubagentCapabilityCeiling(ctx.sessionManager.getSessionId() ?? undefined), recordedPromptSection(pi, "advertised_subagents"));
|
|
736
739
|
if (catalog)
|
|
737
740
|
event.systemPromptOptions.sections.advertised_subagents = catalog;
|
|
738
741
|
});
|
|
@@ -1204,7 +1207,7 @@ export default function registerSubagentExtension(pi) {
|
|
|
1204
1207
|
hasPendingSupervisorRequest: () => [...supervisorChannel.pending.values()].some((request) => request.expectsReply),
|
|
1205
1208
|
advertisedPrompt: async () => {
|
|
1206
1209
|
await waitForAdvertisement();
|
|
1207
|
-
return buildAdvertisedAgentPrompt(advertisedAgents, resolveCurrentSubagentCapabilityCeiling(state.
|
|
1210
|
+
return buildAdvertisedAgentPrompt(advertisedAgents, resolveCurrentSubagentCapabilityCeiling(state.supervisorOwnerSessionId ?? undefined));
|
|
1208
1211
|
},
|
|
1209
1212
|
});
|
|
1210
1213
|
}
|
|
@@ -57,8 +57,8 @@ export type PublicSubagentExecutionNormalization<T> = {
|
|
|
57
57
|
/** A workflow value containing "/" or "\" is a script path; named workflow resource names contain neither. */
|
|
58
58
|
export declare function isWorkflowScriptPath(workflow: string): boolean;
|
|
59
59
|
/**
|
|
60
|
-
* Model tool calls supply script text
|
|
61
|
-
* workflowScript
|
|
60
|
+
* Model tool calls supply script text through workflow: true (the reply block), a script path, or options.script,
|
|
61
|
+
* which flattenSubagentToolOptions lowers to workflowScript, the internal carrier for every other launch path too.
|
|
62
62
|
*/
|
|
63
63
|
export declare function removedModelWorkflowFieldError(params: object): string | undefined;
|
|
64
64
|
export declare function validateWorkflowCapacityOverrides(params: PublicSubagentExecutionParams): string | undefined;
|
|
@@ -7,8 +7,8 @@ export function isWorkflowScriptPath(workflow) {
|
|
|
7
7
|
return workflow.includes("/") || workflow.includes("\\");
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Model tool calls supply script text
|
|
11
|
-
* workflowScript
|
|
10
|
+
* Model tool calls supply script text through workflow: true (the reply block), a script path, or options.script,
|
|
11
|
+
* which flattenSubagentToolOptions lowers to workflowScript, the internal carrier for every other launch path too.
|
|
12
12
|
*/
|
|
13
13
|
export function removedModelWorkflowFieldError(params) {
|
|
14
14
|
if (Object.hasOwn(params, "workflowScript"))
|
|
@@ -56,7 +56,7 @@ export function normalizePublicSubagentExecution(params, options = {}) {
|
|
|
56
56
|
return { ok: false, error: "workflow must be true (the ```js workflow block in this reply), a script path containing '/', or a named workflow resource.", mode: "workflow" };
|
|
57
57
|
}
|
|
58
58
|
const hasNamedWorkflow = typeof workflow === "string" && !isWorkflowScriptPath(workflow);
|
|
59
|
-
// workflowScript is the internal script carrier; model tool calls
|
|
59
|
+
// workflowScript is the internal script carrier; model tool calls set it only through options.script.
|
|
60
60
|
if (workflow !== undefined && params.workflowScript !== undefined) {
|
|
61
61
|
return { ok: false, error: "workflow cannot be combined with an internal workflowScript.", mode: "workflow" };
|
|
62
62
|
}
|
|
@@ -2,10 +2,9 @@ const WORKFLOW_FENCE = /^```(?:js|javascript) workflow[ \t]*$/;
|
|
|
2
2
|
const PLAIN_JS_FENCE = /^```(?:js|javascript)[ \t]*$/;
|
|
3
3
|
const OPEN_FENCE = /^(`{3,}|~{3,})/;
|
|
4
4
|
const CLOSE_FENCE = /^(`{3,}|~{3,})[ \t]*$/;
|
|
5
|
-
//
|
|
6
|
-
// server summary, which drops fenced blocks
|
|
7
|
-
const SUMMARY_NOTE = "On some
|
|
8
|
-
const FILE_FALLBACK = `If the block is in your reply: ${SUMMARY_NOTE}`;
|
|
5
|
+
// Through the Claude Agent SDK (pi-claude-bridge#35), text written after a tool call in the same turn reaches the session
|
|
6
|
+
// only as a server summary, which drops fenced blocks; retrying the block fails again. Tool arguments arrive intact.
|
|
7
|
+
const SUMMARY_NOTE = "On some hosts, text written after a tool call in the same turn reaches the session only as a summary without code blocks, so for the rest of this turn pass the script's source as a tool argument: options: { script: \"...\" } in place of workflow: true, with the call's other fields unchanged.";
|
|
9
8
|
/**
|
|
10
9
|
* Pi persists the whole assistant message before running its tool calls, so the
|
|
11
10
|
* message that issued this subagent call is on the branch and carries the script.
|
|
@@ -40,7 +39,7 @@ function scriptFromReply(content) {
|
|
|
40
39
|
const blocks = tagged.length > 0 ? tagged : plain.length === 1 ? plain : [];
|
|
41
40
|
if (blocks.length !== 1) {
|
|
42
41
|
const untagged = tagged.length === 0 && plain.length > 1 ? ` This reply has ${plain.length} untagged js blocks; open the script's block with the line "\`\`\`js workflow".` : "";
|
|
43
|
-
return { error: `workflow: true needs one fenced block in the same reply as the tool call, opened with the exact line "\`\`\`js workflow" and closed with "\`\`\`"; found ${tagged.length}.${untagged} ${
|
|
42
|
+
return { error: `workflow: true needs one fenced block in the same reply as the tool call, opened with the exact line "\`\`\`js workflow" and closed with "\`\`\`"; found ${tagged.length}.${untagged} If the block is in your reply: ${SUMMARY_NOTE}` };
|
|
44
43
|
}
|
|
45
44
|
const script = blocks[0];
|
|
46
45
|
if (!script.trim())
|
package/src/extension/rpc.js
CHANGED
|
@@ -5,7 +5,7 @@ import { deliverAsyncRunStop } from "../runs/foreground/async-stop-action.js";
|
|
|
5
5
|
import { reconcileAsyncRun } from "../runs/background/stale-run-reconciler.js";
|
|
6
6
|
import { resolveCurrentSessionId } from "../shared/session-identity.js";
|
|
7
7
|
import { ownerSessionIds, ownsRun } from "../runs/shared/run-ownership.js";
|
|
8
|
-
import { DIRS, SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_CHILD_STATUS_EVENT, SUBAGENT_PROCESS_TERMINAL_EVENT, SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, } from "../shared/types.js";
|
|
8
|
+
import { DIRS, SUBAGENT_ASYNC_COMPLETE_EVENT, SUBAGENT_ASYNC_STARTED_EVENT, SUBAGENT_CHILD_STATUS_EVENT, SUBAGENT_JOBS_CHANGED_EVENT, SUBAGENT_PROCESS_TERMINAL_EVENT, SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, } from "../shared/types.js";
|
|
9
9
|
import { sanitizeDisplayText, truncateDisplayText } from "../shared/display-text.js";
|
|
10
10
|
import { readStatus } from "../shared/utils.js";
|
|
11
11
|
import { SubagentFlatParams } from "./schemas.js";
|
|
@@ -336,8 +336,10 @@ function pingData(ctx, options) {
|
|
|
336
336
|
request: SUBAGENT_RPC_REQUEST_EVENT,
|
|
337
337
|
replyPrefix: SUBAGENT_RPC_REPLY_EVENT_PREFIX,
|
|
338
338
|
asyncComplete: SUBAGENT_ASYNC_COMPLETE_EVENT,
|
|
339
|
+
asyncStarted: SUBAGENT_ASYNC_STARTED_EVENT,
|
|
339
340
|
childStatus: SUBAGENT_CHILD_STATUS_EVENT,
|
|
340
341
|
processTerminal: SUBAGENT_PROCESS_TERMINAL_EVENT,
|
|
342
|
+
jobsChanged: SUBAGENT_JOBS_CHANGED_EVENT,
|
|
341
343
|
},
|
|
342
344
|
session: sessionData(ctx),
|
|
343
345
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
2
|
import { Compile } from "typebox/compile";
|
|
3
|
+
import { Value } from "typebox/value";
|
|
3
4
|
import { closestMatch } from "../shared/edit-distance.js";
|
|
4
5
|
import { removedModelWorkflowFieldError } from "./public-execution.js";
|
|
5
6
|
import { SUBAGENT_OPTION_KEYS, SubagentOptionParams, createSubagentParamsSchema } from "./schemas.js";
|
|
@@ -22,6 +23,15 @@ export function flattenSubagentToolOptions(params, disabled) {
|
|
|
22
23
|
const removedField = removedModelWorkflowFieldError(options);
|
|
23
24
|
if (removedField)
|
|
24
25
|
throw new Error(removedField);
|
|
26
|
+
// options.script carries script text as a tool argument for hosts that drop the reply's ```js block (SUMMARY_NOTE).
|
|
27
|
+
const { script, ...rest } = options;
|
|
28
|
+
if (script !== undefined) {
|
|
29
|
+
if (typeof script !== "string" || !script.trim())
|
|
30
|
+
throw new Error("subagent: options.script must be the workflow script's source text.");
|
|
31
|
+
if (topLevel.workflow !== undefined)
|
|
32
|
+
throw new Error("subagent: options.script replaces workflow; pass only one of them.");
|
|
33
|
+
return { ...flattenSubagentToolOptions({ ...topLevel, options: rest }, disabled), workflowScript: script };
|
|
34
|
+
}
|
|
25
35
|
const entries = Object.entries(options);
|
|
26
36
|
const enabledKeys = SUBAGENT_OPTION_KEYS.filter((key) => !disabled?.params.has(key));
|
|
27
37
|
const unknown = entries.map(([key]) => key).filter((key) => !optionKeys.has(key));
|
|
@@ -40,12 +50,15 @@ export function flattenSubagentToolOptions(params, disabled) {
|
|
|
40
50
|
const disabledKey = entries.find(([key]) => disabled?.params.has(key))?.[0];
|
|
41
51
|
if (disabledKey)
|
|
42
52
|
throw new Error(`subagent option 'options.${disabledKey}' is disabled by config ${disabled.params.get(disabledKey)}.`);
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
// Pi converts only the declared top-level parameters ("true" -> true); `options` is an untyped bag in the
|
|
54
|
+
// tool schema, so its values get the same conversion here. Some providers send nested scalars as strings.
|
|
55
|
+
const converted = Value.Convert(SubagentOptionParams, structuredClone(options));
|
|
56
|
+
if (!optionsValidator.Check(converted)) {
|
|
57
|
+
const errors = [...optionsValidator.Errors(converted)].slice(0, 4).map((error) => `options${error.instancePath.replaceAll("/", ".")} ${error.message}`);
|
|
45
58
|
throw new Error(`subagent: ${errors.join("; ")}.`);
|
|
46
59
|
}
|
|
47
|
-
const conflicts = entries.filter(([key, value]) => topLevel[key] !== undefined && !isDeepStrictEqual(topLevel[key], value)).map(([key]) => `'${key}'`);
|
|
60
|
+
const conflicts = Object.entries(converted).filter(([key, value]) => topLevel[key] !== undefined && !isDeepStrictEqual(topLevel[key], value)).map(([key]) => `'${key}'`);
|
|
48
61
|
if (conflicts.length > 0)
|
|
49
62
|
throw new Error(`subagent: ${conflicts.join(", ")} given both at the top level and in options with different values; give each once.`);
|
|
50
|
-
return { ...topLevel, ...
|
|
63
|
+
return { ...topLevel, ...converted };
|
|
51
64
|
}
|
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 } : {}),
|