Skip to content

feat(global): a shell performs a run's jobs one at a time, and six seams a shell asks for - #321

Merged
rami-hatoum merged 7 commits into
mainfrom
refactor/the-core-performs-calls-outside-the-server
Oct 25, 2026
Merged

rami-hatoum merged 7 commits into
mainfrom
refactor/the-core-performs-calls-outside-the-server

Conversation

@rami-hatoum

@rami-hatoum rami-hatoum commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

What this changes

Seven seams a program that performs a run's calls outside the server asked for, each on its own terms.

  • A shell hands out a run's jobs one at a time, from the facts. The reasoning capability exports its steps: stepRecordingOf and stateOfTheAttempt build the state from the run's facts and kept content, jobsOf answers the jobs the state asks for or the ending, performedJob performs one through PerformJob, and answerRecorded records its answer and folds the next state. A shell can perform one model call or one tool call per invocation and never hold an attempt in memory; the Node loop is built from the same steps and behaves as before.
  • A call reaches the context a shell makes of its request. ApiOptions.callContext gives every operation route and MCP endpoint a context of the request, and the tool access and the consent client send a call's requests through the fetch that context carries, one access per fetch, kept while the fetch lives.
  • The identity library of one provider loads through an option the server passes, and nothing reachable from the main or testing entries imports it.
  • The stream reader and writer over any streams are exported from the ledger, so a ledger over lent streams restates nothing.
  • The fakes' handlers are exported apart from their loopback server, so a test on another platform uses the fakes without Node's server; the Node side lives in served-fakes entries of the tool-servers and secrets packages.
  • A waiting call is dispatched again before its deadline ends it: after dispatchedAgainAfterMs since it was last dispatched, the host performs it again, telling the attempt before died, until it is answered or its deadline cancels it; the due time is kept in the calls table.

Tests

  • A run driven one job at a time from the facts gives the same facts, requests and ending as the loop; the context's identity through the MCP hand-off; no module of the main or testing entries reaching the identity library; a ledger over lent streams; one access per fetch and its sessions gone after the call; the testing entry reaching neither node:http nor node:stream; a call dispatched again and answered once.
  • pnpm check passes on each commit, with the PostgreSQL suites running, and the npm package's self-check imports every subpath.

