@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.
@@ -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.Failed,
15
- failure: { kind: ExecutionFailureCode.Unexpected, message: command.message },
16
- }, context);
17
- await root.orchestration.resolveExecutionFailure(run.workflowInstanceId, { activationId: run.activationId, runId: run.runId, reason: command.message }, context);
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-failed:${idempotencyKey}`;
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),
@@ -108,4 +108,4 @@ export function resolveWakeVersion(options = {}) {
108
108
  return `g${headHash.slice(0, 7)}`;
109
109
  return '0.1.0-dev';
110
110
  }
111
- export const wakeVersion = "gdf02688";
111
+ export const wakeVersion = "g2f2caef";
@@ -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: resolution.outcome, finishedAt: context.occurredAt },
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 ('status' in request)
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-failed$/.exec(pathname);
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 = ambiguityFailureResolutionRequest(body);
29
- if ('status' in request)
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
- function ambiguityFailureResolutionRequest(body) {
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
- const message = body.message;
112
- if (typeof message !== 'string' || message.trim() === '')
113
- return invalidRequest('message', 'Must be a non-empty string');
114
- if (message.length > 2_000)
115
- return invalidRequest('message', 'Must be at most 2000 characters');
116
- return { idempotencyKey, message };
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-failed',
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',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.3.68",
3
+ "version": "0.3.69",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {