@automatalabs/workflows 0.57.1 → 0.58.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/README.md +5 -6
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp-server.js +1327 -118
- package/dist/validate.d.ts +2 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +42 -15
- package/package.json +5 -5
package/dist/mcp-server.js
CHANGED
|
@@ -30646,9 +30646,9 @@ import {
|
|
|
30646
30646
|
buildModelFilter,
|
|
30647
30647
|
parseWorkflowScript,
|
|
30648
30648
|
probeHarnessConfig as probeHarnessConfig2,
|
|
30649
|
-
redactText as
|
|
30649
|
+
redactText as redactText3,
|
|
30650
30650
|
validateWorkflowScript,
|
|
30651
|
-
truncateUtf8 as
|
|
30651
|
+
truncateUtf8 as truncateUtf84,
|
|
30652
30652
|
workflowMayUseDefaultModel,
|
|
30653
30653
|
WorkflowError,
|
|
30654
30654
|
WorkflowErrorCode,
|
|
@@ -30661,6 +30661,15 @@ import {
|
|
|
30661
30661
|
|
|
30662
30662
|
// ../mcp-server/src/workflow-tool-input.ts
|
|
30663
30663
|
import { isAbsolute } from "node:path";
|
|
30664
|
+
var permissionResponseSchema = external_exports.object({
|
|
30665
|
+
outcome: external_exports.discriminatedUnion("outcome", [
|
|
30666
|
+
external_exports.object({ outcome: external_exports.literal("cancelled") }).strict(),
|
|
30667
|
+
external_exports.object({ outcome: external_exports.literal("selected"), optionId: external_exports.string().min(1).max(512) }).strict()
|
|
30668
|
+
])
|
|
30669
|
+
}).strict();
|
|
30670
|
+
var WORKFLOW_RESULT_CHUNK_BYTES_DEFAULT = 16384;
|
|
30671
|
+
var WORKFLOW_RESULT_CHUNK_BYTES_MAX = 16384;
|
|
30672
|
+
var WORKFLOW_RESULT_CHUNK_BYTES_MIN = 4;
|
|
30664
30673
|
var checkpointRepliesSchema = external_exports.record(
|
|
30665
30674
|
external_exports.string().refine(
|
|
30666
30675
|
(key) => {
|
|
@@ -30672,17 +30681,17 @@ var checkpointRepliesSchema = external_exports.record(
|
|
|
30672
30681
|
external_exports.unknown()
|
|
30673
30682
|
);
|
|
30674
30683
|
var workflowToolInputShape = {
|
|
30675
|
-
action: external_exports.enum(["run", "config", "inspect", "await", "stop"]).optional().describe(
|
|
30676
|
-
"Operation. Omit or use run to validate then execute; config discovers live backend/model/mode/config options
|
|
30684
|
+
action: external_exports.enum(["run", "config", "inspect", "await", "result", "stop", "permissions-response"]).optional().describe(
|
|
30685
|
+
"Operation. Omit or use run to validate then execute; config discovers live backend/model/mode/config options; inspect/await expose lifecycle and action requirements; result pages the exact completed JSON result; permissions-response resolves one pending ACP permission; stop aborts a run or one in-flight agent."
|
|
30677
30686
|
),
|
|
30678
30687
|
script: external_exports.string().min(1).optional().describe(
|
|
30679
|
-
"Raw JavaScript workflow script (no Markdown fences). Exactly one of script or scriptPath is required for run; both are forbidden for config/inspect/await/stop. First statement MUST be `export const meta = { name, description, phases? }`. When present, phases MUST be an array of objects shaped `{ title: string, detail?: string, model?: string }`, never an array of strings."
|
|
30688
|
+
"Raw JavaScript workflow script (no Markdown fences). Exactly one of script or scriptPath is required for run; both are forbidden for config/inspect/await/result/stop/permissions-response. First statement MUST be `export const meta = { name, description, phases? }`. When present, phases MUST be an array of objects shaped `{ title: string, detail?: string, model?: string }`, never an array of strings."
|
|
30680
30689
|
),
|
|
30681
30690
|
scriptPath: external_exports.string().min(1).refine((value) => isAbsolute(value), "scriptPath must be an absolute path").optional().describe(
|
|
30682
|
-
"Absolute path, on the server's filesystem, to a workflow script file read once at admission. Exactly one of script or scriptPath is required for run; both are forbidden for config/inspect/await/stop. Relative paths are rejected."
|
|
30691
|
+
"Absolute path, on the server's filesystem, to a workflow script file read once at admission. Exactly one of script or scriptPath is required for run; both are forbidden for config/inspect/await/result/stop/permissions-response. Relative paths are rejected."
|
|
30683
30692
|
),
|
|
30684
30693
|
projectDir: external_exports.string().min(1).refine((value) => isAbsolute(value), "projectDir must be an absolute path").optional().describe(
|
|
30685
|
-
"Absolute project directory used as the cwd for config discovery and, for run, the project-scoped store (where the runId, journal, and resume state live) plus default execution cwd. Required for run and config on the shared workflow daemon; on a single-project (in-process) server it defaults to that server's own project. Forbidden for inspect/await/stop \u2014 a runId locates its project."
|
|
30694
|
+
"Absolute project directory used as the cwd for config discovery and, for run, the project-scoped store (where the runId, journal, and resume state live) plus default execution cwd. Required for run and config on the shared workflow daemon; on a single-project (in-process) server it defaults to that server's own project. Forbidden for inspect/await/result/stop/permissions-response \u2014 a runId locates its project."
|
|
30686
30695
|
),
|
|
30687
30696
|
harnesses: external_exports.array(external_exports.string().regex(/^[a-z][a-z0-9._-]*$/i, "invalid backend name")).min(1).max(16).optional().describe('With action="config", backend names to probe. Omit to probe every backend registered on this server.'),
|
|
30688
30697
|
modelSpecs: external_exports.array(external_exports.string().min(1).max(256)).min(1).max(16).optional().describe(
|
|
@@ -30704,7 +30713,9 @@ var workflowToolInputShape = {
|
|
|
30704
30713
|
resumePolicy: external_exports.enum(["auto", "positional"]).optional().describe('Resume matching policy. Default "auto"; requires resumeFromRunId.'),
|
|
30705
30714
|
checkpointReplies: checkpointRepliesSchema.optional().describe("With resumeFromRunId, durable-checkpoint decisions keyed by checkpointContext.callIndex."),
|
|
30706
30715
|
background: external_exports.boolean().optional().describe("Default false. True acknowledges after admission and executes in this server process."),
|
|
30707
|
-
runId: external_exports.string().max(128).regex(/^[a-z0-9]+-[a-z0-9]+$/, "runId must be an engine-generated run ID").optional().describe("Project-scoped workflow run ID. Required for inspect/await/stop; forbidden for config/run."),
|
|
30716
|
+
runId: external_exports.string().max(128).regex(/^[a-z0-9]+-[a-z0-9]+$/, "runId must be an engine-generated run ID").optional().describe("Project-scoped workflow run ID. Required for inspect/await/result/stop/permissions-response; forbidden for config/run."),
|
|
30717
|
+
permissionId: external_exports.string().uuid().optional().describe('With action="permissions-response", the opaque pending permission id returned by inspect/await.'),
|
|
30718
|
+
response: permissionResponseSchema.optional().describe('With action="permissions-response", an exact ACP selected optionId or cancelled outcome.'),
|
|
30708
30719
|
callIndex: external_exports.number().int().nonnegative().safe().optional().describe(
|
|
30709
30720
|
"With action=stop, cancel exactly this in-flight agent call without aborting the run. Forbidden for every other action."
|
|
30710
30721
|
),
|
|
@@ -30716,11 +30727,19 @@ var workflowToolInputShape = {
|
|
|
30716
30727
|
message: "labelGlob must contain from 1 through 128 Unicode code points"
|
|
30717
30728
|
}).optional().describe("Case-sensitive whole-label glob using *, ?, and backslash escaping."),
|
|
30718
30729
|
logLines: external_exports.number().int().min(0).max(50).optional().describe("Latest run-log lines. Default 20; range 0..50."),
|
|
30719
|
-
waitMs: external_exports.number().int().min(0).max(25e3).optional().describe("Await duration in milliseconds. Default 20000; range 0..25000. Zero reads without blocking.")
|
|
30730
|
+
waitMs: external_exports.number().int().min(0).max(25e3).optional().describe("Await duration in milliseconds. Default 20000; range 0..25000. Zero reads without blocking."),
|
|
30731
|
+
offset: external_exports.number().int().nonnegative().safe().optional().describe('With action="result", UTF-8 byte offset into the exact serialized JSON. Default 0; use the previous endOffset.'),
|
|
30732
|
+
maxBytes: external_exports.number().int().min(WORKFLOW_RESULT_CHUNK_BYTES_MIN).max(WORKFLOW_RESULT_CHUNK_BYTES_MAX).optional().describe(`With action="result", maximum UTF-8 bytes returned. Default and maximum ${WORKFLOW_RESULT_CHUNK_BYTES_DEFAULT}.`)
|
|
30720
30733
|
};
|
|
30721
30734
|
function hasConfigFields(raw) {
|
|
30722
30735
|
return raw.harnesses !== void 0 || raw.modelSpecs !== void 0 || raw.modelFilter !== void 0 || raw.probeTimeoutMs !== void 0;
|
|
30723
30736
|
}
|
|
30737
|
+
function hasPermissionFields(raw) {
|
|
30738
|
+
return raw.permissionId !== void 0 || raw.response !== void 0;
|
|
30739
|
+
}
|
|
30740
|
+
function hasResultFields(raw) {
|
|
30741
|
+
return raw.offset !== void 0 || raw.maxBytes !== void 0;
|
|
30742
|
+
}
|
|
30724
30743
|
function hasExecutionFields(raw) {
|
|
30725
30744
|
return raw.script !== void 0 || raw.scriptPath !== void 0 || raw.projectDir !== void 0 || raw.args !== void 0 || raw.maxAgents !== void 0 || raw.concurrency !== void 0 || raw.agentRetries !== void 0 || raw.agentTimeoutMs !== void 0 || raw.agentIdleTimeoutMs !== void 0 || raw.resumeFromRunId !== void 0 || raw.resumePolicy !== void 0 || raw.checkpointReplies !== void 0 || raw.background !== void 0;
|
|
30726
30745
|
}
|
|
@@ -30729,7 +30748,7 @@ function invalid(message) {
|
|
|
30729
30748
|
}
|
|
30730
30749
|
function parseWorkflowToolInput(raw, options = {}) {
|
|
30731
30750
|
if (raw.action === "config") {
|
|
30732
|
-
if (raw.script !== void 0 || raw.scriptPath !== void 0 || raw.args !== void 0 || raw.maxAgents !== void 0 || raw.concurrency !== void 0 || raw.agentRetries !== void 0 || raw.agentTimeoutMs !== void 0 || raw.agentIdleTimeoutMs !== void 0 || raw.resumeFromRunId !== void 0 || raw.resumePolicy !== void 0 || raw.checkpointReplies !== void 0 || raw.background !== void 0 || raw.runId !== void 0 || raw.callIndex !== void 0 || raw.forceOwner !== void 0 || raw.waitMs !== void 0 || raw.lastN !== void 0 || raw.labelGlob !== void 0 || raw.logLines !== void 0) {
|
|
30751
|
+
if (raw.script !== void 0 || raw.scriptPath !== void 0 || raw.args !== void 0 || raw.maxAgents !== void 0 || raw.concurrency !== void 0 || raw.agentRetries !== void 0 || raw.agentTimeoutMs !== void 0 || raw.agentIdleTimeoutMs !== void 0 || raw.resumeFromRunId !== void 0 || raw.resumePolicy !== void 0 || raw.checkpointReplies !== void 0 || raw.background !== void 0 || raw.runId !== void 0 || hasPermissionFields(raw) || raw.callIndex !== void 0 || raw.forceOwner !== void 0 || raw.waitMs !== void 0 || raw.lastN !== void 0 || raw.labelGlob !== void 0 || raw.logLines !== void 0 || hasResultFields(raw)) {
|
|
30733
30752
|
invalid('action="config" accepts only projectDir, harnesses, modelSpecs, modelFilter, and probeTimeoutMs');
|
|
30734
30753
|
}
|
|
30735
30754
|
if (options.requireProjectDir === true && raw.projectDir === void 0) {
|
|
@@ -30746,9 +30765,36 @@ function parseWorkflowToolInput(raw, options = {}) {
|
|
|
30746
30765
|
probeTimeoutMs: raw.probeTimeoutMs
|
|
30747
30766
|
};
|
|
30748
30767
|
}
|
|
30768
|
+
if (raw.action === "result") {
|
|
30769
|
+
if (!raw.runId) invalid('action="result" requires runId');
|
|
30770
|
+
if (hasExecutionFields(raw) || hasConfigFields(raw) || hasPermissionFields(raw) || raw.waitMs !== void 0 || raw.callIndex !== void 0 || raw.forceOwner !== void 0 || raw.lastN !== void 0 || raw.labelGlob !== void 0 || raw.logLines !== void 0) {
|
|
30771
|
+
invalid('action="result" accepts only runId, offset, and maxBytes');
|
|
30772
|
+
}
|
|
30773
|
+
return {
|
|
30774
|
+
action: "result",
|
|
30775
|
+
runId: raw.runId,
|
|
30776
|
+
offset: raw.offset ?? 0,
|
|
30777
|
+
maxBytes: raw.maxBytes ?? WORKFLOW_RESULT_CHUNK_BYTES_DEFAULT
|
|
30778
|
+
};
|
|
30779
|
+
}
|
|
30780
|
+
if (raw.action === "permissions-response") {
|
|
30781
|
+
if (!raw.runId) invalid('action="permissions-response" requires runId');
|
|
30782
|
+
if (!raw.permissionId || raw.response === void 0) {
|
|
30783
|
+
invalid('action="permissions-response" requires permissionId and response');
|
|
30784
|
+
}
|
|
30785
|
+
if (hasExecutionFields(raw) || hasConfigFields(raw) || raw.waitMs !== void 0 || raw.callIndex !== void 0 || raw.forceOwner !== void 0 || raw.lastN !== void 0 || raw.labelGlob !== void 0 || raw.logLines !== void 0 || hasResultFields(raw)) {
|
|
30786
|
+
invalid('action="permissions-response" accepts only runId, permissionId, and response');
|
|
30787
|
+
}
|
|
30788
|
+
return {
|
|
30789
|
+
action: "permissions-response",
|
|
30790
|
+
runId: raw.runId,
|
|
30791
|
+
permissionId: raw.permissionId,
|
|
30792
|
+
response: raw.response
|
|
30793
|
+
};
|
|
30794
|
+
}
|
|
30749
30795
|
if (raw.action === "inspect") {
|
|
30750
30796
|
if (!raw.runId) invalid('action="inspect" requires runId');
|
|
30751
|
-
if (hasExecutionFields(raw) || hasConfigFields(raw) || raw.waitMs !== void 0 || raw.callIndex !== void 0 || raw.forceOwner !== void 0) {
|
|
30797
|
+
if (hasExecutionFields(raw) || hasConfigFields(raw) || hasPermissionFields(raw) || hasResultFields(raw) || raw.waitMs !== void 0 || raw.callIndex !== void 0 || raw.forceOwner !== void 0) {
|
|
30752
30798
|
invalid('action="inspect" cannot include execution fields');
|
|
30753
30799
|
}
|
|
30754
30800
|
return {
|
|
@@ -30761,7 +30807,7 @@ function parseWorkflowToolInput(raw, options = {}) {
|
|
|
30761
30807
|
}
|
|
30762
30808
|
if (raw.action === "await") {
|
|
30763
30809
|
if (!raw.runId) invalid('action="await" requires runId');
|
|
30764
|
-
if (hasExecutionFields(raw) || hasConfigFields(raw) || raw.callIndex !== void 0 || raw.forceOwner !== void 0) {
|
|
30810
|
+
if (hasExecutionFields(raw) || hasConfigFields(raw) || hasPermissionFields(raw) || hasResultFields(raw) || raw.callIndex !== void 0 || raw.forceOwner !== void 0) {
|
|
30765
30811
|
invalid('action="await" cannot include execution fields');
|
|
30766
30812
|
}
|
|
30767
30813
|
return {
|
|
@@ -30775,7 +30821,7 @@ function parseWorkflowToolInput(raw, options = {}) {
|
|
|
30775
30821
|
}
|
|
30776
30822
|
if (raw.action === "stop") {
|
|
30777
30823
|
if (!raw.runId) invalid('action="stop" requires runId');
|
|
30778
|
-
if (hasExecutionFields(raw) || hasConfigFields(raw) || raw.waitMs !== void 0) {
|
|
30824
|
+
if (hasExecutionFields(raw) || hasConfigFields(raw) || hasPermissionFields(raw) || hasResultFields(raw) || raw.waitMs !== void 0) {
|
|
30779
30825
|
invalid('action="stop" cannot include execution fields or waitMs');
|
|
30780
30826
|
}
|
|
30781
30827
|
if (raw.callIndex !== void 0 && raw.forceOwner !== void 0) {
|
|
@@ -30791,7 +30837,7 @@ function parseWorkflowToolInput(raw, options = {}) {
|
|
|
30791
30837
|
logLines: raw.logLines
|
|
30792
30838
|
};
|
|
30793
30839
|
}
|
|
30794
|
-
if (raw.runId !== void 0 || raw.callIndex !== void 0 || raw.forceOwner !== void 0 || raw.waitMs !== void 0 || raw.lastN !== void 0 || raw.labelGlob !== void 0 || raw.logLines !== void 0 || hasConfigFields(raw)) {
|
|
30840
|
+
if (raw.runId !== void 0 || hasPermissionFields(raw) || raw.callIndex !== void 0 || raw.forceOwner !== void 0 || raw.waitMs !== void 0 || raw.lastN !== void 0 || raw.labelGlob !== void 0 || raw.logLines !== void 0 || hasConfigFields(raw) || hasResultFields(raw)) {
|
|
30795
30841
|
invalid("run inputs cannot include inspection fields");
|
|
30796
30842
|
}
|
|
30797
30843
|
const hasScript = raw.script !== void 0;
|
|
@@ -31667,6 +31713,44 @@ var authContextSchema = external_exports.object({
|
|
|
31667
31713
|
external_exports.object({ id: external_exports.string(), type: external_exports.enum(["agent", "terminal"]), name: external_exports.string().optional() })
|
|
31668
31714
|
)
|
|
31669
31715
|
});
|
|
31716
|
+
var permissionOutcomeSchema = external_exports.discriminatedUnion("outcome", [
|
|
31717
|
+
external_exports.object({ outcome: external_exports.literal("cancelled") }).strict(),
|
|
31718
|
+
external_exports.object({ outcome: external_exports.literal("selected"), optionId: external_exports.string() }).strict()
|
|
31719
|
+
]);
|
|
31720
|
+
var pendingPermissionSchema = external_exports.object({
|
|
31721
|
+
version: external_exports.literal(1),
|
|
31722
|
+
permissionId: external_exports.string().uuid(),
|
|
31723
|
+
runId: external_exports.string(),
|
|
31724
|
+
callIndex: external_exports.number().int().nonnegative(),
|
|
31725
|
+
backendId: external_exports.string(),
|
|
31726
|
+
label: external_exports.string().optional(),
|
|
31727
|
+
requestedAt: external_exports.string(),
|
|
31728
|
+
request: external_exports.object({
|
|
31729
|
+
toolCall: external_exports.record(external_exports.string(), external_exports.unknown()),
|
|
31730
|
+
options: external_exports.array(external_exports.object({
|
|
31731
|
+
optionId: external_exports.string(),
|
|
31732
|
+
name: external_exports.string(),
|
|
31733
|
+
kind: external_exports.string(),
|
|
31734
|
+
_meta: external_exports.record(external_exports.string(), external_exports.unknown()).nullable().optional()
|
|
31735
|
+
})),
|
|
31736
|
+
_meta: external_exports.record(external_exports.string(), external_exports.unknown()).nullable().optional()
|
|
31737
|
+
}),
|
|
31738
|
+
requestTruncated: external_exports.boolean(),
|
|
31739
|
+
requestRedacted: external_exports.boolean()
|
|
31740
|
+
});
|
|
31741
|
+
var permissionInteractionSchema = external_exports.object({
|
|
31742
|
+
permissionRequests: external_exports.literal("may-block"),
|
|
31743
|
+
collectWith: external_exports.array(external_exports.enum(["await", "inspect"])),
|
|
31744
|
+
respondWith: external_exports.literal("permissions-response"),
|
|
31745
|
+
elicitation: external_exports.enum(["available", "unavailable"])
|
|
31746
|
+
});
|
|
31747
|
+
var permissionAcknowledgementSchema = external_exports.object({
|
|
31748
|
+
permissionId: external_exports.string().uuid(),
|
|
31749
|
+
runId: external_exports.string(),
|
|
31750
|
+
callIndex: external_exports.number().int().nonnegative(),
|
|
31751
|
+
outcome: permissionOutcomeSchema,
|
|
31752
|
+
respondedAt: external_exports.string()
|
|
31753
|
+
});
|
|
31670
31754
|
var checkpointContextSchema = external_exports.object({
|
|
31671
31755
|
callIndex: external_exports.number().int().nonnegative(),
|
|
31672
31756
|
hash: external_exports.string(),
|
|
@@ -31852,6 +31936,7 @@ var scriptLineageEntrySchema = external_exports.object({
|
|
|
31852
31936
|
});
|
|
31853
31937
|
var inspectionScriptResourceShape = {
|
|
31854
31938
|
scriptUri: external_exports.string(),
|
|
31939
|
+
resultUri: external_exports.string().optional(),
|
|
31855
31940
|
lineage: external_exports.array(scriptLineageEntrySchema)
|
|
31856
31941
|
};
|
|
31857
31942
|
var runStatusShape = {
|
|
@@ -31919,12 +32004,28 @@ var executionResultSchema = external_exports.object({
|
|
|
31919
32004
|
runId: external_exports.string(),
|
|
31920
32005
|
status: external_exports.enum(["paused", "completed", "failed", "aborted"]),
|
|
31921
32006
|
...executionDetailsShape,
|
|
31922
|
-
scriptUri: external_exports.string()
|
|
31923
|
-
|
|
32007
|
+
scriptUri: external_exports.string(),
|
|
32008
|
+
resultUri: external_exports.string().optional()
|
|
32009
|
+
}).strict().superRefine((value, context) => {
|
|
32010
|
+
if (value.resultUri !== void 0 && value.status !== "completed") {
|
|
32011
|
+
context.addIssue({
|
|
32012
|
+
code: "custom",
|
|
32013
|
+
path: ["resultUri"],
|
|
32014
|
+
message: "resultUri is available only for completed workflow outcomes"
|
|
32015
|
+
});
|
|
32016
|
+
}
|
|
32017
|
+
}).meta({
|
|
32018
|
+
allOf: [
|
|
32019
|
+
{
|
|
32020
|
+
if: { required: ["resultUri"] },
|
|
32021
|
+
then: { required: ["status"], properties: { status: { const: "completed" } } }
|
|
32022
|
+
}
|
|
32023
|
+
]
|
|
32024
|
+
});
|
|
31924
32025
|
var waitSchema = external_exports.object({
|
|
31925
32026
|
requestedMs: external_exports.number().int().nonnegative(),
|
|
31926
32027
|
elapsedMs: external_exports.number().int().nonnegative(),
|
|
31927
|
-
returnedBecause: external_exports.enum(["terminal", "timeout", "immediate"])
|
|
32028
|
+
returnedBecause: external_exports.enum(["terminal", "timeout", "immediate", "action-required", "permission-resolved"])
|
|
31928
32029
|
});
|
|
31929
32030
|
var diagnosticRecordSchema = external_exports.record(external_exports.string(), external_exports.unknown());
|
|
31930
32031
|
var sessionModeStateSchema = external_exports.object({
|
|
@@ -31939,6 +32040,9 @@ var sessionModeStateSchema = external_exports.object({
|
|
|
31939
32040
|
});
|
|
31940
32041
|
var harnessDiagnosticSchema = external_exports.object({
|
|
31941
32042
|
backendId: external_exports.string(),
|
|
32043
|
+
defaultModeId: external_exports.string().optional().describe(
|
|
32044
|
+
"AgentPrism's explicit mode when a call omits mode; absent for no-mode/custom backends."
|
|
32045
|
+
),
|
|
31942
32046
|
model: external_exports.string().optional(),
|
|
31943
32047
|
probed: external_exports.boolean(),
|
|
31944
32048
|
error: external_exports.string().optional(),
|
|
@@ -32000,7 +32104,7 @@ var inspectionRequired = [
|
|
|
32000
32104
|
];
|
|
32001
32105
|
var terminalStatuses = ["paused", "completed", "failed", "aborted"];
|
|
32002
32106
|
var nonterminalStatuses = ["pending", "running"];
|
|
32003
|
-
var commonOutputFields = ["runId", "status", "scriptUri", "limits", "replayEligibility"];
|
|
32107
|
+
var commonOutputFields = ["runId", "status", "scriptUri", "resultUri", "limits", "replayEligibility"];
|
|
32004
32108
|
var runOutputRequired = ["runId", "status", "scriptUri"];
|
|
32005
32109
|
var executionDetailFields = [
|
|
32006
32110
|
"result",
|
|
@@ -32027,6 +32131,15 @@ var discoveryOutputFields = [
|
|
|
32027
32131
|
"omittedHarnesses",
|
|
32028
32132
|
"models"
|
|
32029
32133
|
];
|
|
32134
|
+
var resultRetrievalFields = [
|
|
32135
|
+
"mimeType",
|
|
32136
|
+
"encoding",
|
|
32137
|
+
"totalBytes",
|
|
32138
|
+
"offset",
|
|
32139
|
+
"endOffset",
|
|
32140
|
+
"hasMore",
|
|
32141
|
+
"chunk"
|
|
32142
|
+
];
|
|
32030
32143
|
var stopControlSchema = external_exports.object({
|
|
32031
32144
|
state: external_exports.literal("pending"),
|
|
32032
32145
|
operationId: external_exports.string(),
|
|
@@ -32049,6 +32162,10 @@ var variantOutputFields = [
|
|
|
32049
32162
|
"stopped",
|
|
32050
32163
|
"alreadyTerminal",
|
|
32051
32164
|
"control",
|
|
32165
|
+
"pendingPermissions",
|
|
32166
|
+
"interaction",
|
|
32167
|
+
"permissionResponse",
|
|
32168
|
+
...resultRetrievalFields,
|
|
32052
32169
|
...discoveryOutputFields
|
|
32053
32170
|
];
|
|
32054
32171
|
var forbidsRequired = (...fields) => ({
|
|
@@ -32067,7 +32184,7 @@ function hasOnlyFields(value, allowed) {
|
|
|
32067
32184
|
return Object.entries(value).every(([field, fieldValue]) => fieldValue === void 0 || allowedFields.has(field));
|
|
32068
32185
|
}
|
|
32069
32186
|
var workflowToolOutputShape = external_exports.object({
|
|
32070
|
-
action: external_exports.enum(["run", "config"]).optional(),
|
|
32187
|
+
action: external_exports.enum(["run", "config", "result"]).optional(),
|
|
32071
32188
|
ok: external_exports.boolean().optional(),
|
|
32072
32189
|
validation: validationSummarySchema.optional(),
|
|
32073
32190
|
harnessOptions: external_exports.array(harnessDiagnosticSchema).optional(),
|
|
@@ -32078,6 +32195,7 @@ var workflowToolOutputShape = external_exports.object({
|
|
|
32078
32195
|
...executionDetailsShape,
|
|
32079
32196
|
scriptSource: scriptSourceSchema.optional(),
|
|
32080
32197
|
scriptUri: external_exports.string().optional(),
|
|
32198
|
+
resultUri: external_exports.string().optional(),
|
|
32081
32199
|
lineage: inspectionScriptResourceShape.lineage.optional(),
|
|
32082
32200
|
workflowName: runStatusShape.workflowName.optional(),
|
|
32083
32201
|
phases: runStatusShape.phases.optional(),
|
|
@@ -32091,33 +32209,81 @@ var workflowToolOutputShape = external_exports.object({
|
|
|
32091
32209
|
outcome: executionResultSchema.optional(),
|
|
32092
32210
|
stopped: external_exports.boolean().optional(),
|
|
32093
32211
|
alreadyTerminal: external_exports.boolean().optional(),
|
|
32094
|
-
control: stopControlSchema.optional()
|
|
32212
|
+
control: stopControlSchema.optional(),
|
|
32213
|
+
pendingPermissions: external_exports.array(pendingPermissionSchema).optional(),
|
|
32214
|
+
interaction: permissionInteractionSchema.optional(),
|
|
32215
|
+
permissionResponse: permissionAcknowledgementSchema.optional(),
|
|
32216
|
+
mimeType: external_exports.literal("application/json").optional(),
|
|
32217
|
+
encoding: external_exports.literal("utf-8").optional(),
|
|
32218
|
+
totalBytes: external_exports.number().int().nonnegative().optional(),
|
|
32219
|
+
offset: external_exports.number().int().nonnegative().optional(),
|
|
32220
|
+
endOffset: external_exports.number().int().nonnegative().optional(),
|
|
32221
|
+
hasMore: external_exports.boolean().optional(),
|
|
32222
|
+
chunk: external_exports.string().optional()
|
|
32095
32223
|
}).superRefine((value, context) => {
|
|
32096
32224
|
const has = (field) => value[field] !== void 0;
|
|
32097
32225
|
const inspectionComplete = inspectionRequired.every((field) => has(field));
|
|
32098
32226
|
const runCommonComplete = has("runId") && has("status") && has("scriptUri");
|
|
32099
32227
|
const terminal2 = terminalStatuses.includes(value.status);
|
|
32100
32228
|
let valid;
|
|
32101
|
-
if (value.action === "
|
|
32229
|
+
if (value.action === "result") {
|
|
32230
|
+
valid = value.status === "completed" && has("runId") && has("resultUri") && resultRetrievalFields.every((field) => has(field)) && hasOnlyExactFields(value, ["action", "runId", "status", "resultUri", ...resultRetrievalFields]);
|
|
32231
|
+
} else if (value.action === "config") {
|
|
32102
32232
|
valid = has("ok") && has("harnessOptions") && has("omittedHarnesses") && has("models") && hasOnlyExactFields(value, ["action", "ok", "harnessOptions", "omittedHarnesses", "models"]);
|
|
32103
32233
|
} else if (value.action === "run") {
|
|
32104
32234
|
valid = value.status === "rejected" && has("validation") && hasOnlyExactFields(value, ["action", "status", "validation"]);
|
|
32105
32235
|
} else if (has("scriptSource")) {
|
|
32106
|
-
valid = runCommonComplete && has("limits") && (value.status === "running" ? hasOnlyFields(value, ["scriptSource"]) : terminal2 && hasOnlyFields(value, ["scriptSource", ...executionDetailFields]));
|
|
32236
|
+
valid = runCommonComplete && has("limits") && (value.status === "running" ? hasOnlyFields(value, ["scriptSource", "pendingPermissions", "interaction"]) : terminal2 && hasOnlyFields(value, ["scriptSource", ...executionDetailFields]));
|
|
32237
|
+
} else if (has("permissionResponse")) {
|
|
32238
|
+
valid = runCommonComplete && inspectionComplete && hasOnlyFields(value, [...inspectionFields, "pendingPermissions", "interaction", "permissionResponse"]);
|
|
32107
32239
|
} else if (has("control")) {
|
|
32108
32240
|
valid = runCommonComplete && inspectionComplete && value.stopped === false && value.alreadyTerminal === false && (value.status === "pending" || value.status === "running") && hasOnlyFields(value, [...inspectionFields, "stopped", "alreadyTerminal", "control"]);
|
|
32109
32241
|
} else if (has("stopped") || has("alreadyTerminal")) {
|
|
32110
32242
|
valid = runCommonComplete && inspectionComplete && has("stopped") && has("alreadyTerminal") && (value.status === "completed" || value.status === "failed" || value.status === "aborted") && hasOnlyFields(value, [...inspectionFields, "stopped", "alreadyTerminal"]);
|
|
32111
32243
|
} else if (has("wait")) {
|
|
32112
|
-
valid = runCommonComplete && inspectionComplete && hasOnlyFields(value, [...inspectionFields, "wait", "tokenUsage", "outcome"]) && (terminal2 ? has("outcome") : !has("outcome"));
|
|
32244
|
+
valid = runCommonComplete && inspectionComplete && hasOnlyFields(value, [...inspectionFields, "wait", "tokenUsage", "outcome", "pendingPermissions", "interaction"]) && (terminal2 ? has("outcome") : !has("outcome"));
|
|
32113
32245
|
} else {
|
|
32114
|
-
valid = runCommonComplete && inspectionComplete && hasOnlyFields(value, inspectionFields);
|
|
32246
|
+
valid = runCommonComplete && inspectionComplete && hasOnlyFields(value, [...inspectionFields, "pendingPermissions", "interaction"]);
|
|
32115
32247
|
}
|
|
32248
|
+
if (has("resultUri") && value.status !== "completed") valid = false;
|
|
32116
32249
|
if (!valid) {
|
|
32117
32250
|
context.addIssue({ code: "custom", message: "output does not match a workflow result variant" });
|
|
32118
32251
|
}
|
|
32119
32252
|
}).meta({
|
|
32253
|
+
allOf: [
|
|
32254
|
+
{
|
|
32255
|
+
if: { required: ["resultUri"] },
|
|
32256
|
+
then: { required: ["status"], properties: { status: { const: "completed" } } }
|
|
32257
|
+
}
|
|
32258
|
+
],
|
|
32120
32259
|
oneOf: [
|
|
32260
|
+
{
|
|
32261
|
+
title: "Workflow result retrieval",
|
|
32262
|
+
required: ["action", "runId", "status", "resultUri", ...resultRetrievalFields],
|
|
32263
|
+
properties: { action: { const: "result" }, status: { const: "completed" } },
|
|
32264
|
+
...forbidsRequired(
|
|
32265
|
+
"ok",
|
|
32266
|
+
"validation",
|
|
32267
|
+
"harnessOptions",
|
|
32268
|
+
"omittedHarnesses",
|
|
32269
|
+
"models",
|
|
32270
|
+
"scriptUri",
|
|
32271
|
+
"scriptSource",
|
|
32272
|
+
"limits",
|
|
32273
|
+
"replayEligibility",
|
|
32274
|
+
...executionDetailFields,
|
|
32275
|
+
...inspectionFields,
|
|
32276
|
+
"lineage",
|
|
32277
|
+
"wait",
|
|
32278
|
+
"outcome",
|
|
32279
|
+
"stopped",
|
|
32280
|
+
"alreadyTerminal",
|
|
32281
|
+
"control",
|
|
32282
|
+
"pendingPermissions",
|
|
32283
|
+
"interaction",
|
|
32284
|
+
"permissionResponse"
|
|
32285
|
+
)
|
|
32286
|
+
},
|
|
32121
32287
|
{
|
|
32122
32288
|
title: "Workflow config discovery",
|
|
32123
32289
|
required: ["action", "ok", "harnessOptions", "omittedHarnesses", "models"],
|
|
@@ -32168,17 +32334,17 @@ var workflowToolOutputShape = external_exports.object({
|
|
|
32168
32334
|
title: "Workflow background admission",
|
|
32169
32335
|
required: [...runOutputRequired, "scriptSource", "limits"],
|
|
32170
32336
|
properties: { status: { const: "running" } },
|
|
32171
|
-
...forbidsOutside(["scriptSource"])
|
|
32337
|
+
...forbidsOutside(["scriptSource", "pendingPermissions", "interaction"])
|
|
32172
32338
|
},
|
|
32173
32339
|
{
|
|
32174
32340
|
title: "Workflow inspection",
|
|
32175
32341
|
required: [...runOutputRequired, ...inspectionRequired],
|
|
32176
|
-
...forbidsOutside(inspectionFields)
|
|
32342
|
+
...forbidsOutside([...inspectionFields, "pendingPermissions", "interaction"])
|
|
32177
32343
|
},
|
|
32178
32344
|
{
|
|
32179
32345
|
title: "Workflow await",
|
|
32180
32346
|
required: [...runOutputRequired, ...inspectionRequired, "wait"],
|
|
32181
|
-
...forbidsOutside([...inspectionFields, "wait", "tokenUsage", "outcome"]),
|
|
32347
|
+
...forbidsOutside([...inspectionFields, "wait", "tokenUsage", "outcome", "pendingPermissions", "interaction"]),
|
|
32182
32348
|
anyOf: [
|
|
32183
32349
|
{
|
|
32184
32350
|
required: ["outcome"],
|
|
@@ -32190,6 +32356,11 @@ var workflowToolOutputShape = external_exports.object({
|
|
|
32190
32356
|
}
|
|
32191
32357
|
]
|
|
32192
32358
|
},
|
|
32359
|
+
{
|
|
32360
|
+
title: "Workflow permission response acknowledgement",
|
|
32361
|
+
required: [...runOutputRequired, ...inspectionRequired, "permissionResponse"],
|
|
32362
|
+
...forbidsOutside([...inspectionFields, "pendingPermissions", "interaction", "permissionResponse"])
|
|
32363
|
+
},
|
|
32193
32364
|
{
|
|
32194
32365
|
title: "Workflow stop acknowledgement",
|
|
32195
32366
|
required: [...runOutputRequired, ...inspectionRequired, "stopped", "alreadyTerminal"],
|
|
@@ -32239,7 +32410,8 @@ function toWorkflowExecutionOutcome(run, resources) {
|
|
|
32239
32410
|
...run.fallbacks === void 0 ? {} : { fallbacks: run.fallbacks },
|
|
32240
32411
|
...run.checkpointsTaken === void 0 ? {} : { checkpointsTaken: run.checkpointsTaken },
|
|
32241
32412
|
...run.resumeReport === void 0 ? {} : { resumeReport: run.resumeReport },
|
|
32242
|
-
|
|
32413
|
+
scriptUri: resources.scriptUri,
|
|
32414
|
+
...run.status === "completed" && resources.resultUri !== void 0 ? { resultUri: resources.resultUri } : {}
|
|
32243
32415
|
};
|
|
32244
32416
|
}
|
|
32245
32417
|
function toWorkflowToolResult(run, resources) {
|
|
@@ -32413,7 +32585,7 @@ ${trimmed}` : "## Next step\n\nAuthor the workflow script the user asks for, the
|
|
|
32413
32585
|
"",
|
|
32414
32586
|
"Use the connected `docs` tool for version-matched authoring guidance. Read topic `workflow/quickstart` first, then read only the related workflow topics needed for this task; do not load every topic. Workflow scripts and REPL evals have different `agent()` signatures, so use only `workflow/*` topics here.",
|
|
32415
32587
|
"",
|
|
32416
|
-
'When the script pins a model, mode, or configOptions, call the `workflow` tool with `action:"config"` first; after choosing a model, use `modelSpecs` to read its exact option domain.
|
|
32588
|
+
'When the script pins a model, mode, or configOptions, call the `workflow` tool with `action:"config"` first; after choosing a model, use `modelSpecs` to read its exact option domain. Read the harness-owned mode names and descriptions before pinning an exact advertised id. Omission uses the reported `defaultModeId` (Claude auto, Codex agent, OpenCode build; none for Pi). The run action automatically performs static validation, a mocked dry run, and routed no-prompt config checks before admission. Correct any direct rejection diagnostic and re-run.',
|
|
32417
32589
|
"",
|
|
32418
32590
|
taskSection,
|
|
32419
32591
|
""
|
|
@@ -32470,9 +32642,9 @@ var AUTHORING_DOC_TOPICS = [
|
|
|
32470
32642
|
"workflow/run-lifecycle",
|
|
32471
32643
|
"workflow/examples"
|
|
32472
32644
|
],
|
|
32473
|
-
"bytes":
|
|
32474
|
-
"sha256": "
|
|
32475
|
-
"text": '# Workflow scripts: quickstart\n\n**Context:** JavaScript passed to the MCP `workflow` tool. This is not REPL code: workflow scripts use `agent(prompt, options?)`, allow top-level `return`, and start from a required metadata export.\n\nA workflow script is a deterministic orchestrator. Script code owns loops, fan-out, conditionals, aggregation, and checkpoints; `agent()` workers perform repository or research tasks. Workers start fresh sessions and do not share memory, so interpolate every prior result a later worker needs into its prompt.\n\n## Minimal valid script\n\n```js\nexport const meta = {\n name: "review-target",\n description: "Review a target and return concrete findings",\n phases: [{ title: "Review" }],\n};\n\nphase("Review");\nconst report = await agent(\n `Review ${args.target}. Read the relevant files and report concrete findings.`,\n { label: "review" },\n);\nreturn { report };\n```\n\nThe metadata export must be the first statement and a pure object literal. `name` and `description` are required non-empty strings. `phases`, when present, is an array of objects shaped `{ title: string, detail?: string, model?: string }`, never strings.\n\nSubmit the source without Markdown fences using the `workflow` tool\'s run form, with an absolute `projectDir` on the shared daemon. `args` is the JSON value supplied by the tool call. Some hosts may carry caller data as a JSON string, so harden scripts that accept external input:\n\n```js\nconst raw = typeof args === "string" ? (() => {\n try { return JSON.parse(args); } catch { return {}; }\n})() : args;\nconst input = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};\n```\n\n## Core rules\n\n- The DSL primitives are injected globals; do not import them.\n- Top-level `await` and top-level `return` are supported.\n- Scripts are JavaScript, not TypeScript.\n- No `require`, imports, filesystem API, network API, timers, `Date.now()`, `Math.random()`, or no-argument `Date` construction. Pass nondeterministic values through `args`.\n- Every `agent()` call should have a stable descriptive `label`.\n- A recoverable worker failure resolves to `null` after retries. Null-check load-bearing results.\n- `parallel()` takes thunks, not already-started promises:\n\n```js\nconst results = (await parallel([\n () => agent("Review correctness", { label: "review:correctness" }),\n () => agent("Review test coverage", { label: "review:coverage" }),\n])).filter(Boolean);\n```\n\n- Use a plain JSON Schema object in `schema` when script control flow depends on a worker result.\n- Return a compact JSON-serializable result; do not return a transcript.\n\n## Model selection\n\nOmit `model` for the server default, or use a backend-only value such as `"codex"` to retain that backend\'s configured default model. When `AGENTPRISM_DEFAULT_BACKEND` is truly unset, the MCP server probes backend readiness without prompting, pins one project default before validation/execution, and keeps that backend for the run and resume; an explicit environment default always wins. Before pinning a model id, `mode`, or `configOptions`, call `workflow` with `action:"config"`. After choosing a model, use `modelSpecs` to read that exact model\'s option domain.
|
|
32645
|
+
"bytes": 4254,
|
|
32646
|
+
"sha256": "49d4980013555baf3306eee64ae86e26eec2a45cfa352b130cf3862955199ae2",
|
|
32647
|
+
"text": '# Workflow scripts: quickstart\n\n**Context:** JavaScript passed to the MCP `workflow` tool. This is not REPL code: workflow scripts use `agent(prompt, options?)`, allow top-level `return`, and start from a required metadata export.\n\nA workflow script is a deterministic orchestrator. Script code owns loops, fan-out, conditionals, aggregation, and checkpoints; `agent()` workers perform repository or research tasks. Workers start fresh sessions and do not share memory, so interpolate every prior result a later worker needs into its prompt.\n\n## Minimal valid script\n\n```js\nexport const meta = {\n name: "review-target",\n description: "Review a target and return concrete findings",\n phases: [{ title: "Review" }],\n};\n\nphase("Review");\nconst report = await agent(\n `Review ${args.target}. Read the relevant files and report concrete findings.`,\n { label: "review" },\n);\nreturn { report };\n```\n\nThe metadata export must be the first statement and a pure object literal. `name` and `description` are required non-empty strings. `phases`, when present, is an array of objects shaped `{ title: string, detail?: string, model?: string }`, never strings.\n\nSubmit the source without Markdown fences using the `workflow` tool\'s run form, with an absolute `projectDir` on the shared daemon. `args` is the JSON value supplied by the tool call. Some hosts may carry caller data as a JSON string, so harden scripts that accept external input:\n\n```js\nconst raw = typeof args === "string" ? (() => {\n try { return JSON.parse(args); } catch { return {}; }\n})() : args;\nconst input = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};\n```\n\n## Core rules\n\n- The DSL primitives are injected globals; do not import them.\n- Top-level `await` and top-level `return` are supported.\n- Scripts are JavaScript, not TypeScript.\n- No `require`, imports, filesystem API, network API, timers, `Date.now()`, `Math.random()`, or no-argument `Date` construction. Pass nondeterministic values through `args`.\n- Every `agent()` call should have a stable descriptive `label`.\n- A recoverable worker failure resolves to `null` after retries. Null-check load-bearing results.\n- `parallel()` takes thunks, not already-started promises:\n\n```js\nconst results = (await parallel([\n () => agent("Review correctness", { label: "review:correctness" }),\n () => agent("Review test coverage", { label: "review:coverage" }),\n])).filter(Boolean);\n```\n\n- Use a plain JSON Schema object in `schema` when script control flow depends on a worker result.\n- Return a compact JSON-serializable result; do not return a transcript.\n\n## Model selection\n\nOmit `model` for the server default, or use a backend-only value such as `"codex"` to retain that backend\'s configured default model. When `AGENTPRISM_DEFAULT_BACKEND` is truly unset, the MCP server probes backend readiness without prompting, pins one project default before validation/execution, and keeps that backend for the run and resume; an explicit environment default always wins. Before pinning a model id, `mode`, or `configOptions`, call `workflow` with `action:"config"`. After choosing a model, use `modelSpecs` to read that exact model\'s option domain. Config preserves each advertised mode\'s id, name, description, and `_meta`, plus `defaultModeId`. When mode is omitted, AgentPrism applies Claude `auto`, Codex `agent`, OpenCode `build`, or no Pi mode. Pin only exact advertised ids and never guess model or option ids.\n\n## Validation and execution\n\nEvery run is statically parsed, mock-executed, and checked against no-prompt backend configuration before admission. A rejection creates no run ID, reserves no background slot, and spends no tokens. Read the diagnostic, correct the script, and submit it again.\n\nUse foreground execution for short work. Use `background:true` for work that may outlive one tool request; retain the returned `runId`, then use bounded `await`, `inspect`, or `stop` calls.\n\n## What to read next\n\n- `workflow/composition-and-failure` \u2014 metadata, fan-out, phases, and null semantics.\n- `workflow/api-agents` \u2014 every `agent()` option and structured output.\n- `workflow/run-lifecycle` \u2014 config, run, await, inspect, stop, and resume.\n- `workflow/examples` \u2014 complete composition patterns.\n'
|
|
32476
32648
|
},
|
|
32477
32649
|
{
|
|
32478
32650
|
"id": "workflow/run-lifecycle",
|
|
@@ -32485,9 +32657,9 @@ var AUTHORING_DOC_TOPICS = [
|
|
|
32485
32657
|
"workflow/determinism-and-resume",
|
|
32486
32658
|
"workflow/models-and-config"
|
|
32487
32659
|
],
|
|
32488
|
-
"bytes":
|
|
32489
|
-
"sha256": "
|
|
32490
|
-
"text": '## Running workflows \u2014 the MCP `workflow` tool\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\nUse the connected `workflow` tool for deterministic batch orchestration. The shared server daemon owns execution, so admitted runs survive MCP client session churn and tool-request timeouts. During a version upgrade, the successor becomes the front door while a predecessor may remain the execution owner; signed run-control forwarding keeps later-session stop/cancel operations location-independent. Owner-process exit can still interrupt in-flight work. Any later session can await, inspect, or stop a run. Runs, journals, logs, and outstanding whole-stop intents persist per project namespace.\n\nEvery `config` and `run` call on the shared daemon names its project with the required `projectDir` argument \u2014 an absolute path, normally the workspace root. `inspect`/`await`/`stop` take only a `runId`; the run ID locates its project store automatically. In a single-project server, `projectDir` defaults to that server\'s project.\n\n### The `workflow` tool, by action\n\n- **Config** (`{ action: "config", projectDir, harnesses?, modelSpecs?, modelFilter? }`): discover live model, mode, effort, and `configOptions` values from no-prompt backend sessions. Use `harnesses` plus `modelFilter` to find ids, then `modelSpecs` to select exact models and read their model-specific option domains. Each successful entry reports `modes` explicitly: use only exact ids in `modes.availableModes`; `modes:null` means omit `mode`, never guess a default. It starts no workflow and spends zero tokens. Use it only when pinning those values; an omitted model or backend-only model uses configured defaults without discovery.\n- **Run** (default, no `action`): supply exactly one of `script` (the raw source string, no Markdown fences) or `scriptPath` (an absolute path on the server\'s filesystem), plus `projectDir`. The tool automatically performs static validation, a mocked dry run, and routed config checks before admission. Invalid scripts return bounded `status:"rejected"` diagnostics with no run ID, background slot, or token spend. A path is read once at admission and its content snapshotted; later edits affect only a new run. `args` arrives in the script as the `args` global; the run\'s base directory is the `cwd` global. Some hosts hand `args` through as a JSON **string** \u2014 tolerate both shapes (`typeof args === "string" ? JSON.parse(args) : args`). Foreground streams progress but is bound to the request and its timeout. Pass `background: true` for anything that may outlive one request; it acknowledges after durable admission with a `runId`.\n- **Await** (`{ action: "await", runId, waitMs }`): bounded collection for background runs. A timeout is progress, not failure \u2014 call again (`waitMs: 20000` is typical). At terminal status the response adds `outcome`: the authored result or pause context, plus `replayEligibility`, `resumeReport`, `fallbacks`, and `checkpointsTaken`.\n- **Inspect** (`{ action: "inspect", runId, lastN, labelGlob, logLines }`): a bounded snapshot \u2014 the latest matching calls with compact result previews plus the newest log lines. Use a narrow `labelGlob` to diagnose before deciding whether to resume, edit, or stop. Inspection never executes or resumes a script.\n- **Stop**: `{ action: "stop", runId }` durably aborts the whole run and normally returns its final snapshot; stopping a terminal run is a successful no-op. Across a daemon upgrade, the successor persists an idempotent stop intent and forwards to the predecessor that owns execution. If that owner does not settle within the bounded control wait, the successful response remains nonterminal with `control:{ state:"pending", operationId, requestedAt, owner? }`; retry stop, inspect, or await to observe settlement. `{ action: "stop", runId, callIndex }` synchronously routes to the live owner and cancels exactly that in-flight agent: its slot settles to `null` with `AGENT_CANCELLED` and the run stays live; call cancellation is never reconstructed after owner loss. Whole-run `{ action:"stop", runId, forceOwner:true }` explicitly authorizes terminating a superseded owner daemon when graceful control cannot settle and may interrupt sibling runs in that process; it is forbidden with `callIndex`. `labelGlob` only filters the returned snapshot; it never selects what to cancel.\n- **Resume**: a NEW run with `resumeFromRunId` plus the script content re-sent (the same `script` or `scriptPath`) and the desired `args` (+ `checkpointReplies` when answering a durable checkpoint). Read the returned `replayEligibility` for the predicted and observed replay prefix; never assume a prefix hit. Full semantics: **Determinism and resume**.\n\n### Operating rules\n\n- **Always retain the returned `runId`.** A paused, failed, or aborted response carries a redacted final-20 `logTail`. Read it before you change anything. Every admitted script is also an immutable resource at `workflow://runs/{runId}/script`, so a later session can recover a lost inline script.\n- **Two fingerprints control replay.** The identity hash covers the prompt, the resolved model, `mode` when set, non-empty sorted `configOptions`, `tier`, `phase`, `agentType`, the resolved agent definition, and the schema. The input fingerprint covers the resolved label, per-call `cwd` and isolation, `keepSession`, images, MCP servers, session/prompt metadata, and the approved script-backend digest.\n- **Operational bounds are not replay inputs.** Host `concurrency`, `agentRetries`, `agentTimeoutMs`, and `agentIdleTimeoutMs`, plus per-call `timeoutMs`, `idleTimeoutMs`, and `retries`, enter neither fingerprint. A resume does not inherit them from its source run; pass the values you want on every run. `agentTimeoutMs` caps total wall time and is not an idle timer. The separate opt-in `agentIdleTimeoutMs` fires after that long without real backend activity; ACP `session/update` re-arms it and synthetic progress heartbeats do not. Per-call values can tighten but cannot escape finite host ceilings. Every retry gets fresh clocks.\n- **Old journals stay usable.** Input formats below 2 replay positionally with `fallbackReason: "inputs-format-legacy"`. A current-format crash snapshot uses identity matching even without terminal-environment capture. Ancestor-scoped rows carried from \u22640.23 resume chains replay only while that ancestor run is still persisted. Journals resume across filesystem, environment, engine, Node, and V8 changes; `replayEligibility` reports those differences as diagnostics, never as gates.\n- **A background start returns immediately.** It sends no progress after it returns; collect progress with later bounded awaits. Background runs have no live checkpoint channel, so authored `headless` checkpoint modes apply. When a run\'s owner process dies, a pending whole-stop intent is applied under the reclaimed lease; otherwise cold preflights reconcile stale `pending`/`running` state to `paused` with `pauseReason:"interrupted"`. A live owner lease is never stolen because of a timeout.\n- A run paused with `reason: "auth_required"` resumes as a new run after that backend\'s credentials are configured.\n\n### Execution logs \u2014 the events resource\n\nEvery journaling run publishes an MCP resource at `workflow://runs/{runId}/events`. Subscribe to the canonical URI for advisory `resources/updated` hints, then read and paginate with `after`, `limit`, and `streamId`. Progress is coarse and redacted: `agentTranscript` rows are assistant/tool upserts partitioned by `(scope, callIndex, executionStartSeq)` and reduced by greatest revision per entry index. The durable cursor is authoritative when hints coalesce or a subscriber falls behind.\n\nEmbedding hosts can drive the same contract with `runDynamicWorkflow` / `WorkflowManager` from `@automatalabs/workflows`; the script contract is identical either way.\n'
|
|
32660
|
+
"bytes": 9965,
|
|
32661
|
+
"sha256": "1143d846d50a7c2833ab58d0d88131dde8557770a6f818f2b8747c79f791ace7",
|
|
32662
|
+
"text": '## Running workflows \u2014 the MCP `workflow` tool\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\nUse the connected `workflow` tool for deterministic batch orchestration. The shared server daemon owns execution, so admitted runs survive MCP client session churn and tool-request timeouts. During a version upgrade, the successor becomes the front door while a predecessor may remain the execution owner; signed run-control forwarding keeps later-session stop/cancel operations location-independent. Owner-process exit can still interrupt in-flight work. Any later session can await, inspect, or stop a run. Runs, journals, logs, and outstanding whole-stop intents persist per project namespace.\n\nEvery `config` and `run` call on the shared daemon names its project with the required `projectDir` argument \u2014 an absolute path, normally the workspace root. `inspect`/`await`/`result`/`stop` take only a `runId`; the run ID locates its project store automatically. In a single-project server, `projectDir` defaults to that server\'s project.\n\n### The `workflow` tool, by action\n\n- **Config** (`{ action: "config", projectDir, harnesses?, modelSpecs?, modelFilter? }`): discover live model, mode, effort, and `configOptions` values from no-prompt backend sessions. Use `harnesses` plus `modelFilter` to find ids, then `modelSpecs` to select exact models and read their model-specific option domains. Each successful entry preserves every harness-advertised mode id, name, description, and `_meta`, and reports `defaultModeId`. When a call omits mode, AgentPrism applies Claude `auto`, Codex `agent`, OpenCode `build`, or no mode for Pi; authored and default ids must appear in `modes.availableModes`. It starts no workflow and spends zero tokens. Use it only when pinning those values; an omitted model or backend-only model uses configured defaults without discovery.\n- **Run** (default, no `action`): supply exactly one of `script` (the raw source string, no Markdown fences) or `scriptPath` (an absolute path on the server\'s filesystem), plus `projectDir`. The tool automatically performs static validation, a mocked dry run, and routed config checks before admission. Invalid scripts return bounded `status:"rejected"` diagnostics with no run ID, background slot, or token spend. A path is read once at admission and its content snapshotted; later edits affect only a new run. `args` arrives in the script as the `args` global; the run\'s base directory is the `cwd` global. Some hosts hand `args` through as a JSON **string** \u2014 tolerate both shapes (`typeof args === "string" ? JSON.parse(args) : args`). Foreground streams progress but is bound to the request and its timeout. Pass `background: true` for anything that may outlive one request; it acknowledges after durable admission with a `runId`.\n- **Await** (`{ action: "await", runId, waitMs }`): bounded collection for background runs. A timeout is progress, not failure. Await returns early with `wait.returnedBecause:"action-required"` when an ACP permission is pending. At terminal status the response adds `outcome`: the authored result or pause context, plus replay diagnostics.\n- **Inspect** (`{ action: "inspect", runId, lastN, labelGlob, logLines }`): a bounded snapshot \u2014 the latest matching calls, newest log lines, and any live `pendingPermissions`. A completed run also exposes `resultUri` and an exact-result resource link, without putting a large result into the bounded status projection. Elicitation-capable clients present one request\'s exact backend options during inspect/await. Permission diagnostics omit the private ACP session id, are credential-redacted and bounded, and retain the complete ordered exact option-id list or fail closed.\n- **Result** (`{ action: "result", runId, offset?, maxBytes? }`): bounded exact retrieval for a completed authored result. It returns at most 16,384 UTF-8 bytes plus `totalBytes`, `endOffset`, and `hasMore`; continue at the prior `endOffset`. The default offset is zero. UTF-8 code points are never split, and arbitrary offsets inside one fail closed. Running, paused, failed, aborted, unknown, deleted, and completed-without-value runs have no result page.\n- **Permission response** (`{ action:"permissions-response", runId, permissionId, response }`): for clients without elicitation, resolve one pending request with `{ outcome:{ outcome:"selected", optionId } }` using an exact advertised id, or `{ outcome:{ outcome:"cancelled" } }`. Caller-supplied response `_meta` is forbidden; provider effects come only from the selected advertised option. The request belongs to the live execution owner and cannot be answered after owner loss.\n- **Stop**: `{ action: "stop", runId }` durably aborts the whole run and normally returns its final snapshot; stopping a terminal run is a successful no-op. Across a daemon upgrade, the successor persists an idempotent stop intent and forwards to the predecessor that owns execution. If that owner does not settle within the bounded control wait, the successful response remains nonterminal with `control:{ state:"pending", operationId, requestedAt, owner? }`; retry stop, inspect, or await to observe settlement. `{ action: "stop", runId, callIndex }` synchronously routes to the live owner and cancels exactly that in-flight agent: its slot settles to `null` with `AGENT_CANCELLED` and the run stays live; call cancellation is never reconstructed after owner loss. Whole-run `{ action:"stop", runId, forceOwner:true }` explicitly authorizes terminating a superseded owner daemon when graceful control cannot settle and may interrupt sibling runs in that process; it is forbidden with `callIndex`. `labelGlob` only filters the returned snapshot; it never selects what to cancel.\n- **Resume**: a NEW run with `resumeFromRunId` plus the script content re-sent (the same `script` or `scriptPath`) and the desired `args` (+ `checkpointReplies` when answering a durable checkpoint). Read the returned `replayEligibility` for the predicted and observed replay prefix; never assume a prefix hit. Full semantics: **Determinism and resume**.\n\n### Operating rules\n\n- **Always retain the returned `runId`.** A paused, failed, or aborted response carries a redacted final-20 `logTail`. Read it before you change anything. Every admitted script is an immutable resource at `workflow://runs/{runId}/script`, so a later session can recover a lost inline script. A completed JSON result is independently durable at `workflow://runs/{runId}/result`. Completed foreground/await responses copy exact JSON up to 4,096 UTF-8 bytes into model-visible text; larger results stay out of summary text and point to the resource plus `action:"result"` paging. Script and result links are labelled separately.\n- **Two fingerprints control replay.** The identity hash covers the prompt, the resolved model, `mode` when set, non-empty sorted `configOptions`, `tier`, `phase`, `agentType`, the resolved agent definition, and the schema. The input fingerprint covers the resolved label, per-call `cwd` and isolation, `keepSession`, images, MCP servers, session/prompt metadata, and the approved script-backend digest.\n- **Operational bounds are not replay inputs.** Host `concurrency`, `agentRetries`, `agentTimeoutMs`, and `agentIdleTimeoutMs`, plus per-call `timeoutMs`, `idleTimeoutMs`, and `retries`, enter neither fingerprint. A resume does not inherit them from its source run; pass the values you want on every run. `agentTimeoutMs` caps total wall time and is not an idle timer. The separate opt-in `agentIdleTimeoutMs` fires after that long without real backend activity; ACP `session/update` re-arms it and synthetic progress heartbeats do not. Per-call values can tighten but cannot escape finite host ceilings. Every retry gets fresh clocks.\n- **Old journals stay usable.** Input formats below 2 replay positionally with `fallbackReason: "inputs-format-legacy"`. A current-format crash snapshot uses identity matching even without terminal-environment capture. Ancestor-scoped rows carried from \u22640.23 resume chains replay only while that ancestor run is still persisted. Journals resume across filesystem, environment, engine, Node, and V8 changes; `replayEligibility` reports those differences as diagnostics, never as gates.\n- **A background start returns immediately.** It sends no progress after it returns; collect progress with later bounded awaits. Its `interaction` block explains that ACP permissions may block an agent and names await/inspect plus `permissions-response` as the control path. Background runs have no live checkpoint channel, so authored `headless` checkpoint modes apply. When a run\'s owner process dies, a pending whole-stop intent is applied under the reclaimed lease; otherwise cold preflights reconcile stale `pending`/`running` state to `paused` with `pauseReason:"interrupted"`. A live owner lease is never stolen because of a timeout.\n- A run paused with `reason: "auth_required"` resumes as a new run after that backend\'s credentials are configured.\n\n### Execution logs \u2014 the events resource\n\nEvery journaling run publishes an MCP resource at `workflow://runs/{runId}/events`. Subscribe to the canonical URI for advisory `resources/updated` hints, then read and paginate with `after`, `limit`, and `streamId`. This bounded/redacted stream is observability, not an exact result-reconstruction API; use `/result` or `action:"result"` for authored output. Progress is coarse and redacted: `agentTranscript` rows are assistant/tool upserts partitioned by `(scope, callIndex, executionStartSeq)` and reduced by greatest revision per entry index. The durable cursor is authoritative when hints coalesce or a subscriber falls behind.\n\nEmbedding hosts can drive the same contract with `runDynamicWorkflow` / `WorkflowManager` from `@automatalabs/workflows`; the script contract is identical either way.\n'
|
|
32491
32663
|
},
|
|
32492
32664
|
{
|
|
32493
32665
|
"id": "workflow/models-and-config",
|
|
@@ -32500,9 +32672,9 @@ var AUTHORING_DOC_TOPICS = [
|
|
|
32500
32672
|
"workflow/environment-and-tools",
|
|
32501
32673
|
"workflow/run-lifecycle"
|
|
32502
32674
|
],
|
|
32503
|
-
"bytes":
|
|
32504
|
-
"sha256": "
|
|
32505
|
-
"text": '## Choosing the agent for each call\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\nThe backend is selected **per `agent()` call** from its effective `model` string. One script can plan on one vendor\'s agent, implement on another\'s, and review on a third\'s, handing structured results between them.\n\nThe built-in names (`claude`, `codex`, `opencode`, `pi`) come from the runtime backend registry. Registered custom names extend that set.\n\n- **Omit `model` entirely** for maximum portability. In the MCP server, an explicitly present `AGENTPRISM_DEFAULT_BACKEND` wins; when it is truly unset, the first model-less run for a project performs zero-token backend readiness probes, pins one effective backend before validation/execution, and reuses that pin for the run and resume. The SDK runner itself retains its configured default (`AGENTPRISM_DEFAULT_BACKEND`, historical fallback Claude). A script with no model specs remains backend-portable.\n- **Route by one registered first segment.** Split on the first `/`; ASCII-case-insensitive `claude`, `codex`, `opencode`, `pi`, or a registered custom backend name selects that harness and is stripped exactly once. A custom registration wins on a built-in-name collision.\n- **Use a backend name alone** (`claude`, `codex`, `opencode`, `pi`, or a custom name) to preserve the harness\'s configured default model. No model config call is made.\n- **Everything else goes intact to the default backend.** `anthropic/\u2026`, `openai/\u2026`, bare `opus`, and bare `gpt-\u2026` are not routing aliases. When an id remains after routing, it is sent byte-for-byte: no catalog matching, case folding, bracket parsing, effort/Fast option driving, retry, or fallback. Harness rejection is an agent error.\n- **`tier`** (`"small" | "medium" | "big"`) is a coarse alternative resolved from the host\'s tier config \u2014 use it for "a cheap model" without naming a vendor.\n\nThe published examples use ids verified against live harness catalogs: `claude/opus[1m]`, `codex/gpt-5.6-sol`, and `opencode/zai/glm-5.2`. For Pi, `pi/openrouter/vendor/model-id` strips only `pi/`; Pi then splits provider `openrouter` from model id `vendor/model-id`. Prefer backend-only forms when the desired model is configured inside the harness.\n\nNever guess model ids, mode ids, effort values, or option names from memory. With MCP, call the `workflow` tool using `action:"config"` and optional `harnesses` / `modelFilter`; it returns the live catalog without starting a workflow.\n\nOne no-prompt session per harness, zero tokens: each successful harness entry contains `modes`
|
|
32675
|
+
"bytes": 9653,
|
|
32676
|
+
"sha256": "2c5279f1c2968e4f9fd24393b7c1107e48dbf773ec7462e4502896a3202a3403",
|
|
32677
|
+
"text": '## Choosing the agent for each call\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\nThe backend is selected **per `agent()` call** from its effective `model` string. One script can plan on one vendor\'s agent, implement on another\'s, and review on a third\'s, handing structured results between them.\n\nThe built-in names (`claude`, `codex`, `opencode`, `pi`) come from the runtime backend registry. Registered custom names extend that set.\n\n- **Omit `model` entirely** for maximum portability. In the MCP server, an explicitly present `AGENTPRISM_DEFAULT_BACKEND` wins; when it is truly unset, the first model-less run for a project performs zero-token backend readiness probes, pins one effective backend before validation/execution, and reuses that pin for the run and resume. The SDK runner itself retains its configured default (`AGENTPRISM_DEFAULT_BACKEND`, historical fallback Claude). A script with no model specs remains backend-portable.\n- **Route by one registered first segment.** Split on the first `/`; ASCII-case-insensitive `claude`, `codex`, `opencode`, `pi`, or a registered custom backend name selects that harness and is stripped exactly once. A custom registration wins on a built-in-name collision.\n- **Use a backend name alone** (`claude`, `codex`, `opencode`, `pi`, or a custom name) to preserve the harness\'s configured default model. No model config call is made.\n- **Everything else goes intact to the default backend.** `anthropic/\u2026`, `openai/\u2026`, bare `opus`, and bare `gpt-\u2026` are not routing aliases. When an id remains after routing, it is sent byte-for-byte: no catalog matching, case folding, bracket parsing, effort/Fast option driving, retry, or fallback. Harness rejection is an agent error.\n- **`tier`** (`"small" | "medium" | "big"`) is a coarse alternative resolved from the host\'s tier config \u2014 use it for "a cheap model" without naming a vendor.\n\nThe published examples use ids verified against live harness catalogs: `claude/opus[1m]`, `codex/gpt-5.6-sol`, and `opencode/zai/glm-5.2`. For Pi, `pi/openrouter/vendor/model-id` strips only `pi/`; Pi then splits provider `openrouter` from model id `vendor/model-id`. Prefer backend-only forms when the desired model is configured inside the harness.\n\nNever guess model ids, mode ids, effort values, or option names from memory. With MCP, call the `workflow` tool using `action:"config"` and optional `harnesses` / `modelFilter`; it returns the live catalog without starting a workflow.\n\nOne no-prompt session per harness, zero tokens: each successful harness entry contains `modes`, `defaultModeId`, and its config-option catalog. A non-null `modes` object carries `currentModeId` plus every available mode\'s raw id, name, description, and `_meta`; only exact advertised ids are valid. When omitted, AgentPrism applies Claude `auto`, Codex `agent`, OpenCode `build`, or no Pi mode. `modes:null` means the backend supports no mode. Config options list model ids (including bracket variants like `opus[1m]`), effort levels, and every other negotiable option exactly as the installed harness advertises them. `probed:true` means session/config discovery succeeded, **not** that every backend has proven it can authenticate a first prompt: ACP has no universal zero-token auth-status method, and some agents defer that check. Automatic MCP default selection treats failed probes and explicitly empty built-in model catalogs as unavailable, prefers stronger session-open evidence (Codex authorization; Pi\'s credential-filtered catalog), then falls back to the first session-ready backend whose prompt readiness is unknown. One additional caveat: the bare `config` probe reads each harness with its **default model** selected, and option domains are **model-specific**. An option can appear only after a particular model is selected. Ceilings differ per model. Provider-served variants of the same model can advertise different domains. The authoritative per-model probe is the validator run on your real script: it selects each authored model spec first and echoes that pair\'s advertised modes and options. Confirm every pinned value against its own echoed entry; do not read package internals to discover options.\n\n```js\nconst plan = await agent(PLAN_PROMPT, { label: "plan", model: "opencode/zai/glm-5.2", schema: PLAN });\nconst impl = await agent(implPrompt(plan), { label: "implement", model: "codex/gpt-5.6-sol" });\nconst review = await agent(reviewPrompt(impl), { label: "review", model: "claude/opus[1m]", schema: REVIEW });\n```\n\nUse `configOptions` only for exact ACP session options advertised by that routed harness. With MCP, read the selected harness\'s `action:"config"` result before choosing ids or select values; catalogs vary by harness version, login, and machine.\n\n```js\nconst impl = await agent(implPrompt(plan), {\n label: "implement",\n model: "codex",\n configOptions: { "fast-mode": true, reasoning_effort: "high" },\n});\n```\n\nIds and string/boolean values pass through verbatim in ascending id order, after model selection and before the prompt. There are no aliases, coercion, client-side vocabulary, defaults, or cached catalogs. Copy option ids character-for-character from the catalog, punctuation included \u2014 `"fast-mode"`, not `fast_mode` \u2014 and quote ids that are not valid identifiers. Never put `"model"` in `configOptions`; use the dedicated `model` field. A harness rejection follows the ordinary agent-error path.\n\nPi\'s thought-level option is named `thinkingLevel`, and its choices depend on the exact model in the same call:\n\n```js\nconst review = await agent(REVIEW_PROMPT, {\n label: "pi-review",\n model: "pi/openrouter/vendor/model-id",\n configOptions: { thinkingLevel: "high" },\n});\n```\n\nValidation selects `openrouter/vendor/model-id` before reading Pi\'s choices. A listed value passes unchanged. A recognized value above an ordered model\'s ceiling, or in a model-specific gap, passes with a warning that names the effective clamp target. Pi advertises its SDK-derived domain directly. Claude and Codex are also ordered: when their options omit domain metadata, validation enumerates the advertised models and merges their per-model effort orders. A Claude model without an `effort` option does not support effort, and `default` never becomes a ceiling target. OpenCode and custom backends have no declared value order, so validation is exact-set. An unrecognized or unadvertised value fails with exit code `2`. Enumeration stops at 32 advertised models; a larger or inconsistently ordered catalog warns and falls back to exact advertised-value validation.\n\n**The harness is authoritative.** The client never substitutes a nearby model or silently falls back. A rejected id follows the existing agent-error path; a harness that accepts or ignores it determines the outcome. The public `fallbacks`/`onModelFallback` fields remain for compatibility but model resolution does not emit them.\n\n## Structured output\n\nPass `schema` \u2014 a **plain JSON Schema object literal** (no schema builders exist inside the realm) \u2014 and the call resolves to a **validated object** instead of text:\n\n```js\nconst FINDINGS = {\n type: "object",\n additionalProperties: false,\n required: ["findings"],\n properties: {\n findings: {\n type: "array",\n items: {\n type: "object",\n additionalProperties: false,\n required: ["file", "line", "summary"],\n properties: {\n file: { type: "string", description: "Repo-relative path \u2014 copy it exactly, never invent one" },\n line: { type: "number", description: "1-indexed line the finding anchors to" },\n summary: { type: "string", description: "One sentence stating the defect, grounded in code you actually read" },\n },\n },\n },\n },\n};\n\nconst report = await agent("Review the diff on this branch for correctness bugs.", {\n label: "review", schema: FINDINGS,\n});\nreport.findings.forEach((f) => log(`${f.file}:${f.line} ${f.summary}`));\n```\n\nThe same schema works on **every** backend; only the fulfillment channel differs, and the runner picks it for you: Claude uses its `outputFormat`, Codex its strict `outputSchema`, while Pi, OpenCode, and eligible custom ACP agents receive a client-hosted `StructuredOutput` MCP tool when they advertise HTTP MCP support. Pi accepts stdio, Streamable HTTP, and SSE MCP servers. If no valid tool capture exists, Pi retains the runner\'s common prompt-embedded schema and validated final-text JSON fallback. In every channel the runner validates the value client-side (with type coercion) and re-prompts a bounded number of times before failing the call with non-recoverable `SCHEMA_NONCOMPLIANCE`.\n\nSchema authoring rules that keep all channels healthy:\n\n- Root must be an object; set `additionalProperties: false` and list every property in `required`.\n- Put a `description` on every field \u2014 descriptions are the per-field prompt.\n- Keep schemas structurally simple. Exotic keywords (`oneOf`, `patternProperties`, unusual `format`s, backreference regexes) are normalized or stripped on the wire for some backends \u2014 validation still enforces them client-side, which shows up as re-prompt churn. Prefer `anyOf`, `enum`, and plain types.\n- Keep free-text fields small (tens of lines). An oversized structured output can exhaust schema repair and fail the call.\n- Validation checks structure, not truth. Check load-bearing values in script code (for example, reject findings whose `file` is not in a known file list) before spending more agents on them.\n'
|
|
32506
32678
|
},
|
|
32507
32679
|
{
|
|
32508
32680
|
"id": "workflow/composition-and-failure",
|
|
@@ -32545,9 +32717,9 @@ var AUTHORING_DOC_TOPICS = [
|
|
|
32545
32717
|
"workflow/api-resume-and-backends",
|
|
32546
32718
|
"workflow/models-and-config"
|
|
32547
32719
|
],
|
|
32548
|
-
"bytes":
|
|
32549
|
-
"sha256": "
|
|
32550
|
-
"text": '## Working directory, isolation, confinement\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\n- Every agent session runs in the run\'s base `cwd` unless the call narrows it: `agent({ cwd: "packages/api" })` (relative resolves against the base).\n- `isolation: "worktree"` runs the agent in a **throwaway git worktree** (`<repoRoot>/.agentprism/worktrees/\u2026`) so parallel agents can edit without colliding. The worktree and its branch are **always deleted when the call ends \u2014 an isolated agent\'s file edits are discarded**. Have isolated agents *return their work as data* (a unified diff, a file map, a report) and apply it in a later non-isolated step; use worktrees for experiments, builds, and verification, not for persistent edits. Outside a git repo, isolation degrades to the shared tree with a logged notice.\n- `resume: { filesystem: "read-only" }` is a deprecated compatibility annotation. It is not a runner mode and has no effect on replay; completed calls replay by journal correspondence whether they read or write. Use `mode`, tool policy, prompts, and worktrees when you actually need confinement.\n- `mode` requests an agent-advertised ACP session mode
|
|
32720
|
+
"bytes": 5648,
|
|
32721
|
+
"sha256": "562a2372283f5a26c7ed0247ddf7eb972e51b1ede17a4d6b42758b7756256dfa",
|
|
32722
|
+
"text": '## Working directory, isolation, confinement\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\n- Every agent session runs in the run\'s base `cwd` unless the call narrows it: `agent({ cwd: "packages/api" })` (relative resolves against the base).\n- `isolation: "worktree"` runs the agent in a **throwaway git worktree** (`<repoRoot>/.agentprism/worktrees/\u2026`) so parallel agents can edit without colliding. The worktree and its branch are **always deleted when the call ends \u2014 an isolated agent\'s file edits are discarded**. Have isolated agents *return their work as data* (a unified diff, a file map, a report) and apply it in a later non-isolated step; use worktrees for experiments, builds, and verification, not for persistent edits. Outside a git repo, isolation degrades to the shared tree with a logged notice.\n- `resume: { filesystem: "read-only" }` is a deprecated compatibility annotation. It is not a runner mode and has no effect on replay; completed calls replay by journal correspondence whether they read or write. Use `mode`, tool policy, prompts, and worktrees when you actually need confinement.\n- `mode` requests an exact agent-advertised ACP session mode. Config returns raw names, descriptions, and `_meta`; use those backend-owned explanations instead of inferring from an id. When omitted, AgentPrism applies Claude `auto`, Codex `agent`, OpenCode `build`, or no Pi mode. Automatic preflight rejects an authored or built-in default that the selected backend/model does not advertise. Only set `mode` on calls whose `model` you also pin. Use an explicitly advertised read-only/plan mode for reviewers and auditors that must not write.\n- `agentType: "<name>"` binds a reusable subagent definition \u2014 a Markdown file at `<cwd>/.agentprism/agents/<name>.md` (project) or `~/.agentprism/agents/<name>.md` (user; project wins) whose frontmatter sets tool allow/deny lists, a model, and isolation, and whose body is the role prompt. An unknown name logs a warning and degrades to defaults.\n\n## Where a mutating workflow runs\n\nThe run\'s base `cwd` is the USER\'S checkout \u2014 the working copy they launched the host from. Treat it as borrowed: committing onto whatever branch is checked out, switching branches, or resetting it are defects unless the user asked for exactly that. A script that commits should verify its target workspace in a preflight step, or create its own workspace idempotently, and refuse on a mismatch rather than adapt. `isolation: "worktree"` is NOT such a workspace \u2014 it is per-call and throwaway. Note also that a throwaway worktree branches from the run cwd\'s repository: an isolated agent sees another agent\'s commits only when they are reachable there.\n\n## Wiring tools and inputs into a call\n\n- `mcpServers: [{ name, command, args: [], env: [] }]` attaches MCP servers to that agent\'s session \u2014 the portable way to hand any backend a capability (image generation, a browser, a ticket system). The agent sees the server\'s tools natively. Note `env` is a list of `{ name, value }` pairs (ACP shape), not an object map; HTTP/SSE servers use `{ type: "http", name, url, headers: [] }`.\n- `images: [...]` appends base64 image blocks to the prompt (backends without image support receive a bracketed text note instead).\n- `meta` / `promptMeta` pass generic ACP `_meta` through to `session/new` / `session/prompt` \u2014 the escape hatch for driving a custom agent\'s extension surface.\n- `keepSession: true` keeps a successful agent\'s ACP session re-openable after the run: the re-attach record (sessionId, backend, effective pool identity, cwd, reopen capabilities) lands in `WorkflowRunResult.agentSessions`, and the HOST can continue that conversation later via `runner.loadSession()`. Usage/auth pause failures are kept open automatically so managed resume can continue the interrupted occurrence. Scripts themselves never request reattach.\n\n### Custom ACP backends\n\nAny process that speaks ACP over stdio can serve `agent()` calls \u2014 an in-house browser-QA agent, an image generator, a domain-specific executor. Two ways in:\n\n1. **Host-registered** (preferred): the embedder passes `createAcpRunner({ backends: { browser: { command: "/abs/browser-acp" } } })`; the script just routes with `model: "browser"`.\n2. **Script-declared**: the script itself declares the backend in `meta.backends` \u2014 but declarations are **inert until the host approves them** (an elicitation in the MCP server; `allowScriptBackends` in the SDK), because they spawn commands on the host machine. Don\'t rely on them silently working.\n\n```js\nexport const meta = {\n name: "checkout-qa",\n description: "Implement, then QA the checkout flow in a real browser",\n backends: {\n browser: { command: "browser-acp", args: ["--headless"] }, // requires host approval\n },\n};\n\nconst change = await agent("Implement the coupon-code field per the spec in docs/coupon.md.",\n { label: "implement" }); // default backend\nconst verdict = await agent(\n `Open the app, walk through checkout with coupon SAVE20, and verify the discount line. Change summary:\\n${change}`,\n { label: "qa", model: "browser", // the custom agent\n schema: { type: "object", additionalProperties: false, required: ["passed"],\n properties: { passed: { type: "boolean" }, notes: { type: "string" } } } },\n);\nreturn { change, qa: verdict };\n```\n\nStructured output works on custom backends through the same injected-tool/fallback ladder as OpenCode \u2014 no special-casing in the script.\n'
|
|
32551
32723
|
},
|
|
32552
32724
|
{
|
|
32553
32725
|
"id": "workflow/determinism-and-resume",
|
|
@@ -32575,9 +32747,9 @@ var AUTHORING_DOC_TOPICS = [
|
|
|
32575
32747
|
"workflow/environment-and-tools",
|
|
32576
32748
|
"workflow/api-control-flow"
|
|
32577
32749
|
],
|
|
32578
|
-
"bytes":
|
|
32579
|
-
"sha256": "
|
|
32580
|
-
"text": '# Workflow agent API reference\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\n## `agent(prompt, options?)` \u2014 full option table\n\nReturns the agent\'s final assistant text, or the schema-validated object when `schema` is set. Resolves to `null` when a *recoverable* failure survives all retries.\n\n| option | type | meaning |\n|---|---|---|\n| `label` | `string` | Display/telemetry name; also stamped on every live ACP event for this call. Always set it. Not part of the resume hash. |\n| `phase` | `string` | Assign this call to a phase explicitly (needed inside concurrent stages where the global `phase()` state would race). |\n| `schema` | JSON Schema object | Structured output. Plain object literal only \u2014 no schema builders exist in the realm. Part of the resume hash. |\n| `model` | `string` | Model spec: optional registered harness prefix plus a verbatim id, or a backend-only name. See [Model specs & routing](#model-specs--routing). Part of the resume hash. |\n| `tier` | `"small" \\| "medium" \\| "big"` | Coarse tier resolved from host config; beats phase/meta model, loses to explicit `model`. Part of the resume hash. |\n| `mode` | `string` | ACP session mode id advertised by the selected backend/model.
|
|
32750
|
+
"bytes": 8842,
|
|
32751
|
+
"sha256": "aeda144cdea50062a1d2c42d11e3cb26b058777f50c3285318a8ad1a53319eb4",
|
|
32752
|
+
"text": '# Workflow agent API reference\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\n## `agent(prompt, options?)` \u2014 full option table\n\nReturns the agent\'s final assistant text, or the schema-validated object when `schema` is set. Resolves to `null` when a *recoverable* failure survives all retries.\n\n| option | type | meaning |\n|---|---|---|\n| `label` | `string` | Display/telemetry name; also stamped on every live ACP event for this call. Always set it. Not part of the resume hash. |\n| `phase` | `string` | Assign this call to a phase explicitly (needed inside concurrent stages where the global `phase()` state would race). |\n| `schema` | JSON Schema object | Structured output. Plain object literal only \u2014 no schema builders exist in the realm. Part of the resume hash. |\n| `model` | `string` | Model spec: optional registered harness prefix plus a verbatim id, or a backend-only name. See [Model specs & routing](#model-specs--routing). Part of the resume hash. |\n| `tier` | `"small" \\| "medium" \\| "big"` | Coarse tier resolved from host config; beats phase/meta model, loses to explicit `model`. Part of the resume hash. |\n| `mode` | `string` | Exact ACP session mode id advertised by the selected backend/model. Config preserves each raw name, description, and `_meta`. When omitted, AgentPrism applies Claude `auto`, Codex `agent`, OpenCode `build`, or no Pi mode; `defaultModeId` reports that choice. Authored and built-in defaults are validated before prompting. Part of the resume hash only when authored. |\n| `configOptions` | `Record<string, string \\| boolean>` | Exact ACP session option ids and authored values. Applied in ascending id order after model and before the prompt, with no aliases or coercion. `"model"` is reserved for the dedicated `model` field. Part of the resume hash only when non-empty, with sorted keys. With MCP, read the advertised-options table from `workflow` action `config` before choosing values. |\n| `agentType` | `string` | Bind a named subagent definition (tools allow/deny, model, isolation, role prompt). See [agentType definitions](#agenttype-definitions). Part of the resume hash. |\n| `isolation` | `"worktree"` | Run in a throwaway git worktree branched from the run cwd. **Always removed (worktree + branch) when the call ends** \u2014 edits are discarded; return work as data. Degrades to the shared tree outside a git repo (logged). |\n| `resume` | `{ filesystem: "read-only" }` | Deprecated compatibility annotation. It is recorded as legacy diagnostic provenance, is not sent to the runner or hashed, and has no effect on replay. New scripts should omit it. |\n| `cwd` | `string` | Per-session working directory; relative resolves against the run\'s base cwd. Overridden by worktree isolation. Not hashed. |\n| `timeoutMs` | `number \\| null` | Total wall-clock cap for each attempt. A finite value may tighten a finite host `agentTimeoutMs` ceiling but cannot raise or disable it. With no host ceiling, a finite value applies and `null`/omitted is uncapped. |\n| `idleTimeoutMs` | `number \\| null` | No-backend-activity cap for each attempt. It may tighten a finite host `agentIdleTimeoutMs` ceiling but cannot raise or disable it. |\n| `retries` | `number` | Retries after *recoverable* failures (default 0, host-overridable). Exhausted retries \u21D2 the call resolves `null`. |\n| `mcpServers` | `McpServerConfig[]` | MCP servers attached to this session. Stdio shape: `{ name, command, args: [], env: [{ name, value }] }` (`args`/`env` required, `env` is name/value pairs, not a map); `{ type: "http" \\| "sse", name, url, headers: [] }` also accepted. Not hashed. |\n| `images` | `PromptImage[]` | Base64 image blocks appended to the prompt; backends without image support get a bracketed text note. Not hashed. |\n| `meta` | `object` | ACP `_meta` merged into `session/new` \u2014 session-scoped extension passthrough (pairs with custom backends). Not hashed. |\n| `promptMeta` | `object` | ACP `_meta` merged into `session/prompt` \u2014 turn-scoped passthrough. Backend-computed keys win on conflict. Not hashed. |\n| `keepSession` | `boolean` | Skip release-time best-effort `session/close`; the non-secret re-attach record lands in `WorkflowRunResult.agentSessions` for host-side `loadSession()` / `resumeSession()`. Usage/auth pause failures are kept open automatically for managed continuation. Not identity-hashed; included in the input fingerprint. |\n\nThe total-wall clock measures the whole attempt, including backend startup, model/config setup,\ntool work, and streamed output; it is not an idle timer. The separate idle clock is opt-in and\nre-arms on real backend activity (every ACP `session/update`), never synthetic progress heartbeats.\nSize it above the longest expected backend-silent local tool call. Each retry starts fresh clocks.\nExhaustion is recoverable `AGENT_TIMEOUT` or `AGENT_IDLE_TIMEOUT`: the call resolves to `null`,\nreleases its concurrency slot, and asks the ACP session to cancel. A session that keeps running\nafter the cancellation grace is closed where supported and its pooled child is recycled.\n\nEvery new run, including one admitted with `resumeFromRunId`, resolves host limits from that run\'s\nrequest. It does not inherit `agentTimeoutMs`, `agentIdleTimeoutMs`, retries, concurrency, or\nagent-count values from its source, so pass every operational bound the resumed execution should use.\n\n## Model specs & routing\n\nA `model` string is resolved solely from its first segment, then delegated to the harness:\n\n| spec shape | routes to | notes |\n|---|---|---|\n| *(omitted)* | host-pinned/default backend | MCP: explicit `AGENTPRISM_DEFAULT_BACKEND` wins; when truly unset, zero-token readiness discovery pins one project default before validation/execution and preserves it across resume. SDK runner: configured default, historical fallback `claude`. The selected harness keeps its session default model. Most portable. |\n| `claude`, `codex`, `opencode`, `pi`, or `<custom-name>` | that registered harness | Backend-only: no model config call; the harness default remains active. |\n| `claude/<id>`, `codex/<id>`, `opencode/<id>`, `pi/<id>`, or `<custom-name>/<id>` | that registered harness | Match the first segment ASCII-case-insensitively and strip exactly one segment. Custom names take priority on collision. The remaining `<id>` is sent verbatim, including further `/` characters. For Pi, that remainder is its `<provider>/<model-id>` and Pi preserves any further slashes in the model id. |\n| any other string, including `anthropic/\u2026`, `openai/\u2026`, bare `opus`, or bare `gpt-\u2026` | host default backend | The **entire** authored string is sent verbatim; these are not routing aliases. |\n\nSelection is a single `session/set_config_option` with `configId: "model"` and the exact remaining string. There is no catalog matching, case folding, normalization, bracket parsing, nearest-neighbor selection, sibling effort/Fast option driving, retry, or echo verification. Brackets, dots, and provider-style prefixes are ordinary model-id characters.\n\nWhatever the harness returns is the outcome. A rejection follows the existing agent-error path with no resolution-specific code or model fallback event. `onModelFallback` and `WorkflowRunResult.fallbacks` remain public compatibility surfaces; model resolution does not emit entries, while pause recovery emits `kind: "continuation"` reattach/skip notices.\n\n## Structured output channels\n\nOne author API (`schema`), four fulfillment paths \u2014 chosen automatically per backend:\n\n| backend | channel |\n|---|---|\n| Claude | native `outputFormat`, schema normalized to Anthropic\'s structured-outputs subset (e.g. `oneOf` \u2192 `anyOf`; unsupported keywords/formats stripped on the wire) |\n| Codex | native strict `outputSchema` (OpenAI strict subset normalization) |\n| Pi | a client-hosted `StructuredOutput` MCP tool injected when the agent advertises HTTP MCP support; common prompt-embedded schema and validated final-text JSON fallback |\n| OpenCode / custom ACP | a client-hosted **`StructuredOutput` MCP tool** injected into the session when the agent advertises HTTP MCP support (an agent may show it as `structured_output_StructuredOutput`); otherwise prompt-embedded schema + JSON parse of the final message. Custom backends can opt out of tool injection with `structuredOutputTool: false`. |\n\nPi accepts stdio, Streamable HTTP, and SSE MCP servers; ACP-transport MCP hosting remains client-side.\n\nIn every channel the runner coerces + validates client-side and re-prompts a bounded number of times; the final miss fails the call with non-recoverable `SCHEMA_NONCOMPLIANCE`. Constraints stripped from the wire are still enforced client-side \u2014 an exotic schema keyword shows up as re-prompt churn, so keep schemas simple.\n'
|
|
32581
32753
|
},
|
|
32582
32754
|
{
|
|
32583
32755
|
"id": "workflow/api-control-flow",
|
|
@@ -32620,9 +32792,9 @@ var AUTHORING_DOC_TOPICS = [
|
|
|
32620
32792
|
"workflow/composition-and-failure",
|
|
32621
32793
|
"workflow/checkpoints-and-quality"
|
|
32622
32794
|
],
|
|
32623
|
-
"bytes":
|
|
32624
|
-
"sha256": "
|
|
32625
|
-
"text": '## Worked example \u2014 cross-vendor build with every major primitive\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\n```js\nexport const meta = {\n name: "feature-build",\n description: "Plan, gate on approval, implement, cross-vendor review, fix until green",\n phases: [{ title: "Plan" }, { title: "Implement" }, { title: "Review" }],\n};\n\nconst PLAN = { type: "object", additionalProperties: false, required: ["steps", "risks"],\n properties: {\n steps: { type: "array", items: { type: "string", description: "One concrete implementation step" } },\n risks: { type: "array", items: { type: "string" } } } };\nconst VERDICT = { type: "object", additionalProperties: false, required: ["ok"],\n properties: { ok: { type: "boolean" },\n feedback: { type: "string", description: "Required when ok=false: concretely what to change" } } };\n\nphase("Plan");\nconst plan = await agent(\n `Study this repo, then write an implementation plan for: ${args.feature}. Keep steps concrete.`,\n { label: "plan", model: "opencode/zai/glm-5.2", schema: PLAN },\n);\n\nconst approved = await checkpoint(\n `Implement "${args.feature}" with this plan?\\n- ${plan.steps.join("\\n- ")}\\nRisks: ${plan.risks.join("; ")}`,\n { kind: "confirm", default: true },\n);\nif (!approved) return { implemented: false, plan };\n\nphase("Implement");\nconst outcome = await gate(\n (feedback, attempt) => agent(\n `Implement: ${args.feature}\\nPlan:\\n- ${plan.steps.join("\\n- ")}\\n` +\n `Run the project\'s tests before finishing and report results.` +\n (feedback ? `\\n\\nReviewer feedback on attempt ${attempt}:\\n${feedback}\\nAddress every point.` : ""),\n { label: `implement:${attempt + 1}`, model: "codex/gpt-5.6-sol", retries: 1 },\n ),\n async (report) => {\n if (!report) return { ok: false, feedback: "implementation agent produced no result" };\n phase("Review");\n const reviews = (await parallel([ // two reviewers on different vendors\n () => agent(`Review the working-tree diff for correctness. Implementer\'s report:\\n${report}`,\n { label: "review:correctness", model: "claude/opus[1m]", schema: VERDICT }),\n () => agent(`Review the working-tree diff for regressions and missing tests. Report:\\n${report}`,\n { label: "review:coverage", model: "opencode/zai/glm-5.2", schema: VERDICT }),\n ])).filter(Boolean);\n const rejections = reviews.filter((r) => !r.ok);\n return rejections.length\n ? { ok: false, feedback: rejections.map((r) => r.feedback).join("\\n"), reviews }\n : { ok: true, reviews };\n },\n { attempts: 3 },\n);\n\nreturn { implemented: outcome.ok, attempts: outcome.attempts, reviewVerdict: outcome.verdict, plan };\n```\n\n(
|
|
32795
|
+
"bytes": 6133,
|
|
32796
|
+
"sha256": "267c9831a93a8cb4c6f64fa951782803f4a990b3ed8c69ba3ad75e367a60fb8f",
|
|
32797
|
+
"text": '## Worked example \u2014 cross-vendor build with every major primitive\n\n**Context:** JavaScript passed to the MCP `workflow` tool. Workflow scripts use `agent(prompt, options?)`; REPL evals use a different API.\n\n```js\nexport const meta = {\n name: "feature-build",\n description: "Plan, gate on approval, implement, cross-vendor review, fix until green",\n phases: [{ title: "Plan" }, { title: "Implement" }, { title: "Review" }],\n};\n\nconst PLAN = { type: "object", additionalProperties: false, required: ["steps", "risks"],\n properties: {\n steps: { type: "array", items: { type: "string", description: "One concrete implementation step" } },\n risks: { type: "array", items: { type: "string" } } } };\nconst VERDICT = { type: "object", additionalProperties: false, required: ["ok"],\n properties: { ok: { type: "boolean" },\n feedback: { type: "string", description: "Required when ok=false: concretely what to change" } } };\n\nphase("Plan");\nconst plan = await agent(\n `Study this repo, then write an implementation plan for: ${args.feature}. Keep steps concrete.`,\n { label: "plan", model: "opencode/zai/glm-5.2", schema: PLAN },\n);\n\nconst approved = await checkpoint(\n `Implement "${args.feature}" with this plan?\\n- ${plan.steps.join("\\n- ")}\\nRisks: ${plan.risks.join("; ")}`,\n { kind: "confirm", default: true },\n);\nif (!approved) return { implemented: false, plan };\n\nphase("Implement");\nconst outcome = await gate(\n (feedback, attempt) => agent(\n `Implement: ${args.feature}\\nPlan:\\n- ${plan.steps.join("\\n- ")}\\n` +\n `Run the project\'s tests before finishing and report results.` +\n (feedback ? `\\n\\nReviewer feedback on attempt ${attempt}:\\n${feedback}\\nAddress every point.` : ""),\n { label: `implement:${attempt + 1}`, model: "codex/gpt-5.6-sol", retries: 1 },\n ),\n async (report) => {\n if (!report) return { ok: false, feedback: "implementation agent produced no result" };\n phase("Review");\n const reviews = (await parallel([ // two reviewers on different vendors\n () => agent(`Review the working-tree diff for correctness. Implementer\'s report:\\n${report}`,\n { label: "review:correctness", model: "claude/opus[1m]", schema: VERDICT }),\n () => agent(`Review the working-tree diff for regressions and missing tests. Report:\\n${report}`,\n { label: "review:coverage", model: "opencode/zai/glm-5.2", schema: VERDICT }),\n ])).filter(Boolean);\n const rejections = reviews.filter((r) => !r.ok);\n return rejections.length\n ? { ok: false, feedback: rejections.map((r) => r.feedback).join("\\n"), reviews }\n : { ok: true, reviews };\n },\n { attempts: 3 },\n);\n\nreturn { implemented: outcome.ok, attempts: outcome.attempts, reviewVerdict: outcome.verdict, plan };\n```\n\n(An omitted mode uses AgentPrism\'s autonomous built-in default. For a genuinely read-only planner, inspect the backend-owned mode names/descriptions from `action:"config"` and pin the exact advertised read-only/plan id.)\n\n## Worked example \u2014 fully backend-agnostic audit\n\nNo `model` anywhere: this script runs unchanged on whatever backend the host defaults to.\n\n```js\nexport const meta = {\n name: "edge-case-audit",\n description: "Exhaustively hunt edge-case bugs in a target dir, verify each, report gaps",\n phases: [{ title: "Hunt" }, { title: "Verify" }],\n};\n\nconst BUGS = { type: "object", additionalProperties: false, required: ["bugs"],\n properties: { bugs: { type: "array", items: { type: "object", additionalProperties: false,\n required: ["file", "scenario"], properties: {\n file: { type: "string", description: "Repo-relative path you actually opened" },\n scenario: { type: "string", description: "Concrete input/state \u2192 wrong behavior" } } } } } };\n\nphase("Hunt");\nconst seen = []; // what earlier rounds reported, threaded into each new prompt\nconst candidates = await loopUntilDry({\n round: async (i) => {\n const r = await agent(\n `Round ${i + 1}: find edge-case bugs in ${args.target} not already in this list:\\n` +\n JSON.stringify(seen) + `\\nOnly report what you can ground in code you read.`,\n { label: `hunt:${i + 1}`, schema: BUGS },\n );\n const bugs = r ? r.bugs : [];\n seen.push(...bugs);\n return bugs; // loopUntilDry dedups these by `key` across rounds\n },\n key: (b) => `${b.file}:${b.scenario}`,\n consecutiveEmpty: 2,\n maxRounds: 8,\n});\n\nphase("Verify");\nconst confirmed = (await pipeline(\n candidates,\n (bug) => verify(bug, { reviewers: 3, threshold: 0.66, lens: ["correctness", "reproducibility"] }),\n (v, bug) => (v.real ? bug : null),\n)).filter(Boolean);\n\nconst gaps = await completenessCheck(args, confirmed);\nlog(`${confirmed.length}/${candidates.length} confirmed; complete=${gaps.complete}`);\nreturn { confirmed, missing: gaps.missing ?? [] };\n```\n\n## Automatic validation before admission\n\nThe MCP `workflow` tool validates every run automatically before admission: static parse, mocked dry run, then routed no-prompt config checks. Invalid scripts return `status:"rejected"` diagnostics without creating a run ID, reserving a background slot, or spending tokens. When pinning model, mode, or `configOptions`, use `action:"config"` first.\n\nThe mocked pass executes reachable script control flow with schema-conforming fabricated agent results. It can prove that syntax, metadata, helper calls, and reachable branches are structurally executable, but it cannot prove prompt quality, real-world judgment, or convergence through every branch. Keep loops bounded in script code and inspect validation warnings for declared phases that the default fabricated path did not reach.\n\nThe routed config pass probes each distinct backend/model pair without prompting. Unknown option ids, invalid select values, wrong value types, and the reserved `"model"` config key reject the script with direct alternatives. A backend that cannot be probed produces an explicit warning and leaves only that backend\'s option domain unverified.\n\nFor model/config details, read `workflow/models-and-config`. For edited-script replay patterns, read `workflow/determinism-and-resume`.\n'
|
|
32626
32798
|
},
|
|
32627
32799
|
{
|
|
32628
32800
|
"id": "repl/quickstart",
|
|
@@ -32666,9 +32838,9 @@ var AUTHORING_DOC_TOPICS = [
|
|
|
32666
32838
|
"repl/steering-queueing-and-cancellation",
|
|
32667
32839
|
"repl/api-reference"
|
|
32668
32840
|
],
|
|
32669
|
-
"bytes":
|
|
32670
|
-
"sha256": "
|
|
32671
|
-
"text": '# REPL agent calls and persistent handles\n\nREPL delegation uses:\n\n```js\nagent(modelSpec, task, options?) -> PromiseHandle\n```\n\nThis differs from workflow scripts, whose signature is `agent(prompt, options?)`.\n\n## Model routing\n\n`modelSpec` is required. Use a registered backend name alone to preserve its configured default model:\n\n```js\nconst worker = agent("codex", "Investigate the failing parser test");\n```\n\nUse `backend/model-id` only after model discovery:\n\n```js\nconst worker = agent("claude/verified-model-id", "Review the implementation");\n```\n\nThe known built-ins are Claude, Codex, OpenCode, and pi, plus host-registered custom backends. Unknown backend names reject and enumerate known backends. Use the `workflow` tool\'s `action:"config"` with `harnesses`/`modelFilter`, then `modelSpecs`, before pinning model, mode, or config-option values.
|
|
32841
|
+
"bytes": 3565,
|
|
32842
|
+
"sha256": "26f9868ba7d70fac2465cdb8fc0d22726585b61608d852aeca26b12a762dff96",
|
|
32843
|
+
"text": '# REPL agent calls and persistent handles\n\nREPL delegation uses:\n\n```js\nagent(modelSpec, task, options?) -> PromiseHandle\n```\n\nThis differs from workflow scripts, whose signature is `agent(prompt, options?)`.\n\n## Model routing\n\n`modelSpec` is required. Use a registered backend name alone to preserve its configured default model:\n\n```js\nconst worker = agent("codex", "Investigate the failing parser test");\n```\n\nUse `backend/model-id` only after model discovery:\n\n```js\nconst worker = agent("claude/verified-model-id", "Review the implementation");\n```\n\nThe known built-ins are Claude, Codex, OpenCode, and pi, plus host-registered custom backends. Unknown backend names reject and enumerate known backends. Use the `workflow` tool\'s `action:"config"` with `harnesses`/`modelFilter`, then `modelSpecs`, before pinning model, mode, or config-option values. Read the selected entry\'s harness-owned mode names/descriptions before pinning an exact advertised id. Omission uses `defaultModeId` (Claude auto, Codex agent, OpenCode build; none for Pi).\n\n## Exact option vocabulary\n\n```js\nconst worker = agent("codex", "Inspect the parser", {\n schema: {\n type: "object",\n additionalProperties: false,\n required: ["summary"],\n properties: { summary: { type: "string" } },\n },\n cwd: "/absolute/path",\n mode: "advertised-mode-id",\n configOptions: { advertisedOptionId: "advertised-value" },\n});\n```\n\nOptions are exactly:\n\n- `schema`: plain JSON Schema object; the promise resolves to its validated object.\n- `cwd`: absolute worker session working directory.\n- `mode`: exact ACP mode explicitly listed in the selected backend/model\'s discovered `modes.availableModes`.\n- `configOptions`: exact string/boolean ACP option ids and values.\n\nUnknown option keys reject. Options must be JSON-serializable.\n\n## Preserve the handle\n\nThe returned promise is also the live handle:\n\n```js\nconst worker = agent("pi", "Research the issue");\nconst id = worker.id;\nconst answer = await worker;\n```\n\nDo not write this if you intend to reuse the session:\n\n```js\nconst worker = await agent("pi", "Research the issue");\n```\n\nThat variable stores only the answer and loses access to handle methods.\n\nThe founding handle exposes non-enumerable, immutable members:\n\n- `id`: stable call id such as `"c1"`.\n- `queue(prompt, options?)`: create a distinct durable FIFO future turn.\n- `steer(prompt, options?)`: attempt strict control of only the currently active turn.\n- `cancel()`: cancel the session\'s current public turn.\n\nA queued-turn handle exposes its own `id` and `cancel()`.\n\n## Settlement and failures\n\nWithout `schema`, the founding promise resolves to final assistant text. With `schema`, it resolves to the validated object.\n\nA rejected call carries an error with call/backend attribution where available. Errors whose `recoverable` field is not `false` are treated as recoverable by `parallel()` and `pipeline()` and become `null` slots. A non-recoverable error rejects the surrounding combinator/eval.\n\nDirect `await worker` propagates rejection; catch only errors you can handle meaningfully:\n\n```js\nlet answer;\ntry {\n answer = await worker;\n} catch (error) {\n console.error(error);\n answer = null;\n}\n```\n\n## Session continuity\n\nThe founding answer settling does not erase the handle binding. Queue later prompts on the founding handle to continue the same ACP session. Session continuity depends on the backend\'s continuation capability and the workspace\'s durable lane state. Never fabricate a new handle from a saved id; retain the actual promise-handle binding.\n'
|
|
32672
32844
|
},
|
|
32673
32845
|
{
|
|
32674
32846
|
"id": "repl/steering-queueing-and-cancellation",
|
|
@@ -33020,7 +33192,7 @@ function registerReplTool(mcp, options) {
|
|
|
33020
33192
|
mcp.registerTool(
|
|
33021
33193
|
"repl",
|
|
33022
33194
|
{
|
|
33023
|
-
description: 'A persistent QuickJS-in-WASM JavaScript VM you drive interactively to orchestrate subagents \u2014 one VM per projectDir, addressed by the same project model as the workflow tool. Two actions: eval runs code and holds the call open pumping settlements; interrupt cancels one subagent call (by id) or breaks the running eval (no id). Named bindings, pending subagent calls, raised checkpoints, and `_` (the previous eval\'s completion value) PERSIST in the VM between calls \u2014 a later eval sees the same variables and awaits the same promises. Console logging produces output text only and creates no persistent value; nothing lives in the transcript. For deeper syntax and examples, read docs topic repl/quickstart and then one related repl/* topic. Inside code (JavaScript; top-level await is allowed, top-level return is a syntax error; console output is captured) the host bridge provides agent(modelSpec, task, opts?) \u2192 Promise: spawn an ACP subagent on a registry built-in (currently Claude, Codex, OpenCode, and pi) or a registered custom agent. The spec is "backend/model" (a bare "backend" runs its default model); an unknown backend rejects the call immediately, naming the known backends. The opts keys are schema (a structured-output JSON schema, validated per call), cwd, configOptions (backend-specific knobs, validated at admission), and mode. Before setting mode, use workflow action:"config" for that exact modelSpec and
|
|
33195
|
+
description: 'A persistent QuickJS-in-WASM JavaScript VM you drive interactively to orchestrate subagents \u2014 one VM per projectDir, addressed by the same project model as the workflow tool. Two actions: eval runs code and holds the call open pumping settlements; interrupt cancels one subagent call (by id) or breaks the running eval (no id). Named bindings, pending subagent calls, raised checkpoints, and `_` (the previous eval\'s completion value) PERSIST in the VM between calls \u2014 a later eval sees the same variables and awaits the same promises. Console logging produces output text only and creates no persistent value; nothing lives in the transcript. For deeper syntax and examples, read docs topic repl/quickstart and then one related repl/* topic. Inside code (JavaScript; top-level await is allowed, top-level return is a syntax error; console output is captured) the host bridge provides agent(modelSpec, task, opts?) \u2192 Promise: spawn an ACP subagent on a registry built-in (currently Claude, Codex, OpenCode, and pi) or a registered custom agent. The spec is "backend/model" (a bare "backend" runs its default model); an unknown backend rejects the call immediately, naming the known backends. The opts keys are schema (a structured-output JSON schema, validated per call), cwd, configOptions (backend-specific knobs, validated at admission), and mode. Before setting mode, use workflow action:"config" for that exact modelSpec and read the harness-owned mode descriptions. Omission uses defaultModeId (Claude auto, Codex agent, OpenCode build; none for Pi); pin only an exact advertised id. Unknown option keys reject synchronously. agent() returns a persistent promise-handle. Assign the handle before awaiting it: `const a = agent("codex", "inspect the failure"); const first = await a`. a.steer(text) targets only the currently running turn. It never starts or queues another turn and resolves "injected", "idle", or "unsupported"; transport and protocol failures reject. Steering while idle returns "idle" and loses the instruction by design. `const q = a.queue(text)` creates a distinct FIFO turn on the same session. q.id is available immediately, await q returns that turn\'s answer, and q.cancel() or an out-of-band interrupt of q.id cancels that exact turn. Queueing works on every backend that can continue the session; steering requires the ACP server\'s raw steering advertisement. Do not write `const a = await agent(...)` when you intend to reuse the handle, because that stores only the answer. Persistent-workspace example \u2014 first eval: `const a = agent("codex", "Investigate the parser failure")`; a later eval, only while agents() reports a\'s turn as running: `const steering = await a.steer("Focus on the parser state machine")`; after the founding answer settles: `const first = await a; const q1 = a.queue("Implement the fix"); const q2 = a.queue("Run the focused tests"); console.log(q1.id, q2.id, steering); const fixed = await q1; const tested = await q2`. checkpoint(question) parks a promise for a human answer, resolved by checkpoint.answer(id, value) in a later eval. parallel, pipeline, verify, judgePanel, gate, retry, loopUntilDry, and sleep(ms) round out the guest library. Introspection is in-band: workspace() returns { bindings, inFlight, checkpoints, diagnostics }; agents() lists live agents with their call ids and states; reset() tears the workspace down. `_` holds the previous eval\'s completion value. No fs, no net, no timers beyond sleep. Subagents (6 concurrent per workspace) take stable ids c1, c2, \u2026 used by interrupt and reported by agents(). eval { code } runs the code, then HOLDS THE CALL OPEN pumping settlements up to a soft bound (default 60 000 ms; per-call timeoutMs override; hard cap 120 000 ms). If everything the code waits on settles within the bound the result is the finished shape { output, result? } \u2014 output is ONE newline-joined string (console lines, checkpoint lines like "checkpoint c9: <question>", error renderings), result the completion value\'s repr. If the bound elapses first the result is the still-running shape { output, running: [call ids] } and the eval continues server-side \u2014 any later eval drains what settled, and eval with "" (the empty script) is the documented idempotent poll: it re-executes nothing, only reports. State survives MCP-session churn and daemon restarts: every eval and every settlement drain that changed state persists the workspace to the daemon\'s per-project repl store, and the first touch of a stored workspace restores it and reconciles every outstanding call. A stored snapshot that refuses (corrupt, a format upgrade, a wasm-binary mismatch) AUTO-RESETS \u2014 the file is renamed aside, never deleted, and the next eval\'s output leads with a notice naming the file and reason. Reconcile reports and drain errors live in workspace().diagnostics. On last-client disconnect the workspace drains in-flight subagent turns to completion and closes idle children; the next eligible queued turn re-attaches its founding session lazily. Subagent output passes through UNFILTERED \u2014 backend harness noise (e.g. codex\'s "Warning: Skill descriptions were shortened\u2026") is forwarded verbatim, never curated away. Every result carries the machine-readable shape (see the output schema) as structuredContent alongside the human text.',
|
|
33024
33196
|
// STRICT at the wire too: the MCP SDK strips unknown keys from a
|
|
33025
33197
|
// non-strict object schema before the handler runs, so a deleted
|
|
33026
33198
|
// surface like `refs` would be silently discarded instead of
|
|
@@ -33628,6 +33800,7 @@ function projectHarnessOptions(harnesses) {
|
|
|
33628
33800
|
const options = harness.options ?? [];
|
|
33629
33801
|
return boundValue({
|
|
33630
33802
|
backendId: harness.backendId,
|
|
33803
|
+
defaultModeId: harness.defaultModeId,
|
|
33631
33804
|
model: harness.model,
|
|
33632
33805
|
probed: harness.probed,
|
|
33633
33806
|
error: harness.error,
|
|
@@ -33674,14 +33847,16 @@ import {
|
|
|
33674
33847
|
WorkflowManager as WorkflowManager2
|
|
33675
33848
|
} from "@automatalabs/workflows";
|
|
33676
33849
|
var SCRIPT_RESOURCE_MIME_TYPE = "text/javascript";
|
|
33850
|
+
var RESULT_RESOURCE_MIME_TYPE = "application/json";
|
|
33677
33851
|
var SCRIPT_RESOURCE_LIST_LIMIT = 50;
|
|
33678
33852
|
var EVENTS_RESOURCE_MIME_TYPE = "application/json";
|
|
33679
33853
|
var WORKFLOW_RUN_EVENTS_SCHEMA_VERSION = 1;
|
|
33680
33854
|
var SCRIPT_URI_PATTERN = /^workflow:\/\/runs\/([a-z0-9]+-[a-z0-9]+)\/script$/;
|
|
33855
|
+
var RESULT_URI_PATTERN = /^workflow:\/\/runs\/([a-z0-9]+-[a-z0-9]+)\/result$/;
|
|
33681
33856
|
var EVENTS_URI_PATTERN = /^workflow:\/\/runs\/([a-z0-9]+-[a-z0-9]+)\/events(?:\?([^#]*))?$/;
|
|
33682
33857
|
var STREAM_ID_PATTERN = /^[0-9a-f]{32}$/;
|
|
33683
33858
|
function resourceNotFound(uri) {
|
|
33684
|
-
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Workflow
|
|
33859
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Workflow resource not found: ${uri}`);
|
|
33685
33860
|
}
|
|
33686
33861
|
function workflowScriptUri(runId) {
|
|
33687
33862
|
return `workflow://runs/${runId}/script`;
|
|
@@ -33689,9 +33864,18 @@ function workflowScriptUri(runId) {
|
|
|
33689
33864
|
function workflowRunIdFromScriptUri(uri) {
|
|
33690
33865
|
return SCRIPT_URI_PATTERN.exec(uri)?.[1];
|
|
33691
33866
|
}
|
|
33867
|
+
function workflowResultUri(runId) {
|
|
33868
|
+
return `workflow://runs/${runId}/result`;
|
|
33869
|
+
}
|
|
33870
|
+
function workflowRunIdFromResultUri(uri) {
|
|
33871
|
+
return RESULT_URI_PATTERN.exec(uri)?.[1];
|
|
33872
|
+
}
|
|
33692
33873
|
function workflowRunEventsUri(runId) {
|
|
33693
33874
|
return `workflow://runs/${runId}/events`;
|
|
33694
33875
|
}
|
|
33876
|
+
function hasExactResult(state) {
|
|
33877
|
+
return state.status === "completed" && state.result !== void 0;
|
|
33878
|
+
}
|
|
33695
33879
|
function parseDecimal(value, max = Number.MAX_SAFE_INTEGER) {
|
|
33696
33880
|
if (!/^(?:0|[1-9][0-9]*)$/.test(value)) return void 0;
|
|
33697
33881
|
const parsed = Number(value);
|
|
@@ -33758,6 +33942,15 @@ var WorkflowScriptResources = class {
|
|
|
33758
33942
|
this.modernNotifier = modernNotifier;
|
|
33759
33943
|
this.router = source instanceof WorkflowManager2 ? singleStoreRouter(source) : source.router;
|
|
33760
33944
|
this.detachRunDeleted = this.router.onRunDeleted(this.onRunDeleted);
|
|
33945
|
+
this.detachRunEventPersisted = this.modernNotifier ? () => void 0 : this.router.onRunEventPersisted((record2) => {
|
|
33946
|
+
if (record2.event.type !== "complete") return;
|
|
33947
|
+
queueMicrotask(() => {
|
|
33948
|
+
try {
|
|
33949
|
+
if (this.availableResultUri(record2.runId)) void this.mcp.sendResourceListChanged();
|
|
33950
|
+
} catch {
|
|
33951
|
+
}
|
|
33952
|
+
});
|
|
33953
|
+
});
|
|
33761
33954
|
this.detachRunStopped = this.router.onRunStopped(({ runId }) => this.cancelPendingElicitation(runId));
|
|
33762
33955
|
const previousOnClose = this.mcp.server.onclose;
|
|
33763
33956
|
this.mcp.server.onclose = () => {
|
|
@@ -33765,6 +33958,7 @@ var WorkflowScriptResources = class {
|
|
|
33765
33958
|
this.elicitationControllers.clear();
|
|
33766
33959
|
for (const uri of [...this.eventSubscriptions.keys()]) this.closeEventSubscription(uri);
|
|
33767
33960
|
this.detachRunStopped();
|
|
33961
|
+
this.detachRunEventPersisted();
|
|
33768
33962
|
this.detachRunDeleted();
|
|
33769
33963
|
previousOnClose?.();
|
|
33770
33964
|
};
|
|
@@ -33774,6 +33968,7 @@ var WorkflowScriptResources = class {
|
|
|
33774
33968
|
modernNotifier;
|
|
33775
33969
|
router;
|
|
33776
33970
|
detachRunDeleted;
|
|
33971
|
+
detachRunEventPersisted;
|
|
33777
33972
|
detachRunStopped;
|
|
33778
33973
|
subscriptions = /* @__PURE__ */ new Set();
|
|
33779
33974
|
externalReaders = /* @__PURE__ */ new Map();
|
|
@@ -33782,16 +33977,13 @@ var WorkflowScriptResources = class {
|
|
|
33782
33977
|
silentDeletionRunIds = /* @__PURE__ */ new Set();
|
|
33783
33978
|
elicitationControllers = /* @__PURE__ */ new Map();
|
|
33784
33979
|
onRunDeleted = ({ runId }) => {
|
|
33785
|
-
|
|
33786
|
-
this.subscriptions.delete(
|
|
33980
|
+
this.subscriptions.delete(workflowScriptUri(runId));
|
|
33981
|
+
this.subscriptions.delete(workflowResultUri(runId));
|
|
33787
33982
|
this.closeEventSubscription(workflowRunEventsUri(runId));
|
|
33788
33983
|
this.cancelPendingElicitation(runId);
|
|
33789
33984
|
this.deletedRunIds.add(runId);
|
|
33790
33985
|
const notify = !this.silentDeletionRunIds.delete(runId);
|
|
33791
|
-
if (notify)
|
|
33792
|
-
if (this.modernNotifier) this.modernNotifier.resourcesChanged();
|
|
33793
|
-
else void this.mcp.sendResourceListChanged();
|
|
33794
|
-
}
|
|
33986
|
+
if (notify && !this.modernNotifier) void this.mcp.sendResourceListChanged();
|
|
33795
33987
|
};
|
|
33796
33988
|
/** The persistence store containing runId, if any known project store holds it. */
|
|
33797
33989
|
persistenceFor(runId) {
|
|
@@ -33878,6 +34070,57 @@ var WorkflowScriptResources = class {
|
|
|
33878
34070
|
}
|
|
33879
34071
|
return newestToOldest.reverse();
|
|
33880
34072
|
}
|
|
34073
|
+
/** Exact persisted result metadata, available only for completed runs with a JSON value. */
|
|
34074
|
+
serializedResult(runId) {
|
|
34075
|
+
const state = this.loadState(runId);
|
|
34076
|
+
if (!state) {
|
|
34077
|
+
throw new ProtocolError(
|
|
34078
|
+
ProtocolErrorCode.InvalidParams,
|
|
34079
|
+
`No workflow run found for runId "${runId}" in this server's project-scoped run store.`
|
|
34080
|
+
);
|
|
34081
|
+
}
|
|
34082
|
+
if (!hasExactResult(state)) {
|
|
34083
|
+
throw new ProtocolError(
|
|
34084
|
+
ProtocolErrorCode.InvalidParams,
|
|
34085
|
+
state.status === "completed" ? `Workflow run "${runId}" completed without a JSON result.` : `Workflow result for runId "${runId}" is unavailable while the run is ${state.status}.`
|
|
34086
|
+
);
|
|
34087
|
+
}
|
|
34088
|
+
let text;
|
|
34089
|
+
try {
|
|
34090
|
+
text = JSON.stringify(state.result);
|
|
34091
|
+
} catch {
|
|
34092
|
+
throw new ProtocolError(
|
|
34093
|
+
ProtocolErrorCode.InternalError,
|
|
34094
|
+
`Workflow result for runId "${runId}" could not be serialized.`
|
|
34095
|
+
);
|
|
34096
|
+
}
|
|
34097
|
+
if (text === void 0) {
|
|
34098
|
+
throw new ProtocolError(
|
|
34099
|
+
ProtocolErrorCode.InternalError,
|
|
34100
|
+
`Workflow result for runId "${runId}" could not be serialized.`
|
|
34101
|
+
);
|
|
34102
|
+
}
|
|
34103
|
+
return {
|
|
34104
|
+
uri: workflowResultUri(runId),
|
|
34105
|
+
text,
|
|
34106
|
+
bytes: Buffer.byteLength(text, "utf8")
|
|
34107
|
+
};
|
|
34108
|
+
}
|
|
34109
|
+
availableResultUri(runId) {
|
|
34110
|
+
const state = this.loadState(runId);
|
|
34111
|
+
return state && hasExactResult(state) ? workflowResultUri(runId) : void 0;
|
|
34112
|
+
}
|
|
34113
|
+
resultLink(runId) {
|
|
34114
|
+
const state = this.loadState(runId);
|
|
34115
|
+
if (!state || !hasExactResult(state)) return void 0;
|
|
34116
|
+
return {
|
|
34117
|
+
type: "resource_link",
|
|
34118
|
+
uri: workflowResultUri(runId),
|
|
34119
|
+
name: `${state.workflowName} result (${state.runId})`,
|
|
34120
|
+
description: `exact workflow result \xB7 completed ${state.completedAt ?? state.updatedAt}`,
|
|
34121
|
+
mimeType: RESULT_RESOURCE_MIME_TYPE
|
|
34122
|
+
};
|
|
34123
|
+
}
|
|
33881
34124
|
links(lineage) {
|
|
33882
34125
|
const links = [];
|
|
33883
34126
|
for (const entry of lineage) {
|
|
@@ -33887,8 +34130,8 @@ var WorkflowScriptResources = class {
|
|
|
33887
34130
|
links.push({
|
|
33888
34131
|
type: "resource_link",
|
|
33889
34132
|
uri: entry.uri,
|
|
33890
|
-
name: `${state.workflowName} (${state.runId})`,
|
|
33891
|
-
description:
|
|
34133
|
+
name: `${state.workflowName} script (${state.runId})`,
|
|
34134
|
+
description: `workflow script \xB7 ${state.status} \xB7 started ${state.startedAt}`,
|
|
33892
34135
|
mimeType: SCRIPT_RESOURCE_MIME_TYPE
|
|
33893
34136
|
});
|
|
33894
34137
|
}
|
|
@@ -33904,8 +34147,8 @@ var WorkflowScriptResources = class {
|
|
|
33904
34147
|
list: () => ({
|
|
33905
34148
|
resources: this.recentRuns().map((state) => ({
|
|
33906
34149
|
uri: workflowScriptUri(state.runId),
|
|
33907
|
-
name: `${state.workflowName} (${state.runId})`,
|
|
33908
|
-
description:
|
|
34150
|
+
name: `${state.workflowName} script (${state.runId})`,
|
|
34151
|
+
description: `workflow script \xB7 ${state.status} \xB7 started ${state.startedAt}`,
|
|
33909
34152
|
mimeType: SCRIPT_RESOURCE_MIME_TYPE
|
|
33910
34153
|
}))
|
|
33911
34154
|
}),
|
|
@@ -33920,6 +34163,28 @@ var WorkflowScriptResources = class {
|
|
|
33920
34163
|
},
|
|
33921
34164
|
(uri) => this.readResource(uri.toString())
|
|
33922
34165
|
);
|
|
34166
|
+
this.mcp.registerResource(
|
|
34167
|
+
"workflow-run-result",
|
|
34168
|
+
new ResourceTemplate("workflow://runs/{runId}/result", {
|
|
34169
|
+
list: () => ({
|
|
34170
|
+
resources: this.recentRuns().filter(hasExactResult).map((state) => ({
|
|
34171
|
+
uri: workflowResultUri(state.runId),
|
|
34172
|
+
name: `${state.workflowName} result (${state.runId})`,
|
|
34173
|
+
description: `exact workflow result \xB7 completed ${state.completedAt ?? state.updatedAt}`,
|
|
34174
|
+
mimeType: RESULT_RESOURCE_MIME_TYPE
|
|
34175
|
+
}))
|
|
34176
|
+
}),
|
|
34177
|
+
complete: {
|
|
34178
|
+
runId: (partial2) => this.recentRuns().filter(hasExactResult).map((state) => state.runId).filter((runId) => runId.startsWith(partial2))
|
|
34179
|
+
}
|
|
34180
|
+
}),
|
|
34181
|
+
{
|
|
34182
|
+
title: "Workflow run results",
|
|
34183
|
+
description: "Exact JSON results for completed workflow runs. Listing is discovery-only and contains at most the 50 newest runs; direct workflow://runs/{runId}/result reads remain available until run deletion.",
|
|
34184
|
+
mimeType: RESULT_RESOURCE_MIME_TYPE
|
|
34185
|
+
},
|
|
34186
|
+
(uri) => this.readResultResource(uri.toString())
|
|
34187
|
+
);
|
|
33923
34188
|
this.mcp.registerResource(
|
|
33924
34189
|
"workflow-run-events",
|
|
33925
34190
|
new ResourceTemplate("workflow://runs/{runId}/events", {
|
|
@@ -33949,7 +34214,9 @@ var WorkflowScriptResources = class {
|
|
|
33949
34214
|
if (external.available !== void 0 && !external.available(ctx)) resourceNotFound(uri);
|
|
33950
34215
|
return external.read();
|
|
33951
34216
|
}
|
|
33952
|
-
|
|
34217
|
+
if (uri.includes("/events")) return this.readEventsResource(uri);
|
|
34218
|
+
if (workflowRunIdFromResultUri(uri)) return this.readResultResource(uri);
|
|
34219
|
+
return this.readResource(uri);
|
|
33953
34220
|
});
|
|
33954
34221
|
this.mcp.server.setRequestHandler("resources/subscribe", (request, ctx) => {
|
|
33955
34222
|
const uri = request.params.uri;
|
|
@@ -33964,6 +34231,12 @@ var WorkflowScriptResources = class {
|
|
|
33964
34231
|
this.subscriptions.add(uri);
|
|
33965
34232
|
return {};
|
|
33966
34233
|
}
|
|
34234
|
+
const resultRunId = workflowRunIdFromResultUri(uri);
|
|
34235
|
+
if (resultRunId) {
|
|
34236
|
+
if (!this.availableResultUri(resultRunId)) resourceNotFound(uri);
|
|
34237
|
+
this.subscriptions.add(uri);
|
|
34238
|
+
return {};
|
|
34239
|
+
}
|
|
33967
34240
|
if (!uri.includes("/events")) resourceNotFound(uri);
|
|
33968
34241
|
const parsed = parseWorkflowRunEventsUri(uri);
|
|
33969
34242
|
if (!parsed) malformedEventsUri();
|
|
@@ -33977,7 +34250,7 @@ var WorkflowScriptResources = class {
|
|
|
33977
34250
|
if (external.available !== void 0 && !external.available(ctx)) resourceNotFound(uri);
|
|
33978
34251
|
return {};
|
|
33979
34252
|
}
|
|
33980
|
-
const runId = workflowRunIdFromScriptUri(uri);
|
|
34253
|
+
const runId = workflowRunIdFromScriptUri(uri) ?? workflowRunIdFromResultUri(uri);
|
|
33981
34254
|
if (runId) {
|
|
33982
34255
|
if (!this.loadState(runId) && !this.loadTombstone(runId) && !this.deletedRunIds.has(runId) && !this.subscriptions.has(uri)) resourceNotFound(uri);
|
|
33983
34256
|
this.subscriptions.delete(uri);
|
|
@@ -34146,6 +34419,20 @@ var WorkflowScriptResources = class {
|
|
|
34146
34419
|
mapEventError(error51, parsed);
|
|
34147
34420
|
}
|
|
34148
34421
|
}
|
|
34422
|
+
readResultResource(uri) {
|
|
34423
|
+
const runId = workflowRunIdFromResultUri(uri);
|
|
34424
|
+
if (!runId) resourceNotFound(uri);
|
|
34425
|
+
const result = this.serializedResult(runId);
|
|
34426
|
+
return {
|
|
34427
|
+
contents: [
|
|
34428
|
+
{
|
|
34429
|
+
uri: result.uri,
|
|
34430
|
+
mimeType: RESULT_RESOURCE_MIME_TYPE,
|
|
34431
|
+
text: result.text
|
|
34432
|
+
}
|
|
34433
|
+
]
|
|
34434
|
+
};
|
|
34435
|
+
}
|
|
34149
34436
|
readResource(uri) {
|
|
34150
34437
|
const runId = workflowRunIdFromScriptUri(uri);
|
|
34151
34438
|
if (!runId) resourceNotFound(uri);
|
|
@@ -34200,6 +34487,275 @@ function requireDurableStoppedRun(manager, runId) {
|
|
|
34200
34487
|
}
|
|
34201
34488
|
}
|
|
34202
34489
|
|
|
34490
|
+
// ../mcp-server/src/workflow-permissions.ts
|
|
34491
|
+
import { EventEmitter } from "node:events";
|
|
34492
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
34493
|
+
import {
|
|
34494
|
+
decidePermission,
|
|
34495
|
+
redactText as redactText2,
|
|
34496
|
+
truncateUtf8 as truncateUtf83
|
|
34497
|
+
} from "@automatalabs/workflows";
|
|
34498
|
+
var RUN_ID2 = /^[a-z0-9]+-[a-z0-9]+$/;
|
|
34499
|
+
var PERMISSION_ID = /^[0-9a-f-]{36}$/i;
|
|
34500
|
+
var MAX_PUBLIC_REQUEST_BYTES = 64 * 1024;
|
|
34501
|
+
var MAX_PUBLIC_SCALAR_BYTES = 512;
|
|
34502
|
+
var MAX_PERMISSION_OPTIONS = 16;
|
|
34503
|
+
var MAX_OPTION_ID_CODE_UNITS = 512;
|
|
34504
|
+
var MAX_OPTION_ID_BYTES = 2048;
|
|
34505
|
+
var MAX_PUBLIC_ARRAY_ITEMS = 16;
|
|
34506
|
+
var MAX_PUBLIC_OBJECT_KEYS = 20;
|
|
34507
|
+
var MAX_PUBLIC_DEPTH = 4;
|
|
34508
|
+
var SENSITIVE_KEY_PARTS = [
|
|
34509
|
+
"password",
|
|
34510
|
+
"passwd",
|
|
34511
|
+
"secret",
|
|
34512
|
+
"token",
|
|
34513
|
+
"apikey",
|
|
34514
|
+
"credential",
|
|
34515
|
+
"authorization",
|
|
34516
|
+
"cookie",
|
|
34517
|
+
"privatekey"
|
|
34518
|
+
];
|
|
34519
|
+
var PERMISSION_OPTION_KINDS = /* @__PURE__ */ new Set([
|
|
34520
|
+
"allow_once",
|
|
34521
|
+
"allow_always",
|
|
34522
|
+
"reject_once",
|
|
34523
|
+
"reject_always"
|
|
34524
|
+
]);
|
|
34525
|
+
function cloneRequest(request) {
|
|
34526
|
+
return structuredClone(request);
|
|
34527
|
+
}
|
|
34528
|
+
function sensitiveKey(key) {
|
|
34529
|
+
const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
34530
|
+
return SENSITIVE_KEY_PARTS.some((part) => normalized.includes(part));
|
|
34531
|
+
}
|
|
34532
|
+
function sanitizeString(value, state) {
|
|
34533
|
+
const redacted = redactText2(value);
|
|
34534
|
+
const bounded = truncateUtf83(redacted.value, MAX_PUBLIC_SCALAR_BYTES);
|
|
34535
|
+
state.redacted ||= redacted.redacted;
|
|
34536
|
+
state.truncated ||= bounded !== redacted.value;
|
|
34537
|
+
return bounded;
|
|
34538
|
+
}
|
|
34539
|
+
function sanitizeValue(value, state, depth = 0, ancestors = /* @__PURE__ */ new Set()) {
|
|
34540
|
+
if (value === null || typeof value === "boolean" || typeof value === "number") return value;
|
|
34541
|
+
if (typeof value === "string") return sanitizeString(value, state);
|
|
34542
|
+
if (typeof value !== "object") {
|
|
34543
|
+
state.truncated = true;
|
|
34544
|
+
return null;
|
|
34545
|
+
}
|
|
34546
|
+
if (depth >= MAX_PUBLIC_DEPTH || ancestors.has(value)) {
|
|
34547
|
+
state.truncated = true;
|
|
34548
|
+
return depth >= MAX_PUBLIC_DEPTH ? "[max depth]" : "[cycle]";
|
|
34549
|
+
}
|
|
34550
|
+
const nextAncestors = new Set(ancestors);
|
|
34551
|
+
nextAncestors.add(value);
|
|
34552
|
+
if (Array.isArray(value)) {
|
|
34553
|
+
const kept = value.slice(0, MAX_PUBLIC_ARRAY_ITEMS).map(
|
|
34554
|
+
(entry) => sanitizeValue(entry, state, depth + 1, nextAncestors)
|
|
34555
|
+
);
|
|
34556
|
+
if (value.length > MAX_PUBLIC_ARRAY_ITEMS) state.truncated = true;
|
|
34557
|
+
return kept;
|
|
34558
|
+
}
|
|
34559
|
+
const entries = Object.entries(value);
|
|
34560
|
+
const output = {};
|
|
34561
|
+
for (const [key, child] of entries.slice(0, MAX_PUBLIC_OBJECT_KEYS)) {
|
|
34562
|
+
const outwardKey = sanitizeString(key, state);
|
|
34563
|
+
if (Object.hasOwn(output, outwardKey)) {
|
|
34564
|
+
state.truncated = true;
|
|
34565
|
+
continue;
|
|
34566
|
+
}
|
|
34567
|
+
if (sensitiveKey(key)) {
|
|
34568
|
+
output[outwardKey] = "[REDACTED]";
|
|
34569
|
+
state.redacted = true;
|
|
34570
|
+
} else {
|
|
34571
|
+
output[outwardKey] = sanitizeValue(child, state, depth + 1, nextAncestors);
|
|
34572
|
+
}
|
|
34573
|
+
}
|
|
34574
|
+
if (entries.length > MAX_PUBLIC_OBJECT_KEYS) state.truncated = true;
|
|
34575
|
+
return output;
|
|
34576
|
+
}
|
|
34577
|
+
function validMeta(value) {
|
|
34578
|
+
return value === void 0 || value === null || typeof value === "object" && !Array.isArray(value);
|
|
34579
|
+
}
|
|
34580
|
+
function validOption(option) {
|
|
34581
|
+
return typeof option.optionId === "string" && option.optionId.length > 0 && option.optionId.length <= MAX_OPTION_ID_CODE_UNITS && Buffer.byteLength(option.optionId, "utf8") <= MAX_OPTION_ID_BYTES && typeof option.name === "string" && PERMISSION_OPTION_KINDS.has(option.kind) && validMeta(option._meta);
|
|
34582
|
+
}
|
|
34583
|
+
function sanitizeOption(option, state) {
|
|
34584
|
+
return {
|
|
34585
|
+
optionId: option.optionId,
|
|
34586
|
+
name: sanitizeString(option.name, state),
|
|
34587
|
+
kind: option.kind,
|
|
34588
|
+
...option._meta === void 0 ? {} : { _meta: sanitizeValue(option._meta, state) }
|
|
34589
|
+
};
|
|
34590
|
+
}
|
|
34591
|
+
function safeToolCall(toolCall, state) {
|
|
34592
|
+
if (typeof toolCall.toolCallId !== "string" || toolCall.toolCallId.length === 0) return void 0;
|
|
34593
|
+
const sanitized = sanitizeValue(toolCall, state);
|
|
34594
|
+
if (sanitized === null || typeof sanitized !== "object" || Array.isArray(sanitized)) return void 0;
|
|
34595
|
+
return {
|
|
34596
|
+
...sanitized,
|
|
34597
|
+
toolCallId: sanitizeString(toolCall.toolCallId, state)
|
|
34598
|
+
};
|
|
34599
|
+
}
|
|
34600
|
+
function publicRequest(request) {
|
|
34601
|
+
if (!Array.isArray(request.options) || request.options.length === 0 || request.options.length > MAX_PERMISSION_OPTIONS || !request.options.every(validOption) || !validMeta(request._meta)) return void 0;
|
|
34602
|
+
const optionIds = request.options.map((option) => option.optionId);
|
|
34603
|
+
if (new Set(optionIds).size !== optionIds.length) return void 0;
|
|
34604
|
+
const state = { redacted: false, truncated: false };
|
|
34605
|
+
const toolCall = safeToolCall(request.toolCall, state);
|
|
34606
|
+
if (!toolCall) return void 0;
|
|
34607
|
+
const options = request.options.map((option) => sanitizeOption(option, state));
|
|
34608
|
+
const projected = {
|
|
34609
|
+
toolCall,
|
|
34610
|
+
options,
|
|
34611
|
+
...request._meta === void 0 ? {} : { _meta: sanitizeValue(request._meta, state) }
|
|
34612
|
+
};
|
|
34613
|
+
if (Buffer.byteLength(JSON.stringify(projected), "utf8") <= MAX_PUBLIC_REQUEST_BYTES) {
|
|
34614
|
+
return { request: projected, truncated: state.truncated, redacted: state.redacted };
|
|
34615
|
+
}
|
|
34616
|
+
const minimal = {
|
|
34617
|
+
toolCall: {
|
|
34618
|
+
toolCallId: projected.toolCall.toolCallId,
|
|
34619
|
+
...projected.toolCall.title === void 0 ? {} : { title: projected.toolCall.title },
|
|
34620
|
+
...projected.toolCall.name === void 0 ? {} : { name: projected.toolCall.name },
|
|
34621
|
+
...projected.toolCall.kind === void 0 ? {} : { kind: projected.toolCall.kind },
|
|
34622
|
+
...projected.toolCall.status === void 0 ? {} : { status: projected.toolCall.status }
|
|
34623
|
+
},
|
|
34624
|
+
options: projected.options.map(({ optionId, name, kind }) => ({ optionId, name, kind }))
|
|
34625
|
+
};
|
|
34626
|
+
if (Buffer.byteLength(JSON.stringify(minimal), "utf8") > MAX_PUBLIC_REQUEST_BYTES) return void 0;
|
|
34627
|
+
return { request: minimal, truncated: true, redacted: state.redacted };
|
|
34628
|
+
}
|
|
34629
|
+
function validResponse(response) {
|
|
34630
|
+
if ("_meta" in response) return false;
|
|
34631
|
+
if (response.outcome.outcome === "cancelled") return true;
|
|
34632
|
+
return typeof response.outcome.optionId === "string" && response.outcome.optionId.length > 0 && response.outcome.optionId.length <= MAX_OPTION_ID_CODE_UNITS && Buffer.byteLength(response.outcome.optionId, "utf8") <= MAX_OPTION_ID_BYTES;
|
|
34633
|
+
}
|
|
34634
|
+
var WorkflowPermissionBroker = class {
|
|
34635
|
+
byId = /* @__PURE__ */ new Map();
|
|
34636
|
+
idsByRun = /* @__PURE__ */ new Map();
|
|
34637
|
+
changed = new EventEmitter();
|
|
34638
|
+
detachEvents;
|
|
34639
|
+
resolver = (request, context) => {
|
|
34640
|
+
if (context.backendId === "pi" || context.runId === void 0 || !RUN_ID2.test(context.runId) || context.callIndex === void 0 || !Number.isSafeInteger(context.callIndex) || context.callIndex < 0) {
|
|
34641
|
+
return decidePermission(request, {});
|
|
34642
|
+
}
|
|
34643
|
+
return this.park(request, context);
|
|
34644
|
+
};
|
|
34645
|
+
attach(source) {
|
|
34646
|
+
this.detachEvents?.();
|
|
34647
|
+
this.detachEvents = source.on("permission_request", (event) => this.observeFinalOutcome(event));
|
|
34648
|
+
}
|
|
34649
|
+
dispose() {
|
|
34650
|
+
this.detachEvents?.();
|
|
34651
|
+
this.detachEvents = void 0;
|
|
34652
|
+
for (const entry of [...this.byId.values()]) {
|
|
34653
|
+
this.finish(entry, { outcome: { outcome: "cancelled" } });
|
|
34654
|
+
}
|
|
34655
|
+
this.changed.removeAllListeners();
|
|
34656
|
+
}
|
|
34657
|
+
list(runId) {
|
|
34658
|
+
const ids = this.idsByRun.get(runId);
|
|
34659
|
+
if (!ids) return [];
|
|
34660
|
+
return [...ids].map((id) => this.byId.get(id)?.public).filter((entry) => entry !== void 0).sort(
|
|
34661
|
+
(left, right) => left.callIndex - right.callIndex || left.requestedAt.localeCompare(right.requestedAt) || left.permissionId.localeCompare(right.permissionId)
|
|
34662
|
+
).map((entry) => structuredClone(entry));
|
|
34663
|
+
}
|
|
34664
|
+
has(runId, permissionId) {
|
|
34665
|
+
if (permissionId !== void 0) return this.byId.get(permissionId)?.public.runId === runId;
|
|
34666
|
+
return (this.idsByRun.get(runId)?.size ?? 0) > 0;
|
|
34667
|
+
}
|
|
34668
|
+
async waitForPending(runId) {
|
|
34669
|
+
if (this.has(runId)) return;
|
|
34670
|
+
await new Promise((resolve) => {
|
|
34671
|
+
const eventName = `pending:${runId}`;
|
|
34672
|
+
const done = () => {
|
|
34673
|
+
this.changed.off(eventName, done);
|
|
34674
|
+
resolve();
|
|
34675
|
+
};
|
|
34676
|
+
this.changed.on(eventName, done);
|
|
34677
|
+
if (this.has(runId)) done();
|
|
34678
|
+
});
|
|
34679
|
+
}
|
|
34680
|
+
respond(runId, permissionId, response) {
|
|
34681
|
+
if (!RUN_ID2.test(runId) || !PERMISSION_ID.test(permissionId)) {
|
|
34682
|
+
throw new TypeError("Invalid workflow permission identity");
|
|
34683
|
+
}
|
|
34684
|
+
if (!validResponse(response)) throw new TypeError("Invalid workflow permission response");
|
|
34685
|
+
const entry = this.byId.get(permissionId);
|
|
34686
|
+
if (!entry || entry.public.runId !== runId) {
|
|
34687
|
+
throw new TypeError(`Permission request "${permissionId}" is not pending for run "${runId}"`);
|
|
34688
|
+
}
|
|
34689
|
+
if (response.outcome.outcome === "selected") {
|
|
34690
|
+
const selectedOptionId = response.outcome.optionId;
|
|
34691
|
+
if (!entry.request.options.some((option) => option.optionId === selectedOptionId)) {
|
|
34692
|
+
throw new TypeError(
|
|
34693
|
+
`Permission option ${JSON.stringify(selectedOptionId)} was not advertised by request "${permissionId}"`
|
|
34694
|
+
);
|
|
34695
|
+
}
|
|
34696
|
+
}
|
|
34697
|
+
const accepted = structuredClone(response);
|
|
34698
|
+
const acknowledgement = {
|
|
34699
|
+
permissionId,
|
|
34700
|
+
runId,
|
|
34701
|
+
callIndex: entry.public.callIndex,
|
|
34702
|
+
outcome: structuredClone(accepted.outcome),
|
|
34703
|
+
respondedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
34704
|
+
};
|
|
34705
|
+
this.finish(entry, accepted);
|
|
34706
|
+
return acknowledgement;
|
|
34707
|
+
}
|
|
34708
|
+
park(request, context) {
|
|
34709
|
+
const projection = publicRequest(request);
|
|
34710
|
+
if (!projection) return Promise.resolve({ outcome: { outcome: "cancelled" } });
|
|
34711
|
+
const permissionId = randomUUID2();
|
|
34712
|
+
return new Promise((resolve) => {
|
|
34713
|
+
const entry = {
|
|
34714
|
+
request: cloneRequest(request),
|
|
34715
|
+
public: {
|
|
34716
|
+
version: 1,
|
|
34717
|
+
permissionId,
|
|
34718
|
+
runId: context.runId,
|
|
34719
|
+
callIndex: context.callIndex,
|
|
34720
|
+
backendId: context.backendId,
|
|
34721
|
+
...context.label === void 0 ? {} : { label: context.label },
|
|
34722
|
+
requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
34723
|
+
request: projection.request,
|
|
34724
|
+
requestTruncated: projection.truncated,
|
|
34725
|
+
requestRedacted: projection.redacted
|
|
34726
|
+
},
|
|
34727
|
+
settle: resolve
|
|
34728
|
+
};
|
|
34729
|
+
this.byId.set(permissionId, entry);
|
|
34730
|
+
const runIds = this.idsByRun.get(context.runId) ?? /* @__PURE__ */ new Set();
|
|
34731
|
+
runIds.add(permissionId);
|
|
34732
|
+
this.idsByRun.set(context.runId, runIds);
|
|
34733
|
+
this.changed.emit(`run:${context.runId}`);
|
|
34734
|
+
this.changed.emit(`pending:${context.runId}`);
|
|
34735
|
+
});
|
|
34736
|
+
}
|
|
34737
|
+
observeFinalOutcome(event) {
|
|
34738
|
+
for (const entry of this.byId.values()) {
|
|
34739
|
+
if (entry.public.backendId === event.backendId && entry.request.sessionId === event.sessionId && entry.request.toolCall.toolCallId === event.request.toolCall.toolCallId) {
|
|
34740
|
+
this.finish(entry, event.outcome);
|
|
34741
|
+
return;
|
|
34742
|
+
}
|
|
34743
|
+
}
|
|
34744
|
+
}
|
|
34745
|
+
finish(entry, response) {
|
|
34746
|
+
this.remove(entry);
|
|
34747
|
+
entry.settle(response);
|
|
34748
|
+
}
|
|
34749
|
+
remove(entry) {
|
|
34750
|
+
const { permissionId, runId } = entry.public;
|
|
34751
|
+
if (!this.byId.delete(permissionId)) return;
|
|
34752
|
+
const ids = this.idsByRun.get(runId);
|
|
34753
|
+
ids?.delete(permissionId);
|
|
34754
|
+
if (ids?.size === 0) this.idsByRun.delete(runId);
|
|
34755
|
+
this.changed.emit(`run:${runId}`);
|
|
34756
|
+
}
|
|
34757
|
+
};
|
|
34758
|
+
|
|
34203
34759
|
// ../mcp-server/src/server.ts
|
|
34204
34760
|
var SERVER_NAME = "agentprism-workflow";
|
|
34205
34761
|
var DEFAULT_REQUEST_STATE_CODEC = createRequestStateCodec({
|
|
@@ -34207,11 +34763,11 @@ var DEFAULT_REQUEST_STATE_CODEC = createRequestStateCodec({
|
|
|
34207
34763
|
bind: (ctx) => ctx.mcpReq.method
|
|
34208
34764
|
});
|
|
34209
34765
|
var require2 = createRequire(import.meta.url);
|
|
34210
|
-
var SERVER_VERSION = true ? "0.
|
|
34766
|
+
var SERVER_VERSION = true ? "0.38.2" : require2("../package.json").version;
|
|
34211
34767
|
var SERVER_INSTRUCTIONS = [
|
|
34212
34768
|
"This server exposes three model-facing tools for authoring and orchestrating multi-agent work. workflow and repl spawn subagents over the same ACP backends \u2014 the registry built-ins Claude, Codex, OpenCode, and pi, plus any registered custom agents \u2014 and key their durable state by an absolute projectDir (required on the shared daemon; defaults to the server's own project in single-project mode). Backend credentials come from each agent's own login (claude, codex, opencode, pi), so there is nothing auth-shaped to configure here.",
|
|
34213
34769
|
'\u2022 docs \u2014 SELECTIVE VERSION-MATCHED REFERENCE. Omit topic or use topic:"index" for the bounded catalog, then read exactly one workflow/* or repl/* topic. It embeds the selected text/markdown resource, runs no code, opens no backend, and needs no projectDir. Use it when the compact tool descriptions do not contain enough syntax or lifecycle detail.',
|
|
34214
|
-
'\u2022 workflow \u2014 DETERMINISTIC BATCH orchestration. Supply a JavaScript workflow script (inline or by absolute scriptPath) that fans out agent() subagents and optional checkpoint() gates; it runs to completion in the foreground, or background:true returns a durable runId for bounded action:"await"/"inspect"/"stop" calls, with journaling, replay, and resumeFromRunId. Reach for it when the orchestration is known up front and you want it repeatable and resumable. action:"config" discovers the live backend/model option catalog without starting a run, and every run is statically checked, mock-executed, and config-probed before admission. Read docs topic workflow/quickstart first when authoring is unfamiliar.',
|
|
34770
|
+
'\u2022 workflow \u2014 DETERMINISTIC BATCH orchestration. Supply a JavaScript workflow script (inline or by absolute scriptPath) that fans out agent() subagents and optional checkpoint() gates; it runs to completion in the foreground, or background:true returns a durable runId for bounded action:"await"/"inspect"/"permissions-response"/"stop" calls, with journaling, replay, and resumeFromRunId. Await/inspect surface exact live ACP permission options when an agent needs external action. Reach for it when the orchestration is known up front and you want it repeatable and resumable. action:"config" discovers the live backend/model option catalog without starting a run, and every run is statically checked, mock-executed, and config-probed before admission. Read docs topic workflow/quickstart first when authoring is unfamiliar.',
|
|
34215
34771
|
'\u2022 repl \u2014 INTERACTIVE STATEFUL orchestration. A persistent per-project JavaScript VM you drive incrementally with action:"eval"; named bindings, pending subagent calls, raised checkpoints, and `_` (the previous eval\'s completion value) persist between calls and survive daemon restarts. Console logging produces output text only and creates no persistent value. Reach for it when you want to inspect intermediate results and decide the next step adaptively, or keep a human in the loop via checkpoint(). Read docs topic repl/quickstart first when the persistent handle API is unfamiliar.',
|
|
34216
34772
|
"Rule of thumb: use workflow when you can script the whole plan ahead of time; use repl when you want a live, stateful session that evolves call by call."
|
|
34217
34773
|
].join("\n\n");
|
|
@@ -34239,6 +34795,30 @@ function isTerminalStatus(status) {
|
|
|
34239
34795
|
function isAlreadyTerminalForStop(status) {
|
|
34240
34796
|
return status === "completed" || status === "failed" || status === "aborted";
|
|
34241
34797
|
}
|
|
34798
|
+
function permissionInteraction(canElicit) {
|
|
34799
|
+
return {
|
|
34800
|
+
permissionRequests: "may-block",
|
|
34801
|
+
collectWith: ["await", "inspect"],
|
|
34802
|
+
respondWith: "permissions-response",
|
|
34803
|
+
elicitation: canElicit ? "available" : "unavailable"
|
|
34804
|
+
};
|
|
34805
|
+
}
|
|
34806
|
+
async function pendingPermissionsForRun(manager, runId, broker, router) {
|
|
34807
|
+
if (manager.getRun(runId)) return broker.list(runId);
|
|
34808
|
+
return router ? await router.listPermissions(manager, runId) : [];
|
|
34809
|
+
}
|
|
34810
|
+
async function respondToPermission(manager, input, broker, router) {
|
|
34811
|
+
if (manager.getRun(input.runId) && broker.has(input.runId, input.permissionId)) {
|
|
34812
|
+
return broker.respond(input.runId, input.permissionId, input.response);
|
|
34813
|
+
}
|
|
34814
|
+
if (!router) {
|
|
34815
|
+
throw new ProtocolError(
|
|
34816
|
+
ProtocolErrorCode.InvalidParams,
|
|
34817
|
+
`Permission request "${input.permissionId}" is not pending in this server process.`
|
|
34818
|
+
);
|
|
34819
|
+
}
|
|
34820
|
+
return await router.respondPermission(manager, input);
|
|
34821
|
+
}
|
|
34242
34822
|
function readCheckpointDefault(options) {
|
|
34243
34823
|
if (options && typeof options === "object" && "default" in options) {
|
|
34244
34824
|
return options.default;
|
|
@@ -34324,6 +34904,55 @@ function createCheckpointElicitation(prompt, options) {
|
|
|
34324
34904
|
}
|
|
34325
34905
|
};
|
|
34326
34906
|
}
|
|
34907
|
+
function createPermissionElicitation(permission) {
|
|
34908
|
+
const tool = permission.request.toolCall;
|
|
34909
|
+
const title = typeof tool.title === "string" && tool.title.trim() !== "" ? tool.title : `${tool.kind ?? "tool"} request`;
|
|
34910
|
+
const optionLines = permission.request.options.map(
|
|
34911
|
+
(option) => `- ${option.optionId}: ${option.name} (${option.kind})`
|
|
34912
|
+
);
|
|
34913
|
+
return {
|
|
34914
|
+
mode: "form",
|
|
34915
|
+
message: `Workflow agent ${permission.label ? JSON.stringify(permission.label) : `call ${permission.callIndex}`} on ${permission.backendId} requests permission for: ${title}
|
|
34916
|
+
|
|
34917
|
+
${optionLines.join("\n")}
|
|
34918
|
+
|
|
34919
|
+
Select one exact advertised option.`,
|
|
34920
|
+
requestedSchema: {
|
|
34921
|
+
type: "object",
|
|
34922
|
+
properties: {
|
|
34923
|
+
optionId: {
|
|
34924
|
+
type: "string",
|
|
34925
|
+
title: "Permission decision",
|
|
34926
|
+
description: "Exact option advertised by the ACP backend.",
|
|
34927
|
+
enum: permission.request.options.map((option) => option.optionId)
|
|
34928
|
+
}
|
|
34929
|
+
},
|
|
34930
|
+
required: ["optionId"]
|
|
34931
|
+
}
|
|
34932
|
+
};
|
|
34933
|
+
}
|
|
34934
|
+
function formatPendingPermissions(permissions) {
|
|
34935
|
+
if (permissions.length === 0) return "";
|
|
34936
|
+
const lines = [
|
|
34937
|
+
`${permissions.length} workflow permission request(s) require a response:`,
|
|
34938
|
+
...permissions.map((permission) => {
|
|
34939
|
+
const title = permission.request.toolCall.title ?? permission.request.toolCall.kind ?? "tool request";
|
|
34940
|
+
const options = permission.request.options.map((option) => option.optionId).join(", ");
|
|
34941
|
+
return `- ${permission.permissionId} call ${permission.callIndex} (${permission.backendId}) ${title}; options: ${options}`;
|
|
34942
|
+
}),
|
|
34943
|
+
`Use action="permissions-response" with runId, permissionId, and an exact selected optionId or cancelled outcome.`
|
|
34944
|
+
];
|
|
34945
|
+
return truncateUtf84(`
|
|
34946
|
+
${lines.join("\n")}`, 8192, "\u2026[permission summary truncated]");
|
|
34947
|
+
}
|
|
34948
|
+
function permissionResponseFromElicitation(permission, response) {
|
|
34949
|
+
if (response.action !== "accept") return { outcome: { outcome: "cancelled" } };
|
|
34950
|
+
const optionId = response.content?.optionId;
|
|
34951
|
+
if (typeof optionId !== "string" || !permission.request.options.some((option) => option.optionId === optionId)) {
|
|
34952
|
+
return { outcome: { outcome: "cancelled" } };
|
|
34953
|
+
}
|
|
34954
|
+
return { outcome: { outcome: "selected", optionId } };
|
|
34955
|
+
}
|
|
34327
34956
|
function acceptedCheckpointReply(content, options, headlessReply) {
|
|
34328
34957
|
const kind = readCheckpointKind(options);
|
|
34329
34958
|
if (kind === "input") {
|
|
@@ -34445,6 +35074,19 @@ function parseWorkflowRequestState(value, inputHash) {
|
|
|
34445
35074
|
pendingKey: state.pendingKey
|
|
34446
35075
|
};
|
|
34447
35076
|
}
|
|
35077
|
+
if (state.flow === "permission") {
|
|
35078
|
+
if (typeof state.runId !== "string" || !/^[a-z0-9]+-[a-z0-9]+$/.test(state.runId) || typeof state.permissionId !== "string" || !/^[0-9a-f-]{36}$/i.test(state.permissionId)) {
|
|
35079
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, "Invalid workflow permission requestState");
|
|
35080
|
+
}
|
|
35081
|
+
return {
|
|
35082
|
+
version: 1,
|
|
35083
|
+
flow: "permission",
|
|
35084
|
+
inputHash,
|
|
35085
|
+
scriptHash: state.scriptHash,
|
|
35086
|
+
runId: state.runId,
|
|
35087
|
+
permissionId: state.permissionId
|
|
35088
|
+
};
|
|
35089
|
+
}
|
|
34448
35090
|
if (state.flow === "checkpoint") {
|
|
34449
35091
|
if (typeof state.runId !== "string" || !Number.isSafeInteger(state.callIndex) || state.callIndex < 0 || typeof state.checkpointHash !== "string" || !/^[0-9a-f]{64}$/.test(state.checkpointHash) || !Array.isArray(state.approvedKeys) || state.approvedKeys.length > 64 || !state.approvedKeys.every((key) => typeof key === "string" && /^[0-9a-f]{64}$/.test(key)) || state.expiresAt !== void 0 && (typeof state.expiresAt !== "number" || !Number.isFinite(state.expiresAt))) {
|
|
34450
35092
|
throw new ProtocolError(ProtocolErrorCode.InvalidParams, "Invalid workflow checkpoint requestState");
|
|
@@ -34617,10 +35259,10 @@ function formatResumeSummary(eligibility, report) {
|
|
|
34617
35259
|
function formatTerminalSummary(run) {
|
|
34618
35260
|
const lines = [`Workflow run ${run.status}.`, `runId: ${run.runId}`];
|
|
34619
35261
|
if (run.reason) {
|
|
34620
|
-
lines.push(`reason: ${
|
|
35262
|
+
lines.push(`reason: ${truncateUtf84(redactText3(run.reason).value, 512)}`);
|
|
34621
35263
|
}
|
|
34622
35264
|
if (run.resetHint) {
|
|
34623
|
-
lines.push(`reset hint: ${
|
|
35265
|
+
lines.push(`reset hint: ${truncateUtf84(redactText3(run.resetHint).value, 512)}`);
|
|
34624
35266
|
}
|
|
34625
35267
|
if (run.logTail) {
|
|
34626
35268
|
lines.push(`recent run log (last ${run.logTail.lines.length} of ${run.logTail.totalLines}):`);
|
|
@@ -34652,7 +35294,7 @@ function formatTerminalSummary(run) {
|
|
|
34652
35294
|
);
|
|
34653
35295
|
}
|
|
34654
35296
|
}
|
|
34655
|
-
return
|
|
35297
|
+
return truncateUtf84(lines.join("\n"), 12288, "\u2026[text truncated]");
|
|
34656
35298
|
}
|
|
34657
35299
|
function formatRunSummary(run) {
|
|
34658
35300
|
return run.status === "completed" ? formatCompletedSummary(run) : formatTerminalSummary(run);
|
|
@@ -34678,15 +35320,15 @@ function inspectionSummaryLines(status, options = {}) {
|
|
|
34678
35320
|
return lines;
|
|
34679
35321
|
}
|
|
34680
35322
|
function formatInspectionSummary(status) {
|
|
34681
|
-
return
|
|
35323
|
+
return truncateUtf84(inspectionSummaryLines(status).join("\n"), 8192, "\u2026[text truncated]");
|
|
34682
35324
|
}
|
|
34683
35325
|
var MAX_INSPECTION_STRUCTURED_BYTES = 24576;
|
|
34684
35326
|
var MAX_INSPECTION_SCALAR_BYTES = 512;
|
|
34685
35327
|
var MAX_INSPECTION_PHASES = 64;
|
|
34686
35328
|
function retainedInspectionText(value) {
|
|
34687
|
-
const redacted =
|
|
35329
|
+
const redacted = redactText3(value);
|
|
34688
35330
|
return {
|
|
34689
|
-
shortened:
|
|
35331
|
+
shortened: truncateUtf84(redacted.value, MAX_INSPECTION_SCALAR_BYTES) !== redacted.value,
|
|
34690
35332
|
redacted: redacted.redacted
|
|
34691
35333
|
};
|
|
34692
35334
|
}
|
|
@@ -34792,7 +35434,7 @@ function formatStopSummary(result) {
|
|
|
34792
35434
|
"Agent-session cancellation may still be winding down; inspect the per-agent states only if backend cleanup appears hung."
|
|
34793
35435
|
);
|
|
34794
35436
|
}
|
|
34795
|
-
return
|
|
35437
|
+
return truncateUtf84(lines.join("\n"), 8192, "\u2026[text truncated]");
|
|
34796
35438
|
}
|
|
34797
35439
|
function formatPendingStopSummary(result) {
|
|
34798
35440
|
const lines = inspectionSummaryLines(result);
|
|
@@ -34803,7 +35445,7 @@ function formatPendingStopSummary(result) {
|
|
|
34803
35445
|
`Stop request ${result.control.operationId} is durably pending; retry stop, inspect, or await to observe settlement.`,
|
|
34804
35446
|
owner === void 0 ? "No live execution owner is currently discoverable; a later lease holder will apply the intent." : `Execution owner: daemon pid ${owner.pid}${owner.version ? ` v${owner.version}` : ""}${owner.lameDuck ? " (draining)" : ""}.`
|
|
34805
35447
|
);
|
|
34806
|
-
return
|
|
35448
|
+
return truncateUtf84(lines.join("\n"), 8192, "\u2026[text truncated]");
|
|
34807
35449
|
}
|
|
34808
35450
|
function formatAgentCancellationSummary(status, cancellation) {
|
|
34809
35451
|
const lines = inspectionSummaryLines(status);
|
|
@@ -34812,7 +35454,7 @@ function formatAgentCancellationSummary(status, cancellation) {
|
|
|
34812
35454
|
0,
|
|
34813
35455
|
`Agent call ${cancellation.callIndex} ("${cancellation.label}") settled with AGENT_CANCELLED; the workflow run remains live.`
|
|
34814
35456
|
);
|
|
34815
|
-
return
|
|
35457
|
+
return truncateUtf84(lines.join("\n"), 8192, "\u2026[text truncated]");
|
|
34816
35458
|
}
|
|
34817
35459
|
function readScriptAtAdmission(scriptPath) {
|
|
34818
35460
|
try {
|
|
@@ -34861,6 +35503,13 @@ async function settleForegroundRun(manager, started) {
|
|
|
34861
35503
|
throw error51;
|
|
34862
35504
|
}
|
|
34863
35505
|
}
|
|
35506
|
+
async function settleForegroundRunOrPermission(manager, started, broker) {
|
|
35507
|
+
if (broker.has(started.runId)) return { kind: "permission" };
|
|
35508
|
+
return await Promise.race([
|
|
35509
|
+
settleForegroundRun(manager, started).then((run) => ({ kind: "terminal", run })),
|
|
35510
|
+
broker.waitForPending(started.runId).then(() => ({ kind: "permission" }))
|
|
35511
|
+
]);
|
|
35512
|
+
}
|
|
34864
35513
|
function normalizeTokenUsage(usage) {
|
|
34865
35514
|
if (!usage) return void 0;
|
|
34866
35515
|
return {
|
|
@@ -34895,17 +35544,98 @@ function persistedOutcome(persisted, status) {
|
|
|
34895
35544
|
...persisted.checkpointsTaken === void 0 ? {} : { checkpointsTaken: persisted.checkpointsTaken },
|
|
34896
35545
|
...persisted.resumeReport === void 0 ? {} : { resumeReport: persisted.resumeReport },
|
|
34897
35546
|
...persisted.replayEligibility === void 0 ? {} : { replayEligibility: persisted.replayEligibility },
|
|
34898
|
-
scriptUri: workflowScriptUri(persisted.runId)
|
|
35547
|
+
scriptUri: workflowScriptUri(persisted.runId),
|
|
35548
|
+
...status.status === "completed" && persisted.result !== void 0 ? { resultUri: workflowResultUri(persisted.runId) } : {}
|
|
34899
35549
|
};
|
|
34900
35550
|
}
|
|
34901
35551
|
function terminalOutcome(manager, runId, status) {
|
|
35552
|
+
const persisted = manager.getPersistence().load(runId);
|
|
35553
|
+
const resultUri = persisted?.status === "completed" && persisted.result !== void 0 ? workflowResultUri(runId) : void 0;
|
|
34902
35554
|
const live = manager.getRun(runId)?.result;
|
|
34903
35555
|
if (live) {
|
|
34904
|
-
return toWorkflowExecutionOutcome(live, {
|
|
35556
|
+
return toWorkflowExecutionOutcome(live, {
|
|
35557
|
+
scriptUri: workflowScriptUri(runId),
|
|
35558
|
+
...resultUri === void 0 ? {} : { resultUri }
|
|
35559
|
+
});
|
|
34905
35560
|
}
|
|
34906
|
-
const persisted = manager.getPersistence().load(runId);
|
|
34907
35561
|
return persisted ? persistedOutcome(persisted, status) : void 0;
|
|
34908
35562
|
}
|
|
35563
|
+
var INLINE_WORKFLOW_RESULT_MAX_BYTES = 4096;
|
|
35564
|
+
function resultResourceFields(resources, runId) {
|
|
35565
|
+
const resultUri = resources.availableResultUri(runId);
|
|
35566
|
+
return resultUri === void 0 ? {} : { resultUri };
|
|
35567
|
+
}
|
|
35568
|
+
function resultContentBlocks(resources, runId, inline) {
|
|
35569
|
+
const link = resources.resultLink(runId);
|
|
35570
|
+
if (!link) return [];
|
|
35571
|
+
const resultUri = link.uri;
|
|
35572
|
+
if (inline) {
|
|
35573
|
+
const result = resources.serializedResult(runId);
|
|
35574
|
+
if (result.bytes <= INLINE_WORKFLOW_RESULT_MAX_BYTES) {
|
|
35575
|
+
return [
|
|
35576
|
+
{
|
|
35577
|
+
type: "text",
|
|
35578
|
+
text: `Workflow result (exact JSON):
|
|
35579
|
+
${result.text}`,
|
|
35580
|
+
annotations: { audience: ["assistant"] }
|
|
35581
|
+
},
|
|
35582
|
+
link
|
|
35583
|
+
];
|
|
35584
|
+
}
|
|
35585
|
+
return [
|
|
35586
|
+
{
|
|
35587
|
+
type: "text",
|
|
35588
|
+
text: `Exact workflow result: ${result.bytes} UTF-8 bytes at ${resultUri}. Read that resource directly, or call workflow with action="result", runId="${runId}", offset=0 and follow endOffset while hasMore is true for bounded exact chunks.`,
|
|
35589
|
+
annotations: { audience: ["assistant"] }
|
|
35590
|
+
},
|
|
35591
|
+
link
|
|
35592
|
+
];
|
|
35593
|
+
}
|
|
35594
|
+
return [
|
|
35595
|
+
{
|
|
35596
|
+
type: "text",
|
|
35597
|
+
text: `Exact workflow result: ${resultUri}. Read that resource directly, or call workflow with action="result", runId="${runId}", offset=0 and follow endOffset while hasMore is true for bounded exact chunks.`,
|
|
35598
|
+
annotations: { audience: ["assistant"] }
|
|
35599
|
+
},
|
|
35600
|
+
link
|
|
35601
|
+
];
|
|
35602
|
+
}
|
|
35603
|
+
function isUtf8ContinuationByte(byte) {
|
|
35604
|
+
return byte !== void 0 && (byte & 192) === 128;
|
|
35605
|
+
}
|
|
35606
|
+
function resultRetrievalPage(resources, runId, offset, maxBytes) {
|
|
35607
|
+
const result = resources.serializedResult(runId);
|
|
35608
|
+
const buffer = Buffer.from(result.text, "utf8");
|
|
35609
|
+
if (offset > buffer.length) {
|
|
35610
|
+
throw new ProtocolError(
|
|
35611
|
+
ProtocolErrorCode.InvalidParams,
|
|
35612
|
+
`Workflow result offset ${offset} exceeds totalBytes ${buffer.length} for runId "${runId}".`
|
|
35613
|
+
);
|
|
35614
|
+
}
|
|
35615
|
+
if (offset < buffer.length && isUtf8ContinuationByte(buffer[offset])) {
|
|
35616
|
+
throw new ProtocolError(
|
|
35617
|
+
ProtocolErrorCode.InvalidParams,
|
|
35618
|
+
`Workflow result offset ${offset} is not a UTF-8 boundary for runId "${runId}"; use the previous endOffset.`
|
|
35619
|
+
);
|
|
35620
|
+
}
|
|
35621
|
+
let endOffset = Math.min(buffer.length, offset + maxBytes);
|
|
35622
|
+
while (endOffset > offset && endOffset < buffer.length && isUtf8ContinuationByte(buffer[endOffset])) {
|
|
35623
|
+
endOffset--;
|
|
35624
|
+
}
|
|
35625
|
+
return {
|
|
35626
|
+
action: "result",
|
|
35627
|
+
runId,
|
|
35628
|
+
status: "completed",
|
|
35629
|
+
resultUri: result.uri,
|
|
35630
|
+
mimeType: RESULT_RESOURCE_MIME_TYPE,
|
|
35631
|
+
encoding: "utf-8",
|
|
35632
|
+
totalBytes: buffer.length,
|
|
35633
|
+
offset,
|
|
35634
|
+
endOffset,
|
|
35635
|
+
hasMore: endOffset < buffer.length,
|
|
35636
|
+
chunk: buffer.subarray(offset, endOffset).toString("utf8")
|
|
35637
|
+
};
|
|
35638
|
+
}
|
|
34909
35639
|
var AWAIT_CANCELLED = /* @__PURE__ */ Symbol("await-cancelled");
|
|
34910
35640
|
var AWAIT_UNKNOWN_RUN = /* @__PURE__ */ Symbol("await-unknown-run");
|
|
34911
35641
|
var EVENT_LOG_POLL_FALLBACK_CODES = /* @__PURE__ */ new Set([
|
|
@@ -34923,15 +35653,18 @@ var EVENT_LOG_POLL_FALLBACK_CODES = /* @__PURE__ */ new Set([
|
|
|
34923
35653
|
]);
|
|
34924
35654
|
var EVENT_LOG_UNKNOWN_RUN_CODES = /* @__PURE__ */ new Set(["RUN_NOT_FOUND", "ORPHANED_LOG"]);
|
|
34925
35655
|
var TERMINAL_RUN_EVENT_TYPES = /* @__PURE__ */ new Set(["complete", "paused", "error", "stopped"]);
|
|
34926
|
-
async function waitForTerminal(manager, runId, waitMs, signal, localPromise, progress) {
|
|
35656
|
+
async function waitForTerminal(manager, runId, waitMs, signal, localPromise, progress, permissionWait, permissionProbe) {
|
|
34927
35657
|
return await new Promise((resolve, reject) => {
|
|
34928
35658
|
let timer;
|
|
34929
35659
|
let poller;
|
|
35660
|
+
let permissionPoller;
|
|
35661
|
+
let permissionProbeActive = false;
|
|
34930
35662
|
let stream;
|
|
34931
35663
|
let done = false;
|
|
34932
35664
|
const cleanup = () => {
|
|
34933
35665
|
if (timer) clearTimeout(timer);
|
|
34934
35666
|
if (poller) clearInterval(poller);
|
|
35667
|
+
if (permissionPoller) clearInterval(permissionPoller);
|
|
34935
35668
|
stream?.close();
|
|
34936
35669
|
signal.removeEventListener("abort", cancelled);
|
|
34937
35670
|
};
|
|
@@ -34984,6 +35717,23 @@ async function waitForTerminal(manager, runId, waitMs, signal, localPromise, pro
|
|
|
34984
35717
|
() => finish("settled")
|
|
34985
35718
|
);
|
|
34986
35719
|
}
|
|
35720
|
+
if (permissionWait) {
|
|
35721
|
+
void permissionWait.then(() => finish("action-required"), () => void 0);
|
|
35722
|
+
}
|
|
35723
|
+
if (permissionProbe) {
|
|
35724
|
+
const probe = async () => {
|
|
35725
|
+
if (done || permissionProbeActive) return;
|
|
35726
|
+
permissionProbeActive = true;
|
|
35727
|
+
try {
|
|
35728
|
+
if (await permissionProbe()) finish("action-required");
|
|
35729
|
+
} catch {
|
|
35730
|
+
} finally {
|
|
35731
|
+
permissionProbeActive = false;
|
|
35732
|
+
}
|
|
35733
|
+
};
|
|
35734
|
+
permissionPoller = setInterval(() => void probe(), 1e3);
|
|
35735
|
+
void probe();
|
|
35736
|
+
}
|
|
34987
35737
|
try {
|
|
34988
35738
|
const persistence = manager.getPersistence();
|
|
34989
35739
|
const snapshot = persistence.load(runId);
|
|
@@ -35060,7 +35810,7 @@ function formatAwaitSummary(result) {
|
|
|
35060
35810
|
}
|
|
35061
35811
|
}
|
|
35062
35812
|
lines.push(...diagnostics);
|
|
35063
|
-
return
|
|
35813
|
+
return truncateUtf84(lines.join("\n"), 8192, "\u2026[text truncated]");
|
|
35064
35814
|
}
|
|
35065
35815
|
function replEvalTimeoutMs() {
|
|
35066
35816
|
const env = process.env.AGENTPRISM_REPL_EVAL_TIMEOUT_MS;
|
|
@@ -35072,6 +35822,7 @@ function replEvalTimeoutMs() {
|
|
|
35072
35822
|
}
|
|
35073
35823
|
function createWorkflowServer(runner, options = {}) {
|
|
35074
35824
|
const requestStateCodec = options.requestStateCodec ?? DEFAULT_REQUEST_STATE_CODEC;
|
|
35825
|
+
const permissionBroker = options.permissionBroker ?? new WorkflowPermissionBroker();
|
|
35075
35826
|
const mcp = new McpServer(
|
|
35076
35827
|
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
35077
35828
|
{
|
|
@@ -35112,7 +35863,7 @@ function createWorkflowServer(runner, options = {}) {
|
|
|
35112
35863
|
const backendApprovals = /* @__PURE__ */ new Set();
|
|
35113
35864
|
const replPresence = options.replPresence ?? new ReplPresenceLedger(options.replDrainBoundMs ?? REPL_DRAIN_BOUND_MS);
|
|
35114
35865
|
const resolveContext2 = (input) => {
|
|
35115
|
-
if (input.action === "inspect" || input.action === "await" || input.action === "stop") {
|
|
35866
|
+
if (input.action === "inspect" || input.action === "await" || input.action === "stop" || input.action === "permissions-response") {
|
|
35116
35867
|
return projects.storeFor(input.runId) ?? defaultContext;
|
|
35117
35868
|
}
|
|
35118
35869
|
if (input.projectDir !== void 0) {
|
|
@@ -35140,8 +35891,8 @@ function createWorkflowServer(runner, options = {}) {
|
|
|
35140
35891
|
const workflowToolInputSchema = external_exports.object(workflowToolInputShape);
|
|
35141
35892
|
const workflowToolOutputSchema = workflowToolOutputShape;
|
|
35142
35893
|
const workflowToolConfig = {
|
|
35143
|
-
title: "Discover, validate, run, inspect, await, stop, or narrow-cancel
|
|
35144
|
-
description: 'Author and operate JavaScript agent workflows through one project-scoped tool. A script\'s first statement must be `export const meta = { name, description, phases? }`. When present, phases must be an array of objects shaped `{ title: string, detail?: string, model?: string }`, never an array of strings. Inside the deterministic script realm use agent(prompt, options?) for one subagent; parallel([thunks]) for a barrier; pipeline(items, ...stages) for streaming stages; checkpoint(prompt, options?) for a human gate; phase(title) and log(message) for progress; and return the final JSON-serializable value. Top-level await is supported. Imports, require, network APIs, Date.now(), and Math.random() are unavailable. Always label agent calls; schema is a plain JSON Schema object for structured results. The only agent option keys are label, phase, model, tier, mode, configOptions, schema, cwd, timeoutMs, idleTimeoutMs, retries, isolation:"worktree", resume, agentType, mcpServers, images, meta, promptMeta, and keepSession; unknown keys reject before admission. Every parallel entry must be a thunk: parallel([() => agent(...), () => agent(...)]). For deeper syntax, read docs topic workflow/quickstart and then one related workflow/* topic. Minimal script: `export const meta = { name: "review", description: "Review a target", phases: [{ title: "Review" }] }; phase("Review"); const report = await agent("Review " + args.target, { label: "review" }); return { report };`. Omit model for the server default (explicit AGENTPRISM_DEFAULT_BACKEND, else a zero-token auto-selected project pin), or use a backend name alone to preserve that backend\'s configured default. Before choosing a pinned model, mode, or configOptions, call action:"config" with projectDir and optional harnesses/modelFilter; after choosing a model, pass modelSpecs to read its model-specific options.
|
|
35894
|
+
title: "Discover, validate, run, inspect, await, answer permissions, stop, or narrow-cancel a workflow",
|
|
35895
|
+
description: 'Author and operate JavaScript agent workflows through one project-scoped tool. A script\'s first statement must be `export const meta = { name, description, phases? }`. When present, phases must be an array of objects shaped `{ title: string, detail?: string, model?: string }`, never an array of strings. Inside the deterministic script realm use agent(prompt, options?) for one subagent; parallel([thunks]) for a barrier; pipeline(items, ...stages) for streaming stages; checkpoint(prompt, options?) for a human gate; phase(title) and log(message) for progress; and return the final JSON-serializable value. Top-level await is supported. Imports, require, network APIs, Date.now(), and Math.random() are unavailable. Always label agent calls; schema is a plain JSON Schema object for structured results. The only agent option keys are label, phase, model, tier, mode, configOptions, schema, cwd, timeoutMs, idleTimeoutMs, retries, isolation:"worktree", resume, agentType, mcpServers, images, meta, promptMeta, and keepSession; unknown keys reject before admission. Every parallel entry must be a thunk: parallel([() => agent(...), () => agent(...)]). For deeper syntax, read docs topic workflow/quickstart and then one related workflow/* topic. Minimal script: `export const meta = { name: "review", description: "Review a target", phases: [{ title: "Review" }] }; phase("Review"); const report = await agent("Review " + args.target, { label: "review" }); return { report };`. Omit model for the server default (explicit AGENTPRISM_DEFAULT_BACKEND, else a zero-token auto-selected project pin), or use a backend name alone to preserve that backend\'s configured default. Before choosing a pinned model, mode, or configOptions, call action:"config" with projectDir and optional harnesses/modelFilter; after choosing a model, pass modelSpecs to read its model-specific options. Config returns every harness-advertised mode name, description, and metadata plus AgentPrism\'s omitted-mode default: Claude auto, Codex agent, OpenCode build, and no Pi mode. Pin only exact advertised ids. Config opens no-prompt sessions, spends zero tokens, and starts no workflow. action:"run" automatically performs static validation, a mocked dry run, and routed config checks before admission. Invalid scripts return bounded diagnostics with status:"rejected" and create no run ID, reserve no background slot, and spend no tokens. Run, resume, inspect, await, retrieve an exact completed result, answer a live ACP permission, or stop an admitted workflow through the same tool. The script orchestrates agent() subagents (and optional checkpoint() gates) over registry built-ins\u2014currently Claude, Codex, OpenCode, and pi\u2014ACP backends, plus registered custom agents. Supply exactly one of inline script or absolute scriptPath; path content is read once and snapshotted at admission. ' + (requireProjectDir ? "config and run REQUIRE projectDir (absolute): it is the discovery cwd and selects the project-scoped run store/default execution cwd. " : "run optionally takes projectDir (absolute) to select the project-scoped run store; default is this server's own project. ") + `inspect/await/result/stop/permissions-response locate the project store from runId and never accept projectDir. Foreground is the default and streams progress; background:true returns a durable runId for bounded action:"await" calls. run and await honor _meta.progressToken with notifications/progress while they block. Pass resumeFromRunId to execute a new run from a prior journal prefix. In hosts that render MCP Apps, every call of this tool shows a live self-updating run-monitor panel and the panel reports phase starts, pauses, and terminal outcomes on its own \u2014 do NOT poll action:"inspect" to check on a run there; prefer a single bounded action:"await". Use action:"inspect" with a runId when you need machine-readable status data: a safe bounded status, log tail, attributed call previews, and pending ACP permissions. Await returns early with action-required when one appears. Elicitation-capable hosts can present the exact backend options; otherwise use action:"permissions-response" with the returned permissionId and an exact advertised optionId or cancelled outcome. Use action:"stop" to durably abort through the run's execution owner; cross-generation control may return a durable pending operationId before final settlement. Add callIndex to cancel only that live agent and keep the run live. forceOwner explicitly authorizes terminating a superseded owner and is forbidden with callIndex. labelGlob remains an output filter. A final whole-run stop makes resume safe immediately; pending control must be retried or observed with inspect/await. Every admitted script is readable at workflow://runs/{runId}/script. Completed JSON results are readable at workflow://runs/{runId}/result; small results are also copied into text for content-first hosts, while large results can be paged exactly with action:"result", offset, and maxBytes. Result and script resource links are labelled separately. Background runs are tracked per project, capped at four active/starting runs, and use headless checkpoint semantics; checkpointReplies continue a checkpoint pause in a new run.`,
|
|
35145
35896
|
inputSchema: workflowToolInputSchema,
|
|
35146
35897
|
outputSchema: workflowToolOutputSchema,
|
|
35147
35898
|
annotations: void 0
|
|
@@ -35158,7 +35909,7 @@ function createWorkflowServer(runner, options = {}) {
|
|
|
35158
35909
|
let parsedInput = parseWorkflowToolInput(args, { requireProjectDir });
|
|
35159
35910
|
const approvedBackendKeys = /* @__PURE__ */ new Set();
|
|
35160
35911
|
let declinedBackendKey;
|
|
35161
|
-
if (requestState !== void 0) {
|
|
35912
|
+
if (requestState !== void 0 && "approvedKeys" in requestState) {
|
|
35162
35913
|
for (const key of requestState.approvedKeys) approvedBackendKeys.add(key);
|
|
35163
35914
|
}
|
|
35164
35915
|
if (requestState?.flow === "backend-approval") {
|
|
@@ -35231,6 +35982,86 @@ function createWorkflowServer(runner, options = {}) {
|
|
|
35231
35982
|
replPresence.touch(context.repl, options.replClientId?.() ?? "unknown");
|
|
35232
35983
|
const manager = context.manager;
|
|
35233
35984
|
const backgroundRuns = context.backgroundRuns;
|
|
35985
|
+
if (requestState?.flow === "permission") {
|
|
35986
|
+
if (parsedInput.action !== "inspect" && parsedInput.action !== "await" || parsedInput.runId !== requestState.runId) {
|
|
35987
|
+
throw new ProtocolError(
|
|
35988
|
+
ProtocolErrorCode.InvalidParams,
|
|
35989
|
+
"Invalid workflow permission retry: the original inspect/await arguments must be replayed unchanged"
|
|
35990
|
+
);
|
|
35991
|
+
}
|
|
35992
|
+
const pending = await pendingPermissionsForRun(
|
|
35993
|
+
manager,
|
|
35994
|
+
requestState.runId,
|
|
35995
|
+
permissionBroker,
|
|
35996
|
+
options.runControl
|
|
35997
|
+
);
|
|
35998
|
+
const permission = pending.find((entry) => entry.permissionId === requestState.permissionId);
|
|
35999
|
+
if (!permission) {
|
|
36000
|
+
throw new ProtocolError(
|
|
36001
|
+
ProtocolErrorCode.InvalidParams,
|
|
36002
|
+
`Permission request "${requestState.permissionId}" is no longer pending for run "${requestState.runId}"`
|
|
36003
|
+
);
|
|
36004
|
+
}
|
|
36005
|
+
const input2 = inputResponse(ctx.mcpReq.inputResponses, "permission");
|
|
36006
|
+
if (input2.kind !== "elicit") {
|
|
36007
|
+
return inputRequired({
|
|
36008
|
+
inputRequests: { permission: inputRequired.elicit(createPermissionElicitation(permission)) },
|
|
36009
|
+
requestState: await requestStateCodec.mint(requestState, ctx)
|
|
36010
|
+
});
|
|
36011
|
+
}
|
|
36012
|
+
const response = permissionResponseFromElicitation(permission, input2);
|
|
36013
|
+
const acknowledgement = await respondToPermission(
|
|
36014
|
+
manager,
|
|
36015
|
+
{ runId: requestState.runId, permissionId: requestState.permissionId, response },
|
|
36016
|
+
permissionBroker,
|
|
36017
|
+
options.runControl
|
|
36018
|
+
);
|
|
36019
|
+
const status = manager.inspectRun(requestState.runId, {
|
|
36020
|
+
lastN: parsedInput.lastN,
|
|
36021
|
+
labelGlob: parsedInput.labelGlob,
|
|
36022
|
+
logLines: parsedInput.logLines
|
|
36023
|
+
});
|
|
36024
|
+
if (!status) {
|
|
36025
|
+
throw new ProtocolError(
|
|
36026
|
+
ProtocolErrorCode.InvalidParams,
|
|
36027
|
+
`No workflow run found for runId "${requestState.runId}" after its permission response.`
|
|
36028
|
+
);
|
|
36029
|
+
}
|
|
36030
|
+
const lineage = scriptResources.lineage(requestState.runId);
|
|
36031
|
+
const remaining = await pendingPermissionsForRun(
|
|
36032
|
+
manager,
|
|
36033
|
+
requestState.runId,
|
|
36034
|
+
permissionBroker,
|
|
36035
|
+
options.runControl
|
|
36036
|
+
);
|
|
36037
|
+
const projected = addInspectionResourceFields(
|
|
36038
|
+
status,
|
|
36039
|
+
{
|
|
36040
|
+
scriptUri: workflowScriptUri(requestState.runId),
|
|
36041
|
+
...resultResourceFields(scriptResources, requestState.runId),
|
|
36042
|
+
lineage,
|
|
36043
|
+
pendingPermissions: remaining
|
|
36044
|
+
},
|
|
36045
|
+
inspectionRetentionMetadata(manager, requestState.runId, status)
|
|
36046
|
+
);
|
|
36047
|
+
return {
|
|
36048
|
+
structuredContent: {
|
|
36049
|
+
...projected,
|
|
36050
|
+
permissionResponse: acknowledgement
|
|
36051
|
+
},
|
|
36052
|
+
content: [
|
|
36053
|
+
{
|
|
36054
|
+
type: "text",
|
|
36055
|
+
text: `Permission ${acknowledgement.permissionId} resolved for workflow run ${requestState.runId}.
|
|
36056
|
+
${formatInspectionSummary(projected)}`,
|
|
36057
|
+
annotations: { audience: ["assistant"] }
|
|
36058
|
+
},
|
|
36059
|
+
...resultContentBlocks(scriptResources, requestState.runId, false),
|
|
36060
|
+
...scriptResources.links(lineage)
|
|
36061
|
+
],
|
|
36062
|
+
isError: false
|
|
36063
|
+
};
|
|
36064
|
+
}
|
|
35234
36065
|
if (requestState?.flow === "checkpoint") {
|
|
35235
36066
|
if (parsedInput.action !== void 0 && parsedInput.action !== "run" || parsedInput.background || parsedInput.resumeFromRunId !== void 0 || parsedInput.checkpointReplies !== void 0) {
|
|
35236
36067
|
throw new ProtocolError(
|
|
@@ -35301,7 +36132,137 @@ function createWorkflowServer(runner, options = {}) {
|
|
|
35301
36132
|
}
|
|
35302
36133
|
}
|
|
35303
36134
|
}
|
|
36135
|
+
if (parsedInput.action === "result") {
|
|
36136
|
+
try {
|
|
36137
|
+
const page = resultRetrievalPage(
|
|
36138
|
+
scriptResources,
|
|
36139
|
+
parsedInput.runId,
|
|
36140
|
+
parsedInput.offset ?? 0,
|
|
36141
|
+
parsedInput.maxBytes ?? WORKFLOW_RESULT_CHUNK_BYTES_DEFAULT
|
|
36142
|
+
);
|
|
36143
|
+
const resultLink = scriptResources.resultLink(parsedInput.runId);
|
|
36144
|
+
return {
|
|
36145
|
+
structuredContent: { ...page },
|
|
36146
|
+
content: [
|
|
36147
|
+
{
|
|
36148
|
+
type: "text",
|
|
36149
|
+
text: JSON.stringify(page),
|
|
36150
|
+
annotations: { audience: ["assistant"] }
|
|
36151
|
+
},
|
|
36152
|
+
...resultLink === void 0 ? [] : [resultLink]
|
|
36153
|
+
],
|
|
36154
|
+
isError: false
|
|
36155
|
+
};
|
|
36156
|
+
} catch (error51) {
|
|
36157
|
+
if (!(error51 instanceof ProtocolError)) throw error51;
|
|
36158
|
+
return {
|
|
36159
|
+
content: [{ type: "text", text: error51.message }],
|
|
36160
|
+
isError: true
|
|
36161
|
+
};
|
|
36162
|
+
}
|
|
36163
|
+
}
|
|
36164
|
+
if (parsedInput.action === "permissions-response") {
|
|
36165
|
+
const acknowledgement = await respondToPermission(
|
|
36166
|
+
manager,
|
|
36167
|
+
{
|
|
36168
|
+
runId: parsedInput.runId,
|
|
36169
|
+
permissionId: parsedInput.permissionId,
|
|
36170
|
+
response: parsedInput.response
|
|
36171
|
+
},
|
|
36172
|
+
permissionBroker,
|
|
36173
|
+
options.runControl
|
|
36174
|
+
);
|
|
36175
|
+
const status = manager.inspectRun(parsedInput.runId, { lastN: 20, logLines: 20 });
|
|
36176
|
+
if (!status) {
|
|
36177
|
+
throw new ProtocolError(
|
|
36178
|
+
ProtocolErrorCode.InvalidParams,
|
|
36179
|
+
`No workflow run found for runId "${parsedInput.runId}" after its permission response.`
|
|
36180
|
+
);
|
|
36181
|
+
}
|
|
36182
|
+
const lineage = scriptResources.lineage(parsedInput.runId);
|
|
36183
|
+
const pendingPermissions = await pendingPermissionsForRun(
|
|
36184
|
+
manager,
|
|
36185
|
+
parsedInput.runId,
|
|
36186
|
+
permissionBroker,
|
|
36187
|
+
options.runControl
|
|
36188
|
+
);
|
|
36189
|
+
const projected = addInspectionResourceFields(
|
|
36190
|
+
status,
|
|
36191
|
+
{
|
|
36192
|
+
scriptUri: workflowScriptUri(parsedInput.runId),
|
|
36193
|
+
...resultResourceFields(scriptResources, parsedInput.runId),
|
|
36194
|
+
lineage,
|
|
36195
|
+
pendingPermissions
|
|
36196
|
+
},
|
|
36197
|
+
inspectionRetentionMetadata(manager, parsedInput.runId, status)
|
|
36198
|
+
);
|
|
36199
|
+
return {
|
|
36200
|
+
structuredContent: {
|
|
36201
|
+
...projected,
|
|
36202
|
+
permissionResponse: acknowledgement
|
|
36203
|
+
},
|
|
36204
|
+
content: [
|
|
36205
|
+
{
|
|
36206
|
+
type: "text",
|
|
36207
|
+
text: `Permission ${acknowledgement.permissionId} resolved for workflow run ${parsedInput.runId}.
|
|
36208
|
+
` + formatInspectionSummary(projected) + formatPendingPermissions(pendingPermissions),
|
|
36209
|
+
annotations: { audience: ["assistant"] }
|
|
36210
|
+
},
|
|
36211
|
+
...resultContentBlocks(scriptResources, parsedInput.runId, false),
|
|
36212
|
+
...scriptResources.links(lineage)
|
|
36213
|
+
],
|
|
36214
|
+
isError: false
|
|
36215
|
+
};
|
|
36216
|
+
}
|
|
35304
36217
|
if (parsedInput.action === "inspect") {
|
|
36218
|
+
let pendingPermissions = await pendingPermissionsForRun(
|
|
36219
|
+
manager,
|
|
36220
|
+
parsedInput.runId,
|
|
36221
|
+
permissionBroker,
|
|
36222
|
+
options.runControl
|
|
36223
|
+
);
|
|
36224
|
+
let acknowledgement;
|
|
36225
|
+
const canElicitPermission = Boolean(toolCatalog.clientCapabilities(ctx)?.elicitation);
|
|
36226
|
+
if (pendingPermissions.length > 0 && canElicitPermission) {
|
|
36227
|
+
const permission = pendingPermissions[0];
|
|
36228
|
+
if (options.protocolEra === "modern") {
|
|
36229
|
+
const state = {
|
|
36230
|
+
version: 1,
|
|
36231
|
+
flow: "permission",
|
|
36232
|
+
inputHash,
|
|
36233
|
+
scriptHash: workflowScriptHash(parsedInput.runId),
|
|
36234
|
+
runId: parsedInput.runId,
|
|
36235
|
+
permissionId: permission.permissionId
|
|
36236
|
+
};
|
|
36237
|
+
return inputRequired({
|
|
36238
|
+
inputRequests: { permission: inputRequired.elicit(createPermissionElicitation(permission)) },
|
|
36239
|
+
requestState: await requestStateCodec.mint(state, ctx)
|
|
36240
|
+
});
|
|
36241
|
+
}
|
|
36242
|
+
try {
|
|
36243
|
+
await primeCancellableServerRequestId(mcp.server);
|
|
36244
|
+
const elicited = await mcp.server.elicitInput(createPermissionElicitation(permission), {
|
|
36245
|
+
signal: ctx.mcpReq.signal
|
|
36246
|
+
});
|
|
36247
|
+
acknowledgement = await respondToPermission(
|
|
36248
|
+
manager,
|
|
36249
|
+
{
|
|
36250
|
+
runId: parsedInput.runId,
|
|
36251
|
+
permissionId: permission.permissionId,
|
|
36252
|
+
response: permissionResponseFromElicitation(permission, elicited)
|
|
36253
|
+
},
|
|
36254
|
+
permissionBroker,
|
|
36255
|
+
options.runControl
|
|
36256
|
+
);
|
|
36257
|
+
pendingPermissions = await pendingPermissionsForRun(
|
|
36258
|
+
manager,
|
|
36259
|
+
parsedInput.runId,
|
|
36260
|
+
permissionBroker,
|
|
36261
|
+
options.runControl
|
|
36262
|
+
);
|
|
36263
|
+
} catch {
|
|
36264
|
+
}
|
|
36265
|
+
}
|
|
35305
36266
|
const status = manager.inspectRun(parsedInput.runId, {
|
|
35306
36267
|
lastN: parsedInput.lastN,
|
|
35307
36268
|
labelGlob: parsedInput.labelGlob,
|
|
@@ -35323,20 +36284,28 @@ function createWorkflowServer(runner, options = {}) {
|
|
|
35323
36284
|
status,
|
|
35324
36285
|
{
|
|
35325
36286
|
scriptUri: workflowScriptUri(parsedInput.runId),
|
|
35326
|
-
|
|
36287
|
+
...resultResourceFields(scriptResources, parsedInput.runId),
|
|
36288
|
+
lineage,
|
|
36289
|
+
pendingPermissions,
|
|
36290
|
+
interaction: permissionInteraction(canElicitPermission)
|
|
35327
36291
|
},
|
|
35328
36292
|
inspectionRetentionMetadata(manager, parsedInput.runId, status)
|
|
35329
36293
|
);
|
|
35330
36294
|
return {
|
|
35331
|
-
structuredContent: {
|
|
36295
|
+
structuredContent: {
|
|
36296
|
+
...projected,
|
|
36297
|
+
...acknowledgement === void 0 ? {} : { permissionResponse: acknowledgement }
|
|
36298
|
+
},
|
|
35332
36299
|
content: [
|
|
35333
36300
|
// Status summaries are model input, not user-facing chat content (the run-monitor
|
|
35334
36301
|
// panel is the user's live view) — the audience annotation says so per MCP core.
|
|
35335
36302
|
{
|
|
35336
36303
|
type: "text",
|
|
35337
|
-
text: formatInspectionSummary(projected)
|
|
36304
|
+
text: formatInspectionSummary(projected) + (acknowledgement ? `
|
|
36305
|
+
Permission ${acknowledgement.permissionId} resolved.` : "") + formatPendingPermissions(pendingPermissions),
|
|
35338
36306
|
annotations: { audience: ["assistant"] }
|
|
35339
36307
|
},
|
|
36308
|
+
...resultContentBlocks(scriptResources, parsedInput.runId, false),
|
|
35340
36309
|
...scriptResources.links(lineage)
|
|
35341
36310
|
],
|
|
35342
36311
|
isError: false
|
|
@@ -35538,6 +36507,7 @@ function createWorkflowServer(runner, options = {}) {
|
|
|
35538
36507
|
status,
|
|
35539
36508
|
{
|
|
35540
36509
|
scriptUri: workflowScriptUri(parsedInput.runId),
|
|
36510
|
+
...resultResourceFields(scriptResources, parsedInput.runId),
|
|
35541
36511
|
lineage,
|
|
35542
36512
|
stopped,
|
|
35543
36513
|
alreadyTerminal
|
|
@@ -35548,7 +36518,11 @@ function createWorkflowServer(runner, options = {}) {
|
|
|
35548
36518
|
const currentLink = scriptResources.links(lineage).filter((link) => link.uri === workflowScriptUri(parsedInput.runId));
|
|
35549
36519
|
return {
|
|
35550
36520
|
structuredContent: { ...result },
|
|
35551
|
-
content: [
|
|
36521
|
+
content: [
|
|
36522
|
+
{ type: "text", text: formatStopSummary(result) },
|
|
36523
|
+
...resultContentBlocks(scriptResources, parsedInput.runId, false),
|
|
36524
|
+
...currentLink
|
|
36525
|
+
],
|
|
35552
36526
|
isError: false
|
|
35553
36527
|
};
|
|
35554
36528
|
}
|
|
@@ -35585,19 +36559,30 @@ function createWorkflowServer(runner, options = {}) {
|
|
|
35585
36559
|
isError: true
|
|
35586
36560
|
};
|
|
35587
36561
|
}
|
|
36562
|
+
let pendingPermissions = manager.getRun(parsedInput.runId) ? permissionBroker.list(parsedInput.runId) : await pendingPermissionsForRun(
|
|
36563
|
+
manager,
|
|
36564
|
+
parsedInput.runId,
|
|
36565
|
+
permissionBroker,
|
|
36566
|
+
options.runControl
|
|
36567
|
+
);
|
|
35588
36568
|
let returnedBecause;
|
|
35589
36569
|
if (isTerminalStatus(status.status)) {
|
|
35590
36570
|
returnedBecause = "terminal";
|
|
36571
|
+
} else if (pendingPermissions.length > 0) {
|
|
36572
|
+
returnedBecause = "action-required";
|
|
35591
36573
|
} else if (parsedInput.waitMs === 0) {
|
|
35592
36574
|
returnedBecause = "immediate";
|
|
35593
36575
|
} else {
|
|
36576
|
+
const local = manager.getRun(parsedInput.runId) !== void 0;
|
|
35594
36577
|
const waited = await waitForTerminal(
|
|
35595
36578
|
manager,
|
|
35596
36579
|
parsedInput.runId,
|
|
35597
36580
|
parsedInput.waitMs ?? 2e4,
|
|
35598
36581
|
ctx.mcpReq.signal,
|
|
35599
36582
|
backgroundRuns.get(parsedInput.runId),
|
|
35600
|
-
createAwaitProgressReporter(ctx)
|
|
36583
|
+
createAwaitProgressReporter(ctx),
|
|
36584
|
+
local ? permissionBroker.waitForPending(parsedInput.runId) : void 0,
|
|
36585
|
+
!local && options.runControl ? async () => (await options.runControl.listPermissions(manager, parsedInput.runId)).length > 0 : void 0
|
|
35601
36586
|
);
|
|
35602
36587
|
if (waited === AWAIT_CANCELLED) {
|
|
35603
36588
|
return {
|
|
@@ -35621,6 +36606,12 @@ function createWorkflowServer(runner, options = {}) {
|
|
|
35621
36606
|
isError: true
|
|
35622
36607
|
};
|
|
35623
36608
|
}
|
|
36609
|
+
pendingPermissions = await pendingPermissionsForRun(
|
|
36610
|
+
manager,
|
|
36611
|
+
parsedInput.runId,
|
|
36612
|
+
permissionBroker,
|
|
36613
|
+
options.runControl
|
|
36614
|
+
);
|
|
35624
36615
|
status = manager.inspectRun(parsedInput.runId, inspectionOptions);
|
|
35625
36616
|
if (!status) {
|
|
35626
36617
|
return {
|
|
@@ -35633,7 +36624,49 @@ function createWorkflowServer(runner, options = {}) {
|
|
|
35633
36624
|
isError: true
|
|
35634
36625
|
};
|
|
35635
36626
|
}
|
|
35636
|
-
returnedBecause = isTerminalStatus(status.status) ? "terminal" : "timeout";
|
|
36627
|
+
returnedBecause = isTerminalStatus(status.status) ? "terminal" : waited === "action-required" || pendingPermissions.length > 0 ? "action-required" : "timeout";
|
|
36628
|
+
}
|
|
36629
|
+
const canElicitPermission = Boolean(toolCatalog.clientCapabilities(ctx)?.elicitation);
|
|
36630
|
+
if (pendingPermissions.length > 0 && canElicitPermission) {
|
|
36631
|
+
const permission = pendingPermissions[0];
|
|
36632
|
+
if (options.protocolEra === "modern") {
|
|
36633
|
+
const state = {
|
|
36634
|
+
version: 1,
|
|
36635
|
+
flow: "permission",
|
|
36636
|
+
inputHash,
|
|
36637
|
+
scriptHash: workflowScriptHash(parsedInput.runId),
|
|
36638
|
+
runId: parsedInput.runId,
|
|
36639
|
+
permissionId: permission.permissionId
|
|
36640
|
+
};
|
|
36641
|
+
return inputRequired({
|
|
36642
|
+
inputRequests: { permission: inputRequired.elicit(createPermissionElicitation(permission)) },
|
|
36643
|
+
requestState: await requestStateCodec.mint(state, ctx)
|
|
36644
|
+
});
|
|
36645
|
+
}
|
|
36646
|
+
try {
|
|
36647
|
+
await primeCancellableServerRequestId(mcp.server);
|
|
36648
|
+
const elicited = await mcp.server.elicitInput(createPermissionElicitation(permission), {
|
|
36649
|
+
signal: ctx.mcpReq.signal
|
|
36650
|
+
});
|
|
36651
|
+
await respondToPermission(
|
|
36652
|
+
manager,
|
|
36653
|
+
{
|
|
36654
|
+
runId: parsedInput.runId,
|
|
36655
|
+
permissionId: permission.permissionId,
|
|
36656
|
+
response: permissionResponseFromElicitation(permission, elicited)
|
|
36657
|
+
},
|
|
36658
|
+
permissionBroker,
|
|
36659
|
+
options.runControl
|
|
36660
|
+
);
|
|
36661
|
+
pendingPermissions = await pendingPermissionsForRun(
|
|
36662
|
+
manager,
|
|
36663
|
+
parsedInput.runId,
|
|
36664
|
+
permissionBroker,
|
|
36665
|
+
options.runControl
|
|
36666
|
+
);
|
|
36667
|
+
returnedBecause = "permission-resolved";
|
|
36668
|
+
} catch {
|
|
36669
|
+
}
|
|
35637
36670
|
}
|
|
35638
36671
|
const tokenUsage = currentTokenUsage(manager, parsedInput.runId);
|
|
35639
36672
|
const baseOutcome = isTerminalStatus(status.status) ? terminalOutcome(manager, parsedInput.runId, status) : void 0;
|
|
@@ -35649,7 +36682,10 @@ function createWorkflowServer(runner, options = {}) {
|
|
|
35649
36682
|
{
|
|
35650
36683
|
wait,
|
|
35651
36684
|
...tokenUsage === void 0 ? {} : { tokenUsage },
|
|
36685
|
+
pendingPermissions,
|
|
36686
|
+
interaction: permissionInteraction(canElicitPermission),
|
|
35652
36687
|
scriptUri: workflowScriptUri(parsedInput.runId),
|
|
36688
|
+
...resultResourceFields(scriptResources, parsedInput.runId),
|
|
35653
36689
|
lineage
|
|
35654
36690
|
},
|
|
35655
36691
|
inspectionRetentionMetadata(manager, parsedInput.runId, status)
|
|
@@ -35664,9 +36700,10 @@ function createWorkflowServer(runner, options = {}) {
|
|
|
35664
36700
|
// Same audience hint as inspect: the await summary is for the model.
|
|
35665
36701
|
{
|
|
35666
36702
|
type: "text",
|
|
35667
|
-
text: formatAwaitSummary(result),
|
|
36703
|
+
text: formatAwaitSummary(result) + formatPendingPermissions(pendingPermissions),
|
|
35668
36704
|
annotations: { audience: ["assistant"] }
|
|
35669
36705
|
},
|
|
36706
|
+
...resultContentBlocks(scriptResources, parsedInput.runId, true),
|
|
35670
36707
|
...scriptResources.links(lineage)
|
|
35671
36708
|
],
|
|
35672
36709
|
isError: false
|
|
@@ -35766,7 +36803,7 @@ function createWorkflowServer(runner, options = {}) {
|
|
|
35766
36803
|
} catch (error51) {
|
|
35767
36804
|
if (error51 instanceof NoAutoDefaultBackendError) {
|
|
35768
36805
|
return {
|
|
35769
|
-
content: [{ type: "text", text:
|
|
36806
|
+
content: [{ type: "text", text: truncateUtf84(error51.message, 8192, "\u2026[backend diagnostics truncated]") }],
|
|
35770
36807
|
isError: true
|
|
35771
36808
|
};
|
|
35772
36809
|
}
|
|
@@ -35802,7 +36839,7 @@ function createWorkflowServer(runner, options = {}) {
|
|
|
35802
36839
|
if (admissionWarnings.length > lines.length) {
|
|
35803
36840
|
lines.push(`- \u2026 ${admissionWarnings.length - lines.length} more warning(s) omitted`);
|
|
35804
36841
|
}
|
|
35805
|
-
preflightWarningText =
|
|
36842
|
+
preflightWarningText = truncateUtf84(
|
|
35806
36843
|
`
|
|
35807
36844
|
Preflight warnings (the run was admitted):
|
|
35808
36845
|
${lines.join("\n")}`,
|
|
@@ -35843,15 +36880,15 @@ ${lines.join("\n")}`,
|
|
|
35843
36880
|
let lastActivitySeq = 0;
|
|
35844
36881
|
exec.signal = ctx.mcpReq.signal;
|
|
35845
36882
|
exec.onProgress = (snapshot) => {
|
|
35846
|
-
const
|
|
36883
|
+
const settled2 = snapshot.agents.filter(
|
|
35847
36884
|
(a) => a.status === "done" || a.status === "error" || a.status === "skipped"
|
|
35848
36885
|
).length;
|
|
35849
36886
|
const activity = snapshot.latestActivity;
|
|
35850
36887
|
if (activity && activity.seq > lastActivitySeq) {
|
|
35851
36888
|
lastActivitySeq = activity.seq;
|
|
35852
|
-
reporter(
|
|
36889
|
+
reporter(settled2, snapshot.agents.length || void 0, formatAgentProgressMessage(activity.progress));
|
|
35853
36890
|
} else {
|
|
35854
|
-
reporter(
|
|
36891
|
+
reporter(settled2, snapshot.agents.length || void 0, snapshot.currentPhase);
|
|
35855
36892
|
}
|
|
35856
36893
|
};
|
|
35857
36894
|
const canElicit = Boolean(toolCatalog.clientCapabilities(ctx)?.elicitation);
|
|
@@ -35895,7 +36932,9 @@ ${lines.join("\n")}`,
|
|
|
35895
36932
|
scriptSource,
|
|
35896
36933
|
scriptUri: scriptUri2,
|
|
35897
36934
|
limits: admittedRun.limits,
|
|
35898
|
-
...admittedRun.replayEligibility === void 0 ? {} : { replayEligibility: admittedRun.replayEligibility }
|
|
36935
|
+
...admittedRun.replayEligibility === void 0 ? {} : { replayEligibility: admittedRun.replayEligibility },
|
|
36936
|
+
pendingPermissions: permissionBroker.list(started2.runId),
|
|
36937
|
+
interaction: permissionInteraction(Boolean(toolCatalog.clientCapabilities(ctx)?.elicitation))
|
|
35899
36938
|
},
|
|
35900
36939
|
content: [
|
|
35901
36940
|
{
|
|
@@ -35904,7 +36943,7 @@ ${lines.join("\n")}`,
|
|
|
35904
36943
|
runId: ${started2.runId}
|
|
35905
36944
|
` + (preflightWarningText ? `${preflightWarningText.trimStart()}
|
|
35906
36945
|
` : "") + (admittedRun.replayEligibility ? `${formatResumeSummary(admittedRun.replayEligibility)}
|
|
35907
|
-
` : "") + `Call workflow with action="await" and this runId to wait for its result, or action="inspect" for an immediate status snapshot. If a live run-monitor panel is shown for this run, it self-updates and reports phase starts, pauses, and terminal outcomes \u2014 do not poll inspect for status.`
|
|
36946
|
+
` : "") + `Call workflow with action="await" and this runId to wait for its result, or action="inspect" for an immediate status snapshot. Either action returns early when an ACP permission needs a response; use the elicitation shown by capable clients or action="permissions-response" with an exact advertised option. If a live run-monitor panel is shown for this run, it self-updates and reports phase starts, pauses, and terminal outcomes \u2014 do not poll inspect for status.`
|
|
35908
36947
|
},
|
|
35909
36948
|
...links
|
|
35910
36949
|
],
|
|
@@ -35925,7 +36964,41 @@ runId: ${started2.runId}
|
|
|
35925
36964
|
}
|
|
35926
36965
|
);
|
|
35927
36966
|
executionLatch.admit();
|
|
35928
|
-
const
|
|
36967
|
+
const settled = await settleForegroundRunOrPermission(manager, started, permissionBroker);
|
|
36968
|
+
if (settled.kind === "permission") {
|
|
36969
|
+
const admittedRun = manager.getRun(started.runId);
|
|
36970
|
+
if (!admittedRun?.limits) {
|
|
36971
|
+
throw new ProtocolError(ProtocolErrorCode.InternalError, "Workflow permission wait lost its live run limits");
|
|
36972
|
+
}
|
|
36973
|
+
backgroundRuns.track(started.runId, started.promise);
|
|
36974
|
+
const pendingPermissions = permissionBroker.list(started.runId);
|
|
36975
|
+
const scriptUri2 = workflowScriptUri(started.runId);
|
|
36976
|
+
const canElicitPermission = Boolean(toolCatalog.clientCapabilities(ctx)?.elicitation);
|
|
36977
|
+
return {
|
|
36978
|
+
structuredContent: {
|
|
36979
|
+
runId: started.runId,
|
|
36980
|
+
status: "running",
|
|
36981
|
+
scriptSource,
|
|
36982
|
+
scriptUri: scriptUri2,
|
|
36983
|
+
limits: admittedRun.limits,
|
|
36984
|
+
pendingPermissions,
|
|
36985
|
+
interaction: permissionInteraction(canElicitPermission),
|
|
36986
|
+
...admittedRun.replayEligibility === void 0 ? {} : { replayEligibility: admittedRun.replayEligibility }
|
|
36987
|
+
},
|
|
36988
|
+
content: [
|
|
36989
|
+
{
|
|
36990
|
+
type: "text",
|
|
36991
|
+
text: `Workflow "${admittedRun.snapshot.name}" is still running but requires a permission response.
|
|
36992
|
+
runId: ${started.runId}
|
|
36993
|
+
` + formatPendingPermissions(pendingPermissions).trimStart() + `
|
|
36994
|
+
Call workflow with action="await" or action="inspect"; elicitation-capable clients will present the pending choice, and other clients can use action="permissions-response".`
|
|
36995
|
+
},
|
|
36996
|
+
...scriptResources.links([{ runId: started.runId, uri: scriptUri2, available: true }])
|
|
36997
|
+
],
|
|
36998
|
+
isError: false
|
|
36999
|
+
};
|
|
37000
|
+
}
|
|
37001
|
+
const run = settled.run;
|
|
35929
37002
|
if (options.protocolEra === "modern" && toolCatalog.clientCapabilities(ctx)?.elicitation && run.status === "paused" && run.reason === "checkpoint_required" && run.checkpointContext !== void 0) {
|
|
35930
37003
|
const checkpoint = run.checkpointContext;
|
|
35931
37004
|
const elicitation = createCheckpointElicitation(checkpoint.prompt, checkpoint);
|
|
@@ -35950,14 +37023,16 @@ runId: ${started2.runId}
|
|
|
35950
37023
|
});
|
|
35951
37024
|
}
|
|
35952
37025
|
const scriptUri = workflowScriptUri(run.runId);
|
|
37026
|
+
const resultFields = resultResourceFields(scriptResources, run.runId);
|
|
35953
37027
|
const structuredContent = {
|
|
35954
|
-
...toWorkflowToolResult(run, { scriptSource, scriptUri })
|
|
37028
|
+
...toWorkflowToolResult(run, { scriptSource, scriptUri, ...resultFields })
|
|
35955
37029
|
};
|
|
35956
37030
|
const isError = run.status === "failed" || run.status === "aborted";
|
|
35957
37031
|
return {
|
|
35958
37032
|
structuredContent: { ...structuredContent },
|
|
35959
37033
|
content: [
|
|
35960
37034
|
{ type: "text", text: `${formatRunSummary(run)}${preflightWarningText}` },
|
|
37035
|
+
...resultContentBlocks(scriptResources, run.runId, true),
|
|
35961
37036
|
...scriptResources.links([{ runId: run.runId, uri: scriptUri, available: true }])
|
|
35962
37037
|
],
|
|
35963
37038
|
isError
|
|
@@ -36010,7 +37085,7 @@ import { dirname as dirname2 } from "node:path";
|
|
|
36010
37085
|
import { spawn } from "node:child_process";
|
|
36011
37086
|
|
|
36012
37087
|
// ../mcp-server/src/daemon/daemon-info.ts
|
|
36013
|
-
import { randomUUID as
|
|
37088
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
36014
37089
|
import { createHash as createHash2 } from "node:crypto";
|
|
36015
37090
|
import {
|
|
36016
37091
|
chmodSync as chmodSync2,
|
|
@@ -36083,7 +37158,7 @@ function readDaemonInstance(pid) {
|
|
|
36083
37158
|
}
|
|
36084
37159
|
function writeInfoFile(path, info) {
|
|
36085
37160
|
mkdirSync2(dirname(path), { recursive: true });
|
|
36086
|
-
const tmp = `${path}.${info.pid}.${
|
|
37161
|
+
const tmp = `${path}.${info.pid}.${randomUUID3().slice(0, 8)}.tmp`;
|
|
36087
37162
|
writeFileSync2(tmp, `${JSON.stringify(info, null, 2)}
|
|
36088
37163
|
`, { mode: 384 });
|
|
36089
37164
|
chmodSync2(tmp, 384);
|
|
@@ -36170,7 +37245,7 @@ function compareVersions(a, b) {
|
|
|
36170
37245
|
function claimSpawnLock(fingerprint = envFingerprint()) {
|
|
36171
37246
|
const path = daemonLockPath(fingerprint);
|
|
36172
37247
|
mkdirSync2(dirname(path), { recursive: true });
|
|
36173
|
-
const lock = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString(), token:
|
|
37248
|
+
const lock = { pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString(), token: randomUUID3() };
|
|
36174
37249
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
36175
37250
|
try {
|
|
36176
37251
|
writeFileSync2(path, JSON.stringify(lock), { flag: "wx", mode: 384 });
|
|
@@ -36320,7 +37395,7 @@ async function ensureDaemonRunning(options) {
|
|
|
36320
37395
|
}
|
|
36321
37396
|
|
|
36322
37397
|
// ../mcp-server/src/daemon/run-daemon.ts
|
|
36323
|
-
import { randomUUID as
|
|
37398
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
36324
37399
|
import { createAcpRunner } from "@automatalabs/workflows";
|
|
36325
37400
|
|
|
36326
37401
|
// ../mcp-server/src/daemon/daemon-lifecycle.ts
|
|
@@ -36402,7 +37477,7 @@ function installDaemonLifecycle(options) {
|
|
|
36402
37477
|
|
|
36403
37478
|
// ../mcp-server/src/daemon/http-daemon.ts
|
|
36404
37479
|
import http from "node:http";
|
|
36405
|
-
import { randomUUID as
|
|
37480
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
36406
37481
|
|
|
36407
37482
|
// ../../node_modules/.pnpm/@hono+node-server@1.19.14_hono@4.12.27/node_modules/@hono/node-server/dist/index.mjs
|
|
36408
37483
|
import { Http2ServerRequest as Http2ServerRequest2, constants as h2constants } from "http2";
|
|
@@ -37472,7 +38547,7 @@ function verifyRunControlRequest(key, input) {
|
|
|
37472
38547
|
}
|
|
37473
38548
|
|
|
37474
38549
|
// ../mcp-server/src/daemon/run-control.ts
|
|
37475
|
-
import { randomUUID as
|
|
38550
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
37476
38551
|
var FORWARD_TIMEOUT_MS = 5e3;
|
|
37477
38552
|
var FORCE_TERM_WAIT_MS = 5e3;
|
|
37478
38553
|
var FORCE_KILL_WAIT_MS = 2e3;
|
|
@@ -37512,12 +38587,14 @@ var DaemonRunControl = class {
|
|
|
37512
38587
|
this.fetchImpl = options.fetch ?? fetch;
|
|
37513
38588
|
this.killProcess = options.kill ?? ((pid, signal) => process.kill(pid, signal));
|
|
37514
38589
|
this.isPidAlive = options.isPidAlive ?? pidIsAlive;
|
|
38590
|
+
this.permissionBroker = options.permissionBroker ?? new WorkflowPermissionBroker();
|
|
37515
38591
|
}
|
|
37516
38592
|
options;
|
|
37517
38593
|
log;
|
|
37518
38594
|
fetchImpl;
|
|
37519
38595
|
killProcess;
|
|
37520
38596
|
isPidAlive;
|
|
38597
|
+
permissionBroker;
|
|
37521
38598
|
processingPending;
|
|
37522
38599
|
async resolveOwner(manager, runId) {
|
|
37523
38600
|
const lease = manager.getPersistence().inspectRunLease?.(runId);
|
|
@@ -37600,6 +38677,24 @@ var DaemonRunControl = class {
|
|
|
37600
38677
|
return { ok: false, code: "NOT_OWNER", message: `Daemon has no live run ${request.runId}` };
|
|
37601
38678
|
}
|
|
37602
38679
|
try {
|
|
38680
|
+
if (request.action === "list-permissions") {
|
|
38681
|
+
return {
|
|
38682
|
+
ok: true,
|
|
38683
|
+
outcome: "permissions-listed",
|
|
38684
|
+
permissions: this.permissionBroker.list(request.runId)
|
|
38685
|
+
};
|
|
38686
|
+
}
|
|
38687
|
+
if (request.action === "respond-permission") {
|
|
38688
|
+
return {
|
|
38689
|
+
ok: true,
|
|
38690
|
+
outcome: "permission-responded",
|
|
38691
|
+
acknowledgement: this.permissionBroker.respond(
|
|
38692
|
+
request.runId,
|
|
38693
|
+
request.permissionId,
|
|
38694
|
+
request.response
|
|
38695
|
+
)
|
|
38696
|
+
};
|
|
38697
|
+
}
|
|
37603
38698
|
const cancellation = await manager.cancelAgentCall(request.runId, request.callIndex);
|
|
37604
38699
|
return { ok: true, outcome: "agent-cancelled", cancellation };
|
|
37605
38700
|
} catch (error51) {
|
|
@@ -37664,6 +38759,71 @@ var DaemonRunControl = class {
|
|
|
37664
38759
|
}
|
|
37665
38760
|
}
|
|
37666
38761
|
}
|
|
38762
|
+
async listPermissions(manager, runId) {
|
|
38763
|
+
if (manager.getRun(runId)) return this.permissionBroker.list(runId);
|
|
38764
|
+
const owner = await this.resolveOwner(manager, runId);
|
|
38765
|
+
if (!owner) return [];
|
|
38766
|
+
if (!this.controlCapable(owner)) {
|
|
38767
|
+
throw new ProtocolError(
|
|
38768
|
+
ProtocolErrorCode.InvalidParams,
|
|
38769
|
+
`${actionableOwnerMessage(runId, owner, "permission inspection")} Pending permissions are live execution state and require a control-capable owner.`
|
|
38770
|
+
);
|
|
38771
|
+
}
|
|
38772
|
+
let response;
|
|
38773
|
+
try {
|
|
38774
|
+
response = await this.post(owner, {
|
|
38775
|
+
operationId: randomUUID4(),
|
|
38776
|
+
runId,
|
|
38777
|
+
action: "list-permissions"
|
|
38778
|
+
});
|
|
38779
|
+
} catch (error51) {
|
|
38780
|
+
throw new ProtocolError(
|
|
38781
|
+
ProtocolErrorCode.InternalError,
|
|
38782
|
+
`${actionableOwnerMessage(runId, owner, "permission inspection")} ${String(error51)}`
|
|
38783
|
+
);
|
|
38784
|
+
}
|
|
38785
|
+
if (!response.ok || response.outcome !== "permissions-listed") {
|
|
38786
|
+
throw new ProtocolError(
|
|
38787
|
+
response.ok || response.code === "INTERNAL_ERROR" ? ProtocolErrorCode.InternalError : ProtocolErrorCode.InvalidParams,
|
|
38788
|
+
response.ok ? "Owner returned an invalid permission-list response." : response.message
|
|
38789
|
+
);
|
|
38790
|
+
}
|
|
38791
|
+
return response.permissions;
|
|
38792
|
+
}
|
|
38793
|
+
async respondPermission(manager, input) {
|
|
38794
|
+
if (manager.getRun(input.runId) && this.permissionBroker.has(input.runId, input.permissionId)) {
|
|
38795
|
+
return this.permissionBroker.respond(input.runId, input.permissionId, input.response);
|
|
38796
|
+
}
|
|
38797
|
+
const owner = await this.resolveOwner(manager, input.runId);
|
|
38798
|
+
if (!owner || !this.controlCapable(owner)) {
|
|
38799
|
+
throw new ProtocolError(
|
|
38800
|
+
ProtocolErrorCode.InvalidParams,
|
|
38801
|
+
`${actionableOwnerMessage(input.runId, owner, "permission response")} A permission response cannot be reconstructed after owner loss.`
|
|
38802
|
+
);
|
|
38803
|
+
}
|
|
38804
|
+
let response;
|
|
38805
|
+
try {
|
|
38806
|
+
response = await this.post(owner, {
|
|
38807
|
+
operationId: randomUUID4(),
|
|
38808
|
+
runId: input.runId,
|
|
38809
|
+
action: "respond-permission",
|
|
38810
|
+
permissionId: input.permissionId,
|
|
38811
|
+
response: input.response
|
|
38812
|
+
});
|
|
38813
|
+
} catch (error51) {
|
|
38814
|
+
throw new ProtocolError(
|
|
38815
|
+
ProtocolErrorCode.InternalError,
|
|
38816
|
+
`${actionableOwnerMessage(input.runId, owner, "permission response")} ${String(error51)}`
|
|
38817
|
+
);
|
|
38818
|
+
}
|
|
38819
|
+
if (!response.ok || response.outcome !== "permission-responded") {
|
|
38820
|
+
throw new ProtocolError(
|
|
38821
|
+
response.ok || response.code === "INTERNAL_ERROR" ? ProtocolErrorCode.InternalError : ProtocolErrorCode.InvalidParams,
|
|
38822
|
+
response.ok ? "Owner returned an invalid permission-response acknowledgement." : response.message
|
|
38823
|
+
);
|
|
38824
|
+
}
|
|
38825
|
+
return response.acknowledgement;
|
|
38826
|
+
}
|
|
37667
38827
|
async control(manager, input) {
|
|
37668
38828
|
let owner = await this.resolveOwner(manager, input.runId);
|
|
37669
38829
|
if (input.callIndex !== void 0) {
|
|
@@ -37676,7 +38836,7 @@ var DaemonRunControl = class {
|
|
|
37676
38836
|
let response;
|
|
37677
38837
|
try {
|
|
37678
38838
|
response = await this.post(owner, {
|
|
37679
|
-
operationId:
|
|
38839
|
+
operationId: randomUUID4(),
|
|
37680
38840
|
runId: input.runId,
|
|
37681
38841
|
action: "cancel-agent",
|
|
37682
38842
|
callIndex: input.callIndex
|
|
@@ -37906,14 +39066,29 @@ function writeControlResponse(res, status, body) {
|
|
|
37906
39066
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
37907
39067
|
res.end(JSON.stringify(body));
|
|
37908
39068
|
}
|
|
39069
|
+
function isPermissionResponse(value) {
|
|
39070
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
39071
|
+
const row = value;
|
|
39072
|
+
const keys = Object.keys(row).sort();
|
|
39073
|
+
if (keys.join(",") !== "outcome") return false;
|
|
39074
|
+
const outcome = row.outcome;
|
|
39075
|
+
if (outcome === null || typeof outcome !== "object" || Array.isArray(outcome)) return false;
|
|
39076
|
+
const decision = outcome;
|
|
39077
|
+
const decisionKeys = Object.keys(decision).sort().join(",");
|
|
39078
|
+
if (decision.outcome === "cancelled") return decisionKeys === "outcome";
|
|
39079
|
+
return decision.outcome === "selected" && typeof decision.optionId === "string" && decision.optionId.length > 0 && decisionKeys === "optionId,outcome";
|
|
39080
|
+
}
|
|
37909
39081
|
function isInternalRunControlRequest(value) {
|
|
37910
39082
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
37911
39083
|
const row = value;
|
|
37912
39084
|
const keys = Object.keys(row).sort();
|
|
37913
39085
|
if (typeof row.operationId !== "string" || !/^[0-9a-f-]{36}$/i.test(row.operationId) || typeof row.runId !== "string" || !/^[a-z0-9]+-[a-z0-9]+$/.test(row.runId)) return false;
|
|
37914
|
-
if (row.action === "stop") {
|
|
39086
|
+
if (row.action === "stop" || row.action === "list-permissions") {
|
|
37915
39087
|
return keys.join(",") === "action,operationId,runId";
|
|
37916
39088
|
}
|
|
39089
|
+
if (row.action === "respond-permission") {
|
|
39090
|
+
return typeof row.permissionId === "string" && /^[0-9a-f-]{36}$/i.test(row.permissionId) && isPermissionResponse(row.response) && keys.join(",") === "action,operationId,permissionId,response,runId";
|
|
39091
|
+
}
|
|
37917
39092
|
return row.action === "cancel-agent" && Number.isSafeInteger(row.callIndex) && row.callIndex >= 0 && keys.join(",") === "action,callIndex,operationId,runId";
|
|
37918
39093
|
}
|
|
37919
39094
|
async function handleRunControlRequest(req, res, key, runControl) {
|
|
@@ -37981,11 +39156,12 @@ async function createDaemon(options) {
|
|
|
37981
39156
|
const log = options.log ?? ((line) => console.error(line));
|
|
37982
39157
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
37983
39158
|
const ownPid = options.ownPid ?? process.pid;
|
|
37984
|
-
const ownInstanceId = options.ownInstanceId ??
|
|
39159
|
+
const ownInstanceId = options.ownInstanceId ?? randomUUID5();
|
|
37985
39160
|
const version2 = options.version ?? SERVER_VERSION;
|
|
37986
39161
|
const isSuperseded = options.isSuperseded ?? (() => isSupersededBy(ownPid));
|
|
37987
39162
|
const familyFingerprint = envFingerprint(env);
|
|
37988
39163
|
const sessions = new SessionRegistry();
|
|
39164
|
+
const permissionBroker = options.permissionBroker ?? new WorkflowPermissionBroker();
|
|
37989
39165
|
const projects = new WorkflowProjectRegistry(options.runner, { leaseOwnerId: ownInstanceId });
|
|
37990
39166
|
const runControlKey = loadOrCreateRunControlKey();
|
|
37991
39167
|
const runControl = new DaemonRunControl({
|
|
@@ -37993,6 +39169,7 @@ async function createDaemon(options) {
|
|
|
37993
39169
|
ownPid,
|
|
37994
39170
|
ownInstanceId,
|
|
37995
39171
|
key: runControlKey,
|
|
39172
|
+
permissionBroker,
|
|
37996
39173
|
log
|
|
37997
39174
|
});
|
|
37998
39175
|
const replDrainBoundMs = options.replDrainBoundMs ?? options.sessionTtlMs ?? REPL_DRAIN_BOUND_MS;
|
|
@@ -38015,7 +39192,7 @@ async function createDaemon(options) {
|
|
|
38015
39192
|
};
|
|
38016
39193
|
modernHandler = createMcpHandler(
|
|
38017
39194
|
() => {
|
|
38018
|
-
const clientId = `modern:${
|
|
39195
|
+
const clientId = `modern:${randomUUID5()}`;
|
|
38019
39196
|
return createWorkflowServer(options.runner, {
|
|
38020
39197
|
projects,
|
|
38021
39198
|
requireProjectDir: true,
|
|
@@ -38028,7 +39205,8 @@ async function createDaemon(options) {
|
|
|
38028
39205
|
requestStateCodec,
|
|
38029
39206
|
disconnectReplClientOnClose: true,
|
|
38030
39207
|
modernNotifier,
|
|
38031
|
-
runControl
|
|
39208
|
+
runControl,
|
|
39209
|
+
permissionBroker
|
|
38032
39210
|
});
|
|
38033
39211
|
},
|
|
38034
39212
|
{
|
|
@@ -38042,6 +39220,16 @@ async function createDaemon(options) {
|
|
|
38042
39220
|
const detachModernRunDeleted = projects.onRunDeleted(() => modernNotifier.resourcesChanged());
|
|
38043
39221
|
const detachModernRunEvent = projects.onRunEventPersisted((record2) => {
|
|
38044
39222
|
modernNotifier.resourceUpdated(workflowRunEventsUri(record2.runId));
|
|
39223
|
+
if (record2.event.type !== "complete") return;
|
|
39224
|
+
queueMicrotask(() => {
|
|
39225
|
+
try {
|
|
39226
|
+
const state = projects.storeFor(record2.runId)?.manager.getPersistence().load(record2.runId);
|
|
39227
|
+
if (state?.status === "completed" && state.result !== void 0) {
|
|
39228
|
+
modernNotifier.resourcesChanged();
|
|
39229
|
+
}
|
|
39230
|
+
} catch {
|
|
39231
|
+
}
|
|
39232
|
+
});
|
|
38045
39233
|
});
|
|
38046
39234
|
const handleMcpRequest = async (req, res) => {
|
|
38047
39235
|
const sessionHeader = req.headers["mcp-session-id"];
|
|
@@ -38084,7 +39272,7 @@ async function createDaemon(options) {
|
|
|
38084
39272
|
return;
|
|
38085
39273
|
}
|
|
38086
39274
|
const transport = new NodeStreamableHTTPServerTransport({
|
|
38087
|
-
sessionIdGenerator: () =>
|
|
39275
|
+
sessionIdGenerator: () => randomUUID5(),
|
|
38088
39276
|
eventStore: new BoundedEventStore(),
|
|
38089
39277
|
onsessioninitialized: (sid) => {
|
|
38090
39278
|
sessions.add({
|
|
@@ -38107,7 +39295,8 @@ async function createDaemon(options) {
|
|
|
38107
39295
|
replClientId: () => transport.sessionId,
|
|
38108
39296
|
replDrainBoundMs,
|
|
38109
39297
|
replEvalBreakChannel: options.evalBreakChannel,
|
|
38110
|
-
runControl
|
|
39298
|
+
runControl,
|
|
39299
|
+
permissionBroker
|
|
38111
39300
|
});
|
|
38112
39301
|
await server.connect(transport);
|
|
38113
39302
|
const protocolOnClose = transport.onclose;
|
|
@@ -38205,6 +39394,7 @@ async function createDaemon(options) {
|
|
|
38205
39394
|
detachModernRunEvent();
|
|
38206
39395
|
detachModernRunDeleted();
|
|
38207
39396
|
await modernHandler.close();
|
|
39397
|
+
permissionBroker.dispose();
|
|
38208
39398
|
httpServer.closeAllConnections();
|
|
38209
39399
|
await closed;
|
|
38210
39400
|
}
|
|
@@ -38230,9 +39420,14 @@ async function ownDaemonAlreadyRunning() {
|
|
|
38230
39420
|
}
|
|
38231
39421
|
async function runDaemon(options = {}) {
|
|
38232
39422
|
const log = (line) => console.error(line);
|
|
38233
|
-
const
|
|
39423
|
+
const permissionBroker = new WorkflowPermissionBroker();
|
|
39424
|
+
const runner = createAcpRunner({
|
|
39425
|
+
onPermissionRequest: permissionBroker.resolver,
|
|
39426
|
+
enforceToolPolicyBeforePermissionResolver: true
|
|
39427
|
+
});
|
|
39428
|
+
permissionBroker.attach(runner);
|
|
38234
39429
|
const supersede = options.supersede ?? false;
|
|
38235
|
-
const instanceId =
|
|
39430
|
+
const instanceId = randomUUID6();
|
|
38236
39431
|
let daemon;
|
|
38237
39432
|
const sessionTtlMs = envInt(SESSION_IDLE_TTL_ENV, SESSION_IDLE_TTL_MS);
|
|
38238
39433
|
const replDrainBoundMs = envInt(REPL_DRAIN_BOUND_ENV, REPL_DRAIN_BOUND_MS);
|
|
@@ -38242,7 +39437,7 @@ async function runDaemon(options = {}) {
|
|
|
38242
39437
|
return `port ${port} is still held by ${holder.legacy ? "a legacy " : ""}daemon pid ${holder.info.pid} (v${holder.info.version}, started ${holder.info.startedAt})`;
|
|
38243
39438
|
};
|
|
38244
39439
|
const evalBreakChannel = createEvalBreakChannel2();
|
|
38245
|
-
const daemonOptions = { runner, log, replDrainBoundMs, evalBreakChannel, ownInstanceId: instanceId };
|
|
39440
|
+
const daemonOptions = { runner, permissionBroker, log, replDrainBoundMs, evalBreakChannel, ownInstanceId: instanceId };
|
|
38246
39441
|
if (supersede) {
|
|
38247
39442
|
let port = options.port ?? 0;
|
|
38248
39443
|
try {
|
|
@@ -52269,7 +53464,12 @@ var ReplRelayStdioTransport = class {
|
|
|
52269
53464
|
|
|
52270
53465
|
// ../mcp-server/src/index.ts
|
|
52271
53466
|
async function main() {
|
|
52272
|
-
const
|
|
53467
|
+
const permissionBroker = new WorkflowPermissionBroker();
|
|
53468
|
+
const runner = createAcpRunner2({
|
|
53469
|
+
onPermissionRequest: permissionBroker.resolver,
|
|
53470
|
+
enforceToolPolicyBeforePermissionResolver: true
|
|
53471
|
+
});
|
|
53472
|
+
permissionBroker.attach(runner);
|
|
52273
53473
|
const projects = new WorkflowProjectRegistry(runner);
|
|
52274
53474
|
const defaultContext = projects.getOrCreate(process.cwd());
|
|
52275
53475
|
const replPresence = new ReplPresenceLedger(REPL_DRAIN_BOUND_MS);
|
|
@@ -52295,7 +53495,8 @@ async function main() {
|
|
|
52295
53495
|
replDrainBoundMs: REPL_DRAIN_BOUND_MS,
|
|
52296
53496
|
replEvalBreakChannel: evalBreakChannel,
|
|
52297
53497
|
protocolEra: era,
|
|
52298
|
-
disconnectReplClientOnClose: true
|
|
53498
|
+
disconnectReplClientOnClose: true,
|
|
53499
|
+
permissionBroker
|
|
52299
53500
|
});
|
|
52300
53501
|
activeServer = server;
|
|
52301
53502
|
activeEra = era;
|
|
@@ -52312,6 +53513,7 @@ async function main() {
|
|
|
52312
53513
|
replDefaultProjectDir: () => defaultContext.projectDir,
|
|
52313
53514
|
async disposeReplEvalBreakChannel() {
|
|
52314
53515
|
detachModernEvents();
|
|
53516
|
+
permissionBroker.dispose();
|
|
52315
53517
|
await projects.disposeReplStates();
|
|
52316
53518
|
replPresence.disconnectAll();
|
|
52317
53519
|
await evalBreakChannel.dispose();
|
|
@@ -52409,13 +53611,18 @@ export {
|
|
|
52409
53611
|
MCP_ENDPOINT_PATH,
|
|
52410
53612
|
RESOURCE_MIME_TYPE,
|
|
52411
53613
|
RESOURCE_URI_META_KEY,
|
|
53614
|
+
RESULT_RESOURCE_MIME_TYPE,
|
|
52412
53615
|
RUN_MONITOR_RESOURCE_URI,
|
|
52413
53616
|
ReplPresenceLedger,
|
|
52414
53617
|
SCRIPT_RESOURCE_LIST_LIMIT,
|
|
52415
53618
|
SCRIPT_RESOURCE_MIME_TYPE,
|
|
52416
53619
|
SHUTDOWN_DEADLINE_MS,
|
|
52417
53620
|
WORKFLOW_EVENTS_TOOL_NAME,
|
|
53621
|
+
WORKFLOW_RESULT_CHUNK_BYTES_DEFAULT,
|
|
53622
|
+
WORKFLOW_RESULT_CHUNK_BYTES_MAX,
|
|
53623
|
+
WORKFLOW_RESULT_CHUNK_BYTES_MIN,
|
|
52418
53624
|
WORKFLOW_RUN_EVENTS_SCHEMA_VERSION,
|
|
53625
|
+
WorkflowPermissionBroker,
|
|
52419
53626
|
WorkflowProjectRegistry,
|
|
52420
53627
|
appResourceToolMeta,
|
|
52421
53628
|
authoringDocResource,
|
|
@@ -52456,7 +53663,9 @@ export {
|
|
|
52456
53663
|
toWorkflowExecutionOutcome,
|
|
52457
53664
|
toWorkflowToolResult,
|
|
52458
53665
|
validateRequest,
|
|
53666
|
+
workflowResultUri,
|
|
52459
53667
|
workflowRunEventsUri,
|
|
53668
|
+
workflowRunIdFromResultUri,
|
|
52460
53669
|
workflowRunIdFromScriptUri,
|
|
52461
53670
|
workflowScriptUri,
|
|
52462
53671
|
workflowToolInputShape,
|