@otto-code/protocol 0.6.7 → 0.7.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.
@@ -24,6 +24,8 @@ export const KNOWN_MODEL_TIERS = {
24
24
  // Opus variants are "deep", non-1M Opus and Sonnet are "standard", Haiku is
25
25
  // "fast". Fable (1M, most powerful) is "deep".
26
26
  "claude-fable-5": "deep",
27
+ "claude-opus-5[1m]": "deep",
28
+ "claude-opus-5": "standard",
27
29
  "claude-opus-4-8[1m]": "deep",
28
30
  "claude-opus-4-8": "standard",
29
31
  "claude-opus-4-7[1m]": "deep",
@@ -67,6 +67,7 @@ export declare const RunPhaseCandidateSchema: z.ZodObject<{
67
67
  summary: z.ZodOptional<z.ZodString>;
68
68
  }, z.core.$loose>>;
69
69
  summary: z.ZodOptional<z.ZodString>;
70
+ outputFields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
70
71
  }, z.core.$loose>;
71
72
  export type RunPhaseCandidate = z.infer<typeof RunPhaseCandidateSchema>;
72
73
  export declare const RunPhaseSchema: z.ZodObject<{
@@ -93,12 +94,18 @@ export declare const RunPhaseSchema: z.ZodObject<{
93
94
  summary: z.ZodOptional<z.ZodString>;
94
95
  }, z.core.$loose>>;
95
96
  summary: z.ZodOptional<z.ZodString>;
97
+ outputFields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
96
98
  }, z.core.$loose>>>;
97
99
  notes: z.ZodOptional<z.ZodString>;
100
+ skipReason: z.ZodOptional<z.ZodString>;
101
+ retryAttempts: z.ZodOptional<z.ZodNumber>;
102
+ timedOut: z.ZodOptional<z.ZodBoolean>;
98
103
  startedAt: z.ZodOptional<z.ZodString>;
99
104
  completedAt: z.ZodOptional<z.ZodString>;
100
105
  }, z.core.$loose>;
101
106
  export type RunPhase = z.infer<typeof RunPhaseSchema>;
107
+ export declare const GRAPH_SKIP_REASONS: readonly ["condition", "upstream-skipped", "upstream-failed", "canceled"];
108
+ export type GraphSkipReason = (typeof GRAPH_SKIP_REASONS)[number];
102
109
  export declare const RunSchema: z.ZodObject<{
103
110
  id: z.ZodString;
104
111
  title: z.ZodString;
@@ -133,8 +140,12 @@ export declare const RunSchema: z.ZodObject<{
133
140
  summary: z.ZodOptional<z.ZodString>;
134
141
  }, z.core.$loose>>;
135
142
  summary: z.ZodOptional<z.ZodString>;
143
+ outputFields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
136
144
  }, z.core.$loose>>>;
137
145
  notes: z.ZodOptional<z.ZodString>;
146
+ skipReason: z.ZodOptional<z.ZodString>;
147
+ retryAttempts: z.ZodOptional<z.ZodNumber>;
148
+ timedOut: z.ZodOptional<z.ZodBoolean>;
138
149
  startedAt: z.ZodOptional<z.ZodString>;
139
150
  completedAt: z.ZodOptional<z.ZodString>;
140
151
  }, z.core.$loose>>>;
@@ -162,6 +173,30 @@ export declare const GraphInputSchema: z.ZodObject<{
162
173
  defaultValue: z.ZodOptional<z.ZodString>;
163
174
  }, z.core.$loose>;
164
175
  export type GraphInput = z.infer<typeof GraphInputSchema>;
176
+ export declare const PromptTemplateSchema: z.ZodObject<{
177
+ id: z.ZodString;
178
+ name: z.ZodString;
179
+ description: z.ZodOptional<z.ZodString>;
180
+ content: z.ZodString;
181
+ variables: z.ZodOptional<z.ZodArray<z.ZodObject<{
182
+ key: z.ZodString;
183
+ label: z.ZodString;
184
+ description: z.ZodOptional<z.ZodString>;
185
+ multiline: z.ZodOptional<z.ZodBoolean>;
186
+ required: z.ZodOptional<z.ZodBoolean>;
187
+ defaultValue: z.ZodOptional<z.ZodString>;
188
+ }, z.core.$loose>>>;
189
+ snippet: z.ZodOptional<z.ZodBoolean>;
190
+ builtIn: z.ZodOptional<z.ZodBoolean>;
191
+ createdAt: z.ZodOptional<z.ZodString>;
192
+ updatedAt: z.ZodOptional<z.ZodString>;
193
+ }, z.core.$loose>;
194
+ export type PromptTemplate = z.infer<typeof PromptTemplateSchema>;
195
+ export declare const NodePromptTemplateRefSchema: z.ZodObject<{
196
+ templateId: z.ZodString;
197
+ variables: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
198
+ }, z.core.$loose>;
199
+ export type NodePromptTemplateRef = z.infer<typeof NodePromptTemplateRefSchema>;
165
200
  export declare const GRAPH_NODE_KINDS: readonly ["orchestrator", "agent"];
