@noetaris/harness 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -2
- package/dist/index.d.ts +64 -1
- package/dist/index.js +70 -10
- package/dist/index.js.map +1 -1
- package/package.json +9 -13
package/README.md
CHANGED
|
@@ -156,6 +156,157 @@ const resumed = run.resume(response, interruptId)
|
|
|
156
156
|
const resumed = agent.resume(response, sessionId, interruptId)
|
|
157
157
|
```
|
|
158
158
|
|
|
159
|
+
**Resume replays the interrupted step from the top.** On resume, the step that called
|
|
160
|
+
`ctx.interrupt()` runs again from its first line; each `ctx.interrupt()` call it reaches
|
|
161
|
+
returns the stored response instead of pausing. State updates from the paused attempt are
|
|
162
|
+
not kept — a step's update is applied only when `run` returns. So any side effect a step
|
|
163
|
+
performs *before* `ctx.interrupt()` (an HTTP call, a DB write, a sub agent run) happens
|
|
164
|
+
again on resume. Making that safe is up to the step: guard the side effect so a replay
|
|
165
|
+
skips or reuses it.
|
|
166
|
+
|
|
167
|
+
**Inside a fork, only paused branches replay.** When a fork pauses, each branch is saved
|
|
168
|
+
separately. On resume, a branch that already finished is not run again — its saved state
|
|
169
|
+
goes straight to the join. Every branch that is still paused runs again from its paused
|
|
170
|
+
step, even if the response you sent was for a different branch; a branch with no response
|
|
171
|
+
yet reaches its `ctx.interrupt()` again and re-pauses. The session stays paused until
|
|
172
|
+
every pending interrupt (see `agent.status(sessionId).pendingInterrupts`) is answered.
|
|
173
|
+
If any branch fails, the fork fails fast: the other branches are aborted and paused
|
|
174
|
+
branches are dropped. The same happens when a branch throws a graph-definition error
|
|
175
|
+
(for example a signal with no matching `.on()`): the other branches are aborted, and the
|
|
176
|
+
run rejects with that error only after they have stopped.
|
|
177
|
+
|
|
178
|
+
**Stopping a fork.** `stop()` while a fork runs stops each branch at its next step
|
|
179
|
+
boundary. What happens next depends on whether any branch is waiting on an interrupt:
|
|
180
|
+
|
|
181
|
+
| At the stop | Run settles with | To continue |
|
|
182
|
+
|---|---|---|
|
|
183
|
+
| No branch is waiting on an interrupt | `signal: null` — a plain stop; `agent.status()` shows `paused` with no pending interrupts | `agent.run()` on the same session continues the fork: finished branches are not run again, stopped branches continue from where they stopped. `resume()` throws `NoInterruptError`. |
|
|
184
|
+
| At least one branch (at any depth) is waiting on an interrupt | `signal: '$interrupt'` | `resume()` answers it and re-runs every paused branch, including the ones that were only stopped |
|
|
185
|
+
|
|
186
|
+
Input passed to the `agent.run()` that continues a stopped fork is merged into the main
|
|
187
|
+
state only. The branches that continue keep their own saved state and do not see it; it
|
|
188
|
+
is visible from the join onward.
|
|
189
|
+
|
|
190
|
+
#### Calling sub agents from a step
|
|
191
|
+
|
|
192
|
+
A sub agent is just another agent with its own session. When it pauses, its `run()`
|
|
193
|
+
resolves with `signal: '$interrupt'` — it does not throw — so the calling step must pass
|
|
194
|
+
the interrupt up with `ctx.interrupt()` and, after resume, call the sub agent's
|
|
195
|
+
`resume()`. Because the step replays, the call must be idempotent. Checking the sub
|
|
196
|
+
agent's session phase is enough:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
async function callSubAgent(agent: Agent, sessionId: string, input: object, ctx: any) {
|
|
200
|
+
const status = await agent.status(sessionId)
|
|
201
|
+
if (status.phase === 'completed') return loadResult(sessionId) // your own lookup — don't run again
|
|
202
|
+
let outcome
|
|
203
|
+
if (status.phase === 'paused') {
|
|
204
|
+
const pending = status.pendingInterrupts[0]!
|
|
205
|
+
const answer = await ctx.interrupt(pending.prompt, `${sessionId}:${pending.interruptId}`)
|
|
206
|
+
outcome = await agent.resume(answer, sessionId, pending.interruptId)
|
|
207
|
+
} else {
|
|
208
|
+
outcome = await agent.run(input, { sessionId })
|
|
209
|
+
}
|
|
210
|
+
if (outcome.signal === '$interrupt') {
|
|
211
|
+
const { interruptId, prompt } = outcome.state.$interrupt
|
|
212
|
+
await ctx.interrupt(prompt, `${sessionId}:${interruptId}`) // pauses the main agent
|
|
213
|
+
}
|
|
214
|
+
return outcome.state
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Use a stable sub agent `sessionId` (for example derived from the main `sessionId`) so a
|
|
219
|
+
replay finds the same session. To run several sub agents in parallel, put each in its own
|
|
220
|
+
fork branch rather than `Promise.all` in one step: then, when one pauses, the finished ones
|
|
221
|
+
are not run again on resume.
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
h.loop(l =>
|
|
225
|
+
l.start()
|
|
226
|
+
.fork('research')
|
|
227
|
+
.branch('web', b => b.start()
|
|
228
|
+
.step('callWeb', {
|
|
229
|
+
run: async (s, ctx) => ({ web: await callSubAgent(webAgent, `${ctx.sessionId}:web`, {}, ctx) }),
|
|
230
|
+
route: () => 'done',
|
|
231
|
+
})
|
|
232
|
+
.on('done').end()) // inside a branch, .end() ends the branch
|
|
233
|
+
.branch('docs', b => b.start()
|
|
234
|
+
.step('callDocs', {
|
|
235
|
+
run: async (s, ctx) => ({ docs: await callSubAgent(docsAgent, `${ctx.sessionId}:docs`, {}, ctx) }),
|
|
236
|
+
route: () => 'done',
|
|
237
|
+
})
|
|
238
|
+
.on('done').end())
|
|
239
|
+
.join('merge', { run: mergeResults, route: () => 'done' })
|
|
240
|
+
.on('done').end()
|
|
241
|
+
)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
If a fork fails fast while a sub agent is paused, that sub agent's own session stays
|
|
245
|
+
paused in its store — clean it up or reuse it the next time the step runs.
|
|
246
|
+
|
|
247
|
+
### Observers
|
|
248
|
+
|
|
249
|
+
An `Observer` receives telemetry hooks for a run. Pass it as `observer` in the resources
|
|
250
|
+
of `agent.run()` or `agent.resume()`. Combine several with `composeObservers([a, b])`;
|
|
251
|
+
a hook that throws is reported to `onObserverError` (or `console.error`) and never stops
|
|
252
|
+
the run. Every hook is optional.
|
|
253
|
+
|
|
254
|
+
| Hook | Fires |
|
|
255
|
+
|---|---|
|
|
256
|
+
| `onRunStart(ctx)` | once when a run starts |
|
|
257
|
+
| `onRunEnd(ctx, { signal, durationMs })` | once when a run settles (`$stopped` for a stop) |
|
|
258
|
+
| `onStepStart(ctx)` | before each step |
|
|
259
|
+
| `onStepEnd(ctx, { durationMs })` | after a step's `run` succeeds, before its `route` |
|
|
260
|
+
| `onStepError(ctx, { error, durationMs })` | when a step's `run` throws |
|
|
261
|
+
| `onInterrupt(ctx, { prompt, interruptId })` | when a step calls `ctx.interrupt()` |
|
|
262
|
+
| `onEvent(ctx, type, payload)` | on `ctx.emit()` and adapter events such as `llm.response` |
|
|
263
|
+
| `onStepSettled(ctx, event)` | once per step, when its outcome and destination are final |
|
|
264
|
+
|
|
265
|
+
**`onStepSettled`** is the one hook that tells you, for every step, how it ended and
|
|
266
|
+
where the run goes next. It fires exactly once for each `onStepStart`, on every path:
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
onStepStart → onStepEnd | onStepError | onInterrupt → onStepSettled → onRunEnd
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
It also fires for a fork node and for each step inside a branch (`ctx.branchPath` names the
|
|
273
|
+
branch). A graph-definition error (`UnknownSignalError`, `NoNextStepError`,
|
|
274
|
+
`MissingReducerError`) settles with `next.kind === 'throw'` before the run rejects. It does
|
|
275
|
+
not fire when a run stops before starting a step, or for a finished branch that a resume
|
|
276
|
+
skips.
|
|
277
|
+
|
|
278
|
+
| `event` field | Meaning |
|
|
279
|
+
|---|---|
|
|
280
|
+
| `outcome` | `'ok'`, `'error'`, `'interrupt'`, or `'stopped'` (a fork whose branches were stopped) |
|
|
281
|
+
| `durationMs` | from `onStepStart` until the destination is known — includes `route` |
|
|
282
|
+
| `update` | what `run` returned, present only when it was applied to state. The raw object — copy it if you keep it |
|
|
283
|
+
| `signal` | what `route` returned, when it was called |
|
|
284
|
+
| `next` | `{ kind: 'step', name }`, `{ kind: 'end' }`, `{ kind: 'pause' }`, or `{ kind: 'throw' }` |
|
|
285
|
+
| `error` | present when `outcome` is `'error'` |
|
|
286
|
+
| `interrupt` | `{ interruptId, prompt }`, when a plain step paused on an interrupt |
|
|
287
|
+
| `fork` | on a fork node that ended done or paused: `{ branches: [{ name, status, touchedKeys }] }` |
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
const logger: Observer = {
|
|
291
|
+
onStepSettled(ctx, e) {
|
|
292
|
+
const next = e.next.kind === 'step' ? `→ ${e.next.name}` : e.next.kind
|
|
293
|
+
console.log(`${ctx.stepName}: ${e.outcome} ${next}`, e.signal ? `(signal ${e.signal})` : '')
|
|
294
|
+
},
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const h = createHarness()({ total: field({ default: () => 0 }) }).loop(l =>
|
|
298
|
+
l.start()
|
|
299
|
+
.step('fetch', { run: async () => ({ total: 3 }) })
|
|
300
|
+
.step('decide', { route: s => (s.total > 0 ? 'done' : 'empty') })
|
|
301
|
+
.on('done').end()
|
|
302
|
+
.on('empty').end()
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
await createAgent('demo', h, {}).run({}, { observer: logger })
|
|
306
|
+
// fetch: ok → decide
|
|
307
|
+
// decide: ok end (signal done)
|
|
308
|
+
```
|
|
309
|
+
|
|
159
310
|
## API
|
|
160
311
|
|
|
161
312
|
| Export | Description |
|
|
@@ -165,10 +316,12 @@ const resumed = agent.resume(response, sessionId, interruptId)
|
|
|
165
316
|
| `field<T>(opts)` | Declares a state field with a default and optional reduce function. |
|
|
166
317
|
| `required()` | Marks a provider slot as required at `createAgent()`. |
|
|
167
318
|
| `runtime()` | Marks a provider slot as required at `agent.run()`. |
|
|
168
|
-
| `composeObservers(
|
|
319
|
+
| `composeObservers([a, b], onObserverError?)` | Merges multiple `Observer` instances into one fan-out observer; a throwing observer is isolated and reported to `onObserverError`. |
|
|
169
320
|
| `SessionStore` | Interface for session persistence backends. |
|
|
170
321
|
| `StoredRun` | Type for a persisted run snapshot. Includes `agentId`, `runId`, `sessionId`, `phase`, and state. |
|
|
171
|
-
| `Observer` | Interface for telemetry hooks on run and step lifecycle events. |
|
|
322
|
+
| `Observer` | Interface for telemetry hooks on run and step lifecycle events (see [Observers](#observers)). |
|
|
323
|
+
| `StepSettledEvent` | Payload of `Observer.onStepSettled` — outcome, duration, update, signal, next, error, interrupt, fork. |
|
|
324
|
+
| `StepSettledNext` | Where the run goes after a step settles: `step`, `end`, `pause`, or `throw`. |
|
|
172
325
|
| `ObserverAware` | Interface for provider objects that accept an `Observer` binding via `bindObserver()`. |
|
|
173
326
|
| `RunContext` | Context passed to run-level observer hooks — `agentId`, `sessionId`. |
|
|
174
327
|
| `StepContext` | Context passed to step-level observer hooks — `agentId`, `sessionId`, `stepName`. |
|
package/dist/index.d.ts
CHANGED
|
@@ -145,6 +145,62 @@ interface StepContext {
|
|
|
145
145
|
*/
|
|
146
146
|
readonly branchPath?: readonly string[];
|
|
147
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Where execution goes after a step settles.
|
|
150
|
+
*
|
|
151
|
+
* - `step` — the next step to run (a route target, `.next()`, the implicit next step, the
|
|
152
|
+
* `l.onError()` fallback, or the join after a fork).
|
|
153
|
+
* - `end` — the loop ended via `.end()` (inside a fork branch: the branch ended).
|
|
154
|
+
* - `pause` — the run paused (interrupt, stop, or an unhandled error) and can be continued later.
|
|
155
|
+
* - `throw` — a graph-definition error (`UnknownSignalError`, `NoNextStepError`,
|
|
156
|
+
* `MissingReducerError`) is about to reject the run.
|
|
157
|
+
*/
|
|
158
|
+
type StepSettledNext = {
|
|
159
|
+
readonly kind: 'step';
|
|
160
|
+
readonly name: string;
|
|
161
|
+
} | {
|
|
162
|
+
readonly kind: 'end';
|
|
163
|
+
} | {
|
|
164
|
+
readonly kind: 'pause';
|
|
165
|
+
} | {
|
|
166
|
+
readonly kind: 'throw';
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* Payload of {@link Observer.onStepSettled}: everything known about one step once its
|
|
170
|
+
* outcome and destination are final. Optional keys are omitted, never set to `undefined`.
|
|
171
|
+
*/
|
|
172
|
+
interface StepSettledEvent {
|
|
173
|
+
/** `'stopped'` only occurs on fork nodes whose branches were stopped without an interrupt. */
|
|
174
|
+
readonly outcome: 'ok' | 'error' | 'interrupt' | 'stopped';
|
|
175
|
+
/** From `onStepStart` until the destination is known — includes `route()`. */
|
|
176
|
+
readonly durationMs: number;
|
|
177
|
+
/**
|
|
178
|
+
* The value `run()` returned, present iff it was applied to state. The raw reference,
|
|
179
|
+
* unfiltered — it may be aliased into state, so copy it before the next step if you keep it.
|
|
180
|
+
*/
|
|
181
|
+
readonly update?: Record<string, unknown>;
|
|
182
|
+
/** Present iff `route()` was called and returned a signal. */
|
|
183
|
+
readonly signal?: string;
|
|
184
|
+
readonly next: StepSettledNext;
|
|
185
|
+
/** Present iff `outcome` is `'error'`. */
|
|
186
|
+
readonly error?: Error;
|
|
187
|
+
/** Present iff `outcome` is `'interrupt'` on a plain (non-fork) step. */
|
|
188
|
+
readonly interrupt?: {
|
|
189
|
+
readonly interruptId: string;
|
|
190
|
+
readonly prompt: unknown;
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* Fork nodes only, when every branch ended done or paused (not when the fork failed):
|
|
194
|
+
* each branch in declaration order.
|
|
195
|
+
*/
|
|
196
|
+
readonly fork?: {
|
|
197
|
+
readonly branches: readonly {
|
|
198
|
+
readonly name: string;
|
|
199
|
+
readonly status: 'done' | 'paused';
|
|
200
|
+
readonly touchedKeys: readonly string[];
|
|
201
|
+
}[];
|
|
202
|
+
};
|
|
203
|
+
}
|
|
148
204
|
/**
|
|
149
205
|
* Observability hook interface. All methods are optional — implement only
|
|
150
206
|
* the hooks you need.
|
|
@@ -193,6 +249,13 @@ interface Observer {
|
|
|
193
249
|
* by LLM adapters (e.g. `'llm.response'`).
|
|
194
250
|
*/
|
|
195
251
|
onEvent?: (ctx: StepContext, type: string, payload: unknown) => void;
|
|
252
|
+
/**
|
|
253
|
+
* Called exactly once per step, on every path, once the step's outcome and destination
|
|
254
|
+
* are final: after `onStepEnd` / `onStepError` / `onInterrupt` (when that hook fires on the
|
|
255
|
+
* path) and before `onRunEnd`. Also fires for fork nodes and for steps inside fork branches
|
|
256
|
+
* (with `ctx.branchPath`). Not fired when a run stops before starting a step.
|
|
257
|
+
*/
|
|
258
|
+
onStepSettled?: (ctx: StepContext, event: StepSettledEvent) => void;
|
|
196
259
|
}
|
|
197
260
|
/**
|
|
198
261
|
* The telemetry a resource needs to attribute an `invoke()` call to the correct
|
|
@@ -983,4 +1046,4 @@ declare class LeaseExpiredError extends Error {
|
|
|
983
1046
|
constructor(sessionId: string);
|
|
984
1047
|
}
|
|
985
1048
|
|
|
986
|
-
export { type Agent, type BranchCursor, type BranchDef, type ClaimOptions, type Cursor, type DeepWithMarkers, type FieldDefinition, type ForkCursor, type ForkDef, type FrameworkState, type Harness, type Lease, LeaseExpiredError, type LoopDefinition, type LoopNode, LoopNotDefinedError, NoInterruptError, type Observer, type ObserverAware, type ObserverErrorContext, type ObserverErrorSink, REQUIRED_TAG, RUNTIME_TAG, type RequiredMarker, type RouteFn, type RunContext, type RunFn, type RuntimeMarker, SessionBusyError, SessionInFlightError, SessionPendingInterruptError, type SessionStore, type SignalTransition, type StateFromSchema, type StepContext, type StepDef, type StepState, StoreLoadError, type StoredRun, type StoredRunMetadata, type TelemetryContext, type TransitionTarget, composeObservers, createAgent, createHarness, field, isForkCursor, isForkDef, isRequiredMarker, isRuntimeMarker, required, runtime };
|
|
1049
|
+
export { type Agent, type BranchCursor, type BranchDef, type ClaimOptions, type Cursor, type DeepWithMarkers, type FieldDefinition, type ForkCursor, type ForkDef, type FrameworkState, type Harness, type Lease, LeaseExpiredError, type LoopDefinition, type LoopNode, LoopNotDefinedError, NoInterruptError, type Observer, type ObserverAware, type ObserverErrorContext, type ObserverErrorSink, REQUIRED_TAG, RUNTIME_TAG, type RequiredMarker, type RouteFn, type RunContext, type RunFn, type RuntimeMarker, SessionBusyError, SessionInFlightError, SessionPendingInterruptError, type SessionStore, type SignalTransition, type StateFromSchema, type StepContext, type StepDef, type StepSettledEvent, type StepSettledNext, type StepState, StoreLoadError, type StoredRun, type StoredRunMetadata, type TelemetryContext, type TransitionTarget, composeObservers, createAgent, createHarness, field, isForkCursor, isForkDef, isRequiredMarker, isRuntimeMarker, required, runtime };
|
package/dist/index.js
CHANGED
|
@@ -532,6 +532,9 @@ function composeObservers(observers, onObserverError) {
|
|
|
532
532
|
},
|
|
533
533
|
onEvent: (ctx, type, payload) => {
|
|
534
534
|
for (const o of observers) safeInvoke("onEvent", () => o.onEvent?.(ctx, type, payload), onObserverError);
|
|
535
|
+
},
|
|
536
|
+
onStepSettled: (ctx, event) => {
|
|
537
|
+
for (const o of observers) safeInvoke("onStepSettled", () => o.onStepSettled?.(ctx, event), onObserverError);
|
|
535
538
|
}
|
|
536
539
|
};
|
|
537
540
|
}
|
|
@@ -645,6 +648,15 @@ function applyUpdate(state, update, schema, touchedKeys) {
|
|
|
645
648
|
touchedKeys?.add(key);
|
|
646
649
|
}
|
|
647
650
|
}
|
|
651
|
+
function fireSettled(obs, stepCtx, stepStart, fields, onObserverError) {
|
|
652
|
+
const hook = obs?.onStepSettled;
|
|
653
|
+
if (hook === void 0) return;
|
|
654
|
+
const event = { ...fields, durationMs: Date.now() - stepStart };
|
|
655
|
+
safeInvoke("onStepSettled", () => hook(stepCtx, event), onObserverError);
|
|
656
|
+
}
|
|
657
|
+
function asError(e) {
|
|
658
|
+
return e instanceof Error ? e : new Error(String(e));
|
|
659
|
+
}
|
|
648
660
|
async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callbacks) {
|
|
649
661
|
const { onBeforeStep, onAfterStep, onError, onComplete, onInterrupt } = callbacks ?? {};
|
|
650
662
|
const obs = callbacks?.observer;
|
|
@@ -742,6 +754,8 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
|
|
|
742
754
|
}
|
|
743
755
|
}
|
|
744
756
|
let runSucceeded = false;
|
|
757
|
+
let applied;
|
|
758
|
+
const settle = (fields) => fireSettled(obs, stepCtx, stepStart, { ...fields, ...applied !== void 0 ? { update: applied } : {} }, onObserverError);
|
|
745
759
|
if (step.run !== void 0) {
|
|
746
760
|
const hadResponses = Object.keys(state.$interruptResponses).length > 0;
|
|
747
761
|
try {
|
|
@@ -750,6 +764,7 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
|
|
|
750
764
|
ctx
|
|
751
765
|
);
|
|
752
766
|
applyUpdate(state, update, schema, callbacks?.touchedKeys);
|
|
767
|
+
applied = update;
|
|
753
768
|
if (hadResponses) {
|
|
754
769
|
state.$interrupt = null;
|
|
755
770
|
state.$interruptResponses = {};
|
|
@@ -759,6 +774,7 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
|
|
|
759
774
|
if (isInterruptPause(e)) {
|
|
760
775
|
onInterrupt?.(e.prompt, e.interruptId);
|
|
761
776
|
safeInvoke("onInterrupt", () => obs?.onInterrupt?.(stepCtx, { prompt: e.prompt, interruptId: e.interruptId }), onObserverError);
|
|
777
|
+
settle({ outcome: "interrupt", interrupt: { interruptId: e.interruptId, prompt: e.prompt }, next: { kind: "pause" } });
|
|
762
778
|
safeInvoke("onRunEnd", () => obs?.onRunEnd?.(runCtx, { signal: "$interrupt", durationMs: Date.now() - runStart }), onObserverError);
|
|
763
779
|
return { state, signal: "$interrupt", cursor, paused: true };
|
|
764
780
|
}
|
|
@@ -780,31 +796,42 @@ async function runLoop(graph, state, ctx, schema, shouldStop, startCursor, callb
|
|
|
780
796
|
signal = routeFn(state);
|
|
781
797
|
} catch (e) {
|
|
782
798
|
state.$error = e instanceof Error ? e : new Error(String(e));
|
|
799
|
+
settle({ outcome: "error", error: state.$error, next: { kind: "pause" } });
|
|
783
800
|
safeInvoke("onRunEnd", () => obs?.onRunEnd?.(runCtx, { signal: "$error", durationMs: Date.now() - runStart }), onObserverError);
|
|
784
801
|
return { state, signal: "$error", cursor, paused: true };
|
|
785
802
|
}
|
|
803
|
+
const routed = runSucceeded || step.run === void 0 ? { outcome: "ok", signal } : { outcome: "error", signal, error: state.$error };
|
|
786
804
|
const transition = step.transitions.find((t) => t.signal === signal);
|
|
787
805
|
if (transition === void 0) {
|
|
788
|
-
|
|
806
|
+
const unknownSignal = new UnknownSignalError(cursor, signal);
|
|
807
|
+
settle({ outcome: "error", signal, error: unknownSignal, next: { kind: "throw" } });
|
|
808
|
+
throw unknownSignal;
|
|
789
809
|
}
|
|
790
810
|
if (transition.target.kind === "end") {
|
|
811
|
+
settle({ ...routed, next: { kind: "end" } });
|
|
791
812
|
onComplete?.(state, signal);
|
|
792
813
|
safeInvoke("onRunEnd", () => obs?.onRunEnd?.(runCtx, { signal, durationMs: Date.now() - runStart }), onObserverError);
|
|
793
814
|
return { state, signal, cursor: null, paused: false };
|
|
794
815
|
}
|
|
816
|
+
settle({ ...routed, next: { kind: "step", name: transition.target.name } });
|
|
795
817
|
cursor = transition.target.name;
|
|
796
818
|
} else if (step.run !== void 0 && !runSucceeded) {
|
|
797
819
|
if (graph.onError !== void 0) {
|
|
820
|
+
settle({ outcome: "error", error: state.$error, next: { kind: "step", name: graph.onError } });
|
|
798
821
|
cursor = graph.onError;
|
|
799
822
|
} else {
|
|
823
|
+
settle({ outcome: "error", error: state.$error, next: { kind: "pause" } });
|
|
800
824
|
safeInvoke("onRunEnd", () => obs?.onRunEnd?.(runCtx, { signal: "$error", durationMs: Date.now() - runStart }), onObserverError);
|
|
801
825
|
return { state, signal: "$error", cursor, paused: true };
|
|
802
826
|
}
|
|
803
827
|
} else {
|
|
804
828
|
const next = step.next ?? implicitNextMap.get(cursor) ?? null;
|
|
805
829
|
if (next === null) {
|
|
806
|
-
|
|
830
|
+
const noNext = new NoNextStepError(cursor);
|
|
831
|
+
settle({ outcome: "error", error: noNext, next: { kind: "throw" } });
|
|
832
|
+
throw noNext;
|
|
807
833
|
}
|
|
834
|
+
settle({ outcome: "ok", next: { kind: "step", name: next } });
|
|
808
835
|
cursor = next;
|
|
809
836
|
}
|
|
810
837
|
}
|
|
@@ -842,16 +869,25 @@ async function dispatchFork(forkNode, priorBranches, outerState, outerCtx, schem
|
|
|
842
869
|
);
|
|
843
870
|
const { onBeforeStep: _outerOnBeforeStep, onAfterStep: _outerOnAfterStep, onComplete: _outerOnComplete, ...branchCallbacksRest } = callbacks ?? {};
|
|
844
871
|
const branchCallbacksBase = callbacks !== void 0 ? branchCallbacksRest : void 0;
|
|
845
|
-
const
|
|
872
|
+
const settled = await Promise.allSettled(
|
|
846
873
|
forkNode.branches.map(async (branch, i) => {
|
|
847
874
|
const prior = priorByName.get(branch.name);
|
|
848
|
-
|
|
875
|
+
let outcome;
|
|
876
|
+
try {
|
|
877
|
+
outcome = await runBranch(branch, prior, outerState, outerCtx, schema, shouldStop, branchSignals[i], branchCallbacksBase);
|
|
878
|
+
} catch (e) {
|
|
879
|
+
for (const controller of localAborts) controller.abort();
|
|
880
|
+
throw e;
|
|
881
|
+
}
|
|
849
882
|
if (outcome.result.paused && outcome.result.signal === "$error") {
|
|
850
883
|
for (const controller of localAborts) controller.abort();
|
|
851
884
|
}
|
|
852
885
|
return outcome;
|
|
853
886
|
})
|
|
854
887
|
);
|
|
888
|
+
const rejected = settled.find((r) => r.status === "rejected");
|
|
889
|
+
if (rejected !== void 0) throw rejected.reason;
|
|
890
|
+
const outcomes = settled.map((r) => r.value);
|
|
855
891
|
const failed = outcomes.find((o) => o.result.paused && o.result.signal === "$error");
|
|
856
892
|
if (failed !== void 0) {
|
|
857
893
|
return { kind: "failed", error: failed.result.state.$error };
|
|
@@ -875,7 +911,8 @@ async function dispatchFork(forkNode, priorBranches, outerState, outerCtx, schem
|
|
|
875
911
|
touchedKeys: [...o.touchedKeys]
|
|
876
912
|
};
|
|
877
913
|
});
|
|
878
|
-
|
|
914
|
+
const interrupted = outcomes.some((o) => o.result.paused && o.result.signal === "$interrupt");
|
|
915
|
+
return { kind: "paused", forkCursor: { kind: "fork", fork: forkNode.name, branches }, interrupted };
|
|
879
916
|
}
|
|
880
917
|
const keyOwners = /* @__PURE__ */ new Map();
|
|
881
918
|
for (const outcome of outcomes) {
|
|
@@ -899,7 +936,10 @@ async function dispatchFork(forkNode, priorBranches, outerState, outerCtx, schem
|
|
|
899
936
|
}
|
|
900
937
|
}
|
|
901
938
|
}
|
|
902
|
-
return {
|
|
939
|
+
return {
|
|
940
|
+
kind: "success",
|
|
941
|
+
branches: outcomes.map((o) => ({ name: o.name, status: "done", touchedKeys: [...o.touchedKeys] }))
|
|
942
|
+
};
|
|
903
943
|
}
|
|
904
944
|
async function handleFork(forkNode, priorBranches, state, ctx, schema, shouldStop, callbacks, obs, runCtx, runStart, implicitNextMap, graph) {
|
|
905
945
|
const stepCtx = {
|
|
@@ -911,18 +951,35 @@ async function handleFork(forkNode, priorBranches, state, ctx, schema, shouldSto
|
|
|
911
951
|
const stepStart = Date.now();
|
|
912
952
|
const onObserverError = callbacks?.onObserverError;
|
|
913
953
|
safeInvoke("onStepStart", () => obs?.onStepStart?.(stepCtx), onObserverError);
|
|
914
|
-
|
|
954
|
+
let dispatch;
|
|
955
|
+
try {
|
|
956
|
+
dispatch = await dispatchFork(forkNode, priorBranches, state, ctx, schema, shouldStop, callbacks);
|
|
957
|
+
} catch (e) {
|
|
958
|
+
fireSettled(obs, stepCtx, stepStart, { outcome: "error", error: asError(e), next: { kind: "throw" } }, onObserverError);
|
|
959
|
+
throw e;
|
|
960
|
+
}
|
|
915
961
|
if (dispatch.kind === "paused") {
|
|
916
|
-
|
|
917
|
-
|
|
962
|
+
const fork = {
|
|
963
|
+
branches: dispatch.forkCursor.branches.map((b) => ({ name: b.name, status: b.status, touchedKeys: b.touchedKeys }))
|
|
964
|
+
};
|
|
965
|
+
if (dispatch.interrupted === true) {
|
|
966
|
+
fireSettled(obs, stepCtx, stepStart, { outcome: "interrupt", next: { kind: "pause" }, fork }, onObserverError);
|
|
967
|
+
safeInvoke("onRunEnd", () => obs?.onRunEnd?.(runCtx, { signal: "$interrupt", durationMs: Date.now() - runStart }), onObserverError);
|
|
968
|
+
return { done: true, result: { state, signal: "$interrupt", cursor: dispatch.forkCursor, paused: true } };
|
|
969
|
+
}
|
|
970
|
+
fireSettled(obs, stepCtx, stepStart, { outcome: "stopped", next: { kind: "pause" }, fork }, onObserverError);
|
|
971
|
+
safeInvoke("onRunEnd", () => obs?.onRunEnd?.(runCtx, { signal: "$stopped", durationMs: Date.now() - runStart }), onObserverError);
|
|
972
|
+
return { done: true, result: { state, signal: null, cursor: dispatch.forkCursor, paused: true } };
|
|
918
973
|
}
|
|
919
974
|
if (dispatch.kind === "failed") {
|
|
920
975
|
state.$error = dispatch.error;
|
|
921
976
|
callbacks?.onError?.(state.$error, forkNode.name);
|
|
922
977
|
safeInvoke("onStepError", () => obs?.onStepError?.(stepCtx, { error: state.$error, durationMs: Date.now() - stepStart }), onObserverError);
|
|
923
978
|
if (graph.onError !== void 0) {
|
|
979
|
+
fireSettled(obs, stepCtx, stepStart, { outcome: "error", error: state.$error, next: { kind: "step", name: graph.onError } }, onObserverError);
|
|
924
980
|
return { done: false, nextCursor: graph.onError };
|
|
925
981
|
}
|
|
982
|
+
fireSettled(obs, stepCtx, stepStart, { outcome: "error", error: state.$error, next: { kind: "pause" } }, onObserverError);
|
|
926
983
|
safeInvoke("onRunEnd", () => obs?.onRunEnd?.(runCtx, { signal: "$error", durationMs: Date.now() - runStart }), onObserverError);
|
|
927
984
|
return { done: true, result: { state, signal: "$error", cursor: forkNode.name, paused: true } };
|
|
928
985
|
}
|
|
@@ -931,8 +988,11 @@ async function handleFork(forkNode, priorBranches, state, ctx, schema, shouldSto
|
|
|
931
988
|
callbacks?.onAfterStep?.(forkNode.name, state);
|
|
932
989
|
const next = forkNode.next ?? implicitNextMap.get(forkNode.name) ?? null;
|
|
933
990
|
if (next === null) {
|
|
934
|
-
|
|
991
|
+
const noNext = new NoNextStepError(forkNode.name);
|
|
992
|
+
fireSettled(obs, stepCtx, stepStart, { outcome: "error", error: noNext, next: { kind: "throw" } }, onObserverError);
|
|
993
|
+
throw noNext;
|
|
935
994
|
}
|
|
995
|
+
fireSettled(obs, stepCtx, stepStart, { outcome: "ok", next: { kind: "step", name: next }, fork: { branches: dispatch.branches } }, onObserverError);
|
|
936
996
|
return { done: false, nextCursor: next };
|
|
937
997
|
}
|
|
938
998
|
|