@hraness/kb 0.17.1
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/DISCLOSURE +43 -0
- package/LICENSE +21 -0
- package/README.md +545 -0
- package/dist/agent-context.js +35 -0
- package/dist/agent-guide-audit.js +18 -0
- package/dist/attachments.js +19 -0
- package/dist/authoring.js +33 -0
- package/dist/benchmark.js +19 -0
- package/dist/capture.js +49 -0
- package/dist/cli.js +3646 -0
- package/dist/clip/acquire.js +57 -0
- package/dist/clip/args.js +21 -0
- package/dist/clip/bounded-byte-buffer.js +9 -0
- package/dist/clip/browser-profiles.js +17 -0
- package/dist/clip/bundle-reader.js +11 -0
- package/dist/clip/cli.js +27 -0
- package/dist/clip/cookies.js +21 -0
- package/dist/clip/defuddle-worker.js +84 -0
- package/dist/clip/doctor.js +32 -0
- package/dist/clip/jobs.js +784 -0
- package/dist/clip/network-proxy.js +9 -0
- package/dist/clip/network.js +28 -0
- package/dist/clip/persist.js +30 -0
- package/dist/clip/refresh.js +11 -0
- package/dist/clip/terminal.js +11 -0
- package/dist/evaluation-builder.js +12238 -0
- package/dist/evaluation-kb.js +26 -0
- package/dist/evaluation.js +35 -0
- package/dist/git.js +39 -0
- package/dist/graph.js +39 -0
- package/dist/index-01jj6rbv.js +1103 -0
- package/dist/index-06c9ctr6.js +738 -0
- package/dist/index-0kavxzqj.js +2480 -0
- package/dist/index-1gwbassd.js +743 -0
- package/dist/index-1n418kb9.js +827 -0
- package/dist/index-1vrd1rmn.js +56 -0
- package/dist/index-1xxnjn0d.js +171 -0
- package/dist/index-3rm7cz6h.js +373 -0
- package/dist/index-3v2z4f0q.js +328 -0
- package/dist/index-48pz4jpc.js +392 -0
- package/dist/index-4j3tt0c3.js +234 -0
- package/dist/index-5n05se68.js +307 -0
- package/dist/index-5vwpzb5a.js +764 -0
- package/dist/index-6g2pv9d2.js +370 -0
- package/dist/index-7fzc32gf.js +712 -0
- package/dist/index-84x0vjjp.js +432 -0
- package/dist/index-adx6khj5.js +488 -0
- package/dist/index-b88v3vtm.js +805 -0
- package/dist/index-cv6fh7z5.js +462 -0
- package/dist/index-cxfrakt7.js +1067 -0
- package/dist/index-d13v9ckt.js +150 -0
- package/dist/index-dycjbhw2.js +1943 -0
- package/dist/index-dyqwejk5.js +531 -0
- package/dist/index-e5fbsywq.js +658 -0
- package/dist/index-f984hw45.js +927 -0
- package/dist/index-gh719d91.js +63 -0
- package/dist/index-hgve9rh2.js +2046 -0
- package/dist/index-hya40gb2.js +474 -0
- package/dist/index-j4zgmzjr.js +152 -0
- package/dist/index-jsmvyyvf.js +1166 -0
- package/dist/index-kh0pwvdd.js +1974 -0
- package/dist/index-mqx4nd6v.js +119 -0
- package/dist/index-mxxxytys.js +1254 -0
- package/dist/index-n5dd7r0v.js +711 -0
- package/dist/index-npg9z1a4.js +431 -0
- package/dist/index-pj501bh1.js +140 -0
- package/dist/index-qry4vhxk.js +1041 -0
- package/dist/index-s2gw5aw9.js +199 -0
- package/dist/index-vvdj6kca.js +70 -0
- package/dist/index-w2zc0vwa.js +571 -0
- package/dist/index-x3fthpsc.js +585 -0
- package/dist/index-zr53sf63.js +44 -0
- package/dist/index-zxdy5pby.js +2728 -0
- package/dist/index-zzhgcwyt.js +966 -0
- package/dist/index.js +453 -0
- package/dist/navigation.js +13 -0
- package/dist/pdf.js +85 -0
- package/dist/percolate.js +24 -0
- package/dist/portfolio.js +77 -0
- package/dist/query.js +29 -0
- package/dist/repository-memory.js +60 -0
- package/dist/sdk.js +36 -0
- package/dist/search-rules.js +34 -0
- package/dist/search.js +22 -0
- package/dist/semantic.js +39 -0
- package/dist/source-inbox.js +17 -0
- package/dist/untrusted-content.js +32 -0
- package/dist/url-intelligence.js +74 -0
- package/dist/workflow.js +23 -0
- package/dist/workflows/decision-context.js +20 -0
- package/dist/workflows/explain-change.js +8 -0
- package/dist/workflows/index.js +28 -0
- package/dist/workflows/plan-radar.js +10 -0
- package/package.json +409 -0
- package/skills/kb/AGENTS.md +19 -0
- package/skills/kb/SKILL.md +82 -0
- package/skills/kb/agents/openai.yaml +4 -0
- package/skills/kb/references/pdf-review.md +51 -0
- package/skills/kb/references/percolate.md +144 -0
- package/skills/kb/references/plan-structure.md +105 -0
- package/skills/kb/references/plan.md +115 -0
- package/skills/kb/references/query.md +231 -0
- package/skills/kb/references/refresh.md +133 -0
- package/skills/kb/references/save-pdf.md +125 -0
- package/skills/kb/references/save-url.md +178 -0
- package/skills/kb/references/url-authentication.md +75 -0
- package/skills/kb/references/url-platforms.md +25 -0
- package/src/agent-context.ts +1241 -0
- package/src/agent-guide-audit.ts +750 -0
- package/src/attachments.ts +723 -0
- package/src/authoring.ts +1725 -0
- package/src/benchmark.ts +362 -0
- package/src/capture.ts +68 -0
- package/src/cli.ts +3660 -0
- package/src/clip/acquire.ts +1273 -0
- package/src/clip/archive-today.ts +402 -0
- package/src/clip/args.ts +423 -0
- package/src/clip/assets.ts +262 -0
- package/src/clip/bounded-byte-buffer.ts +75 -0
- package/src/clip/browser-profiles.ts +372 -0
- package/src/clip/bundle-reader.ts +608 -0
- package/src/clip/capture.ts +1054 -0
- package/src/clip/cli.ts +181 -0
- package/src/clip/cookies.ts +512 -0
- package/src/clip/doctor.ts +1110 -0
- package/src/clip/extract.ts +868 -0
- package/src/clip/jobs.ts +959 -0
- package/src/clip/lib.ts +861 -0
- package/src/clip/media.ts +1474 -0
- package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
- package/src/clip/metadata-search-tool/Cargo.toml +18 -0
- package/src/clip/metadata-search-tool/runner.ts +685 -0
- package/src/clip/metadata-search-tool/src/main.rs +389 -0
- package/src/clip/metadata-search.ts +687 -0
- package/src/clip/network-proxy.ts +627 -0
- package/src/clip/network.ts +950 -0
- package/src/clip/package-root.ts +54 -0
- package/src/clip/persist.ts +1472 -0
- package/src/clip/platforms.ts +1824 -0
- package/src/clip/refresh.ts +197 -0
- package/src/clip/structured.ts +432 -0
- package/src/clip/terminal.ts +197 -0
- package/src/clip/url-intelligence.ts +581 -0
- package/src/clip/url-metadata-backfill.ts +616 -0
- package/src/clip/url-metadata-cli.ts +251 -0
- package/src/clip/url-metadata.ts +1397 -0
- package/src/evaluation-analysis-v2.ts +1440 -0
- package/src/evaluation-builder.ts +2539 -0
- package/src/evaluation-corpus-authoring.ts +2052 -0
- package/src/evaluation-evidence.ts +1147 -0
- package/src/evaluation-execution-v2.ts +426 -0
- package/src/evaluation-implementation.ts +131 -0
- package/src/evaluation-kb-closure.ts +1430 -0
- package/src/evaluation-kb-runner-v2.ts +735 -0
- package/src/evaluation-kb-v2.ts +2567 -0
- package/src/evaluation-kb.ts +1018 -0
- package/src/evaluation-measurement-v2.ts +1462 -0
- package/src/evaluation-packing-v2.ts +452 -0
- package/src/evaluation-redaction.ts +28 -0
- package/src/evaluation-v2.ts +3857 -0
- package/src/evaluation.ts +1220 -0
- package/src/git.ts +1276 -0
- package/src/graph.ts +1487 -0
- package/src/index.ts +20 -0
- package/src/init.ts +123 -0
- package/src/navigation.ts +277 -0
- package/src/note-lock.ts +529 -0
- package/src/pdf/args.ts +202 -0
- package/src/pdf/capture.ts +298 -0
- package/src/pdf/cli.ts +159 -0
- package/src/pdf/extract.ts +303 -0
- package/src/pdf/layout.ts +618 -0
- package/src/pdf/markdown.ts +154 -0
- package/src/pdf/model.ts +234 -0
- package/src/pdf/ocr.ts +190 -0
- package/src/pdf/persist.ts +314 -0
- package/src/pdf/source.ts +135 -0
- package/src/pdf/tools.ts +146 -0
- package/src/pdf.ts +92 -0
- package/src/percolate.ts +877 -0
- package/src/portfolio-audit.ts +574 -0
- package/src/portfolio-identity.ts +162 -0
- package/src/portfolio-registry.ts +473 -0
- package/src/portfolio.ts +765 -0
- package/src/query.ts +646 -0
- package/src/repository-memory.ts +1150 -0
- package/src/sdk.ts +1452 -0
- package/src/search-rules.ts +712 -0
- package/src/search.ts +710 -0
- package/src/semantic-runtime.ts +1139 -0
- package/src/semantic.ts +2480 -0
- package/src/source-inbox.ts +232 -0
- package/src/untrusted-content.ts +401 -0
- package/src/url-intelligence.ts +104 -0
- package/src/vault.ts +636 -0
- package/src/workflow.ts +653 -0
- package/src/workflows/decision-context.ts +81 -0
- package/src/workflows/explain-change.ts +73 -0
- package/src/workflows/index.ts +3 -0
- package/src/workflows/plan-radar.ts +101 -0
package/src/workflow.ts
ADDED
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
import { serialize } from "node:v8";
|
|
2
|
+
|
|
3
|
+
import type { KnowledgeBaseSession } from "./sdk.js";
|
|
4
|
+
|
|
5
|
+
export const MAX_WORKFLOW_NODES = 64;
|
|
6
|
+
export const MAX_WORKFLOW_CONCURRENCY = 8;
|
|
7
|
+
export const MAX_GIT_WORKFLOW_CONCURRENCY = 4;
|
|
8
|
+
export const DEFAULT_WORKFLOW_OUTPUT_BYTES = 16 * 1_024 * 1_024;
|
|
9
|
+
export const MAX_WORKFLOW_OUTPUT_BYTES = 64 * 1_024 * 1_024;
|
|
10
|
+
|
|
11
|
+
const workflowIdPattern = /^[a-z][a-z0-9-]{0,63}$/u;
|
|
12
|
+
|
|
13
|
+
export type WorkflowResource = "default" | "git" | "qmd";
|
|
14
|
+
|
|
15
|
+
type DefaultWorkflowResults = Readonly<Record<string, unknown>>;
|
|
16
|
+
type WorkflowResultId<Results extends object> = Extract<keyof Results, string>;
|
|
17
|
+
type WorkflowResultValue<Results extends object> = Results[WorkflowResultId<Results>];
|
|
18
|
+
|
|
19
|
+
export type WorkflowNodeContext<
|
|
20
|
+
Input,
|
|
21
|
+
KnowledgeBase,
|
|
22
|
+
Results extends object = DefaultWorkflowResults,
|
|
23
|
+
> = {
|
|
24
|
+
readonly input: Input;
|
|
25
|
+
readonly kb: KnowledgeBase;
|
|
26
|
+
readonly signal: AbortSignal;
|
|
27
|
+
readonly results: ReadonlyMap<WorkflowResultId<Results>, WorkflowResultValue<Results>>;
|
|
28
|
+
readonly result: <Id extends WorkflowResultId<Results>>(id: Id) => Results[Id];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type WorkflowNode<
|
|
32
|
+
Input,
|
|
33
|
+
KnowledgeBase,
|
|
34
|
+
Results extends object = DefaultWorkflowResults,
|
|
35
|
+
> = {
|
|
36
|
+
readonly [Id in WorkflowResultId<Results>]: {
|
|
37
|
+
readonly id: Id;
|
|
38
|
+
readonly needs?: readonly WorkflowResultId<Results>[];
|
|
39
|
+
readonly resource?: WorkflowResource;
|
|
40
|
+
readonly run: (
|
|
41
|
+
context: WorkflowNodeContext<Input, KnowledgeBase, Results>,
|
|
42
|
+
) => Results[Id] | Promise<Results[Id]>;
|
|
43
|
+
};
|
|
44
|
+
}[WorkflowResultId<Results>];
|
|
45
|
+
|
|
46
|
+
export type WorkflowDefinition<
|
|
47
|
+
Input,
|
|
48
|
+
KnowledgeBase,
|
|
49
|
+
Results extends object = DefaultWorkflowResults,
|
|
50
|
+
Output extends WorkflowResultId<Results> = WorkflowResultId<Results>,
|
|
51
|
+
> = {
|
|
52
|
+
readonly kind: "hraness-kb-workflow";
|
|
53
|
+
readonly version: 1;
|
|
54
|
+
readonly id: string;
|
|
55
|
+
readonly nodes: readonly WorkflowNode<Input, KnowledgeBase, Results>[];
|
|
56
|
+
readonly output: Output;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type WorkflowSpecification<
|
|
60
|
+
Input,
|
|
61
|
+
KnowledgeBase,
|
|
62
|
+
Results extends object = DefaultWorkflowResults,
|
|
63
|
+
Output extends WorkflowResultId<Results> = WorkflowResultId<Results>,
|
|
64
|
+
> = Omit<
|
|
65
|
+
WorkflowDefinition<Input, KnowledgeBase, Results, Output>,
|
|
66
|
+
"kind" | "version"
|
|
67
|
+
>;
|
|
68
|
+
|
|
69
|
+
type WorkflowBuilderNode<
|
|
70
|
+
Input,
|
|
71
|
+
KnowledgeBase,
|
|
72
|
+
Results extends object,
|
|
73
|
+
Id extends string,
|
|
74
|
+
Needs extends readonly WorkflowResultId<Results>[],
|
|
75
|
+
Returned,
|
|
76
|
+
> = {
|
|
77
|
+
readonly id: Id & (Id extends WorkflowResultId<Results> ? never : unknown);
|
|
78
|
+
readonly needs?: Needs;
|
|
79
|
+
readonly resource?: WorkflowResource;
|
|
80
|
+
readonly run: (
|
|
81
|
+
context: WorkflowNodeContext<
|
|
82
|
+
Input,
|
|
83
|
+
KnowledgeBase,
|
|
84
|
+
Pick<Results, Needs[number]>
|
|
85
|
+
>,
|
|
86
|
+
) => Returned;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export type WorkflowBuilder<
|
|
90
|
+
Input,
|
|
91
|
+
KnowledgeBase = KnowledgeBaseSession,
|
|
92
|
+
Results extends object = Readonly<Record<never, never>>,
|
|
93
|
+
> = {
|
|
94
|
+
readonly node: <
|
|
95
|
+
const Id extends string,
|
|
96
|
+
Returned,
|
|
97
|
+
const Needs extends readonly WorkflowResultId<Results>[] = readonly [],
|
|
98
|
+
>(
|
|
99
|
+
node: WorkflowBuilderNode<
|
|
100
|
+
Input,
|
|
101
|
+
KnowledgeBase,
|
|
102
|
+
Results,
|
|
103
|
+
Id,
|
|
104
|
+
Needs,
|
|
105
|
+
Returned
|
|
106
|
+
>,
|
|
107
|
+
) => WorkflowBuilder<
|
|
108
|
+
Input,
|
|
109
|
+
KnowledgeBase,
|
|
110
|
+
Results & Readonly<Record<Id, Awaited<Returned>>>
|
|
111
|
+
>;
|
|
112
|
+
readonly output: <Output extends WorkflowResultId<Results>>(
|
|
113
|
+
output: Output,
|
|
114
|
+
) => WorkflowDefinition<Input, KnowledgeBase, Results, Output>;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export type WorkflowNodeTrace = {
|
|
118
|
+
readonly id: string;
|
|
119
|
+
readonly needs: readonly string[];
|
|
120
|
+
readonly resource: WorkflowResource;
|
|
121
|
+
readonly status: "completed";
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export type WorkflowRunResult<
|
|
125
|
+
Results extends object = DefaultWorkflowResults,
|
|
126
|
+
Output extends WorkflowResultId<Results> = WorkflowResultId<Results>,
|
|
127
|
+
> = {
|
|
128
|
+
readonly workflow: string;
|
|
129
|
+
readonly outputNode: Output;
|
|
130
|
+
readonly output: Results[Output];
|
|
131
|
+
/** Sum of each completed node result's structured-serialization bytes. */
|
|
132
|
+
readonly outputBytes: number;
|
|
133
|
+
readonly trace: readonly WorkflowNodeTrace[];
|
|
134
|
+
readonly results: ReadonlyMap<WorkflowResultId<Results>, WorkflowResultValue<Results>>;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export type WorkflowRunOptions<Input, KnowledgeBase> = {
|
|
138
|
+
readonly input: Input;
|
|
139
|
+
readonly kb: KnowledgeBase;
|
|
140
|
+
readonly concurrency?: number;
|
|
141
|
+
readonly resourceConcurrency?: Partial<Readonly<Record<WorkflowResource, number>>>;
|
|
142
|
+
/** Aggregate structured-serialization bytes across completed node results. */
|
|
143
|
+
readonly maxOutputBytes?: number;
|
|
144
|
+
readonly signal?: AbortSignal;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
export type WorkflowFailureKind = "aborted" | "node-failed" | "output-limit";
|
|
148
|
+
|
|
149
|
+
export class WorkflowRunError extends Error {
|
|
150
|
+
readonly kind: WorkflowFailureKind;
|
|
151
|
+
readonly node?: string;
|
|
152
|
+
|
|
153
|
+
constructor(
|
|
154
|
+
kind: WorkflowFailureKind,
|
|
155
|
+
message: string,
|
|
156
|
+
options: ErrorOptions & { readonly node?: string } = {},
|
|
157
|
+
) {
|
|
158
|
+
super(message, options);
|
|
159
|
+
this.name = "WorkflowRunError";
|
|
160
|
+
this.kind = kind;
|
|
161
|
+
if (options.node !== undefined) this.node = options.node;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
166
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function needsFor<Input, KnowledgeBase, Results extends object>(
|
|
170
|
+
node: WorkflowNode<Input, KnowledgeBase, Results>,
|
|
171
|
+
): readonly WorkflowResultId<Results>[] {
|
|
172
|
+
return node.needs ?? [];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function validateWorkflowId(id: string, label: string): void {
|
|
176
|
+
if (!workflowIdPattern.test(id)) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`${label} must start with a lowercase letter and contain at most 64 lowercase letters, digits, or hyphens.`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function validateWorkflow<
|
|
184
|
+
Input,
|
|
185
|
+
KnowledgeBase,
|
|
186
|
+
Results extends object,
|
|
187
|
+
Output extends WorkflowResultId<Results>,
|
|
188
|
+
>(
|
|
189
|
+
definition: WorkflowDefinition<Input, KnowledgeBase, Results, Output>,
|
|
190
|
+
): void {
|
|
191
|
+
validateWorkflowId(definition.id, "Workflow id");
|
|
192
|
+
if (definition.nodes.length < 1 || definition.nodes.length > MAX_WORKFLOW_NODES) {
|
|
193
|
+
throw new RangeError(
|
|
194
|
+
`A workflow must contain from 1 through ${MAX_WORKFLOW_NODES} nodes.`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
const nodesById = new Map<string, WorkflowNode<Input, KnowledgeBase, Results>>();
|
|
198
|
+
for (const node of definition.nodes) {
|
|
199
|
+
validateWorkflowId(node.id, "Workflow node id");
|
|
200
|
+
if (nodesById.has(node.id)) {
|
|
201
|
+
throw new Error(`Workflow node id ${JSON.stringify(node.id)} is duplicated.`);
|
|
202
|
+
}
|
|
203
|
+
if (typeof node.run !== "function") {
|
|
204
|
+
throw new Error(`Workflow node ${JSON.stringify(node.id)} must define run().`);
|
|
205
|
+
}
|
|
206
|
+
const needs = needsFor(node);
|
|
207
|
+
if (new Set(needs).size !== needs.length) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`Workflow node ${JSON.stringify(node.id)} repeats a dependency.`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
nodesById.set(node.id, node);
|
|
213
|
+
}
|
|
214
|
+
if (!nodesById.has(definition.output)) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`Workflow output node ${JSON.stringify(definition.output)} does not exist.`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
for (const node of definition.nodes) {
|
|
220
|
+
for (const dependency of needsFor(node)) {
|
|
221
|
+
if (!nodesById.has(dependency)) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Workflow node ${JSON.stringify(node.id)} depends on unknown node ${JSON.stringify(dependency)}.`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (dependency === node.id) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
`Workflow node ${JSON.stringify(node.id)} cannot depend on itself.`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const remaining = new Set(nodesById.keys());
|
|
235
|
+
const completed = new Set<string>();
|
|
236
|
+
while (remaining.size > 0) {
|
|
237
|
+
const ready = definition.nodes.filter((node) =>
|
|
238
|
+
remaining.has(node.id)
|
|
239
|
+
&& needsFor(node).every((dependency) => completed.has(dependency)));
|
|
240
|
+
if (ready.length === 0) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`Workflow contains a dependency cycle among: ${[...remaining].join(", ")}.`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
for (const node of ready) {
|
|
246
|
+
remaining.delete(node.id);
|
|
247
|
+
completed.add(node.id);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function createWorkflowBuilder<
|
|
253
|
+
Input,
|
|
254
|
+
KnowledgeBase,
|
|
255
|
+
Results extends object,
|
|
256
|
+
>(
|
|
257
|
+
id: string,
|
|
258
|
+
nodes: readonly WorkflowNode<Input, KnowledgeBase, Results>[],
|
|
259
|
+
): WorkflowBuilder<Input, KnowledgeBase, Results> {
|
|
260
|
+
const builder: WorkflowBuilder<Input, KnowledgeBase, Results> = {
|
|
261
|
+
node: <
|
|
262
|
+
const Id extends string,
|
|
263
|
+
Returned,
|
|
264
|
+
const Needs extends readonly WorkflowResultId<Results>[] = readonly [],
|
|
265
|
+
>(
|
|
266
|
+
node: WorkflowBuilderNode<
|
|
267
|
+
Input,
|
|
268
|
+
KnowledgeBase,
|
|
269
|
+
Results,
|
|
270
|
+
Id,
|
|
271
|
+
Needs,
|
|
272
|
+
Returned
|
|
273
|
+
>,
|
|
274
|
+
) => createWorkflowBuilder<
|
|
275
|
+
Input,
|
|
276
|
+
KnowledgeBase,
|
|
277
|
+
Results & Readonly<Record<Id, Awaited<Returned>>>
|
|
278
|
+
>(
|
|
279
|
+
id,
|
|
280
|
+
[...nodes, node] as unknown as readonly WorkflowNode<
|
|
281
|
+
Input,
|
|
282
|
+
KnowledgeBase,
|
|
283
|
+
Results & Readonly<Record<Id, Awaited<Returned>>>
|
|
284
|
+
>[],
|
|
285
|
+
),
|
|
286
|
+
output: (output) => defineWorkflow({
|
|
287
|
+
id,
|
|
288
|
+
nodes,
|
|
289
|
+
output,
|
|
290
|
+
}),
|
|
291
|
+
};
|
|
292
|
+
return builder;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Define a finite trusted TypeScript workflow. Runtime validation is repeated at execution. */
|
|
296
|
+
export function defineWorkflow<
|
|
297
|
+
Input,
|
|
298
|
+
KnowledgeBase = KnowledgeBaseSession,
|
|
299
|
+
>(id: string): WorkflowBuilder<Input, KnowledgeBase>;
|
|
300
|
+
export function defineWorkflow<Input, KnowledgeBase>(
|
|
301
|
+
specification: WorkflowSpecification<Input, KnowledgeBase>,
|
|
302
|
+
): WorkflowDefinition<Input, KnowledgeBase>;
|
|
303
|
+
export function defineWorkflow<
|
|
304
|
+
Input,
|
|
305
|
+
KnowledgeBase,
|
|
306
|
+
Results extends object,
|
|
307
|
+
Output extends WorkflowResultId<Results>,
|
|
308
|
+
>(
|
|
309
|
+
specification: WorkflowSpecification<Input, KnowledgeBase, Results, Output>,
|
|
310
|
+
): WorkflowDefinition<Input, KnowledgeBase, Results, Output>;
|
|
311
|
+
export function defineWorkflow<
|
|
312
|
+
Input,
|
|
313
|
+
KnowledgeBase,
|
|
314
|
+
Results extends object,
|
|
315
|
+
Output extends WorkflowResultId<Results>,
|
|
316
|
+
>(
|
|
317
|
+
specification: string | WorkflowSpecification<
|
|
318
|
+
Input,
|
|
319
|
+
KnowledgeBase,
|
|
320
|
+
Results,
|
|
321
|
+
Output
|
|
322
|
+
>,
|
|
323
|
+
):
|
|
324
|
+
| WorkflowDefinition<Input, KnowledgeBase, Results, Output>
|
|
325
|
+
| WorkflowBuilder<Input, KnowledgeBase> {
|
|
326
|
+
if (typeof specification === "string") {
|
|
327
|
+
return createWorkflowBuilder<Input, KnowledgeBase, Readonly<Record<never, never>>>(
|
|
328
|
+
specification,
|
|
329
|
+
[],
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
const definition: WorkflowDefinition<Input, KnowledgeBase, Results, Output> = {
|
|
333
|
+
kind: "hraness-kb-workflow",
|
|
334
|
+
version: 1,
|
|
335
|
+
...specification,
|
|
336
|
+
};
|
|
337
|
+
validateWorkflow(definition);
|
|
338
|
+
return definition;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Validate a dynamically imported code-mode value before executing it. */
|
|
342
|
+
export function workflowFromUnknown(
|
|
343
|
+
value: unknown,
|
|
344
|
+
): WorkflowDefinition<unknown, unknown> {
|
|
345
|
+
if (!isRecord(value)) throw new Error("Workflow export must be an object.");
|
|
346
|
+
if (value.kind !== "hraness-kb-workflow" || value.version !== 1) {
|
|
347
|
+
throw new Error("Workflow export has an unsupported kind or version.");
|
|
348
|
+
}
|
|
349
|
+
if (
|
|
350
|
+
typeof value.id !== "string"
|
|
351
|
+
|| typeof value.output !== "string"
|
|
352
|
+
|| !Array.isArray(value.nodes)
|
|
353
|
+
) {
|
|
354
|
+
throw new Error("Workflow export has malformed id, output, or nodes.");
|
|
355
|
+
}
|
|
356
|
+
const nodes: WorkflowNode<unknown, unknown>[] = value.nodes.map((item, index) => {
|
|
357
|
+
if (!isRecord(item) || typeof item.id !== "string") {
|
|
358
|
+
throw new Error(`Workflow export node ${index} is malformed.`);
|
|
359
|
+
}
|
|
360
|
+
const run = item.run;
|
|
361
|
+
if (typeof run !== "function") {
|
|
362
|
+
throw new Error(`Workflow export node ${index} is malformed.`);
|
|
363
|
+
}
|
|
364
|
+
const needs = item.needs;
|
|
365
|
+
if (needs !== undefined && (!Array.isArray(needs) || needs.some((entry) => typeof entry !== "string"))) {
|
|
366
|
+
throw new Error(`Workflow export node ${index}.needs must be an array of strings.`);
|
|
367
|
+
}
|
|
368
|
+
const resource = item.resource;
|
|
369
|
+
if (resource !== undefined && resource !== "default" && resource !== "git" && resource !== "qmd") {
|
|
370
|
+
throw new Error(`Workflow export node ${index}.resource is invalid.`);
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
id: item.id,
|
|
374
|
+
...(needs === undefined ? {} : { needs }),
|
|
375
|
+
...(resource === undefined ? {} : { resource }),
|
|
376
|
+
run: (context) => {
|
|
377
|
+
const returned: unknown = Reflect.apply(run, undefined, [context]);
|
|
378
|
+
return returned;
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
});
|
|
382
|
+
const definition: WorkflowDefinition<unknown, unknown> = {
|
|
383
|
+
kind: "hraness-kb-workflow",
|
|
384
|
+
version: 1,
|
|
385
|
+
id: value.id,
|
|
386
|
+
nodes,
|
|
387
|
+
output: value.output,
|
|
388
|
+
};
|
|
389
|
+
validateWorkflow(definition);
|
|
390
|
+
return definition;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function checkedConcurrency(value: number | undefined): number {
|
|
394
|
+
const concurrency = value ?? 4;
|
|
395
|
+
if (
|
|
396
|
+
!Number.isSafeInteger(concurrency)
|
|
397
|
+
|| concurrency < 1
|
|
398
|
+
|| concurrency > MAX_WORKFLOW_CONCURRENCY
|
|
399
|
+
) {
|
|
400
|
+
throw new RangeError(
|
|
401
|
+
`Workflow concurrency must be an integer from 1 through ${MAX_WORKFLOW_CONCURRENCY}.`,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
return concurrency;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function resourceLimits(
|
|
408
|
+
concurrency: number,
|
|
409
|
+
configured: WorkflowRunOptions<unknown, unknown>["resourceConcurrency"],
|
|
410
|
+
): Readonly<Record<WorkflowResource, number>> {
|
|
411
|
+
const limits: Record<WorkflowResource, number> = {
|
|
412
|
+
default: concurrency,
|
|
413
|
+
git: Math.min(MAX_GIT_WORKFLOW_CONCURRENCY, concurrency),
|
|
414
|
+
qmd: 1,
|
|
415
|
+
};
|
|
416
|
+
for (const resource of ["default", "git", "qmd"] as const) {
|
|
417
|
+
const value = configured?.[resource];
|
|
418
|
+
if (value === undefined) continue;
|
|
419
|
+
if (resource === "qmd" && value !== 1) {
|
|
420
|
+
throw new RangeError("Workflow qmd concurrency is fixed at 1.");
|
|
421
|
+
}
|
|
422
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > MAX_WORKFLOW_CONCURRENCY) {
|
|
423
|
+
throw new RangeError(
|
|
424
|
+
`Workflow ${resource} concurrency must be an integer from 1 through ${MAX_WORKFLOW_CONCURRENCY}.`,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
limits[resource] = resource === "git"
|
|
428
|
+
? Math.min(value, concurrency, MAX_GIT_WORKFLOW_CONCURRENCY)
|
|
429
|
+
: Math.min(value, concurrency);
|
|
430
|
+
}
|
|
431
|
+
return limits;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function checkedOutputBytes(value: number | undefined): number {
|
|
435
|
+
const maximum = value ?? DEFAULT_WORKFLOW_OUTPUT_BYTES;
|
|
436
|
+
if (
|
|
437
|
+
!Number.isSafeInteger(maximum)
|
|
438
|
+
|| maximum < 1
|
|
439
|
+
|| maximum > MAX_WORKFLOW_OUTPUT_BYTES
|
|
440
|
+
) {
|
|
441
|
+
throw new RangeError(
|
|
442
|
+
`Workflow output byte limit must be an integer from 1 through ${MAX_WORKFLOW_OUTPUT_BYTES}.`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
return maximum;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function structuredOutputBytes(value: unknown, node: string): number {
|
|
449
|
+
try {
|
|
450
|
+
return serialize(value).byteLength;
|
|
451
|
+
} catch (error: unknown) {
|
|
452
|
+
throw new WorkflowRunError(
|
|
453
|
+
"node-failed",
|
|
454
|
+
`Workflow node ${JSON.stringify(node)} returned a result that cannot be structurally serialized.`,
|
|
455
|
+
{ node, cause: error },
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
type NodeOutcome =
|
|
461
|
+
| { readonly ok: true; readonly id: string; readonly value: unknown }
|
|
462
|
+
| { readonly ok: false; readonly id: string; readonly error: unknown };
|
|
463
|
+
|
|
464
|
+
function abortedError(): WorkflowRunError {
|
|
465
|
+
return new WorkflowRunError("aborted", "Workflow execution was aborted.");
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Execute ready nodes concurrently while keeping QMD and Git resource groups bounded. */
|
|
469
|
+
export async function runWorkflow<
|
|
470
|
+
Input,
|
|
471
|
+
KnowledgeBase,
|
|
472
|
+
Results extends object,
|
|
473
|
+
Output extends WorkflowResultId<Results>,
|
|
474
|
+
>(
|
|
475
|
+
definition: WorkflowDefinition<Input, KnowledgeBase, Results, Output>,
|
|
476
|
+
options: WorkflowRunOptions<Input, KnowledgeBase>,
|
|
477
|
+
): Promise<WorkflowRunResult<Results, Output>> {
|
|
478
|
+
validateWorkflow(definition);
|
|
479
|
+
const concurrency = checkedConcurrency(options.concurrency);
|
|
480
|
+
const limits = resourceLimits(concurrency, options.resourceConcurrency);
|
|
481
|
+
const outputLimit = checkedOutputBytes(options.maxOutputBytes);
|
|
482
|
+
if (options.signal?.aborted === true) throw abortedError();
|
|
483
|
+
|
|
484
|
+
const controller = new AbortController();
|
|
485
|
+
let abortedByCaller = false;
|
|
486
|
+
const abort = () => {
|
|
487
|
+
abortedByCaller = true;
|
|
488
|
+
controller.abort(options.signal?.reason);
|
|
489
|
+
};
|
|
490
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
491
|
+
const pending = new Set(definition.nodes.map(({ id }) => id));
|
|
492
|
+
const completed = new Set<string>();
|
|
493
|
+
const values = new Map<
|
|
494
|
+
WorkflowResultId<Results>,
|
|
495
|
+
WorkflowResultValue<Results>
|
|
496
|
+
>();
|
|
497
|
+
const active = new Map<string, Promise<NodeOutcome>>();
|
|
498
|
+
const activeResource = new Map<string, WorkflowResource>();
|
|
499
|
+
const resourceCounts: Record<WorkflowResource, number> = {
|
|
500
|
+
default: 0,
|
|
501
|
+
git: 0,
|
|
502
|
+
qmd: 0,
|
|
503
|
+
};
|
|
504
|
+
let failure: WorkflowRunError | undefined;
|
|
505
|
+
let outputBytes = 0;
|
|
506
|
+
|
|
507
|
+
const completedValue = <Id extends WorkflowResultId<Results>>(id: Id): Results[Id] => {
|
|
508
|
+
if (!values.has(id)) {
|
|
509
|
+
throw new Error(
|
|
510
|
+
`Workflow result ${JSON.stringify(id)} is unavailable after its dependency completed.`,
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
return values.get(id) as Results[Id];
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const launch = (node: WorkflowNode<Input, KnowledgeBase, Results>): void => {
|
|
517
|
+
const resource = node.resource ?? "default";
|
|
518
|
+
pending.delete(node.id);
|
|
519
|
+
resourceCounts[resource] += 1;
|
|
520
|
+
activeResource.set(node.id, resource);
|
|
521
|
+
const needs = needsFor(node);
|
|
522
|
+
const allowedResults = new Set(needs);
|
|
523
|
+
const dependencyResults = new Map<
|
|
524
|
+
WorkflowResultId<Results>,
|
|
525
|
+
WorkflowResultValue<Results>
|
|
526
|
+
>(needs.map((id) => [id, completedValue(id)] as const));
|
|
527
|
+
const context: WorkflowNodeContext<Input, KnowledgeBase, Results> = {
|
|
528
|
+
input: options.input,
|
|
529
|
+
kb: options.kb,
|
|
530
|
+
signal: controller.signal,
|
|
531
|
+
results: dependencyResults,
|
|
532
|
+
result: <Id extends WorkflowResultId<Results>>(id: Id): Results[Id] => {
|
|
533
|
+
if (!allowedResults.has(id)) {
|
|
534
|
+
throw new Error(
|
|
535
|
+
`Workflow result ${JSON.stringify(id)} is not a declared dependency of ${JSON.stringify(node.id)}.`,
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
return dependencyResults.get(id) as Results[Id];
|
|
539
|
+
},
|
|
540
|
+
};
|
|
541
|
+
const outcome: Promise<NodeOutcome> = Promise.resolve()
|
|
542
|
+
.then(() => node.run(context))
|
|
543
|
+
.then((value): NodeOutcome => ({ ok: true, id: node.id, value }))
|
|
544
|
+
.catch((error: unknown): NodeOutcome => ({
|
|
545
|
+
ok: false,
|
|
546
|
+
id: node.id,
|
|
547
|
+
error,
|
|
548
|
+
}));
|
|
549
|
+
active.set(node.id, outcome);
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
while (completed.size + (failure === undefined ? 0 : pending.size) < definition.nodes.length) {
|
|
554
|
+
if (controller.signal.aborted && failure === undefined) failure = abortedError();
|
|
555
|
+
|
|
556
|
+
if (failure === undefined) {
|
|
557
|
+
for (const node of definition.nodes) {
|
|
558
|
+
if (!pending.has(node.id) || active.size >= concurrency) continue;
|
|
559
|
+
if (!needsFor(node).every((dependency) => completed.has(dependency))) continue;
|
|
560
|
+
const resource = node.resource ?? "default";
|
|
561
|
+
if (resourceCounts[resource] >= limits[resource]) continue;
|
|
562
|
+
launch(node);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (active.size === 0) {
|
|
567
|
+
if (failure !== undefined) break;
|
|
568
|
+
throw new Error("Workflow scheduler made no progress after validation.");
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const outcome = await Promise.race(active.values());
|
|
572
|
+
active.delete(outcome.id);
|
|
573
|
+
const resource = activeResource.get(outcome.id);
|
|
574
|
+
if (resource !== undefined) resourceCounts[resource] -= 1;
|
|
575
|
+
activeResource.delete(outcome.id);
|
|
576
|
+
if (outcome.ok && failure === undefined) {
|
|
577
|
+
try {
|
|
578
|
+
const resultBytes = structuredOutputBytes(outcome.value, outcome.id);
|
|
579
|
+
if (resultBytes > outputLimit - outputBytes) {
|
|
580
|
+
throw new WorkflowRunError(
|
|
581
|
+
"output-limit",
|
|
582
|
+
`Workflow results exceed the ${outputLimit}-byte output limit at node ${JSON.stringify(outcome.id)}.`,
|
|
583
|
+
{ node: outcome.id },
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
outputBytes += resultBytes;
|
|
587
|
+
values.set(
|
|
588
|
+
outcome.id as WorkflowResultId<Results>,
|
|
589
|
+
outcome.value as WorkflowResultValue<Results>,
|
|
590
|
+
);
|
|
591
|
+
completed.add(outcome.id);
|
|
592
|
+
} catch (error: unknown) {
|
|
593
|
+
controller.abort(error);
|
|
594
|
+
failure = error instanceof WorkflowRunError
|
|
595
|
+
? error
|
|
596
|
+
: new WorkflowRunError(
|
|
597
|
+
"node-failed",
|
|
598
|
+
`Workflow node ${JSON.stringify(outcome.id)} failed while validating its result.`,
|
|
599
|
+
{ node: outcome.id, cause: error },
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
} else if (!outcome.ok && failure === undefined) {
|
|
603
|
+
controller.abort(outcome.error);
|
|
604
|
+
failure = abortedByCaller
|
|
605
|
+
? abortedError()
|
|
606
|
+
: new WorkflowRunError(
|
|
607
|
+
"node-failed",
|
|
608
|
+
`Workflow node ${JSON.stringify(outcome.id)} failed.`,
|
|
609
|
+
{ node: outcome.id, cause: outcome.error },
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
while (active.size > 0) {
|
|
615
|
+
const outcome = await Promise.race(active.values());
|
|
616
|
+
active.delete(outcome.id);
|
|
617
|
+
}
|
|
618
|
+
if (failure !== undefined) throw failure;
|
|
619
|
+
let finalOutputBytes = 0;
|
|
620
|
+
for (const node of definition.nodes) {
|
|
621
|
+
const resultBytes = structuredOutputBytes(values.get(node.id), node.id);
|
|
622
|
+
if (resultBytes > outputLimit - finalOutputBytes) {
|
|
623
|
+
throw new WorkflowRunError(
|
|
624
|
+
"output-limit",
|
|
625
|
+
`Workflow results exceed the ${outputLimit}-byte output limit at node ${JSON.stringify(node.id)}.`,
|
|
626
|
+
{ node: node.id },
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
finalOutputBytes += resultBytes;
|
|
630
|
+
}
|
|
631
|
+
const orderedValues = new Map<
|
|
632
|
+
WorkflowResultId<Results>,
|
|
633
|
+
WorkflowResultValue<Results>
|
|
634
|
+
>(
|
|
635
|
+
definition.nodes.map((node) => [node.id, completedValue(node.id)] as const),
|
|
636
|
+
);
|
|
637
|
+
return {
|
|
638
|
+
workflow: definition.id,
|
|
639
|
+
outputNode: definition.output,
|
|
640
|
+
output: values.get(definition.output) as Results[Output],
|
|
641
|
+
outputBytes: finalOutputBytes,
|
|
642
|
+
trace: definition.nodes.map((node): WorkflowNodeTrace => ({
|
|
643
|
+
id: node.id,
|
|
644
|
+
needs: needsFor(node),
|
|
645
|
+
resource: node.resource ?? "default",
|
|
646
|
+
status: "completed",
|
|
647
|
+
})),
|
|
648
|
+
results: orderedValues,
|
|
649
|
+
};
|
|
650
|
+
} finally {
|
|
651
|
+
options.signal?.removeEventListener("abort", abort);
|
|
652
|
+
}
|
|
653
|
+
}
|