166
201
  export type GraphNodeKind = (typeof GRAPH_NODE_KINDS)[number];
167
202
  export declare const GraphNodeLoopSchema: z.ZodObject<{
@@ -173,6 +208,47 @@ export declare const GraphNodeLoopSchema: z.ZodObject<{
173
208
  }, z.core.$loose>>;
174
209
  }, z.core.$loose>;
175
210
  export type GraphNodeLoop = z.infer<typeof GraphNodeLoopSchema>;
211
+ export declare const GRAPH_OUTPUT_FIELD_TYPES: readonly ["string", "number", "boolean", "array"];
212
+ export type GraphOutputFieldType = (typeof GRAPH_OUTPUT_FIELD_TYPES)[number];
213
+ export declare const GraphOutputFieldSchema: z.ZodObject<{
214
+ key: z.ZodString;
215
+ type: z.ZodString;
216
+ description: z.ZodOptional<z.ZodString>;
217
+ required: z.ZodOptional<z.ZodBoolean>;
218
+ }, z.core.$loose>;
219
+ export type GraphOutputField = z.infer<typeof GraphOutputFieldSchema>;
220
+ export declare const GraphNodeOutputSchema: z.ZodObject<{
221
+ fields: z.ZodArray<z.ZodObject<{
222
+ key: z.ZodString;
223
+ type: z.ZodString;
224
+ description: z.ZodOptional<z.ZodString>;
225
+ required: z.ZodOptional<z.ZodBoolean>;
226
+ }, z.core.$loose>>;
227
+ }, z.core.$loose>;
228
+ export type GraphNodeOutput = z.infer<typeof GraphNodeOutputSchema>;
229
+ export declare const GRAPH_QUERY_TOOL_KINDS: readonly ["command", "http-get", "file-read"];
230
+ export type GraphQueryToolKind = (typeof GRAPH_QUERY_TOOL_KINDS)[number];
231
+ export declare const GraphQueryToolSchema: z.ZodObject<{
232
+ name: z.ZodString;
233
+ description: z.ZodString;
234
+ kind: z.ZodString;
235
+ parameters: z.ZodOptional<z.ZodArray<z.ZodObject<{
236
+ key: z.ZodString;
237
+ type: z.ZodString;
238
+ description: z.ZodOptional<z.ZodString>;
239
+ required: z.ZodOptional<z.ZodBoolean>;
240
+ }, z.core.$loose>>>;
241
+ command: z.ZodOptional<z.ZodArray<z.ZodString>>;
242
+ url: z.ZodOptional<z.ZodString>;
243
+ path: z.ZodOptional<z.ZodString>;
244
+ }, z.core.$loose>;
245
+ export type GraphQueryTool = z.infer<typeof GraphQueryToolSchema>;
246
+ export declare const GraphNodeRetrySchema: z.ZodObject<{
247
+ maxAttempts: z.ZodNumber;
248
+ backoffMs: z.ZodNumber;
249
+ multiplier: z.ZodOptional<z.ZodNumber>;
250
+ }, z.core.$loose>;
251
+ export type GraphNodeRetry = z.infer<typeof GraphNodeRetrySchema>;
176
252
  export declare const GraphNodeSchema: z.ZodObject<{
177
253
  id: z.ZodString;
178
254
  kind: z.ZodString;
@@ -180,6 +256,10 @@ export declare const GraphNodeSchema: z.ZodObject<{
180
256
  role: z.ZodOptional<z.ZodString>;
181
257
  prompt: z.ZodOptional<z.ZodString>;
182
258
  promptFromInput: z.ZodOptional<z.ZodString>;
259
+ promptTemplate: z.ZodOptional<z.ZodObject<{
260
+ templateId: z.ZodString;
261
+ variables: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
262
+ }, z.core.$loose>>;
183
263
  autonomous: z.ZodOptional<z.ZodBoolean>;
184
264
  loop: z.ZodOptional<z.ZodObject<{
185
265
  times: z.ZodOptional<z.ZodNumber>;
@@ -189,6 +269,36 @@ export declare const GraphNodeSchema: z.ZodObject<{
189
269
  max: z.ZodNumber;
190
270
  }, z.core.$loose>>;
191
271
  }, z.core.$loose>>;
272
+ output: z.ZodOptional<z.ZodObject<{
273
+ fields: z.ZodArray<z.ZodObject<{
274
+ key: z.ZodString;
275
+ type: z.ZodString;
276
+ description: z.ZodOptional<z.ZodString>;
277
+ required: z.ZodOptional<z.ZodBoolean>;
278
+ }, z.core.$loose>>;
279
+ }, z.core.$loose>>;
280
+ access: z.ZodOptional<z.ZodString>;
281
+ tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
282
+ queryTools: z.ZodOptional<z.ZodArray<z.ZodObject<{
283
+ name: z.ZodString;
284
+ description: z.ZodString;
285
+ kind: z.ZodString;
286
+ parameters: z.ZodOptional<z.ZodArray<z.ZodObject<{
287
+ key: z.ZodString;
288
+ type: z.ZodString;
289
+ description: z.ZodOptional<z.ZodString>;
290
+ required: z.ZodOptional<z.ZodBoolean>;
291
+ }, z.core.$loose>>>;
292
+ command: z.ZodOptional<z.ZodArray<z.ZodString>>;
293
+ url: z.ZodOptional<z.ZodString>;
294
+ path: z.ZodOptional<z.ZodString>;
295
+ }, z.core.$loose>>>;
296
+ retry: z.ZodOptional<z.ZodObject<{
297
+ maxAttempts: z.ZodNumber;
298
+ backoffMs: z.ZodNumber;
299
+ multiplier: z.ZodOptional<z.ZodNumber>;
300
+ }, z.core.$loose>>;
301
+ timeoutMs: z.ZodOptional<z.ZodNumber>;
192
302
  model: z.ZodOptional<z.ZodString>;
193
303
  position: z.ZodOptional<z.ZodObject<{
194
304
  x: z.ZodNumber;
@@ -196,10 +306,21 @@ export declare const GraphNodeSchema: z.ZodObject<{
196
306
  }, z.core.$loose>>;
197
307
  }, z.core.$loose>;
198
308
  export type GraphNode = z.infer<typeof GraphNodeSchema>;
309
+ export declare const GraphEdgeConditionSchema: z.ZodObject<{
310
+ expression: z.ZodString;
311
+ }, z.core.$loose>;
312
+ export type GraphEdgeCondition = z.infer<typeof GraphEdgeConditionSchema>;
199
313
  export declare const GraphEdgeSchema: z.ZodObject<{
200
314
  id: z.ZodOptional<z.ZodString>;
201
315
  from: z.ZodString;
202
316
  to: z.ZodString;
317
+ fromPort: z.ZodOptional<z.ZodString>;
318
+ toPort: z.ZodOptional<z.ZodString>;
319
+ when: z.ZodOptional<z.ZodObject<{
320
+ expression: z.ZodString;
321
+ }, z.core.$loose>>;
322
+ fields: z.ZodOptional<z.ZodArray<z.ZodString>>;
323
+ label: z.ZodOptional<z.ZodString>;
203
324
  }, z.core.$loose>;
204
325
  export type GraphEdge = z.infer<typeof GraphEdgeSchema>;
205
326
  export declare const OrchestrationGraphSchema: z.ZodObject<{
@@ -221,6 +342,10 @@ export declare const OrchestrationGraphSchema: z.ZodObject<{
221
342
  role: z.ZodOptional<z.ZodString>;
222
343
  prompt: z.ZodOptional<z.ZodString>;
223
344
  promptFromInput: z.ZodOptional<z.ZodString>;
345
+ promptTemplate: z.ZodOptional<z.ZodObject<{
346
+ templateId: z.ZodString;
347
+ variables: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
348
+ }, z.core.$loose>>;
224
349
  autonomous: z.ZodOptional<z.ZodBoolean>;
225
350
  loop: z.ZodOptional<z.ZodObject<{
226
351
  times: z.ZodOptional<z.ZodNumber>;
@@ -230,6 +355,36 @@ export declare const OrchestrationGraphSchema: z.ZodObject<{
230
355
  max: z.ZodNumber;
231
356
  }, z.core.$loose>>;
232
357
  }, z.core.$loose>>;
358
+ output: z.ZodOptional<z.ZodObject<{
359
+ fields: z.ZodArray<z.ZodObject<{
360
+ key: z.ZodString;
361
+ type: z.ZodString;
362
+ description: z.ZodOptional<z.ZodString>;
363
+ required: z.ZodOptional<z.ZodBoolean>;
364
+ }, z.core.$loose>>;
365
+ }, z.core.$loose>>;
366
+ access: z.ZodOptional<z.ZodString>;
367
+ tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
368
+ queryTools: z.ZodOptional<z.ZodArray<z.ZodObject<{
369
+ name: z.ZodString;
370
+ description: z.ZodString;
371
+ kind: z.ZodString;
372
+ parameters: z.ZodOptional<z.ZodArray<z.ZodObject<{
373
+ key: z.ZodString;
374
+ type: z.ZodString;
375
+ description: z.ZodOptional<z.ZodString>;
376
+ required: z.ZodOptional<z.ZodBoolean>;
377
+ }, z.core.$loose>>>;
378
+ command: z.ZodOptional<z.ZodArray<z.ZodString>>;
379
+ url: z.ZodOptional<z.ZodString>;
380
+ path: z.ZodOptional<z.ZodString>;
381
+ }, z.core.$loose>>>;
382
+ retry: z.ZodOptional<z.ZodObject<{
383
+ maxAttempts: z.ZodNumber;
384
+ backoffMs: z.ZodNumber;
385
+ multiplier: z.ZodOptional<z.ZodNumber>;
386
+ }, z.core.$loose>>;
387
+ timeoutMs: z.ZodOptional<z.ZodNumber>;
233
388
  model: z.ZodOptional<z.ZodString>;
234
389
  position: z.ZodOptional<z.ZodObject<{
235
390
  x: z.ZodNumber;
@@ -240,6 +395,13 @@ export declare const OrchestrationGraphSchema: z.ZodObject<{
240
395
  id: z.ZodOptional<z.ZodString>;
241
396
  from: z.ZodString;
242
397
  to: z.ZodString;
398
+ fromPort: z.ZodOptional<z.ZodString>;
399
+ toPort: z.ZodOptional<z.ZodString>;
400
+ when: z.ZodOptional<z.ZodObject<{
401
+ expression: z.ZodString;
402
+ }, z.core.$loose>>;
403
+ fields: z.ZodOptional<z.ZodArray<z.ZodString>>;
404
+ label: z.ZodOptional<z.ZodString>;
243
405
  }, z.core.$loose>>>;
244
406
  builtIn: z.ZodOptional<z.ZodBoolean>;
245
407
  createdAt: z.ZodOptional<z.ZodString>;
@@ -247,4 +409,14 @@ export declare const OrchestrationGraphSchema: z.ZodObject<{
247
409
  }, z.core.$loose>;
248
410
  export type OrchestrationGraph = z.infer<typeof OrchestrationGraphSchema>;
249
411
  export declare function validateOrchestrationGraph(graph: OrchestrationGraph): string[];
412
+ /**
413
+ * Advisory findings: things that are almost certainly authoring mistakes but
414
+ * leave the graph executable. Separate from `validateOrchestrationGraph`
415
+ * because that one is the daemon's hard gate — anything it returns stops a run,
416
+ * and none of these should.
417
+ *
418
+ * Expression *syntax* is not checked here: the JSONata parser is daemon-side,
419
+ * so this stays dependency-free for the client bundle.
420
+ */
421
+ export declare function reviewOrchestrationGraph(graph: OrchestrationGraph): string[];
250
422
  //# sourceMappingURL=orchestration.d.ts.map
@@ -141,6 +141,9 @@ export const RunPhaseCandidateSchema = z
141
141
  // The candidate's final message (synthesis input); may be large — clients
142
142
  // truncate for display.
143
143
  summary: z.string().optional(),
144
+ // Validated output fields, when the node declared them (GraphNode.output).
145
+ // Values only — anything large belongs in a file the next node reads.
146
+ outputFields: z.record(z.string(), z.unknown()).optional(),
144
147
  })
145
148
  .passthrough();
146
149
  export const RunPhaseSchema = z
@@ -159,10 +162,29 @@ export const RunPhaseSchema = z
159
162
  candidates: z.array(RunPhaseCandidateSchema).optional(),
160
163
  // Free-text runtime notes (why it blocked, which cap tripped, gap named).
161
164
  notes: z.string().optional(),
165
+ // Machine-readable reason a phase is "skipped" — the human sentence stays in
166
+ // `notes`. Absent on a skipped phase means an older daemon wrote it (read it
167
+ // as "upstream-failed", the only skip that existed then). Open vocabulary,
168
+ // plain string on the wire; known values in GRAPH_SKIP_REASONS.
169
+ skipReason: z.string().optional(),
170
+ // How many extra attempts a node's retry policy spent (0/absent = none).
171
+ retryAttempts: z.number().int().min(0).optional(),
172
+ // True when the phase's last attempt ended at its time limit rather than
173
+ // by failing on its own — a different diagnosis, so a different flag.
174
+ timedOut: z.boolean().optional(),
162
175
  startedAt: z.string().optional(),
163
176
  completedAt: z.string().optional(),
164
177
  })
165
178
  .passthrough();
179
+ // Why a phase is "skipped" (see RunPhase.skipReason). A skip is never an error:
180
+ // "condition" means an edge's condition routed around this node, the other two
181
+ // mean an upstream node was itself skipped or failed. Plain string on the wire.
182
+ export const GRAPH_SKIP_REASONS = [
183
+ "condition",
184
+ "upstream-skipped",
185
+ "upstream-failed",
186
+ "canceled",
187
+ ];
166
188
  export const RunSchema = z
167
189
  .object({
168
190
  id: z.string().min(1),
@@ -228,6 +250,38 @@ export const GraphInputSchema = z
228
250
  defaultValue: z.string().optional(),
229
251
  })
230
252
  .passthrough();
253
+ // ── Prompt templates ────────────────────────────────────────────────────────
254
+ // Host-level reusable prompts, stored like Graphs. A template with
255
+ // `snippet: true` is meant to be included by other templates rather than bound
256
+ // to a node directly — the shared "how to submit your output" block is the
257
+ // motivating case, since repeating it in every node is both duplication and
258
+ // tokens on every dispatch.
259
+ export const PromptTemplateSchema = z
260
+ .object({
261
+ id: z.string().min(1),
262
+ name: z.string().min(1),
263
+ description: z.string().optional(),
264
+ // EJS source. Rendered with HTML escaping disabled — these are prompts,
265
+ // not markup, and characters like & must survive verbatim.
266
+ content: z.string(),
267
+ // Variables the template expects, so the designer can render a binding
268
+ // form instead of asking the author to remember them.
269
+ variables: z.array(GraphInputSchema).optional(),
270
+ snippet: z.boolean().optional(),
271
+ builtIn: z.boolean().optional(),
272
+ createdAt: z.string().optional(),
273
+ updatedAt: z.string().optional(),
274
+ })
275
+ .passthrough();
276
+ // A node's binding to a stored template. A value is a literal, `$inputs.<key>`
277
+ // (a declared graph input), or `$output.<nodeId>.<field>` (an upstream node's
278
+ // output field).
279
+ export const NodePromptTemplateRefSchema = z
280
+ .object({
281
+ templateId: z.string().min(1),
282
+ variables: z.record(z.string(), z.string()).optional(),
283
+ })
284
+ .passthrough();
231
285
  // Node kinds (open vocabulary): "orchestrator" — the single root that hosts
232
286
  // the orchestration chat and anchors the Visualizer; "agent" — a worker node.
233
287
  export const GRAPH_NODE_KINDS = ["orchestrator", "agent"];
@@ -249,6 +303,69 @@ export const GraphNodeLoopSchema = z
249
303
  .optional(),
250
304
  })
