diff --git a/AGENTS.md b/AGENTS.md index 4dc524f..5ec4f77 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,60 +1,86 @@ -## Memory Tools (via @alkdev/open-memory plugin) +# @alkdev/flowgraph — Agent Guide -You have access to two tools for managing your context and accessing session history: +Project-specific context for AI agents working on this codebase. -### memory({tool: "...", args: {...}}) +## What This Is -Read-only tool for introspecting your session history and context state. Available operations: -- `memory({tool: "help"})` — full reference with examples -- `memory({tool: "summary"})` — quick counts of projects, sessions, messages, todos -- `memory({tool: "sessions"})` — list recent sessions (useful for finding past work) -- `memory({tool: "children", args: {sessionId: "ses_..."}})` — list sub-agent sessions spawned from a parent -- `memory({tool: "messages", args: {sessionId: "..."}})` — read a session's conversation -- `memory({tool: "messages", args: {sessionId: "...", role: "assistant"}})` — read only assistant messages -- `memory({tool: "messages", args: {sessionId: "...", showTools: true}})` — include tool-call output -- `memory({tool: "message", args: {messageId: "msg_..."}})` — read a single message by ID -- `memory({tool: "search", args: {query: "..."}})` — search across all conversations -- `memory({tool: "compactions", args: {sessionId: "..."}})` — view compaction checkpoints -- `memory({tool: "context"})` — check your current context window usage +DAG-based workflow orchestration library. Wraps graphology `DirectedGraph`, enforces DAG invariants, provides ujsx template composition and reactive signal-driven execution. Sits between `@alkdev/operations` (what can be called) and `@alkdev/alkhub` (what was called) — defines *how calls are orchestrated*. -### memory_compact() +## Build & Test Commands -Trigger compaction on the current session. This summarizes the conversation so far to free context space. +```bash +npm run build # tsup (ESM + CJS + dts + sourcemaps) +npm run build:tsc # tsc type-check only +npm run lint # tsc --noEmit +npm run test # vitest run +npm run test:watch # vitest watch +npm run test:coverage # vitest run --coverage +``` -**When to use memory_compact:** -- When context is above 80% (check with `memory({tool: "context"})`) -- When you notice you're losing track of earlier conversation details -- At natural breakpoints in multi-step tasks (after completing a subtask, before starting a new one) -- When the system prompt shows a yellow/red/critical context warning -- Proactively, rather than waiting for automatic compaction at 92% +Always run `npm run lint && npm run test` after making changes. -**When NOT to use memory_compact:** -- When context is below 50% (it wastes a compaction cycle) -- In the middle of a complex edit that you need immediate context for -- When the task is nearly complete (just finish the task instead) +## Source Structure -Compaction preserves your most important context in a structured summary — you will continue the session with the summary as your starting point. +``` +src/ + index.ts # Barrel — re-exports all sub-modules + component/ # ujsx workflow components (Operation, Sequential, Parallel, Conditional, Map) + host/ # HostConfig implementations (GraphologyHostConfig, ReactiveHostConfig) + schema/ # TypeBox schemas, enums, node/edge attribute types + graph/ # FlowGraph class (construction, mutation, query, serialization) + reactive/ # WorkflowReactiveRoot, signal-driven status, event log projection + analysis/ # Pure functions: typeCompat, validate, topologicalOrder, parallelGroups, criticalPath + error/ # FlowgraphError hierarchy +``` -## Worktree Tool (via @alkimiadev/open-coordinator plugin) +## Subpath Exports -You have access to the `worktree` tool for git worktree management and session coordination. Call with `{action, args}`: +The package has 8 export subpaths. Root `@alkdev/flowgraph` re-exports everything. Subpath imports make dependencies explicit: -### Coordinator Operations (available when session is not spawned by another session) +| Subpath | Key Exports | +|---------|-------------| +| `/graph` | `FlowGraph`, `FlowGraphOptions`, `OperationSpec`, `CallEventMapValue` | +| `/schema` | `CallStatus`, `NodeStatus`, `EdgeType`, `OperationType`, all node/edge attribute types | +| `/component` | `Operation`, `Sequential`, `Parallel`, `Conditional`, `Map` | +| `/host` | `GraphologyHostConfig`, `ReactiveHostConfig` | +| `/analysis` | `typeCompat`, `buildTypeEdges`, `validateGraph`, `validateSchema`, `validate`, `validateTemplate`, `topologicalOrder`, `parallelGroups`, `criticalPath` | +| `/reactive` | `WorkflowReactiveRoot`, `EventLogProjection`, `WorkflowNode`, `FailurePolicy` | +| `/error` | `FlowgraphError`, `ConstructionError`, `CycleError`, `InvalidInputError`, `InvalidTransitionError` | -- `worktree({action: "list"})` — List git worktrees -- `worktree({action: "dashboard"})` — Worktree dashboard with session info -- `worktree({action: "spawn", args: {tasks: [...], prompt: "..."}})` — Spawn parallel worktrees + sessions -- `worktree({action: "sessions"})` — Query spawned session status -- `worktree({action: "message", args: {sessionID: "...", message: "..."}})` — Message a session -- `worktree({action: "abort", args: {sessionID: "..."}})` — Abort a session -- `worktree({action: "cleanup", args: {action: "remove", pathOrBranch: "..."}})` — Remove worktree -- `worktree({action: "help"})` — Show all available operations +## Key Patterns -### Implementation Operations (available when session is spawned by a coordinator) +- **TypeBox schema + Static type pairs**: Every schema is exported as both a const (runtime) and an inferred type. `const FooSchema = Type.Object({...}); type Foo = Static;` +- **Delegation model**: `FlowGraph` wraps a graphology `DirectedGraph` (does not extend it). `flowGraph.graph` is an escape hatch that bypasses validation. +- **DAG enforcement**: `addEdge()` throws `CycleError` if the edge would create a cycle. `fromJSON()` validates DAG invariants on deserialization. +- **Event log as source of truth**: Call protocol events (`call.requested`, `call.responded`, `call.error`, `call.aborted`, `call.completed`) are appended to `WorkflowReactiveRoot`. Status/results are derived projections. +- **Signal-driven execution**: `@preact/signals-core` powers `WorkflowReactiveRoot`. `preconditions`, `canStart`, `blockedByFailure` are `ReadonlySignal` computed from predecessor status. +- **`dispose()` is mandatory**: `WorkflowReactiveRoot.dispose()` must be called to release signal subscriptions. -- `worktree({action: "current"})` — Show your worktree mapping -- `worktree({action: "notify", args: {message: "...", level: "info|blocking"}})` — Report to coordinator -- `worktree({action: "status"})` — Show worktree git status +## Architecture Docs -The plugin auto-injects `workdir` for bash commands when the session is mapped to a worktree. +`docs/architecture/` contains detailed specs (all `status: reviewed`): +- `README.md` — overview, relationship to sibling packages, design decisions +- `flowgraph-api.md` — FlowGraph class full API +- `consumer-integration.md` — end-to-end integration walkthrough (5 phases) +- `schema.md` — TypeBox Module, all node/edge attribute schemas +- `operation-graph.md` — static graph from OperationSpecs +- `call-graph.md` — dynamic graph from call events +- `workflow-templates.md` — ujsx components, composition rules, template→DAG hydration +- `host-configs.md` — GraphologyHostConfig, ReactiveHostConfig +- `reactive-execution.md` — signal-driven status propagation, lifecycle, error boundaries +- `analysis.md` — type-compatibility checking, precondition validation, execution ordering +- `error-handling.md` — FlowgraphError hierarchy +- `build-distribution.md` — package structure, exports map +- `decisions/` — ADRs 001–006 + +Consult these for anything non-trivial. The README is surface-level; architecture docs are the specification. + +## Constraints for Agent Modifications + +- **Never add cycles** — this is a DAG-only library. Any edge that would create a cycle must throw `CycleError`. +- **Never mutate operation graphs after construction** — `fromSpecs()` graphs are conventionally immutable. +- **Keep schema as pure data** — `src/schema/` contains TypeBox schemas and types only, no runtime logic. +- **Keep analysis as pure functions** — `src/analysis/` functions take a `FlowGraph` or `DirectedGraph` as input and return results without side effects. +- **Maintain the delegation model** — `FlowGraph` delegates to graphology. Don't expose raw graphology methods that could violate DAG invariants without explicit validation. +- **tsup builds all subpath entries** — `tsup.config.ts` lists every subpath. If you add a new top-level module, add the entry there and update `package.json` exports. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8a0efbe --- /dev/null +++ b/LICENSE @@ -0,0 +1,6 @@ +This project is dual-licensed under either of the following: + +- MIT License (LICENSE-MIT) +- Apache License 2.0 (LICENSE-APACHE) + +You may choose either license at your option. \ No newline at end of file diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..0977a4b --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable by + such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding any notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed as + modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limitation damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2026 alkdev + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..525b510 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 alkdev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9160dbe --- /dev/null +++ b/README.md @@ -0,0 +1,320 @@ +# @alkdev/flowgraph + +DAG-based workflow orchestration over graphology, with ujsx template composition and reactive signal-driven execution. + +## What This Does + +Flowgraph sits between `@alkdev/operations` (which defines *what can be called*) and `@alkdev/alkhub` (which records *what was called*). Flowgraph defines **how calls are orchestrated** — the structure, validation, and execution of workflows. + +Three conceptual graphs, each for a different purpose: + +1. **Operation Graph** — static graph built from `OperationSpec`s at startup. Nodes are operations, edges are type-compatibility relationships. Enables cycle detection, topological ordering, and validation. +2. **Call Graph** — dynamic graph built from call protocol events at runtime. Nodes are call invocations with status/timestamps, edges are parent-child relationships. Enables abort cascading and observability. +3. **Workflow Template** — declarative ujsx tree defining a reusable workflow structure. A validated path through the operation graph, instantiated as a call graph at runtime. + +**The graph is the specification. The template is the authoring surface. The call graph is the execution record.** + +## Installation + +```bash +npm install @alkdev/flowgraph +``` + +Peer dependency: `@alkdev/operations ^0.1.0` + +## Quick Start + +### Build an Operation Graph + +```typescript +import { FlowGraph } from "@alkdev/flowgraph/graph"; +import type { OperationSpec } from "@alkdev/flowgraph/graph"; + +const specs: OperationSpec[] = [ + { namespace: "task", name: "classify", type: "query", version: "1.0.0", inputSchema: {...}, outputSchema: {...} }, + { namespace: "task", name: "enrich", type: "query", version: "1.0.0", inputSchema: {...}, outputSchema: {...} }, + { namespace: "task", name: "summarize", type: "mutation", version: "1.0.0", inputSchema: {...}, outputSchema: {...} }, +]; + +const graph = FlowGraph.fromSpecs(specs); +// Type-compatibility edges added automatically +graph.hasEdge("task.classify", "task.enrich"); +``` + +### Define a Workflow Template + +```typescript +import { h } from "@alkdev/ujsx"; +import { Operation, Sequential, Parallel, Conditional } from "@alkdev/flowgraph/component"; + +const template = h(Sequential, {}, + h(Operation, { name: "task.classify" }), + h(Conditional, { + test: (results) => results["task.classify"].output.confidence > 0.8, + }, + h(Parallel, {}, + h(Operation, { name: "task.enrich" }), + h(Operation, { name: "task.summarize" }), + ), + h(Operation, { name: "task.classify" }), + ), +); +``` + +### Validate the Template + +```typescript +import { validateTemplate } from "@alkdev/flowgraph/analysis"; + +const errors = validateTemplate(template, graph); +if (errors.length > 0) { + for (const error of errors) { + console.error(`[${error.type}]`, error); + } +} +``` + +### Populate a Call Graph from Events + +```typescript +import { FlowGraph } from "@alkdev/flowgraph/graph"; + +const callGraph = FlowGraph.fromCallEvents(eventArray); +callGraph.filterByStatus("running"); +callGraph.children("req_abc123"); +callGraph.lineage("req_xyz789"); +callGraph.duration("req_abc123"); +``` + +### Drive Reactive Execution + +```typescript +import { WorkflowReactiveRoot } from "@alkdev/flowgraph/reactive"; + +const workflow = new WorkflowReactiveRoot(dag, { + failurePolicy: "abort-dependents", +}); + +// Append call protocol events — status derives reactively +workflow.append({ type: "call.requested", requestId: "req_1", operationId: "task.classify", input: {}, timestamp: "..." }); +workflow.append({ type: "call.responded", requestId: "req_1", output: { confidence: 0.95 }, timestamp: "..." }); + +// Read reactive state +workflow.getStatus("task.enrich"); +workflow.getResult("task.classify"); + +// Abort cascading +workflow.abortAll(); +workflow.dispose(); +``` + +## Subpath Exports + +| Subpath | Purpose | Key Exports | +|---------|---------|-------------| +| `@alkdev/flowgraph` | Root — re-exports everything | All public types and functions | +| `@alkdev/flowgraph/graph` | Core DAG class | `FlowGraph`, `FlowGraphOptions`, `OperationSpec`, `CallEventMapValue` | +| `@alkdev/flowgraph/schema` | TypeBox schemas and types | `CallStatus`, `NodeStatus`, `EdgeType`, `OperationType`, `CallNodeAttrs`, `OperationNodeAttrs`, `OperationEdgeAttrs`, `CallEdgeAttrs`, `TemplateEdgeAttrs`, `CallResult`, `FlowGraphSerialized` | +| `@alkdev/flowgraph/component` | ujsx workflow components | `Operation`, `Sequential`, `Parallel`, `Conditional`, `Map` | +| `@alkdev/flowgraph/host` | Rendering backends | `GraphologyHostConfig`, `ReactiveHostConfig` | +| `@alkdev/flowgraph/analysis` | Validation and analysis functions | `typeCompat`, `buildTypeEdges`, `validateGraph`, `validateSchema`, `validate`, `validateTemplate`, `validatePreconditions`, `topologicalOrder`, `parallelGroups`, `criticalPath`, `reachableFrom` | +| `@alkdev/flowgraph/reactive` | Reactive execution engine | `WorkflowReactiveRoot`, `EventLogProjection`, `WorkflowNode`, `ReactiveContext`, `FailurePolicy`, `AggregateStatus` | +| `@alkdev/flowgraph/error` | Error hierarchy | `FlowgraphError`, `ConstructionError`, `DuplicateNodeError`, `DuplicateEdgeError`, `NodeNotFoundError`, `CycleError`, `InvalidInputError`, `InvalidTransitionError` | + +## Core API: FlowGraph Class + +`FlowGraph` wraps a graphology `DirectedGraph` and enforces DAG invariants. It delegates graph operations to graphology while adding flowgraph-specific construction, mutation, and query methods. + +### Factory Methods + +```typescript +FlowGraph.fromSpecs(specs: OperationSpec[]): OperationGraph +FlowGraph.fromCallEvents(events: CallEventMapValue[]): CallGraph +FlowGraph.fromJSON(data: FlowGraphSerialized): FlowGraph +``` + +### Node Operations + +```typescript +graph.addNode(key, attrs) // throws DuplicateNodeError +graph.removeNode(key) // throws NodeNotFoundError +graph.updateNode(key, partialAttrs) // throws NodeNotFoundError +graph.hasNode(key): boolean +graph.getNodeAttributes(key): NodeAttrs +graph.forEachNode(callback): void +``` + +### Edge Operations + +```typescript +graph.addEdge(source, target, attrs?) // throws NodeNotFoundError, DuplicateEdgeError, CycleError +graph.removeEdge(source, target) // no-op if not found +graph.hasEdge(source, target): boolean +graph.getEdgeAttributes(source, target): EdgeAttrs +graph.forEachEdge(callback): void +``` + +### Traversal + +```typescript +graph.topologicalOrder(): string[] +graph.ancestors(nodeId): string[] +graph.descendants(nodeId): string[] +graph.predecessors(nodeId): string[] +graph.successors(nodeId): string[] +graph.reachableFrom(nodeIds): Set +graph.hasCycles(): boolean +graph.findCycles(): string[][] +``` + +### Call Graph Convenience + +```typescript +graph.addCall(attrs: CallNodeAttrs): void +graph.addDependency(source, target): void +graph.updateStatus(requestId, status, extra?): void // throws InvalidTransitionError +graph.updateCall(requestId, partialAttrs): void +graph.removeCall(requestId): void +graph.updateFromEvent(event: CallEventMapValue): void +graph.filterByStatus(status: CallStatus): string[] +graph.getRoots(): string[] +graph.children(requestId): string[] +graph.duration(requestId): number +graph.lineage(requestId): string[] +``` + +### Serialization + +```typescript +graph.export(): FlowGraphSerialized +graph.toJSON(): FlowGraphSerialized +graph.toString(): string +``` + +### Escape Hatch + +```typescript +graph.graph // → DirectedGraph (raw graphology instance) +``` + +Direct mutation via `graph.graph` bypasses flowgraph validation. Use with caution. + +## Schema Enums + +| Enum | Values | +|------|--------| +| `CallStatus` | `pending`, `running`, `completed`, `failed`, `aborted` | +| `NodeStatus` | `idle`, `waiting`, `ready`, `running`, `completed`, `failed`, `skipped`, `aborted` | +| `EdgeType` | `triggered`, `depends_on`, `typed`, `sequential`, `conditional` | +| `OperationType` | `query`, `mutation`, `subscription` | + +Call status transitions: `pending → running → completed|failed|aborted`. Terminal states are immutable. `InvalidTransitionError` is thrown on invalid transitions. + +## Workflow Components + +| Component | Props | Behavior | +|-----------|-------|----------| +| `` | `name`, `input?`, `retries?`, `timeout?` | Declares an operation node in the workflow | +| `` | `id?` | Children execute in order; edges are `sequential` | +| `` | `id?`, `maxConcurrency?` | Children execute concurrently | +| `` | `test`, `else?` | Branches on `test(results)`. Children = then-branch, `else` prop = else-branch | +| `` | `over`, `as` | Iterates over `over` collection, binding each item as `as` variable | + +## Analysis Functions + +```typescript +import { typeCompat, validateTemplate, topologicalOrder, parallelGroups, criticalPath } from "@alkdev/flowgraph/analysis"; + +typeCompat(outputSchema, inputSchema): TypeCompatResult | undefined +validateTemplate(template, operationGraph): AnyValidationError[] +topologicalOrder(graph): string[] +parallelGroups(graph): string[][] // topological generations +criticalPath(graph): string[] // longest path +validateGraph(graph): GraphValidationError[] +validateSchema(graph, schema): ValidationError[] +validate(graph, schema): AnyValidationError[] // combined +``` + +## Reactive Execution + +`WorkflowReactiveRoot` implements `EventLogProjection` — call protocol events are the source of truth, status/results are derived projections. + +```typescript +const workflow = new WorkflowReactiveRoot(dag, { + failurePolicy: "abort-dependents", // or "continue-running" + parallelGroups: { group1: { siblings: ["a", "b"], maxConcurrency: 2 } }, +}); + +// Per-node reactive signals +workflow.statusMap // Map> +workflow.preconditions // Map> — all predecessors completed/skipped +workflow.canStart // Map> — preconditions + concurrency +workflow.blockedByFailure // Map> — any predecessor failed/aborted +workflow.resultMap // Map> + +// Event-driven updates +workflow.append(event: CallEventMapValue): void + +// Queries +workflow.getStatus(nodeId): NodeStatus +workflow.getResult(nodeId): CallResult | undefined +workflow.isComplete(): boolean +workflow.getAggregateStatus(): AggregateStatus + +// Lifecycle +workflow.abortAll(): void +workflow.abortNode(nodeId): void +workflow.dispose(): void // mandatory cleanup — releases signal subscriptions +``` + +## Error Hierarchy + +``` +FlowgraphError (base) +├── ConstructionError +│ ├── DuplicateNodeError (readonly key) +│ ├── DuplicateEdgeError (readonly source, target) +│ ├── NodeNotFoundError (readonly key) +│ ├── CycleError (readonly cycles: string[][]) +│ └── InvalidInputError (readonly errors: ValidationError[]) +└── InvalidTransitionError (readonly requestId, from, to) +``` + +## Design Principles + +1. **DAG-only, no cycles** — `addEdge()` rejects cycle-creating edges at mutation time (ADR-002). This differs from taskgraph, which allows cycles and detects them after the fact. + +2. **Storage is decoupled** — flowgraph handles in-memory graph construction, validation, and analysis. Persistence is the caller's concern. `export()`/`fromJSON()` provides the serialization boundary. + +3. **Template → DAG → Execution is a pipeline** — each representation serves a different phase and can exist independently. Validate a template without executing it. Build a call graph from events without a template. Run reactive execution directly from a DAG. + +4. **Event log as source of truth** — call protocol events (`call.requested`, `call.responded`, `call.error`, `call.aborted`, `call.completed`) are the ground truth. Status, results, and the call graph are projections derived from the event log (ADR-005). + +5. **Delegation, not inheritance** — `FlowGraph` wraps a graphology `DirectedGraph`, exposing a curated API. The raw graphology instance is available via the `.graph` escape hatch. + +## For AI Agents + +When working with this library programmatically: + +- **Use subpath imports** — `@alkdev/flowgraph/graph`, `@alkdev/flowgraph/analysis`, etc. The root export re-exports everything but subpath imports make dependencies explicit. +- **Always call `dispose()` on `WorkflowReactiveRoot`** — signal subscriptions leak without it. +- **Function-valued props don't survive JSON serialization** — `Conditional.test` and `Map.over` with function values need runtime resolution. Use string references for stored templates. +- **`fromSpecs()` graphs are conventionally immutable** — don't mutate operation graphs after construction. If the registry changes, rebuild via `fromSpecs()`. +- **Call graph mutation uses event protocol** — use `updateFromEvent()` or `addCall()`/`updateStatus()`, not direct node mutation. +- **`typeCompat()` returns `undefined` for `unknown`/`any` schemas** — this means "no meaningful check possible", not "incompatible". +- **Architecture specs are in `docs/architecture/`** — detailed design decisions, ADRs, and open questions live there. This README is a surface-level guide. Consult the architecture docs for anything non-trivial. + +## Dependencies + +| Package | Relationship | +|---------|-------------| +| `graphology` | Direct — the underlying directed graph data structure | +| `graphology-dag` | Direct — topological sort, cycle detection, DAG traversal | +| `@alkdev/ujsx` | Direct — `UNode` trees and `HostConfig` for workflow template rendering | +| `@alkdev/typebox` | Direct — all schemas are TypeBox Modules | +| `@preact/signals-core` | Direct — reactive state management for `WorkflowReactiveRoot` | +| `@alkdev/operations` | **Peer** — provides `OperationSpec`, `OperationRegistry`, call event types | + +## License + +Dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE) at your option. \ No newline at end of file diff --git a/package.json b/package.json index c792a93..b44c386 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,15 @@ "operations" ], "license": "MIT OR Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://git.alk.dev/alkdev/flowgraph.git" + }, + "homepage": "https://git.alk.dev/alkdev/flowgraph", + "bugs": { + "url": "https://git.alk.dev/alkdev/flowgraph/issues" + }, + "sideEffects": false, "dependencies": { "@alkdev/typebox": "^0.34.49", "@alkdev/ujsx": "^0.1.0",