@atolis-hq/wake 0.3.68 → 0.3.69
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/dist/src/bootstrap/surface-api-execution-applications.js +15 -6
- package/dist/src/bootstrap/surface-cli-applications.js +21 -1
- package/dist/src/bootstrap/version.js +1 -1
- package/dist/src/execution/application/recovery-service.js +4 -1
- package/dist/src/surfaces/api/routes/commands.js +28 -13
- package/dist/src/surfaces/api/routes/execution.js +1 -1
- package/dist/src/surfaces/cli/main.js +80 -0
- package/dist/src/surfaces/cli/usage.js +3 -0
- package/package.json +1 -1
|
@@ -10,11 +10,20 @@ export function createExecutionApplications(root, now) {
|
|
|
10
10
|
return {
|
|
11
11
|
async resolveAmbiguousRun(runId, command) {
|
|
12
12
|
const context = resolutionContext(runId, command.idempotencyKey, now);
|
|
13
|
-
const run = await root.recovery.resolve(runId,
|
|
14
|
-
kind: RunStatus.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
const run = await root.recovery.resolve(runId, command.status === RunStatus.Succeeded
|
|
14
|
+
? { kind: RunStatus.Succeeded, outcome: command.outcome }
|
|
15
|
+
: {
|
|
16
|
+
kind: RunStatus.Failed,
|
|
17
|
+
failure: { kind: ExecutionFailureCode.Unexpected, message: command.reason },
|
|
18
|
+
}, context);
|
|
19
|
+
if (command.status === RunStatus.Succeeded)
|
|
20
|
+
await root.orchestration.acceptOutcome({
|
|
21
|
+
workflowInstanceId: run.workflowInstanceId,
|
|
22
|
+
activationId: run.activationId,
|
|
23
|
+
outcome: run.outcome,
|
|
24
|
+
}, context);
|
|
25
|
+
else
|
|
26
|
+
await root.orchestration.resolveExecutionFailure(run.workflowInstanceId, { activationId: run.activationId, runId: run.runId, reason: command.reason }, context);
|
|
18
27
|
return commandAccepted(command, now());
|
|
19
28
|
},
|
|
20
29
|
async pauseRunner(runnerId, command) {
|
|
@@ -97,7 +106,7 @@ export function createExecutionApplications(root, now) {
|
|
|
97
106
|
};
|
|
98
107
|
}
|
|
99
108
|
function resolutionContext(runId, idempotencyKey, now) {
|
|
100
|
-
const commandId = `run:${runId}:resolve
|
|
109
|
+
const commandId = `run:${runId}:resolve:${idempotencyKey}`;
|
|
101
110
|
return {
|
|
102
111
|
commandId,
|
|
103
112
|
correlationId: correlationId(commandId),
|
|
@@ -6,7 +6,7 @@ import { createInterface } from 'node:readline/promises';
|
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
7
|
import { BuiltInActivityName, agentActivityDefinition } from '../activities/index.js';
|
|
8
8
|
import { IntakeHost, ResidentHost, TickHost } from '../control-plane/index.js';
|
|
9
|
-
import { ExecutionCancellationReason, RunStatus, loadPromptTemplate } from '../execution/index.js';
|
|
9
|
+
import { ExecutionCancellationReason, ExecutionFailureCode, RunStatus, loadPromptTemplate, } from '../execution/index.js';
|
|
10
10
|
import { EventActorKind, correlationId } from '../kernel/index.js';
|
|
11
11
|
import { ResourceCorrelationRole, resourceId } from '../resources/index.js';
|
|
12
12
|
import { DockerProcessError, createApiDispatcher, createApiHttpServer, createLoggedDockerCli, createPackagedAssetSource, createProcessLogSink, createSandboxDockerPort, drainProcessOutput, runDoctor, runSandbox, runSandboxEntrypoint, runSandboxSetup, runSelfUpdateLatestLoop, runTargetSmoke, verifyResidentStart, waitForActiveRuns, } from '../surfaces/index.js';
|
|
@@ -109,6 +109,26 @@ export function createSurfaceCliApplications(root, api, now) {
|
|
|
109
109
|
return root.resources.correlate(resourceId(resource), workItemId(work), ResourceCorrelationRole.Primary, commandContext(`correlate:${resource}:${work}`, now()));
|
|
110
110
|
},
|
|
111
111
|
},
|
|
112
|
+
runs: {
|
|
113
|
+
async resolve(runId, resolution) {
|
|
114
|
+
const context = commandContext(`run:${runId}:resolve`, now());
|
|
115
|
+
const run = await root.recovery.resolve(runId, resolution.status === RunStatus.Succeeded
|
|
116
|
+
? { kind: RunStatus.Succeeded, outcome: resolution.outcome }
|
|
117
|
+
: {
|
|
118
|
+
kind: RunStatus.Failed,
|
|
119
|
+
failure: { kind: ExecutionFailureCode.Unexpected, message: resolution.reason },
|
|
120
|
+
}, context);
|
|
121
|
+
if (resolution.status === RunStatus.Succeeded)
|
|
122
|
+
await root.orchestration.acceptOutcome({
|
|
123
|
+
workflowInstanceId: run.workflowInstanceId,
|
|
124
|
+
activationId: run.activationId,
|
|
125
|
+
outcome: run.outcome,
|
|
126
|
+
}, context);
|
|
127
|
+
else
|
|
128
|
+
await root.orchestration.resolveExecutionFailure(run.workflowInstanceId, { activationId: run.activationId, runId: run.runId, reason: resolution.reason }, context);
|
|
129
|
+
return run;
|
|
130
|
+
},
|
|
131
|
+
},
|
|
112
132
|
validateState: createValidationApplications(root),
|
|
113
133
|
sandboxRuntime: createSandboxRuntimeApplications(root),
|
|
114
134
|
operational: createOperationalApplications(root),
|
|
@@ -53,11 +53,14 @@ export class RecoveryService {
|
|
|
53
53
|
return loaded.view;
|
|
54
54
|
throw new Error(`Run ${id} is not escalated`);
|
|
55
55
|
}
|
|
56
|
+
const outcome = resolution.kind === RunStatus.Succeeded
|
|
57
|
+
? this.activities.validateOutcome(loaded.view.activity, resolution.outcome)
|
|
58
|
+
: undefined;
|
|
56
59
|
const draft = resolution.kind === RunStatus.Succeeded
|
|
57
60
|
? createRunExecutionEventDraft({
|
|
58
61
|
...resolutionMetadata(currentRunId, loaded.view, context),
|
|
59
62
|
eventType: ExecutionEventType.RunSucceeded,
|
|
60
|
-
payload: { outcome:
|
|
63
|
+
payload: { outcome: outcome, finishedAt: context.occurredAt },
|
|
61
64
|
})
|
|
62
65
|
: createRunExecutionEventDraft({
|
|
63
66
|
...resolutionMetadata(currentRunId, loaded.view, context),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RunStatus } from '../../../execution/index.js';
|
|
1
2
|
import { normalizePage } from './pagination.js';
|
|
2
3
|
import { accepted, ApiPathError, decodePathSegment, invalidPath, invalidQuery, invalidRequest, isObject, problem, unavailable, } from './responses.js';
|
|
3
4
|
export async function dispatchCommand(applications, url, body) {
|
|
@@ -7,7 +8,7 @@ export async function dispatchCommand(applications, url, body) {
|
|
|
7
8
|
if (ambiguityResolution !== undefined)
|
|
8
9
|
return ambiguityResolution;
|
|
9
10
|
const request = commandRequest(body);
|
|
10
|
-
if (
|
|
11
|
+
if (isHttpResponse(request))
|
|
11
12
|
return request;
|
|
12
13
|
const work = await dispatchWorkCommand(applications, url.pathname, request);
|
|
13
14
|
if (work !== undefined)
|
|
@@ -19,14 +20,14 @@ export async function dispatchCommand(applications, url, body) {
|
|
|
19
20
|
return runner ?? problem(404, 'Not Found', `No command route for ${url.pathname}`);
|
|
20
21
|
}
|
|
21
22
|
async function dispatchAmbiguousRunResolution(applications, pathname, body) {
|
|
22
|
-
const match = /^\/api\/v1\/runs\/([^/]+)\/commands\/resolve
|
|
23
|
+
const match = /^\/api\/v1\/runs\/([^/]+)\/commands\/resolve$/.exec(pathname);
|
|
23
24
|
if (match === null)
|
|
24
25
|
return undefined;
|
|
25
26
|
const runId = decodePathSegment(match[1]);
|
|
26
27
|
if (runId instanceof ApiPathError)
|
|
27
28
|
return invalidPath(runId.message);
|
|
28
|
-
const request =
|
|
29
|
-
if (
|
|
29
|
+
const request = runResolutionRequest(body);
|
|
30
|
+
if (isHttpResponse(request))
|
|
30
31
|
return request;
|
|
31
32
|
const operation = applications.execution.resolveAmbiguousRun;
|
|
32
33
|
if (operation === undefined)
|
|
@@ -98,20 +99,34 @@ function commandRequest(body) {
|
|
|
98
99
|
? { idempotencyKey: value }
|
|
99
100
|
: invalidRequest('idempotencyKey', 'Must be at most 200 characters');
|
|
100
101
|
}
|
|
101
|
-
|
|
102
|
+
// eslint-disable-next-line complexity -- each resolution mode validates its exclusive request shape.
|
|
103
|
+
function runResolutionRequest(body) {
|
|
102
104
|
if (!isObject(body))
|
|
103
105
|
return invalidRequest('idempotencyKey', 'A JSON object with an idempotency key is required');
|
|
104
|
-
if (Object.keys(body).some((key) => key !== 'idempotencyKey' && key !== 'message'))
|
|
105
|
-
return invalidRequest('', 'The command body contains unknown fields');
|
|
106
106
|
const idempotencyKey = body.idempotencyKey;
|
|
107
107
|
if (typeof idempotencyKey !== 'string' || idempotencyKey.trim() === '')
|
|
108
108
|
return invalidRequest('idempotencyKey', 'Must be a non-empty string');
|
|
109
109
|
if (idempotencyKey.length > 200)
|
|
110
110
|
return invalidRequest('idempotencyKey', 'Must be at most 200 characters');
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
111
|
+
if (body.status === RunStatus.Succeeded) {
|
|
112
|
+
if (Object.keys(body).some((key) => key !== 'idempotencyKey' && key !== 'status' && key !== 'outcome'))
|
|
113
|
+
return invalidRequest('', 'The command body contains unknown fields');
|
|
114
|
+
if (!Object.hasOwn(body, 'outcome'))
|
|
115
|
+
return invalidRequest('outcome', 'Is required for success');
|
|
116
|
+
return { idempotencyKey, status: RunStatus.Succeeded, outcome: body.outcome };
|
|
117
|
+
}
|
|
118
|
+
if (body.status === RunStatus.Failed) {
|
|
119
|
+
if (Object.keys(body).some((key) => key !== 'idempotencyKey' && key !== 'status' && key !== 'reason'))
|
|
120
|
+
return invalidRequest('', 'The command body contains unknown fields');
|
|
121
|
+
const reason = body.reason;
|
|
122
|
+
if (typeof reason !== 'string' || reason.trim() === '')
|
|
123
|
+
return invalidRequest('reason', 'Must be a non-empty string');
|
|
124
|
+
if (reason.length > 2_000)
|
|
125
|
+
return invalidRequest('reason', 'Must be at most 2000 characters');
|
|
126
|
+
return { idempotencyKey, status: RunStatus.Failed, reason };
|
|
127
|
+
}
|
|
128
|
+
return invalidRequest('status', 'Must be either succeeded or failed');
|
|
129
|
+
}
|
|
130
|
+
function isHttpResponse(value) {
|
|
131
|
+
return typeof value === 'object' && value !== null && 'body' in value;
|
|
117
132
|
}
|
|
@@ -2,7 +2,7 @@ export const executionRoutes = [
|
|
|
2
2
|
'/api/v1/runs',
|
|
3
3
|
'/api/v1/runs/:runId',
|
|
4
4
|
'/api/v1/runs/:runId/transcript',
|
|
5
|
-
'/api/v1/runs/:runId/commands/resolve
|
|
5
|
+
'/api/v1/runs/:runId/commands/resolve',
|
|
6
6
|
'/api/v1/runners',
|
|
7
7
|
'/api/v1/runners/:runnerId/commands/pause',
|
|
8
8
|
'/api/v1/runners/:runnerId/commands/unpause',
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { ExecutionStreamKind, RunStatus } from '../../execution/index.js';
|
|
1
3
|
const defaultBudget = { maxAdvances: 100, maxRuns: 100, maxDurationMs: 30_000 };
|
|
2
4
|
/** Parses Surface vocabulary only; Bootstrap supplies every application facade. */
|
|
3
5
|
// eslint-disable-next-line complexity -- command vocabulary is intentionally exhaustive at the Surface boundary.
|
|
@@ -12,6 +14,8 @@ export function parseWakeCommand(arguments_) {
|
|
|
12
14
|
resource: requiredArgument(first, 'correlate resource'),
|
|
13
15
|
workItemId: requiredArgument(second, 'correlate work item'),
|
|
14
16
|
};
|
|
17
|
+
case ExecutionStreamKind.Run:
|
|
18
|
+
return parseRunCommand(arguments_.slice(1));
|
|
15
19
|
case 'validate-state':
|
|
16
20
|
return parseValidateState(arguments_.slice(1));
|
|
17
21
|
case 'api':
|
|
@@ -33,6 +37,64 @@ export function parseWakeCommand(arguments_) {
|
|
|
33
37
|
throw new Error(`Unknown wake command: ${command ?? ''}`);
|
|
34
38
|
}
|
|
35
39
|
}
|
|
40
|
+
// eslint-disable-next-line complexity -- the resolution flags are mutually exclusive by design.
|
|
41
|
+
function parseRunCommand(arguments_) {
|
|
42
|
+
if (arguments_[0] !== 'resolve')
|
|
43
|
+
throw new Error(`Unknown run command: ${arguments_[0] ?? ''}`);
|
|
44
|
+
const runId = requiredArgument(arguments_[1], 'run id');
|
|
45
|
+
const values = new Map();
|
|
46
|
+
for (let index = 2; index < arguments_.length; index += 1) {
|
|
47
|
+
const flag = arguments_[index];
|
|
48
|
+
if (flag === '--succeeded' || flag === '--failed') {
|
|
49
|
+
if (values.has(flag))
|
|
50
|
+
throw new Error(`Duplicate option: ${flag}`);
|
|
51
|
+
values.set(flag, '');
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (flag !== '--outcome' && flag !== '--outcome-file' && flag !== '--reason')
|
|
55
|
+
throw new Error(`Unknown option: ${flag}`);
|
|
56
|
+
const value = arguments_[index + 1];
|
|
57
|
+
if (value === undefined || value.startsWith('--'))
|
|
58
|
+
throw new Error(`Missing value for ${flag}`);
|
|
59
|
+
if (values.has(flag))
|
|
60
|
+
throw new Error(`Duplicate option: ${flag}`);
|
|
61
|
+
values.set(flag, value);
|
|
62
|
+
index += 1;
|
|
63
|
+
}
|
|
64
|
+
const succeeded = values.has('--succeeded');
|
|
65
|
+
const failed = values.has('--failed');
|
|
66
|
+
if (succeeded === failed)
|
|
67
|
+
throw new Error('run resolve requires exactly one of --succeeded or --failed');
|
|
68
|
+
if (succeeded) {
|
|
69
|
+
const outcome = values.get('--outcome');
|
|
70
|
+
const outcomeFile = values.get('--outcome-file');
|
|
71
|
+
if ((outcome === undefined) === (outcomeFile === undefined))
|
|
72
|
+
throw new Error('Successful resolution requires exactly one of --outcome or --outcome-file');
|
|
73
|
+
if (values.has('--reason'))
|
|
74
|
+
throw new Error('--reason is only valid with --failed');
|
|
75
|
+
return {
|
|
76
|
+
kind: 'run-resolve',
|
|
77
|
+
runId,
|
|
78
|
+
resolution: outcome === undefined
|
|
79
|
+
? { status: RunStatus.Succeeded, outcomeFile: outcomeFile }
|
|
80
|
+
: { status: RunStatus.Succeeded, outcome: parseJson(outcome) },
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (values.has('--outcome') || values.has('--outcome-file'))
|
|
84
|
+
throw new Error('--outcome and --outcome-file are only valid with --succeeded');
|
|
85
|
+
const reason = values.get('--reason');
|
|
86
|
+
if (reason === undefined || reason.trim() === '')
|
|
87
|
+
throw new Error('Failed resolution requires --reason <message>');
|
|
88
|
+
return { kind: 'run-resolve', runId, resolution: { status: RunStatus.Failed, reason } };
|
|
89
|
+
}
|
|
90
|
+
function parseJson(value) {
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(value);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
throw new Error('Outcome must be valid JSON');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
36
98
|
function parseValidateState(arguments_) {
|
|
37
99
|
let rebuildProjections = false;
|
|
38
100
|
let wakeRoot;
|
|
@@ -110,6 +172,9 @@ export async function runWakeCommand(command, applications, output, signal) {
|
|
|
110
172
|
case 'correlate':
|
|
111
173
|
output.write(`${JSON.stringify(await applications.correlate.correlate(command.resource, command.workItemId))}\n`);
|
|
112
174
|
return;
|
|
175
|
+
case 'run-resolve':
|
|
176
|
+
output.write(`${JSON.stringify(await runs(applications).resolve(command.runId, await resolveCliOutcome(command.resolution)))}\n`);
|
|
177
|
+
return;
|
|
113
178
|
case 'validate-state': {
|
|
114
179
|
if (command.rebuildProjections)
|
|
115
180
|
await applications.validateState.rebuildProjections();
|
|
@@ -118,6 +183,21 @@ export async function runWakeCommand(command, applications, output, signal) {
|
|
|
118
183
|
}
|
|
119
184
|
}
|
|
120
185
|
}
|
|
186
|
+
async function resolveCliOutcome(resolution) {
|
|
187
|
+
if (resolution.status === RunStatus.Failed)
|
|
188
|
+
return resolution;
|
|
189
|
+
return {
|
|
190
|
+
status: RunStatus.Succeeded,
|
|
191
|
+
outcome: resolution.outcomeFile === undefined
|
|
192
|
+
? resolution.outcome
|
|
193
|
+
: parseJson(await readFile(resolution.outcomeFile, 'utf8')),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function runs(applications) {
|
|
197
|
+
if (applications.runs === undefined)
|
|
198
|
+
throw new Error('Run CLI applications were not composed');
|
|
199
|
+
return applications.runs;
|
|
200
|
+
}
|
|
121
201
|
function writeResult(output, value) {
|
|
122
202
|
if (value !== undefined)
|
|
123
203
|
output.write(`${JSON.stringify(value)}\n`);
|
|
@@ -12,11 +12,14 @@ export const usage = [
|
|
|
12
12
|
' wake ui Run the control-plane UI server',
|
|
13
13
|
' wake audit Show autonomous decision audit history',
|
|
14
14
|
' wake correlate Manually correlate a resource to a work item',
|
|
15
|
+
' wake run resolve Resolve an escalated ambiguous run',
|
|
15
16
|
' wake doctor Diagnose config/GitHub/Docker/sandbox setup problems',
|
|
16
17
|
' wake --version Print the installed Wake version',
|
|
17
18
|
' wake --help Show this message',
|
|
18
19
|
'',
|
|
19
20
|
'Additional target commands:',
|
|
21
|
+
' wake run resolve <run-id> --succeeded (--outcome <json> | --outcome-file <path>)',
|
|
22
|
+
' wake run resolve <run-id> --failed --reason <message>',
|
|
20
23
|
' wake api Run the target API surface',
|
|
21
24
|
' wake sandbox-entrypoint Run the sandbox resident entrypoint',
|
|
22
25
|
' wake self-update Safely update a source installation',
|