251
305
  .passthrough();
306
+ // ── Output fields (the value plane) ─────────────────────────────────────────
307
+ // What a node declares it will produce. Plain JSON descriptors, never a
308
+ // serialized Zod schema: they have to be three things at once — wire-safe, a
309
+ // form the designer can render, and compilable to both Zod (validation) and
310
+ // JSON Schema (the submit_output tool's input). `type` is an open string
311
+ // vocabulary so a new type can never break an old parser.
312
+ export const GRAPH_OUTPUT_FIELD_TYPES = ["string", "number", "boolean", "array"];
313
+ export const GraphOutputFieldSchema = z
314
+ .object({
315
+ key: z.string().min(1),
316
+ type: z.string().min(1),
317
+ // Shown to the node's agent as the field's description — this is the whole
318
+ // instruction it gets about what to put here, so it earns its place.
319
+ description: z.string().optional(),
320
+ // Absent ⇒ required. You declared the field; producing it is the contract.
321
+ required: z.boolean().optional(),
322
+ })
323
+ .passthrough();
324
+ export const GraphNodeOutputSchema = z
325
+ .object({
326
+ fields: z.array(GraphOutputFieldSchema),
327
+ })
328
+ .passthrough();
329
+ // ── Query tools (per-node read-only lookups) ────────────────────────────────
330
+ // Author-defined tools that exist only inside one node's session, so a node can
331
+ // be given exactly the lookup it needs instead of the whole workspace.
332
+ //
333
+ // Three kinds, all read-only by construction:
334
+ // command argv array only — no shell, so no operators and no injection
335
+ // surface. A pipeline belongs in a script the tool points at.
336
+ // http-get GET only, no author-supplied headers (credentials can't leak
337
+ // into a graph template).
338
+ // file-read path must resolve inside the run's cwd.
339
+ export const GRAPH_QUERY_TOOL_KINDS = ["command", "http-get", "file-read"];
340
+ export const GraphQueryToolSchema = z
341
+ .object({
342
+ // Tool name the agent calls. Namespaced at registration so it can never
343
+ // shadow a built-in Otto tool.
344
+ name: z.string().min(1),
345
+ description: z.string().min(1),
346
+ kind: z.string().min(1),
347
+ // Declared parameters, reusing the output-field descriptor shape.
348
+ parameters: z.array(GraphOutputFieldSchema).optional(),
349
+ // kind "command": the executable plus its arguments, each argument a
350
+ // separate entry. `{{param}}` substitutes a declared parameter's value as
351
+ // one argument — never as a fragment the shell could re-split.
352
+ command: z.array(z.string()).optional(),
353
+ // kind "http-get": the URL, with `{{param}}` substitution (encoded).
354
+ url: z.string().optional(),
355
+ // kind "file-read": workspace-relative path, with `{{param}}` substitution.
356
+ path: z.string().optional(),
357
+ })
358
+ .passthrough();
359
+ // Bounded re-dispatch with exponential backoff. `maxAttempts` counts the first
360
+ // try, so 1 means "no retry" and is allowed (a designer default that hasn't
361
+ // been changed yet shouldn't be a validation error).
362
+ export const GraphNodeRetrySchema = z
363
+ .object({
364
+ maxAttempts: z.number().int().min(1).max(10),
365
+ backoffMs: z.number().int().min(0).max(600000),
366
+ multiplier: z.number().min(1).max(4).optional(),
367
+ })
368
+ .passthrough();
252
369
  export const GraphNodeSchema = z
