experimental-ash 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/docs/internals/hooks.md +13 -16
  3. package/dist/docs/internals/message-runtime.md +1 -1
  4. package/dist/docs/public/auth-and-route-protection.md +3 -3
  5. package/dist/docs/public/typescript-api.md +2 -2
  6. package/dist/src/channel/types.d.ts +10 -12
  7. package/dist/src/chunks/{dev-authored-source-watcher-d_35Mp8T.js → dev-authored-source-watcher-B4PaZGUr.js} +1 -1
  8. package/dist/src/chunks/host-DsW72Q-w.js +65 -0
  9. package/dist/src/chunks/{paths-YoCQlavu.js → paths-OknjaYR8.js} +24 -24
  10. package/dist/src/chunks/prewarm-B4YblQ5m.js +6 -0
  11. package/dist/src/cli/commands/info.js +1 -1
  12. package/dist/src/cli/run.js +1 -1
  13. package/dist/src/compiled/.vendor-stamp.json +2 -2
  14. package/dist/src/compiled/@workflow/core/classify-error.d.ts +4 -3
  15. package/dist/src/compiled/@workflow/core/encryption.d.ts +7 -1
  16. package/dist/src/compiled/@workflow/core/index.js +2 -2
  17. package/dist/src/compiled/@workflow/core/package.json +1 -1
  18. package/dist/src/compiled/@workflow/core/runtime/constants.d.ts +47 -0
  19. package/dist/src/compiled/@workflow/core/runtime/replay-budget.d.ts +98 -0
  20. package/dist/src/compiled/@workflow/core/runtime.js +27 -27
  21. package/dist/src/compiled/@workflow/core/serialization/types.d.ts +14 -0
  22. package/dist/src/compiled/@workflow/core/serialization.d.ts +8 -0
  23. package/dist/src/compiled/@workflow/core/symbols.d.ts +16 -0
  24. package/dist/src/compiled/@workflow/core/version.d.ts +1 -1
  25. package/dist/src/compiled/@workflow/core/workflow.js +1 -1
  26. package/dist/src/compiled/@workflow/errors/error-codes.d.ts +5 -1
  27. package/dist/src/compiled/@workflow/errors/index.d.ts +15 -1
  28. package/dist/src/compiled/@workflow/errors/index.js +1 -1
  29. package/dist/src/compiled/@workflow/errors/package.json +1 -1
  30. package/dist/src/compiled/_chunks/workflow/{context-errors-zbKocOyk.js → context-errors-Bbvvp-li.js} +2 -2
  31. package/dist/src/compiled/_chunks/workflow/{dist-0iNBqPYp.js → dist-C7wPwOI9.js} +2 -2
  32. package/dist/src/compiled/_chunks/workflow/{dist-D774SUM4.js → dist-C_oiE-l7.js} +1 -1
  33. package/dist/src/compiled/_chunks/workflow/resume-hook-C3VWUPii.js +12 -0
  34. package/dist/src/compiled/_chunks/workflow/sleep-QTkC1VFe.js +1 -0
  35. package/dist/src/compiled/_chunks/workflow/{symbols-D-4tVV8x.js → symbols-QezhMuLg.js} +1 -1
  36. package/dist/src/evals/cli/eval.js +1 -1
  37. package/dist/src/execution/await-authorization-orchestrator.d.ts +2 -1
  38. package/dist/src/execution/await-authorization-orchestrator.js +4 -0
  39. package/dist/src/execution/connection-auth-steps.d.ts +4 -0
  40. package/dist/src/execution/connection-auth-steps.js +9 -11
  41. package/dist/src/execution/node-step.d.ts +4 -5
  42. package/dist/src/execution/subagent-adapter.d.ts +0 -27
  43. package/dist/src/execution/subagent-adapter.js +2 -66
  44. package/dist/src/execution/subagent-hitl-proxy.d.ts +2 -2
  45. package/dist/src/execution/subagent-hitl-proxy.js +2 -2
  46. package/dist/src/execution/task-mode.d.ts +3 -3
  47. package/dist/src/execution/task-mode.js +3 -3
  48. package/dist/src/execution/turn-workflow.d.ts +41 -0
  49. package/dist/src/execution/turn-workflow.js +96 -0
  50. package/dist/src/execution/workflow-entry.js +77 -87
  51. package/dist/src/execution/workflow-errors.d.ts +14 -0
  52. package/dist/src/execution/workflow-errors.js +54 -0
  53. package/dist/src/execution/workflow-runtime.d.ts +34 -3
  54. package/dist/src/execution/workflow-runtime.js +52 -10
  55. package/dist/src/execution/workflow-steps.d.ts +27 -2
  56. package/dist/src/execution/workflow-steps.js +31 -26
  57. package/dist/src/harness/instrumentation-config.js +14 -7
  58. package/dist/src/harness/messages.d.ts +7 -7
  59. package/dist/src/harness/messages.js +4 -4
  60. package/dist/src/harness/runtime-actions.d.ts +4 -4
  61. package/dist/src/internal/application/package.js +1 -1
  62. package/dist/src/internal/workflow-bundle/workflow-builders.d.ts +1 -1
  63. package/dist/src/internal/workflow-bundle/workflow-builders.js +20 -8
  64. package/dist/src/internal/workflow-bundle/workflow-transformer.d.ts +13 -0
  65. package/dist/src/internal/workflow-bundle/workflow-transformer.js +10 -4
  66. package/package.json +4 -4
  67. package/dist/src/chunks/host-tji7W0Nn.js +0 -65
  68. package/dist/src/chunks/prewarm-6duWvvb5.js +0 -6
  69. package/dist/src/compiled/_chunks/workflow/resume-hook-CL8Ed91K.js +0 -12
  70. package/dist/src/compiled/_chunks/workflow/sleep-Dn3i9nxI.js +0 -1
  71. package/dist/src/execution/continuous-entry.d.ts +0 -59
  72. package/dist/src/execution/continuous-entry.js +0 -487
  73. package/dist/src/execution/continuous-runtime.d.ts +0 -17
  74. package/dist/src/execution/continuous-runtime.js +0 -123
