@agent-compose/sdk 0.5.1 → 0.5.5
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/active-step.d.ts +60 -0
- package/dist/agent/agent-loop-steer.test.d.ts +1 -0
- package/dist/agent/agent-loop.d.ts +46 -0
- package/dist/agent/async-queue.d.ts +29 -0
- package/dist/agent/protocol.d.ts +9 -1
- package/dist/agent/resolve-agent-id.test.d.ts +1 -0
- package/dist/agent/run-agent.d.ts +16 -3
- package/dist/agent/steer-control.d.ts +57 -0
- package/dist/agent/steer-control.test.d.ts +1 -0
- package/dist/client.d.ts +161 -0
- package/dist/index.d.ts +16 -6
- package/dist/index.js +1586 -158
- package/dist/pause/__tests__/agent-loop-checkpoint.test.d.ts +1 -0
- package/dist/pause/__tests__/checkpoint.test.d.ts +1 -0
- package/dist/pause/__tests__/errors.test.d.ts +1 -0
- package/dist/pause/__tests__/manager.test.d.ts +1 -0
- package/dist/pause/__tests__/pause-core.test.d.ts +1 -0
- package/dist/pause/__tests__/state-dir.test.d.ts +1 -0
- package/dist/pause/__tests__/wrappers.test.d.ts +1 -0
- package/dist/pause/checkpoint.d.ts +28 -0
- package/dist/pause/errors.d.ts +52 -0
- package/dist/pause/manager.d.ts +63 -0
- package/dist/pause/pause-core.d.ts +101 -0
- package/dist/pause/state-dir.d.ts +80 -0
- package/dist/pause/wrappers.d.ts +41 -0
- package/dist/request-context/request-context.d.ts +12 -0
- package/dist/runtimes/_cli-agent.d.ts +72 -0
- package/dist/runtimes/amp.d.ts +22 -0
- package/dist/runtimes/claude.d.ts +6 -0
- package/dist/runtimes/codex.d.ts +20 -0
- package/dist/runtimes/openai-desktop.d.ts +2 -0
- package/dist/runtimes/openai-desktop.js +1578 -157
- package/dist/runtimes/vercel.d.ts +53 -1
- package/dist/runtimes/vercel.js +60 -8
- package/dist/runtimes/vercel.test.d.ts +1 -0
- package/dist/sse.d.ts +2 -3
- package/dist/step-invocation/index.d.ts +2 -2
- package/dist/step-invocation/invoker.d.ts +3 -0
- package/dist/step-invocation/protocol.d.ts +12 -0
- package/dist/step-invocation/server.d.ts +1 -0
- package/dist/step-invocation/types.d.ts +40 -5
- package/dist/types/events.d.ts +9 -0
- package/dist/types/execution-context.d.ts +25 -0
- package/dist/types/protocol.d.ts +8 -0
- package/dist/types/runtime.d.ts +55 -0
- package/dist/types/sandbox.d.ts +4 -4
- package/dist/utils/schemas.d.ts +2 -0
- package/dist/workflow-steps/__tests__/pause-wiring.test.d.ts +1 -0
- package/dist/workflow-steps/index.d.ts +2 -0
- package/dist/workflow-steps/observability.d.ts +43 -11
- package/dist/workflow-steps/run-callback.d.ts +39 -0
- package/dist/workflow-steps/runner.d.ts +8 -0
- package/package.json +1 -1
- package/src/active-step.ts +124 -0
- package/src/agent/agent-loop.ts +253 -19
- package/src/agent/async-queue.ts +61 -0
- package/src/agent/protocol.ts +12 -2
- package/src/agent/run-agent.ts +184 -8
- package/src/agent/steer-control.ts +125 -0
- package/src/client.ts +277 -0
- package/src/index.ts +38 -4
- package/src/pause/checkpoint.ts +44 -0
- package/src/pause/errors.ts +70 -0
- package/src/pause/manager.ts +177 -0
- package/src/pause/pause-core.ts +267 -0
- package/src/pause/state-dir.ts +262 -0
- package/src/pause/wrappers.ts +79 -0
- package/src/request-context/request-context.ts +17 -2
- package/src/runtimes/_cli-agent.ts +161 -0
- package/src/runtimes/amp.ts +94 -0
- package/src/runtimes/claude.ts +101 -6
- package/src/runtimes/codex.ts +109 -0
- package/src/runtimes/openai-desktop.ts +11 -0
- package/src/runtimes/vercel.ts +78 -2
- package/src/sandbox.ts +39 -20
- package/src/sse.ts +8 -6
- package/src/step-invocation/index.ts +2 -1
- package/src/step-invocation/invoker.ts +107 -29
- package/src/step-invocation/protocol.ts +16 -0
- package/src/step-invocation/server.ts +45 -12
- package/src/step-invocation/types.ts +43 -7
- package/src/tools/coding.ts +16 -5
- package/src/types/events.ts +9 -0
- package/src/types/execution-context.ts +25 -0
- package/src/types/protocol.ts +8 -0
- package/src/types/runtime.ts +52 -0
- package/src/types/sandbox.ts +8 -4
- package/src/types/workflow.ts +6 -1
- package/src/utils/bundler.ts +8 -3
- package/src/utils/schemas.ts +2 -0
- package/src/workflow-steps/index.ts +3 -0
- package/src/workflow-steps/observability.ts +84 -13
- package/src/workflow-steps/run-callback.ts +72 -0
- package/src/workflow-steps/runner.ts +70 -8
- package/dist/utils/discovery.d.ts +0 -2
- package/src/utils/discovery.ts +0 -4
package/src/types/workflow.ts
CHANGED
|
@@ -210,7 +210,12 @@ function compileRunForm<TOutput, TInput extends Record<string, unknown>>(
|
|
|
210
210
|
setMetadata: stepCtx.setMetadata,
|
|
211
211
|
step: stepCtx.step,
|
|
212
212
|
agentEvents: stepCtx.agentEvents,
|
|
213
|
-
|
|
213
|
+
checkpoint: stepCtx.checkpoint,
|
|
214
|
+
pause: stepCtx.pause,
|
|
215
|
+
requestDecision: stepCtx.requestDecision,
|
|
216
|
+
sleep: stepCtx.sleep,
|
|
217
|
+
waitForEvent: stepCtx.waitForEvent,
|
|
218
|
+
processors: metadata.processors ?? [],
|
|
214
219
|
};
|
|
215
220
|
const sandbox = stepCtx.sandbox;
|
|
216
221
|
if (!sandbox) throw new Error("legacy run-form workflow requires a sandbox in StepContext");
|
package/src/utils/bundler.ts
CHANGED
|
@@ -111,8 +111,9 @@ async function bundle(path: string, label: string): Promise<string> {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
/** Dynamically import a bundled source in-process and extract fields from its
|
|
114
|
-
* default export. Safe for CLI/test contexts.
|
|
115
|
-
*
|
|
114
|
+
* default export. Safe for CLI/test contexts. Evaluation failures are
|
|
115
|
+
* surfaced with their original cause so users fix the real import/runtime
|
|
116
|
+
* problem instead of being told the default export shape is wrong. */
|
|
116
117
|
async function extractFromBundle<T>(
|
|
117
118
|
source: string,
|
|
118
119
|
label: string,
|
|
@@ -123,7 +124,11 @@ async function extractFromBundle<T>(
|
|
|
123
124
|
const loaded = await importSourceModule<Record<string, unknown>>(source, tmpPath);
|
|
124
125
|
try { return pick(loaded.mod.default); }
|
|
125
126
|
finally { await loaded.cleanup(); }
|
|
126
|
-
} catch {
|
|
127
|
+
} catch (err) {
|
|
128
|
+
throw new WorkflowSourceValidationError(
|
|
129
|
+
`Bundled ${label} could not be evaluated: ${err instanceof Error ? err.message : String(err)}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
127
132
|
}
|
|
128
133
|
|
|
129
134
|
/**
|
package/src/utils/schemas.ts
CHANGED
|
@@ -28,3 +28,6 @@ export { WORKFLOW_BRAND } from "./types.js";
|
|
|
28
28
|
|
|
29
29
|
export { StepObservabilityCollector } from "./observability.js";
|
|
30
30
|
export type { StepObservability, SubStepEvent } from "./observability.js";
|
|
31
|
+
|
|
32
|
+
export { makeRunCallbackEmitterFromEnv } from "./run-callback.js";
|
|
33
|
+
export type { LiveAgentEventEmitter } from "./run-callback.js";
|
|
@@ -5,19 +5,34 @@
|
|
|
5
5
|
* tokenised stdout sentinel; the activity persists the snapshot to the
|
|
6
6
|
* run's metadata + lifecycle event tables.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
8
|
+
* Live streaming + batch backstop — the no-double-write design:
|
|
9
|
+
*
|
|
10
|
+
* 1. `agentEvents.emit` immediately fires the optional `liveEmitter`
|
|
11
|
+
* (the runner's POST to `/internal/runs/.../events`).
|
|
12
|
+
* 2. The emitter returns `Promise<boolean>` — true means the server
|
|
13
|
+
* accepted and persisted this event, false means it failed (POST
|
|
14
|
+
* error, server 5xx, network blip).
|
|
15
|
+
* 3. The collector tracks which seqs were successfully ack'd.
|
|
16
|
+
* 4. At `snapshot()` time we await any in-flight emits (with a small
|
|
17
|
+
* grace window so the agent loop's final-burst posts can finish),
|
|
18
|
+
* then STRIP ack'd events from the returned `events` array.
|
|
19
|
+
*
|
|
20
|
+
* The result: the snapshot's `events` array only contains events
|
|
21
|
+
* that the live path didn't successfully deliver. The server's
|
|
22
|
+
* batch-flush in `persistStepObservability` becomes a true backstop
|
|
23
|
+
* for the FAILURE path — it never re-writes (and never re-notifies)
|
|
24
|
+
* the events the live route already handled. No double pg_notify,
|
|
25
|
+
* no dashboard duplicates.
|
|
26
|
+
*
|
|
27
|
+
* `metadata` and `subSteps` remain batch-only because they're
|
|
28
|
+
* naturally boundary events (no streaming benefit) and aren't
|
|
29
|
+
* written by the live route at all.
|
|
17
30
|
*/
|
|
18
31
|
|
|
19
32
|
import type { AgentLifecycleEvent } from "../agent/agent-loop.js";
|
|
20
33
|
import type { AgentEventSink } from "../types/workflow.js";
|
|
34
|
+
import type { LiveAgentEventEmitter } from "./run-callback.js";
|
|
35
|
+
import { PauseSignal, isPauseSignal } from "../pause/pause-core.js";
|
|
21
36
|
|
|
22
37
|
/** One named sub-step (from `ctx.step("name", async () => ...)`).
|
|
23
38
|
* Becomes a `workflow_substep_*` lifecycle event on the run timeline. */
|
|
@@ -39,6 +54,13 @@ export interface StepObservability {
|
|
|
39
54
|
subSteps?: SubStepEvent[];
|
|
40
55
|
}
|
|
41
56
|
|
|
57
|
+
/** Maximum time we'll wait for in-flight live-emit POSTs to settle
|
|
58
|
+
* before taking the snapshot. Tuned to be longer than a healthy POST
|
|
59
|
+
* (~50ms) but short enough that a totally-broken live path doesn't
|
|
60
|
+
* block step return — the batch backstop will handle whatever doesn't
|
|
61
|
+
* resolve in time. */
|
|
62
|
+
const LIVE_EMIT_DRAIN_TIMEOUT_MS = 1_500;
|
|
63
|
+
|
|
42
64
|
/**
|
|
43
65
|
* Append-only collector bound to a single step's `StepContext`. The
|
|
44
66
|
* step's `setMetadata` / `step` / `agentEvents` properties all point at
|
|
@@ -53,6 +75,15 @@ export class StepObservabilityCollector {
|
|
|
53
75
|
private metadata: Record<string, unknown> = {};
|
|
54
76
|
private events: AgentLifecycleEvent[] = [];
|
|
55
77
|
private subSteps: SubStepEvent[] = [];
|
|
78
|
+
private readonly liveEmitter?: LiveAgentEventEmitter;
|
|
79
|
+
/** Seqs of events the server confirmed via the live route. */
|
|
80
|
+
private readonly ackedSeqs: Set<number> = new Set();
|
|
81
|
+
/** Promises for in-flight live emits — awaited at snapshot time. */
|
|
82
|
+
private readonly inFlight: Set<Promise<void>> = new Set();
|
|
83
|
+
|
|
84
|
+
constructor(opts: { liveEmitter?: LiveAgentEventEmitter } = {}) {
|
|
85
|
+
this.liveEmitter = opts.liveEmitter;
|
|
86
|
+
}
|
|
56
87
|
|
|
57
88
|
readonly setMetadata = async (data: Record<string, unknown>): Promise<void> => {
|
|
58
89
|
Object.assign(this.metadata, data);
|
|
@@ -70,6 +101,9 @@ export class StepObservabilityCollector {
|
|
|
70
101
|
});
|
|
71
102
|
return result;
|
|
72
103
|
} catch (err) {
|
|
104
|
+
// A pause inside ctx.step is control flow, not a failed sub-step — let
|
|
105
|
+
// it propagate untouched so serveStep emits the pause sentinel.
|
|
106
|
+
if (isPauseSignal(err)) throw err;
|
|
73
107
|
this.subSteps.push({
|
|
74
108
|
name,
|
|
75
109
|
startedAt,
|
|
@@ -83,20 +117,57 @@ export class StepObservabilityCollector {
|
|
|
83
117
|
|
|
84
118
|
readonly agentEvents: AgentEventSink = {
|
|
85
119
|
emit: (event: AgentLifecycleEvent) => {
|
|
120
|
+
// `seq` is the event's index in `events`, captured BEFORE push so
|
|
121
|
+
// it matches the index the server-side batch flush uses (its
|
|
122
|
+
// `.entries()` loop). Same index → same idempotency key on the
|
|
123
|
+
// server. The live route's `acceptedSeqs` response uses this seq;
|
|
124
|
+
// we strip those from the snapshot in `snapshot()`.
|
|
125
|
+
const seq = this.events.length;
|
|
86
126
|
this.events.push(event);
|
|
127
|
+
if (this.liveEmitter) {
|
|
128
|
+
const tracked = this.liveEmitter(event, seq)
|
|
129
|
+
.then((ok) => { if (ok) this.ackedSeqs.add(seq); })
|
|
130
|
+
.catch(() => { /* leave unacked → batch backstop delivers */ });
|
|
131
|
+
this.inFlight.add(tracked);
|
|
132
|
+
// Self-clean so completed promises don't leak across long-
|
|
133
|
+
// running steps.
|
|
134
|
+
void tracked.finally(() => this.inFlight.delete(tracked));
|
|
135
|
+
}
|
|
87
136
|
},
|
|
88
137
|
};
|
|
89
138
|
|
|
139
|
+
/** Wait for in-flight live emits to settle (or timeout) so the
|
|
140
|
+
* ackedSeqs set is maximally up-to-date before we filter. Used by
|
|
141
|
+
* `snapshot()` — exposed separately for tests. */
|
|
142
|
+
private async drainInFlight(timeoutMs = LIVE_EMIT_DRAIN_TIMEOUT_MS): Promise<void> {
|
|
143
|
+
if (this.inFlight.size === 0) return;
|
|
144
|
+
await Promise.race([
|
|
145
|
+
Promise.allSettled([...this.inFlight]),
|
|
146
|
+
new Promise<void>((resolve) => setTimeout(resolve, timeoutMs)),
|
|
147
|
+
]);
|
|
148
|
+
}
|
|
149
|
+
|
|
90
150
|
/** Snapshot the accumulated state. Returns `undefined` when nothing
|
|
91
|
-
* was recorded so the wire payload can drop the field entirely.
|
|
92
|
-
|
|
151
|
+
* was recorded so the wire payload can drop the field entirely.
|
|
152
|
+
*
|
|
153
|
+
* Async because we drain in-flight live emits first. Any event the
|
|
154
|
+
* server acknowledged is REMOVED from the returned `events` array
|
|
155
|
+
* so the server-side batch flush doesn't re-write/re-notify it.
|
|
156
|
+
* Events that failed live delivery (POST error, timeout) stay in
|
|
157
|
+
* the array as the durable backstop. */
|
|
158
|
+
async snapshot(): Promise<StepObservability | undefined> {
|
|
159
|
+
await this.drainInFlight();
|
|
160
|
+
|
|
93
161
|
const hasMetadata = Object.keys(this.metadata).length > 0;
|
|
94
|
-
const hasEvents = this.events.length > 0;
|
|
95
162
|
const hasSubSteps = this.subSteps.length > 0;
|
|
163
|
+
// Filter out ack'd events — the live route already wrote them.
|
|
164
|
+
const remainingEvents = this.events.filter((_, idx) => !this.ackedSeqs.has(idx));
|
|
165
|
+
const hasEvents = remainingEvents.length > 0;
|
|
166
|
+
|
|
96
167
|
if (!hasMetadata && !hasEvents && !hasSubSteps) return undefined;
|
|
97
168
|
const result: StepObservability = {};
|
|
98
169
|
if (hasMetadata) result.metadata = { ...this.metadata };
|
|
99
|
-
if (hasEvents) result.events =
|
|
170
|
+
if (hasEvents) result.events = remainingEvents;
|
|
100
171
|
if (hasSubSteps) result.subSteps = [...this.subSteps];
|
|
101
172
|
return result;
|
|
102
173
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runner → server live event emitter.
|
|
3
|
+
*
|
|
4
|
+
* Pairs with the server-side `/api/v1/internal/runs/:runId/steps/:stepIndex/events`
|
|
5
|
+
* route. The runner reads `AGENT_COMPOSE_URL`, `AGENT_COMPOSE_RUN_TOKEN`,
|
|
6
|
+
* and `RUN_ID` from env; if all three are present it builds a POST
|
|
7
|
+
* function that sends each agent lifecycle event to the server as
|
|
8
|
+
* soon as the agent loop emits it.
|
|
9
|
+
*
|
|
10
|
+
* Non-blocking: the agent loop doesn't await the emitter — the runtime
|
|
11
|
+
* keeps emitting events at full speed. But the emitter still returns a
|
|
12
|
+
* `Promise<boolean>` so the collector can later decide whether to
|
|
13
|
+
* include each event in the batch-end durable backstop. `true` = server
|
|
14
|
+
* accepted the event (don't re-deliver in batch); `false` = POST
|
|
15
|
+
* failed (DO re-deliver). The collector awaits these promises with a
|
|
16
|
+
* short grace window at snapshot time.
|
|
17
|
+
*
|
|
18
|
+
* This is the seam that lets us avoid the double-write problem: every
|
|
19
|
+
* successfully-ack'd live event is stripped from `observability.events`
|
|
20
|
+
* before the batch flush runs, so the durable backstop only carries
|
|
21
|
+
* events that genuinely failed live delivery. No more two paths writing
|
|
22
|
+
* the same row + double pg_notify.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { AgentLifecycleEvent } from "../agent/agent-loop.js";
|
|
26
|
+
|
|
27
|
+
export type LiveAgentEventEmitter =
|
|
28
|
+
(event: AgentLifecycleEvent, seq: number) => Promise<boolean>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build a live emitter from process env + the caller-supplied stepIndex.
|
|
32
|
+
* Returns `undefined` when any required env var is missing (local tests,
|
|
33
|
+
* non-sandbox callers) so the caller can wire `undefined` straight
|
|
34
|
+
* through to the collector and get batch-only delivery without
|
|
35
|
+
* conditional plumbing.
|
|
36
|
+
*
|
|
37
|
+
* `stepIndex` is a parameter rather than an env read because
|
|
38
|
+
* `serveStep` scrubs `AC_STEP_INDEX` from `process.env` before invoking
|
|
39
|
+
* the workflow handler (to keep the transport envelope unreachable
|
|
40
|
+
* from user code); the handler still holds the parsed integer and
|
|
41
|
+
* passes it here.
|
|
42
|
+
*/
|
|
43
|
+
export function makeRunCallbackEmitterFromEnv(stepIndex: number): LiveAgentEventEmitter | undefined {
|
|
44
|
+
const baseUrl = process.env.AGENT_COMPOSE_URL;
|
|
45
|
+
const token = process.env.AGENT_COMPOSE_RUN_TOKEN;
|
|
46
|
+
const runId = process.env.RUN_ID;
|
|
47
|
+
if (!baseUrl || !token || !runId) return undefined;
|
|
48
|
+
if (!Number.isFinite(stepIndex) || stepIndex < 0) return undefined;
|
|
49
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/api/v1/internal/runs/${runId}/steps/${stepIndex}/events`;
|
|
50
|
+
const headers = {
|
|
51
|
+
"content-type": "application/json",
|
|
52
|
+
"authorization": `Bearer ${token}`,
|
|
53
|
+
} as const;
|
|
54
|
+
return async (event, seq) => {
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetch(url, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers,
|
|
59
|
+
body: JSON.stringify({ events: [{ ...event, seq }] }),
|
|
60
|
+
});
|
|
61
|
+
if (!res.ok) return false;
|
|
62
|
+
// Parse the route's response to confirm THIS seq was accepted.
|
|
63
|
+
// The route returns `{ acceptedSeqs: number[] }`; absence means
|
|
64
|
+
// the server logged but didn't persist (rare — DB hiccup), and
|
|
65
|
+
// we should leave the event in the batch backstop.
|
|
66
|
+
const body = await res.json().catch(() => null) as { acceptedSeqs?: number[] } | null;
|
|
67
|
+
return Array.isArray(body?.acceptedSeqs) && body.acceptedSeqs.includes(seq);
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -28,6 +28,12 @@ import type { RequestContext } from "../request-context/request-context.js";
|
|
|
28
28
|
import type { SandboxProvider } from "../types/sandbox.js";
|
|
29
29
|
import type { WorkflowRun, WorkflowCtx } from "../types/workflow.js";
|
|
30
30
|
import { StepObservabilityCollector, type StepObservability } from "./observability.js";
|
|
31
|
+
import type { LiveAgentEventEmitter } from "./run-callback.js";
|
|
32
|
+
import { scopedCheckpoint } from "../pause/checkpoint.js";
|
|
33
|
+
import { corePause, PauseSignal, isPauseSignal, type PauseRequest } from "../pause/pause-core.js";
|
|
34
|
+
import type { StepPauseRequest } from "../step-invocation/types.js";
|
|
35
|
+
import { buildPauseWrappers, type PauseFn, type KindedPauseFn } from "../pause/wrappers.js";
|
|
36
|
+
import { runWithActiveStep, setActiveStepBridge, restoreActiveStepBridge } from "../active-step.js";
|
|
31
37
|
|
|
32
38
|
export class StepValidationError extends Error {
|
|
33
39
|
readonly kind = "step-validation" as const;
|
|
@@ -84,6 +90,11 @@ export interface RunWorkflowStepsOpts<TInput, TOutput> {
|
|
|
84
90
|
onStepStarted?(stepIndex: number, stepName: string): void | Promise<void>;
|
|
85
91
|
/** Child workflow invocation implementation. Defaults to a clear unsupported error. */
|
|
86
92
|
invokeChild?: WorkflowCtx["invokeChild"];
|
|
93
|
+
/** Optional live-stream emitter for agent lifecycle events. The runner
|
|
94
|
+
* passes a fetch-based emitter wired to the per-run callback token so
|
|
95
|
+
* the dashboard sees events as the agent loop produces them; tests
|
|
96
|
+
* leave it undefined and get batch-only delivery. */
|
|
97
|
+
liveAgentEventEmitter?: LiveAgentEventEmitter;
|
|
87
98
|
}
|
|
88
99
|
|
|
89
100
|
export interface RunWorkflowStepsResult<TOutput> {
|
|
@@ -101,6 +112,8 @@ export interface RunWorkflowSingleStepOpts {
|
|
|
101
112
|
sandbox?: SandboxProvider;
|
|
102
113
|
abortSignal?: AbortSignal;
|
|
103
114
|
invokeChild?: WorkflowCtx["invokeChild"];
|
|
115
|
+
/** Optional live-stream emitter — see `RunWorkflowStepsOpts.liveAgentEventEmitter`. */
|
|
116
|
+
liveAgentEventEmitter?: LiveAgentEventEmitter;
|
|
104
117
|
}
|
|
105
118
|
|
|
106
119
|
/** Result of one step run — output plus whatever the step's observability
|
|
@@ -116,7 +129,14 @@ export async function runWorkflowSingleStep(opts: RunWorkflowSingleStepOpts): Pr
|
|
|
116
129
|
if (!step) throw new Error(`Step index ${opts.stepIndex} not found in workflow "${opts.workflow.id}"`);
|
|
117
130
|
const parsedInput = step.input.safeParse(opts.input);
|
|
118
131
|
if (!parsedInput.success) throw new StepValidationError(step.name, "input", parsedInput.error);
|
|
119
|
-
const collector = new StepObservabilityCollector();
|
|
132
|
+
const collector = new StepObservabilityCollector({ liveEmitter: opts.liveAgentEventEmitter });
|
|
133
|
+
const coord = { runId: opts.run.id, stepIndex: opts.stepIndex };
|
|
134
|
+
const pauseWithKind: KindedPauseFn = function <T>(req: PauseRequest<T>, kind: StepPauseRequest["kind"]): Promise<T> {
|
|
135
|
+
return corePause(req, coord, kind);
|
|
136
|
+
};
|
|
137
|
+
const pause: PauseFn = function <T>(req: PauseRequest<T>): Promise<T> {
|
|
138
|
+
return corePause(req, coord);
|
|
139
|
+
};
|
|
120
140
|
const stepCtx: StepContext<unknown> = {
|
|
121
141
|
input: parsedInput.data,
|
|
122
142
|
requestContext: opts.requestContext,
|
|
@@ -128,11 +148,35 @@ export async function runWorkflowSingleStep(opts: RunWorkflowSingleStepOpts): Pr
|
|
|
128
148
|
setMetadata: collector.setMetadata,
|
|
129
149
|
step: collector.step,
|
|
130
150
|
agentEvents: collector.agentEvents,
|
|
151
|
+
checkpoint: scopedCheckpoint(`step${opts.stepIndex}`),
|
|
152
|
+
pause,
|
|
153
|
+
...buildPauseWrappers(pauseWithKind),
|
|
131
154
|
};
|
|
132
|
-
|
|
155
|
+
let output: unknown;
|
|
156
|
+
try {
|
|
157
|
+
// Set the active step so `agent()` / `ctx.pause` derive deterministic
|
|
158
|
+
// ids from the un-scrubbed index (see active-step.ts). Async-local scope
|
|
159
|
+
// keeps concurrent in-process runs from clobbering each other. ALSO publish
|
|
160
|
+
// on the cross-instance bridge: a bundled workflow inlines its own SDK copy
|
|
161
|
+
// (a separate AsyncLocalStorage), so its `agent()` can't see the ALS we set
|
|
162
|
+
// here — the bridge carries the step across that boundary. One step per
|
|
163
|
+
// subprocess in the sandbox ⇒ no concurrency on the global slot.
|
|
164
|
+
const bridged = setActiveStepBridge({ stepIndex: opts.stepIndex });
|
|
165
|
+
try {
|
|
166
|
+
output = await runWithActiveStep({ stepIndex: opts.stepIndex }, () => Promise.resolve(step.run(stepCtx)));
|
|
167
|
+
} finally {
|
|
168
|
+
restoreActiveStepBridge(bridged);
|
|
169
|
+
}
|
|
170
|
+
} catch (err) {
|
|
171
|
+
if (isPauseSignal(err)) {
|
|
172
|
+
const observability = await collector.snapshot();
|
|
173
|
+
throw new PauseSignal(err.pauseId, err.pauseRequest, observability);
|
|
174
|
+
}
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
133
177
|
const parsedOutput = step.output.safeParse(output);
|
|
134
178
|
if (!parsedOutput.success) throw new StepValidationError(step.name, "output", parsedOutput.error);
|
|
135
|
-
const observability = collector.snapshot();
|
|
179
|
+
const observability = await collector.snapshot();
|
|
136
180
|
return observability === undefined
|
|
137
181
|
? { output: parsedOutput.data }
|
|
138
182
|
: { output: parsedOutput.data, observability };
|
|
@@ -179,7 +223,14 @@ export async function runWorkflowSteps<TInput, TOutput>(
|
|
|
179
223
|
throw err;
|
|
180
224
|
}
|
|
181
225
|
|
|
182
|
-
const collector = new StepObservabilityCollector();
|
|
226
|
+
const collector = new StepObservabilityCollector({ liveEmitter: opts.liveAgentEventEmitter });
|
|
227
|
+
const coord = { runId: run.id, stepIndex: i };
|
|
228
|
+
const pauseWithKind: KindedPauseFn = function <T>(req: PauseRequest<T>, kind: StepPauseRequest["kind"]): Promise<T> {
|
|
229
|
+
return corePause(req, coord, kind);
|
|
230
|
+
};
|
|
231
|
+
const pause: PauseFn = function <T>(req: PauseRequest<T>): Promise<T> {
|
|
232
|
+
return corePause(req, coord);
|
|
233
|
+
};
|
|
183
234
|
const stepCtx: StepContext<unknown> = {
|
|
184
235
|
input: parsedStepInput.data,
|
|
185
236
|
requestContext,
|
|
@@ -191,16 +242,27 @@ export async function runWorkflowSteps<TInput, TOutput>(
|
|
|
191
242
|
setMetadata: collector.setMetadata,
|
|
192
243
|
step: collector.step,
|
|
193
244
|
agentEvents: collector.agentEvents,
|
|
245
|
+
checkpoint: scopedCheckpoint(`step${i}`),
|
|
246
|
+
pause,
|
|
247
|
+
...buildPauseWrappers(pauseWithKind),
|
|
194
248
|
};
|
|
195
249
|
|
|
196
250
|
const startedAt = Date.now();
|
|
197
251
|
let output: unknown;
|
|
198
252
|
try {
|
|
199
|
-
|
|
253
|
+
// Active step for deterministic agent/pause id derivation (active-step.ts).
|
|
254
|
+
// Async-local scope keeps concurrent in-process runs isolated.
|
|
255
|
+
output = await runWithActiveStep({ stepIndex: i }, () => Promise.resolve(step.run(stepCtx)));
|
|
200
256
|
} catch (err) {
|
|
257
|
+
// A pause is control flow, not a failure — let it propagate so serveStep
|
|
258
|
+
// emits the pause sentinel instead of recording a failed step.
|
|
259
|
+
if (isPauseSignal(err)) {
|
|
260
|
+
const observability = await collector.snapshot();
|
|
261
|
+
throw new PauseSignal(err.pauseId, err.pauseRequest, observability);
|
|
262
|
+
}
|
|
201
263
|
const wrapped = err instanceof Error ? err : new Error(String(err));
|
|
202
264
|
const durationMs = Date.now() - startedAt;
|
|
203
|
-
const observability = collector.snapshot();
|
|
265
|
+
const observability = await collector.snapshot();
|
|
204
266
|
stepResults.push({
|
|
205
267
|
name: step.name, status: "failed", error: wrapped.message, durationMs,
|
|
206
268
|
...(observability ? { observability } : {}),
|
|
@@ -213,7 +275,7 @@ export async function runWorkflowSteps<TInput, TOutput>(
|
|
|
213
275
|
if (!parsedOutput.success) {
|
|
214
276
|
const err = new StepValidationError(step.name, "output", parsedOutput.error);
|
|
215
277
|
const durationMs = Date.now() - startedAt;
|
|
216
|
-
const observability = collector.snapshot();
|
|
278
|
+
const observability = await collector.snapshot();
|
|
217
279
|
stepResults.push({
|
|
218
280
|
name: step.name, status: "failed", error: err.message, durationMs,
|
|
219
281
|
...(observability ? { observability } : {}),
|
|
@@ -224,7 +286,7 @@ export async function runWorkflowSteps<TInput, TOutput>(
|
|
|
224
286
|
|
|
225
287
|
const durationMs = Date.now() - startedAt;
|
|
226
288
|
current = parsedOutput.data;
|
|
227
|
-
const observability = collector.snapshot();
|
|
289
|
+
const observability = await collector.snapshot();
|
|
228
290
|
stepResults.push({
|
|
229
291
|
name: step.name, status: "completed", output: parsedOutput.data, durationMs,
|
|
230
292
|
...(observability ? { observability } : {}),
|
package/src/utils/discovery.ts
DELETED