UNPKG

trellis

Version:

Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications

661 lines (657 loc) 23.3 kB
// src/core/ontology/registry.ts var OntologyRegistry = class { schemas = /* @__PURE__ */ new Map(); /** Resolved entity defs (with inherited attributes merged). */ resolvedEntities = /* @__PURE__ */ new Map(); /** Resolved relation defs. */ resolvedRelations = /* @__PURE__ */ new Map(); /** * Register an ontology schema. * Throws if an ontology with the same ID is already registered at the same version. */ register(schema) { const existing = this.schemas.get(schema.id); if (existing && existing.version === schema.version) { throw new Error( `Ontology "${schema.id}" v${schema.version} is already registered.` ); } this.schemas.set(schema.id, schema); this._resolve(schema); } /** * Unregister an ontology by ID. */ unregister(id) { const schema = this.schemas.get(id); if (!schema) return; for (const [name, entry] of this.resolvedEntities) { if (entry.ontologyId === id) { this.resolvedEntities.delete(name); } } for (const [name, entries] of this.resolvedRelations) { const filtered = entries.filter((e) => e.ontologyId !== id); if (filtered.length === 0) { this.resolvedRelations.delete(name); } else { this.resolvedRelations.set(name, filtered); } } this.schemas.delete(id); } /** * Get a registered ontology schema by ID. */ get(id) { return this.schemas.get(id); } /** * List all registered ontology schemas. */ list() { return [...this.schemas.values()]; } /** * Get the resolved entity definition for an entity type name. * Returns the entity def with inherited attributes merged in. */ getEntityDef(typeName) { return this.resolvedEntities.get(typeName)?.def; } /** * Get the ontology ID that defines a given entity type. */ getEntityOntology(typeName) { return this.resolvedEntities.get(typeName)?.ontologyId; } /** * List all known entity type names. */ listEntityTypes() { return [...this.resolvedEntities.keys()]; } /** * Get all relation definitions involving a given entity type * (either as source or target). */ getRelationsForType(typeName) { const results = []; for (const [, entries] of this.resolvedRelations) { for (const entry of entries) { if (entry.def.sourceTypes.includes(typeName) || entry.def.targetTypes.includes(typeName)) { results.push(entry.def); } } } return results; } /** * Get a specific relation definition by name. */ getRelationDef(name) { const entries = this.resolvedRelations.get(name); return entries?.[0]?.def; } /** * List all known relation names. */ listRelationNames() { return [...this.resolvedRelations.keys()]; } /** * Check if an entity type is known to any registered ontology. */ hasEntityType(typeName) { return this.resolvedEntities.has(typeName); } /** * Get the required attributes for an entity type. */ getRequiredAttributes(typeName) { const def = this.getEntityDef(typeName); if (!def) return []; return def.attributes.filter((a) => a.required); } // ------------------------------------------------------------------------- // Resolution (inheritance) // ------------------------------------------------------------------------- _resolve(schema) { for (const entity of schema.entities) { const resolved = this._resolveEntity(entity, schema); this.resolvedEntities.set(entity.name, { def: resolved, ontologyId: schema.id }); } for (const relation of schema.relations) { const existing = this.resolvedRelations.get(relation.name) ?? []; existing.push({ def: relation, ontologyId: schema.id }); this.resolvedRelations.set(relation.name, existing); } } _resolveEntity(entity, schema) { if (!entity.extends) return entity; let parent = schema.entities.find((e) => e.name === entity.extends); if (!parent) { const resolved = this.resolvedEntities.get(entity.extends); parent = resolved?.def; } if (!parent) { throw new Error( `Entity "${entity.name}" extends "${entity.extends}" which is not defined.` ); } const resolvedParent = this._resolveEntity(parent, schema); const childAttrNames = new Set(entity.attributes.map((a) => a.name)); const mergedAttrs = [ ...resolvedParent.attributes.filter((a) => !childAttrNames.has(a.name)), ...entity.attributes ]; return { ...entity, attributes: mergedAttrs }; } }; // src/core/ontology/builtins.ts var projectOntology = { id: "trellis:project", name: "Project Ontology", version: "1.0.0", description: "Entity types for software project management.", entities: [ { name: "Project", description: "A software project or repository.", attributes: [ { name: "name", type: "string", required: true, description: "Project name" }, { name: "description", type: "string", description: "Project description" }, { name: "status", type: "string", enum: ["active", "archived", "draft", "deprecated"], default: "active" }, { name: "url", type: "string", description: "Project URL or repository link" }, { name: "language", type: "string", description: "Primary programming language" }, { name: "createdAt", type: "date", description: "Creation timestamp" }, { name: "updatedAt", type: "date", description: "Last update timestamp" } ] }, { name: "Module", description: "A logical module or package within a project.", attributes: [ { name: "name", type: "string", required: true }, { name: "path", type: "string", description: "Filesystem path relative to project root" }, { name: "description", type: "string" } ] }, { name: "Feature", description: "A product feature or capability.", attributes: [ { name: "name", type: "string", required: true }, { name: "description", type: "string" }, { name: "status", type: "string", enum: ["planned", "in-progress", "shipped", "cut"], default: "planned" }, { name: "priority", type: "string", enum: ["critical", "high", "medium", "low"], default: "medium" } ] }, { name: "Dependency", description: "An external dependency or library.", attributes: [ { name: "name", type: "string", required: true }, { name: "version", type: "string" }, { name: "registry", type: "string", description: "Package registry (npm, pypi, etc.)" }, { name: "scope", type: "string", enum: ["runtime", "dev", "optional"], default: "runtime" } ] }, { name: "Config", description: "A configuration entry or setting.", attributes: [ { name: "key", type: "string", required: true }, { name: "value", type: "any", required: true }, { name: "description", type: "string" }, { name: "scope", type: "string", enum: ["project", "user", "system"], default: "project" } ] }, { name: "Artifact", description: "A build artifact, release asset, or output file.", attributes: [ { name: "name", type: "string", required: true }, { name: "path", type: "string" }, { name: "size", type: "number" }, { name: "hash", type: "string" }, { name: "format", type: "string" } ] }, { name: "Release", description: "A versioned release of a project.", attributes: [ { name: "version", type: "string", required: true }, { name: "tag", type: "string" }, { name: "date", type: "date" }, { name: "notes", type: "string" }, { name: "status", type: "string", enum: ["draft", "published", "yanked"], default: "draft" } ] } ], relations: [ { name: "contains", sourceTypes: ["Project"], targetTypes: ["Module", "Feature", "Config"], cardinality: "many", description: "Project contains modules/features/configs" }, { name: "dependsOn", sourceTypes: ["Project", "Module"], targetTypes: ["Dependency", "Module", "Project"], cardinality: "many", description: "Depends on another entity" }, { name: "implementedBy", sourceTypes: ["Feature"], targetTypes: ["Module"], cardinality: "many", description: "Feature is implemented by modules" }, { name: "produces", sourceTypes: ["Project", "Release"], targetTypes: ["Artifact"], cardinality: "many", description: "Produces artifacts" }, { name: "releases", sourceTypes: ["Project"], targetTypes: ["Release"], cardinality: "many", description: "Project has releases" } ] }; var teamOntology = { id: "trellis:team", name: "Team Ontology", version: "1.0.0", description: "Entity types for team and developer organization.", entities: [ { name: "Team", description: "A team or organizational group.", attributes: [ { name: "name", type: "string", required: true }, { name: "description", type: "string" }, { name: "slug", type: "string", description: "URL-safe identifier" } ] }, { name: "Developer", description: "A developer or contributor.", attributes: [ { name: "name", type: "string", required: true }, { name: "email", type: "string" }, { name: "handle", type: "string", description: "Username or handle" }, { name: "role", type: "string", enum: ["admin", "maintainer", "contributor", "reviewer"] } ] }, { name: "Role", description: "A named role with specific permissions.", attributes: [ { name: "name", type: "string", required: true }, { name: "description", type: "string" }, { name: "permissions", type: "string", unique: false, description: "Permission strings (multi-valued)" } ] }, { name: "Capability", description: "A skill or capability.", attributes: [ { name: "name", type: "string", required: true }, { name: "category", type: "string" }, { name: "level", type: "string", enum: ["beginner", "intermediate", "advanced", "expert"] } ] } ], relations: [ { name: "hasMember", sourceTypes: ["Team"], targetTypes: ["Developer"], cardinality: "many", inverse: "memberOf", description: "Team has member" }, { name: "memberOf", sourceTypes: ["Developer"], targetTypes: ["Team"], cardinality: "many", inverse: "hasMember", description: "Developer is member of team" }, { name: "owns", sourceTypes: ["Developer"], targetTypes: ["Project", "Module"], cardinality: "many", description: "Developer owns/maintains" }, { name: "reviewsFor", sourceTypes: ["Developer"], targetTypes: ["Project", "Module"], cardinality: "many", description: "Developer reviews for" }, { name: "hasCapability", sourceTypes: ["Developer"], targetTypes: ["Capability"], cardinality: "many", description: "Developer has capability" }, { name: "hasRole", sourceTypes: ["Developer"], targetTypes: ["Role"], cardinality: "many", description: "Developer has role" }, { name: "assignedTo", sourceTypes: ["Developer"], targetTypes: ["Feature"], cardinality: "many", description: "Developer is assigned to feature" } ] }; var agentOntology = { id: "trellis:agent", name: "Agent Ontology", version: "1.0.0", description: "Entity types for AI agents, runs, plans, and tools.", entities: [ { name: "Agent", description: "An AI agent definition.", attributes: [ { name: "name", type: "string", required: true }, { name: "description", type: "string" }, { name: "model", type: "string", description: "LLM model identifier" }, { name: "provider", type: "string", description: "LLM provider (openai, anthropic, local, etc.)" }, { name: "systemPrompt", type: "string" }, { name: "status", type: "string", enum: ["active", "inactive", "deprecated"], default: "active" } ] }, { name: "AgentCapability", description: "A capability or skill an agent possesses.", attributes: [ { name: "name", type: "string", required: true }, { name: "description", type: "string" }, { name: "category", type: "string" } ] }, { name: "AgentRun", description: "A single execution run of an agent.", attributes: [ { name: "startedAt", type: "date", required: true }, { name: "completedAt", type: "date" }, { name: "status", type: "string", enum: ["running", "plan_pending", "paused", "completed", "failed", "cancelled"], default: "running" }, { name: "input", type: "string" }, { name: "output", type: "string" }, { name: "totalTokens", type: "number" }, { name: "promptTokens", type: "number" }, { name: "completionTokens", type: "number" } ] }, { name: "AgentPlan", description: "A plan or strategy created by an agent.", attributes: [ { name: "title", type: "string", required: true }, { name: "description", type: "string" }, { name: "status", type: "string", enum: ["draft", "active", "completed", "abandoned"], default: "draft" } ] }, { name: "Tool", description: "A tool available to agents.", attributes: [ { name: "name", type: "string", required: true }, { name: "description", type: "string" }, { name: "schema", type: "string", description: "JSON schema for tool parameters" }, { name: "endpoint", type: "string" } ] } ], relations: [ { name: "hasCapability", sourceTypes: ["Agent"], targetTypes: ["AgentCapability"], cardinality: "many" }, { name: "hasTool", sourceTypes: ["Agent"], targetTypes: ["Tool"], cardinality: "many" }, { name: "executedBy", sourceTypes: ["AgentRun"], targetTypes: ["Agent"], cardinality: "one" }, { name: "hasPlan", sourceTypes: ["AgentRun"], targetTypes: ["AgentPlan"], cardinality: "many" }, { name: "usedTool", sourceTypes: ["AgentRun"], targetTypes: ["Tool"], cardinality: "many" }, { name: "createdBy", sourceTypes: ["AgentPlan"], targetTypes: ["Agent"], cardinality: "one" } ] }; var builtinOntologies = [ projectOntology, teamOntology, agentOntology ]; // src/core/ontology/validator.ts function validateEntity(entityId, store, registry) { const errors = []; const warnings = []; const facts = store.getFactsByEntity(entityId); if (facts.length === 0) { return { valid: true, errors: [], warnings: [] }; } const typeFact = facts.find((f) => f.a === "type"); if (!typeFact) { warnings.push({ entityId, entityType: "(unknown)", field: "type", message: 'Entity has no "type" attribute.', severity: "warning" }); return { valid: true, errors, warnings }; } const entityType = String(typeFact.v); const def = registry.getEntityDef(entityType); if (!def) { warnings.push({ entityId, entityType, field: "type", message: `Entity type "${entityType}" is not defined in any registered ontology.`, severity: "warning" }); return { valid: true, errors, warnings }; } if (def.abstract) { errors.push({ entityId, entityType, field: "type", message: `Cannot instantiate abstract entity type "${entityType}".`, severity: "error" }); } for (const attr of def.attributes) { if (attr.required && attr.name !== "type") { const hasFact = facts.some((f) => f.a === attr.name); if (!hasFact) { errors.push({ entityId, entityType, field: attr.name, message: `Required attribute "${attr.name}" is missing.`, severity: "error" }); } } } for (const fact of facts) { if (fact.a === "type" || fact.a === "createdAt" || fact.a === "updatedAt") continue; const attrDef = def.attributes.find((a) => a.name === fact.a); if (!attrDef) { warnings.push({ entityId, entityType, field: fact.a, message: `Attribute "${fact.a}" is not defined in the "${entityType}" ontology.`, severity: "warning" }); continue; } const typeErr = validateAttrType(fact.v, attrDef); if (typeErr) { errors.push({ entityId, entityType, field: fact.a, message: typeErr, severity: "error" }); } if (attrDef.enum && !attrDef.enum.includes(fact.v)) { errors.push({ entityId, entityType, field: fact.a, message: `Value "${fact.v}" is not in allowed values: [${attrDef.enum.join(", ")}].`, severity: "error" }); } if (attrDef.pattern && typeof fact.v === "string") { if (!new RegExp(attrDef.pattern).test(fact.v)) { errors.push({ entityId, entityType, field: fact.a, message: `Value "${fact.v}" does not match pattern /${attrDef.pattern}/.`, severity: "error" }); } } if (attrDef.min !== void 0) { if (typeof fact.v === "number" && fact.v < attrDef.min) { errors.push({ entityId, entityType, field: fact.a, message: `Value ${fact.v} is below minimum ${attrDef.min}.`, severity: "error" }); } if (typeof fact.v === "string" && fact.v.length < attrDef.min) { errors.push({ entityId, entityType, field: fact.a, message: `String length ${fact.v.length} is below minimum ${attrDef.min}.`, severity: "error" }); } } if (attrDef.max !== void 0) { if (typeof fact.v === "number" && fact.v > attrDef.max) { errors.push({ entityId, entityType, field: fact.a, message: `Value ${fact.v} exceeds maximum ${attrDef.max}.`, severity: "error" }); } if (typeof fact.v === "string" && fact.v.length > attrDef.max) { errors.push({ entityId, entityType, field: fact.a, message: `String length ${fact.v.length} exceeds maximum ${attrDef.max}.`, severity: "error" }); } } } const links = store.getLinksByEntity(entityId); for (const link of links) { if (link.e1 !== entityId) continue; const relDef = registry.getRelationDef(link.a); if (!relDef) continue; if (!relDef.sourceTypes.includes(entityType)) { errors.push({ entityId, entityType, field: link.a, message: `Entity type "${entityType}" is not allowed as source for relation "${link.a}".`, severity: "error" }); } const targetFacts = store.getFactsByEntity(link.e2); const targetType = targetFacts.find((f) => f.a === "type"); if (targetType && !relDef.targetTypes.includes(String(targetType.v))) { errors.push({ entityId, entityType, field: link.a, message: `Target type "${targetType.v}" is not allowed for relation "${link.a}" (expected: ${relDef.targetTypes.join(", ")}).`, severity: "error" }); } } return { valid: errors.length === 0, errors, warnings }; } function validateStore(store, registry) { const allErrors = []; const allWarnings = []; const typeFacts = store.getFactsByAttribute("type"); const entityIds = new Set(typeFacts.map((f) => f.e)); for (const entityId of entityIds) { const result = validateEntity(entityId, store, registry); allErrors.push(...result.errors); allWarnings.push(...result.warnings); } return { valid: allErrors.length === 0, errors: allErrors, warnings: allWarnings }; } function validateAttrType(value, def) { if (def.type === "any") return null; switch (def.type) { case "string": if (typeof value !== "string") return `Expected string, got ${typeof value}.`; break; case "number": if (typeof value !== "number") return `Expected number, got ${typeof value}.`; break; case "boolean": if (typeof value !== "boolean") return `Expected boolean, got ${typeof value}.`; break; case "date": if (typeof value === "string") { if (isNaN(Date.parse(value))) return `Expected ISO date string, got "${value}".`; } else if (!(value instanceof Date)) { return `Expected date, got ${typeof value}.`; } break; case "ref": if (typeof value !== "string") return `Expected entity reference (string), got ${typeof value}.`; break; } return null; } function createValidationMiddleware(registry, options) { const strict = options?.strict ?? false; return { name: "ontology-validator", handleOp: (op, ctx, next) => { if (op.facts && op.facts.length > 0) { for (const fact of op.facts) { if (fact.a === "type") continue; if (fact.a === "createdAt" || fact.a === "updatedAt") continue; const typeFact = op.facts.find((f) => f.e === fact.e && f.a === "type"); if (!typeFact) continue; const entityType = String(typeFact.v); const def = registry.getEntityDef(entityType); if (!def) { if (strict) { throw new Error( `[ontology-validator] Unknown entity type "${entityType}" for entity "${fact.e}".` ); } continue; } const attrDef = def.attributes.find((a) => a.name === fact.a); if (!attrDef) { if (strict) { throw new Error( `[ontology-validator] Unknown attribute "${fact.a}" for type "${entityType}".` ); } continue; } const typeErr = validateAttrType(fact.v, attrDef); if (typeErr) { throw new Error( `[ontology-validator] Entity "${fact.e}" attribute "${fact.a}": ${typeErr}` ); } if (attrDef.enum && !attrDef.enum.includes(fact.v)) { throw new Error( `[ontology-validator] Entity "${fact.e}" attribute "${fact.a}": value "${fact.v}" not in [${attrDef.enum.join(", ")}].` ); } } } if (op.links && op.links.length > 0) { for (const link of op.links) { const relDef = registry.getRelationDef(link.a); if (!relDef) continue; const sourceTypeFact = op.facts?.find( (f) => f.e === link.e1 && f.a === "type" ); if (sourceTypeFact && !relDef.sourceTypes.includes(String(sourceTypeFact.v))) { throw new Error( `[ontology-validator] Relation "${link.a}": source type "${sourceTypeFact.v}" not allowed (expected: ${relDef.sourceTypes.join(", ")}).` ); } } } return next(op, ctx); } }; } export { OntologyRegistry, projectOntology, teamOntology, agentOntology, builtinOntologies, validateEntity, validateStore, createValidationMiddleware };