@@ -2,8 +2,8 @@
2
2
  * Creates the invariant error raised when task-mode execution attempts to
3
3
  * park instead of finishing inside the current invocation.
4
4
  *
5
- * Thrown by `continuousEntry` and `workflow-entry.ts` when the harness
6
- * returns `next: null` in task mode. The message is stable so callers can
7
- * assert against it in tests.
5
+ * Thrown by `workflow-entry.ts` when the harness returns `next: null`
6
+ * in task mode. The message is stable so callers can assert against it
7
+ * in tests.
8
8
  */
9
9
  export declare function createTaskModeWaitError(): Error;
@@ -3,9 +3,9 @@ const TASK_MODE_WAIT_ERROR_MESSAGE = "Task mode cannot wait for follow-up input
3
3
  * Creates the invariant error raised when task-mode execution attempts to
4
4
  * park instead of finishing inside the current invocation.
5
5
  *
6
- * Thrown by `continuousEntry` and `workflow-entry.ts` when the harness
7
- * returns `next: null` in task mode. The message is stable so callers can
8
- * assert against it in tests.
6
+ * Thrown by `workflow-entry.ts` when the harness returns `next: null`
7
+ * in task mode. The message is stable so callers can assert against it
8
+ * in tests.
9
9
  */
10
10
  export function createTaskModeWaitError() {
11
11
  return new Error(TASK_MODE_WAIT_ERROR_MESSAGE);
@@ -0,0 +1,41 @@
1
+ import type { HookPayload, SessionCapabilities } from "#channel/types.js";
2
+ import type { HarnessSession } from "#harness/types.js";
3
+ import type { RunMode } from "#run-mode.js";
4
+ export interface TurnResultPayload {
5
+ readonly action: "done" | "park";
6
+ readonly kind: "turn-result";
7
+ readonly output?: string;
8
+ readonly serializedContext: Record<string, unknown>;
9
+ readonly session: HarnessSession;
10
+ }
11
+ export interface TurnErrorPayload {
12
+ readonly error: unknown;
13
+ readonly kind: "turn-error";
14
+ }
15
+ export type TurnCompletionPayload = TurnResultPayload | TurnErrorPayload;
16
+ export interface TurnWorkflowInput {
17
+ readonly capabilities: SessionCapabilities | undefined;
18
+ readonly completionToken: string;
19
+ readonly delivery: HookPayload;
20
+ readonly mode: RunMode;
21
+ readonly parentWritable: WritableStream<Uint8Array>;
22
+ readonly serializedContext: Record<string, unknown>;
23
+ readonly session: HarnessSession;
24
+ }
25
+ /**
26
+ * Short-lived workflow that owns one runtime turn for the durable
27
+ * driver.
28
+ *
29
+ * `parentWritable` is received from the driver input object and
30
+ * threaded into every step so the child's writes land directly on the
31
+ * driver run's stream.
32
+ */
33
+ export declare function turnWorkflow(input: TurnWorkflowInput): Promise<void>;
34
+ /**
35
+ * Completes a driver-owned one-shot hook with the result of the child
36
+ * turn workflow.
37
+ */
38
+ export declare function notifyDriverStep(input: {
39
+ readonly completionToken: string;
40
+ readonly payload: TurnCompletionPayload;
41
+ }): Promise<void>;
@@ -0,0 +1,96 @@
1
+ import { hasPendingInputBatch } from "#harness/input-requests.js";
2
+ import { hasPendingRuntimeActionBatch } from "#harness/runtime-actions.js";
3
+ import { awaitAuthorizationAndResolve } from "#execution/await-authorization-orchestrator.js";
4
+ import { createTaskModeWaitError } from "#execution/task-mode.js";
5
+ import { normalizeSerializableError } from "#execution/workflow-errors.js";
6
+ import { turnStep } from "#execution/workflow-steps.js";
7
+ /**
8
+ * Short-lived workflow that owns one runtime turn for the durable
9
+ * driver.
10
+ *
11
+ * `parentWritable` is received from the driver input object and
12
+ * threaded into every step so the child's writes land directly on the
13
+ * driver run's stream.
14
+ */
15
+ export async function turnWorkflow(input) {
16
+ "use workflow";
17
+ let currentSession = input.session;
18
+ let currentSerializedContext = input.serializedContext;
19
+ let currentInput = input.delivery;
20
+ const parentWritable = input.parentWritable;
21
+ try {
22
+ while (true) {
23
+ const result = await turnStep({
24
+ input: currentInput,
25
+ parentWritable,
26
+ serializedContext: currentSerializedContext,
27
+ session: currentSession,
28
+ });
29
+ currentSession = result.session;
30
+ currentSerializedContext = result.serializedContext;
31
+ if (result.action === "done") {
32
+ await notifyDriverStep({
33
+ completionToken: input.completionToken,
34
+ payload: {
35
+ action: "done",
36
+ kind: "turn-result",
37
+ output: result.output ?? "",
38
+ serializedContext: currentSerializedContext,
39
+ session: currentSession,
40
+ },
41
+ });
42
+ return;
43
+ }
44
+ if (result.action === "park") {
45
+ if (hasPendingRuntimeActionBatch(currentSession) ||
46
+ (hasPendingInputBatch(currentSession) && input.capabilities?.requestInput === true) ||
47
+ input.mode === "conversation") {
48
+ await notifyDriverStep({
49
+ completionToken: input.completionToken,
50
+ payload: {
51
+ action: "park",
52
+ kind: "turn-result",
53
+ serializedContext: currentSerializedContext,
54
+ session: currentSession,
55
+ },
56
+ });
57
+ return;
58
+ }
59
+ throw createTaskModeWaitError();
60
+ }
61
+ if (result.action === "await-authorization") {
62
+ const resolved = await awaitAuthorizationAndResolve({
63
+ parentWritable,
64
+ pendingAuths: result.pendingAuths,
65
+ pendingToolCalls: result.pendingToolCalls,
66
+ serializedContext: currentSerializedContext,
67
+ session: currentSession,
68
+ });
69
+ currentSession = resolved.session;
70
+ currentSerializedContext = resolved.serializedContext;
71
+ currentInput = undefined;
72
+ continue;
73
+ }
74
+ currentInput = undefined;
75
+ }
76
+ }
77
+ catch (error) {
78
+ await notifyDriverStep({
79
+ completionToken: input.completionToken,
80
+ payload: {
81
+ error: normalizeSerializableError(error),
82
+ kind: "turn-error",
83
+ },
84
+ });
85
+ throw error;
86
+ }
87
+ }
88
+ /**
89
+ * Completes a driver-owned one-shot hook with the result of the child
90
+ * turn workflow.
91
+ */
92
+ export async function notifyDriverStep(input) {
93
+ "use step";
94
+ const { resumeHook } = await import("#compiled/@workflow/core/runtime.js");
95
+ await resumeHook(input.completionToken, input.payload);
96
+ }
@@ -1,15 +1,13 @@
1
- import { createHook, getWorkflowMetadata } from "#compiled/@workflow/core/index.js";
1
+ import { createHook, getWorkflowMetadata, getWritable, } from "#compiled/@workflow/core/index.js";
2
2
  import { SUBAGENT_ADAPTER_KIND } from "#execution/subagent-adapter.js";
3
3
  import { ChannelKey } from "#context/keys.js";
4
4
  import { deserializeContext } from "#context/serialize.js";
5
- import { hasPendingInputBatch } from "#harness/input-requests.js";
6
5
  import { coalesceDeliveries } from "#harness/messages.js";
7
6
  import { hasProxyInputRequests } from "#harness/proxy-input-requests.js";
8
7
  import { accumulateRuntimeActionResults, hasPendingRuntimeActionBatch, } from "#harness/runtime-actions.js";
9
8
  import { toErrorMessage } from "#shared/errors.js";
10
- import { awaitAuthorizationAndResolve } from "#execution/await-authorization-orchestrator.js";
11
- import { createTaskModeWaitError } from "#execution/task-mode.js";
12
- import { createSessionStep, dispatchPendingRuntimeActionsStep, durableRunStep, emitTerminalSessionFailureStep, routeProxiedDeliverStep, runProxyInputRequestStep, } from "#execution/workflow-steps.js";
9
+ import { normalizeSerializableError, rebuildSerializableError, } from "#execution/workflow-errors.js";
10
+ import { createSessionStep, dispatchTurnStep, dispatchPendingRuntimeActionsStep, emitTerminalSessionFailureStep, routeProxiedDeliverStep, runProxyInputRequestStep, } from "#execution/workflow-steps.js";
13
11
  /**
14
12
  * Long-lived workflow entrypoint for the durable runtime.
15
13
  *
@@ -30,6 +28,7 @@ export async function workflowEntry(input) {
30
28
  // failure emitter can stamp it onto the `session.failed` event
31
29
  // even if `createSessionStep` itself throws.
32
30
  input.serializedContext["ash.sessionId"] = sessionId;
31
+ const driverWritable = getWritable();
33
32
  try {
34
33
  const session = await createSessionStep({
35
34
  compiledArtifactsSource: serializedBundle.source,
@@ -37,8 +36,9 @@ export async function workflowEntry(input) {
37
36
  nodeId: serializedBundle.nodeId,
38
37
  sessionId,
39
38
  });
40
- return await runWorkflowLoop({
39
+ return await runDriverLoop({
41
40
  capabilities,
41
+ driverWritable,
42
42
  initialInput: {
43
43
  kind: "deliver",
44
44
  payloads: [{ message: input.input.message, modelContext: input.input.modelContext }],
@@ -61,6 +61,7 @@ export async function workflowEntry(input) {
61
61
  // itself cannot import `internal/logging.ts` (node:util gate).
62
62
  await emitTerminalSessionFailureStep({
63
63
  error: normalizeSerializableError(error),
64
+ parentWritable: driverWritable,
64
65
  serializedContext: input.serializedContext,
65
66
  });
66
67
  await notifyDelegatedParentStep({
@@ -70,34 +71,19 @@ export async function workflowEntry(input) {
70
71
  throw error;
71
72
  }
72
73
  }
73
- /**
74
- * Reduces an arbitrary throwable to a shape that survives the
75
- * serialization the workflow devkit performs on step inputs.
76
- *
77
- * Native `Error` objects do not cross the step boundary cleanly —
78
- * their stack, cause chain, and non-enumerable `message` / `name`
79
- * fields are dropped by structured clone. We project the error onto
80
- * a plain object whose own properties the downstream step can inspect
81
- * via the same `util.inspect` dump the logger uses.
82
- */
83
- function normalizeSerializableError(error) {
84
- if (!(error instanceof Error)) {
85
- return error;
86
- }
87
- return {
88
- // Enumerable own properties (HTTP statusCode, responseBody, etc.)
89
- // fan out first so the pinned fields below win on collision.
90
- ...error,
91
- message: error.message,
92
- name: error.name,
93
- stack: error.stack,
94
- cause: error.cause === undefined ? undefined : normalizeSerializableError(error.cause),
95
- };
96
- }
97
- async function runWorkflowLoop(input) {
74
+ async function runDriverLoop(input) {
98
75
  let currentSession = input.session;
99
76
  let currentSerializedContext = input.serializedContext;
100
- let currentResult = await runTurn(currentSerializedContext, currentSession, input.initialInput, input.mode, input.capabilities);
77
+ let turnGeneration = 0;
78
+ let currentResult = await dispatchAndAwaitTurn({
79
+ capabilities: input.capabilities,
80
+ delivery: input.initialInput,
81
+ mode: input.mode,
82
+ parentWritable: input.driverWritable,
83
+ serializedContext: currentSerializedContext,
84
+ session: currentSession,
85
+ turnGeneration: ++turnGeneration,
86
+ });
101
87
  const bufferedDeliveries = [];
102
88
  currentSession = currentResult.session;
103
89
  currentSerializedContext = currentResult.serializedContext;
@@ -112,7 +98,7 @@ async function runWorkflowLoop(input) {
112
98
  // `ctx.session.setContinuationToken(...)` (e.g. Slack auto-anchor on
113
99
  // first post). `currentSession.continuationToken` already reflects
114
100
  // the latest token thanks to `reconcileSessionContinuationToken`
115
- // inside `durableRunStep`, so creating the park hook here lands at
101
+ // inside `turnStep`, so creating the park hook here lands at
116
102
  // the right token from the start.
117
103
  if (!currentSession.continuationToken) {
118
104
  throw new Error("Cannot park: no continuation token available. The channel must " +
@@ -153,6 +139,7 @@ async function runWorkflowLoop(input) {
153
139
  while (true) {
154
140
  if (hasPendingRuntimeActionBatch(currentSession)) {
155
141
  currentSession = await dispatchPendingRuntimeActionsStep({
142
+ parentWritable: input.driverWritable,
156
143
  serializedContext: currentSerializedContext,
157
144
  session: currentSession,
158
145
  });
@@ -161,6 +148,7 @@ async function runWorkflowLoop(input) {
161
148
  getNextPromise,
162
149
  consumeNext,
163
150
  rekeyHook,
151
+ parentWritable: input.driverWritable,
164
152
  serializedContext: currentSerializedContext,
165
153
  session: currentSession,
166
154
  });
@@ -169,10 +157,18 @@ async function runWorkflowLoop(input) {
169
157
  }
170
158
  currentSession = runtimeResults.session;
171
159
  currentSerializedContext = runtimeResults.serializedContext;
172
- currentResult = await runTurn(currentSerializedContext, currentSession, {
173
- kind: "runtime-action-result",
174
- results: runtimeResults.results,
175
- }, input.mode, input.capabilities);
160
+ currentResult = await dispatchAndAwaitTurn({
161
+ capabilities: input.capabilities,
162
+ delivery: {
163
+ kind: "runtime-action-result",
164
+ results: runtimeResults.results,
165
+ },
166
+ mode: input.mode,
167
+ parentWritable: input.driverWritable,
168
+ serializedContext: currentSerializedContext,
169
+ session: currentSession,
170
+ turnGeneration: ++turnGeneration,
171
+ });
176
172
  }
177
173
  else {
178
174
  const nextDeliver = await waitForNextDeliver({
@@ -185,6 +181,7 @@ async function runWorkflowLoop(input) {
185
181
  }
186
182
  const remainder = await routeDeliverForChildren({
187
183
  auth: nextDeliver.auth,
184
+ parentWritable: input.driverWritable,
188
185
  payloads: nextDeliver.payloads,
189
186
  session: currentSession,
190
187
  });
@@ -193,11 +190,19 @@ async function runWorkflowLoop(input) {
193
190
  // model turn to run right now.
194
191
  continue;
195
192
  }
196
- currentResult = await runTurn(currentSerializedContext, currentSession, {
197
- auth: nextDeliver.auth,
198
- kind: "deliver",
199
- payloads: [remainder],
200
- }, input.mode, input.capabilities);
193
+ currentResult = await dispatchAndAwaitTurn({
194
+ capabilities: input.capabilities,
195
+ delivery: {
196
+ auth: nextDeliver.auth,
197
+ kind: "deliver",
198
+ payloads: [remainder],
199
+ },
200
+ mode: input.mode,
201
+ parentWritable: input.driverWritable,
202
+ serializedContext: currentSerializedContext,
203
+ session: currentSession,
204
+ turnGeneration: ++turnGeneration,
205
+ });
201
206
  }
202
207
  currentSession = currentResult.session;
203
208
  currentSerializedContext = currentResult.serializedContext;
@@ -217,59 +222,41 @@ async function runWorkflowLoop(input) {
217
222
  }
218
223
  return { output: "" };
219
224
  }
220
- /**
221
- * Runs one full turn: loops `durableRunStep` until the harness parks or
222
- * signals done. Tool-loop continuations (`action: "continue"`) stay
223
- * inside this loop so each runtime resume maps to exactly one turn.
224
- */
225
- async function runTurn(serializedContext, session, input, mode, capabilities) {
226
- let currentSession = session;
227
- let currentInput = input;
228
- let currentSerializedContext = serializedContext;
229
- while (true) {
230
- const result = await durableRunStep(currentSerializedContext, currentSession, currentInput);
231
- currentSession = result.session;
232
- currentSerializedContext = result.serializedContext;
233
- if (result.action === "done") {
234
- return result;
235
- }
236
- if (result.action === "park") {
237
- if (hasPendingRuntimeActionBatch(currentSession)) {
238
- return result;
239
- }
240
- if (hasPendingInputBatch(currentSession) && capabilities?.requestInput === true) {
241
- return result;
242
- }
243
- if (mode === "conversation") {
244
- return result;
245
- }
246
- throw createTaskModeWaitError();
247
- }
248
- if (result.action === "await-authorization") {
249
- // Drive the interactive-OAuth authorization cycle in the
250
- // workflow body so `createHook` is called outside a step
251
- // (required for stable callback tokens across replay) and
252
- // awaiting the hook suspends the run.
253
- const resolved = await awaitAuthorizationAndResolve({
254
- pendingToolCalls: result.pendingToolCalls,
255
- pendingAuths: result.pendingAuths,
256
- serializedContext: currentSerializedContext,
257
- session: currentSession,
258
- });
259
- currentSession = resolved.session;
260
- currentSerializedContext = resolved.serializedContext;
261
- currentInput = undefined;
262
- continue;
225
+ async function dispatchAndAwaitTurn(input) {
226
+ const completionToken = `ash://turn/${input.session.sessionId}/${input.turnGeneration}`;
227
+ const completion = createHook({ token: completionToken });
228
+ try {
229
+ await dispatchTurnStep({
230
+ capabilities: input.capabilities,
231
+ completionToken,
232
+ delivery: input.delivery,
233
+ mode: input.mode,
234
+ parentWritable: input.parentWritable,
235
+ serializedContext: input.serializedContext,
236
+ session: input.session,
237
+ });
238
+ const payload = await awaitHookPayload(completion);
239
+ if (payload.kind === "turn-error") {
240
+ throw rebuildSerializableError(payload.error);
263
241
  }
264
- currentInput = undefined;
242
+ return payload;
243
+ }
244
+ finally {
245
+ await disposeHook(completion);
246
+ }
247
+ }
248
+ async function awaitHookPayload(hook) {
249
+ for await (const value of hook) {
250
+ return value;
265
251
  }
252
+ throw new Error("Turn completion hook closed before delivering a result.");
266
253
  }
267
254
  async function waitForPendingRuntimeActionResults(input) {
268
255
  let currentSession = input.session;
269
256
  // The proxied HITL step persists adapter-state mutations on the
270
257
  // context (e.g. Slack's `pendingRequests` cache) and returns the
271
258
  // updated serialized context. We thread that forward so subsequent
272
- // deliveries and the eventual post-wait `durableRunStep` observe
259
+ // deliveries and the eventual post-wait `turnStep` observe
273
260
  // the cached state instead of re-deserializing a stale snapshot.
274
261
  let currentSerializedContext = input.serializedContext;
275
262
  const results = await accumulateRuntimeActionResults({
@@ -292,6 +279,7 @@ async function waitForPendingRuntimeActionResults(input) {
292
279
  // parent↔child deadlock this branch exists to prevent.
293
280
  const remainder = await routeDeliverForChildren({
294
281
  auth: value.auth,
282
+ parentWritable: input.parentWritable,
295
283
  payloads: value.payloads,
296
284
  session: currentSession,
297
285
  });
@@ -312,6 +300,7 @@ async function waitForPendingRuntimeActionResults(input) {
312
300
  // parent's adapter, record the routing entry, and keep waiting.
313
301
  const proxyResult = await runProxyInputRequestStep({
314
302
  hookPayload: value,
303
+ parentWritable: input.parentWritable,
315
304
  serializedContext: currentSerializedContext,
316
305
  session: currentSession,
317
306
  });
@@ -350,6 +339,7 @@ async function routeDeliverForChildren(input) {
350
339
  }
351
340
  const routed = await routeProxiedDeliverStep({
352
341
  auth: input.auth,
342
+ parentWritable: input.parentWritable,
353
343
  payload: coalesced,
354
344
  session: input.session,
355
345
  });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Serializes and rebuilds workflow errors across hook and step
3
+ * boundaries.
4
+ */
5
+ /**
6
+ * Reduces an arbitrary throwable to a shape that survives the
7
+ * serialization the workflow devkit performs on step inputs.
8
+ */
9
+ export declare function normalizeSerializableError(error: unknown): unknown;
10
+ /**
11
+ * Rebuilds an {@link Error} from the normalized hook payload sent by a
12
+ * child workflow.
13
+ */
14
+ export declare function rebuildSerializableError(error: unknown): Error;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Serializes and rebuilds workflow errors across hook and step
3
+ * boundaries.
4
+ */
5
+ /**
6
+ * Reduces an arbitrary throwable to a shape that survives the
7
+ * serialization the workflow devkit performs on step inputs.
8
+ */
9
+ export function normalizeSerializableError(error) {
10
+ if (!(error instanceof Error)) {
11
+ return error;
12
+ }
13
+ const ownProperties = Object.fromEntries(Object.entries(error));
14
+ return {
15
+ ...ownProperties,
16
+ cause: error.cause === undefined ? undefined : normalizeSerializableError(error.cause),
17
+ message: error.message,
18
+ name: error.name,
19
+ stack: error.stack,
20
+ };
21
+ }
22
+ /**
23
+ * Rebuilds an {@link Error} from the normalized hook payload sent by a
24
+ * child workflow.
25
+ */
26
+ export function rebuildSerializableError(error) {
27
+ if (!isRecord(error)) {
28
+ return new Error(String(error));
29
+ }
30
+ const message = typeof error.message === "string" ? error.message : String(error);
31
+ const rebuilt = new Error(message);
32
+ if (typeof error.name === "string") {
33
+ rebuilt.name = error.name;
34
+ }
35
+ if (typeof error.stack === "string") {
36
+ rebuilt.stack = error.stack;
37
+ }
38
+ if ("cause" in error) {
39
+ rebuilt.cause = isRecord(error.cause)
40
+ ? rebuildSerializableError(error.cause)
41
+ : error.cause;
42
+ }
43
+ const mutable = rebuilt;
44
+ for (const [key, value] of Object.entries(error)) {
45
+ if (key === "message" || key === "name" || key === "stack" || key === "cause") {
46
+ continue;
47
+ }
48
+ mutable[key] = value;
49
+ }
50
+ return rebuilt;
51
+ }
52
+ function isRecord(value) {
53
+ return value !== null && typeof value === "object";
54
+ }
@@ -1,17 +1,48 @@
1
+ import type { Run } from "#compiled/@workflow/core/runtime.js";
2
+ import type { WorkflowFunction, WorkflowMetadata } from "#compiled/@workflow/core/runtime/start.js";
1
3
  import type { Runtime } from "#channel/types.js";
2
4
  import type { RuntimeCompiledArtifactsSource } from "#runtime/compiled-artifacts-source.js";
5
+ export declare const LATEST_DEPLOYMENT_UNSUPPORTED_MESSAGE = "deploymentId 'latest' requires a World that implements resolveLatestDeploymentId()";
6
+ /**
7
+ * Workflow function names whose bundled id is stable across deployments
8
+ * (no `@<pkg.version>` stamp). The bundler reads this set when emitting
9
+ * the workflow id so cross-deployment routing — `start(ref, args, {
10
+ * deploymentId: "latest" })` — finds the same workflow on a newer
11
+ * deployment even when the ash version differs.
12
+ *
13
+ * Both halves of the contract (bundler output and runtime reference
14
+ * template) read this single set so they cannot drift.
15
+ */
16
+ export declare const STABLE_WORKFLOW_NAMES: ReadonlySet<string>;
3
17
  /**
4
18
  * Stable workflow reference used by `start()` to locate the workflow
5
- * entrypoint registered by the Workflow DevKit builder.
19
+ * entrypoint registered by the Workflow DevKit builder. The id omits
20
+ * the package version stamp so the long-lived driver can rotate across
21
+ * deployments without rewriting the registry key.
6
22
  */
7
23
  export declare const workflowEntryReference: {
8
24
  workflowId: string;
9
25
  };
10
26
  /**
11
- * Creates a workflow-backed runtime where a single long-lived workflow
12
- * spans the entire session.
27
+ * Stable workflow reference used by the driver to dispatch per-turn
28
+ * child workflow runs. The id omits the package version stamp so
29
+ * `start(turnWorkflowReference, args, { deploymentId: "latest" })`
30
+ * routes to the latest deployment's turn workflow even when the ash
31
+ * version differs from the caller's deployment.
32
+ */
33
+ export declare const turnWorkflowReference: {
34
+ workflowId: string;
35
+ };
36
+ /**
37
+ * Creates a workflow-backed runtime whose long-lived driver owns the
38
+ * session stream and dispatches each turn as a child workflow run.
13
39
  */
14
40
  export declare function createWorkflowRuntime(config: {
15
41
  readonly compiledArtifactsSource: RuntimeCompiledArtifactsSource;
16
42
  readonly nodeId?: string;
17
43
  }): Runtime;
44
+ /**
45
+ * Starts a workflow on the latest deployment when the active world supports
46
+ * it, while preserving local/dev worlds that do not implement latest routing.
47
+ */
48
+ export declare function startWorkflowPreferLatest<TArgs extends unknown[], TResult>(workflow: WorkflowFunction<TArgs, TResult> | WorkflowMetadata, args: TArgs): Promise<Run<unknown> | Run<TResult>>;
@@ -6,17 +6,46 @@ import { getCompiledRuntimeAgentBundle } from "#runtime/sessions/compiled-agent-
6
6
  import { buildRunContext } from "#execution/runtime-context.js";
7
7
  import { RuntimeNoActiveSessionError } from "#execution/runtime-errors.js";
8
8
  const WORKFLOW_ENTRY_NAME = "workflowEntry";
9
+ const TURN_WORKFLOW_NAME = "turnWorkflow";
9
10
  const ASH_PACKAGE_INFO = resolveInstalledPackageInfo();
11
+ export const LATEST_DEPLOYMENT_UNSUPPORTED_MESSAGE = "deploymentId 'latest' requires a World that implements resolveLatestDeploymentId()";
12
+ /**
13
+ * Workflow function names whose bundled id is stable across deployments
14
+ * (no `@<pkg.version>` stamp). The bundler reads this set when emitting
15
+ * the workflow id so cross-deployment routing — `start(ref, args, {
16
+ * deploymentId: "latest" })` — finds the same workflow on a newer
17
+ * deployment even when the ash version differs.
18
+ *
19
+ * Both halves of the contract (bundler output and runtime reference
20
+ * template) read this single set so they cannot drift.
21
+ */
22
+ export const STABLE_WORKFLOW_NAMES = new Set([
23
+ WORKFLOW_ENTRY_NAME,
24
+ TURN_WORKFLOW_NAME,
25
+ ]);
26
+ const STABLE_ID_BASE = ASH_PACKAGE_INFO.name;
10
27
  /**
11
28
  * Stable workflow reference used by `start()` to locate the workflow
12
- * entrypoint registered by the Workflow DevKit builder.
29
+ * entrypoint registered by the Workflow DevKit builder. The id omits
30
+ * the package version stamp so the long-lived driver can rotate across
31
+ * deployments without rewriting the registry key.
13
32
  */
14
33
  export const workflowEntryReference = {
15
- workflowId: `workflow//${ASH_PACKAGE_INFO.name}@${ASH_PACKAGE_INFO.version}//${WORKFLOW_ENTRY_NAME}`,
34
+ workflowId: `workflow//${STABLE_ID_BASE}//${WORKFLOW_ENTRY_NAME}`,
35
+ };
36
+ /**
37
+ * Stable workflow reference used by the driver to dispatch per-turn
38
+ * child workflow runs. The id omits the package version stamp so
39
+ * `start(turnWorkflowReference, args, { deploymentId: "latest" })`
40
+ * routes to the latest deployment's turn workflow even when the ash
41
+ * version differs from the caller's deployment.
42
+ */
43
+ export const turnWorkflowReference = {
44
+ workflowId: `workflow//${STABLE_ID_BASE}//${TURN_WORKFLOW_NAME}`,
16
45
  };
17
46
  /**
18
- * Creates a workflow-backed runtime where a single long-lived workflow
19
- * spans the entire session.
47
+ * Creates a workflow-backed runtime whose long-lived driver owns the
48
+ * session stream and dispatches each turn as a child workflow run.
20
49
  */
21
50
  export function createWorkflowRuntime(config) {
22
51
  return {
@@ -27,20 +56,15 @@ export function createWorkflowRuntime(config) {
27
56
  });
28
57
  const ctx = buildRunContext({ bundle, run: input });
29
58
  const serializedContext = serializeContext(ctx);
30
- const run = await start(workflowEntryReference, [
59
+ const run = await startWorkflowPreferLatest(workflowEntryReference, [
31
60
  {
32
61
  input: input.input,
33
62
  serializedContext,
34
63
  },
35
64
  ]);
36
- const result = (async () => {
37
- const wfResult = (await run.returnValue);
38
- return { status: "completed", output: wfResult.output };
39
- })();
40
65
  return {
41
66
  continuationToken: input.continuationToken ?? run.runId,
42
67
  events: parseNdjsonStream(getRun(run.runId).getReadable()),
43
- result,
44
68
  sessionId: run.runId,
45
69
  };
46
70
  },
@@ -69,6 +93,24 @@ export function createWorkflowRuntime(config) {
69
93
  },
70
94
  };
71
95
  }
96
+ /**
97
+ * Starts a workflow on the latest deployment when the active world supports
98
+ * it, while preserving local/dev worlds that do not implement latest routing.
99
+ */
100
+ export async function startWorkflowPreferLatest(workflow, args) {
101
+ try {
102
+ return await start(workflow, args, { deploymentId: "latest" });
103
+ }
104
+ catch (error) {
105
+ if (!isLatestDeploymentUnsupportedError(error)) {
106
+ throw error;
107
+ }
108
+ return await start(workflow, args);
109
+ }
110
+ }
111
+ function isLatestDeploymentUnsupportedError(error) {
112
+ return error instanceof Error && error.message.includes(LATEST_DEPLOYMENT_UNSUPPORTED_MESSAGE);
113
+ }
72
114
  function normalizeWorkflowHook(value) {
73
115
  if (value === null || typeof value !== "object" || !("runId" in value)) {
74
116
  throw new Error("Workflow hook did not include a run id.");