@zibby/skills 0.1.15 → 0.1.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zibby/skills",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Built-in skill definitions for Zibby test automation framework",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,227 @@
1
+ ---
2
+ sidebar_position: 6
3
+ title: Sub-graphs (parent → child)
4
+ ---
5
+
6
+ # Sub-graphs
7
+
8
+ A **sub-graph node** runs another deployed workflow as a child of the current one. Use it when a step is large enough to deserve its own workflow definition — its own state schema, its own version, its own activity-tab history — but you want a parent to dispatch it as part of a larger flow.
9
+
10
+ The shape is one extra field on the existing node config:
11
+
12
+ ```js
13
+ g.addNode('audit', {
14
+ workflow: 'deep-audit', // ← name of another workflow in this project
15
+ });
16
+ ```
17
+
18
+ That's it. No new imports, no UUID, no separate class. The engine recognizes `workflow:` and turns this node into a sub-graph dispatcher.
19
+
20
+ ## When to use a sub-graph
21
+
22
+ | Scenario | Sub-graph? |
23
+ |---|---|
24
+ | Two parents need the same multi-node flow | ✅ Yes — define it once as a child, reference by name |
25
+ | One step needs different state schema than the rest | ✅ Yes — each workflow has its own schema |
26
+ | You want per-step activity-tab history + replay | ✅ Yes — each child run gets its own row |
27
+ | Step is a single LLM call | ❌ No — just add a regular node |
28
+ | Step has its own retry policy | Either works, but a sub-graph gives independent control |
29
+
30
+ ## Sync vs async
31
+
32
+ `async:` flips the dispatch mode:
33
+
34
+ ```js
35
+ g.addNode('audit', { workflow: 'deep-audit' }); // sync (default)
36
+ g.addNode('notify', { workflow: 'slack-notifier', async: true }); // fire-and-forget
37
+ ```
38
+
39
+ | Mode | Behavior | Returns to parent | Use for |
40
+ |---|---|---|---|
41
+ | **sync** (default) | Parent blocks, polls child until terminal status, merges result into parent state | the extracted value (see `output:` below) | Steps where downstream nodes depend on the child's result |
42
+ | **async** (`async: true`) | Parent dispatches the child and continues immediately. No polling. | a dispatch handle `{ jobId, status, workflow }` | Fan-out, notifications, side-effect work the parent shouldn't wait for |
43
+
44
+ Quota: every sub-graph run counts as a separate execution against the account's monthly cap (parent + 3 children = 4 executions).
45
+
46
+ ## Full option surface
47
+
48
+ ```js
49
+ g.addNode('audit', {
50
+ // ─── Required ─────────────────────────────────────────────────────
51
+ workflow: 'deep-audit', // resolved by name within this project
52
+
53
+ // ─── Mode (default sync) ──────────────────────────────────────────
54
+ async: false, // false = block + merge, true = fire-forget
55
+
56
+ // ─── State plumbing ───────────────────────────────────────────────
57
+ input: (state) => ({ // shape parent state → child input
58
+ ticketId: state.ticketId,
59
+ }), // OR a plain object OR omit (child gets {})
60
+
61
+ output: 'audit.score', // dot-path on child finalState
62
+ // OR (childState) => ({...}) function form
63
+ // OR omit → entire child finalState
64
+
65
+ // ─── Sync tunings (ignored when async: true) ──────────────────────
66
+ timeoutMs: 5 * 60 * 1000, // throw after this long (default 10min)
67
+ pollIntervalMs: 2000, // status-check frequency (default 2s)
68
+
69
+ // ─── Cross-cutting concerns ───────────────────────────────────────
70
+ retries: 3, // engine retries whole dispatch on transient failure
71
+ onComplete: (state, result) => result,
72
+
73
+ // ─── Advanced ─────────────────────────────────────────────────────
74
+ conversationId: 'inherit', // 'inherit' (default) | 'new' | (state) => string
75
+ });
76
+ ```
77
+
78
+ ## How state flows
79
+
80
+ Each workflow has its own state schema — they're independent. The parent must transform its state into the child's input shape, and (optionally) extract whatever it needs back out.
81
+
82
+ ### A complete example — `parent-orchestrator` calls `child-doubler`
83
+
84
+ ```js
85
+ // child-doubler — takes a number, returns it doubled.
86
+ class ChildDoublerAgent extends WorkflowAgent {
87
+ buildGraph() {
88
+ const g = new WorkflowGraph();
89
+ g.setStateSchema(z.object({
90
+ value: z.number(),
91
+ double: z.object({ doubled: z.number() }).optional(),
92
+ }));
93
+ g.addNode('double', {
94
+ _isCustomCode: true,
95
+ outputSchema: z.object({ doubled: z.number() }),
96
+ execute: async (ctx) => ({ doubled: ctx.state.getAll().value * 2 }),
97
+ });
98
+ g.setEntryPoint('double');
99
+ g.addEdge('double', 'END');
100
+ return g;
101
+ }
102
+ }
103
+
104
+ // parent-orchestrator — picks a number, calls child-doubler, reports.
105
+ class ParentOrchestratorAgent extends WorkflowAgent {
106
+ buildGraph() {
107
+ const g = new WorkflowGraph();
108
+ g.setStateSchema(z.object({
109
+ seed: z.number(),
110
+ pick_number: z.object({ value: z.number(), label: z.string() }).optional(),
111
+ call_doubler: z.number().optional(), // ← child's result lands here
112
+ report: z.object({ summary: z.string() }).optional(),
113
+ }));
114
+
115
+ g.addNode('pick_number', pickNumberNode);
116
+
117
+ g.addNode('call_doubler', {
118
+ workflow: 'child-doubler',
119
+ input: (state) => ({ value: state.pick_number.value }),
120
+ output: 'double.doubled', // dot-path through child's node name
121
+ });
122
+
123
+ g.addNode('report', reportNode); // reads state.call_doubler
124
+
125
+ g.setEntryPoint('pick_number');
126
+ g.addEdge('pick_number', 'call_doubler');
127
+ g.addEdge('call_doubler', 'report');
128
+ g.addEdge('report', 'END');
129
+ return g;
130
+ }
131
+ }
132
+ ```
133
+
134
+ Triggering the parent with `{ seed: 21 }`:
135
+
136
+ | Step | What happens | State after |
137
+ |---|---|---|
138
+ | 1 | `pick_number` runs | `{ seed: 21, pick_number: { value: 21, label: '…' } }` |
139
+ | 2 | `call_doubler.input(state)` fires | returns `{ value: 21 }` |
140
+ | 3 | Server validates `{ value: 21 }` against child's `stateSchema` | passes |
141
+ | 4 | Child runs in its own Fargate task. Final state: `{ value: 21, double: { doubled: 42 } }` | (parent waiting) |
142
+ | 5 | Engine extracts `output: 'double.doubled'` → `42` | `state.call_doubler = 42` |
143
+ | 6 | `report` runs, reads `state.call_doubler` | `state.report.summary = '…42…'` |
144
+
145
+ ### Why `output: 'double.doubled'` and not `'doubled'`?
146
+
147
+ Each node's output is stored at `state[nodeName]` in its own graph. So when the child's `double` node returns `{ doubled: 42 }`, that lands at `childState.double.doubled` — `doubled` is *nested under the node name*, not promoted to the top level.
148
+
149
+ If you want multiple fields, use the function form:
150
+
151
+ ```js
152
+ output: (childState) => ({
153
+ doubled: childState.double.doubled,
154
+ echoed: childState.value,
155
+ isDouble: childState.double.doubled === childState.value * 2,
156
+ }),
157
+ // → state.audit = { doubled: 42, echoed: 21, isDouble: true }
158
+ ```
159
+
160
+ Or omit `output:` and the entire `childState` lands at `state[nodeName]` — useful when you don't know yet which fields you'll need.
161
+
162
+ ## Schema validation at the boundary
163
+
164
+ The server runs the same input gate sub-graph triggers hit as user-initiated ones. If the parent's `input:` callback returns a value that doesn't satisfy the child's `stateSchema`, the trigger 400s **before** any Fargate spawn — no wasted compute. The parent's sub-graph node throws a typed error with the missing fields listed.
165
+
166
+ ## Errors
167
+
168
+ Sub-graph failures throw with a `code` field so you can branch:
169
+
170
+ | `err.code` | Meaning | Useful properties |
171
+ |---|---|---|
172
+ | `SUBGRAPH_INVALID_INPUT` | Parent's `input:` produced data that violates the child's stateSchema | `err.missing[]`, `err.validationErrors` |
173
+ | `SUBGRAPH_QUOTA_EXCEEDED` | Account is over its execution cap; child can't dispatch | `err.quotaInfo` |
174
+ | `SUBGRAPH_TRIGGER_FAILED` | Any other HTTP failure from the trigger endpoint | `err.status` |
175
+
176
+ Sync-mode terminal failures (child completed in `failed` / `canceled` / `timeout`):
177
+
178
+ ```js
179
+ err.subgraphJobId // child's executionId — look up in activity tab
180
+ err.subgraphStatus // 'failed' | 'canceled' | 'timeout'
181
+ ```
182
+
183
+ `retries:` on the sub-graph node re-runs the whole dispatch (trigger + poll) on transient failures, same semantics as a regular node retry.
184
+
185
+ ## What's deployed vs what you write
186
+
187
+ You only ever reference workflows by **name**. The cloud handles the UUID resolution.
188
+
189
+ | Stage | What you write | What the backend does |
190
+ |---|---|---|
191
+ | `zibby workflow deploy child-doubler` | nothing about UUIDs | mints UUID, stores `(projectId, workflowType='child-doubler', uuid='…')` |
192
+ | `subgraph('child-doubler')` in parent code | just the name | stored as a string in the parent's graph definition |
193
+ | `zibby workflow deploy parent-orchestrator` | nothing | looks up `'child-doubler'` → UUID, snapshots the dependency |
194
+ | Parent runs in Fargate → hits sub-graph node | nothing | POSTs to `/workflows/<uuid>/trigger` with `parentExecutionId` |
195
+
196
+ Names are unique per project (DDB primary key enforces this), so `subgraph('child-doubler')` resolves unambiguously within the parent's project.
197
+
198
+ ## Activity-tab tree-view
199
+
200
+ Each child execution row carries `parentExecutionId` pointing at the parent. The activity tab uses this to render parent runs as collapsible groups — expand to see the chain of children.
201
+
202
+ | Row | `parentExecutionId` | Type |
203
+ |---|---|---|
204
+ | Parent (orchestrator) | `null` | top-level (user-triggered) |
205
+ | Child (doubler) | `<parent's executionId>` | dispatched as sub-graph |
206
+
207
+ ## Local development
208
+
209
+ Sub-graph dispatch needs the `PROGRESS_API_URL` env var (the public API base). That's set automatically on Fargate runs. For local dev, you have two options:
210
+
211
+ 1. **Deploy both workflows to cloud, then trigger the parent.** The cloud path always works.
212
+ 2. **Mock the trigger + status endpoints locally.** See [`workflows/parent-orchestrator/mock-server.mjs`](https://github.com/ZibbyHQ/agent-workflow/tree/main/examples) in the agent-workflow repo for a 90-line example that simulates the dispatch + poll loop.
213
+
214
+ In-process sub-graph execution (running the child in the parent's Node process directly, no HTTP) is **not supported** — we picked consistency between local and cloud over the 10s spawn-time savings.
215
+
216
+ ## Cross-project sub-graphs
217
+
218
+ `workflow: 'name'` resolves within the parent's own project. To call another project's workflow, pass an explicit project ID:
219
+
220
+ ```js
221
+ g.addNode('audit', {
222
+ workflow: 'shared-audit',
223
+ project: 'b6219c3a-…', // explicit cross-project reference
224
+ });
225
+ ```
226
+
227
+ The caller must have access to the destination project (same account, or invited). Cross-**account** sub-graphs are not in v1.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zibby/skills",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Built-in skill definitions for Zibby test automation framework",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",