253
370
  .object({
254
371
  id: z.string().min(1),
@@ -261,16 +378,60 @@ export const GraphNodeSchema = z
261
378
  prompt: z.string().optional(),
262
379
  // Key of a declared input whose value joins (or forms) the prompt.
263
380
  promptFromInput: z.string().optional(),
381
+ // Load this node's prompt from a stored template instead of `prompt`.
382
+ // Resolution failure falls back to `prompt`, so a deleted template
383
+ // degrades a node rather than breaking a graph.
384
+ promptTemplate: NodePromptTemplateRefSchema.optional(),
264
385
  // Leaf-only: the node may orchestrate its own agents (full otto toolset
265
386
  // minus start_run). Non-autonomous nodes get no orchestration tools at all.
266
387
  autonomous: z.boolean().optional(),
267
388
  loop: GraphNodeLoopSchema.optional(),
389
+ // Declared output fields. Present ⇒ the node's agent gets a submit_output
390
+ // tool it must call, its result is validated, and downstream nodes receive
391
+ // the fields as data instead of re-reading prose. Absent ⇒ today exactly.
392
+ output: GraphNodeOutputSchema.optional(),
393
+ // How much of the workspace this node's agent may touch: "none" (no
394
+ // filesystem), "read", or "write". Absent ⇒ "write", today's behaviour.
395
+ //
396
+ // Enforced by withholding tools at spawn, never by asking the model — and a
397
+ // provider that can't enforce it refuses the node at compile time rather
398
+ // than running it with full access. Open string vocabulary; known values in
399
+ // WORKSPACE_ACCESS_LEVELS (protocol/agent-types).
400
+ access: z.string().optional(),
401
+ // Per-node Otto tool allowlist, by group (see OTTO_TOOL_GROUPS). Absent ⇒
402
+ // whatever the node's tool policy already allows. Present ⇒ only these
403
+ // groups, intersected with that policy and the daemon-wide allowlist — a
404
+ // node can narrow its own authority, never widen it.
405
+ //
406
+ // Narrowing is a cost lever as much as a safety one: the tool catalog is
407
+ // paid for in input tokens on every request the node's agent makes, and a
408
+ // smaller catalog measurably helps smaller models stay on task.
409
+ tools: z.array(z.string().min(1)).optional(),
410
+ // Read-only lookups available only inside this node's session.
411
+ queryTools: z.array(GraphQueryToolSchema).optional(),
412
+ // Resilience against transient failure. Distinct from `loop`, which is
413
+ // quality iteration: a loop re-runs work that succeeded but wasn't good
414
+ // enough; a retry re-runs work that never completed. Retry wraps the whole
415
+ // node including its loop, and every attempt is charged to the run's agent
416
+ // cap — a retry is never a private allowance.
417
+ retry: GraphNodeRetrySchema.optional(),
418
+ // Wall-clock ceiling for one attempt of this node. On expiry the agent is
419
+ // really cancelled (not merely stopped being awaited) and the node fails,
420
+ // which its retry policy may then catch.
421
+ timeoutMs: z.number().int().min(1000).max(21600000).optional(),
268
422
  // Explicit model override (otherwise the resolved personality/team decides).
269
423
  model: z.string().optional(),
270
424
  // Designer canvas layout.
271
425
  position: z.object({ x: z.number(), y: z.number() }).passthrough().optional(),
272
426
  })
