pi-subagents 0.80.0 → 0.82.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/bundle/agents.js +18 -0
- package/bundle/background-work.js +16 -0
- package/bundle/capability-ceiling.js +22 -0
- package/bundle/child-tool-plan.js +11 -0
- package/bundle/chunk-43SCHIMD.js +57 -0
- package/bundle/chunk-4QFZZV3O.js +57 -0
- package/bundle/chunk-4SDU2BZX.js +1190 -0
- package/bundle/chunk-52UBALJL.js +753 -0
- package/bundle/chunk-5LFLVSTM.js +260 -0
- package/bundle/chunk-5TYMSZX3.js +235 -0
- package/bundle/chunk-62OCY2S4.js +14 -0
- package/bundle/chunk-7F6RKT6W.js +132 -0
- package/bundle/chunk-7IVSUMQQ.js +1337 -0
- package/bundle/chunk-7J4ZXMY7.js +18 -0
- package/bundle/chunk-7PCKZHR5.js +1234 -0
- package/bundle/chunk-7YMXOYEU.js +1373 -0
- package/bundle/chunk-B5EKXDHE.js +16870 -0
- package/bundle/chunk-CIZQXJ5Q.js +8619 -0
- package/bundle/chunk-CTPAE36Y.js +94 -0
- package/bundle/chunk-D4EHT5SJ.js +14 -0
- package/bundle/chunk-GC4DSPMH.js +946 -0
- package/bundle/chunk-GJECHSAR.js +179 -0
- package/bundle/chunk-M2CTFYT3.js +4164 -0
- package/bundle/chunk-MHZ4G23A.js +1296 -0
- package/bundle/chunk-N2BN7BF5.js +159 -0
- package/bundle/chunk-NF7IEKBH.js +490 -0
- package/bundle/chunk-NGGAFDL6.js +742 -0
- package/bundle/chunk-NIXWDZZV.js +9 -0
- package/bundle/chunk-NRQ75MGH.js +8327 -0
- package/bundle/chunk-OYF6AFDY.js +458 -0
- package/bundle/chunk-PLCCBLPY.js +526 -0
- package/bundle/chunk-RMQFMMAT.js +416 -0
- package/bundle/chunk-SIJ4S3EN.js +276 -0
- package/bundle/chunk-UKG4V45O.js +228 -0
- package/bundle/chunk-VH5OAWVG.js +4096 -0
- package/bundle/chunk-W2SZ22EM.js +5462 -0
- package/bundle/chunk-WVD4UEL4.js +9367 -0
- package/bundle/chunk-XSWOTIA3.js +175 -0
- package/bundle/chunk-YFCK7H2S.js +129 -0
- package/bundle/chunk-YKEWIB4M.js +380 -0
- package/bundle/chunk-YYREO77V.js +137 -0
- package/bundle/chunk-Z7KVHQHL.js +173 -0
- package/bundle/chunk-ZLINIYTP.js +8613 -0
- package/bundle/chunk-ZQGSDHL6.js +128 -0
- package/bundle/control-channel.js +11 -0
- package/bundle/delegation.js +14 -0
- package/bundle/external-job-provider.js +20 -0
- package/bundle/external-runs.js +21 -0
- package/bundle/index.js +30 -0
- package/bundle/inspectors.js +8 -0
- package/bundle/intercom-bridge.js +11 -0
- package/bundle/preflight.js +425 -0
- package/bundle/project-panes.js +28 -0
- package/bundle/required-child-extensions.js +6 -0
- package/bundle/shared-types.js +8 -0
- package/bundle/workflow-resources.js +7 -0
- package/docs/agents.md +31 -0
- package/docs/configuration.md +9 -1
- package/docs/extension-api.md +27 -0
- package/docs/observability.md +6 -2
- package/docs/tool-reference.md +3 -1
- package/docs/workflows.md +16 -0
- package/package.json +19 -19
- package/skills/pi-subagents/references/execution-controls.md +5 -4
- package/src/agents/agent-management.js +3 -3
- package/src/agents/agent-memory.js +3 -1
- package/src/agents/agents.js +6 -6
- package/src/agents/frontmatter.d.ts +4 -0
- package/src/agents/frontmatter.js +24 -3
- package/src/agents/skills.js +2 -1
- package/src/api/delegation.d.ts +20 -0
- package/src/extension/config.js +3 -0
- package/src/extension/herdr-pi-bridge.js +17 -10
- package/src/extension/index.js +109 -9
- package/src/extension/schemas.js +3 -3
- package/src/intercom/native-supervisor-channel.js +20 -0
- package/src/missions/lifecycle.js +7 -1
- package/src/runs/background/active-async-capacity.d.ts +1 -0
- package/src/runs/background/active-async-capacity.js +9 -3
- package/src/runs/background/async-execution.js +20 -10
- package/src/runs/background/async-job-tracker.d.ts +6 -1
- package/src/runs/background/async-job-tracker.js +9 -4
- package/src/runs/background/async-status.d.ts +2 -0
- package/src/runs/background/async-status.js +21 -2
- package/src/runs/background/await-async-run.js +3 -7
- package/src/runs/background/boot-id.d.ts +12 -0
- package/src/runs/background/boot-id.js +47 -0
- package/src/runs/background/interrupted-elsewhere.d.ts +22 -0
- package/src/runs/background/interrupted-elsewhere.js +68 -0
- package/src/runs/background/notify.d.ts +2 -0
- package/src/runs/background/notify.js +13 -0
- package/src/runs/background/result-delivery-ownership.d.ts +6 -0
- package/src/runs/background/result-delivery-ownership.js +19 -1
- package/src/runs/background/result-watcher.d.ts +3 -1
- package/src/runs/background/result-watcher.js +24 -8
- package/src/runs/background/run-child-session.d.ts +1 -6
- package/src/runs/background/run-child-session.js +19 -1
- package/src/runs/background/run-status.js +2 -1
- package/src/runs/background/stale-run-reconciler.d.ts +25 -0
- package/src/runs/background/stale-run-reconciler.js +54 -13
- package/src/runs/background/subagent-runner-bootstrap.js +3 -0
- package/src/runs/background/subagent-runner.d.ts +4 -0
- package/src/runs/background/subagent-runner.js +13 -5
- package/src/runs/background/subagent-wait.d.ts +1 -1
- package/src/runs/background/subagent-wait.js +6 -1
- package/src/runs/background/terminal-run-index.d.ts +1 -0
- package/src/runs/background/terminal-run-index.js +4 -2
- package/src/runs/foreground/async-stop-action.js +3 -1
- package/src/runs/foreground/execution.js +45 -5
- package/src/runs/foreground/foreground-history.d.ts +16 -2
- package/src/runs/foreground/foreground-history.js +133 -31
- package/src/runs/foreground/subagent-executor.d.ts +11 -0
- package/src/runs/foreground/subagent-executor.js +277 -74
- package/src/runs/shared/child-launch.d.ts +2 -2
- package/src/runs/shared/child-session.d.ts +3 -2
- package/src/runs/shared/child-session.js +51 -8
- package/src/runs/shared/child-tool-plan.d.ts +7 -0
- package/src/runs/shared/child-tool-plan.js +21 -6
- package/src/runs/shared/herdr-connection.d.ts +17 -8
- package/src/runs/shared/herdr-connection.js +19 -7
- package/src/runs/shared/herdr-external-adapters.d.ts +3 -2
- package/src/runs/shared/herdr-external-adapters.js +1 -1
- package/src/runs/shared/herdr-machine.d.ts +12 -2
- package/src/runs/shared/herdr-machine.js +55 -9
- package/src/runs/shared/herdr-pane-host.d.ts +88 -0
- package/src/runs/shared/herdr-pane-host.js +155 -0
- package/src/runs/shared/herdr-placed-run.d.ts +27 -150
- package/src/runs/shared/herdr-placed-run.js +48 -395
- package/src/runs/shared/local-machine-access.d.ts +5 -0
- package/src/runs/shared/local-machine-access.js +28 -0
- package/src/runs/shared/orca-progress-tabs.d.ts +0 -1
- package/src/runs/shared/orca-progress-tabs.js +1 -24
- package/src/runs/shared/parallel-utils.d.ts +1 -1
- package/src/runs/shared/placed-run-owner.d.ts +49 -0
- package/src/runs/shared/placed-run-owner.js +93 -0
- package/src/runs/shared/placement-host.d.ts +99 -0
- package/src/runs/shared/placement-host.js +157 -0
- package/src/runs/shared/rex-machine.d.ts +25 -0
- package/src/runs/shared/rex-machine.js +83 -0
- package/src/runs/shared/rex-pane-host.d.ts +62 -0
- package/src/runs/shared/rex-pane-host.js +327 -0
- package/src/runs/shared/structured-output.d.ts +1 -0
- package/src/runs/shared/structured-output.js +1 -0
- package/src/shared/accessible-dir.js +4 -1
- package/src/shared/completion-owner.d.ts +5 -0
- package/src/shared/completion-owner.js +5 -0
- package/src/shared/provider-diagnostics.d.ts +9 -0
- package/src/shared/provider-diagnostics.js +75 -0
- package/src/shared/session-identity.d.ts +2 -0
- package/src/shared/session-identity.js +26 -0
- package/src/shared/types.d.ts +48 -6
- package/src/slash/delegation-adapters.d.ts +3 -1
- package/src/slash/delegation-adapters.js +7 -0
- package/src/slash/prompt-template-bridge.js +3 -1
- package/src/slash/subagents-admin.d.ts +1 -0
- package/src/slash/subagents-admin.js +1 -1
- package/src/tui/fleet-layout.d.ts +20 -0
- package/src/tui/fleet-layout.js +17 -0
- package/src/tui/fleet-viewport.d.ts +12 -0
- package/src/tui/fleet-viewport.js +35 -0
- package/src/tui/fleet.d.ts +6 -6
- package/src/tui/fleet.js +93 -66
- package/src/tui/render.d.ts +6 -0
- package/src/tui/render.js +3 -3
- package/src/tui/subagent-messages.js +1 -0
- package/src/workflows/scripted-workflow.d.ts +1 -1
- package/src/workflows/structured-workflow-scripts.d.ts +1 -0
- package/src/workflows/structured-workflow-scripts.js +5 -4
- package/src/workflows/workflow-auto-resume.d.ts +32 -0
- package/src/workflows/workflow-auto-resume.js +102 -0
- package/src/workflows/workflow-launch-record.d.ts +27 -0
- package/src/workflows/workflow-launch-record.js +48 -0
- package/src/workflows/workflow-resources.js +2 -2
- package/src/workflows/workflow-resume-claim.d.ts +82 -0
- package/src/workflows/workflow-resume-claim.js +210 -0
- package/src/workflows/workflow-resume.d.ts +56 -0
- package/src/workflows/workflow-resume.js +106 -0
- package/src/workflows/workflow-reuse.d.ts +15 -9
- package/src/workflows/workflow-reuse.js +77 -14
- package/src/workflows/workflow-revival.d.ts +2 -0
- package/src/workflows/workflow-revival.js +2 -1
- package/src/workflows/workflow-stop-cause.d.ts +6 -0
- package/src/workflows/workflow-stop-cause.js +8 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// src/api/background-work.ts
|
|
2
|
+
var BACKGROUND_WORK_PROTOCOL_VERSION = 1;
|
|
3
|
+
var BACKGROUND_WORK_REGISTRY_KEY = "pi-subagents.background-work.v1";
|
|
4
|
+
var MAX_PROVIDER_NAME_LENGTH = 128;
|
|
5
|
+
var MAX_PROVIDERS = 100;
|
|
6
|
+
var MAX_ITEM_ID_LENGTH = 256;
|
|
7
|
+
var MAX_SESSION_ID_LENGTH = 4096;
|
|
8
|
+
var MAX_WAKE_CHANNEL_LENGTH = 256;
|
|
9
|
+
var MAX_ITEMS_PER_PROVIDER = 1e4;
|
|
10
|
+
function registry() {
|
|
11
|
+
const key = Symbol.for(BACKGROUND_WORK_REGISTRY_KEY);
|
|
12
|
+
const globalObject = globalThis;
|
|
13
|
+
const existing = globalObject[key];
|
|
14
|
+
if (existing === void 0) {
|
|
15
|
+
const created = {
|
|
16
|
+
version: BACKGROUND_WORK_PROTOCOL_VERSION,
|
|
17
|
+
providers: /* @__PURE__ */ new Map()
|
|
18
|
+
};
|
|
19
|
+
globalObject[key] = created;
|
|
20
|
+
return created;
|
|
21
|
+
}
|
|
22
|
+
if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
|
|
23
|
+
throw new Error(`Malformed background-work registry at Symbol.for("${BACKGROUND_WORK_REGISTRY_KEY}").`);
|
|
24
|
+
}
|
|
25
|
+
const candidate = existing;
|
|
26
|
+
if (candidate.version !== BACKGROUND_WORK_PROTOCOL_VERSION || !(candidate.providers instanceof Map)) {
|
|
27
|
+
throw new Error(`Unsupported background-work registry at Symbol.for("${BACKGROUND_WORK_REGISTRY_KEY}").`);
|
|
28
|
+
}
|
|
29
|
+
return candidate;
|
|
30
|
+
}
|
|
31
|
+
function validateString(value, field, maxLength) {
|
|
32
|
+
if (typeof value !== "string" || value.length === 0 || value.trim() !== value) {
|
|
33
|
+
throw new Error(`${field} must be a non-empty string without leading or trailing whitespace.`);
|
|
34
|
+
}
|
|
35
|
+
if (value.length > maxLength) throw new Error(`${field} must be at most ${maxLength} characters.`);
|
|
36
|
+
if (value.includes("\0")) throw new Error(`${field} must not contain NUL characters.`);
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function validateProvider(value) {
|
|
40
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
41
|
+
throw new Error("Background-work provider must be an object.");
|
|
42
|
+
}
|
|
43
|
+
const provider = value;
|
|
44
|
+
const unknownFields = Object.keys(provider).filter((key) => !["name", "listActiveWork", "wakeChannels", "reconcile"].includes(key));
|
|
45
|
+
if (unknownFields.length > 0) throw new Error(`Background-work provider has unknown fields: ${unknownFields.join(", ")}.`);
|
|
46
|
+
const name = validateString(provider.name, "Background-work provider name", MAX_PROVIDER_NAME_LENGTH);
|
|
47
|
+
if (typeof provider.listActiveWork !== "function") {
|
|
48
|
+
throw new Error(`Background-work provider '${name}' must expose listActiveWork().`);
|
|
49
|
+
}
|
|
50
|
+
if (provider.reconcile !== void 0 && typeof provider.reconcile !== "function") {
|
|
51
|
+
throw new Error(`Background-work provider '${name}' reconcile must be a function when provided.`);
|
|
52
|
+
}
|
|
53
|
+
if (provider.wakeChannels !== void 0) {
|
|
54
|
+
if (!Array.isArray(provider.wakeChannels)) {
|
|
55
|
+
throw new Error(`Background-work provider '${name}' wakeChannels must be an array when provided.`);
|
|
56
|
+
}
|
|
57
|
+
const channels = provider.wakeChannels.map((channel, index) => validateString(channel, `Background-work provider '${name}' wakeChannels[${index}]`, MAX_WAKE_CHANNEL_LENGTH));
|
|
58
|
+
if (new Set(channels).size !== channels.length) {
|
|
59
|
+
throw new Error(`Background-work provider '${name}' wakeChannels must not contain duplicates.`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
function validateItem(provider, value, index) {
|
|
65
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
66
|
+
throw new Error(`Background-work provider '${provider}' item ${index} must be an object.`);
|
|
67
|
+
}
|
|
68
|
+
const item = value;
|
|
69
|
+
const unknownFields = Object.keys(item).filter((key) => key !== "id" && key !== "sessionId");
|
|
70
|
+
if (unknownFields.length > 0) {
|
|
71
|
+
throw new Error(`Background-work provider '${provider}' item ${index} has unknown fields: ${unknownFields.join(", ")}.`);
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
id: validateString(item.id, `Background-work provider '${provider}' item ${index} id`, MAX_ITEM_ID_LENGTH),
|
|
75
|
+
sessionId: validateString(item.sessionId, `Background-work provider '${provider}' item ${index} sessionId`, MAX_SESSION_ID_LENGTH)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function registerBackgroundWorkProvider(provider) {
|
|
79
|
+
const validated = validateProvider(provider);
|
|
80
|
+
const current = registry();
|
|
81
|
+
if (!current.providers.has(validated.name) && current.providers.size >= MAX_PROVIDERS) {
|
|
82
|
+
throw new Error(`Background-work registry supports at most ${MAX_PROVIDERS} providers.`);
|
|
83
|
+
}
|
|
84
|
+
current.providers.set(validated.name, validated);
|
|
85
|
+
return () => {
|
|
86
|
+
if (current.providers.get(validated.name) === validated) current.providers.delete(validated.name);
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function listBackgroundWorkProviders() {
|
|
90
|
+
const current = registry();
|
|
91
|
+
if (current.providers.size > MAX_PROVIDERS) throw new Error(`Background-work registry contains more than ${MAX_PROVIDERS} providers.`);
|
|
92
|
+
const providers = [];
|
|
93
|
+
for (const [key, value] of current.providers) {
|
|
94
|
+
const provider = validateProvider(value);
|
|
95
|
+
if (key !== provider.name) throw new Error(`Background-work registry key '${key}' does not match provider name '${provider.name}'.`);
|
|
96
|
+
providers.push(provider);
|
|
97
|
+
}
|
|
98
|
+
return providers;
|
|
99
|
+
}
|
|
100
|
+
function listBackgroundWorkWakeChannels() {
|
|
101
|
+
const channels = /* @__PURE__ */ new Set();
|
|
102
|
+
for (const provider of listBackgroundWorkProviders()) {
|
|
103
|
+
for (const channel of provider.wakeChannels ?? []) channels.add(channel);
|
|
104
|
+
}
|
|
105
|
+
return [...channels];
|
|
106
|
+
}
|
|
107
|
+
function snapshotBackgroundWork(sessionId, nowMs = Date.now()) {
|
|
108
|
+
validateString(sessionId, "Background-work snapshot sessionId", MAX_SESSION_ID_LENGTH);
|
|
109
|
+
const providers = listBackgroundWorkProviders();
|
|
110
|
+
const items = [];
|
|
111
|
+
const identities = /* @__PURE__ */ new Set();
|
|
112
|
+
for (const provider of providers) {
|
|
113
|
+
try {
|
|
114
|
+
provider.reconcile?.({ sessionId, nowMs });
|
|
115
|
+
} catch (error) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Background-work provider '${provider.name}' reconcile failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
118
|
+
{ cause: error }
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
let active;
|
|
122
|
+
try {
|
|
123
|
+
active = provider.listActiveWork({ sessionId, nowMs });
|
|
124
|
+
} catch (error) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Background-work provider '${provider.name}' listActiveWork failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
127
|
+
{ cause: error }
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (!Array.isArray(active)) {
|
|
131
|
+
throw new Error(`Background-work provider '${provider.name}' listActiveWork() must return an array.`);
|
|
132
|
+
}
|
|
133
|
+
if (active.length > MAX_ITEMS_PER_PROVIDER) {
|
|
134
|
+
throw new Error(`Background-work provider '${provider.name}' returned ${active.length} items; maximum is ${MAX_ITEMS_PER_PROVIDER}.`);
|
|
135
|
+
}
|
|
136
|
+
active.forEach((value, index) => {
|
|
137
|
+
const item = validateItem(provider.name, value, index);
|
|
138
|
+
const identity = `${provider.name}\0${item.sessionId}\0${item.id}`;
|
|
139
|
+
if (identities.has(identity)) {
|
|
140
|
+
throw new Error(`Background-work provider '${provider.name}' returned duplicate item '${item.id}' for session '${item.sessionId}'.`);
|
|
141
|
+
}
|
|
142
|
+
identities.add(identity);
|
|
143
|
+
if (item.sessionId === sessionId) items.push({ provider: provider.name, ...item });
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
providers: providers.map((provider) => provider.name),
|
|
148
|
+
items
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export {
|
|
153
|
+
BACKGROUND_WORK_PROTOCOL_VERSION,
|
|
154
|
+
BACKGROUND_WORK_REGISTRY_KEY,
|
|
155
|
+
registerBackgroundWorkProvider,
|
|
156
|
+
listBackgroundWorkProviders,
|
|
157
|
+
listBackgroundWorkWakeChannels,
|
|
158
|
+
snapshotBackgroundWork
|
|
159
|
+
};
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import {
|
|
2
|
+
stableJsonDigest
|
|
3
|
+
} from "./chunk-7F6RKT6W.js";
|
|
4
|
+
|
|
5
|
+
// src/shared/workflow-child-permit.ts
|
|
6
|
+
var records = /* @__PURE__ */ new WeakMap();
|
|
7
|
+
function projectionDigest(input) {
|
|
8
|
+
return stableJsonDigest({
|
|
9
|
+
version: 1,
|
|
10
|
+
childKey: input.childKey,
|
|
11
|
+
agent: input.agent,
|
|
12
|
+
launchContractDigest: input.launchContractDigest,
|
|
13
|
+
context: input.context,
|
|
14
|
+
runner: input.runner
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function required(value, label) {
|
|
18
|
+
if (!value.trim() || value !== value.trim()) throw new Error(`${label} must be a non-empty trimmed string.`);
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function validateWorkflowChildPermitRoot(permit, workflowRunId) {
|
|
22
|
+
const record = records.get(permit);
|
|
23
|
+
if (!record) return "Workflow child permit is invalid.";
|
|
24
|
+
if (record.state !== "available") return "Workflow child permit is already consumed.";
|
|
25
|
+
if (record.workflowRunId !== workflowRunId) return "Workflow child permit does not match this workflow root.";
|
|
26
|
+
return void 0;
|
|
27
|
+
}
|
|
28
|
+
function claimWorkflowChildPermit(permit, workflowRunId, childKey) {
|
|
29
|
+
const record = records.get(permit);
|
|
30
|
+
if (!record) return "Workflow child permit is invalid.";
|
|
31
|
+
if (record.state !== "available") return "Workflow child permit is already consumed.";
|
|
32
|
+
if (record.workflowRunId !== workflowRunId) return "Workflow child permit does not match this workflow root.";
|
|
33
|
+
record.state = record.childKey === childKey ? "claimed" : "consumed";
|
|
34
|
+
if (record.childKey !== childKey) return "Workflow child permit child key mismatch.";
|
|
35
|
+
return void 0;
|
|
36
|
+
}
|
|
37
|
+
function consumeWorkflowChildPermit(permit, launch) {
|
|
38
|
+
const record = records.get(permit);
|
|
39
|
+
if (!record) return "Workflow child permit is invalid.";
|
|
40
|
+
if (record.state === "available") return "Workflow child permit launch was not claimed.";
|
|
41
|
+
if (record.state === "consumed") return "Workflow child permit is already consumed.";
|
|
42
|
+
if (record.workflowRunId !== launch.workflowRunId) return "Workflow child permit does not match this workflow root.";
|
|
43
|
+
record.state = "consumed";
|
|
44
|
+
if (record.childKey !== launch.childKey) return "Workflow child permit child key mismatch.";
|
|
45
|
+
if (record.agent !== launch.agent) return "Workflow child permit agent mismatch.";
|
|
46
|
+
if (launch.runner !== "pi") return "Workflow child permit supports native Pi children only.";
|
|
47
|
+
if (record.expectedProjectionDigest !== projectionDigest(launch)) return "Workflow child permit does not match the final launch projection.";
|
|
48
|
+
return void 0;
|
|
49
|
+
}
|
|
50
|
+
var resourceRecords = /* @__PURE__ */ new WeakMap();
|
|
51
|
+
function cloneWorkflowResourceAuthority(authority) {
|
|
52
|
+
if (!authority || typeof authority !== "object" || Array.isArray(authority)) throw new Error("Workflow resource authority must be an object.");
|
|
53
|
+
if (authority.host === void 0) return Object.freeze({});
|
|
54
|
+
if (!Array.isArray(authority.host) || authority.host.length > 32) throw new Error("Workflow resource host authority must be an array of at most 32 grants.");
|
|
55
|
+
const keys = /* @__PURE__ */ new Set();
|
|
56
|
+
const host = Array.from(authority.host, (grant) => {
|
|
57
|
+
if (!grant || typeof grant !== "object" || Object.keys(grant).some((field) => field !== "key" && field !== "command")) throw new Error("Workflow resource host grant must contain only key and command.");
|
|
58
|
+
const { key, command } = grant;
|
|
59
|
+
if (typeof key !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(key) || keys.has(key)) throw new Error("Workflow resource host grant requires a unique safe key.");
|
|
60
|
+
if (typeof command !== "string" || !command.trim() || Buffer.byteLength(command.trim(), "utf8") > 16 * 1024 || command.includes("\0")) throw new Error("Workflow resource host grant requires a non-empty command of at most 16384 bytes without NUL.");
|
|
61
|
+
keys.add(key);
|
|
62
|
+
return Object.freeze({ key, command: command.trim() });
|
|
63
|
+
});
|
|
64
|
+
return Object.freeze({ host: Object.freeze(host) });
|
|
65
|
+
}
|
|
66
|
+
function createWorkflowResourcePermit(input) {
|
|
67
|
+
const resourceName = required(input.resourceName, "resourceName");
|
|
68
|
+
const resourceId = required(input.resourceId, "resourceId");
|
|
69
|
+
const scriptDigest = required(input.scriptDigest, "scriptDigest");
|
|
70
|
+
if (!Number.isInteger(input.resourceVersion) || input.resourceVersion < 1) throw new Error("resourceVersion must be a positive integer.");
|
|
71
|
+
const authority = cloneWorkflowResourceAuthority(input.authority);
|
|
72
|
+
const permit = Object.freeze(/* @__PURE__ */ Object.create(null));
|
|
73
|
+
resourceRecords.set(permit, {
|
|
74
|
+
resourceName,
|
|
75
|
+
resourceVersion: input.resourceVersion,
|
|
76
|
+
resourceId,
|
|
77
|
+
scriptDigest,
|
|
78
|
+
authority,
|
|
79
|
+
provenance: Object.freeze({
|
|
80
|
+
kind: "workflow",
|
|
81
|
+
name: resourceName,
|
|
82
|
+
version: input.resourceVersion,
|
|
83
|
+
invocation: "named",
|
|
84
|
+
expansion: "resolved",
|
|
85
|
+
id: resourceId
|
|
86
|
+
}),
|
|
87
|
+
state: "available"
|
|
88
|
+
});
|
|
89
|
+
return permit;
|
|
90
|
+
}
|
|
91
|
+
function consumeWorkflowResourcePermit(permit, script) {
|
|
92
|
+
const record = resourceRecords.get(permit);
|
|
93
|
+
if (!record) return "Workflow resource permit is invalid.";
|
|
94
|
+
if (record.state !== "available") return "Workflow resource permit is already consumed.";
|
|
95
|
+
if (stableJsonDigest(script) !== record.scriptDigest) return "Workflow resource permit does not match the resolved workflow script.";
|
|
96
|
+
record.state = "consumed";
|
|
97
|
+
return { provenance: record.provenance, authority: record.authority };
|
|
98
|
+
}
|
|
99
|
+
function authorizeWorkflowResourceHost(permit, key, command) {
|
|
100
|
+
const record = resourceRecords.get(permit);
|
|
101
|
+
if (!record || record.state !== "consumed") return "Workflow resource authority is unavailable.";
|
|
102
|
+
const host = record.authority.host;
|
|
103
|
+
if (!host) return "runs.host is not allowed for this workflow resource.";
|
|
104
|
+
if (!host.some((grant) => grant.key === key && grant.command === command.trim())) return `The command for runs.host('${key}') is not allowed for workflow resource '${record.resourceName}'.`;
|
|
105
|
+
return void 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/workflows/workflow-resources.ts
|
|
109
|
+
import { randomUUID } from "node:crypto";
|
|
110
|
+
|
|
111
|
+
// src/workflows/structured-workflow-scripts.ts
|
|
112
|
+
var OUTPUT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
113
|
+
var PLACEHOLDER_PATTERN = /\{(?:task|previous|outputs\.([A-Za-z_][A-Za-z0-9_]*))\}/g;
|
|
114
|
+
var CHILD_FIELDS = ["cwd", "model"];
|
|
115
|
+
var TOP_LEVEL_CONTROLS_HINT = " Put worktree and other child controls on the subagent call itself; they apply to every child.";
|
|
116
|
+
function isPlainRecord(value) {
|
|
117
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
118
|
+
const prototype = Object.getPrototypeOf(value);
|
|
119
|
+
return prototype === Object.prototype || prototype === null;
|
|
120
|
+
}
|
|
121
|
+
function nonEmptyString(value, path) {
|
|
122
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`${path} must be a non-empty string.`);
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
function exactFields(value, path, allowed, hint) {
|
|
126
|
+
if (!isPlainRecord(value)) throw new Error(`${path} must be an object.`);
|
|
127
|
+
const unsupported = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
128
|
+
if (unsupported.length > 0) throw new Error(`${path} contains unsupported fields: ${unsupported.join(", ")}. ${hint}`);
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
function compileTemplate(template, scope) {
|
|
132
|
+
const parts = [];
|
|
133
|
+
let cursor = 0;
|
|
134
|
+
for (const match of template.matchAll(PLACEHOLDER_PATTERN)) {
|
|
135
|
+
const literal2 = template.slice(cursor, match.index);
|
|
136
|
+
cursor = match.index + match[0].length;
|
|
137
|
+
let variable;
|
|
138
|
+
if (match[0] === "{task}") {
|
|
139
|
+
if (!scope.hasOriginalTask) throw new Error(`${scope.path} references {task}, but no top-level task was provided.`);
|
|
140
|
+
variable = "originalTask";
|
|
141
|
+
} else if (match[0] === "{previous}") {
|
|
142
|
+
if ("unavailable" in scope.previous) throw new Error(`${scope.path} cannot use {previous}; ${scope.previous.unavailable}`);
|
|
143
|
+
variable = scope.previous.variable;
|
|
144
|
+
} else {
|
|
145
|
+
const name = match[1];
|
|
146
|
+
if (!scope.outputs) throw new Error(`${scope.path} cannot use {outputs.${name}}; ${scope.outputsUnavailable}`);
|
|
147
|
+
const named = scope.outputs.get(name);
|
|
148
|
+
if (!named) throw new Error(`${scope.path} references {outputs.${name}}, but no earlier sequential step sets as: "${name}".`);
|
|
149
|
+
variable = named;
|
|
150
|
+
}
|
|
151
|
+
if (literal2) parts.push(JSON.stringify(literal2));
|
|
152
|
+
parts.push(variable);
|
|
153
|
+
}
|
|
154
|
+
const literal = template.slice(cursor);
|
|
155
|
+
if (literal || parts.length === 0) parts.push(JSON.stringify(literal));
|
|
156
|
+
return parts.join(" + ");
|
|
157
|
+
}
|
|
158
|
+
function prelude(originalTask) {
|
|
159
|
+
return originalTask === void 0 ? [SETTLE_HELPER] : [SETTLE_HELPER, `const originalTask = ${JSON.stringify(originalTask)};`];
|
|
160
|
+
}
|
|
161
|
+
function childFields(record, path) {
|
|
162
|
+
const fields = {};
|
|
163
|
+
for (const field of CHILD_FIELDS) {
|
|
164
|
+
if (record[field] !== void 0) fields[field] = nonEmptyString(record[field], `${path}.${field}`);
|
|
165
|
+
}
|
|
166
|
+
return fields;
|
|
167
|
+
}
|
|
168
|
+
function runsAllCall(children) {
|
|
169
|
+
return `await runs.all([${children.map((child) => {
|
|
170
|
+
const fields = Object.entries(child.fields).map(([field, value]) => `, ${field}: ${JSON.stringify(value)}`).join("");
|
|
171
|
+
return `{ key: ${JSON.stringify(child.key)}, agent: ${JSON.stringify(child.agent)}, task: ${child.taskExpression}${fields} }`;
|
|
172
|
+
}).join(", ")}])`;
|
|
173
|
+
}
|
|
174
|
+
var SETTLE_HELPER = [
|
|
175
|
+
"function settle(results, agents) {",
|
|
176
|
+
" return results.map(function (result, index) {",
|
|
177
|
+
' const child = { key: result.key, agent: agents[index], ok: result.ok === true, output: typeof result.output === "string" ? result.output : "" };',
|
|
178
|
+
' if (!child.ok && typeof result.error === "string") child.error = result.error;',
|
|
179
|
+
" return child;",
|
|
180
|
+
" });",
|
|
181
|
+
"}",
|
|
182
|
+
// A failed child fails the workflow; the runtime keeps every settled child in the partial results.
|
|
183
|
+
"function failure(label, children) {",
|
|
184
|
+
' return new Error(label + ": " + children.filter(function (child) { return !child.ok; }).map(function (child) { return child.key + " (" + child.agent + ") failed" + (child.error ? ": " + child.error : ""); }).join("; "));',
|
|
185
|
+
"}"
|
|
186
|
+
].join("\n");
|
|
187
|
+
function buildTasksScript(steps, originalTask) {
|
|
188
|
+
if (!Array.isArray(steps) || steps.length === 0) throw new Error("tasks must be a non-empty array of { agent, task } items.");
|
|
189
|
+
const children = steps.map((item, index) => {
|
|
190
|
+
const path = `tasks[${index}]`;
|
|
191
|
+
const record = exactFields(item, path, ["agent", "task", ...CHILD_FIELDS], `Each tasks item accepts agent and task, plus optional cwd and model.${TOP_LEVEL_CONTROLS_HINT}`);
|
|
192
|
+
const agent = nonEmptyString(record.agent, `${path}.agent`);
|
|
193
|
+
const task = nonEmptyString(record.task, `${path}.task`);
|
|
194
|
+
const taskExpression = compileTemplate(task, {
|
|
195
|
+
path: `${path}.task`,
|
|
196
|
+
hasOriginalTask: originalTask !== void 0,
|
|
197
|
+
previous: { unavailable: "tasks items may only use {task}." },
|
|
198
|
+
outputsUnavailable: "tasks items may only use {task}."
|
|
199
|
+
});
|
|
200
|
+
return { key: `task-${index + 1}`, agent, taskExpression, fields: childFields(record, path) };
|
|
201
|
+
});
|
|
202
|
+
return [
|
|
203
|
+
...prelude(originalTask),
|
|
204
|
+
`const run1 = ${runsAllCall(children)};`,
|
|
205
|
+
`const children = settle(run1, ${JSON.stringify(children.map((child) => child.agent))});`,
|
|
206
|
+
'if (!children.every(function (child) { return child.ok; })) throw failure("tasks failed", children);',
|
|
207
|
+
"return { ok: true, children };"
|
|
208
|
+
].join("\n");
|
|
209
|
+
}
|
|
210
|
+
function buildChainScript(steps, originalTask) {
|
|
211
|
+
if (!Array.isArray(steps) || steps.length === 0) throw new Error("chain must be a non-empty array of steps.");
|
|
212
|
+
const lines = [...prelude(originalTask), "const children = [];"];
|
|
213
|
+
const hasOriginalTask = originalTask !== void 0;
|
|
214
|
+
const outputs = /* @__PURE__ */ new Map();
|
|
215
|
+
for (const [index, step] of steps.entries()) {
|
|
216
|
+
const number = index + 1;
|
|
217
|
+
const path = `chain[${index}]`;
|
|
218
|
+
const previous = index === 0 ? { unavailable: "the first chain step has no previous output." } : { variable: `output${index}` };
|
|
219
|
+
let stepChildren;
|
|
220
|
+
let outputName;
|
|
221
|
+
if (isPlainRecord(step) && Object.hasOwn(step, "parallel")) {
|
|
222
|
+
const record = exactFields(step, path, ["parallel"], `A parallel step accepts only parallel; as is supported on sequential steps only.${TOP_LEVEL_CONTROLS_HINT}`);
|
|
223
|
+
const items = record.parallel;
|
|
224
|
+
if (!Array.isArray(items) || items.length === 0) throw new Error(`${path}.parallel must be a non-empty array of { agent, task } items.`);
|
|
225
|
+
stepChildren = items.map((item, itemIndex) => {
|
|
226
|
+
const itemPath = `${path}.parallel[${itemIndex}]`;
|
|
227
|
+
const itemRecord = exactFields(item, itemPath, ["agent", "task", ...CHILD_FIELDS], `Each parallel item accepts agent and task, plus optional cwd and model.${TOP_LEVEL_CONTROLS_HINT}`);
|
|
228
|
+
const agent = nonEmptyString(itemRecord.agent, `${itemPath}.agent`);
|
|
229
|
+
const task = nonEmptyString(itemRecord.task, `${itemPath}.task`);
|
|
230
|
+
return { key: `step-${number}-${itemIndex + 1}`, agent, fields: childFields(itemRecord, itemPath), taskExpression: compileTemplate(task, { path: `${itemPath}.task`, hasOriginalTask, previous, outputs }) };
|
|
231
|
+
});
|
|
232
|
+
} else {
|
|
233
|
+
const record = exactFields(step, path, ["agent", "task", "as", ...CHILD_FIELDS], `A sequential step accepts agent, task, as, cwd and model; a parallel step accepts only parallel.${TOP_LEVEL_CONTROLS_HINT}`);
|
|
234
|
+
const agent = nonEmptyString(record.agent, `${path}.agent`);
|
|
235
|
+
let task;
|
|
236
|
+
if (record.task === void 0) {
|
|
237
|
+
if (index === 0) throw new Error(`${path}.task is required; the first chain step has no previous output.`);
|
|
238
|
+
task = "{previous}";
|
|
239
|
+
} else task = nonEmptyString(record.task, `${path}.task`);
|
|
240
|
+
if (record.as !== void 0) {
|
|
241
|
+
if (typeof record.as !== "string" || !OUTPUT_NAME_PATTERN.test(record.as)) throw new Error(`${path}.as must be an identifier matching ${OUTPUT_NAME_PATTERN}.`);
|
|
242
|
+
if (outputs.has(record.as)) throw new Error(`${path}.as '${record.as}' is already used by an earlier step.`);
|
|
243
|
+
outputName = record.as;
|
|
244
|
+
}
|
|
245
|
+
stepChildren = [{ key: `step-${number}`, agent, fields: childFields(record, path), taskExpression: compileTemplate(task, { path: `${path}.task`, hasOriginalTask, previous, outputs }) }];
|
|
246
|
+
}
|
|
247
|
+
lines.push(
|
|
248
|
+
`const run${number} = ${runsAllCall(stepChildren)};`,
|
|
249
|
+
`const step${number} = settle(run${number}, ${JSON.stringify(stepChildren.map((child) => child.agent))});`,
|
|
250
|
+
`children.push(...step${number});`,
|
|
251
|
+
`if (!step${number}.every(function (child) { return child.ok; })) throw failure(${JSON.stringify(`chain stopped at step ${number}`)}, step${number});`
|
|
252
|
+
);
|
|
253
|
+
if (index < steps.length - 1) lines.push(`const output${number} = step${number}.map(function (child) { return child.output; }).join("\\n\\n");`);
|
|
254
|
+
if (outputName !== void 0) outputs.set(outputName, `output${number}`);
|
|
255
|
+
}
|
|
256
|
+
lines.push("return { ok: true, children };");
|
|
257
|
+
return lines.join("\n");
|
|
258
|
+
}
|
|
259
|
+
function buildStructuredWorkflowScript(kind, steps, originalTask) {
|
|
260
|
+
return kind === "tasks" ? buildTasksScript(steps, originalTask) : buildChainScript(steps, originalTask);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/workflows/workflow-resources.ts
|
|
264
|
+
var RESOURCE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
265
|
+
var MAX_ARGS_BYTES = 16 * 1024;
|
|
266
|
+
var MAX_ARGS_FIELDS = 16;
|
|
267
|
+
var MAX_ARGS_ITEMS = 64;
|
|
268
|
+
var MAX_ARGS_DEPTH = 8;
|
|
269
|
+
var MAX_STRING_BYTES = 16 * 1024;
|
|
270
|
+
function registry() {
|
|
271
|
+
const key = /* @__PURE__ */ Symbol.for("pi-subagents.workflow-resources.v1");
|
|
272
|
+
const globalObject = globalThis;
|
|
273
|
+
const existing = globalObject[key];
|
|
274
|
+
if (existing === void 0) {
|
|
275
|
+
const created = { version: 1, bySession: /* @__PURE__ */ new Map() };
|
|
276
|
+
globalObject[key] = created;
|
|
277
|
+
return created;
|
|
278
|
+
}
|
|
279
|
+
if (!isPlainRecord(existing) || existing.version !== 1 || !(existing.bySession instanceof Map)) throw new Error("Malformed or unsupported workflow resource registry.");
|
|
280
|
+
return existing;
|
|
281
|
+
}
|
|
282
|
+
function registerWorkflowResource(input) {
|
|
283
|
+
if (!isPlainRecord(input) || Object.keys(input).some((key) => key !== "sessionId" && key !== "definition")) throw new Error("Workflow registration requires only sessionId and definition.");
|
|
284
|
+
const { sessionId, definition } = input;
|
|
285
|
+
if (typeof sessionId !== "string" || !sessionId || sessionId.trim() !== sessionId || sessionId.length > 256 || sessionId.includes("\0")) throw new Error("Workflow registration requires a non-empty trimmed sessionId of at most 256 characters without NUL.");
|
|
286
|
+
if (!isPlainRecord(definition) || Object.keys(definition).some((key) => !["name", "version", "resolve"].includes(key))) throw new Error("Workflow definition requires only name, version and resolve.");
|
|
287
|
+
const { name, version, resolve } = definition;
|
|
288
|
+
if (typeof name !== "string" || !RESOURCE_NAME_PATTERN.test(name)) throw new Error("Workflow definition requires a safe resource name.");
|
|
289
|
+
if (!Number.isSafeInteger(version) || version < 1) throw new Error("Workflow definition version must be a positive safe integer.");
|
|
290
|
+
if (typeof resolve !== "function") throw new Error("Workflow definition requires a synchronous resolve function.");
|
|
291
|
+
if (findWorkflowResource(name) || STRUCTURED_WORKFLOW_RESOURCE_NAMES.includes(name)) throw new Error(`Workflow resource '${name}' is a protected builtin.`);
|
|
292
|
+
const current = registry();
|
|
293
|
+
const bucket = current.bySession.get(sessionId) ?? /* @__PURE__ */ new Map();
|
|
294
|
+
if (!(bucket instanceof Map)) throw new Error("Malformed workflow resource session registry.");
|
|
295
|
+
if (bucket.has(name)) throw new Error(`Workflow resource '${name}' is already registered in this session; dispose it first.`);
|
|
296
|
+
const snapshot = Object.freeze({ name, version, resolve });
|
|
297
|
+
bucket.set(name, snapshot);
|
|
298
|
+
current.bySession.set(sessionId, bucket);
|
|
299
|
+
let disposed = false;
|
|
300
|
+
return {
|
|
301
|
+
dispose() {
|
|
302
|
+
if (disposed) return;
|
|
303
|
+
disposed = true;
|
|
304
|
+
if (bucket.get(name) === snapshot) bucket.delete(name);
|
|
305
|
+
if (bucket.size === 0 && current.bySession.get(sessionId) === bucket) current.bySession.delete(sessionId);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function jsonByteLength(value) {
|
|
310
|
+
try {
|
|
311
|
+
const encoded = JSON.stringify(value);
|
|
312
|
+
if (encoded === void 0) throw new Error("must contain JSON data");
|
|
313
|
+
return Buffer.byteLength(encoded, "utf8");
|
|
314
|
+
} catch (error) {
|
|
315
|
+
throw new Error(`must contain plain JSON data: ${error instanceof Error ? error.message : String(error)}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function validatePlainJson(value, path, depth = 0) {
|
|
319
|
+
if (depth > MAX_ARGS_DEPTH) throw new Error(`${path} is too deeply nested.`);
|
|
320
|
+
if (value === null || typeof value === "boolean") return;
|
|
321
|
+
if (typeof value === "string") {
|
|
322
|
+
if (!value.trim()) throw new Error(`${path} must not be empty.`);
|
|
323
|
+
if (Buffer.byteLength(value, "utf8") > MAX_STRING_BYTES) throw new Error(`${path} exceeds ${MAX_STRING_BYTES} bytes.`);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (typeof value === "number") {
|
|
327
|
+
if (!Number.isFinite(value)) throw new Error(`${path} must be finite.`);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (Array.isArray(value)) {
|
|
331
|
+
if (value.length > MAX_ARGS_ITEMS) throw new Error(`${path} contains too many items.`);
|
|
332
|
+
for (const [index, entry] of value.entries()) validatePlainJson(entry, `${path}[${index}]`, depth + 1);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (!isPlainRecord(value)) throw new Error(`${path} must contain plain JSON data.`);
|
|
336
|
+
if (Object.keys(value).length > MAX_ARGS_FIELDS) throw new Error(`${path} contains too many fields.`);
|
|
337
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
338
|
+
if (!key.trim()) throw new Error(`${path} contains an empty field name.`);
|
|
339
|
+
validatePlainJson(entry, `${path}.${key}`, depth + 1);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function normalizeWorkflowArgs(value) {
|
|
343
|
+
if (value === void 0) return { args: {} };
|
|
344
|
+
if (!isPlainRecord(value)) return { error: "workflow args must be a plain JSON object." };
|
|
345
|
+
try {
|
|
346
|
+
validatePlainJson(value, "workflow args");
|
|
347
|
+
if (jsonByteLength(value) > MAX_ARGS_BYTES) return { error: `workflow args exceed ${MAX_ARGS_BYTES} bytes.` };
|
|
348
|
+
return { args: JSON.parse(JSON.stringify(value)) };
|
|
349
|
+
} catch (error) {
|
|
350
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function deepFreezeWorkflowArgs(args) {
|
|
354
|
+
deepFreezeWorkflowValue(args);
|
|
355
|
+
return args;
|
|
356
|
+
}
|
|
357
|
+
function deepFreezeWorkflowValue(value) {
|
|
358
|
+
if (!Array.isArray(value) && !isPlainRecord(value)) return;
|
|
359
|
+
for (const entry of Object.values(value)) deepFreezeWorkflowValue(entry);
|
|
360
|
+
Object.freeze(value);
|
|
361
|
+
}
|
|
362
|
+
function resolveRunCi(args) {
|
|
363
|
+
const allowed = /* @__PURE__ */ new Set(["command", "timeoutMs"]);
|
|
364
|
+
const unsupported = Object.keys(args).filter((key) => !allowed.has(key));
|
|
365
|
+
if (unsupported.length > 0) return { error: `workflow 'run-ci' args contain unsupported fields: ${unsupported.join(", ")}.` };
|
|
366
|
+
const command = args.command === void 0 ? "npm test" : args.command;
|
|
367
|
+
if (command !== "npm test" && command !== "npm run typecheck") return { error: "workflow 'run-ci' args.command must be 'npm test' or 'npm run typecheck'." };
|
|
368
|
+
const timeoutMs = args.timeoutMs === void 0 ? 12e4 : args.timeoutMs;
|
|
369
|
+
if (typeof timeoutMs !== "number" || !Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 864e5) return { error: "workflow 'run-ci' args.timeoutMs must be an integer from 1 to 86400000." };
|
|
370
|
+
const params = { kind: "command", command, timeoutMs, role: "ci" };
|
|
371
|
+
return {
|
|
372
|
+
script: `return await runs.host("ci", ${JSON.stringify(params)});`,
|
|
373
|
+
hostCommands: [{ key: "ci", command }]
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function resolveParallel(args) {
|
|
377
|
+
const unsupported = Object.keys(args).filter((key) => key !== "tasks");
|
|
378
|
+
if (unsupported.length > 0) return { error: `workflow 'parallel' args contain unsupported fields: ${unsupported.join(", ")}. Pass args.tasks: [{ agent, task, cwd?, model? }, ...].${TOP_LEVEL_CONTROLS_HINT}` };
|
|
379
|
+
try {
|
|
380
|
+
return { script: buildStructuredWorkflowScript("tasks", args.tasks, void 0) };
|
|
381
|
+
} catch (error) {
|
|
382
|
+
return { error: `workflow 'parallel' args.${error instanceof Error ? error.message : String(error)}` };
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function resolveReview(args) {
|
|
386
|
+
const unsupported = Object.keys(args).filter((key) => key !== "task");
|
|
387
|
+
if (unsupported.length > 0) return { error: `workflow 'review' args contain unsupported fields: ${unsupported.join(", ")}.` };
|
|
388
|
+
const task = args.task;
|
|
389
|
+
if (typeof task !== "string" || !task.trim()) return { error: "workflow 'review' requires a non-empty string args.task." };
|
|
390
|
+
return {
|
|
391
|
+
script: `return (await runs.run("review", { agent: "reviewer", task: ${JSON.stringify(task.trim())} })).output;`
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
var WORKFLOW_RESOURCES = [
|
|
395
|
+
{ name: "review", version: 1, resolve: resolveReview },
|
|
396
|
+
{ name: "run-ci", version: 1, resolve: resolveRunCi },
|
|
397
|
+
{ name: "parallel", version: 1, resolve: resolveParallel }
|
|
398
|
+
];
|
|
399
|
+
function findWorkflowResource(name) {
|
|
400
|
+
return WORKFLOW_RESOURCES.find((resource) => resource.name === name);
|
|
401
|
+
}
|
|
402
|
+
function listWorkflowResourceNames() {
|
|
403
|
+
return WORKFLOW_RESOURCES.map((resource) => resource.name);
|
|
404
|
+
}
|
|
405
|
+
var STRUCTURED_WORKFLOW_RESOURCE_NAMES = ["tasks", "chain"];
|
|
406
|
+
var STRUCTURED_WORKFLOW_RESOURCE_VERSION = 1;
|
|
407
|
+
function resolveStructuredWorkflowResource(input) {
|
|
408
|
+
try {
|
|
409
|
+
if (!isPlainRecord(input) || input.kind !== "tasks" && input.kind !== "chain") return { ok: false, error: "Structured workflow kind must be 'tasks' or 'chain'." };
|
|
410
|
+
const { kind, steps, task } = input;
|
|
411
|
+
if (!Array.isArray(steps) || steps.length === 0) return { ok: false, error: `${kind} must be a non-empty array.` };
|
|
412
|
+
validatePlainJson(steps, kind);
|
|
413
|
+
if (task !== void 0) {
|
|
414
|
+
if (typeof task !== "string" || !task.trim()) return { ok: false, error: "task must be a non-empty string when provided." };
|
|
415
|
+
validatePlainJson(task, "task");
|
|
416
|
+
}
|
|
417
|
+
if (jsonByteLength({ [kind]: steps, task }) > MAX_ARGS_BYTES) return { ok: false, error: `${kind} input exceeds ${MAX_ARGS_BYTES} bytes.` };
|
|
418
|
+
const script = buildStructuredWorkflowScript(kind, JSON.parse(JSON.stringify(steps)), task);
|
|
419
|
+
return { ok: true, resource: issueWorkflowResource(kind, STRUCTURED_WORKFLOW_RESOURCE_VERSION, script) };
|
|
420
|
+
} catch (error) {
|
|
421
|
+
return { ok: false, error: error instanceof Error ? error.message.slice(0, 4096) : "Structured workflow resolution failed." };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function resolveWorkflowResource(nameValue, argsValue, sessionId) {
|
|
425
|
+
try {
|
|
426
|
+
const result = resolveResource(nameValue, argsValue, sessionId);
|
|
427
|
+
return result.ok ? result : { ok: false, error: result.error.slice(0, 4096) };
|
|
428
|
+
} catch (error) {
|
|
429
|
+
return { ok: false, error: error instanceof Error ? error.message.slice(0, 4096) : "Workflow resource resolution failed." };
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function resolveResource(nameValue, argsValue, sessionId) {
|
|
433
|
+
if (typeof nameValue !== "string" || !nameValue.trim()) return { ok: false, error: "workflow must be a non-empty resource name." };
|
|
434
|
+
const name = nameValue.trim();
|
|
435
|
+
if (!RESOURCE_NAME_PATTERN.test(name)) return { ok: false, error: "workflow must use a safe resource name." };
|
|
436
|
+
const resource = findWorkflowResource(name) ?? (sessionId ? registry().bySession.get(sessionId)?.get(name) : void 0);
|
|
437
|
+
if (!resource) return { ok: false, error: `Unknown workflow resource '${name}'. Available resources: ${listWorkflowResourceNames().join(", ")}.` };
|
|
438
|
+
const normalizedArgs = normalizeWorkflowArgs(argsValue);
|
|
439
|
+
if ("error" in normalizedArgs) return { ok: false, error: normalizedArgs.error };
|
|
440
|
+
const resolved = resource.resolve(normalizedArgs.args);
|
|
441
|
+
if (resolved && typeof resolved.then === "function") {
|
|
442
|
+
void Promise.resolve(resolved).catch(() => {
|
|
443
|
+
});
|
|
444
|
+
throw new Error("Workflow resource resolve must be synchronous.");
|
|
445
|
+
}
|
|
446
|
+
if (!isPlainRecord(resolved)) throw new Error("Workflow resource resolve must return an expansion or error object.");
|
|
447
|
+
if ("error" in resolved) {
|
|
448
|
+
if (Object.keys(resolved).some((key) => key !== "error") || typeof resolved.error !== "string" || !resolved.error.trim()) throw new Error("Workflow resource returned an invalid error.");
|
|
449
|
+
return { ok: false, error: resolved.error };
|
|
450
|
+
}
|
|
451
|
+
const { script, hostCommands } = resolved;
|
|
452
|
+
if (Object.keys(resolved).some((key) => key !== "script" && key !== "hostCommands") || typeof script !== "string" || !script.trim()) throw new Error("Workflow resource returned an invalid expansion.");
|
|
453
|
+
return { ok: true, resource: issueWorkflowResource(resource.name, resource.version, script, hostCommands) };
|
|
454
|
+
}
|
|
455
|
+
function issueWorkflowResource(name, version, script, hostCommands) {
|
|
456
|
+
const resourceId = randomUUID();
|
|
457
|
+
const permit = createWorkflowResourcePermit({
|
|
458
|
+
resourceName: name,
|
|
459
|
+
resourceVersion: version,
|
|
460
|
+
resourceId,
|
|
461
|
+
scriptDigest: stableJsonDigest(script),
|
|
462
|
+
authority: { host: hostCommands }
|
|
463
|
+
});
|
|
464
|
+
const provenance = Object.freeze({
|
|
465
|
+
kind: "workflow",
|
|
466
|
+
name,
|
|
467
|
+
version,
|
|
468
|
+
invocation: "named",
|
|
469
|
+
expansion: "resolved",
|
|
470
|
+
id: resourceId
|
|
471
|
+
});
|
|
472
|
+
return { script, permit, provenance };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export {
|
|
476
|
+
validateWorkflowChildPermitRoot,
|
|
477
|
+
claimWorkflowChildPermit,
|
|
478
|
+
consumeWorkflowChildPermit,
|
|
479
|
+
consumeWorkflowResourcePermit,
|
|
480
|
+
authorizeWorkflowResourceHost,
|
|
481
|
+
MAX_ARGS_BYTES,
|
|
482
|
+
MAX_ARGS_FIELDS,
|
|
483
|
+
MAX_ARGS_ITEMS,
|
|
484
|
+
MAX_ARGS_DEPTH,
|
|
485
|
+
registerWorkflowResource,
|
|
486
|
+
normalizeWorkflowArgs,
|
|
487
|
+
deepFreezeWorkflowArgs,
|
|
488
|
+
resolveStructuredWorkflowResource,
|
|
489
|
+
resolveWorkflowResource
|
|
490
|
+
};
|