@dypai-ai/workflow-core 0.1.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.
Files changed (38) hide show
  1. package/fixtures/capability-catalog.json +126 -0
  2. package/fixtures/legacy-create-booking.yaml +40 -0
  3. package/package.json +40 -0
  4. package/src/adapters/adapters.test.ts +168 -0
  5. package/src/adapters/engineBinding.ts +35 -0
  6. package/src/adapters/flowDefinitionToIr.ts +141 -0
  7. package/src/adapters/irToWorkflowCodeV2.ts +293 -0
  8. package/src/adapters/legacyYamlToIr.ts +340 -0
  9. package/src/adapters/placeholderToRef.ts +74 -0
  10. package/src/adapters/refToLegacyPlaceholder.ts +33 -0
  11. package/src/adapters/sqlBuilders.ts +81 -0
  12. package/src/adapters/triggers.ts +86 -0
  13. package/src/adapters/types.ts +15 -0
  14. package/src/adapters/workflowCodeTypes.ts +45 -0
  15. package/src/capabilities/agentBrief.ts +42 -0
  16. package/src/capabilities/capabilities.test.ts +126 -0
  17. package/src/capabilities/capabilityRegistry.ts +112 -0
  18. package/src/capabilities/catalogSchema.ts +14 -0
  19. package/src/capabilities/fromCatalog.ts +30 -0
  20. package/src/capabilities/index.ts +35 -0
  21. package/src/capabilities/types.ts +57 -0
  22. package/src/fixtures/createBooking.flow.ts +64 -0
  23. package/src/fixtures/createBooking.ir.ts +103 -0
  24. package/src/fixtures/listBookings.ir.ts +61 -0
  25. package/src/index.ts +172 -0
  26. package/src/ir/refs.ts +103 -0
  27. package/src/ir/schema.ts +149 -0
  28. package/src/ir/sourceMap.ts +59 -0
  29. package/src/ir/types.ts +147 -0
  30. package/src/ir/validate.test.ts +181 -0
  31. package/src/ir/validate.ts +365 -0
  32. package/src/registry/defineNode.ts +19 -0
  33. package/src/registry/nodeRegistry.ts +87 -0
  34. package/src/registry/nodes/dypaiDb.ts +164 -0
  35. package/src/registry/nodes/dypaiEmail.ts +57 -0
  36. package/src/registry/nodes/dypaiFlow.ts +25 -0
  37. package/src/registry/nodes/legacyWorkflow.ts +27 -0
  38. package/tsconfig.json +12 -0
