Skip to content

Read runs through the typed client and add the run core - #115

Open
knihit wants to merge 11 commits into
wc/05b-themefrom
wc/06a-run-core
Open

knihit wants to merge 11 commits into
wc/05b-themefrom
wc/06a-run-core

Conversation

@knihit

@knihit knihit commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Description

Part 1 of 3 of the Execution tab (#116 is part 2, #93 part 3). This part adds the code that reads and models a run. It has no UI: nothing here changes what the page shows.

API client (src/api/):

  • WorkflowApi gains startRun and getRun. createHttpApi sends the goal as { "params": { "goal": ... } }, reads run_id, and reads a run into an ordered list of stages. A reply that breaks the contract is a bad reply: a run id outside the RunId pattern, a status it does not name, an exit_code that is not an integer or null, a stage name twice in order, a stage in stages that order lacks, or a status that does not fit the exit code and the stage states (running with an exit code, passed with a stage not passed, halted without exactly one stage blocked). The fix of an error body is added to its sentence. A start with no answer, or a 2xx start whose reply cannot be read, is told to the core as RunStartUnsure: the run may have started. The run id is encoded in the path. ApiError carries the run_id of an error body when it has the shape of a run id; the contract uses it in the 409 already_running. Each run call takes an AbortSignal, and every call gives up after 15 seconds; a body that stops arriving counts as no answer (status 0).
  • loadWorkflow ties the run calls to the session token, so only those calls hold it. It turns a 409 already_running with a run id into RunAlreadyActive, and a 4xx or unreadable reply on a read into RunReadRefused.

Run core (src/execution/core/, no IO):

  • The state of the tab and its reducer, startAndFollow (starts a run, or follows the one that is already active, then reads it every 1000 ms until it is no longer running; a read that fails with no answer, a 5xx, 408 or 429 is tried again, up to 3 in a row, while a refused read or a reading of another run ends the run with an error), the state of each node, the status sentences, and the goal check (1 to 2048 characters).
  • A failed event names the run it belongs to, and the reducer ignores one that names another run or none once a run is being followed, so a second loop that was refused cannot end the state of the first.
  • Its ports are RunPort (start and read a run, each with a signal) and Scheduler (wait for some milliseconds). Tests use a fake scheduler that they move by hand, so no test sleeps.
  • test-support/execution-purity.test.ts holds the core to the same rule as the graph core: it imports only its own modules, the graph core and the wire types, and reads no clock, randomness, environment or network.

Docs: the README names the four calls of the port, the retry and timeout rules and the purity test of the run core.

Graph (src/graph/): withStatuses adds a status to the nodes it names. buildGraph is unchanged.

Type of Change

  • Other: the run core and API client of the workflow canvas, in workflow-canvas/

Testing

  • Root npm test (tests/scripts/**/*.test.js): not run; this PR changes only workflow-canvas/.
  • If cli/ changed: not applicable
  • Smoke tested affected agent(s): not applicable, no agent changed
  • If an agent's or skill's behavior changed: not applicable
  • New or changed code files carry the required SPDX header (see AGENTS.md's License headers rule)

Checked in workflow-canvas/ at b4e0f06: npm run lint, npm run typecheck and npm test (52 files, 886 tests) pass. The e2e suite is run on the top of the stack, as no page changes here.

Tests added: getRun and startRun (request shape, token, encoded id, parsing, bad replies including a wrong exit_code and a repeated stage name, error sentences, the run_id of a 409); the reducer (every state, an event that does not fit is ignored, a reading or a failure of another run is ignored); startAndFollow with the fake scheduler (polls every 1000 ms, no wait before the first read, stops on passed and on halted, retries and gives up, a refused read, joining an active run, two overlapping loops, a read that never answers and is aborted, nothing emitted after an abort); the HTTP adapter with a signal, a timeout, a dropped body and a hung body; node states for a running, passed and halted run; status sentences; the goal check; withStatuses; the no-IO rule of the core.

Checklist

  • My code follows the style guidelines of this project (see AGENTS.md's Code Style & Conventions)
  • I have performed a self-review of my own changes
  • I have commented my code where necessary
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added or updated tests that prove my change works (see Testing above for the exact commands)

Notes for Reviewers

Fixes from the second adversarial review

Fixes from the third adversarial review

  • A start that got no clear answer ends as an error that says the run may have started (RunStartUnsure, maybeStarted), instead of "did not start".
  • 408 and 429 are tried again like a 5xx; a run_id in an error body must look like a run id.
  • FollowRequest says the caller runs one loop at a time, and startAndFollow says it rejects only when emit throws. The README says "each run call" takes a signal.
  • Rebased onto the new wc/05b-theme.
  • Rejected: the nits N2 to N7 of the first re-review (test helper loops, a positional constructor, repeated unions, a nested ternary in a fixture, the queued name for a node that did not run, trimming the goal): no behaviour.

Restack

Round 6

Rebased on #85. A 401 on a run start or a run read now clears the kept session through the guard added in #83 and fails with the "server no longer knows this session" message (before, only the load did). New tests: start and read forget the session, a failure that is not a 401 keeps it, a newer session is not forgotten. The earlier test that expected the raw 401 sentence on a read is replaced.

Round 7: no change to the product code. One test of the refused-address notice builds its API with the helper that has every method.

Round 8: rebased on #85. The tests added in #83 for a pasted session build their API with the helper that has every method. npm test (55 files, 969 tests), lint and typecheck pass.

Round 9: rebased on #85. One test of the pending session builds its API with the helper that has every method. npm test (55 files, 980 tests), lint and typecheck pass.

@knihit
knihit added this pull request to stack #117 October 27, 2026 22:42
@knihit
knihit force-pushed the wc/06a-run-core branch 2 times, most recently from 1b8d610 to 47fe83a Compare October 28, 2026 14:31
@knihit
knihit marked this pull request as ready for review October 28, 2026 15:37
@knihit
knihit force-pushed the wc/06a-run-core branch 2 times, most recently from c5274b7 to 3e0ed33 Compare October 28, 2026 18:31
knihit added 11 commits October 29, 2026 02:40
The API client gains startRun and getRun, parsed against the contract. The
run core has no IO: the state of a run, the loop that starts and follows it
through a clock port, the state of each node and the status sentences. A
test holds the core to the same no-IO rule as the graph core.
ApiError carries the run_id of the error body, so a caller can find the
run that is already active. A run reply with an exit_code that is not an
integer or null, or with a stage name twice in order, is a bad reply
instead of being read as null or as two stages of one name.
A reading of another run ends the loop with a failed event instead of
leaving the tab in the following state. The abort listener of the fake
scheduler is removed once it fires, the abort test builds its own loop
so it asserts the whole event list, and the README describes the four
calls of the port and the purity test of the run core.
- A failure names the run it belongs to, so a loop that lost cannot end the
  state of the one that won.
- A start refused with 409 already_running and a run id follows that run.
- A read that fails with no answer or a 5xx is tried again, up to three in a
  row. Any other 4xx, or a reply the page cannot read, ends at once.
- Every call takes an AbortSignal and gives up after 15 seconds. A body that
  stops arriving counts as no answer.
- A run reply must hold together: id pattern, exit code and stage states for
  its status, and the stages in order. A 2xx start with an unreadable reply
  says the run may have started. The fix of an error body is shown.
… run core docs

A start with no answer or an unreadable accepted reply now ends as an error
that says the run may have started. A run_id in an error body must look like a
run id. FollowRequest says one loop runs at a time, and startAndFollow says it
rejects only when emit throws.
… session

A run start or read that the server answers with 401 now clears the kept token through the same guard as the load, and fails with the same message. The test fakes of the new session tests carry the run calls.
The api of a test now carries the run calls, so the two tests that resume a pasted session and keep a quiet notice build it with apiWith like their neighbours.

This branch has not been deployed

No deployments
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.

1 participant