273
427
  .passthrough();
428
+ // An edge condition. Kept as a wrapper object rather than a bare string so a
429
+ // future evaluation dialect can be named without a second field.
430
+ export const GraphEdgeConditionSchema = z
431
+ .object({
432
+ expression: z.string().min(1),
433
+ })
434
+ .passthrough();
274
435
  // A directed edge: `from`'s final output becomes labeled input material for
275
436
  // `to`. Fan-in is an all-inputs barrier held by the daemon — agents never know
276
437
  // about waiting.
@@ -279,6 +440,30 @@ export const GraphEdgeSchema = z
279
440
  id: z.string().min(1).optional(),
280
441
  from: z.string().min(1),
281
442
  to: z.string().min(1),
443
+ // Named ports. Absent ⇒ "output" → "input", which is every edge today, so
444
+ // existing graphs parse and execute identically. Reserved ahead of the
445
+ // control nodes that need more than one outcome (a gate's approved/rejected,
446
+ // a check's pass/fail): adding two optional fields now is free, and adding a
447
+ // second port later would mean changing the canvas, the schema and the
448
+ // engine's value routing at once.
449
+ fromPort: z.string().min(1).optional(),
450
+ toPort: z.string().min(1).optional(),
451
+ // Condition: a JSONata expression evaluated against the upstream node's
452
+ // output fields once it settles (falling back to its prose as `output`
453
+ // when it declared none). Truthy ⇒ this edge delivers; falsy ⇒ the edge is
454
+ // inactive and its target is skipped with reason "condition". Absent ⇒
455
+ // always delivers, which is every edge today.
456
+ //
457
+ // The condition lives on the edge rather than inside a node so the branch
458
+ // is visible as a labelled wire — the graph shows its own control flow.
459
+ when: GraphEdgeConditionSchema.optional(),
460
+ // Which of the upstream node's output fields this edge carries. Absent ⇒
461
+ // all of them. Selection only, never renaming: downstream is a prompt, not
462
+ // a typed function signature, so remapping keys would buy nothing and
463
+ // cost the reader the ability to trace a value by name.
464
+ fields: z.array(z.string().min(1)).optional(),
465
+ // Display-only label for the wire.
466
+ label: z.string().optional(),
282
467
  })
