Feat: Workflow graph, chain syntax and validation (Part 2/3) - #321
Closed
AmaadMartin wants to merge 2 commits into
Closed
Feat: Workflow graph, chain syntax and validation (Part 2/3)#321AmaadMartin wants to merge 2 commits into
AmaadMartin wants to merge 2 commits into
Conversation
added 2 commits
July 30, 2026 11:08
Introduces core/src/workflow/ with the node contract a graph-based workflow is built from: BaseNode and the START sentinel, FunctionNode for wrapping a plain function, JoinNode for fan-in, NodeContext (the per-run context nodes report output and route on), and the node() factory. Ported from adk-python's src/google/adk/workflow/. Node output and route travel on the context rather than on Event, because adk-js's Event carries neither field. Part 1 of 3; the graph and the executable Workflow follow.
Adds Graph, Edge and the chain syntax that expands [START, a, [b, c]] into edges, conditional route selection with a DEFAULT_ROUTE fallback, and the structural validators: duplicate names and edges, a missing or re-entered START, routed START edges, unreachable nodes, DEFAULT_ROUTE misuse, and cycles made only of unrouted edges. Ported from adk-python's workflow/_graph.py, utils/_graph_parser.py and utils/_graph_validation.py. Part 2 of 3; the executable Workflow follows.
AmaadMartin
force-pushed
the
feat/workflow-graph-core-part2
branch
from
July 30, 2026 18:08
9a2653b to
132829a
Compare
Owner
Author
|
Closing: a teammate is actively working on workflow orchestration, and this stack would duplicate that work. Closed by the Foundry supervisor at the owner's request — not a quality judgement. The branch |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
Related: Support for Workflows google/adk-js#366
Problem: With the node model from Part 1 in place, there is still no way to describe how nodes connect: no edges, no conditional routing, and no validation to catch a graph that cannot run (a missing entry point, an unreachable node, a cycle with no way out).
Solution: Add
Graph,Edgeand the chain syntax, plus the structural validators, ported fromadk-python'sworkflow/_graph.py,utils/_graph_parser.pyandutils/_graph_validation.py.Graphderives its nodes from its edges — deduplicated by object identity, in first-seen order — and computes its terminal nodes (those with no outgoing edge).getNextPendingNodes(name, routes)selects the edges to follow: unrouted edges always fire; a routed edge fires when the emitted route matches any of its declared routes; theDEFAULT_ROUTEedge fires only when nothing else matched; and if a node has routed edges but nothing matched at all, it logs a warning and ends that branch.[START, a, [b, c], d]expands to unrouted edges from every node of one step to every node of the next, so fan-out and fan-in are one array. A bare function in a chain is wrapped once and memoised by identity, so referencing it twice yields one graph node.validate()rejects, in order: duplicate node names, a missingSTART, routed edges out ofSTART, nodes unreachable fromSTART, incoming edges intoSTART, duplicate edges,DEFAULT_ROUTEmisuse (in a list, or twice from one node), and cycles made only of unrouted edges. Every message starts withGraph validation failed.so callers can match on it. An empty graph is trivially valid, which is what makes an empty workflow run and produce nothing.Stacked PR — this is Part 2/3. It targets
feat/workflow-graph-core-part1(#320), notmain. Part 3 addsrunNodeand the executableWorkflow.Collision check —
gh pr list --repo AmaadMartin/adk-js --state open --limit 200was scanned forworkflow|graph|orchestrat. The four hits are unrelated (#306 and #296 are CI npm caching, #184 is the eval service, #153 is context-cache orchestration). No open PR touchescore/src/workflow/.Deliberate deviations from adk-python
getNextPendingNodesreturnsBaseNode[], not names. The caller always needs the node itself; returning names forced a reverse lookup whose "node not found" arm was unreachable and therefore untestable. This is internal to the process, so local convention wins over wire-shape parity.RoutingMapchain sugar ({route: node}inside a chain) is not ported. A JS object literal coerces numeric and boolean keys to strings, which would silently break strict-equality route matching. Routed edges are written as explicitEdgeobjects, which covers every routing behaviour the reference tests exercise. Queued as a follow-up.Edgetakes nodes, notNodeLike. Matchingadk-python, where pydantic validatesfrom_node/to_nodeasBaseNode. Wrap a function withnode(fn)first. Chains still accept bare functions._validate_static_schemasand_validate_chat_agent_wiringare not ported — schema validation andLlmAgent-as-node are out of scope for this port.validate()as inadk-python, sovalidate()is a pure check andterminalNodeNamesis always populated.Testing Plan
Please describe the tests that you ran to verify your changes. This is required for all PRs that are not small documentation or typo fixes.
Unit Tests:
[x] I have added or updated unit tests for my change.
[x] All unit tests pass locally.
npx vitest run --project unit:core core/test/workflow— 7 files, 66 tests, all passing (33 new here, on top of Part 1's 33).New suites:
graph_test.ts(24 cases: node derivation and ordering, terminal nodes, all seven route-selection behaviours including the unmatched-route warning captured throughsetLogger, and each of the eight validation failures asserted on its message) andgraph_parser_test.ts(9 cases: chain expansion, fan-out, fan-in, the'START'literal, function memoisation by identity, mixing explicit edges with chains, and edge copying).Coverage. Measured with
npx vitest run --project unit:core --coverage --coverage.include='core/src/workflow/**' core/test/workflow:graph.ts,graph_parser.ts,graph_validation.tsandroute.tsare at 100% statements / branches / functions / lines, as is everything from Part 1.retry_config.tsstill reports 0% at this point in the stack because it contains only type declarations, which v8 erases before it can count them; it reaches 100% in Part 3.Proof the new tests can fail. Each was run against mutated source and confirmed to fail:
graph.ts: drop the!matchedSpecificRouteguard soDEFAULT_ROUTEalways firesGraph.getNextPendingNodes > skips the DEFAULT_ROUTE edge when a specific route matchedexpected [ 'b', 'c' ] to deeply equal [ 'b' ]graph_validation.ts: skipdetectUnconditionalCyclesGraph.validate > rejects a cycle made only of unrouted edgesexpected [Function] to throw an errorgraph_validation.ts: make the cycle walk'spath.indexOfalways return-1Graph.validate > rejects a cycle made only of unrouted edgesexpected [Function] to throw error including 'Graph validation failed. Unconditiona…' but got 'Maximum call stack size exceeded'One mutation deliberately survived and led to a code change rather than a new test: excluding
STARTfrom the terminal-node computation turned out to be unreachable, because a graph that passes validation always has at least one edge out ofSTART. The guard was removed instead of pinning dead code with a test.No suppressions were added: no
any,as any,as never,@ts-expect-error,@ts-ignore,eslint-disableor coverage ignores, insrc/or in the tests.graph_parser_test.tsimports everything relatively, with a comment explaining why:parseEdgeItemsis internal, and mixing a relative import with@google/adkwould load a second copy of the node classes that TypeScript treats as unrelated.Manual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.
No network, credentials or model access are needed.
npm installnpm run buildnpx vitest run --project unit:core core/test/workflownpm run lint && npm run format:checknpm run docs:check— confirms every new public type is exported and documented.Graph construction can be inspected directly in a REPL:
The graph is exercised end to end by the executable
Workflowin Part 3 of this stack.CI status: absent. This PR targets
feat/workflow-graph-core-part1rather thanmain, and.github/workflows/validation.yamltriggers onpull_request: branches: [main], so the test job never fires for a stacked base. Validated locally instead, on the exact pushed commit:npm run build,npm run lint,npm run format:check,npm run docs:checkandnpx vitest run --project unit:core core/test/workflowall pass.Checklist
[x] I have read the CONTRIBUTING.md document.
[x] I have performed a self-review of my own code.
[x] I have commented my code, particularly in hard-to-understand areas.
[x] I have added tests that prove my fix is effective or that my feature works.
[x] New and existing unit tests pass locally with my changes.