feat(examples): dogfood the durable agent runtime in examples/agents (RFC 0017 Part 4a) - #692
Merged
Merged
Conversation
7nohe
force-pushed
the
feat/rfc0017-part4a-guren-dev-agent
branch
from
September 6, 2026 05:49
6a29640 to
5a2f9c2
Compare
…(RFC 0017 Part 4a) A standalone demo app with one real durable agent, driven end to end under `wrangler dev --local`: a Triager sweeps stale tickets, parks a close behind the approval queue, and the pending-approval ledger retries it on its own alarm once a human approves. The app exercises the whole of Parts 1-3 and the Part 2b build wiring: two `.agent()` routes, an `AgentApprovalStore` over a table, an operator API, the generated worker's named export and guarded `/agents/*` mount, and the bindings verifier's copy-pasteable JSON. Three framework fixes came out of it: - `onToolApprovalSettled` now carries the parked call's `args`. The queue keeps no reversible copy by design, so an application had to hold a second `requestId` -> arguments map beside the ledger's own to know which call a human answered. - `isWorkersRuntime()` is exported from `@guren/plugin-cloudflare/env`. Every app hand-wrote it in `config/database.ts`, and the package README already used the name with no import. - `make:agent`'s `config/agents.ts` comment fits the `guren/comment-length` rule a scaffolded app installs, so `make:agent` followed by `bun run lint` no longer fails on the scaffolder's own output.
7nohe
force-pushed
the
feat/rfc0017-part4a-guren-dev-agent
branch
from
September 6, 2026 05:57
5a2f9c2 to
fa64404
Compare
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.
Summary
RFC 0017 Part 4a: a standalone demo app,
examples/agents(@guren/example-agents), that dogfoods the durable agent runtime end to end — and the framework fixes the dogfooding found. The app is deployed to a Workers Free plan account (its own Worker and D1, nothing shared with guren.dev) and the walkthrough was run against it; the README carries the measured numbers.The demo
Triager(app/Agents/Triager.ts): an hourly cron sweep lists open tickets throughtickets.index, and for every ticket older than seven days callstickets.close, which isapproval: 'required'— the call parks, the Part 3 ledger retries it once a human approves,onToolApprovalSettledrecords the outcome and remembers a rejection so the agent does not ask again. Public RPCsweep()/report().GET /tickets(read-only, zodoutput) andPOST /tickets/:id/closebehindrequireAuthenticated()+authorizeMiddleware('close-ticket'). One Gate rule admits operators (ausersrow, numeric id) and the seam principalagent:triager:*(string id);guren checkrequires the authorization on a mutating tool, and the seam deliberately does not satisfycreateBearerTokenMiddleware.DrizzleApprovalStore: the firstAgentApprovalStoreimplementation in the repo —findMatchnewest-unconsumed,consumeas a compare-and-setUPDATE … WHERE consumed_at IS NULL … RETURNING, portable across bun:sqlite and D1. Generic enough to become a framework default later (not moved here)./approvalsand/ops/agents/triager/*, bearer tokens (DrizzleApiTokenStore), gated by anoperateability so the agent principal can never resolve an approval;/ops/agents/…because the generated worker reserves/agents/*for the SDK router (kept deny-all — the app talks to its agent through theTRIAGERbinding).db/seed-d1.tsfor D1, 11 Bun tests, README with the localwrangler devtranscript and the Free-plan measurements.Framework fixes (changesets)
@guren/plugin-agentsminor:onToolApprovalSettlednow carriesargs(absent only for an'unreadable'row). Without it an app had to keep its ownrequestId → argumentsmap beside the ledger's — which already decrypts them for the retry. The sweep asks the queue about a row that has passed its expiry before settling it, so a human who rejected in the last backoff window is reportedrejected, notexpired(an unanswerable lapsed row is dropped asexpiredrather than kept forever). README status blockquote corrected (the ledger shipped in Part 3) andokdocumented as "dispatched",outcome.isErroras the verdict.@guren/plugin-cloudflareminor:isWorkersRuntime()exported from@guren/plugin-cloudflare/env; every app and both docs guides had hand-written the same three lines.@guren/clipatch:make:agent'sconfig/agents.tscomment trimmed to theguren/comment-lengthrule scaffolded apps now install.What the Free plan run measured
The alarm fired 30 s after the calls parked with no request touching the Worker, and the retry closed the ticket; the walkthrough was repeated after each review round (four deploys). The demo stays inside the Free plan's 50 D1 queries per invocation: a sweep asks at most 10 new tickets (1 + 2×10 = 21 queries), skips tickets already parked, and reports the rest as
deferred. Cold-boot requests exceed the Free plan's stated 10 ms per invocation and were tolerated (outcome: ok); the README says what to watch and when the Paid plan removes the ceiling.Found in production, fixed here
initialStateseeds a new instance only; the deployedTriagerkept the previous deploy's state shape and the first sweep after a field was added threwCannot convert undefined or null to object. The demo layersinitialStateunderthis.stateon every read; the RFC records it as the runtime half of Open Question 5 (amigrateStatehook orGurenAgentapplying defaults itself).result.okis dispatch, not success: atickets.indexthat answers HTTP 5xx is a blind sweep (errorin the summary), a failed close counts asrefused, and a settled retry reportsretried: 'ok' | 'failed'.POST /approvals/prune.Recorded for follow-up (not fixed here)
guren auditandguren tool:listdefault toroutes/web.tswithout probing forroutes/api.tsasguren checkdoes;auditthen warns, skips every route-level check, and exits 0. The example passes--routes routes/api.ts; the gate itself wants its own PR with tests.make:agentoutput does not typecheck in a fresh app (Envundeclared,@cloudflare/workers-typesnot mentioned); the plugin-agents README says the command "writes all three" files when it writes two;guren check's body-schema warning fires on a POST tool whose action validates only params.Verification
Example:
typecheck,test(11),guren check(10 passed / 4 warnings),guren audit --routes routes/api.ts(21 passed),cloudflare:build,wrangler dev --localwalkthrough, deployed walkthrough on the Free plan. Root:bun run lint,bun run typecheck,test:bun plugin-agents plugin-cloudflare,test:agents(32 workerd), cli suite without--isolate,audit:core-first audit:docs audit:workspace-scripts audit:agent-catalog audit:plugin-compat.