283
468
  .passthrough();
284
469
  export const OrchestrationGraphSchema = z
@@ -347,6 +532,28 @@ function validateGraphEdges(graph, nodeIds) {
347
532
  }
348
533
  return problems;
349
534
  }
535
+ /**
536
+ * Advisory findings: things that are almost certainly authoring mistakes but
537
+ * leave the graph executable. Separate from `validateOrchestrationGraph`
538
+ * because that one is the daemon's hard gate — anything it returns stops a run,
539
+ * and none of these should.
540
+ *
541
+ * Expression *syntax* is not checked here: the JSONata parser is daemon-side,
542
+ * so this stays dependency-free for the client bundle.
543
+ */
544
+ export function reviewOrchestrationGraph(graph) {
545
+ const warnings = [];
546
+ for (const edge of graph.edges ?? []) {
547
+ if (!edge.when?.expression?.trim()) {
548
+ continue;
549
+ }
550
+ const source = graph.nodes.find((node) => node.id === edge.from);
551
+ if (source && !source.output?.fields?.length) {
552
+ warnings.push(`The edge from "${source.title}" has a condition, but "${source.title}" declares no output fields — the condition can only test its prose as \`output\`.`);
553
+ }
554
+ }
555
+ return warnings;
556
+ }
350
557
  // Cycle check (loops are node-level annotations, never cyclic edges): Kahn.
