Skip to content

feat(workflow): event model extensions and shared node primitives (Part 1) - #587

Open
kalenkevich wants to merge 4 commits into
mainfrom
feat/workflows_part1
Open

feat(workflow): event model extensions and shared node primitives (Part 1)#587
kalenkevich wants to merge 4 commits into
mainfrom
feat/workflows_part1

Conversation

@kalenkevich

@kalenkevich kalenkevich commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

2. Or, if no issue exists, describe the change:

Problem:
The feature/workflows branch is a single, very large change (41 commits, ~15k lines across ~155 files) that is impractical to review or merge as one unit. It also carries an initial workflow implementation that a later rewrite fully replaced, so a straight commit-by-commit review would surface dead code.

Solution:
Split the work into small, stacked, dependency-ordered PRs recomposed from the final tree state (not a replay of the 41 commits), so each PR is coherent and reviewable and reviewers never see the superseded implementation.

This is Part 1 of 9. It adds only the leaf primitives the engine builds on — none depend on the engine core — plus the additive event-model changes the engine needs. There is no orchestration, node, or runner wiring yet, so there is nothing user-facing to exercise beyond the new types.

Included:

  • Workflow leaf primitives (core/src/workflow/): errors.ts (NodeInterruptedError, NodeTimeoutError, DynamicNodeFailError), node_status.ts (NodeStatus), node_state.ts (NodeState, createNodeState, isNodeState), retry_config.ts (RetryConfig, ErrorClass, normalizeRetryExceptions), utils/retry_utils.ts (shouldRetryNode, getRetryDelaySeconds), trigger.ts (Trigger), branch_path.ts (BranchPath), utils/event_channel.ts (EventChannel).
  • Event model (additive, mirrors adk-python): events/event.ts adds Event.{output, route, nodeInfo, isolationScope}, the NodeInfo type, CreateEventParams, an isEvent guard, and PRESERVE_KEYS entries so node output and checkpointed agentState survive snake/camel round-trips; events/event_actions.ts adds optional fields (output, agentState, endOfAgent, route, …); common.ts exports the new event types.

Note for reviewers: event.ts changes are purely additive vs main. The branch predates main's relocation of the client-function-call-id helpers into event.ts; the file was 3-way merged so those helpers are preserved and only the workflow fields are added (the 2 removed lines are the intended createEvent signature change, not a reversion).

Intentionally deferred to later parts: engine core (Part 2), node types (Part 3), parallelism (Part 4), dynamic scheduling (Part 5), workflow orchestrator + runner + public barrel export (Part 6), LLM-as-node + task mode (Part 7), HITL (Part 8), samples (Part 9). The finish_task_tool export in common.ts and the workflow/index.js export in core/src/index.ts are deferred to the PRs that introduce those files.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

Bundled tests: workflow/foundations_test.ts, workflow/event_channel_test.ts, workflow/event_model_test.ts, events/event_test.ts.

$ npx vitest run --project unit:core \
    core/test/events/event_test.ts \
    core/test/workflow/event_model_test.ts \
    core/test/workflow/event_channel_test.ts \
    core/test/workflow/foundations_test.ts

 ✓ core/test/workflow/event_model_test.ts   (2 tests)
 ✓ core/test/workflow/event_channel_test.ts (6 tests)
 ✓ core/test/workflow/foundations_test.ts   (17 tests)
 ✓ core/test/events/event_test.ts           (38 tests)

 Test Files  4 passed (4)
      Tests  63 passed (63)

Typecheck is clean: npx tsc --noEmit -p core/tsconfig.json.

Manual End-to-End (E2E) Tests:

N/A — this part adds only leaf primitives and additive types with no user-facing behavior. End-to-end coverage arrives with the runner integration in Part 6.

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

@kalenkevich kalenkevich linked an issue Jul 30, 2026 that may be closed by this pull request
@kalenkevich kalenkevich self-assigned this Jul 30, 2026

