@otto-code/protocol 0.8.19 → 0.9.2
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 +22 -0
- package/dist/agent-labels.js +42 -1
- package/dist/agent-profiles.d.ts +1 -1
- package/dist/agent-profiles.js +3 -3
- package/dist/agent-rate-limit.d.ts +13 -0
- package/dist/agent-rate-limit.js +18 -0
- package/dist/agent-types.d.ts +6 -0
- package/dist/architectural-views/rpc-schemas.d.ts +465 -0
- package/dist/architectural-views/rpc-schemas.js +205 -0
- package/dist/artifacts/rpc-schemas.d.ts +360 -2
- package/dist/artifacts/rpc-schemas.js +75 -1
- package/dist/artifacts/types.d.ts +71 -0
- package/dist/artifacts/types.js +27 -0
- package/dist/binary-frames/terminal.d.ts +1 -1
- package/dist/brain.d.ts +3 -3
- package/dist/browser-automation/capabilities.d.ts +1 -1
- package/dist/browser-automation/rpc-schemas.d.ts +16 -16
- package/dist/code-intelligence.d.ts +5 -5
- package/dist/daemon-config.d.ts +66 -0
- package/dist/daemon-config.js +76 -3
- package/dist/file-operations.d.ts +1 -1
- package/dist/generated/validation/ws-outbound.aot.js +81113 -69135
- package/dist/loop/rpc-schemas.d.ts +6 -6
- package/dist/messages.d.ts +11477 -1097
- package/dist/messages.js +633 -117
- package/dist/model-preferences.d.ts +26 -0
- package/dist/model-preferences.js +47 -0
- package/dist/model-tiers.js +2 -0
- package/dist/personality-schemas.d.ts +4 -4
- package/dist/project-knowledge.d.ts +38 -16
- package/dist/project-knowledge.js +17 -1
- package/dist/provider-config.js +1 -1
- package/dist/provider-icon-names.js +1 -0
- package/dist/schedule/rpc-schemas.d.ts +92 -12
- package/dist/schedule/rpc-schemas.js +2 -0
- package/dist/schedule/types.d.ts +47 -3
- package/dist/schedule/types.js +22 -0
- package/dist/search/text-match.d.ts +16 -0
- package/dist/search/text-match.js +31 -2
- package/dist/suggested-tasks.d.ts +7 -3
- package/dist/suggested-tasks.js +2 -0
- package/dist/validation/ws-outbound-schema-metadata.d.ts +2348 -111
- package/dist/websocket-control.d.ts +39 -0
- package/dist/websocket-control.js +36 -0
- package/dist/workflow.d.ts +3060 -0
- package/dist/{orchestration.js → workflow.js} +461 -29
- package/dist/workspace-labels.d.ts +9 -0
- package/dist/workspace-labels.js +19 -0
- package/package.json +1 -1
- package/dist/orchestration.d.ts +0 -1148
|
@@ -5,7 +5,7 @@ import { JudgeVerdictSchema } from "./judge-verdict.js";
|
|
|
5
5
|
// See projects/agent-orchestration/agent-orchestration.md. This is Otto's
|
|
6
6
|
// provider-agnostic answer to a harness "Workflow": the conductor (an
|
|
7
7
|
// orchestrator-role agent) DECLARES the shape (typed phases, assignments, the
|
|
8
|
-
// loop target) via `
|
|
8
|
+
// loop target) via `start_workflow`, and the daemon runtime drives control flow -
|
|
9
9
|
// fan-out, gather-barrier, gate, loop - in code, so orchestrating is cheaper
|
|
10
10
|
// than hand-tracking N agent ids across async notifications.
|
|
11
11
|
//
|
|
@@ -57,6 +57,7 @@ export const RUN_PHASE_STATUSES = [
|
|
|
57
57
|
"done",
|
|
58
58
|
"failed",
|
|
59
59
|
"skipped",
|
|
60
|
+
"canceled", // stopped by the user (run cancel or gate rejection), not by an error
|
|
60
61
|
];
|
|
61
62
|
export const RUN_STATUSES = [
|
|
62
63
|
"draft", // a user orchestration created by the dialog, graph not yet executed
|
|
@@ -81,9 +82,9 @@ export function isTerminalRunStatus(value) {
|
|
|
81
82
|
}
|
|
82
83
|
/** Terminal phase statuses - the phase will not change again on its own. */
|
|
83
84
|
export function isTerminalPhaseStatus(value) {
|
|
84
|
-
return value === "done" || value === "failed" || value === "skipped";
|
|
85
|
+
return value === "done" || value === "failed" || value === "skipped" || value === "canceled";
|
|
85
86
|
}
|
|
86
|
-
// ── Declaration schema (the `
|
|
87
|
+
// ── Declaration schema (the `start_workflow` input) ─────────────────────────
|
|
87
88
|
// What the conductor DECLARES. Kept minimal and schema-validated so a bad plan
|
|
88
89
|
// is rejected at the tool boundary. `role` overrides the phase-type default;
|
|
89
90
|
// `fanOut` spawns N parallel candidates; `judge` attaches a verify sub-step so a
|
|
@@ -129,6 +130,21 @@ export const RunPlanSchema = z
|
|
|
129
130
|
phases: z.array(RunPhaseDeclarationSchema).min(1).max(64),
|
|
130
131
|
})
|
|
131
132
|
.passthrough();
|
|
133
|
+
/**
|
|
134
|
+
* A Workflow with this many known agents needs an explicit user confirmation
|
|
135
|
+
* before it starts. This is an agent-count boundary, not a price estimate:
|
|
136
|
+
* providers do not expose one reliable comparable cost signal.
|
|
137
|
+
*/
|
|
138
|
+
export const WORKFLOW_START_CONFIRMATION_AGENT_THRESHOLD = 4;
|
|
139
|
+
/** Count the children a declared AI plan asks the daemon to start initially. */
|
|
140
|
+
export function describeRunPlanStart(plan) {
|
|
141
|
+
const workerPhases = plan.phases.filter((phase) => phase.type !== "gate");
|
|
142
|
+
return {
|
|
143
|
+
plannedAgentCount: workerPhases.reduce((total, phase) => total + (phase.fanOut ?? 1), 0),
|
|
144
|
+
fanOutPhaseCount: workerPhases.filter((phase) => (phase.fanOut ?? 1) > 1).length,
|
|
145
|
+
phaseCount: plan.phases.length,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
132
148
|
// ── Projection schema (the Run the daemon persists + pushes to clients) ─────
|
|
133
149
|
// One spawned candidate for a phase: the observable child agent plus, when the
|
|
134
150
|
// phase judged it, that candidate's verdict.
|
|
@@ -141,6 +157,9 @@ export const RunPhaseCandidateSchema = z
|
|
|
141
157
|
// The candidate's final message (synthesis input); may be large - clients
|
|
142
158
|
// truncate for display.
|
|
143
159
|
summary: z.string().optional(),
|
|
160
|
+
// A durable terminal error when this candidate could not produce a result.
|
|
161
|
+
// Optional so persisted runs from older daemons continue to parse.
|
|
162
|
+
error: z.string().optional(),
|
|
144
163
|
// Validated output fields, when the node declared them (GraphNode.output).
|
|
145
164
|
// Values only - anything large belongs in a file the next node reads.
|
|
146
165
|
outputFields: z.record(z.string(), z.unknown()).optional(),
|
|
@@ -181,26 +200,104 @@ export const RunPhaseSchema = z
|
|
|
181
200
|
// mean an upstream node was itself skipped or failed. Plain string on the wire.
|
|
182
201
|
export const GRAPH_SKIP_REASONS = [
|
|
183
202
|
"condition",
|
|
203
|
+
"port",
|
|
184
204
|
"upstream-skipped",
|
|
185
205
|
"upstream-failed",
|
|
186
206
|
"canceled",
|
|
187
207
|
];
|
|
188
|
-
|
|
208
|
+
// The exact Graph document captured when a Graph Run is drafted or started.
|
|
209
|
+
// This must remain declared before RunSchema: the protocol's generated
|
|
210
|
+
// validators load schemas eagerly. Graph documents evolve independently, so
|
|
211
|
+
// their nested fields stay open here; the graph was already validated against
|
|
212
|
+
// OrchestrationGraphSchema before the daemon persisted this snapshot.
|
|
213
|
+
export const RunGraphSnapshotSchema = z
|
|
214
|
+
.object({
|
|
215
|
+
id: z.string().min(1),
|
|
216
|
+
name: z.string().min(1),
|
|
217
|
+
description: z.string().optional(),
|
|
218
|
+
inputs: z.array(z.unknown()).optional(),
|
|
219
|
+
nodes: z.array(z.unknown()),
|
|
220
|
+
edges: z.array(z.unknown()).optional(),
|
|
221
|
+
builtIn: z.boolean().optional(),
|
|
222
|
+
createdAt: z.string().optional(),
|
|
223
|
+
updatedAt: z.string().optional(),
|
|
224
|
+
})
|
|
225
|
+
.passthrough();
|
|
226
|
+
// New Workflow records carry this only when created through the category store.
|
|
227
|
+
// Optional means legacy daemon-global records remain readable and visible.
|
|
228
|
+
// COMPAT(categoryStorageResolver): added in v0.9.0, remove after 2027-02-28.
|
|
229
|
+
export const WorkflowStorageProvenanceSchema = z
|
|
230
|
+
.object({
|
|
231
|
+
schemaVersion: z.number().int().min(1),
|
|
232
|
+
projectRoot: z.string().min(1).optional(),
|
|
233
|
+
projectId: z.string().min(1).optional(),
|
|
234
|
+
projectKey: z.string().min(1).optional(),
|
|
235
|
+
location: z.enum(["repository", "host"]),
|
|
236
|
+
storeKey: z.string().min(1),
|
|
237
|
+
hostId: z.string().min(1).optional(),
|
|
238
|
+
hostName: z.string().min(1).optional(),
|
|
239
|
+
source: z.enum(["project-store", "legacy-host-library"]),
|
|
240
|
+
})
|
|
241
|
+
.passthrough();
|
|
242
|
+
/** Selects only the destination for future project-owned Workflow writes. */
|
|
243
|
+
export const ProjectWorkflowStoreSetRequestSchema = z.object({
|
|
244
|
+
type: z.literal("project.workflow.store.set.request"),
|
|
245
|
+
projectId: z.string(),
|
|
246
|
+
// Null inherits the independent host-wide Workflow default.
|
|
247
|
+
location: z.enum(["repository", "host"]).nullable(),
|
|
248
|
+
requestId: z.string(),
|
|
249
|
+
});
|
|
250
|
+
export const ProjectWorkflowStoreSetResponseSchema = z.object({
|
|
251
|
+
type: z.literal("project.workflow.store.set.response"),
|
|
252
|
+
payload: z.object({
|
|
253
|
+
requestId: z.string(),
|
|
254
|
+
projectId: z.string(),
|
|
255
|
+
accepted: z.boolean(),
|
|
256
|
+
error: z.string().nullable(),
|
|
257
|
+
}),
|
|
258
|
+
});
|
|
259
|
+
/** The durable Schedule fire that launched this Workflow, when applicable. */
|
|
260
|
+
export const WorkflowScheduleSourceSchema = z
|
|
261
|
+
.object({
|
|
262
|
+
scheduleId: z.string().min(1),
|
|
263
|
+
scheduleRunId: z.string().min(1),
|
|
264
|
+
})
|
|
265
|
+
.passthrough();
|
|
266
|
+
/**
|
|
267
|
+
* A daemon-owned start boundary, separate from an ordinary declared gate.
|
|
268
|
+
* `model-plan-declared` pauses before any child agent starts; `agent-threshold`
|
|
269
|
+
* is used by the Graph start review. The client presents the known shape and
|
|
270
|
+
* sends an explicit decision back to the daemon.
|
|
271
|
+
*/
|
|
272
|
+
export const WorkflowStartConfirmationSchema = z
|
|
273
|
+
.object({
|
|
274
|
+
reason: z.string().min(1),
|
|
275
|
+
plannedAgentCount: z.number().int().min(0),
|
|
276
|
+
fanOutPhaseCount: z.number().int().min(0),
|
|
277
|
+
phaseCount: z.number().int().min(0),
|
|
278
|
+
agentCap: z.number().int().min(1),
|
|
279
|
+
threshold: z.number().int().min(1),
|
|
280
|
+
})
|
|
281
|
+
.passthrough();
|
|
282
|
+
export const WorkflowSchema = z
|
|
189
283
|
.object({
|
|
190
284
|
id: z.string().min(1),
|
|
191
285
|
title: z.string().min(1),
|
|
192
286
|
// User-authored description from the New Orchestration dialog (what this
|
|
193
287
|
// orchestration is for). Distinct from `summary`, which is AI-generated
|
|
194
|
-
// after the run settles. Absent on conductor-declared (
|
|
288
|
+
// after the run settles. Absent on conductor-declared (start_workflow) runs.
|
|
195
289
|
description: z.string().optional(),
|
|
196
290
|
status: z.string().min(1),
|
|
197
291
|
// Which engine drives this orchestration: absent/"phases" = the conductor
|
|
198
292
|
// -declared phase plan; "graph" = a user-authored deterministic graph
|
|
199
293
|
// (projects/orchestration-graphs). Open vocabulary, plain string on the wire.
|
|
200
294
|
kind: z.string().optional(),
|
|
201
|
-
// Graph runs only: the
|
|
202
|
-
// user supplied for
|
|
295
|
+
// Graph runs only: the graph template id, its exact source document, and
|
|
296
|
+
// the fill-in values the user supplied for inputs. A draft may be re-saved;
|
|
297
|
+
// an execution keeps this source document as immutable history. Optional so
|
|
298
|
+
// older persisted runs and clients continue to parse.
|
|
203
299
|
graphId: z.string().optional(),
|
|
300
|
+
graphSnapshot: RunGraphSnapshotSchema.optional(),
|
|
204
301
|
graphInputs: z.record(z.string(), z.string()).optional(),
|
|
205
302
|
// Immutable requirements block (see RunPlan.requirements).
|
|
206
303
|
requirements: z.array(z.string().min(1)).optional(),
|
|
@@ -222,6 +319,14 @@ export const RunSchema = z
|
|
|
222
319
|
// or runs without the run-summary feature.
|
|
223
320
|
summary: z.string().optional(),
|
|
224
321
|
summaryStatus: z.string().optional(),
|
|
322
|
+
// A pending cost/agent confirmation before an AI-declared plan starts.
|
|
323
|
+
// This is not a Graph/phase gate and never changes the plan's autopilot or
|
|
324
|
+
// permission mode.
|
|
325
|
+
startConfirmation: WorkflowStartConfirmationSchema.optional(),
|
|
326
|
+
workflowStorage: WorkflowStorageProvenanceSchema.optional(),
|
|
327
|
+
// A Schedule may start a saved definition, but it must not erase the
|
|
328
|
+
// source identity that explains why this durable run exists.
|
|
329
|
+
scheduleSource: WorkflowScheduleSourceSchema.optional(),
|
|
225
330
|
// Total child agents this run spawned (makers + judgers) - a complexity
|
|
226
331
|
// signal surfaced in the Runs display. Grows as the run executes.
|
|
227
332
|
agentCount: z.number().int().min(0).optional(),
|
|
@@ -229,6 +334,9 @@ export const RunSchema = z
|
|
|
229
334
|
updatedAt: z.string().optional(),
|
|
230
335
|
})
|
|
231
336
|
.passthrough();
|
|
337
|
+
// COMPAT(runDomainType): renamed to Workflow in v0.9.0; remove after
|
|
338
|
+
// 2027-02-28 once downstream extensions have moved to the Workflow API.
|
|
339
|
+
export const RunSchema = WorkflowSchema;
|
|
232
340
|
// Summary generation lifecycle (plain-string on the wire; see RunSchema.summaryStatus).
|
|
233
341
|
export const RUN_SUMMARY_STATUSES = ["pending", "ready", "failed"];
|
|
234
342
|
// ── Orchestration graphs (user orchestrations) ──────────────────────────────
|
|
@@ -271,6 +379,7 @@ export const PromptTemplateSchema = z
|
|
|
271
379
|
builtIn: z.boolean().optional(),
|
|
272
380
|
createdAt: z.string().optional(),
|
|
273
381
|
updatedAt: z.string().optional(),
|
|
382
|
+
workflowStorage: WorkflowStorageProvenanceSchema.optional(),
|
|
274
383
|
})
|
|
275
384
|
.passthrough();
|
|
276
385
|
// A node's binding to a stored template. A value is a literal, `$inputs.<key>`
|
|
@@ -283,8 +392,10 @@ export const NodePromptTemplateRefSchema = z
|
|
|
283
392
|
})
|
|
284
393
|
.passthrough();
|
|
285
394
|
// Node kinds (open vocabulary): "orchestrator" - the single root that hosts
|
|
286
|
-
// the orchestration chat and anchors the Visualizer; "agent" - a worker node
|
|
287
|
-
|
|
395
|
+
// the orchestration chat and anchors the Visualizer; "agent" - a worker node;
|
|
396
|
+
// "gate" - an attended human approval boundary; "check" - a deterministic
|
|
397
|
+
// JSONata assertion over upstream output. Gates and checks make no model call.
|
|
398
|
+
export const GRAPH_NODE_KINDS = ["orchestrator", "agent", "gate", "check"];
|
|
288
399
|
// Loop annotation - exactly one of `times` (fixed repeat) or `until` (bounded
|
|
289
400
|
// retry graded by a structured judge between iterations; self-grading is not
|
|
290
401
|
// an exit test). `max` is a hard cap in both readings.
|
|
@@ -366,6 +477,22 @@ export const GraphNodeRetrySchema = z
|
|
|
366
477
|
multiplier: z.number().min(1).max(4).optional(),
|
|
367
478
|
})
|
|
368
479
|
.passthrough();
|
|
480
|
+
// A deterministic assertion over the named upstream material that reached a
|
|
481
|
+
// Check node. JSONata keeps user-authored graph data out of JavaScript `eval`.
|
|
482
|
+
// `message` is the actionable failure text shown on the durable Run.
|
|
483
|
+
export const GraphNodeCheckSchema = z
|
|
484
|
+
.object({
|
|
485
|
+
expression: z.string().min(1),
|
|
486
|
+
message: z.string().min(1).optional(),
|
|
487
|
+
})
|
|
488
|
+
.passthrough();
|
|
489
|
+
/**
|
|
490
|
+
* A Check settles on exactly one named control-flow output. These are open
|
|
491
|
+
* wire strings on GraphEdge so a newer client can still parse on an older
|
|
492
|
+
* daemon, but the shared validator rejects an unsupported Check port before a
|
|
493
|
+
* run begins.
|
|
494
|
+
*/
|
|
495
|
+
export const GRAPH_CHECK_OUTPUT_PORTS = ["pass", "fail"];
|
|
369
496
|
export const GraphNodeSchema = z
|
|
370
497
|
.object({
|
|
371
498
|
id: z.string().min(1),
|
|
@@ -415,6 +542,9 @@ export const GraphNodeSchema = z
|
|
|
415
542
|
// node including its loop, and every attempt is charged to the run's agent
|
|
416
543
|
// cap - a retry is never a private allowance.
|
|
417
544
|
retry: GraphNodeRetrySchema.optional(),
|
|
545
|
+
// Check nodes only: a deterministic pass/fail assertion over the named
|
|
546
|
+
// upstream output material. It never dispatches an agent.
|
|
547
|
+
check: GraphNodeCheckSchema.optional(),
|
|
418
548
|
// Wall-clock ceiling for one attempt of this node. On expiry the agent is
|
|
419
549
|
// really cancelled (not merely stopped being awaited) and the node fails,
|
|
420
550
|
// which its retry policy may then catch.
|
|
@@ -466,8 +596,22 @@ export const GraphEdgeSchema = z
|
|
|
466
596
|
label: z.string().optional(),
|
|
467
597
|
})
|
|
468
598
|
.passthrough();
|
|
599
|
+
// ── Graph document compatibility ────────────────────────────────────────────
|
|
600
|
+
//
|
|
601
|
+
// Graphs were originally daemon-local records, so their persisted shape has no
|
|
602
|
+
// document version. Keep that legacy shape executable, but give caller-supplied
|
|
603
|
+
// documents a stable compatibility boundary before import/export exists. The
|
|
604
|
+
// schema remains additive and parser-safe; version interpretation happens here,
|
|
605
|
+
// after parsing, rather than in a wire-schema transform.
|
|
606
|
+
export const GRAPH_DOCUMENT_FORMAT = "otto.workflow.graph";
|
|
607
|
+
export const GRAPH_DOCUMENT_FORMAT_VERSION = 1;
|
|
469
608
|
export const OrchestrationGraphSchema = z
|
|
470
609
|
.object({
|
|
610
|
+
// A missing format/version is a legacy daemon-local Graph. New portable
|
|
611
|
+
// documents write both fields; their semantics are checked explicitly by
|
|
612
|
+
// `validateGraphDocument` below.
|
|
613
|
+
format: z.string().min(1).optional(),
|
|
614
|
+
formatVersion: z.number().int().min(1).optional(),
|
|
471
615
|
id: z.string().min(1),
|
|
472
616
|
name: z.string().min(1),
|
|
473
617
|
description: z.string().optional(),
|
|
@@ -476,10 +620,107 @@ export const OrchestrationGraphSchema = z
|
|
|
476
620
|
edges: z.array(GraphEdgeSchema).optional(),
|
|
477
621
|
// Bundled starter graphs; copy-on-edit, never deleted in place.
|
|
478
622
|
builtIn: z.boolean().optional(),
|
|
623
|
+
// Capability declarations are reserved for portable documents. They are
|
|
624
|
+
// intentionally open strings so a newer exporter still parses on an older
|
|
625
|
+
// peer; execution compatibility is a daemon-side preflight concern.
|
|
626
|
+
requires: z.array(z.string().min(1)).optional(),
|
|
479
627
|
createdAt: z.string().optional(),
|
|
480
628
|
updatedAt: z.string().optional(),
|
|
629
|
+
workflowStorage: WorkflowStorageProvenanceSchema.optional(),
|
|
630
|
+
})
|
|
631
|
+
.passthrough();
|
|
632
|
+
// A portable Graph package is intentionally data-only. Its source descriptor is
|
|
633
|
+
// display/audit provenance supplied by the exporter, never an authority grant:
|
|
634
|
+
// the destination daemon independently validates the Graph and only persists it
|
|
635
|
+
// after the caller confirms the review response.
|
|
636
|
+
export const WorkflowGraphShareLocationSchema = z
|
|
637
|
+
.object({
|
|
638
|
+
storeKey: z.string().min(1),
|
|
639
|
+
location: z.enum(["repository", "host"]),
|
|
640
|
+
hostName: z.string().min(1).optional(),
|
|
641
|
+
source: z.enum(["project-store", "legacy-host-library"]),
|
|
642
|
+
})
|
|
643
|
+
.passthrough();
|
|
644
|
+
export const WorkflowGraphExportSchema = z
|
|
645
|
+
.object({
|
|
646
|
+
schemaVersion: z.literal(1),
|
|
647
|
+
graph: OrchestrationGraphSchema,
|
|
648
|
+
source: WorkflowGraphShareLocationSchema,
|
|
649
|
+
exportedAt: z.string().min(1),
|
|
650
|
+
contentHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
481
651
|
})
|
|
482
652
|
.passthrough();
|
|
653
|
+
export const WorkflowGraphImportResultSchema = z
|
|
654
|
+
.object({
|
|
655
|
+
status: z.enum(["review_required", "imported", "failed"]),
|
|
656
|
+
graph: OrchestrationGraphSchema.optional(),
|
|
657
|
+
source: WorkflowGraphShareLocationSchema.optional(),
|
|
658
|
+
destination: WorkflowGraphShareLocationSchema.optional(),
|
|
659
|
+
contentHash: z
|
|
660
|
+
.string()
|
|
661
|
+
.regex(/^[a-f0-9]{64}$/)
|
|
662
|
+
.optional(),
|
|
663
|
+
remediation: z.string().min(1),
|
|
664
|
+
})
|
|
665
|
+
.passthrough();
|
|
666
|
+
/**
|
|
667
|
+
* Validate the portable-document wrapper without changing a Graph's content.
|
|
668
|
+
* A caller can use this in a local, read-only validation path; it never
|
|
669
|
+
* resolves templates, evaluates expressions, or consults a daemon.
|
|
670
|
+
*/
|
|
671
|
+
export function validateGraphDocument(graph) {
|
|
672
|
+
const diagnostics = [];
|
|
673
|
+
// `passthrough()` preserved arbitrary fields before this contract existed.
|
|
674
|
+
// Treat a pre-existing unrelated `format` key as legacy unless its companion
|
|
675
|
+
// version, or our exact portable format marker, says it is a document header.
|
|
676
|
+
const declaresPortableFormat = graph.formatVersion !== undefined || graph.format === GRAPH_DOCUMENT_FORMAT;
|
|
677
|
+
if (!declaresPortableFormat) {
|
|
678
|
+
diagnostics.push({
|
|
679
|
+
code: "GRAPH_DOCUMENT_LEGACY_UNVERSIONED",
|
|
680
|
+
severity: "warning",
|
|
681
|
+
path: "",
|
|
682
|
+
message: "This Graph has no portable document format version.",
|
|
683
|
+
recovery: `Export it as ${GRAPH_DOCUMENT_FORMAT} v${GRAPH_DOCUMENT_FORMAT_VERSION} before sharing it.`,
|
|
684
|
+
});
|
|
685
|
+
return diagnostics;
|
|
686
|
+
}
|
|
687
|
+
if (graph.format !== GRAPH_DOCUMENT_FORMAT) {
|
|
688
|
+
diagnostics.push({
|
|
689
|
+
code: "GRAPH_DOCUMENT_FORMAT_UNSUPPORTED",
|
|
690
|
+
severity: "error",
|
|
691
|
+
path: "/format",
|
|
692
|
+
message: `Graph format "${graph.format ?? "(missing)"}" is not supported.`,
|
|
693
|
+
recovery: `Use format "${GRAPH_DOCUMENT_FORMAT}".`,
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
if (graph.formatVersion === undefined) {
|
|
697
|
+
diagnostics.push({
|
|
698
|
+
code: "GRAPH_DOCUMENT_VERSION_MISSING",
|
|
699
|
+
severity: "error",
|
|
700
|
+
path: "/formatVersion",
|
|
701
|
+
message: "A portable Graph document needs formatVersion.",
|
|
702
|
+
recovery: `Use formatVersion ${GRAPH_DOCUMENT_FORMAT_VERSION}.`,
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
else if (graph.formatVersion > GRAPH_DOCUMENT_FORMAT_VERSION) {
|
|
706
|
+
diagnostics.push({
|
|
707
|
+
code: "GRAPH_DOCUMENT_VERSION_UNSUPPORTED",
|
|
708
|
+
severity: "error",
|
|
709
|
+
path: "/formatVersion",
|
|
710
|
+
message: `Graph document version ${graph.formatVersion} is newer than this Otto host supports.`,
|
|
711
|
+
recovery: "Update Otto, or export the Graph in a supported format version.",
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
return diagnostics;
|
|
715
|
+
}
|
|
716
|
+
// A Graph id becomes a file name in every Graph store (`{id}.json`) and, since
|
|
717
|
+
// Graph packages can be imported from another host, it is untrusted input.
|
|
718
|
+
// Reject anything that is not one plain path segment so an id can never leave
|
|
719
|
+
// its store directory or shadow another store's file.
|
|
720
|
+
const SAFE_GRAPH_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
721
|
+
export function isSafeGraphId(id) {
|
|
722
|
+
return SAFE_GRAPH_ID.test(id) && id !== "." && id !== "..";
|
|
723
|
+
}
|
|
483
724
|
// ── Graph structural validation ──────────────────────────────────────────────
|
|
484
725
|
// Shared by the daemon (hard gate before execute) and the designer (live
|
|
485
726
|
// feedback). Returns human-readable problems; empty ⇒ executable. Split into
|
|
@@ -487,6 +728,14 @@ export const OrchestrationGraphSchema = z
|
|
|
487
728
|
export function validateOrchestrationGraph(graph) {
|
|
488
729
|
const nodeIds = new Set();
|
|
489
730
|
const problems = [];
|
|
731
|
+
if (!isSafeGraphId(graph.id)) {
|
|
732
|
+
problems.push(`Graph id "${graph.id}" must be a single file-name segment (letters, digits, "-", "_", ".").`);
|
|
733
|
+
}
|
|
734
|
+
for (const diagnostic of validateGraphDocument(graph)) {
|
|
735
|
+
if (diagnostic.severity === "error") {
|
|
736
|
+
problems.push(diagnostic.message);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
490
739
|
for (const node of graph.nodes) {
|
|
491
740
|
if (nodeIds.has(node.id))
|
|
492
741
|
problems.push(`Duplicate node id "${node.id}".`);
|
|
@@ -500,7 +749,13 @@ export function validateOrchestrationGraph(graph) {
|
|
|
500
749
|
problems.push("The graph has more than one Orchestrator node.");
|
|
501
750
|
}
|
|
502
751
|
problems.push(...validateGraphEdges(graph, nodeIds));
|
|
503
|
-
const declaredInputs = new Set(
|
|
752
|
+
const declaredInputs = new Set();
|
|
753
|
+
for (const input of graph.inputs ?? []) {
|
|
754
|
+
if (declaredInputs.has(input.key)) {
|
|
755
|
+
problems.push(`Duplicate Graph input key "${input.key}".`);
|
|
756
|
+
}
|
|
757
|
+
declaredInputs.add(input.key);
|
|
758
|
+
}
|
|
504
759
|
for (const node of graph.nodes) {
|
|
505
760
|
problems.push(...validateGraphNode(node, declaredInputs));
|
|
506
761
|
}
|
|
@@ -519,6 +774,12 @@ function validateGraphEdges(graph, nodeIds) {
|
|
|
519
774
|
problems.push(`Edge to unknown node "${edge.to}".`);
|
|
520
775
|
if (edge.from === edge.to)
|
|
521
776
|
problems.push(`Node "${edge.from}" connects to itself.`);
|
|
777
|
+
const source = graph.nodes.find((node) => node.id === edge.from);
|
|
778
|
+
if (source?.kind === "check" &&
|
|
779
|
+
edge.fromPort !== undefined &&
|
|
780
|
+
!GRAPH_CHECK_OUTPUT_PORTS.includes(edge.fromPort)) {
|
|
781
|
+
problems.push(`Check "${source.title}" has edge port "${edge.fromPort}"; use "pass" or "fail".`);
|
|
782
|
+
}
|
|
522
783
|
// Edges INTO the orchestrator are passive answer-delivery, not execution
|
|
523
784
|
// dependencies - excluding them here keeps "root kicks off A, A delivers
|
|
524
785
|
// back to root" from reading as a cycle.
|
|
@@ -576,7 +837,9 @@ function hasGraphCycle(graph, outgoing, incoming) {
|
|
|
576
837
|
const GRAPH_INPUT_REF = /\{\{\s*inputs\.([A-Za-z0-9_-]+)\s*\}\}/g;
|
|
577
838
|
function validateGraphNode(node, declaredInputs) {
|
|
578
839
|
const isRoot = node.kind === "orchestrator";
|
|
579
|
-
|
|
840
|
+
const isGate = node.kind === "gate";
|
|
841
|
+
const isCheck = node.kind === "check";
|
|
842
|
+
if (!isRoot && node.kind !== "agent" && !isGate && !isCheck)
|
|
580
843
|
return []; // unknown kinds pass through
|
|
581
844
|
const problems = [];
|
|
582
845
|
// Autonomous nodes may feed results onward via edges; what they must not
|
|
@@ -585,9 +848,10 @@ function validateGraphNode(node, declaredInputs) {
|
|
|
585
848
|
if (node.autonomous && isRoot) {
|
|
586
849
|
problems.push("The Orchestrator node can't be autonomous.");
|
|
587
850
|
}
|
|
588
|
-
if (!isRoot && !node.prompt?.trim() && !node.promptFromInput) {
|
|
851
|
+
if (!isRoot && !isGate && !isCheck && !node.prompt?.trim() && !node.promptFromInput) {
|
|
589
852
|
problems.push(`Node "${node.title}" has no prompt and no prompt input.`);
|
|
590
853
|
}
|
|
854
|
+
problems.push(...validateGraphNodeCheck(node, isCheck));
|
|
591
855
|
if (node.promptFromInput && !declaredInputs.has(node.promptFromInput)) {
|
|
592
856
|
problems.push(`Node "${node.title}" reads input "${node.promptFromInput}", which isn't declared.`);
|
|
593
857
|
}
|
|
@@ -600,6 +864,11 @@ function validateGraphNode(node, declaredInputs) {
|
|
|
600
864
|
problems.push(...validateGraphNodeOutput(node));
|
|
601
865
|
return problems;
|
|
602
866
|
}
|
|
867
|
+
function validateGraphNodeCheck(node, isCheck) {
|
|
868
|
+
return isCheck && !node.check?.expression.trim()
|
|
869
|
+
? [`Check "${node.title}" needs a JSONata expression.`]
|
|
870
|
+
: [];
|
|
871
|
+
}
|
|
603
872
|
function validateGraphNodeOutput(node) {
|
|
604
873
|
if (!node.output) {
|
|
605
874
|
return [];
|
|
@@ -775,6 +1044,65 @@ export const RunsGraphsChangedNotificationSchema = z.object({
|
|
|
775
1044
|
graphs: z.array(OrchestrationGraphSchema),
|
|
776
1045
|
}),
|
|
777
1046
|
});
|
|
1047
|
+
// Graph sharing is deliberately separate from save/run. Export is explicit;
|
|
1048
|
+
// import first returns a review and needs a second confirmed request before a
|
|
1049
|
+
// destination store is touched. New names use the dotted RPC contract.
|
|
1050
|
+
export const WorkflowsGraphsListRequestSchema = z.object({
|
|
1051
|
+
type: z.literal("workflows.graphs.list.request"),
|
|
1052
|
+
cwd: z.string().min(1),
|
|
1053
|
+
requestId: z.string(),
|
|
1054
|
+
});
|
|
1055
|
+
export const WorkflowsGraphsListResponseSchema = z.object({
|
|
1056
|
+
type: z.literal("workflows.graphs.list.response"),
|
|
1057
|
+
payload: z.object({
|
|
1058
|
+
graphs: z.array(OrchestrationGraphSchema),
|
|
1059
|
+
error: z.string().optional(),
|
|
1060
|
+
requestId: z.string(),
|
|
1061
|
+
}),
|
|
1062
|
+
});
|
|
1063
|
+
/** Project-scoped Graph writes. Legacy runs.graphs.* remains the visible library. */
|
|
1064
|
+
export const WorkflowsGraphSaveRequestSchema = z.object({
|
|
1065
|
+
type: z.literal("workflows.graph.save.request"),
|
|
1066
|
+
cwd: z.string().min(1),
|
|
1067
|
+
graph: OrchestrationGraphSchema,
|
|
1068
|
+
requestId: z.string(),
|
|
1069
|
+
});
|
|
1070
|
+
export const WorkflowsGraphSaveResponseSchema = z.object({
|
|
1071
|
+
type: z.literal("workflows.graph.save.response"),
|
|
1072
|
+
payload: z.object({
|
|
1073
|
+
graph: OrchestrationGraphSchema.optional(),
|
|
1074
|
+
error: z.string().optional(),
|
|
1075
|
+
requestId: z.string(),
|
|
1076
|
+
}),
|
|
1077
|
+
});
|
|
1078
|
+
export const WorkflowsGraphExportRequestSchema = z.object({
|
|
1079
|
+
type: z.literal("workflows.graph.export.request"),
|
|
1080
|
+
graphId: z.string().min(1),
|
|
1081
|
+
requestId: z.string(),
|
|
1082
|
+
});
|
|
1083
|
+
export const WorkflowsGraphExportResponseSchema = z.object({
|
|
1084
|
+
type: z.literal("workflows.graph.export.response"),
|
|
1085
|
+
payload: z.object({
|
|
1086
|
+
export: WorkflowGraphExportSchema.optional(),
|
|
1087
|
+
error: z.string().optional(),
|
|
1088
|
+
requestId: z.string(),
|
|
1089
|
+
}),
|
|
1090
|
+
});
|
|
1091
|
+
export const WorkflowsGraphImportRequestSchema = z.object({
|
|
1092
|
+
type: z.literal("workflows.graph.import.request"),
|
|
1093
|
+
cwd: z.string().min(1),
|
|
1094
|
+
export: WorkflowGraphExportSchema,
|
|
1095
|
+
confirmed: z.boolean(),
|
|
1096
|
+
requestId: z.string(),
|
|
1097
|
+
});
|
|
1098
|
+
export const WorkflowsGraphImportResponseSchema = z.object({
|
|
1099
|
+
type: z.literal("workflows.graph.import.response"),
|
|
1100
|
+
payload: z.object({
|
|
1101
|
+
result: WorkflowGraphImportResultSchema.optional(),
|
|
1102
|
+
error: z.string().optional(),
|
|
1103
|
+
requestId: z.string(),
|
|
1104
|
+
}),
|
|
1105
|
+
});
|
|
778
1106
|
// ── Prompt templates ────────────────────────────────────────────────────────
|
|
779
1107
|
// Host-level reusable prompts and snippets a graph node can bind to. Same shape
|
|
780
1108
|
// as the graph trio above, for the same reason: one store, list/save/delete,
|
|
@@ -822,15 +1150,81 @@ export const RunsTemplatesChangedNotificationSchema = z.object({
|
|
|
822
1150
|
templates: z.array(PromptTemplateSchema),
|
|
823
1151
|
}),
|
|
824
1152
|
});
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1153
|
+
export const WorkflowsTemplatesListRequestSchema = z.object({
|
|
1154
|
+
type: z.literal("workflows.templates.list.request"),
|
|
1155
|
+
cwd: z.string().min(1),
|
|
1156
|
+
requestId: z.string(),
|
|
1157
|
+
});
|
|
1158
|
+
export const WorkflowsTemplatesListResponseSchema = z.object({
|
|
1159
|
+
type: z.literal("workflows.templates.list.response"),
|
|
1160
|
+
payload: z.object({
|
|
1161
|
+
templates: z.array(PromptTemplateSchema),
|
|
1162
|
+
error: z.string().optional(),
|
|
1163
|
+
requestId: z.string(),
|
|
1164
|
+
}),
|
|
1165
|
+
});
|
|
1166
|
+
export const WorkflowsTemplateSaveRequestSchema = z.object({
|
|
1167
|
+
type: z.literal("workflows.template.save.request"),
|
|
1168
|
+
cwd: z.string().min(1),
|
|
1169
|
+
template: PromptTemplateSchema,
|
|
1170
|
+
requestId: z.string(),
|
|
1171
|
+
});
|
|
1172
|
+
export const WorkflowsTemplateSaveResponseSchema = z.object({
|
|
1173
|
+
type: z.literal("workflows.template.save.response"),
|
|
1174
|
+
payload: z.object({
|
|
1175
|
+
template: PromptTemplateSchema.optional(),
|
|
1176
|
+
error: z.string().optional(),
|
|
1177
|
+
requestId: z.string(),
|
|
1178
|
+
}),
|
|
1179
|
+
});
|
|
1180
|
+
// Transfer addresses records by stable id plus the current project scope. The
|
|
1181
|
+
// caller never receives a daemon file path, and a receipt is written before a
|
|
1182
|
+
// destination record so an interrupted attempt stays explainable.
|
|
1183
|
+
export const WorkflowTransferReceiptSchema = z.object({
|
|
1184
|
+
schemaVersion: z.literal(1),
|
|
1185
|
+
receiptId: z.string().min(1),
|
|
1186
|
+
recordKind: z.enum(["graph", "template", "run"]),
|
|
1187
|
+
recordId: z.string().min(1),
|
|
1188
|
+
mode: z.enum(["copy", "move"]),
|
|
1189
|
+
source: z.object({
|
|
1190
|
+
source: z.enum(["legacy-host-library", "repository", "host"]),
|
|
1191
|
+
storeKey: z.string().min(1),
|
|
1192
|
+
}),
|
|
1193
|
+
destination: z.object({
|
|
1194
|
+
location: z.enum(["repository", "host"]),
|
|
1195
|
+
storeKey: z.string().min(1),
|
|
1196
|
+
}),
|
|
1197
|
+
contentHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
1198
|
+
status: z.enum(["prepared", "verified", "moved", "source-retained", "failed"]),
|
|
1199
|
+
createdAt: z.string().min(1),
|
|
1200
|
+
updatedAt: z.string().min(1),
|
|
1201
|
+
recovery: z.string().optional(),
|
|
1202
|
+
});
|
|
1203
|
+
export const WorkflowsStorageTransferRequestSchema = z.object({
|
|
1204
|
+
type: z.literal("workflows.storage.transfer.request"),
|
|
1205
|
+
cwd: z.string().min(1),
|
|
1206
|
+
recordKind: z.enum(["graph", "template", "run"]),
|
|
1207
|
+
recordId: z.string().min(1),
|
|
1208
|
+
source: z.enum(["legacy-host-library", "repository", "host"]),
|
|
1209
|
+
destination: z.enum(["repository", "host"]),
|
|
1210
|
+
mode: z.enum(["copy", "move"]),
|
|
1211
|
+
requestId: z.string(),
|
|
1212
|
+
});
|
|
1213
|
+
export const WorkflowsStorageTransferResponseSchema = z.object({
|
|
1214
|
+
type: z.literal("workflows.storage.transfer.response"),
|
|
1215
|
+
payload: z.object({
|
|
1216
|
+
receipt: WorkflowTransferReceiptSchema.optional(),
|
|
1217
|
+
error: z.string().optional(),
|
|
1218
|
+
requestId: z.string(),
|
|
1219
|
+
}),
|
|
1220
|
+
});
|
|
1221
|
+
// Start (or draft) a user-initiated Workflow. `flavor` is an open vocabulary:
|
|
1222
|
+
// "ai" (prompt-and-go - the daemon spawns an orchestrator agent that declares
|
|
1223
|
+
// its own plan via start_workflow) or "graph" (deterministic - the daemon
|
|
1224
|
+
// executes `graphId` with `graphInputs`). `draft: true` creates the record
|
|
1225
|
+
// without executing (the designer flow); `runId` executes an existing draft in
|
|
1226
|
+
// place - or, with `draft: true`, re-saves that draft in place.
|
|
1227
|
+
const WorkflowStartRequestFieldsSchema = z.object({
|
|
834
1228
|
flavor: z.string(),
|
|
835
1229
|
cwd: z.string(),
|
|
836
1230
|
workspaceId: z.string().optional(),
|
|
@@ -845,21 +1239,59 @@ export const RunsStartRequestSchema = z.object({
|
|
|
845
1239
|
prompt: z.string().optional(),
|
|
846
1240
|
graphId: z.string().optional(),
|
|
847
1241
|
graphInputs: z.record(z.string(), z.string()).optional(),
|
|
1242
|
+
// A daemon-issued, request-bound token is required after Graph review. It
|
|
1243
|
+
// cannot be replaced by a client-side "confirmed" assertion.
|
|
1244
|
+
startConfirmationToken: z.string().min(1).optional(),
|
|
848
1245
|
draft: z.boolean().optional(),
|
|
849
1246
|
runId: z.string().optional(),
|
|
850
1247
|
requestId: z.string(),
|
|
851
1248
|
});
|
|
1249
|
+
const WorkflowStartResponsePayloadSchema = z.object({
|
|
1250
|
+
runId: z.string().optional(),
|
|
1251
|
+
// The root/orchestrator agent whose chat the client navigates to, and the
|
|
1252
|
+
// workspace the daemon resolved it into (the dialog only knows a project
|
|
1253
|
+
// target's cwd).
|
|
1254
|
+
agentId: z.string().optional(),
|
|
1255
|
+
workspaceId: z.string().optional(),
|
|
1256
|
+
// Returned without starting anything when the daemon requires an explicit
|
|
1257
|
+
// Graph-start confirmation. The caller renders this factual shape, then
|
|
1258
|
+
// resubmits the daemon-issued token with the unchanged launch request.
|
|
1259
|
+
confirmation: WorkflowStartConfirmationSchema.optional(),
|
|
1260
|
+
confirmationToken: z.string().min(1).optional(),
|
|
1261
|
+
error: z.string().optional(),
|
|
1262
|
+
requestId: z.string(),
|
|
1263
|
+
});
|
|
1264
|
+
/** The canonical Workflow launch RPC. */
|
|
1265
|
+
export const WorkflowsStartRequestSchema = WorkflowStartRequestFieldsSchema.extend({
|
|
1266
|
+
type: z.literal("workflows.start.request"),
|
|
1267
|
+
});
|
|
1268
|
+
export const WorkflowsStartResponseSchema = z.object({
|
|
1269
|
+
type: z.literal("workflows.start.response"),
|
|
1270
|
+
payload: WorkflowStartResponsePayloadSchema,
|
|
1271
|
+
});
|
|
1272
|
+
// COMPAT(runsStartRpc): renamed to workflows.start in v0.9.0; accept and
|
|
1273
|
+
// answer the legacy pair through 2027-02-28 so separately shipped apps and
|
|
1274
|
+
// daemons retain the established Workflow-launch behavior.
|
|
1275
|
+
export const RunsStartRequestSchema = WorkflowStartRequestFieldsSchema.extend({
|
|
1276
|
+
type: z.literal("runs.start.request"),
|
|
1277
|
+
});
|
|
852
1278
|
export const RunsStartResponseSchema = z.object({
|
|
853
1279
|
type: z.literal("runs.start.response"),
|
|
1280
|
+
payload: WorkflowStartResponsePayloadSchema,
|
|
1281
|
+
});
|
|
1282
|
+
/** Respond to an AI Workflow's daemon-owned start confirmation. */
|
|
1283
|
+
export const WorkflowsStartConfirmationRespondRequestSchema = z.object({
|
|
1284
|
+
type: z.literal("workflows.start_confirmation.respond.request"),
|
|
1285
|
+
runId: z.string().min(1),
|
|
1286
|
+
approved: z.boolean(),
|
|
1287
|
+
requestId: z.string(),
|
|
1288
|
+
});
|
|
1289
|
+
export const WorkflowsStartConfirmationRespondResponseSchema = z.object({
|
|
1290
|
+
type: z.literal("workflows.start_confirmation.respond.response"),
|
|
854
1291
|
payload: z.object({
|
|
855
|
-
runId: z.string()
|
|
856
|
-
|
|
857
|
-
// workspace the daemon resolved it into (the dialog only knows a project
|
|
858
|
-
// target's cwd).
|
|
859
|
-
agentId: z.string().optional(),
|
|
860
|
-
workspaceId: z.string().optional(),
|
|
861
|
-
error: z.string().optional(),
|
|
1292
|
+
runId: z.string(),
|
|
1293
|
+
accepted: z.boolean(),
|
|
862
1294
|
requestId: z.string(),
|
|
863
1295
|
}),
|
|
864
1296
|
});
|
|
865
|
-
//# sourceMappingURL=
|
|
1297
|
+
//# sourceMappingURL=workflow.js.map
|