@agent-compose/sdk 0.8.0 → 0.8.1

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.
@@ -77,6 +77,13 @@ export interface StepWorkflowDefinition<TInput, TOutput> {
77
77
  * server skips mounting the shared factory drive for its runs (#13). See
78
78
  * `WorkflowMetadata.environmentBuild`. */
79
79
  environmentBuild?: boolean;
80
+ /** Whether this workflow's runs need the factory drive. Omit (⇒
81
+ * `"required"`) for any workflow that reads or writes /factory: a failed
82
+ * mount then FAILS the run instead of silently proceeding drive-less.
83
+ * Declare `"none"` for a drive-agnostic workflow — the server skips the
84
+ * /factory mount entirely for its runs. See
85
+ * `WorkflowMetadata.factoryDrive`. */
86
+ factoryDrive?: "required" | "none";
80
87
  }
81
88
 
82
89
  export function createStepWorkflow<TInput, TOutput>(
@@ -1,6 +1,32 @@
1
1
  import { AgentComposeClient } from "../client.js";
2
+ import { getActiveStep, nextPauseOrdinalInActiveStep } from "../active-step.js";
2
3
  import type { InvokeChild } from "../types/execution-context.js";
3
4
 
5
+ /**
6
+ * Derive the deterministic per-call `Idempotency-Key` for a `ctx.invokeChild`
7
+ * dispatch: `invoke-child:<parentRunId>:step<stepIndex>:<childName>:<ordinal>`.
8
+ *
9
+ * Replay safety: a replayed step re-runs its body from the top, so the k-th
10
+ * `invokeChild(name)` call in a step re-derives the SAME key (the per-scope
11
+ * ordinal counter lives on the active-step state, which resets identically on
12
+ * every (re-)entry — the `nextPauseOrdinalInActiveStep` pattern). The server's
13
+ * idempotency window then returns the original child run instead of
14
+ * double-dispatching. Outside step execution (local dev, tests) there is no
15
+ * stable coordinate to key on — returns null and the dispatch is unkeyed,
16
+ * exactly the old behavior.
17
+ *
18
+ * Key syntax matches the server's `Idempotency-Key` grammar
19
+ * (`[A-Za-z0-9_\-:.]{1,255}`): runId is a UUID, step index a number, and
20
+ * workflow names are kebab-case.
21
+ */
22
+ export function deriveInvokeChildIdempotencyKey(parentRunId: string, childName: string): string | null {
23
+ const step = getActiveStep();
24
+ if (!step) return null;
25
+ const ordinal = nextPauseOrdinalInActiveStep(`invoke-child:${childName}`);
26
+ if (ordinal === null) return null;
27
+ return `invoke-child:${parentRunId}:step${step.stepIndex}:${childName}:${ordinal}`;
28
+ }
29
+
4
30
  /**
5
31
  * Build the public-API child workflow invoker used by legacy and sandboxed
6
32
  * workflow execution. Provider-backed engines may inject a different
@@ -21,15 +47,25 @@ export function buildInvokeChild(
21
47
  childClient = new AgentComposeClient({ apiKey, baseUrl });
22
48
  return childClient;
23
49
  };
24
- return (name, input, childOpts) => getChildClient().invokeAndWait(name, input, {
25
- ...childOpts,
26
- parentRunId: runId,
27
- // The runner sets `AGENT_COMPOSE_FACTORY` (the run's factory slug — see
28
- // activities.ts loadRunnerEnvs). Default the child to it so it lands in the
29
- // SAME factory as the parent — the per-run API key is factory-scoped, so a
30
- // child dispatched into another factory 403s. (Was reading the never-set
31
- // `AGENT_COMPOSE_FACTORY_SLUG`, silently falling back to "default".)
32
- factorySlug: childOpts?.factorySlug ?? opts.defaultFactorySlug
33
- ?? process.env.AGENT_COMPOSE_FACTORY ?? process.env.AGENT_COMPOSE_FACTORY_SLUG ?? "default",
34
- });
50
+ return (name, input, childOpts) => {
51
+ // Replay-safe dispatch: derive a deterministic per-step key so a step
52
+ // re-entered after pause/resume (or a crash-replayed attempt) dedupes to
53
+ // the original child run instead of double-dispatching. An explicit
54
+ // caller-supplied key wins.
55
+ const idempotencyKey = childOpts?.idempotencyKey
56
+ ?? deriveInvokeChildIdempotencyKey(runId, name)
57
+ ?? undefined;
58
+ return getChildClient().invokeAndWait(name, input, {
59
+ ...childOpts,
60
+ ...(idempotencyKey !== undefined ? { idempotencyKey } : {}),
61
+ parentRunId: runId,
62
+ // The runner sets `AGENT_COMPOSE_FACTORY` (the run's factory slug — see
63
+ // activities.ts loadRunnerEnvs). Default the child to it so it lands in the
64
+ // SAME factory as the parent — the per-run API key is factory-scoped, so a
65
+ // child dispatched into another factory 403s. (Was reading the never-set
66
+ // `AGENT_COMPOSE_FACTORY_SLUG`, silently falling back to "default".)
67
+ factorySlug: childOpts?.factorySlug ?? opts.defaultFactorySlug
68
+ ?? process.env.AGENT_COMPOSE_FACTORY ?? process.env.AGENT_COMPOSE_FACTORY_SLUG ?? "default",
69
+ });
70
+ };
35
71
  }