@anchrd/intel-contract 0.2.1 → 0.2.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/contract/contract.d.ts +636 -159
- package/dist/contract/contract.js +344 -39
- package/package.json +1 -1
|
@@ -16,9 +16,14 @@ export const SessionUser = z.strictObject({
|
|
|
16
16
|
email: z.email(),
|
|
17
17
|
name: z.string().min(1).max(240).nullable(),
|
|
18
18
|
});
|
|
19
|
-
|
|
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"]);
|
|
20
23
|
export const ContextPolicy = z.enum(["pinned", "relevant", "explicit"]);
|
|
21
|
-
|
|
24
|
+
// One verb per grant, granted independently (ADR-0004 §2). Not a ladder: seeing a process must be
|
|
25
|
+
// separable from being allowed to start it, and `execute` is meaningful only where a flow can live.
|
|
26
|
+
export const ResourceVerb = z.enum(["read", "write", "execute", "share"]);
|
|
22
27
|
export const SharePrincipal = z.discriminatedUnion("type", [
|
|
23
28
|
z.strictObject({ type: z.literal("user"), id: IntelId }),
|
|
24
29
|
z.strictObject({ type: z.literal("email"), email: z.email() }),
|
|
@@ -108,32 +113,94 @@ export const KnowledgeAttachment = z.strictObject({
|
|
|
108
113
|
version: KnowledgeVersion,
|
|
109
114
|
resourceUri: z.string().regex(/^intel:\/\/knowledge\/[^/]+\/attachment$/),
|
|
110
115
|
});
|
|
116
|
+
// A table is CSV, and CSV is the whole format: it is what is stored, what is downloaded and what a
|
|
117
|
+
// machine reads. There is no second representation to keep in step with it (#40).
|
|
118
|
+
export const TableMediaType = "text/csv";
|
|
119
|
+
// A column name is the contract between the table and everyone who appends to it, so it is trimmed,
|
|
120
|
+
// non-empty and bounded like a title. Cells are not: a cell is text, and text is what CSV carries.
|
|
121
|
+
export const TableColumn = z.string().trim().min(1).max(120);
|
|
122
|
+
export const TableCell = z.string().max(4_000);
|
|
123
|
+
export const TableRow = z.array(TableCell).min(1).max(64);
|
|
124
|
+
// Writing the header, once. The columns are the contract (#40's comment), which is why this refuses
|
|
125
|
+
// on a table that already has one: changing the header would silently reinterpret every row that
|
|
126
|
+
// was appended under the old one.
|
|
127
|
+
export const DefineKnowledgeTableInput = z.strictObject({
|
|
128
|
+
nodeId: IntelId,
|
|
129
|
+
columns: z
|
|
130
|
+
.array(TableColumn)
|
|
131
|
+
.min(1)
|
|
132
|
+
.max(64)
|
|
133
|
+
.refine((columns) => new Set(columns.map((column) => column.toLowerCase())).size === columns.length, { error: "Column names must be distinct" }),
|
|
134
|
+
idempotencyKey: z.string().min(8).max(200),
|
|
135
|
+
});
|
|
136
|
+
// ⚠️ No `baseVersionId`, and that absence is the feature. A document replaces its content and needs
|
|
137
|
+
// to know which content it replaces; an append adds to the end and cannot collide with a second
|
|
138
|
+
// append, so demanding a base version would invent a conflict that does not exist and force the
|
|
139
|
+
// caller to read the whole table first — the exact cost #40 exists to remove.
|
|
140
|
+
export const AppendKnowledgeTableRowsInput = z.strictObject({
|
|
141
|
+
nodeId: IntelId,
|
|
142
|
+
rows: z.array(TableRow).min(1).max(1_000),
|
|
143
|
+
idempotencyKey: z.string().min(8).max(200),
|
|
144
|
+
});
|
|
145
|
+
export const GetKnowledgeTableInput = z.strictObject({ nodeId: IntelId });
|
|
146
|
+
// The table as a grid rather than as text: the server owns the one CSV reader, so no surface has to
|
|
147
|
+
// grow a second one that would disagree with it about quoting.
|
|
148
|
+
export const KnowledgeTable = z.strictObject({
|
|
149
|
+
node: KnowledgeNode,
|
|
150
|
+
columns: z.array(z.string()),
|
|
151
|
+
rows: z.array(z.array(z.string())),
|
|
152
|
+
// The newest append, or `null` while the table has no header yet.
|
|
153
|
+
versionId: IntelId.nullable(),
|
|
154
|
+
});
|
|
155
|
+
export const AppendKnowledgeTableRowsResult = z.strictObject({
|
|
156
|
+
node: KnowledgeNode,
|
|
157
|
+
version: KnowledgeVersion,
|
|
158
|
+
appended: z.number().int().positive(),
|
|
159
|
+
});
|
|
160
|
+
// ⚠️ Kept for what is already stored, not for what is written. Relations were picked in a dialog
|
|
161
|
+
// until #41; a link is now made where it is meant — in the text — and every link written from now
|
|
162
|
+
// on is a `references`. Rewriting the old rows would destroy a distinction somebody chose on
|
|
163
|
+
// purpose, and dropping the column would destroy it with them, so both stay readable.
|
|
111
164
|
export const KnowledgeLinkRelation = z.enum(["references", "related", "depends_on", "implements"]);
|
|
165
|
+
// Where the link came from. `text` links are derived from a document's content and are rewritten
|
|
166
|
+
// whenever it is saved; `manual` links were made in the dialog #41 removed and are now history.
|
|
167
|
+
//
|
|
168
|
+
// ⚠️ This is provenance, never a second sort of relationship. Nothing offers the reader a choice
|
|
169
|
+
// between them, and nothing may start writing `manual` again — that would be the two ways of saying
|
|
170
|
+
// one thing that #41 exists to end. It exists so that saving a document cannot delete a link
|
|
171
|
+
// somebody made before there was another way to make one.
|
|
172
|
+
export const KnowledgeLinkOrigin = z.enum(["text", "manual"]);
|
|
112
173
|
export const KnowledgeLink = z.strictObject({
|
|
113
174
|
id: IntelId,
|
|
114
175
|
sourceNodeId: IntelId,
|
|
115
176
|
targetNodeId: IntelId,
|
|
116
177
|
relation: KnowledgeLinkRelation,
|
|
178
|
+
origin: KnowledgeLinkOrigin,
|
|
117
179
|
label: z.string().trim().min(1).max(120).nullable(),
|
|
118
180
|
createdBy: IntelId,
|
|
119
181
|
createdAt: IsoDateTime,
|
|
120
182
|
});
|
|
121
183
|
export const KnowledgeLinkList = z.strictObject({ items: z.array(KnowledgeLink) });
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
184
|
+
// The inline element a document link is, inside a BlockNote document (#41).
|
|
185
|
+
//
|
|
186
|
+
// ⚠️ The ID and nothing else. No title and no path travel with it: a stored title would go stale
|
|
187
|
+
// the moment the target is renamed, a stored path the moment it is moved — and either one would
|
|
188
|
+
// put a name the reader may not see into a document they may.
|
|
189
|
+
export const DocumentLinkInlineType = "documentLink";
|
|
190
|
+
export const ResolveKnowledgeLinksInput = z.strictObject({
|
|
191
|
+
nodeIds: z.array(IntelId).min(1).max(200),
|
|
192
|
+
});
|
|
193
|
+
export const ResolvedKnowledgeLink = z.strictObject({
|
|
194
|
+
nodeId: IntelId,
|
|
195
|
+
title: z.string().min(1).max(240),
|
|
132
196
|
});
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
197
|
+
// ⚠️ Only what the asking reader may see is in here, and an unreachable target is simply absent —
|
|
198
|
+
// never a row with an empty title, never a count, never a "restricted" marker. The list is what one
|
|
199
|
+
// side of a document link is drawn from, and an entry that says "something is here" is exactly the
|
|
200
|
+
// leak this schema has to make impossible to write by accident. Deleted and unreadable therefore
|
|
201
|
+
// look identical from the outside, which is the point.
|
|
202
|
+
export const ResolveKnowledgeLinksResult = z.strictObject({
|
|
203
|
+
items: z.array(ResolvedKnowledgeLink),
|
|
137
204
|
});
|
|
138
205
|
export const KnowledgeGraphInput = z.strictObject({
|
|
139
206
|
limit: z.number().int().min(1).max(500).default(250),
|
|
@@ -153,7 +220,7 @@ export const ResourceGrant = z.strictObject({
|
|
|
153
220
|
id: IntelId,
|
|
154
221
|
resourceId: IntelId,
|
|
155
222
|
principal: SharePrincipal,
|
|
156
|
-
|
|
223
|
+
verb: ResourceVerb,
|
|
157
224
|
expiresAt: IsoDateTime.nullable(),
|
|
158
225
|
createdBy: IntelId,
|
|
159
226
|
createdAt: IsoDateTime,
|
|
@@ -161,7 +228,7 @@ export const ResourceGrant = z.strictObject({
|
|
|
161
228
|
export const ShareKnowledgeInput = z.strictObject({
|
|
162
229
|
resourceId: IntelId,
|
|
163
230
|
principal: SharePrincipal,
|
|
164
|
-
|
|
231
|
+
verb: ResourceVerb,
|
|
165
232
|
expiresAt: IsoDateTime.nullable().default(null),
|
|
166
233
|
idempotencyKey: z.string().min(8).max(200),
|
|
167
234
|
});
|
|
@@ -170,7 +237,30 @@ export const RevokeKnowledgeGrantInput = z.strictObject({
|
|
|
170
237
|
grantId: IntelId,
|
|
171
238
|
idempotencyKey: z.string().min(8).max(200),
|
|
172
239
|
});
|
|
173
|
-
|
|
240
|
+
// What a grant does not cover, reported to whoever just made it. A flow in the shared folder may
|
|
241
|
+
// read a document outside it, and the run is re-authorized against the person running it — so the
|
|
242
|
+
// grant can be complete and the flow still stop for them (ADR-0004 §4).
|
|
243
|
+
//
|
|
244
|
+
// ⚠️ `titles` holds only the documents the sharer may see; everything else is in `hidden` as a
|
|
245
|
+
// number. A warning must not become a way of reading titles out of the tree.
|
|
246
|
+
export const UnreadableKnowledge = z.strictObject({
|
|
247
|
+
titles: z.array(z.string().min(1).max(240)),
|
|
248
|
+
hidden: z.number().int().nonnegative(),
|
|
249
|
+
});
|
|
250
|
+
// The grant is in the answer, so the warning cannot be mistaken for a refusal: it is written first
|
|
251
|
+
// and described afterwards. Blocking would force everyone who uses a central policy document to
|
|
252
|
+
// duplicate it, which is the opposite of what one tree is for (ADR-0004 §4).
|
|
253
|
+
export const ShareKnowledgeResult = z.strictObject({
|
|
254
|
+
grant: ResourceGrant,
|
|
255
|
+
unreadable: UnreadableKnowledge,
|
|
256
|
+
});
|
|
257
|
+
// `applicableVerbs` travels with the list because the answer is the business layer's, not the
|
|
258
|
+
// screen's: a document has nothing to execute, so `execute` is not offered on one (ADR-0004 §2).
|
|
259
|
+
export const ResourceGrantList = z.strictObject({
|
|
260
|
+
resourceId: IntelId,
|
|
261
|
+
applicableVerbs: z.array(ResourceVerb).min(1),
|
|
262
|
+
items: z.array(ResourceGrant),
|
|
263
|
+
});
|
|
174
264
|
export const SearchKnowledgeInput = z.strictObject({
|
|
175
265
|
query: z.string().trim().min(1).max(500),
|
|
176
266
|
limit: z.number().int().min(1).max(50).default(10),
|
|
@@ -190,7 +280,6 @@ export const SearchKnowledgeResult = z.strictObject({
|
|
|
190
280
|
});
|
|
191
281
|
export const ReindexKnowledgeResult = z.strictObject({ queued: z.number().int().nonnegative() });
|
|
192
282
|
export const RevokeGrantResult = z.strictObject({ revoked: z.boolean() });
|
|
193
|
-
export const DeleteKnowledgeLinkResult = z.strictObject({ deleted: z.boolean() });
|
|
194
283
|
function isPrivateIpv4(hostname) {
|
|
195
284
|
const parts = hostname.split(".").map(Number);
|
|
196
285
|
if (parts.length !== 4 ||
|
|
@@ -272,21 +361,42 @@ export const ToolTestResult = z.strictObject({
|
|
|
272
361
|
});
|
|
273
362
|
export const FlowNodeId = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/);
|
|
274
363
|
export const FlowPosition = z.strictObject({ x: z.number().finite(), y: z.number().finite() });
|
|
364
|
+
// A node has a title and nothing else to write in prose (#39). A second free text beside it was
|
|
365
|
+
// kept half up to date on both sides, and for an instruction the same sentence already belongs in
|
|
366
|
+
// the instruction itself.
|
|
275
367
|
const FlowNodeBase = {
|
|
276
368
|
id: FlowNodeId,
|
|
277
369
|
position: FlowPosition,
|
|
278
370
|
label: z.string().trim().min(1).max(160),
|
|
279
|
-
description: z.string().trim().max(2_000).nullable().default(null),
|
|
280
371
|
};
|
|
372
|
+
// Which version of the callee a sub-flow call takes (ADR-0004 §5). Three states rather than an
|
|
373
|
+
// optional identifier, because the difference between them is a decision and has to be readable:
|
|
374
|
+
//
|
|
375
|
+
// - `latest` the draft default. Nobody should have to version things while building, and
|
|
376
|
+
// publishing turns this into `pinned` — visibly, before the author publishes.
|
|
377
|
+
// - `follows` "always latest", chosen on purpose. The call rides along with the callee, so a
|
|
378
|
+
// change to the building block changes this flow too. Publishing leaves it alone.
|
|
379
|
+
// - `pinned` one immutable version, whatever is published elsewhere.
|
|
380
|
+
//
|
|
381
|
+
// ⚠️ Without the freeze a change to a building block would silently change the behavior of every
|
|
382
|
+
// published flow using it, which contradicts the immutable versions and pinned schema fingerprints
|
|
383
|
+
// Intel otherwise guarantees. That is why `latest` cannot survive publishing.
|
|
384
|
+
export const SubflowVersionMode = z.enum(["latest", "follows", "pinned"]);
|
|
385
|
+
export const SubflowVersion = z.discriminatedUnion("mode", [
|
|
386
|
+
z.strictObject({ mode: z.literal("latest") }),
|
|
387
|
+
z.strictObject({ mode: z.literal("follows") }),
|
|
388
|
+
z.strictObject({ mode: z.literal("pinned"), versionId: IntelId }),
|
|
389
|
+
]);
|
|
281
390
|
export const FlowNode = z.discriminatedUnion("kind", [
|
|
282
391
|
z.strictObject({
|
|
283
392
|
...FlowNodeBase,
|
|
284
393
|
kind: z.literal("trigger"),
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
394
|
+
// ⚠️ `manual` is the only mode there is. `webhook` and `schedule` stood here and fired nothing:
|
|
395
|
+
// a flow is carried out by an external agent that brings its own schedule, which is what the
|
|
396
|
+
// code does rather than what it intends — `step()` hands back the current node, `completeStep`
|
|
397
|
+
// takes the result from outside, and the Cloudflare workflow waits rather than drives. #32 was
|
|
398
|
+
// closed on that basis, and a mode nothing triggers is a promise nobody keeps.
|
|
399
|
+
configuration: z.strictObject({ mode: z.literal("manual") }),
|
|
290
400
|
}),
|
|
291
401
|
z.strictObject({
|
|
292
402
|
...FlowNodeBase,
|
|
@@ -331,16 +441,37 @@ export const FlowNode = z.discriminatedUnion("kind", [
|
|
|
331
441
|
timeout: z.string().regex(/^\d+\s+(minute|minutes|hour|hours|day|days)$/),
|
|
332
442
|
}),
|
|
333
443
|
}),
|
|
444
|
+
// The eighth kind: one flow calls another (ADR-0004 §3). A schema addition, not hidden behavior in
|
|
445
|
+
// a generic code node — which flow is called has to be readable from the graph, or neither the
|
|
446
|
+
// publish-time call rule nor the sidebar could see it.
|
|
447
|
+
z.strictObject({
|
|
448
|
+
...FlowNodeBase,
|
|
449
|
+
kind: z.literal("subflow"),
|
|
450
|
+
// What the called flow is given travels through `StartFlowRunInput.input` — the schema every run
|
|
451
|
+
// already uses. A second, static input here would be a promise the execution does not keep.
|
|
452
|
+
configuration: z.strictObject({
|
|
453
|
+
flowId: IntelId,
|
|
454
|
+
version: SubflowVersion.default({ mode: "latest" }),
|
|
455
|
+
}),
|
|
456
|
+
}),
|
|
334
457
|
z.strictObject({
|
|
335
458
|
...FlowNodeBase,
|
|
336
459
|
kind: z.literal("output"),
|
|
337
460
|
configuration: z.strictObject({ template: z.string().max(50_000).default("") }),
|
|
338
461
|
}),
|
|
339
462
|
]);
|
|
463
|
+
// The two things an edge can mean (#37). `flow` is the order of work — "and then". `context` is what
|
|
464
|
+
// a step works with: a document consulted in exactly this step, an approval obtained in exactly this
|
|
465
|
+
// step, or, at the output, the table a result is written to.
|
|
466
|
+
//
|
|
467
|
+
// ⚠️ `flow` is the default, and that is the whole of the migration: every edge stored before this
|
|
468
|
+
// existed parses into the meaning it already had. Nothing about saved graphs has to be rewritten.
|
|
469
|
+
export const FlowEdgeKind = z.enum(["flow", "context"]);
|
|
340
470
|
export const FlowEdge = z.strictObject({
|
|
341
471
|
id: FlowNodeId,
|
|
342
472
|
source: FlowNodeId,
|
|
343
473
|
target: FlowNodeId,
|
|
474
|
+
kind: FlowEdgeKind.default("flow"),
|
|
344
475
|
label: z.string().trim().min(1).max(120).nullable().default(null),
|
|
345
476
|
sourceHandle: z.string().trim().min(1).max(120).nullable().default(null),
|
|
346
477
|
});
|
|
@@ -376,6 +507,25 @@ export const FlowDocument = z.strictObject({
|
|
|
376
507
|
version: FlowVersion.nullable(),
|
|
377
508
|
});
|
|
378
509
|
export const FlowList = z.strictObject({ items: z.array(Flow) });
|
|
510
|
+
export const FlowKnowledgeReference = z.strictObject({
|
|
511
|
+
id: IntelId,
|
|
512
|
+
title: z.string().min(1).max(240),
|
|
513
|
+
});
|
|
514
|
+
// What a flow touches: the documents its Knowledge steps name and the tools its Tool steps call,
|
|
515
|
+
// read straight out of the graph. Deliberately not a conflict report — there is no arithmetic here
|
|
516
|
+
// and nothing that can go stale, because the graph is the answer. Whether a given person may reach
|
|
517
|
+
// any of it is decided where it can be decided honestly: when the folder is shared, and at runtime
|
|
518
|
+
// (ADR-0004 §4). For tools it can only ever be the latter, because the catalog is a live query with
|
|
519
|
+
// the requesting user's own token (ADR-0003).
|
|
520
|
+
//
|
|
521
|
+
// ⚠️ `knowledge` names only what the asking user may see. The rest is `hiddenKnowledge`, a count.
|
|
522
|
+
export const FlowRequirements = z.strictObject({
|
|
523
|
+
flowId: IntelId,
|
|
524
|
+
versionId: IntelId.nullable(),
|
|
525
|
+
knowledge: z.array(FlowKnowledgeReference),
|
|
526
|
+
hiddenKnowledge: z.number().int().nonnegative(),
|
|
527
|
+
tools: z.array(ToolName),
|
|
528
|
+
});
|
|
379
529
|
export const CreateFlowInput = z.strictObject({
|
|
380
530
|
parentId: IntelId.nullable().default(null),
|
|
381
531
|
title: z.string().trim().min(1).max(240),
|
|
@@ -413,18 +563,73 @@ export const PublishFlowInput = z.strictObject({
|
|
|
413
563
|
versionId: IntelId,
|
|
414
564
|
idempotencyKey: z.string().min(8).max(200),
|
|
415
565
|
});
|
|
416
|
-
export const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
566
|
+
export const PreviewFlowPublishInput = z.strictObject({ flowId: IntelId, versionId: IntelId });
|
|
567
|
+
// One sub-flow call as publishing will leave it (ADR-0004 §5). `freezes` is the whole point of the
|
|
568
|
+
// preview: it marks the calls whose `latest` publishing turns into `versionId`, so the author reads
|
|
569
|
+
// the decision before making it rather than after.
|
|
570
|
+
//
|
|
571
|
+
// ⚠️ Only callees the asking actor may reach are listed at all. A call whose callee they cannot see
|
|
572
|
+
// is left out rather than named, because a title is the thing an unreachable flow must not hand out.
|
|
573
|
+
export const FlowPublishCall = z.strictObject({
|
|
574
|
+
nodeId: FlowNodeId,
|
|
575
|
+
nodeLabel: z.string().min(1).max(160),
|
|
576
|
+
calleeId: IntelId,
|
|
577
|
+
calleeTitle: z.string().min(1).max(240),
|
|
578
|
+
mode: SubflowVersionMode,
|
|
579
|
+
// The version this call will take once published. `null` when it follows the callee, which is the
|
|
580
|
+
// one case where the answer is only known at run time.
|
|
581
|
+
versionId: IntelId.nullable(),
|
|
582
|
+
versionSequence: z.number().int().positive().nullable(),
|
|
583
|
+
freezes: z.boolean(),
|
|
584
|
+
// The callee has a published version to be called at all. `false` is what publishing will refuse.
|
|
585
|
+
available: z.boolean(),
|
|
586
|
+
});
|
|
587
|
+
export const FlowPublishPreview = z.strictObject({
|
|
588
|
+
flowId: IntelId,
|
|
589
|
+
versionId: IntelId,
|
|
590
|
+
calls: z.array(FlowPublishCall),
|
|
591
|
+
});
|
|
592
|
+
// A flow has no share schema of its own. A grant sits on the folder a flow is filed in and inherits
|
|
593
|
+
// down from there (ADR-0004 §2); a narrower grant beside it would destroy the subtree guarantee
|
|
594
|
+
// section 3 rests on, so per-flow grants were removed rather than deprecated.
|
|
595
|
+
// What accesses what, for one level of the shared tree (#19). A folder answers it for its contents,
|
|
596
|
+
// a single flow for itself. Documents and flows are two kinds of thing that share one tree
|
|
597
|
+
// (ADR-0004 §1), so the graph carries both and says which of them it is.
|
|
598
|
+
export const RelationNodeKind = z.enum(["folder", "document", "attachment", "table", "flow"]);
|
|
599
|
+
export const RelationNode = z.strictObject({
|
|
600
|
+
id: IntelId,
|
|
601
|
+
kind: RelationNodeKind,
|
|
602
|
+
title: z.string().min(1).max(240),
|
|
603
|
+
// Inside the level being shown, rather than something it reaches out to. A flow reading a policy
|
|
604
|
+
// document from another folder pulls that document in, and the difference should be legible.
|
|
605
|
+
inScope: z.boolean(),
|
|
606
|
+
});
|
|
607
|
+
export const RelationEdge = z.strictObject({
|
|
608
|
+
id: z.string().min(1).max(400),
|
|
609
|
+
source: IntelId,
|
|
610
|
+
target: IntelId,
|
|
611
|
+
relation: z.enum(["reads", "calls"]),
|
|
612
|
+
});
|
|
613
|
+
export const RelationGraphScope = z.discriminatedUnion("of", [
|
|
614
|
+
z.strictObject({ of: z.literal("folder"), folderId: IntelId.nullable() }),
|
|
615
|
+
z.strictObject({ of: z.literal("flow"), flowId: IntelId }),
|
|
616
|
+
]);
|
|
617
|
+
export const RelationGraphInput = z.strictObject({
|
|
618
|
+
scope: RelationGraphScope,
|
|
619
|
+
// How much is drawn before the answer is summarized instead. A big folder has to stay usable, and
|
|
620
|
+
// the cut-off is reported rather than swallowed.
|
|
621
|
+
limit: z.number().int().min(1).max(300).default(60),
|
|
622
|
+
});
|
|
623
|
+
// ⚠️ Only what the asking user may see is in here. A node they may not reach is absent, not greyed
|
|
624
|
+
// out and not counted: an edge to a placeholder would already tell them the thing exists, which is
|
|
625
|
+
// the leak this schema has to make impossible to write by accident. `omitted` is about the size
|
|
626
|
+
// limit alone, never about permissions.
|
|
627
|
+
export const RelationGraph = z.strictObject({
|
|
628
|
+
scope: RelationGraphScope,
|
|
629
|
+
nodes: z.array(RelationNode),
|
|
630
|
+
edges: z.array(RelationEdge),
|
|
631
|
+
omitted: z.number().int().nonnegative(),
|
|
632
|
+
limit: z.number().int().positive(),
|
|
428
633
|
});
|
|
429
634
|
export const FlowRunStatus = z.enum([
|
|
430
635
|
"queued",
|
|
@@ -444,14 +649,36 @@ export const FlowRun = z.strictObject({
|
|
|
444
649
|
output: z.unknown().nullable(),
|
|
445
650
|
error: z.string().max(2_000).nullable(),
|
|
446
651
|
initiatedBy: IntelId,
|
|
652
|
+
// The subflow node this run was called from, and the run that node belongs to. A called run is a
|
|
653
|
+
// run of its own: it has its own version, its own steps and its own authorization, and only these
|
|
654
|
+
// two fields say where its result goes back to.
|
|
655
|
+
parentRunId: IntelId.nullable().default(null),
|
|
656
|
+
parentNodeId: FlowNodeId.nullable().default(null),
|
|
447
657
|
createdAt: IsoDateTime,
|
|
448
658
|
updatedAt: IsoDateTime,
|
|
449
659
|
completedAt: IsoDateTime.nullable(),
|
|
450
660
|
});
|
|
451
|
-
|
|
661
|
+
// Which step of which flow is running, outermost caller first. Readable rather than reconstructed
|
|
662
|
+
// from `parentRunId` by whoever is looking (#17).
|
|
663
|
+
export const FlowRunTrailEntry = z.strictObject({
|
|
664
|
+
runId: IntelId,
|
|
665
|
+
flowId: IntelId,
|
|
666
|
+
flowTitle: z.string().min(1).max(240),
|
|
667
|
+
nodeId: FlowNodeId.nullable(),
|
|
668
|
+
nodeLabel: z.string().max(160).nullable(),
|
|
669
|
+
});
|
|
670
|
+
export const FlowRunStep = z.strictObject({
|
|
671
|
+
run: FlowRun,
|
|
672
|
+
node: FlowNode.nullable(),
|
|
673
|
+
trail: z.array(FlowRunTrailEntry).default([]),
|
|
674
|
+
});
|
|
452
675
|
export const StartFlowRunInput = z.strictObject({
|
|
453
676
|
flowId: IntelId,
|
|
454
677
|
input: z.record(z.string(), z.unknown()).default({}),
|
|
678
|
+
// Present when this run is the call a subflow node makes. It names a place, never a permission:
|
|
679
|
+
// the callee's `execute` is asked of the user exactly as it is for a run they start themselves,
|
|
680
|
+
// and the parent run must be the caller's own and standing on that very node.
|
|
681
|
+
parent: z.strictObject({ runId: IntelId, nodeId: FlowNodeId }).nullable().default(null),
|
|
455
682
|
idempotencyKey: z.string().min(8).max(200),
|
|
456
683
|
});
|
|
457
684
|
export const GetFlowRunInput = z.strictObject({ runId: IntelId });
|
|
@@ -464,3 +691,81 @@ export const CompleteFlowRunStepInput = z.strictObject({
|
|
|
464
691
|
error: z.string().max(2_000).nullable().default(null),
|
|
465
692
|
idempotencyKey: z.string().min(8).max(200),
|
|
466
693
|
});
|
|
694
|
+
// Why a run started. Derived when it is read and deliberately not a column: `subflow` when the run
|
|
695
|
+
// is the call another run made, otherwise the mode of the trigger node in the immutable version the
|
|
696
|
+
// run took. A stored copy would be a second answer that could disagree with the graph that ran.
|
|
697
|
+
//
|
|
698
|
+
// ⚠️ `webhook` and `schedule` stood here until #39 took them out of the trigger node. Being derived
|
|
699
|
+
// rather than stored is exactly what makes that safe: no run carries a trigger of its own, so once
|
|
700
|
+
// the 0005 migration has rewritten every stored trigger to `manual`, there is nowhere left for the
|
|
701
|
+
// old values to come from. Had this been a column, the enum would have had to keep reading them or
|
|
702
|
+
// every old run would have failed to parse the moment somebody opened the list.
|
|
703
|
+
export const FlowRunTrigger = z.enum(["manual", "subflow"]);
|
|
704
|
+
// Which step ended a run, and why, in the words the failure already used (#20).
|
|
705
|
+
//
|
|
706
|
+
// ⚠️ A call that failed carries its reason in the *called* run, and that run is a run of its own
|
|
707
|
+
// with its own authorization. `calledRunId` is therefore filled only when the asking user may see
|
|
708
|
+
// that run through the very rule every other run answer uses; otherwise the failure is named by the
|
|
709
|
+
// calling step alone — the caller's own label — and `detail` says no more than that it did not
|
|
710
|
+
// finish. Naming a callee's step or document here would be the leak #17, #19 and #20 each closed.
|
|
711
|
+
export const FlowRunFailure = z.strictObject({
|
|
712
|
+
nodeId: FlowNodeId,
|
|
713
|
+
nodeLabel: z.string().max(160),
|
|
714
|
+
detail: z.string().max(2_000),
|
|
715
|
+
calledRunId: IntelId.nullable(),
|
|
716
|
+
});
|
|
717
|
+
// One run as a list shows it: what it did, never what it produced.
|
|
718
|
+
//
|
|
719
|
+
// ⚠️ Neither `input` nor `output` is in here, on purpose. A run reaches its Knowledge and its tools
|
|
720
|
+
// with the rights of whoever started it, so its result is a way to content the next reader of this
|
|
721
|
+
// list may have no claim to. Whoever wants a result asks for the run itself, where the same rule
|
|
722
|
+
// decides again.
|
|
723
|
+
export const FlowRunSummary = z.strictObject({
|
|
724
|
+
id: IntelId,
|
|
725
|
+
flowId: IntelId,
|
|
726
|
+
// The version the run took. Together with the run's stored input it is what a later "run this
|
|
727
|
+
// again with the old data" would need; replaying is a separate ticket, this only keeps it possible.
|
|
728
|
+
versionId: IntelId,
|
|
729
|
+
status: FlowRunStatus,
|
|
730
|
+
trigger: FlowRunTrigger,
|
|
731
|
+
startedAt: IsoDateTime,
|
|
732
|
+
completedAt: IsoDateTime.nullable(),
|
|
733
|
+
durationMs: z.number().int().nonnegative().nullable(),
|
|
734
|
+
initiatedBy: IntelId,
|
|
735
|
+
parentRunId: IntelId.nullable(),
|
|
736
|
+
failure: FlowRunFailure.nullable(),
|
|
737
|
+
});
|
|
738
|
+
// One filter and nothing else: "only the failed ones" is the question asked in almost every case,
|
|
739
|
+
// and every further facet is a report rather than a search for a fault.
|
|
740
|
+
export const ListFlowRunsInput = z.strictObject({
|
|
741
|
+
flowId: IntelId,
|
|
742
|
+
failedOnly: z.boolean().default(false),
|
|
743
|
+
limit: z.number().int().min(1).max(50).default(20),
|
|
744
|
+
// The `nextCursor` of the previous page. Keyset rather than an offset, because runs arrive while
|
|
745
|
+
// someone reads and an offset would skip or repeat rows exactly when a flow is busy.
|
|
746
|
+
cursor: z.string().min(1).max(400).nullable().default(null),
|
|
747
|
+
});
|
|
748
|
+
export const FlowRunList = z.strictObject({
|
|
749
|
+
items: z.array(FlowRunSummary),
|
|
750
|
+
nextCursor: z.string().max(400).nullable(),
|
|
751
|
+
});
|
|
752
|
+
// One completed step of one run. `detail` is the step's own error text; an output is absent for the
|
|
753
|
+
// same reason it is absent from the summary.
|
|
754
|
+
export const FlowRunStepRecord = z.strictObject({
|
|
755
|
+
nodeId: FlowNodeId,
|
|
756
|
+
nodeLabel: z.string().max(160),
|
|
757
|
+
outcome: z.enum(["completed", "failed"]),
|
|
758
|
+
branch: z.string().max(120).nullable(),
|
|
759
|
+
detail: z.string().max(2_000).nullable(),
|
|
760
|
+
calledRunId: IntelId.nullable(),
|
|
761
|
+
completedAt: IsoDateTime,
|
|
762
|
+
});
|
|
763
|
+
// What one run did, step by step, oldest first, with the call chain it belongs to (#17). The trail
|
|
764
|
+
// is what makes a nested run readable: which step of which flow this run is.
|
|
765
|
+
export const FlowRunHistory = z.strictObject({
|
|
766
|
+
runId: IntelId,
|
|
767
|
+
flowId: IntelId,
|
|
768
|
+
status: FlowRunStatus,
|
|
769
|
+
steps: z.array(FlowRunStepRecord),
|
|
770
|
+
trail: z.array(FlowRunTrailEntry),
|
|
771
|
+
});
|