351
558
  function hasGraphCycle(graph, outgoing, incoming) {
352
559
  const indegree = new Map();
@@ -390,6 +597,25 @@ function validateGraphNode(node, declaredInputs) {
390
597
  }
391
598
  }
392
599
  problems.push(...validateGraphNodeLoop(node));
600
+ problems.push(...validateGraphNodeOutput(node));
601
+ return problems;
602
+ }
603
+ function validateGraphNodeOutput(node) {
604
+ if (!node.output) {
605
+ return [];
606
+ }
607
+ const fields = node.output.fields ?? [];
608
+ if (fields.length === 0) {
609
+ return [`Node "${node.title}" declares output fields but lists none.`];
610
+ }
611
+ const problems = [];
612
+ const seen = new Set();
613
+ for (const field of fields) {
614
+ if (seen.has(field.key)) {
615
+ problems.push(`Node "${node.title}" declares the output field "${field.key}" twice.`);
616
+ }
617
+ seen.add(field.key);
618
+ }
393
619
  return problems;
394
620
  }
395
621
  function validateGraphNodeLoop(node) {
@@ -40,7 +40,7 @@ export declare const ProviderProfileModelSchema: z.ZodObject<{
40
40
  * of these groups; omitting the selection means all groups. Kept deliberately
41
41
  * coarse — users pick groups, not individual tools.
42
42
  */
43
- export declare const OTTO_TOOL_GROUPS: readonly ["preview", "browser", "web", "agents", "terminals", "schedules", "artifacts", "workspace"];
43
+ export declare const OTTO_TOOL_GROUPS: readonly ["preview", "browser", "web", "agents", "terminals", "schedules", "artifacts", "widgets", "workspace"];
44
44
  export type OttoToolGroup = (typeof OTTO_TOOL_GROUPS)[number];
45
45
  /**
46
46
  * Map a tool name to its group. Covers both Otto's catalog tools and the
@@ -148,6 +148,7 @@ export declare const ProviderOverrideSchema: z.ZodObject<{
148
148
  agents: "agents";
149
149
  terminals: "terminals";
150
150
  schedules: "schedules";
151
+ widgets: "widgets";
151
152
  workspace: "workspace";
152
153
  }>>>;
153
154
  mcpServers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -221,6 +222,7 @@ export declare const ProviderOverridesSchema: z.ZodRecord<z.ZodString, z.ZodObje
221
222
  agents: "agents";
222
223
  terminals: "terminals";
223
224
  schedules: "schedules";
225
+ widgets: "widgets";
224
226
  workspace: "workspace";
225
227
  }>>>;
226
228
  mcpServers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -48,6 +48,7 @@ export const OTTO_TOOL_GROUPS = [
48
48
  "terminals",
49
49
  "schedules",
50
50
  "artifacts",
51
+ "widgets",
51
52
  "workspace",
52
53
  ];
53
54
  /**
@@ -68,6 +69,8 @@ export function ottoToolGroupForName(name) {
68
69
  return "schedules";
69
70
  if (name.includes("artifact"))
70
71
  return "artifacts";
72
+ if (name === "show_widget" || name === "widget_contract")
73
+ return "widgets";
71
74
  if (name.includes("worktree") || name.includes("workspace"))
72
75
  return "workspace";
73
76
  return "agents";