@@ -0,0 +1,33 @@
1
+ import { isRefIR, refToPlaceholder } from "../ir/refs";
2
+ import type { RefIR } from "../ir/types";
3
+
4
+ export { refToPlaceholder };
5
+
6
+ export function serializeConfigValue(value: unknown): unknown {
7
+ if (isRefIR(value)) return refToPlaceholder(value);
8
+ if (Array.isArray(value)) return value.map((item) => serializeConfigValue(item));
9
+ if (value && typeof value === "object") {
10
+ const out: Record<string, unknown> = {};
11
+ for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {
12
+ out[key] = serializeConfigValue(nested);
13
+ }
14
+ return out;
15
+ }
16
+ return value;
17
+ }
18
+
19
+ export function serializeConfig(config: Record<string, unknown>): Record<string, unknown> {
20
+ return serializeConfigValue(config) as Record<string, unknown>;
21
+ }
22
+
23
+ export function refToLegacyPlaceholder(ref: RefIR): string {
24
+ return refToPlaceholder(ref);
25
+ }
26
+
27
+ export function responseMapToComposeFields(responseMap: Record<string, RefIR>): Record<string, string> {
28
+ const fields: Record<string, string> = {};
29
+ for (const [key, ref] of Object.entries(responseMap)) {
30
+ fields[key] = refToPlaceholder(ref);
31
+ }
32
+ return fields;
33
+ }
@@ -0,0 +1,81 @@
1
+ import { isRefIR } from "../ir/refs";
2
+ import { serializeConfigValue } from "./refToLegacyPlaceholder";
3
+
4
+ function quoteSqlString(value: string): string {
5
+ return `'${value.replace(/'/g, "''")}'`;
6
+ }
7
+
8
+ function sqlLiteral(value: unknown): string {
9
+ if (value === null || value === undefined) return "NULL";
10
+ if (typeof value === "number" && !Number.isNaN(value)) return String(value);
11
+ if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
12
+ if (typeof value === "string") return quoteSqlString(value);
13
+ return quoteSqlString(JSON.stringify(value));
14
+ }
15
+
16
+ function sqlExpression(value: unknown): string {
17
+ if (isRefIR(value)) return serializeConfigValue(value) as string;
18
+ return sqlLiteral(value);
19
+ }
20
+
21
+ export function buildInsertSql(config: {
22
+ table: string;
23
+ values: Record<string, unknown>;
24
+ returning?: string[];
25
+ }): string {
26
+ const columns = Object.keys(config.values).sort();
27
+ const assignments = columns.map((column) => sqlExpression(config.values[column]));
28
+ const returning = config.returning?.length
29
+ ? config.returning.join(", ")
30
+ : "*";
31
+ return [
32
+ `INSERT INTO ${config.table} (${columns.join(", ")})`,
33
+ `VALUES (${assignments.join(", ")})`,
34
+ `RETURNING ${returning}`,
35
+ ].join("\n");
36
+ }
37
+
38
+ export function buildUpdateSql(config: {
39
+ table: string;
40
+ where: Record<string, unknown>;
41
+ set: Record<string, unknown>;
42
+ returning?: string[];
43
+ }): string {
44
+ const setColumns = Object.keys(config.set).sort();
45
+ const whereColumns = Object.keys(config.where).sort();
46
+ const setClause = setColumns
47
+ .map((column) => `${column} = ${sqlExpression(config.set[column])}`)
48
+ .join(",\n ");
49
+ const whereClause = whereColumns
50
+ .map((column) => `${column} = ${sqlExpression(config.where[column])}`)
51
+ .join("\n AND ");
52
+ const returning = config.returning?.length
53
+ ? `\nRETURNING ${config.returning.join(", ")}`
54
+ : "";
55
+ return [
56
+ `UPDATE ${config.table}`,
57
+ `SET ${setClause}`,
58
+ `WHERE ${whereClause}${returning}`,
59
+ ].join("\n");
60
+ }
61
+
62
+ export function buildListSql(config: {
63
+ table: string;
64
+ where?: Record<string, unknown>;
65
+ orderBy?: string;
66
+ limit?: unknown;
67
+ offset?: unknown;
68
+ }): string {
69
+ const lines = [`SELECT * FROM ${config.table}`];
70
+ const whereColumns = config.where ? Object.keys(config.where).sort() : [];
71
+ if (whereColumns.length) {
72
+ const whereClause = whereColumns
73
+ .map((column) => `${column} = ${sqlExpression(config.where![column])}`)
74
+ .join("\n AND ");
75
+ lines.push(`WHERE ${whereClause}`);
76
+ }
77
+ if (config.orderBy?.trim()) lines.push(`ORDER BY ${config.orderBy.trim()}`);
78
+ if (config.limit !== undefined) lines.push(`LIMIT ${sqlExpression(config.limit)}`);
79
+ if (config.offset !== undefined) lines.push(`OFFSET ${sqlExpression(config.offset)}`);
80
+ return lines.join("\n");
81
+ }
@@ -0,0 +1,86 @@
1
+ import { isRecord } from "../ir/schema";
2
+ import type { AuthIR, HttpMethod, TriggerIR } from "../ir/types";
3
+
4
+ export function authFromLegacyTrigger(
5
+ trigger: Record<string, unknown>,
6
+ allowedRoles: string[] | undefined,
7
+ ): AuthIR {
8
+ const httpApi = trigger.http_api === true ? {} : trigger.http_api;
9
+ const authMode = isRecord(httpApi)
10
+ ? String(httpApi.auth_mode || httpApi.mode || "jwt").toLowerCase()
11
+ : "jwt";
12
+
13
+ if (authMode === "public" || authMode === "none") {
14
+ return { mode: "public" };
15
+ }
16
+
17
+ const roles = (allowedRoles || []).filter((role) => role.trim().length > 0);
18
+ return { mode: "jwt", allowedRoles: roles };
19
+ }
20
+
21
+ export function triggerFromLegacyDoc(
22
+ trigger: unknown,
23
+ method: HttpMethod,
24
+ ): TriggerIR {
25
+ const value = isRecord(trigger) ? trigger : {};
26
+ if (isRecord(value.schedule) || value.schedule === true) {
27
+ const schedule = value.schedule === true ? {} : value.schedule;
28
+ return {
29
+ type: "schedule",
30
+ cron: isRecord(schedule) && typeof schedule.cron === "string" ? schedule.cron : "",
31
+ };
32
+ }
33
+ if (value.webhook) {
34
+ const webhook = value.webhook === true ? {} : value.webhook;
35
+ return {
36
+ type: "webhook",
37
+ provider: isRecord(webhook) && typeof webhook.provider === "string"
38
+ ? webhook.provider
39
+ : undefined,
40
+ };
41
+ }
42
+ return { type: "http", method };
43
+ }
44
+
45
+ export function triggersToEngineConfig(
46
+ trigger: TriggerIR,
47
+ auth: AuthIR,
48
+ ): { triggers: Record<string, unknown> } {
49
+ const out: Record<string, unknown> = {
50
+ webhook: { path: "", enabled: false },
51
+ http_api: { enabled: false, auth_mode: "jwt" },
52
+ schedule: { enabled: false },
53
+ };
54
+
55
+ if (trigger.type === "http") {
56
+ out.http_api = {
57
+ enabled: true,
58
+ auth_mode: auth.mode === "public" ? "public" : "jwt",
59
+ };
60
+ } else if (trigger.type === "webhook") {
61
+ out.http_api = { enabled: false, auth_mode: "jwt" };
62
+ out.webhook = { path: "", enabled: true, ...(trigger.provider ? { provider: trigger.provider } : {}) };
63
+ } else if (trigger.type === "schedule") {
64
+ out.http_api = { enabled: false, auth_mode: "jwt" };
65
+ out.schedule = { enabled: true, cron: trigger.cron };
66
+ }
67
+
68
+ return { triggers: out };
69
+ }
70
+
71
+ export function responseCardinalityFromLegacy(value: unknown): "single" | "many" | "none" | undefined {
72
+ if (typeof value !== "string") return undefined;
73
+ const normalized = value.trim().toLowerCase();
74
+ if (normalized === "single" || normalized === "one") return "single";
75
+ if (normalized === "many") return "many";
76
+ if (normalized === "none") return "none";
77
+ return undefined;
78
+ }
79
+
80
+ export function normalizeHttpMethod(value: unknown, fallback: HttpMethod = "GET"): HttpMethod {
81
+ const method = typeof value === "string" ? value.trim().toUpperCase() : fallback;
82
+ if (method === "GET" || method === "POST" || method === "PUT" || method === "PATCH" || method === "DELETE") {
83
+ return method;
84
+ }
85
+ return fallback;
86
+ }
@@ -0,0 +1,15 @@
1
+ import type { AdapterTarget } from "../ir/types";
2
+
3
+ export type AdapterDeclaration = {
4
+ target: AdapterTarget;
5
+ implemented: boolean;
6
+ notes?: string;
7
+ };
8
+
9
+ export type AdapterRegistry = Record<string, AdapterDeclaration[]>;
10
+
11
+ export const pendingAdapters: AdapterRegistry = {
12
+ "dypai.db": [{ target: "workflow_code.v2", implemented: true }],
13
+ "dypai.email": [{ target: "workflow_code.v2", implemented: true }],
14
+ "dypai.flow": [{ target: "workflow_code.v2", implemented: true }],
15
+ };
@@ -0,0 +1,45 @@
1
+ export type EngineWorkflowNode = {
2
+ id: string;
3
+ node_type: string;
4
+ parameters: Record<string, unknown>;
5
+ variable?: string;
6
+ is_return?: boolean;
7
+ };
8
+
9
+ export type EngineWorkflowEdge = {
10
+ source: string;
11
+ target: string;
12
+ condition?: unknown;
13
+ source_handle?: string;
14
+ target_handle?: string;
15
+ };
16
+
17
+ export type WorkflowCodeV2 = {
18
+ schema_version: "2.0";
19
+ nodes: EngineWorkflowNode[];
20
+ edges: EngineWorkflowEdge[];
21
+ execution_config: {
22
+ triggers: Record<string, unknown>;
23
+ };
24
+ metadata?: {
25
+ ir_name?: string;
26
+ ir_version?: string;
27
+ capability_catalog_hash?: string;
28
+ capability_engine_version?: string;
29
+ response_cardinality?: string;
30
+ source?: {
31
+ kind?: string;
32
+ file?: string;
33
+ };
34
+ };
35
+ };
36
+
37
+ export type AdapterResult<T> = {
38
+ ok: boolean;
39
+ value?: T;
40
+ diagnostics: import("../ir/types").IRDiagnostic[];
41
+ };
42
+
43
+ export type ImportYamlResult = AdapterResult<import("../ir/types").WorkflowIR>;
44
+
45
+ export type AdaptWorkflowCodeResult = AdapterResult<WorkflowCodeV2>;
@@ -0,0 +1,42 @@
1
+ import { CapabilityRegistry } from "./capabilityRegistry";
2
+ import type { CapabilityCatalog } from "./types";
3
+
4
+ export function generateCapabilityBrief(registry: CapabilityRegistry): string {
5
+ const lines: string[] = [
6
+ "# DYPAI Capability Brief",
7
+ "",
8
+ "Grouped authoring capabilities synced from the engine catalog.",
9
+ "",
10
+ ];
11
+
12
+ for (const namespace of registry.listNamespaces()) {
13
+ lines.push(`## ${namespace}`);
14
+ lines.push("");
15
+
16
+ for (const group of registry.listGroupsByNamespace(namespace)) {
17
+ lines.push(`### ${group.id}`);
18
+ if (group.docs) {
19
+ lines.push(group.docs);
20
+ lines.push("");
21
+ }
22
+
23
+ for (const operation of group.operations) {
24
+ lines.push(`- ${operation.authoring_signature}`);
25
+ if (operation.description) {
26
+ lines.push(` ${operation.description}`);
27
+ }
28
+ if (operation.required_credentials?.length) {
29
+ lines.push(` Credentials: ${operation.required_credentials.join(", ")}`);
30
+ }
31
+ }
32
+
33
+ lines.push("");
34
+ }
35
+ }
36
+
37
+ return lines.join("\n").trimEnd();
38
+ }
39
+
40
+ export function generateCapabilityBriefFromCatalog(catalog: CapabilityCatalog): string {
41
+ return generateCapabilityBrief(CapabilityRegistry.fromCatalog(catalog));
42
+ }
@@ -0,0 +1,126 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { describe, expect, test } from "bun:test";
5
+ import { flowDefinitionToIr } from "../adapters/flowDefinitionToIr";
6
+ import { irToWorkflowCodeV2 } from "../adapters/irToWorkflowCodeV2";
7
+ import {
8
+ CapabilityRegistry,
9
+ generateCapabilityBrief,
10
+ isCapabilityCatalog,
11
+ type CapabilityCatalog,
12
+ } from "../capabilities";
13
+ import { WORKFLOW_IR_VERSION, type WorkflowIR } from "../ir/types";
14
+ import { validateWorkflowIR } from "../ir/validate";
15
+
16
+ const fixturePath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "fixtures", "capability-catalog.json");
17
+ const fixtureCatalog = JSON.parse(readFileSync(fixturePath, "utf-8")) as CapabilityCatalog;
18
+
19
+ describe("dynamic capability catalog", () => {
20
+ test("loads fixture catalog", () => {
21
+ expect(isCapabilityCatalog(fixtureCatalog)).toBe(true);
22
+ expect(fixtureCatalog.groups.length).toBeGreaterThan(0);
23
+ });
24
+
25
+ test("lists groups by namespace from catalog", () => {
26
+ const registry = CapabilityRegistry.fromCatalog(fixtureCatalog);
27
+ expect(registry.listNamespaces()).toEqual(["dypai", "google", "stripe"]);
28
+ expect(registry.resolveCall("stripe.checkout.createSession")).toEqual({
29
+ node: "stripe.checkout",
30
+ operation: "createSession",
31
+ version: 1,
32
+ });
33
+ });
34
+
35
+ test("generates brief from dynamic catalog", () => {
36
+ const registry = CapabilityRegistry.fromCatalog(fixtureCatalog);
37
+ const brief = generateCapabilityBrief(registry);
38
+ expect(brief).toContain("### dypai.db");
39
+ expect(brief).toContain("stripe.checkout.createSession");
40
+ });
41
+
42
+ test("validates and adapts grouped workflow using catalog fixture", () => {
43
+ const workflow: WorkflowIR = {
44
+ version: WORKFLOW_IR_VERSION,
45
+ name: "checkout",
46
+ trigger: { type: "http", method: "POST" },
47
+ auth: { mode: "public" },
48
+ inputSchema: { type: "object", properties: {} },
49
+ outputSchema: { type: "object", properties: { url: { type: "string" } } },
50
+ response: { cardinality: "single" },
51
+ steps: [
52
+ {
53
+ id: "checkout",
54
+ node: "stripe.checkout",
55
+ operation: "createSession",
56
+ version: 1,
57
+ config: {
58
+ mode: "payment",
59
+ price_id: "price_123",
60
+ success_url: "https://example.com/success",
61
+ cancel_url: "https://example.com/cancel",
62
+ },
63
+ },
64
+ {
65
+ id: "return",
66
+ node: "dypai.flow",
67
+ operation: "return",
68
+ version: 1,
69
+ config: {},
70
+ return: true,
71
+ },
72
+ ],
73
+ responseMap: { url: { kind: "literal", value: "https://checkout.test" } },
74
+ };
75
+
76
+ expect(validateWorkflowIR(workflow, { capabilityCatalog: fixtureCatalog }).ok).toBe(true);
77
+ const adapted = irToWorkflowCodeV2(workflow, { capabilityCatalog: fixtureCatalog });
78
+ expect(adapted.ok).toBe(true);
79
+ expect(adapted.value?.nodes.find((node) => node.id === "checkout")).toMatchObject({
80
+ node_type: "stripe",
81
+ parameters: {
82
+ operation: "stripe_create_checkout_session",
83
+ price_id: "price_123",
84
+ },
85
+ });
86
+ expect(adapted.value?.metadata?.capability_catalog_hash).toBe("fixture-capability-catalog");
87
+ });
88
+
89
+ test("keeps legacy db/email aliases without catalog", () => {
90
+ const workflow = flowDefinitionToIr({
91
+ name: "legacy-alias",
92
+ trigger: { type: "http", method: "GET" },
93
+ auth: { mode: "public" },
94
+ input: { type: "object", properties: {} },
95
+ output: { type: "object", properties: { ok: { type: "boolean" } } },
96
+ steps: [{ id: "ping", call: "db.query", config: { sql: "SELECT 1 AS ok" } }],
97
+ returns: { ok: { kind: "literal", value: true } },
98
+ });
99
+
100
+ expect(workflow.steps[0]).toMatchObject({ node: "dypai.db", operation: "query" });
101
+ expect(validateWorkflowIR(workflow).ok).toBe(true);
102
+ expect(irToWorkflowCodeV2(workflow).ok).toBe(true);
103
+ });
104
+
105
+ test("resolves grouped flow calls using catalog fixture", () => {
106
+ const workflow = flowDefinitionToIr({
107
+ name: "grouped-flow",
108
+ trigger: { type: "http", method: "POST" },
109
+ auth: { mode: "public" },
110
+ input: { type: "object", properties: {} },
111
+ output: { type: "object", properties: { rows: { type: "array" } } },
112
+ steps: [{
113
+ id: "sheet",
114
+ call: "google.sheets.readRows",
115
+ config: { spreadsheet_id: "abc123" },
116
+ }],
117
+ returns: { rows: { kind: "literal", value: [] } },
118
+ }, { capabilityCatalog: fixtureCatalog });
119
+
120
+ expect(workflow.steps[0]).toMatchObject({
121
+ node: "google.sheets",
122
+ operation: "readRows",
123
+ });
124
+ expect(validateWorkflowIR(workflow, { capabilityCatalog: fixtureCatalog }).ok).toBe(true);
125
+ });
126
+ });
@@ -0,0 +1,112 @@
1
+ import type { NodeDefinition } from "../ir/types";
2
+ import { nodeDefinitionsFromCatalog } from "./fromCatalog";
3
+ import type {
4
+ CapabilityCallResolution,
5
+ CapabilityCatalog,
6
+ CapabilityGroupJson,
7
+ CapabilityLookup,
8
+ CapabilityOperationJson,
9
+ } from "./types";
10
+ import { isCapabilityCatalog } from "./catalogSchema";
11
+
12
+ export class CapabilityRegistry {
13
+ private readonly groups = new Map<string, CapabilityGroupJson>();
14
+ private readonly operations = new Map<string, CapabilityOperationJson>();
15
+ private catalog: CapabilityCatalog | null = null;
16
+
17
+ static fromCatalog(catalog: CapabilityCatalog): CapabilityRegistry {
18
+ const registry = new CapabilityRegistry();
19
+ registry.loadCatalog(catalog);
20
+ return registry;
21
+ }
22
+
23
+ loadCatalog(catalog: CapabilityCatalog): void {
24
+ if (!isCapabilityCatalog(catalog)) {
25
+ throw new Error("Invalid capability catalog payload.");
26
+ }
27
+ this.catalog = catalog;
28
+ this.groups.clear();
29
+ this.operations.clear();
30
+ for (const group of catalog.groups) {
31
+ this.groups.set(group.id, group);
32
+ for (const operation of group.operations) {
33
+ this.operations.set(operation.id, operation);
34
+ }
35
+ }
36
+ }
37
+
38
+ getCatalog(): CapabilityCatalog | null {
39
+ return this.catalog;
40
+ }
41
+
42
+ getGroup(groupId: string): CapabilityGroupJson | undefined {
43
+ return this.groups.get(groupId);
44
+ }
45
+
46
+ listGroups(): CapabilityGroupJson[] {
47
+ return [...this.groups.values()].sort((a, b) => a.id.localeCompare(b.id));
48
+ }
49
+
50
+ listGroupsByNamespace(namespace: string): CapabilityGroupJson[] {
51
+ return this.listGroups().filter((group) => group.namespace === namespace);
52
+ }
53
+
54
+ listNamespaces(): string[] {
55
+ return [...new Set(this.listGroups().map((group) => group.namespace))].sort();
56
+ }
57
+
58
+ lookup(groupId: string, operation: string, version: number): CapabilityLookup | undefined {
59
+ const group = this.groups.get(groupId);
60
+ if (!group || group.version !== version) return undefined;
61
+ const op = group.operations.find((item) => item.operation === operation);
62
+ if (!op) return undefined;
63
+ return { group, operation: op };
64
+ }
65
+
66
+ resolveCall(call: string): CapabilityCallResolution | null {
67
+ const trimmed = call.trim();
68
+ if (!trimmed) return null;
69
+
70
+ const parts = trimmed.split(".");
71
+ for (let splitAt = parts.length - 1; splitAt >= 1; splitAt -= 1) {
72
+ const groupId = parts.slice(0, splitAt).join(".");
73
+ const operation = parts.slice(splitAt).join(".");
74
+ const group = this.groups.get(groupId);
75
+ if (!group) continue;
76
+ const match = group.operations.find((item) => item.operation === operation);
77
+ if (!match) continue;
78
+ return {
79
+ node: groupId,
80
+ operation,
81
+ version: group.version,
82
+ };
83
+ }
84
+
85
+ return null;
86
+ }
87
+
88
+ listNodeDefinitions(): NodeDefinition[] {
89
+ if (!this.catalog) return [];
90
+ return nodeDefinitionsFromCatalog(this.catalog);
91
+ }
92
+
93
+ listOperationSignatures(groupId?: string): string[] {
94
+ const groups = groupId
95
+ ? this.listGroups().filter((group) => group.id === groupId)
96
+ : this.listGroups();
97
+ const signatures: string[] = [];
98
+ for (const group of groups) {
99
+ for (const operation of group.operations) {
100
+ signatures.push(operation.authoring_signature);
101
+ }
102
+ }
103
+ return signatures.sort();
104
+ }
105
+ }
106
+
107
+ export function resolveCapabilityCall(
108
+ call: string,
109
+ registry: CapabilityRegistry,
110
+ ): CapabilityCallResolution | null {
111
+ return registry.resolveCall(call);
112
+ }
@@ -0,0 +1,14 @@
1
+ import type { CapabilityCatalog, CapabilityGroupJson } from "./types";
2
+
3
+ export function isCapabilityCatalog(value: unknown): value is CapabilityCatalog {
4
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
5
+ const candidate = value as CapabilityCatalog;
6
+ return candidate.version === "capability-catalog.v1" && Array.isArray(candidate.groups);
7
+ }
8
+
9
+ export function flattenCapabilityOperations(catalog: CapabilityCatalog): CapabilityGroupJson[] {
10
+ return catalog.groups.map((group) => ({
11
+ ...group,
12
+ operations: [...group.operations].sort((a, b) => a.operation.localeCompare(b.operation)),
13
+ })).sort((a, b) => a.id.localeCompare(b.id));
14
+ }
@@ -0,0 +1,30 @@
1
+ import { defineNode, defineOperation } from "../registry/defineNode";
2
+ import type { NodeDefinition } from "../ir/types";
3
+ import type { CapabilityCatalog, CapabilityGroupJson, CapabilityOperationJson } from "./types";
4
+ import { flattenCapabilityOperations } from "./catalogSchema";
5
+
6
+ function operationFromJson(group: CapabilityGroupJson, operation: CapabilityOperationJson) {
7
+ return defineOperation({
8
+ label: operation.name,
9
+ docs: operation.description,
10
+ inputSchema: operation.input_schema,
11
+ outputSchema: operation.output_schema ?? { type: "object" },
12
+ sideEffects: operation.side_effects,
13
+ });
14
+ }
15
+
16
+ export function nodeDefinitionsFromCatalog(catalog: CapabilityCatalog): NodeDefinition[] {
17
+ return flattenCapabilityOperations(catalog).map((group) => defineNode({
18
+ id: group.id,
19
+ version: group.version,
20
+ label: group.label,
21
+ category: group.category,
22
+ docs: group.docs,
23
+ operations: Object.fromEntries(
24
+ group.operations.map((operation) => [
25
+ operation.operation,
26
+ operationFromJson(group, operation),
27
+ ]),
28
+ ),
29
+ }));
30
+ }
@@ -0,0 +1,35 @@
1
+ import type { CapabilityCatalog } from "./types";
2
+ import { CapabilityRegistry } from "./capabilityRegistry";
3
+ import { generateCapabilityBrief } from "./agentBrief";
4
+
5
+ export type {
6
+ CapabilityCallResolution,
7
+ CapabilityCatalog,
8
+ CapabilityGroupJson,
9
+ CapabilityLookup,
10
+ CapabilityOperationJson,
11
+ EngineBindingJson,
12
+ } from "./types";
13
+
14
+ export { isCapabilityCatalog, flattenCapabilityOperations } from "./catalogSchema";
15
+ export { nodeDefinitionsFromCatalog } from "./fromCatalog";
16
+ export { CapabilityRegistry, resolveCapabilityCall } from "./capabilityRegistry";
17
+ export { generateCapabilityBrief, generateCapabilityBriefFromCatalog } from "./agentBrief";
18
+
19
+ export function createCapabilityRegistry(catalog?: CapabilityCatalog | null): CapabilityRegistry {
20
+ if (!catalog) return new CapabilityRegistry();
21
+ return CapabilityRegistry.fromCatalog(catalog);
22
+ }
23
+
24
+ export function mergeNodeRegistryDefinitions(
25
+ baseDefinitions: import("../ir/types").NodeDefinition[],
26
+ catalog?: CapabilityCatalog | null,
27
+ ): import("../ir/types").NodeDefinition[] {
28
+ if (!catalog) return baseDefinitions;
29
+ const registry = CapabilityRegistry.fromCatalog(catalog);
30
+ const byId = new Map(baseDefinitions.map((node) => [node.id, node]));
31
+ for (const node of registry.listNodeDefinitions()) {
32
+ byId.set(node.id, node);
33
+ }
34
+ return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
35
+ }
@@ -0,0 +1,57 @@
1
+ import type { JsonSchema, SideEffectKind } from "../ir/types";
2
+
3
+ /** JSON-serializable engine binding stored in DB/API/MCP artifacts. */
4
+ export type EngineBindingJson = {
5
+ node_type: string;
6
+ operation?: string;
7
+ parameter_map?: Record<string, string>;
8
+ static_parameters?: Record<string, unknown>;
9
+ /** Special adapter hint for compile-time transforms (e.g. dypai.db SQL builders). */
10
+ adapter?: "dypai.db" | "dypai.email";
11
+ };
12
+
13
+ export type CapabilityOperationJson = {
14
+ id: string;
15
+ group_id: string;
16
+ operation: string;
17
+ name: string;
18
+ description?: string;
19
+ authoring_signature: string;
20
+ input_schema: JsonSchema;
21
+ output_schema?: JsonSchema;
22
+ required_credentials?: string[];
23
+ side_effects?: SideEffectKind[];
24
+ examples?: Record<string, unknown>[];
25
+ engine_binding: EngineBindingJson;
26
+ };
27
+
28
+ export type CapabilityGroupJson = {
29
+ id: string;
30
+ namespace: string;
31
+ resource: string;
32
+ version: number;
33
+ label: string;
34
+ category: string;
35
+ provider?: string;
36
+ docs?: string;
37
+ operations: CapabilityOperationJson[];
38
+ };
39
+
40
+ export type CapabilityCatalog = {
41
+ version: "capability-catalog.v1";
42
+ engine_version?: string;
43
+ content_hash?: string;
44
+ synced_at?: string;
45
+ groups: CapabilityGroupJson[];
46
+ };
47
+
48
+ export type CapabilityCallResolution = {
49
+ node: string;
50
+ operation: string;
51
+ version: number;
52
+ };
53
+
54
+ export type CapabilityLookup = {
55
+ group: CapabilityGroupJson;
56
+ operation: CapabilityOperationJson;
57
+ };