@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.
- package/dist/agent-labels.d.ts +26 -0
- package/dist/agent-labels.js +82 -0
- package/dist/agent-types.d.ts +55 -0
- package/dist/agent-types.js +14 -0
- package/dist/browser-automation/capabilities.d.ts +1 -1
- package/dist/browser-automation/rpc-schemas.d.ts +1 -1
- package/dist/generated/validation/ws-outbound.aot.js +39721 -31424
- package/dist/git-hosting.d.ts +2 -0
- package/dist/git-hosting.js +6 -0
- package/dist/messages.d.ts +10543 -1185
- package/dist/messages.js +2013 -8
- package/dist/model-tiers.js +2 -0
- package/dist/orchestration.d.ts +271 -1
- package/dist/orchestration.js +434 -0
- package/dist/provider-config.d.ts +3 -1
- package/dist/provider-config.js +3 -0
- package/dist/validation/ws-outbound-schema-metadata.d.ts +1852 -216
- package/dist/widgets/bridge.d.ts +59 -0
- package/dist/widgets/bridge.js +87 -0
- package/dist/widgets/document.d.ts +18 -0
- package/dist/widgets/document.js +261 -0
- package/dist/widgets/icons.d.ts +20 -0
- package/dist/widgets/icons.js +90 -0
- package/dist/widgets/theme.d.ts +37 -0
- package/dist/widgets/theme.js +149 -0
- package/dist/widgets/types.d.ts +78 -0
- package/dist/widgets/types.js +88 -0
- package/package.json +1 -1
package/dist/orchestration.js
CHANGED
|
@@ -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<{
|
package/dist/provider-config.js
CHANGED
|
@@ -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";
|