@otto-code/protocol 0.6.6 → 0.7.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.
@@ -59,6 +59,7 @@ export const RUN_PHASE_STATUSES = [
59
59
  "skipped",
60
60
  ];
61
61
  export const RUN_STATUSES = [
62
+ "draft", // a user orchestration created by the dialog, graph not yet executed
62
63
  "pending",
63
64
  "running",
64
65
  "paused", // stopped at an attended gate, awaiting the user
@@ -140,6 +141,9 @@ export const RunPhaseCandidateSchema = z
140
141
  // The candidate's final message (synthesis input); may be large — clients
141
142
  // truncate for display.
142
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(),
143
147
  })
144
148
  .passthrough();
145
149
  export const RunPhaseSchema = z
@@ -158,15 +162,46 @@ export const RunPhaseSchema = z
158
162
  candidates: z.array(RunPhaseCandidateSchema).optional(),
159
163
  // Free-text runtime notes (why it blocked, which cap tripped, gap named).
160
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(),
161
175
  startedAt: z.string().optional(),
162
176
  completedAt: z.string().optional(),
163
177
  })
164
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
+ ];
165
188
  export const RunSchema = z
166
189
  .object({
167
190
  id: z.string().min(1),
168
191
  title: z.string().min(1),
192
+ // User-authored description from the New Orchestration dialog (what this
193
+ // orchestration is for). Distinct from `summary`, which is AI-generated
194
+ // after the run settles. Absent on conductor-declared (start_run) runs.
195
+ description: z.string().optional(),
169
196
  status: z.string().min(1),
197
+ // Which engine drives this orchestration: absent/"phases" = the conductor
198
+ // -declared phase plan; "graph" = a user-authored deterministic graph
199
+ // (projects/orchestration-graphs). Open vocabulary, plain string on the wire.
200
+ kind: z.string().optional(),
201
+ // Graph runs only: the executed graph template and the fill-in values the
202
+ // user supplied for its declared inputs.
203
+ graphId: z.string().optional(),
204
+ graphInputs: z.record(z.string(), z.string()).optional(),
170
205
  // Immutable requirements block (see RunPlan.requirements).
171
206
  requirements: z.array(z.string().min(1)).optional(),
172
207
  autopilot: z.boolean().optional(),
@@ -196,4 +231,403 @@ export const RunSchema = z
196
231
  .passthrough();
197
232
  // Summary generation lifecycle (plain-string on the wire; see RunSchema.summaryStatus).
198
233
  export const RUN_SUMMARY_STATUSES = ["pending", "ready", "failed"];
234
+ // ── Orchestration graphs (user orchestrations) ──────────────────────────────
235
+ // The reusable template a User orchestration executes — authored in the graph
236
+ // designer, stored host-level, parameterized by declared inputs. Executing a
237
+ // graph starts an orchestration (a Run with kind "graph"). See
238
+ // projects/orchestration-graphs. Same wire-forward-compat posture as the Run
239
+ // schemas: open string vocabularies, `.passthrough()` objects, no transforms.
240
+ // A declared fill-in parameter. The New Orchestration dialog renders these as
241
+ // a form when the graph is picked; values substitute into node prompts via
242
+ // {{inputs.<key>}} and via GraphNode.promptFromInput.
243
+ export const GraphInputSchema = z
244
+ .object({
245
+ key: z.string().min(1),
246
+ label: z.string().min(1),
247
+ description: z.string().optional(),
248
+ multiline: z.boolean().optional(),
249
+ required: z.boolean().optional(),
250
+ defaultValue: z.string().optional(),
251
+ })
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();
285
+ // Node kinds (open vocabulary): "orchestrator" — the single root that hosts
286
+ // the orchestration chat and anchors the Visualizer; "agent" — a worker node.
287
+ export const GRAPH_NODE_KINDS = ["orchestrator", "agent"];
288
+ // Loop annotation — exactly one of `times` (fixed repeat) or `until` (bounded
289
+ // retry graded by a structured judge between iterations; self-grading is not
290
+ // an exit test). `max` is a hard cap in both readings.
291
+ export const GraphNodeLoopSchema = z
292
+ .object({
293
+ times: z.number().int().min(1).max(64).optional(),
294
+ until: z
295
+ .object({
296
+ // What the judge grades each iteration's output against.
297
+ criteria: z.array(z.string().min(1)).min(1),
298
+ // Role that fills the judge seat; defaults to "judger".
299
+ judgeRole: z.string().min(1).optional(),
300
+ max: z.number().int().min(1).max(16),
301
+ })
302
+ .passthrough()
303
+ .optional(),
304
+ })
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();
369
+ export const GraphNodeSchema = z
370
+ .object({
371
+ id: z.string().min(1),
372
+ kind: z.string().min(1),
373
+ title: z.string().min(1),
374
+ // Team role the node dispatches to (agent nodes). Resolution at execute
375
+ // time: active team fills the role → dialog personality → bare model.
376
+ role: z.string().min(1).optional(),
377
+ // Fixed prompt text; may reference {{inputs.<key>}}.
378
+ prompt: z.string().optional(),
379
+ // Key of a declared input whose value joins (or forms) the prompt.
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(),
385
+ // Leaf-only: the node may orchestrate its own agents (full otto toolset
386
+ // minus start_run). Non-autonomous nodes get no orchestration tools at all.
387
+ autonomous: z.boolean().optional(),
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(),
422
+ // Explicit model override (otherwise the resolved personality/team decides).
423
+ model: z.string().optional(),
424
+ // Designer canvas layout.
425
+ position: z.object({ x: z.number(), y: z.number() }).passthrough().optional(),
426
+ })
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();
435
+ // A directed edge: `from`'s final output becomes labeled input material for
436
+ // `to`. Fan-in is an all-inputs barrier held by the daemon — agents never know
437
+ // about waiting.
438
+ export const GraphEdgeSchema = z
439
+ .object({
440
+ id: z.string().min(1).optional(),
441
+ from: z.string().min(1),
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(),
467
+ })
468
+ .passthrough();
469
+ export const OrchestrationGraphSchema = z
470
+ .object({
471
+ id: z.string().min(1),
472
+ name: z.string().min(1),
473
+ description: z.string().optional(),
474
+ inputs: z.array(GraphInputSchema).optional(),
475
+ nodes: z.array(GraphNodeSchema),
476
+ edges: z.array(GraphEdgeSchema).optional(),
477
+ // Bundled starter graphs; copy-on-edit, never deleted in place.
478
+ builtIn: z.boolean().optional(),
479
+ createdAt: z.string().optional(),
480
+ updatedAt: z.string().optional(),
481
+ })
482
+ .passthrough();
483
+ // ── Graph structural validation ──────────────────────────────────────────────
484
+ // Shared by the daemon (hard gate before execute) and the designer (live
485
+ // feedback). Returns human-readable problems; empty ⇒ executable. Split into
486
+ // per-concern helpers to stay under the complexity ceiling.
487
+ export function validateOrchestrationGraph(graph) {
488
+ const nodeIds = new Set();
489
+ const problems = [];
490
+ for (const node of graph.nodes) {
491
+ if (nodeIds.has(node.id))
492
+ problems.push(`Duplicate node id "${node.id}".`);
493
+ nodeIds.add(node.id);
494
+ }
495
+ const roots = graph.nodes.filter((n) => n.kind === "orchestrator");
496
+ if (roots.length === 0) {
497
+ problems.push("The graph needs exactly one Orchestrator node (the root).");
498
+ }
499
+ else if (roots.length > 1) {
500
+ problems.push("The graph has more than one Orchestrator node.");
501
+ }
502
+ problems.push(...validateGraphEdges(graph, nodeIds));
503
+ const declaredInputs = new Set((graph.inputs ?? []).map((i) => i.key));
504
+ for (const node of graph.nodes) {
505
+ problems.push(...validateGraphNode(node, declaredInputs));
506
+ }
507
+ return problems;
508
+ }
509
+ function validateGraphEdges(graph, nodeIds) {
510
+ const problems = [];
511
+ const edges = graph.edges ?? [];
512
+ const rootId = graph.nodes.find((node) => node.kind === "orchestrator")?.id ?? null;
513
+ const outgoing = new Map();
514
+ const incoming = new Map();
515
+ for (const edge of edges) {
516
+ if (!nodeIds.has(edge.from))
517
+ problems.push(`Edge from unknown node "${edge.from}".`);
518
+ if (!nodeIds.has(edge.to))
519
+ problems.push(`Edge to unknown node "${edge.to}".`);
520
+ if (edge.from === edge.to)
521
+ problems.push(`Node "${edge.from}" connects to itself.`);
522
+ // Edges INTO the orchestrator are passive answer-delivery, not execution
523
+ // dependencies — excluding them here keeps "root kicks off A, A delivers
524
+ // back to root" from reading as a cycle.
525
+ if (edge.to === rootId)
526
+ continue;
527
+ outgoing.set(edge.from, [...(outgoing.get(edge.from) ?? []), edge.to]);
528
+ incoming.set(edge.to, [...(incoming.get(edge.to) ?? []), edge.from]);
529
+ }
530
+ if (hasGraphCycle(graph, outgoing, incoming)) {
531
+ problems.push("The graph contains a cycle.");
532
+ }
533
+ return problems;
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
+ }
557
+ // Cycle check (loops are node-level annotations, never cyclic edges): Kahn.
558
+ function hasGraphCycle(graph, outgoing, incoming) {
559
+ const indegree = new Map();
560
+ for (const node of graph.nodes)
561
+ indegree.set(node.id, incoming.get(node.id)?.length ?? 0);
562
+ const queue = graph.nodes.filter((n) => (indegree.get(n.id) ?? 0) === 0).map((n) => n.id);
563
+ let visited = 0;
564
+ while (queue.length > 0) {
565
+ const id = queue.shift();
566
+ visited += 1;
567
+ for (const next of outgoing.get(id) ?? []) {
568
+ const d = (indegree.get(next) ?? 0) - 1;
569
+ indegree.set(next, d);
570
+ if (d === 0)
571
+ queue.push(next);
572
+ }
573
+ }
574
+ return visited !== graph.nodes.length;
575
+ }
576
+ const GRAPH_INPUT_REF = /\{\{\s*inputs\.([A-Za-z0-9_-]+)\s*\}\}/g;
577
+ function validateGraphNode(node, declaredInputs) {
578
+ const isRoot = node.kind === "orchestrator";
579
+ if (!isRoot && node.kind !== "agent")
580
+ return []; // unknown kinds pass through
581
+ const problems = [];
582
+ // Autonomous nodes may feed results onward via edges; what they must not
583
+ // do is orchestrate deterministic children — which edges don't express, so
584
+ // autonomy is allowed on any node except the root.
585
+ if (node.autonomous && isRoot) {
586
+ problems.push("The Orchestrator node can't be autonomous.");
587
+ }
588
+ if (!isRoot && !node.prompt?.trim() && !node.promptFromInput) {
589
+ problems.push(`Node "${node.title}" has no prompt and no prompt input.`);
590
+ }
591
+ if (node.promptFromInput && !declaredInputs.has(node.promptFromInput)) {
592
+ problems.push(`Node "${node.title}" reads input "${node.promptFromInput}", which isn't declared.`);
593
+ }
594
+ for (const match of (node.prompt ?? "").matchAll(GRAPH_INPUT_REF)) {
595
+ if (!declaredInputs.has(match[1])) {
596
+ problems.push(`Node "${node.title}" references {{inputs.${match[1]}}}, which isn't declared.`);
597
+ }
598
+ }
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
+ }
619
+ return problems;
620
+ }
621
+ function validateGraphNodeLoop(node) {
622
+ if (!node.loop) {
623
+ return [];
624
+ }
625
+ if (node.loop.times === undefined && node.loop.until === undefined) {
626
+ return [`Node "${node.title}" has a loop with neither "times" nor "until".`];
627
+ }
628
+ if (node.loop.times !== undefined && node.loop.until !== undefined) {
629
+ return [`Node "${node.title}" has both loop forms — pick "times" or "until".`];
630
+ }
631
+ return [];
632
+ }
199
633
  //# sourceMappingURL=orchestration.js.map
@@ -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";