@AmaadMartin AmaadMartin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read the whole thing against the head tree. The stacking strategy is the right call — recomposing from the final tree instead of replaying 41 commits is what makes this reviewable, and the note about the 3-way merge on event.ts saved me from misreading the 2 removed lines as a reversion. Tests are real (the retry tests inject the RNG rather than asserting on randomness), and CI is green on all three OS legs.

One real bug, then a few type-discipline points that I'm raising because they're the same standard this repo holds contributors to.

There's also a behaviour change in here that the description undersells — see the note on event.ts:135. Reviewers of Parts 2-9 will want to know it landed in Part 1.

Comment thread core/src/events/event_actions.ts Outdated
Comment thread core/src/events/event_actions.ts Outdated
Comment thread core/src/events/event_actions.ts Outdated
Comment thread core/src/events/event.ts
Comment thread core/src/events/event.ts
constructor(message = 'Node interrupted (awaiting resume input).') {
super(message);
this.name = 'NodeInterruptedError';
// Restore prototype chain for `instanceof` across transpilation targets.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit — instanceof as the dispatch mechanism, in the one place it's known to break.

    // Restore prototype chain for `instanceof` across transpilation targets.
    Object.setPrototypeOf(this, NodeInterruptedError.prototype);

The setPrototypeOf calls are correct for what they do. The question is the strategy: this is the objection you raised on #364"Problem with instanceof is that when user will have multiple adk-js packages in their runtime it will not able to mix objects from one to another" — which is why isGeminiModel-style guards exist instead.

These three errors are caught across the node/engine boundary (NodeInterruptedError by the parent's NodeRunner, DynamicNodeFailError likewise), which is exactly where two copies of the package would meet: the throw site resolves one class, the catch site another, and the instanceof silently returns false — the node hangs instead of resuming.

Exporting guards alongside the classes would keep the catch sites copy-safe:

export function isNodeInterruptedError(e: unknown): e is NodeInterruptedError {
  return e instanceof Error && e.name === 'NodeInterruptedError';
}

Fine to defer to Part 2 where the catch sites actually land, but better decided before three more parts are written against instanceof.

Comment thread core/src/workflow/branch_path.ts Outdated
Comment thread core/src/workflow/utils/event_channel.ts Outdated
Comment thread core/src/workflow/utils/retry_utils.ts Outdated
Comment thread core/src/workflow/utils/retry_utils.ts Outdated

@AmaadMartin AmaadMartin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up: one finding I missed on the first pass. I checked each new primitive against what already exists in core/src and EventChannel has a near-twin — details inline on event_channel.ts.

I also chased three other hypotheses and they came back clean, so recording them here rather than filing noise: the RetryConfig.exceptions string-or-class union and the exact-name (non-subclass) matching in shouldRetryNode both faithfully mirror _retry_config.py / _retry_utils.py — Python does exactly the same via field_validator and type(exception).__name__, so no parity divergence; the new Trigger collides with nothing on main (only ContextCompactionTrigger exists, unrelated); and a diff-hygiene scan is clean — no CHANGELOG/lockfile churn, no stray artifacts, zero console.log, zero .only/.skip, zero type suppressions, and event_test.ts is +45/-0 so no existing test was rewritten.

Comment thread core/src/workflow/utils/event_channel.ts Outdated
kalenkevich added a commit that referenced this pull request Jul 30, 2026
Resolves review comments on #587:

- events: drop 6 unused EventActions fields (output, joinCompleted,
  toolExecution, requestInput, nodeExecutionReplay, route) that were dead
  across the whole branch and left arbitrary `unknown` payloads unpreserved;
  removing them also eliminates the round-trip hole and the ambiguous second
  `route` carrier. Add shared RouteKey/Route types for Event.route.
- errors: add name-based guards (isNodeInterruptedError, isNodeTimeoutError,
  isDynamicNodeFailError) so catch sites stay correct across duplicate-package
  boundaries instead of relying on `instanceof`.
- branch_path: convert createSubBranch/commonPrefixOf from static methods to
  standalone util functions (kept fromString/commonPrefix as factories).
- async_queue: fold EventChannel into the existing AsyncQueue instead of
  shipping a duplicate queue — port drain-before-error, sticky failure, and
  error()-closes; add isClosed/size getters and a fail() method (error()
  retained as an alias). Delete event_channel.ts and move its tests. This also
  fixes the close()-then-fail() error-swallow bug.
- retry: introduce prepareRetryConfig/PreparedRetryConfig so a node's exception
  filter is normalized+validated once at construction, not re-normalized (and
  potentially thrown) on every retry check; make shouldRetryNode/
  getRetryDelaySeconds take a required prepared config and drop the unreachable
  no-config branches (and the misleading test that pinned one).

Full core suite green (2338 tests).
@kalenkevich kalenkevich changed the title feat(workflow): event model extensions and shared node primitives (Part 1/9) feat(workflow): event model extensions and shared node primitives (Part 1) Jul 30, 2026
First slice of the workflow engine split (Part 1/9). Adds the leaf
primitives the engine builds on, none of which depend on the engine core:

- workflow/errors, node_status, node_state, retry_config,
  utils/retry_utils, trigger, branch_path, and utils/event_channel
- Event model extensions mirroring adk-python: Event.{output, route,
  nodeInfo, isolationScope}, the NodeInfo type, CreateEventParams, an
  isEvent guard, and additive optional EventActions fields; PRESERVE_KEYS
  entries so node output and checkpointed state survive snake/camel
  round-trips
- export the new event types from common.ts

Bundled tests: foundations, event_channel, event_model, and event.
Recomposed from the final feature/workflows tree onto current main
(3-way merged event.ts to preserve main's relocated function-call-id
helpers). Stacked PRs follow.
Address PR1 review feedback: identify Event objects via a
Symbol.for('google.adk.event') brand — set by createEvent and checked by
isEvent — matching the signature-symbol guards used across ADK (isBaseTool,
isBaseAgent, ...) instead of structural duck-typing.

The brand is declared optional on the Event interface and is a
non-serializable runtime marker, so events reconstructed from storage/session
payloads are intentionally unbranded (documented on the interface and covered
by a round-trip test). Adds isEvent unit tests (branded event, impostor
rejection, non-object rejection, round-trip brand drop).
Resolves review comments on #587:

- events: drop 6 unused EventActions fields (output, joinCompleted,
  toolExecution, requestInput, nodeExecutionReplay, route) that were dead
  across the whole branch and left arbitrary `unknown` payloads unpreserved;
  removing them also eliminates the round-trip hole and the ambiguous second
  `route` carrier. Add shared RouteKey/Route types for Event.route.
- errors: add name-based guards (isNodeInterruptedError, isNodeTimeoutError,
  isDynamicNodeFailError) so catch sites stay correct across duplicate-package
  boundaries instead of relying on `instanceof`.
- branch_path: convert createSubBranch/commonPrefixOf from static methods to
  standalone util functions (kept fromString/commonPrefix as factories).
- async_queue: fold EventChannel into the existing AsyncQueue instead of
  shipping a duplicate queue — port drain-before-error, sticky failure, and
  error()-closes; add isClosed/size getters and a fail() method (error()
  retained as an alias). Delete event_channel.ts and move its tests. This also
  fixes the close()-then-fail() error-swallow bug.
- retry: introduce prepareRetryConfig/PreparedRetryConfig so a node's exception
  filter is normalized+validated once at construction, not re-normalized (and
  potentially thrown) on every retry check; make shouldRetryNode/
  getRetryDelaySeconds take a required prepared config and drop the unreachable
  no-config branches (and the misleading test that pinned one).

Full core suite green (2338 tests).
@kalenkevich
kalenkevich force-pushed the feat/workflows_part1 branch from 92b4c12 to 953242b Compare July 30, 2026 23:54
typedoc `docs:check` runs with --treatWarningsAsErrors and flagged the
[EVENT_SIGNATURE_SYMBOL] doc comment (inherited by Event, CreateEventParams and
CompactedEvent) for linking to `isEvent`, which is not part of the exported/
documented API. Reference it as inline code instead of a doc link. No API or
behavior change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for Workflows

3 participants