BEGIN_COMMIT_OVERRIDE
feat(reasoning): a shell hands out a run's jobs one at a time, from the facts (#321)
feat(api): a call reaches the context a shell makes of its request (#321)
refactor(reasoning): one provider's identity library loads through an option the server passes (#321)
feat(ledger): the stream reader and writer over any streams are exported (#321)
feat(tool-servers): the tool access and the consent client send a call's requests through its fetch (#321)
refactor(tool-servers): the fakes' handlers are exported apart from their loopback server (#321)
feat(workflow-host): a waiting call is dispatched again before its deadline ends it (#321)
END_COMMIT_OVERRIDE

rami-hatoum and others added 7 commits October 25, 2026 08:51
…he facts

The jobs a run's steps perform, PerformJob, ModelCallJob and ToolCallJob, and
performedInProcess, which performs them in this process, were not exported. A shell that
performs one model call or one tool call per invocation could not hand a job out, and could
not decide the next job of a run without performing the whole attempt.

The main entry now exports them, with the step functions that drive a run from its facts:

- stepRecordingOf(services, run, tools), what the steps of a run record through, without a
  performer;
- stateOfTheAttempt(recording, prompt, attempt), the state folded from the attempt's calls
  and the content they kept, as a resumed run folds it;
- jobsOf(recording, state), which decides what follows and records the start of each job it
  hands out, or answers the ending;
- performedJob(perform, job), which performs one job and answers its JobAnswer;
- answerRecorded(recording, state, answer), which records its end.

It also exports what the adapter does around the steps, which such a shell does itself:
beforeTheSteps, promptOfTheAttempt, requestFor, admitted and withTools before them,
stepsAnswerOf and finishedOf, the ending and its record, and resumesAfterMsOf, the quiet
period of a run. definitionRun now ends a run through finishedOf. @on.auto/definitions
exports attemptIsLive(quietMs, resumesAfterMs), the liveness rule its resume applies.

The loop of this process now makes each job's beginning and end with the same functions
around its own performer, so the server is unchanged. A run driven one job at a time, its
state folded from the facts before each step, records the same facts and model requests
as the loop, and ends the same way (src/steps/step-jobs.test.ts). A run whose playbook
keeps no content goes on across the jobs of one invocation, its state kept in memory, and
ends unreadable once a later invocation folds it from its facts. A start refused because
the run's cancel was recorded ends it cancelled.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A dispatched call got the effect alone, and an MCP endpoint handed the SDK its caller
through a schema, so a shell could bind an invocation's fetch and identity to a request
only through the request's span.

ApiOptions now takes callContext(request), an Effect Context of the services a shell binds
to the invocation that answers the request. Every route that dispatches provides it:

- an operation route provides it to the call it dispatches;
- an MCP endpoint hands it to the SDK beside the caller, and each tool provides it to the
  call it dispatches.

The dispatcher keeps those services, so the operation reads them as it reads any service.
A server that gives none provides an empty context, so the server is unchanged.

src/operations/call-context.test.ts holds that an operation reads the service a call
context made of a request header, over HTTP and through an MCP tool.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… option the server passes

The model access imported one provider's identity library, for that provider without a key,
a package the runtime does not depend on, from a module its main entry reaches. A bundle of
the core elsewhere could not resolve it, and had to alias it to a module that is not found.

That import, and installedPackages, which resolves the package, now live in an entry of
their own, @on.auto/reasoning/installed-packages. The server passes its loadEntraIdentity
to makeModelAccess. Without a loader the identity library is absent, and that provider
without a key is not configured.

src/providers/credential-libraries.test.ts holds that no module the main and testing entries
reach imports the package, and that the new entry does. The test of model access with its
defaults holds that the provider without a key is configured only when the access is given
the loader.

The loader answers null for no identity, where it answered undefined, so an absent identity
is a value rather than a missing one.

The adapter folder, past thirty modules with the new entry's, splits by concept:
src/providers makes the providers' models and the model access, and src/model-calls makes one
call to a model. The resolution of a model and the language model it resolves to join
src/model, the check of a gateway's options src/settings, the JSON output src/schema and the
unclassified error src/failure.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A ledger over streams a shell lends, rather than over a store of this package, restated
the load of a stream and the composition of decisionLoop with eventAppenderOf, since the
ledger made both inside makeLedger alone.

streamPortsOf(streams) now answers them over any Streams, its read, its append and the
most events it takes in one append: load(stream, fold), and execute(stream, decider,
command, lineage?), the decision loop over the append. makeLedger is made of it. Streams
is exported beside it.

src/decision-loop.test.ts holds the ports over lent streams. A command is executed and the
state it left is loaded. When another writer appends between the load and the append, the
command is decided again on the stream it left.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…l's requests through its fetch

A shell that sends the requests of each call through the invocation that performs it had to
compose the tool access and the consent client again for every invocation, since each took
one fetch when it was made and kept it for its whole life.

CallFetch of @on.auto/operations is the fetch of one call. A shell puts it in the context it
makes of a request, callContext of @on.auto/api, and composes both once:

- makeToolAccess answers each of its ways through an access over the call's fetch, made the
  first time that fetch is lent and kept while it lives (overFetches, over a WeakMap). Its
  links, their token sources and the token sources of a consent it asks of the consent client,
  serving.fetch, send through it. A stdio link sends nothing through a fetch, so the access
  over a call's fetch borrows the stdio links of the access's own and starts no second process.
- makeConsentClient answers hadSources, start, finish and revoke over the call's fetch, Auto's
  consent service and its listing among them, and a token source over the fetch serving names.
  The single flight of its dynamic registrations, and the registrations the store could not
  keep yet, are made once with the client and shared by the requests over every fetch, so one
  issuer still sees one registration flight within the process.

Where the context holds no CallFetch, as on the server, both send through the fetch they were
made with. makeConsentClient moves to src/consent/consent-making.ts, which keeps
consent-client.ts within its dependencies.

src/call-fetch/access-of-the-call.test.ts calls a tool through a lent fetch and through the
access's own, two calls through one lent fetch that take one access and mint one token and hold
no session after, and a server on stdio started once through both. consent-making.test.ts
starts a consent through a lent fetch and registers once for two starts under two fetches, and
src/consent-tokens/consent-calls.test.ts renews the tokens of a connection connected by consent
through one. The README names what a shell pays once for each fetch it lends.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…heir loopback server

@on.auto/tool-servers/testing reached the loopback server of the fakes, node:http and
node:stream, and through @on.auto/secrets/testing the fake key service and parameter store,
so a shell whose tests check under web-standard types built fakes of its own.

The fakes' answers are now handlers of the web's Request and Response, exported from the
testing entry: fakeMcpAnswers(options) with handlerAt(origin), fakeApiAnswers(options) with
handler and, for a client, issuing.handlerAt(origin), and fakeConsentServiceAnswers(options)
with handler, each with the records and controls of its server, beside FetchHandler and
LoopbackServer. The servers that serve them on loopback, servedForConsent, servedThroughAuto,
openFakeToolRun, the behaviour suite and fakeStdioServerPath move to a new entry,
@on.auto/tool-servers/served-fakes, which also exports the types of what each serves.
@on.auto/secrets/served-fakes takes serveFakeKms and serveFakeParameterStore the same way.
Every importer takes them from there.

src/testing/testing-entries.test.ts loads each entry in a process of its own and lists the
modules of Node it loaded through a resolve hook, so an import of the runtime's modules and of
the packages they take counts alike: the testing entry loads neither node:http nor node:stream,
and the entry of the served fakes loads both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…adline ends it

A shell that hands each call to a queue loses a call with the consumer that took it. The host
only cancelled such a call when its call_deadline timer fired, so the shell kept a due time of
its own beside each call to dispatch it again, and bounded its lease by the longest any run
takes, since StartCall.longestMs did not reach the run port.

PartsOptions.dispatchedAgainAfterMs turns the dispatch again on, and the server gives none. A
call's row keeps the moment it was last dispatched, dispatched_at, written as it moves to
waiting. callsDueAgainOf is due work of the host that hands out, among the work that calls out,
each waiting call whose moment is that long past. It asks first whether the run the call waits
for is live, by attemptIsLive of @on.auto/definitions over the age of that run's last fact:
while the run shows a fact within the interval the dispatch waits, its moment moved to that
fact; once the run is quiet longer, or has no fact, the host performs the call again through
perform, told the attempt before died, under the permits of the calls
(HostExecutor.dispatchedAgain). A call that still waits moves its moment to the dispatch and is
due again an interval later. Any other answer answers it. The deadline still ends it, since a
cancelled row is waiting no more. The table is made with the column and its index. The README
gives the rule for the interval: at least the longest gap between two facts of a live attempt,
the resumesAfterMs of the capability whose runs the calls start.

DefinitionRunRequest of @on.auto/coordination carries the call's longestMs. The server's
nestedRuns runs in process, bounded by its capability, and takes none.

The start of a call moves to src/calls/call-starts.ts with CallAttempt, which keeps
host-executor.ts within its lines. The suites both stores run move from src/testing to
src/suite-testing, which keeps each folder under fifteen modules, and
src/suite-testing/dispatch-suite.ts dispatches calls again on SQLite and on PostgreSQL, a call
whose run is still live among them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@rami-hatoum
rami-hatoum force-pushed the refactor/the-core-performs-calls-outside-the-server branch from 0dc5cfe to ec56df4 Compare October 25, 2026 09:12
@rami-hatoum
rami-hatoum merged commit 8a60a21 into main Oct 25, 2026
33 of 35 checks passed
@rami-hatoum
rami-hatoum deleted the refactor/the-core-performs-calls-outside-the-server branch October 25, 2026 10:06
rami-hatoum pushed a commit that referenced this pull request Oct 25, 2026
🤖 I have created a release *beep* *boop*
---


## [0.21.0](v0.20.0...v0.21.0) (2026-10-25)


### Features

* **api:** a call reaches the context a shell makes of its request
  ([#321](#321)) ([8a60a21](ht
  tps://github.com/BeOnAuto/runtime/commit/8a60a21a57aa2ac3d9af5c465b3e5
  14ee20f298a))
* **ledger:** the stream reader and writer over any streams are exported
  ([#321](#321)) ([8a60a21](ht
  tps://github.com/BeOnAuto/runtime/commit/8a60a21a57aa2ac3d9af5c465b3e5
  14ee20f298a))
* **reasoning:** a shell hands out a run's jobs one at a time, from the
  facts ([#321](#321)) ([8a60a
  21](8a60a21a57aa2ac3d9af5c4
  65b3e514ee20f298a))
* **tool-servers:** the tool access and the consent client send a call's
  requests through its fetch
  ([#321](#321)) ([8a60a21](ht
  tps://github.com/BeOnAuto/runtime/commit/8a60a21a57aa2ac3d9af5c465b3e5
  14ee20f298a))
* **workflow-host:** a waiting call is dispatched again before its
  deadline ends it
  ([#321](#321)) ([8a60a21](ht
  tps://github.com/BeOnAuto/runtime/commit/8a60a21a57aa2ac3d9af5c465b3e5
  14ee20f298a))

---
This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
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