@dazl/api-client 1.0.43 → 1.0.45
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-document.d.ts +71 -0
- package/dist/agent-document.d.ts.map +1 -0
- package/dist/agent-document.js +77 -0
- package/dist/agent-document.js.map +1 -0
- package/dist/db/generated-types.d.ts +108 -0
- package/dist/db/generated-types.d.ts.map +1 -1
- package/dist/internal/index.d.ts +2 -2
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +1 -1
- package/dist/internal/index.js.map +1 -1
- package/dist/internal/sdk.gen.d.ts +29 -1
- package/dist/internal/sdk.gen.d.ts.map +1 -1
- package/dist/internal/sdk.gen.js +192 -0
- package/dist/internal/sdk.gen.js.map +1 -1
- package/dist/internal/types.gen.d.ts +1785 -0
- package/dist/internal/types.gen.d.ts.map +1 -1
- package/dist/workflow-document.d.ts +243 -0
- package/dist/workflow-document.d.ts.map +1 -0
- package/dist/workflow-document.js +282 -0
- package/dist/workflow-document.js.map +1 -0
- package/package.json +5 -2
- package/src/agent-document.ts +98 -0
- package/src/db/generated-types.ts +115 -0
- package/src/internal/index.ts +2 -2
- package/src/internal/sdk.gen.ts +219 -1
- package/src/internal/types.gen.ts +1962 -0
- package/src/workflow-document.ts +333 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/** The only schema version there has ever been. Interactive fields are ordinary optional fields. */
|
|
4
|
+
export const WORKFLOW_DOCUMENT_SCHEMA_VERSION = "1";
|
|
5
|
+
|
|
6
|
+
/** Directory the editor creates workflow files in. Discovery is by extension, not by directory. */
|
|
7
|
+
export const WORKFLOWS_DIR = "workflows";
|
|
8
|
+
export const WORKFLOW_FILE_EXTENSION = ".flow.json";
|
|
9
|
+
|
|
10
|
+
/** How a run can start. A closed set: nothing else starts a run, and there is no schedule trigger. */
|
|
11
|
+
export const TRIGGER_SOURCES = ["workspace-prompt", "chat-prompt", "chat-agent-tool"] as const;
|
|
12
|
+
export type TriggerSource = (typeof TRIGGER_SOURCES)[number];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Whether a path stays inside the project root it is resolved against: not an absolute POSIX, drive
|
|
16
|
+
* or UNC path, not a URL, no `..` segment, no NUL byte. Both `/` and `\` count as separators, so the
|
|
17
|
+
* guarantee holds for a consumer on Windows too. A backslash is otherwise allowed, because git
|
|
18
|
+
* C-quotes a non-ASCII name (`"\303\251.txt"`) and observed paths carry that verbatim. `.` and empty
|
|
19
|
+
* segments cannot escape, so a `./` prefix or a doubled slash is accepted rather than rejected.
|
|
20
|
+
*/
|
|
21
|
+
function staysInProjectRoot(path: string): boolean {
|
|
22
|
+
return (
|
|
23
|
+
!/^[/\\]/.test(path) &&
|
|
24
|
+
!/^[a-z]:/i.test(path) &&
|
|
25
|
+
!/^[a-z][a-z\d+.-]*:\/\//i.test(path) &&
|
|
26
|
+
!path.split(/[/\\]/).includes("..") &&
|
|
27
|
+
!path.includes("\0")
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const relativePathSchema = z.string().refine(staysInProjectRoot, {
|
|
32
|
+
message: "Must be a project-relative path: not absolute, not a URL, and without '..' segments",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/** A non-empty path relative to the project root that cannot leave it. */
|
|
36
|
+
export const projectRelativePathSchema = relativePathSchema.min(1);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* What every node produces. The same shape for every node in every workflow — it is not authored and
|
|
40
|
+
* is not part of the document. A step that needs to hand on anything richer than prose writes a file
|
|
41
|
+
* and names it here.
|
|
42
|
+
*/
|
|
43
|
+
export const workflowNodeOutputSchema = z.object({
|
|
44
|
+
text: z.string(),
|
|
45
|
+
/** Project-relative paths. Never absolute, never a URL, never escaping the root. */
|
|
46
|
+
files: z.array(projectRelativePathSchema).default([]),
|
|
47
|
+
/**
|
|
48
|
+
* What the executor observed this step change, one entry per repository whose head moved.
|
|
49
|
+
*
|
|
50
|
+
* Written by the executor after the agent's own output, never by the agent: `text` and `files`
|
|
51
|
+
* are what a step says it did, and this is what it was seen to do. Optional because an output
|
|
52
|
+
* stored before this existed must still parse, and because a step that moved no head records
|
|
53
|
+
* nothing at all. Absent therefore means "nothing was observed", which covers both a step that
|
|
54
|
+
* changed nothing and a step nobody was watching - the two are deliberately not distinguished
|
|
55
|
+
* here. What is kept distinct is absent versus an empty array, which no producer ever writes.
|
|
56
|
+
*/
|
|
57
|
+
changes: z
|
|
58
|
+
.array(
|
|
59
|
+
z.object({
|
|
60
|
+
/** Directory under the project root; '' when the project root is itself the repo. */
|
|
61
|
+
dirName: relativePathSchema,
|
|
62
|
+
from: z.string(),
|
|
63
|
+
to: z.string(),
|
|
64
|
+
commits: z.number().int().nonnegative(),
|
|
65
|
+
/** Project-relative, as `getChangedFilesBetween` reports them. */
|
|
66
|
+
files: z.array(projectRelativePathSchema),
|
|
67
|
+
}),
|
|
68
|
+
)
|
|
69
|
+
.optional(),
|
|
70
|
+
});
|
|
71
|
+
export type WorkflowNodeOutput = z.infer<typeof workflowNodeOutputSchema>;
|
|
72
|
+
|
|
73
|
+
const nodePositionSchema = z.object({ x: z.number(), y: z.number() });
|
|
74
|
+
|
|
75
|
+
const nodeSizeSchema = z.object({ width: z.number(), height: z.number() });
|
|
76
|
+
|
|
77
|
+
const baseNodeSchema = z.object({
|
|
78
|
+
id: z.string().min(1),
|
|
79
|
+
title: z.string(),
|
|
80
|
+
description: z.string().optional(),
|
|
81
|
+
position: nodePositionSchema,
|
|
82
|
+
/** Set once a user drags a node, so auto-layout stops moving it. */
|
|
83
|
+
manualPosition: z.boolean().optional(),
|
|
84
|
+
size: nodeSizeSchema.optional(),
|
|
85
|
+
/** The one node that follows this one. Absent means the run ends here. */
|
|
86
|
+
nextStep: z.string().optional(),
|
|
87
|
+
skippable: z.boolean().default(false),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/** One reviewer on a review node: an agent, optionally speaking for a person. */
|
|
91
|
+
export const workflowReviewerSchema = z.object({
|
|
92
|
+
/**
|
|
93
|
+
* An agent document's `meta.name`. Deliberately not `.min(1)`, for the same reason as the `work`
|
|
94
|
+
* node's `agent` field below: a reviewer can be added before its agent is picked, and rejecting
|
|
95
|
+
* `""` here would make the whole document non-conforming over the step the user just touched.
|
|
96
|
+
* `validateWorkflowChain` warns about an empty reviewer agent; sync time is where it becomes an
|
|
97
|
+
* error.
|
|
98
|
+
*/
|
|
99
|
+
agent: z.string(),
|
|
100
|
+
prompt: z.string().optional(),
|
|
101
|
+
/** Email of the person this reviewer represents, matching how rule owners are named. */
|
|
102
|
+
onBehalfOf: z.string().optional(),
|
|
103
|
+
/** Omission means personal. Only an explicit designation makes a conversation workspace-visible. */
|
|
104
|
+
conversationRole: z.enum(["personal", "workspace"]).optional(),
|
|
105
|
+
});
|
|
106
|
+
export type WorkflowReviewer = z.infer<typeof workflowReviewerSchema>;
|
|
107
|
+
|
|
108
|
+
export const workflowNodeSchema = z.discriminatedUnion("kind", [
|
|
109
|
+
baseNodeSchema.extend({
|
|
110
|
+
kind: z.literal("trigger"),
|
|
111
|
+
source: z.enum(TRIGGER_SOURCES),
|
|
112
|
+
}),
|
|
113
|
+
baseNodeSchema.extend({
|
|
114
|
+
kind: z.literal("work"),
|
|
115
|
+
interaction: z.enum(["automatic", "interactive"]).optional(),
|
|
116
|
+
/**
|
|
117
|
+
* An agent document's `meta.name`. Resolved at sync time, warned about while authoring -
|
|
118
|
+
* deliberately not `.min(1)`. A work step exists before an agent is chosen for it, and a
|
|
119
|
+
* `.min(1)` here would make the whole document non-conforming the instant such a step is
|
|
120
|
+
* created, collapsing the panel around the step the user just added. `""` is a safe sentinel
|
|
121
|
+
* for "not picked yet" because every other agent reference in this file is `.min(1)`, so no
|
|
122
|
+
* real agent can be named the empty string. `validateWorkflowChain` warns about it instead.
|
|
123
|
+
*/
|
|
124
|
+
agent: z.string(),
|
|
125
|
+
/** Extra instruction for this step, appended to the agent's own system prompt. */
|
|
126
|
+
prompt: z.string().optional(),
|
|
127
|
+
}),
|
|
128
|
+
baseNodeSchema.extend({
|
|
129
|
+
kind: z.literal("review"),
|
|
130
|
+
interaction: z.enum(["automatic", "interactive"]).optional(),
|
|
131
|
+
reviewers: z.array(workflowReviewerSchema).default([]),
|
|
132
|
+
}),
|
|
133
|
+
]);
|
|
134
|
+
export type WorkflowNode = z.infer<typeof workflowNodeSchema>;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Kinds that can only ever *begin* a chain: nothing may name one as its `nextStep`.
|
|
138
|
+
*
|
|
139
|
+
* A named list rather than a `=== "trigger"` written out wherever the question comes up, because it
|
|
140
|
+
* is asked in three places that must agree - `validateWorkflowChain` below, the editor's chain
|
|
141
|
+
* mutations, and the editor's add menus - and two of those are in another repository reaching this
|
|
142
|
+
* file through the SDK. It is a list rather than a single value because several triggers in one
|
|
143
|
+
* workflow are coming, each starting its own chain, and because a second head-only kind would then
|
|
144
|
+
* be one entry here rather than a fourth copy of the same comparison.
|
|
145
|
+
*/
|
|
146
|
+
export const CHAIN_HEAD_KINDS: readonly WorkflowNode["kind"][] = ["trigger"];
|
|
147
|
+
|
|
148
|
+
/** Whether a node of this kind can only start a chain. `undefined` - an unknown node - is not one. */
|
|
149
|
+
export function isChainHeadKind(kind: string | undefined): boolean {
|
|
150
|
+
return kind !== undefined && (CHAIN_HEAD_KINDS as readonly string[]).includes(kind);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const workflowDocumentSchema = z.object({
|
|
154
|
+
schemaVersion: z.literal(WORKFLOW_DOCUMENT_SCHEMA_VERSION).optional(),
|
|
155
|
+
meta: z.object({
|
|
156
|
+
/**
|
|
157
|
+
* Deliberately not `.min(1)`. A user clearing the field to retype it would otherwise invalidate
|
|
158
|
+
* the document mid-keystroke and collapse the form around them — the exact defect step 1a shipped
|
|
159
|
+
* and had to fix. An empty name is recoverable; a missing one is still an error.
|
|
160
|
+
*/
|
|
161
|
+
name: z.string(),
|
|
162
|
+
description: z.string().optional(),
|
|
163
|
+
}),
|
|
164
|
+
nodes: z.array(workflowNodeSchema).default([]),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
export type WorkflowDocument = z.infer<typeof workflowDocumentSchema>;
|
|
168
|
+
export type WorkflowDocumentInput = z.input<typeof workflowDocumentSchema>;
|
|
169
|
+
|
|
170
|
+
/** Inspects the whole document. Execution must first restrict this to its selected reachable chain. */
|
|
171
|
+
export function isInteractiveWorkflowDocument(document: WorkflowDocument): boolean {
|
|
172
|
+
return document.nodes.some((node) => node.kind !== "trigger" && node.interaction === "interactive");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface WorkflowDocumentError {
|
|
176
|
+
path: string;
|
|
177
|
+
message: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export type WorkflowDocumentValidation =
|
|
181
|
+
{ valid: true; document: WorkflowDocument } | { valid: false; errors: WorkflowDocumentError[] };
|
|
182
|
+
|
|
183
|
+
export function validateWorkflowDocument(value: unknown): WorkflowDocumentValidation {
|
|
184
|
+
const result = workflowDocumentSchema.safeParse(value);
|
|
185
|
+
if (result.success) {
|
|
186
|
+
return { valid: true, document: result.data };
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
valid: false,
|
|
190
|
+
errors: result.error.issues.map((issue) => ({
|
|
191
|
+
path: issue.path.length > 0 ? issue.path.join(".") : "document",
|
|
192
|
+
message: issue.message,
|
|
193
|
+
})),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface WorkflowChainWarning {
|
|
198
|
+
nodeId: string;
|
|
199
|
+
message: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Chain integrity, reported separately from schema validity and deliberately as warnings.
|
|
204
|
+
*
|
|
205
|
+
* A half-built workflow is a normal state to be in while authoring — a node whose `nextStep` is not
|
|
206
|
+
* written yet must not make the file invalid. Step 2's sync is where these become failures.
|
|
207
|
+
*/
|
|
208
|
+
export function validateWorkflowChain(nodes: readonly WorkflowNode[]): WorkflowChainWarning[] {
|
|
209
|
+
const warnings: WorkflowChainWarning[] = [];
|
|
210
|
+
|
|
211
|
+
// Not a chain-shape problem, but the same "authoring problem, not a schema violation" category:
|
|
212
|
+
// `agent` accepts "" so that adding a step or a reviewer never invalidates the document, and this
|
|
213
|
+
// is where that loosened value gets flagged instead.
|
|
214
|
+
for (const node of nodes) {
|
|
215
|
+
if (node.kind === "work" && node.agent === "") {
|
|
216
|
+
warnings.push({ nodeId: node.id, message: "This step has no agent assigned yet." });
|
|
217
|
+
}
|
|
218
|
+
if (node.kind === "review") {
|
|
219
|
+
if (
|
|
220
|
+
node.interaction === "interactive" &&
|
|
221
|
+
node.reviewers.filter((r) => r.conversationRole === "workspace").length !== 1
|
|
222
|
+
) {
|
|
223
|
+
warnings.push({
|
|
224
|
+
nodeId: node.id,
|
|
225
|
+
message: "An interactive review must designate exactly one workspace reviewer.",
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
for (const reviewer of node.reviewers) {
|
|
229
|
+
if (reviewer.agent === "") {
|
|
230
|
+
warnings.push({ nodeId: node.id, message: "This step has a reviewer with no agent assigned yet." });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
237
|
+
const incoming = new Map<string, number>();
|
|
238
|
+
const seen = new Set<string>();
|
|
239
|
+
|
|
240
|
+
for (const node of nodes) {
|
|
241
|
+
if (node.nextStep === undefined) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (node.nextStep === node.id) {
|
|
245
|
+
warnings.push({ nodeId: node.id, message: "This step points at itself." });
|
|
246
|
+
seen.add(node.id);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const target = byId.get(node.nextStep);
|
|
250
|
+
if (target === undefined) {
|
|
251
|
+
warnings.push({ nodeId: node.id, message: `Next step "${node.nextStep}" is not a step in this workflow.` });
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
// A trigger is a chain head and nothing may point at one. Reported against the step holding the
|
|
255
|
+
// link rather than against the trigger, because the link is the thing that is wrong and the step
|
|
256
|
+
// holding it is the only one that can be repaired - the trigger itself is fine as it is.
|
|
257
|
+
if (isChainHeadKind(target.kind)) {
|
|
258
|
+
warnings.push({
|
|
259
|
+
nodeId: node.id,
|
|
260
|
+
message: "This step points at a trigger, and a trigger can only start a chain.",
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
incoming.set(node.nextStep, (incoming.get(node.nextStep) ?? 0) + 1);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
for (const [nodeId, count] of incoming) {
|
|
267
|
+
if (count > 1) {
|
|
268
|
+
warnings.push({ nodeId, message: "More than one step leads here, and the flow is a single chain." });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// A cycle is only reachable once every hop resolves, so this runs after the checks above.
|
|
273
|
+
for (const node of nodes) {
|
|
274
|
+
if (seen.has(node.id)) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const path = new Set<string>();
|
|
278
|
+
let current: WorkflowNode | undefined = node;
|
|
279
|
+
while (current && !seen.has(current.id)) {
|
|
280
|
+
if (path.has(current.id)) {
|
|
281
|
+
warnings.push({ nodeId: current.id, message: "These steps form a cycle, so the run would never end." });
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
path.add(current.id);
|
|
285
|
+
current = current.nextStep === undefined ? undefined : byId.get(current.nextStep);
|
|
286
|
+
}
|
|
287
|
+
for (const id of path) {
|
|
288
|
+
seen.add(id);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return warnings;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function createDefaultWorkflowDocument(name: string): WorkflowDocumentInput {
|
|
296
|
+
return {
|
|
297
|
+
schemaVersion: WORKFLOW_DOCUMENT_SCHEMA_VERSION,
|
|
298
|
+
meta: { name },
|
|
299
|
+
nodes: [
|
|
300
|
+
{
|
|
301
|
+
// A uuid, like every other node id. `id` is what becomes `WorkflowNodeRun.nodeKey` when a
|
|
302
|
+
// run is projected, and the editor's own `createNode` uses `crypto.randomUUID()` - so the
|
|
303
|
+
// literal "trigger" here meant every workflow ever created shipped with exactly one node
|
|
304
|
+
// whose key was not a uuid, and two workflows whose first steps collided on it.
|
|
305
|
+
id: crypto.randomUUID(),
|
|
306
|
+
kind: "trigger",
|
|
307
|
+
title: "Start",
|
|
308
|
+
source: "workspace-prompt",
|
|
309
|
+
position: { x: 0, y: 0 },
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Filename for a workflow, derived from its display name.
|
|
317
|
+
*
|
|
318
|
+
* Throws rather than returning a bare extension for a name with no ASCII alphanumerics, matching
|
|
319
|
+
* `CanvasManager.createCanvasFileName`. Step 1a's agent equivalent shipped without this guard and a
|
|
320
|
+
* non-Latin name silently wrote a dotfile.
|
|
321
|
+
*/
|
|
322
|
+
export function toSafeWorkflowFileName(name: string): string {
|
|
323
|
+
const slug = name
|
|
324
|
+
.replace(/[^a-zA-Z0-9]/g, "-")
|
|
325
|
+
.replace(/-+/g, "-")
|
|
326
|
+
.replace(/^-+|-+$/g, "")
|
|
327
|
+
.slice(0, 60)
|
|
328
|
+
.toLowerCase();
|
|
329
|
+
if (!slug) {
|
|
330
|
+
throw new Error("Workflow name must contain at least one letter or number");
|
|
331
|
+
}
|
|
332
|
+
return `${slug}${WORKFLOW_FILE_EXTENSION}`;
|
|
333
|
+
}
|