@anchrd/intel-contract 0.2.1 → 0.3.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/contract/contract.d.ts +813 -281
- package/dist/contract/contract.js +414 -65
- package/package.json +1 -1
|
@@ -16,9 +16,19 @@ export const SessionUser = z.strictObject({
|
|
|
16
16
|
email: z.email(),
|
|
17
17
|
name: z.string().min(1).max(240).nullable(),
|
|
18
18
|
});
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
// The fourth kind is `table` (#40). It is a kind of node, not a kind of thing: it hangs in the same
|
|
20
|
+
// folder tree, inherits the same folder grants, carries the same immutable versions and the same
|
|
21
|
+
// R2 body as a document (ADR-0004 §1). Only its media type and the one operation below differ.
|
|
22
|
+
export const KnowledgeNodeKind = z.enum(["folder", "document", "attachment", "table"]);
|
|
23
|
+
// ⚠️ There is no `ContextPolicy`, and it is not coming back in this shape (#76). It said whether a
|
|
24
|
+
// document should be pinned into a context, be found by relevance, or be named explicitly — an
|
|
25
|
+
// instruction to a retrieval Intel does not perform. Intel hands out references and the agent
|
|
26
|
+
// fetches what it needs (D24), so nothing here could ever have read it, and nothing did.
|
|
27
|
+
//
|
|
28
|
+
// Semantic search stays: as an MCP tool the agent calls, over everything or over one area.
|
|
29
|
+
// One verb per grant, granted independently (ADR-0004 §2). Not a ladder: seeing a process must be
|
|
30
|
+
// separable from being allowed to start it, and `execute` is meaningful only where a flow can live.
|
|
31
|
+
export const ResourceVerb = z.enum(["read", "write", "execute", "share"]);
|
|
22
32
|
export const SharePrincipal = z.discriminatedUnion("type", [
|
|
23
33
|
z.strictObject({ type: z.literal("user"), id: IntelId }),
|
|
24
34
|
z.strictObject({ type: z.literal("email"), email: z.email() }),
|
|
@@ -30,7 +40,6 @@ export const KnowledgeNode = z.strictObject({
|
|
|
30
40
|
kind: KnowledgeNodeKind,
|
|
31
41
|
title: z.string().min(1).max(240),
|
|
32
42
|
description: z.string().max(2_000).nullable(),
|
|
33
|
-
contextPolicy: ContextPolicy,
|
|
34
43
|
ownerId: IntelId,
|
|
35
44
|
currentVersionId: IntelId.nullable(),
|
|
36
45
|
createdAt: IsoDateTime,
|
|
@@ -59,7 +68,6 @@ export const CreateKnowledgeNodeInput = z.strictObject({
|
|
|
59
68
|
kind: KnowledgeNodeKind,
|
|
60
69
|
title: z.string().trim().min(1).max(240),
|
|
61
70
|
description: z.string().trim().max(2_000).nullable().default(null),
|
|
62
|
-
contextPolicy: ContextPolicy.default("relevant"),
|
|
63
71
|
idempotencyKey: z.string().min(8).max(200),
|
|
64
72
|
});
|
|
65
73
|
export const SaveKnowledgeVersionInput = z.strictObject({
|
|
@@ -82,14 +90,10 @@ export const UpdateKnowledgeNodeInput = z
|
|
|
82
90
|
baseUpdatedAt: IsoDateTime,
|
|
83
91
|
title: z.string().trim().min(1).max(240).optional(),
|
|
84
92
|
description: z.string().trim().max(2_000).nullable().optional(),
|
|
85
|
-
contextPolicy: ContextPolicy.optional(),
|
|
86
93
|
parentId: IntelId.nullable().optional(),
|
|
87
94
|
idempotencyKey: z.string().min(8).max(200),
|
|
88
95
|
})
|
|
89
|
-
.refine((input) => input.title !== undefined ||
|
|
90
|
-
input.description !== undefined ||
|
|
91
|
-
input.contextPolicy !== undefined ||
|
|
92
|
-
input.parentId !== undefined, { message: "At least one change is required" });
|
|
96
|
+
.refine((input) => input.title !== undefined || input.description !== undefined || input.parentId !== undefined, { message: "At least one change is required" });
|
|
93
97
|
export const ArchiveKnowledgeNodeInput = z.strictObject({
|
|
94
98
|
nodeId: IntelId,
|
|
95
99
|
baseUpdatedAt: IsoDateTime,
|
|
@@ -108,32 +112,94 @@ export const KnowledgeAttachment = z.strictObject({
|
|
|
108
112
|
version: KnowledgeVersion,
|
|
109
113
|
resourceUri: z.string().regex(/^intel:\/\/knowledge\/[^/]+\/attachment$/),
|
|
110
114
|
});
|
|
115
|
+
// A table is CSV, and CSV is the whole format: it is what is stored, what is downloaded and what a
|
|
116
|
+
// machine reads. There is no second representation to keep in step with it (#40).
|
|
117
|
+
export const TableMediaType = "text/csv";
|
|
118
|
+
// A column name is the contract between the table and everyone who appends to it, so it is trimmed,
|
|
119
|
+
// non-empty and bounded like a title. Cells are not: a cell is text, and text is what CSV carries.
|
|
120
|
+
export const TableColumn = z.string().trim().min(1).max(120);
|
|
121
|
+
export const TableCell = z.string().max(4_000);
|
|
122
|
+
export const TableRow = z.array(TableCell).min(1).max(64);
|
|
123
|
+
// Writing the header, once. The columns are the contract (#40's comment), which is why this refuses
|
|
124
|
+
// on a table that already has one: changing the header would silently reinterpret every row that
|
|
125
|
+
// was appended under the old one.
|
|
126
|
+
export const DefineKnowledgeTableInput = z.strictObject({
|
|
127
|
+
nodeId: IntelId,
|
|
128
|
+
columns: z
|
|
129
|
+
.array(TableColumn)
|
|
130
|
+
.min(1)
|
|
131
|
+
.max(64)
|
|
132
|
+
.refine((columns) => new Set(columns.map((column) => column.toLowerCase())).size === columns.length, { error: "Column names must be distinct" }),
|
|
133
|
+
idempotencyKey: z.string().min(8).max(200),
|
|
134
|
+
});
|
|
135
|
+
// ⚠️ No `baseVersionId`, and that absence is the feature. A document replaces its content and needs
|
|
136
|
+
// to know which content it replaces; an append adds to the end and cannot collide with a second
|
|
137
|
+
// append, so demanding a base version would invent a conflict that does not exist and force the
|
|
138
|
+
// caller to read the whole table first — the exact cost #40 exists to remove.
|
|
139
|
+
export const AppendKnowledgeTableRowsInput = z.strictObject({
|
|
140
|
+
nodeId: IntelId,
|
|
141
|
+
rows: z.array(TableRow).min(1).max(1_000),
|
|
142
|
+
idempotencyKey: z.string().min(8).max(200),
|
|
143
|
+
});
|
|
144
|
+
export const GetKnowledgeTableInput = z.strictObject({ nodeId: IntelId });
|
|
145
|
+
// The table as a grid rather than as text: the server owns the one CSV reader, so no surface has to
|
|
146
|
+
// grow a second one that would disagree with it about quoting.
|
|
147
|
+
export const KnowledgeTable = z.strictObject({
|
|
148
|
+
node: KnowledgeNode,
|
|
149
|
+
columns: z.array(z.string()),
|
|
150
|
+
rows: z.array(z.array(z.string())),
|
|
151
|
+
// The newest append, or `null` while the table has no header yet.
|
|
152
|
+
versionId: IntelId.nullable(),
|
|
153
|
+
});
|
|
154
|
+
export const AppendKnowledgeTableRowsResult = z.strictObject({
|
|
155
|
+
node: KnowledgeNode,
|
|
156
|
+
version: KnowledgeVersion,
|
|
157
|
+
appended: z.number().int().positive(),
|
|
158
|
+
});
|
|
159
|
+
// ⚠️ Kept for what is already stored, not for what is written. Relations were picked in a dialog
|
|
160
|
+
// until #41; a link is now made where it is meant — in the text — and every link written from now
|
|
161
|
+
// on is a `references`. Rewriting the old rows would destroy a distinction somebody chose on
|
|
162
|
+
// purpose, and dropping the column would destroy it with them, so both stay readable.
|
|
111
163
|
export const KnowledgeLinkRelation = z.enum(["references", "related", "depends_on", "implements"]);
|
|
164
|
+
// Where the link came from. `text` links are derived from a document's content and are rewritten
|
|
165
|
+
// whenever it is saved; `manual` links were made in the dialog #41 removed and are now history.
|
|
166
|
+
//
|
|
167
|
+
// ⚠️ This is provenance, never a second sort of relationship. Nothing offers the reader a choice
|
|
168
|
+
// between them, and nothing may start writing `manual` again — that would be the two ways of saying
|
|
169
|
+
// one thing that #41 exists to end. It exists so that saving a document cannot delete a link
|
|
170
|
+
// somebody made before there was another way to make one.
|
|
171
|
+
export const KnowledgeLinkOrigin = z.enum(["text", "manual"]);
|
|
112
172
|
export const KnowledgeLink = z.strictObject({
|
|
113
173
|
id: IntelId,
|
|
114
174
|
sourceNodeId: IntelId,
|
|
115
175
|
targetNodeId: IntelId,
|
|
116
176
|
relation: KnowledgeLinkRelation,
|
|
177
|
+
origin: KnowledgeLinkOrigin,
|
|
117
178
|
label: z.string().trim().min(1).max(120).nullable(),
|
|
118
179
|
createdBy: IntelId,
|
|
119
180
|
createdAt: IsoDateTime,
|
|
120
181
|
});
|
|
121
182
|
export const KnowledgeLinkList = z.strictObject({ items: z.array(KnowledgeLink) });
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
183
|
+
// The inline element a document link is, inside a BlockNote document (#41).
|
|
184
|
+
//
|
|
185
|
+
// ⚠️ The ID and nothing else. No title and no path travel with it: a stored title would go stale
|
|
186
|
+
// the moment the target is renamed, a stored path the moment it is moved — and either one would
|
|
187
|
+
// put a name the reader may not see into a document they may.
|
|
188
|
+
export const DocumentLinkInlineType = "documentLink";
|
|
189
|
+
export const ResolveKnowledgeLinksInput = z.strictObject({
|
|
190
|
+
nodeIds: z.array(IntelId).min(1).max(200),
|
|
191
|
+
});
|
|
192
|
+
export const ResolvedKnowledgeLink = z.strictObject({
|
|
193
|
+
nodeId: IntelId,
|
|
194
|
+
title: z.string().min(1).max(240),
|
|
132
195
|
});
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
196
|
+
// ⚠️ Only what the asking reader may see is in here, and an unreachable target is simply absent —
|
|
197
|
+
// never a row with an empty title, never a count, never a "restricted" marker. The list is what one
|
|
198
|
+
// side of a document link is drawn from, and an entry that says "something is here" is exactly the
|
|
199
|
+
// leak this schema has to make impossible to write by accident. Deleted and unreadable therefore
|
|
200
|
+
// look identical from the outside, which is the point.
|
|
201
|
+
export const ResolveKnowledgeLinksResult = z.strictObject({
|
|
202
|
+
items: z.array(ResolvedKnowledgeLink),
|
|
137
203
|
});
|
|
138
204
|
export const KnowledgeGraphInput = z.strictObject({
|
|
139
205
|
limit: z.number().int().min(1).max(500).default(250),
|
|
@@ -153,7 +219,7 @@ export const ResourceGrant = z.strictObject({
|
|
|
153
219
|
id: IntelId,
|
|
154
220
|
resourceId: IntelId,
|
|
155
221
|
principal: SharePrincipal,
|
|
156
|
-
|
|
222
|
+
verb: ResourceVerb,
|
|
157
223
|
expiresAt: IsoDateTime.nullable(),
|
|
158
224
|
createdBy: IntelId,
|
|
159
225
|
createdAt: IsoDateTime,
|
|
@@ -161,7 +227,7 @@ export const ResourceGrant = z.strictObject({
|
|
|
161
227
|
export const ShareKnowledgeInput = z.strictObject({
|
|
162
228
|
resourceId: IntelId,
|
|
163
229
|
principal: SharePrincipal,
|
|
164
|
-
|
|
230
|
+
verb: ResourceVerb,
|
|
165
231
|
expiresAt: IsoDateTime.nullable().default(null),
|
|
166
232
|
idempotencyKey: z.string().min(8).max(200),
|
|
167
233
|
});
|
|
@@ -170,7 +236,30 @@ export const RevokeKnowledgeGrantInput = z.strictObject({
|
|
|
170
236
|
grantId: IntelId,
|
|
171
237
|
idempotencyKey: z.string().min(8).max(200),
|
|
172
238
|
});
|
|
173
|
-
|
|
239
|
+
// What a grant does not cover, reported to whoever just made it. A flow in the shared folder may
|
|
240
|
+
// read a document outside it, and the run is re-authorized against the person running it — so the
|
|
241
|
+
// grant can be complete and the flow still stop for them (ADR-0004 §4).
|
|
242
|
+
//
|
|
243
|
+
// ⚠️ `titles` holds only the documents the sharer may see; everything else is in `hidden` as a
|
|
244
|
+
// number. A warning must not become a way of reading titles out of the tree.
|
|
245
|
+
export const UnreadableKnowledge = z.strictObject({
|
|
246
|
+
titles: z.array(z.string().min(1).max(240)),
|
|
247
|
+
hidden: z.number().int().nonnegative(),
|
|
248
|
+
});
|
|
249
|
+
// The grant is in the answer, so the warning cannot be mistaken for a refusal: it is written first
|
|
250
|
+
// and described afterwards. Blocking would force everyone who uses a central policy document to
|
|
251
|
+
// duplicate it, which is the opposite of what one tree is for (ADR-0004 §4).
|
|
252
|
+
export const ShareKnowledgeResult = z.strictObject({
|
|
253
|
+
grant: ResourceGrant,
|
|
254
|
+
unreadable: UnreadableKnowledge,
|
|
255
|
+
});
|
|
256
|
+
// `applicableVerbs` travels with the list because the answer is the business layer's, not the
|
|
257
|
+
// screen's: a document has nothing to execute, so `execute` is not offered on one (ADR-0004 §2).
|
|
258
|
+
export const ResourceGrantList = z.strictObject({
|
|
259
|
+
resourceId: IntelId,
|
|
260
|
+
applicableVerbs: z.array(ResourceVerb).min(1),
|
|
261
|
+
items: z.array(ResourceGrant),
|
|
262
|
+
});
|
|
174
263
|
export const SearchKnowledgeInput = z.strictObject({
|
|
175
264
|
query: z.string().trim().min(1).max(500),
|
|
176
265
|
limit: z.number().int().min(1).max(50).default(10),
|
|
@@ -190,7 +279,6 @@ export const SearchKnowledgeResult = z.strictObject({
|
|
|
190
279
|
});
|
|
191
280
|
export const ReindexKnowledgeResult = z.strictObject({ queued: z.number().int().nonnegative() });
|
|
192
281
|
export const RevokeGrantResult = z.strictObject({ revoked: z.boolean() });
|
|
193
|
-
export const DeleteKnowledgeLinkResult = z.strictObject({ deleted: z.boolean() });
|
|
194
282
|
function isPrivateIpv4(hostname) {
|
|
195
283
|
const parts = hostname.split(".").map(Number);
|
|
196
284
|
if (parts.length !== 4 ||
|
|
@@ -272,36 +360,86 @@ export const ToolTestResult = z.strictObject({
|
|
|
272
360
|
});
|
|
273
361
|
export const FlowNodeId = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/);
|
|
274
362
|
export const FlowPosition = z.strictObject({ x: z.number().finite(), y: z.number().finite() });
|
|
363
|
+
// A node has a title and nothing else to write in prose (#39). A second free text beside it was
|
|
364
|
+
// kept half up to date on both sides, and for an instruction the same sentence already belongs in
|
|
365
|
+
// the instruction itself.
|
|
275
366
|
const FlowNodeBase = {
|
|
276
367
|
id: FlowNodeId,
|
|
277
368
|
position: FlowPosition,
|
|
278
369
|
label: z.string().trim().min(1).max(160),
|
|
279
|
-
description: z.string().trim().max(2_000).nullable().default(null),
|
|
280
370
|
};
|
|
371
|
+
// Which version of the callee a sub-flow call takes (ADR-0004 §5). Three states rather than an
|
|
372
|
+
// optional identifier, because the difference between them is a decision and has to be readable:
|
|
373
|
+
//
|
|
374
|
+
// - `latest` the draft default. Nobody should have to version things while building, and
|
|
375
|
+
// publishing turns this into `pinned` — visibly, before the author publishes.
|
|
376
|
+
// - `follows` "always latest", chosen on purpose. The call rides along with the callee, so a
|
|
377
|
+
// change to the building block changes this flow too. Publishing leaves it alone.
|
|
378
|
+
// - `pinned` one immutable version, whatever is published elsewhere.
|
|
379
|
+
//
|
|
380
|
+
// ⚠️ Without the freeze a change to a building block would silently change the behavior of every
|
|
381
|
+
// published flow using it, which contradicts the immutable versions and pinned schema fingerprints
|
|
382
|
+
// Intel otherwise guarantees. That is why `latest` cannot survive publishing.
|
|
383
|
+
export const SubflowVersionMode = z.enum(["latest", "follows", "pinned"]);
|
|
384
|
+
export const SubflowVersion = z.discriminatedUnion("mode", [
|
|
385
|
+
z.strictObject({ mode: z.literal("latest") }),
|
|
386
|
+
z.strictObject({ mode: z.literal("follows") }),
|
|
387
|
+
z.strictObject({ mode: z.literal("pinned"), versionId: IntelId }),
|
|
388
|
+
]);
|
|
389
|
+
// The three layers a node can belong to (D25). Which one a kind is in decides what it may carry and
|
|
390
|
+
// where it may sit, and both rules are enforced in `compileFlow` rather than only drawn in the
|
|
391
|
+
// editor — a graph arrives over MCP as readily as from the canvas.
|
|
392
|
+
//
|
|
393
|
+
// ⚠️ A LINK never stands in the chain. It hangs off a step on a `context` edge, and that is the
|
|
394
|
+
// distinction the graph has drawn since #37 without anyone enforcing it — which is exactly how a
|
|
395
|
+
// start with an attachment once began its run at the attachment (the "flow edges only" comments in
|
|
396
|
+
// `flows.ts`). Marker and step keep the chain; a link is what a step works with.
|
|
397
|
+
export const FlowNodeLayer = z.enum(["marker", "step", "link"]);
|
|
398
|
+
export const flowNodeLayer = {
|
|
399
|
+
trigger: "marker",
|
|
400
|
+
output: "marker",
|
|
401
|
+
instruction: "step",
|
|
402
|
+
condition: "step",
|
|
403
|
+
subflow: "step",
|
|
404
|
+
folder: "link",
|
|
405
|
+
document: "link",
|
|
406
|
+
upload: "link",
|
|
407
|
+
table: "link",
|
|
408
|
+
tool: "link",
|
|
409
|
+
};
|
|
410
|
+
// One reference, never a list. The old `knowledge` node carried up to a hundred, plus a retrieval
|
|
411
|
+
// mode and a query that nothing read — three things in one, and the last two describing a retrieval
|
|
412
|
+
// Intel does not perform (D24: the agent fetches, Intel does not put anything into a context).
|
|
413
|
+
// Splitting by kind is what makes "exactly one" sayable at all, and it lets the editor filter the
|
|
414
|
+
// picker by what the node is for.
|
|
415
|
+
const LinkConfiguration = z.strictObject({ resourceId: IntelId });
|
|
281
416
|
export const FlowNode = z.discriminatedUnion("kind", [
|
|
282
417
|
z.strictObject({
|
|
283
418
|
...FlowNodeBase,
|
|
284
419
|
kind: z.literal("trigger"),
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
420
|
+
// ⚠️ `manual` is the only mode there is. `webhook` and `schedule` stood here and fired nothing:
|
|
421
|
+
// a flow is carried out by an external agent that brings its own schedule, which is what the
|
|
422
|
+
// code does rather than what it intends — `step()` hands back the current node, `completeStep`
|
|
423
|
+
// takes the result from outside, and the Cloudflare workflow waits rather than drives. #32 was
|
|
424
|
+
// closed on that basis, and a mode nothing triggers is a promise nobody keeps.
|
|
425
|
+
configuration: z.strictObject({ mode: z.literal("manual") }),
|
|
290
426
|
}),
|
|
291
427
|
z.strictObject({
|
|
292
428
|
...FlowNodeBase,
|
|
293
429
|
kind: z.literal("instruction"),
|
|
294
430
|
configuration: z.strictObject({ prompt: z.string().min(1).max(50_000) }),
|
|
295
431
|
}),
|
|
432
|
+
// The four link kinds that name something in the shared tree. They are separate kinds rather than
|
|
433
|
+
// one with a `kind` field so the canvas, the palette and the picker can each say what they mean
|
|
434
|
+
// without reading into a configuration — and so a stored graph says it too.
|
|
435
|
+
z.strictObject({ ...FlowNodeBase, kind: z.literal("folder"), configuration: LinkConfiguration }),
|
|
296
436
|
z.strictObject({
|
|
297
437
|
...FlowNodeBase,
|
|
298
|
-
kind: z.literal("
|
|
299
|
-
configuration:
|
|
300
|
-
resourceIds: z.array(IntelId).min(1).max(100),
|
|
301
|
-
mode: z.enum(["full", "relevant"]),
|
|
302
|
-
query: z.string().max(1_000).nullable().default(null),
|
|
303
|
-
}),
|
|
438
|
+
kind: z.literal("document"),
|
|
439
|
+
configuration: LinkConfiguration,
|
|
304
440
|
}),
|
|
441
|
+
z.strictObject({ ...FlowNodeBase, kind: z.literal("upload"), configuration: LinkConfiguration }),
|
|
442
|
+
z.strictObject({ ...FlowNodeBase, kind: z.literal("table"), configuration: LinkConfiguration }),
|
|
305
443
|
z.strictObject({
|
|
306
444
|
...FlowNodeBase,
|
|
307
445
|
kind: z.literal("tool"),
|
|
@@ -323,24 +461,47 @@ export const FlowNode = z.discriminatedUnion("kind", [
|
|
|
323
461
|
instruction: z.string().min(1).max(10_000),
|
|
324
462
|
}),
|
|
325
463
|
}),
|
|
464
|
+
// ⚠️ There is no `approval` kind, and adding one back is a product decision rather than a schema
|
|
465
|
+
// addition (#73). It waited on a named person with a deadline, which needs a queue, a
|
|
466
|
+
// notification, a stand-in and an answer to "the deadline passed" — none of which exist. Without
|
|
467
|
+
// it, D24 holds without exception: nothing in Intel waits. A stored graph that still carries one
|
|
468
|
+
// is rewritten to a `condition` by migration 0007, because the question it asked is one the agent
|
|
469
|
+
// can answer.
|
|
470
|
+
//
|
|
471
|
+
// The seventh kind: one flow calls another (ADR-0004 §3). A schema addition, not hidden behavior
|
|
472
|
+
// in a generic code node — which flow is called has to be readable from the graph, or neither the
|
|
473
|
+
// publish-time call rule nor the sidebar could see it.
|
|
326
474
|
z.strictObject({
|
|
327
475
|
...FlowNodeBase,
|
|
328
|
-
kind: z.literal("
|
|
476
|
+
kind: z.literal("subflow"),
|
|
477
|
+
// What the called flow is given travels through `StartFlowRunInput.input` — the schema every run
|
|
478
|
+
// already uses. A second, static input here would be a promise the execution does not keep.
|
|
329
479
|
configuration: z.strictObject({
|
|
330
|
-
|
|
331
|
-
|
|
480
|
+
flowId: IntelId,
|
|
481
|
+
version: SubflowVersion.default({ mode: "latest" }),
|
|
332
482
|
}),
|
|
333
483
|
}),
|
|
484
|
+
// ⚠️ The end marks, it does not make. It used to carry a `template` that nothing ever read, and a
|
|
485
|
+
// node called "Result" that appeared to produce one is what everybody read it as. The result of a
|
|
486
|
+
// run is what the last step before it hands in (D25) — `completeStep` carries that forward.
|
|
334
487
|
z.strictObject({
|
|
335
488
|
...FlowNodeBase,
|
|
336
489
|
kind: z.literal("output"),
|
|
337
|
-
configuration: z.strictObject({
|
|
490
|
+
configuration: z.strictObject({}),
|
|
338
491
|
}),
|
|
339
492
|
]);
|
|
493
|
+
// The two things an edge can mean (#37). `flow` is the order of work — "and then". `context` is what
|
|
494
|
+
// a step works with: a document consulted in exactly this step, an approval obtained in exactly this
|
|
495
|
+
// step, or, at the output, the table a result is written to.
|
|
496
|
+
//
|
|
497
|
+
// ⚠️ `flow` is the default, and that is the whole of the migration: every edge stored before this
|
|
498
|
+
// existed parses into the meaning it already had. Nothing about saved graphs has to be rewritten.
|
|
499
|
+
export const FlowEdgeKind = z.enum(["flow", "context"]);
|
|
340
500
|
export const FlowEdge = z.strictObject({
|
|
341
501
|
id: FlowNodeId,
|
|
342
502
|
source: FlowNodeId,
|
|
343
503
|
target: FlowNodeId,
|
|
504
|
+
kind: FlowEdgeKind.default("flow"),
|
|
344
505
|
label: z.string().trim().min(1).max(120).nullable().default(null),
|
|
345
506
|
sourceHandle: z.string().trim().min(1).max(120).nullable().default(null),
|
|
346
507
|
});
|
|
@@ -376,6 +537,41 @@ export const FlowDocument = z.strictObject({
|
|
|
376
537
|
version: FlowVersion.nullable(),
|
|
377
538
|
});
|
|
378
539
|
export const FlowList = z.strictObject({ items: z.array(Flow) });
|
|
540
|
+
export const FlowKnowledgeReference = z.strictObject({
|
|
541
|
+
id: IntelId,
|
|
542
|
+
title: z.string().min(1).max(240),
|
|
543
|
+
});
|
|
544
|
+
// What a flow touches: the documents its Knowledge steps name and the tools its Tool steps call,
|
|
545
|
+
// read straight out of the graph. Deliberately not a conflict report — there is no arithmetic here
|
|
546
|
+
// and nothing that can go stale, because the graph is the answer. Whether a given person may reach
|
|
547
|
+
// any of it is decided where it can be decided honestly: when the folder is shared, and at runtime
|
|
548
|
+
// (ADR-0004 §4). For tools it can only ever be the latter, because the catalog is a live query with
|
|
549
|
+
// the requesting user's own token (ADR-0003).
|
|
550
|
+
//
|
|
551
|
+
// ⚠️ `knowledge` names only what the asking user may see. The rest is `hiddenKnowledge`, a count.
|
|
552
|
+
export const FlowRequirements = z.strictObject({
|
|
553
|
+
flowId: IntelId,
|
|
554
|
+
versionId: IntelId.nullable(),
|
|
555
|
+
knowledge: z.array(FlowKnowledgeReference),
|
|
556
|
+
hiddenKnowledge: z.number().int().nonnegative(),
|
|
557
|
+
tools: z.array(ToolName),
|
|
558
|
+
});
|
|
559
|
+
// What stands between this flow and a run, asked on demand and answered for the person asking.
|
|
560
|
+
//
|
|
561
|
+
// ⚠️ A snapshot, and it says so. The tool catalog is a live query with the requesting user's own
|
|
562
|
+
// token (ADR-0003), so what is reachable now can be different tomorrow, and the same flow answers
|
|
563
|
+
// differently for two people. That is why this is a question one asks rather than a badge on the
|
|
564
|
+
// flow: a standing "this flow has conflicts" would be wrong for tools by construction (#72, D24).
|
|
565
|
+
export const FlowValidation = z.strictObject({
|
|
566
|
+
flowId: IntelId,
|
|
567
|
+
versionId: IntelId.nullable(),
|
|
568
|
+
// Empty means it would start now — for this person, at this moment.
|
|
569
|
+
problems: z.array(z.strictObject({
|
|
570
|
+
code: z.string().min(1).max(80),
|
|
571
|
+
detail: z.string().min(1).max(2_000),
|
|
572
|
+
})),
|
|
573
|
+
checkedAt: IsoDateTime,
|
|
574
|
+
});
|
|
379
575
|
export const CreateFlowInput = z.strictObject({
|
|
380
576
|
parentId: IntelId.nullable().default(null),
|
|
381
577
|
title: z.string().trim().min(1).max(240),
|
|
@@ -413,27 +609,80 @@ export const PublishFlowInput = z.strictObject({
|
|
|
413
609
|
versionId: IntelId,
|
|
414
610
|
idempotencyKey: z.string().min(8).max(200),
|
|
415
611
|
});
|
|
416
|
-
export const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
612
|
+
export const PreviewFlowPublishInput = z.strictObject({ flowId: IntelId, versionId: IntelId });
|
|
613
|
+
// One sub-flow call as publishing will leave it (ADR-0004 §5). `freezes` is the whole point of the
|
|
614
|
+
// preview: it marks the calls whose `latest` publishing turns into `versionId`, so the author reads
|
|
615
|
+
// the decision before making it rather than after.
|
|
616
|
+
//
|
|
617
|
+
// ⚠️ Only callees the asking actor may reach are listed at all. A call whose callee they cannot see
|
|
618
|
+
// is left out rather than named, because a title is the thing an unreachable flow must not hand out.
|
|
619
|
+
export const FlowPublishCall = z.strictObject({
|
|
620
|
+
nodeId: FlowNodeId,
|
|
621
|
+
nodeLabel: z.string().min(1).max(160),
|
|
622
|
+
calleeId: IntelId,
|
|
623
|
+
calleeTitle: z.string().min(1).max(240),
|
|
624
|
+
mode: SubflowVersionMode,
|
|
625
|
+
// The version this call will take once published. `null` when it follows the callee, which is the
|
|
626
|
+
// one case where the answer is only known at run time.
|
|
627
|
+
versionId: IntelId.nullable(),
|
|
628
|
+
versionSequence: z.number().int().positive().nullable(),
|
|
629
|
+
freezes: z.boolean(),
|
|
630
|
+
// The callee has a published version to be called at all. `false` is what publishing will refuse.
|
|
631
|
+
available: z.boolean(),
|
|
632
|
+
});
|
|
633
|
+
export const FlowPublishPreview = z.strictObject({
|
|
634
|
+
flowId: IntelId,
|
|
635
|
+
versionId: IntelId,
|
|
636
|
+
calls: z.array(FlowPublishCall),
|
|
637
|
+
});
|
|
638
|
+
// A flow has no share schema of its own. A grant sits on the folder a flow is filed in and inherits
|
|
639
|
+
// down from there (ADR-0004 §2); a narrower grant beside it would destroy the subtree guarantee
|
|
640
|
+
// section 3 rests on, so per-flow grants were removed rather than deprecated.
|
|
641
|
+
// What accesses what, for one level of the shared tree (#19). A folder answers it for its contents,
|
|
642
|
+
// a single flow for itself. Documents and flows are two kinds of thing that share one tree
|
|
643
|
+
// (ADR-0004 §1), so the graph carries both and says which of them it is.
|
|
644
|
+
export const RelationNodeKind = z.enum(["folder", "document", "attachment", "table", "flow"]);
|
|
645
|
+
export const RelationNode = z.strictObject({
|
|
646
|
+
id: IntelId,
|
|
647
|
+
kind: RelationNodeKind,
|
|
648
|
+
title: z.string().min(1).max(240),
|
|
649
|
+
// Inside the level being shown, rather than something it reaches out to. A flow reading a policy
|
|
650
|
+
// document from another folder pulls that document in, and the difference should be legible.
|
|
651
|
+
inScope: z.boolean(),
|
|
652
|
+
});
|
|
653
|
+
export const RelationEdge = z.strictObject({
|
|
654
|
+
id: z.string().min(1).max(400),
|
|
655
|
+
source: IntelId,
|
|
656
|
+
target: IntelId,
|
|
657
|
+
relation: z.enum(["reads", "calls"]),
|
|
658
|
+
});
|
|
659
|
+
export const RelationGraphScope = z.discriminatedUnion("of", [
|
|
660
|
+
z.strictObject({ of: z.literal("folder"), folderId: IntelId.nullable() }),
|
|
661
|
+
z.strictObject({ of: z.literal("flow"), flowId: IntelId }),
|
|
436
662
|
]);
|
|
663
|
+
export const RelationGraphInput = z.strictObject({
|
|
664
|
+
scope: RelationGraphScope,
|
|
665
|
+
// How much is drawn before the answer is summarized instead. A big folder has to stay usable, and
|
|
666
|
+
// the cut-off is reported rather than swallowed.
|
|
667
|
+
limit: z.number().int().min(1).max(300).default(60),
|
|
668
|
+
});
|
|
669
|
+
// ⚠️ Only what the asking user may see is in here. A node they may not reach is absent, not greyed
|
|
670
|
+
// out and not counted: an edge to a placeholder would already tell them the thing exists, which is
|
|
671
|
+
// the leak this schema has to make impossible to write by accident. `omitted` is about the size
|
|
672
|
+
// limit alone, never about permissions.
|
|
673
|
+
export const RelationGraph = z.strictObject({
|
|
674
|
+
scope: RelationGraphScope,
|
|
675
|
+
nodes: z.array(RelationNode),
|
|
676
|
+
edges: z.array(RelationEdge),
|
|
677
|
+
omitted: z.number().int().nonnegative(),
|
|
678
|
+
limit: z.number().int().positive(),
|
|
679
|
+
});
|
|
680
|
+
// ⚠️ No `waiting`. It was written for the approval node and never set by anything — the two places
|
|
681
|
+
// that tested for it only ever saw `running` (#73). The column's CHECK constraint still allows the
|
|
682
|
+
// value, deliberately: rewriting it means rebuilding the table in D1 for a value nothing writes,
|
|
683
|
+
// and migration 0007 turns any row that somehow carries it into `failed` rather than leave a status
|
|
684
|
+
// the contract cannot parse.
|
|
685
|
+
export const FlowRunStatus = z.enum(["queued", "running", "completed", "failed", "cancelled"]);
|
|
437
686
|
export const FlowRun = z.strictObject({
|
|
438
687
|
id: IntelId,
|
|
439
688
|
flowId: IntelId,
|
|
@@ -444,14 +693,36 @@ export const FlowRun = z.strictObject({
|
|
|
444
693
|
output: z.unknown().nullable(),
|
|
445
694
|
error: z.string().max(2_000).nullable(),
|
|
446
695
|
initiatedBy: IntelId,
|
|
696
|
+
// The subflow node this run was called from, and the run that node belongs to. A called run is a
|
|
697
|
+
// run of its own: it has its own version, its own steps and its own authorization, and only these
|
|
698
|
+
// two fields say where its result goes back to.
|
|
699
|
+
parentRunId: IntelId.nullable().default(null),
|
|
700
|
+
parentNodeId: FlowNodeId.nullable().default(null),
|
|
447
701
|
createdAt: IsoDateTime,
|
|
448
702
|
updatedAt: IsoDateTime,
|
|
449
703
|
completedAt: IsoDateTime.nullable(),
|
|
450
704
|
});
|
|
451
|
-
|
|
705
|
+
// Which step of which flow is running, outermost caller first. Readable rather than reconstructed
|
|
706
|
+
// from `parentRunId` by whoever is looking (#17).
|
|
707
|
+
export const FlowRunTrailEntry = z.strictObject({
|
|
708
|
+
runId: IntelId,
|
|
709
|
+
flowId: IntelId,
|
|
710
|
+
flowTitle: z.string().min(1).max(240),
|
|
711
|
+
nodeId: FlowNodeId.nullable(),
|
|
712
|
+
nodeLabel: z.string().max(160).nullable(),
|
|
713
|
+
});
|
|
714
|
+
export const FlowRunStep = z.strictObject({
|
|
715
|
+
run: FlowRun,
|
|
716
|
+
node: FlowNode.nullable(),
|
|
717
|
+
trail: z.array(FlowRunTrailEntry).default([]),
|
|
718
|
+
});
|
|
452
719
|
export const StartFlowRunInput = z.strictObject({
|
|
453
720
|
flowId: IntelId,
|
|
454
721
|
input: z.record(z.string(), z.unknown()).default({}),
|
|
722
|
+
// Present when this run is the call a subflow node makes. It names a place, never a permission:
|
|
723
|
+
// the callee's `execute` is asked of the user exactly as it is for a run they start themselves,
|
|
724
|
+
// and the parent run must be the caller's own and standing on that very node.
|
|
725
|
+
parent: z.strictObject({ runId: IntelId, nodeId: FlowNodeId }).nullable().default(null),
|
|
455
726
|
idempotencyKey: z.string().min(8).max(200),
|
|
456
727
|
});
|
|
457
728
|
export const GetFlowRunInput = z.strictObject({ runId: IntelId });
|
|
@@ -464,3 +735,81 @@ export const CompleteFlowRunStepInput = z.strictObject({
|
|
|
464
735
|
error: z.string().max(2_000).nullable().default(null),
|
|
465
736
|
idempotencyKey: z.string().min(8).max(200),
|
|
466
737
|
});
|
|
738
|
+
// Why a run started. Derived when it is read and deliberately not a column: `subflow` when the run
|
|
739
|
+
// is the call another run made, otherwise the mode of the trigger node in the immutable version the
|
|
740
|
+
// run took. A stored copy would be a second answer that could disagree with the graph that ran.
|
|
741
|
+
//
|
|
742
|
+
// ⚠️ `webhook` and `schedule` stood here until #39 took them out of the trigger node. Being derived
|
|
743
|
+
// rather than stored is exactly what makes that safe: no run carries a trigger of its own, so once
|
|
744
|
+
// the 0005 migration has rewritten every stored trigger to `manual`, there is nowhere left for the
|
|
745
|
+
// old values to come from. Had this been a column, the enum would have had to keep reading them or
|
|
746
|
+
// every old run would have failed to parse the moment somebody opened the list.
|
|
747
|
+
export const FlowRunTrigger = z.enum(["manual", "subflow"]);
|
|
748
|
+
// Which step ended a run, and why, in the words the failure already used (#20).
|
|
749
|
+
//
|
|
750
|
+
// ⚠️ A call that failed carries its reason in the *called* run, and that run is a run of its own
|
|
751
|
+
// with its own authorization. `calledRunId` is therefore filled only when the asking user may see
|
|
752
|
+
// that run through the very rule every other run answer uses; otherwise the failure is named by the
|
|
753
|
+
// calling step alone — the caller's own label — and `detail` says no more than that it did not
|
|
754
|
+
// finish. Naming a callee's step or document here would be the leak #17, #19 and #20 each closed.
|
|
755
|
+
export const FlowRunFailure = z.strictObject({
|
|
756
|
+
nodeId: FlowNodeId,
|
|
757
|
+
nodeLabel: z.string().max(160),
|
|
758
|
+
detail: z.string().max(2_000),
|
|
759
|
+
calledRunId: IntelId.nullable(),
|
|
760
|
+
});
|
|
761
|
+
// One run as a list shows it: what it did, never what it produced.
|
|
762
|
+
//
|
|
763
|
+
// ⚠️ Neither `input` nor `output` is in here, on purpose. A run reaches its Knowledge and its tools
|
|
764
|
+
// with the rights of whoever started it, so its result is a way to content the next reader of this
|
|
765
|
+
// list may have no claim to. Whoever wants a result asks for the run itself, where the same rule
|
|
766
|
+
// decides again.
|
|
767
|
+
export const FlowRunSummary = z.strictObject({
|
|
768
|
+
id: IntelId,
|
|
769
|
+
flowId: IntelId,
|
|
770
|
+
// The version the run took. Together with the run's stored input it is what a later "run this
|
|
771
|
+
// again with the old data" would need; replaying is a separate ticket, this only keeps it possible.
|
|
772
|
+
versionId: IntelId,
|
|
773
|
+
status: FlowRunStatus,
|
|
774
|
+
trigger: FlowRunTrigger,
|
|
775
|
+
startedAt: IsoDateTime,
|
|
776
|
+
completedAt: IsoDateTime.nullable(),
|
|
777
|
+
durationMs: z.number().int().nonnegative().nullable(),
|
|
778
|
+
initiatedBy: IntelId,
|
|
779
|
+
parentRunId: IntelId.nullable(),
|
|
780
|
+
failure: FlowRunFailure.nullable(),
|
|
781
|
+
});
|
|
782
|
+
// One filter and nothing else: "only the failed ones" is the question asked in almost every case,
|
|
783
|
+
// and every further facet is a report rather than a search for a fault.
|
|
784
|
+
export const ListFlowRunsInput = z.strictObject({
|
|
785
|
+
flowId: IntelId,
|
|
786
|
+
failedOnly: z.boolean().default(false),
|
|
787
|
+
limit: z.number().int().min(1).max(50).default(20),
|
|
788
|
+
// The `nextCursor` of the previous page. Keyset rather than an offset, because runs arrive while
|
|
789
|
+
// someone reads and an offset would skip or repeat rows exactly when a flow is busy.
|
|
790
|
+
cursor: z.string().min(1).max(400).nullable().default(null),
|
|
791
|
+
});
|
|
792
|
+
export const FlowRunList = z.strictObject({
|
|
793
|
+
items: z.array(FlowRunSummary),
|
|
794
|
+
nextCursor: z.string().max(400).nullable(),
|
|
795
|
+
});
|
|
796
|
+
// One completed step of one run. `detail` is the step's own error text; an output is absent for the
|
|
797
|
+
// same reason it is absent from the summary.
|
|
798
|
+
export const FlowRunStepRecord = z.strictObject({
|
|
799
|
+
nodeId: FlowNodeId,
|
|
800
|
+
nodeLabel: z.string().max(160),
|
|
801
|
+
outcome: z.enum(["completed", "failed"]),
|
|
802
|
+
branch: z.string().max(120).nullable(),
|
|
803
|
+
detail: z.string().max(2_000).nullable(),
|
|
804
|
+
calledRunId: IntelId.nullable(),
|
|
805
|
+
completedAt: IsoDateTime,
|
|
806
|
+
});
|
|
807
|
+
// What one run did, step by step, oldest first, with the call chain it belongs to (#17). The trail
|
|
808
|
+
// is what makes a nested run readable: which step of which flow this run is.
|
|
809
|
+
export const FlowRunHistory = z.strictObject({
|
|
810
|
+
runId: IntelId,
|
|
811
|
+
flowId: IntelId,
|
|
812
|
+
status: FlowRunStatus,
|
|
813
|
+
steps: z.array(FlowRunStepRecord),
|
|
814
|
+
trail: z.array(FlowRunTrailEntry),
|
|
815
|
+
});
|