feat(cloud-agent): event-driven agent host + message-first CLI - #1
feat(cloud-agent): event-driven agent host + message-first CLI#1SarkarShubhdeep wants to merge 13 commits into
Conversation
- Add @mieweb/cloud-agent with hostAgent() API - Session DO for queue-driven turns, suspend/resume, alarms - Storage module for sessions, events, messages, activity_events, summaries - Add @mieweb/cloud-agent-cli with message-first dispatcher - Support --call (streaming) and -txt/--put (fire-and-forget) modes - Update README with new packages Co-authored-by: Cursor <cursoragent@cursor.com>
…a createTools POST /messages now executes the turn inline instead of only enqueueing, and hostAgent accepts an optional createTools(ctx) factory so agents like Jerry can bind runtime tools with DB/vector/alarm context. Co-authored-by: Cursor <cursoragent@cursor.com>
Document the feature/cloud-agent PR opened on mieweb/cloud and track Jerry's vendor/cloud pin (ecb8aa7) in phase-1 plan, README, and chat10. Co-authored-by: Cursor <cursoragent@cursor.com>
|
| export async function initSchema(db: CloudDatabase): Promise<void> { | ||
| await db.exec(` | ||
| CREATE TABLE IF NOT EXISTS sessions ( | ||
| id TEXT PRIMARY KEY, | ||
| user_id TEXT, | ||
| status TEXT NOT NULL DEFAULT 'idle', | ||
| conversation_id TEXT, | ||
| continuation TEXT, | ||
| created_at TEXT NOT NULL, | ||
| updated_at TEXT NOT NULL | ||
| ); | ||
|
|
||
| CREATE TABLE IF NOT EXISTS events ( | ||
| id TEXT PRIMARY KEY, | ||
| session_id TEXT NOT NULL, |
There was a problem hiding this comment.
I'm wondering if this should be using https://orm.drizzle.team/
Take a look and see if this makes more sense. it might be more of a mieweb/cloud thing that we should support drizzle.
There was a problem hiding this comment.
Looked into Drizzle for cloud-agent storage. Right now this is pretty small, just one agent (Jerry), a few simple tables, and the schema lives in storage.ts with basic D1-style queries. Adding Drizzle here would mostly add dependency and migration overhead without much benefit.
Note: If we want Drizzle later, it should be a @mieweb/cloud / vendor/cloud platform thing (optional helper on top of CloudDatabase), not something Jerry owns. That way any future agent can use it across Cloudflare, local SQLite, and libSQL.
Fine to defer for now and revisit when we have more agents or the schema/query surface gets more complex.
There was a problem hiding this comment.
Pull request overview
This PR introduces an event-driven “agent host” package (@mieweb/cloud-agent) that wires an AgentDefinition + AgentRuntime into a Durable Object with queue-driven turns, plus a message-first CLI dispatcher package (@mieweb/cloud-agent-cli) for synchronous --call and async enqueue usage.
Changes:
- Added
@mieweb/cloud-agenthost wiring (hostAgent, session DO) and a D1/SQLite-compatible storage layer with initial tests. - Added
@mieweb/cloud-agent-cliwith argument parsing, HTTP client (streaming-or-JSON), and a generic bin dispatcher. - Updated the packages README to document the new packages.
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/README.md | Documents the new cloud-agent and cloud-agent-cli packages in the monorepo index. |
| packages/cloud-agent/src/types.ts | Defines host/session/storage/runtime types for the agent host. |
| packages/cloud-agent/src/storage.ts | Implements schema init + CRUD helpers for sessions/events/messages/activity/summaries. |
| packages/cloud-agent/src/storage.test.ts | Adds basic tests for storage helpers using a mock DB. |
| packages/cloud-agent/src/session.ts | Implements the session Durable Object turn lifecycle, suspend/resume, and alarm handling. |
| packages/cloud-agent/src/index.ts | Public exports for the cloud-agent package. |
| packages/cloud-agent/src/host.ts | Worker wiring: HTTP routes (/v1/sessions/...) + queue forwarding into the session DO. |
| packages/cloud-agent/package.json | Declares the new @mieweb/cloud-agent package metadata/exports/scripts. |
| packages/cloud-agent-cli/src/types.ts | Defines CLI config/options and streamed event types. |
| packages/cloud-agent-cli/src/run.ts | Implements CLI command dispatch and help/version output. |
| packages/cloud-agent-cli/src/parse.ts | Implements message-first CLI argument parsing. |
| packages/cloud-agent-cli/src/parse.test.ts | Adds tests for CLI parsing behavior. |
| packages/cloud-agent-cli/src/index.ts | Public exports for the cloud-agent-cli package. |
| packages/cloud-agent-cli/src/client.ts | Implements HTTP client for /messages (call) and /enqueue (put). |
| packages/cloud-agent-cli/package.json | Declares the new @mieweb/cloud-agent-cli package metadata/bin/scripts. |
| packages/cloud-agent-cli/bin/agent-cli.js | Adds a generic agent CLI bin entrypoint. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Add lockfile entries for @types/node@^20.0.0 in cloud-agent and cloud-agent-cli so CI pnpm install --frozen-lockfile succeeds. Co-authored-by: Cursor <cursoragent@cursor.com>
Tests use `node --import tsx` but tsx was not declared, causing CI unit job failures on a clean install. Co-authored-by: Cursor <cursoragent@cursor.com>
✅
|
| Contract | Backend | Result |
|---|---|---|
| D1 | libSQL | ✅ pass |
| Vectorize | libsql-vec | ✅ pass |
| R2 | MinIO (S3) | ✅ pass |
| KV | Valkey | ✅ pass |
| Queue | valkey-queue | ✅ pass |
5/5 passed.
2. Jerry on mieweb target (mieweb --target mieweb dev)
| Check | Binding exercised | Result |
|---|---|---|
GET /health |
worker on mieweb target | ✅ pass |
GET /v1/sessions/:id/status |
libSQL + inproc DO | ✅ pass |
POST /v1/events |
libSQL (activity_events) |
✅ pass |
POST /v1/sessions/:id/enqueue |
Valkey queue + DO | ✅ pass |
| session alive after queue turn | queue consumer | ✅ pass |
Persistence confirmed in libSQL: sessions = 2 rows, activity_events = 2 rows.
Note
Full end-to-end agent turns (jerry --call … / sync /messages) require a running Ollama (:11434), which is application-level and outside target/infra conformance. The DB / KV / queue / object-storage contracts all pass on the mieweb backends.
CI status on the branch: unit ✅ + conformance ✅ (after the lockfile + tsx devDependency fixes).
- Forward profile and userId through the /enqueue path into TurnJob so async turns honor the per-request privacy profile. - Persist the enqueued user message and status transitions inside handleTurn so queued turns actually consume the new message; slim handleMessage to delegate and avoid double-inserts. - Reset session status to idle (and log an error event) when a turn throws, preventing sessions from getting stuck in "running". - Retry failed/exception queue jobs instead of ack-ing them so transient failures are not dropped permanently. Co-authored-by: Cursor <cursoragent@cursor.com>
- alarm(): create the scheduled_wake event first and reuse its eventId for the queued wake job, and only delete alarm_payload after JOBS.send() succeeds so a failed enqueue can be retried. - insertEvent(): only store NULL when payload is undefined, so falsy payloads (0, false, "") are persisted instead of silently dropped. Co-authored-by: Cursor <cursoragent@cursor.com>
The host and CLI hard-coded JERRY_URL / JERRY_SESSION, which tied a generic platform package to one consumer. Derive the environment namespace from the agent name instead (`jerry` -> JERRY_*, `assistant` -> ASSISTANT_*) with a shared AGENT_* fallback, so any agent gets its own namespace and none are named in the package. - Add envPrefix()/readEnv() and thread the agent name through parseArgs/run - Resolve baseUrl from the agent namespace when a wrapper omits it - Interpolate the env var names in --help instead of printing them literally - Neutralize the @example blocks and the AgentRuntime port reference Behavior is unchanged for existing consumers: an agent named `jerry` still resolves JERRY_URL and JERRY_SESSION, and its --help output is identical. Co-authored-by: Cursor <cursoragent@cursor.com>
Removed "Jerry" from the host layer (
|
| File | What changed |
|---|---|
cloud-agent-cli/src/env.ts |
New. envPrefix() derives the prefix from the agent name; readEnv() reads agent-scoped first, then AGENT_* |
cloud-agent-cli/src/env.test.ts |
New. 7 tests for prefix normalization and fallback order |
cloud-agent-cli/src/parse.ts |
process.env.JERRY_SESSION → readEnv(agent, "SESSION"); parseArgs takes an optional agent |
cloud-agent-cli/src/run.ts |
Passes config.agent into parseArgs; resolves baseUrl from the agent namespace when a wrapper omits it; --help interpolates the env var names instead of printing them literally |
cloud-agent-cli/bin/agent-cli.js |
Dropped the hard-coded baseUrl: process.env.JERRY_URL ?? … — run() resolves it now |
cloud-agent-cli/src/index.ts |
Exports envPrefix / readEnv; neutralized the @example block |
cloud-agent/src/index.ts |
@example uses a neutral agent name |
cloud-agent/src/types.ts |
Dropped the @mieweb/jerry-agent-runtime reference from the AgentRuntime doc comment |
cloud-agent-cli/src/parse.test.ts |
Fixtures use AGENT_SESSION; added coverage for scoped-vs-fallback precedence |
rg -i jerry packages/cloud-agent packages/cloud-agent-cli now returns nothing.
Tests: 25 pass in cloud-agent-cli, 6 in cloud-agent.
Not in this PR
ModelPolicy is the one real gap — the privacy/model profile is still profile?: unknown in a few places. Typing it changes the wire contract with consumers, so I'd rather do it separately. Same for the banned-identifier lint rule; happy to open a follow-up issue for both.
…works on D1 D1's exec() splits input on newlines and requires each line to be a complete statement, so the multi-line CREATE TABLE block failed with D1_EXEC_ERROR on workerd. initSchema() runs on every request, so nothing worked on Cloudflare: health, session status, turns, and queue consumption all returned 500. Local (better-sqlite3 db.exec) and mieweb (libSQL executeMultiple) both accept multi-line SQL, which is why conformance passed on those targets and missed it. Type checking could not catch it either, since exec(string) is a valid call. Apply each statement through prepare().run() instead — the one path all three adapters implement, with no line restrictions. - Split the schema into one statement per table/index - Make the storage test mock reject multi-line exec() the way D1 does, so the old code would now fail the suite - Add a Miniflare smoke harness under smoke/ that boots hostAgent() on workerd with real D1, Queues, and Durable Object bindings Verified on wrangler dev: health, DO status, D1 event write, synchronous turn, and queue producer -> consumer -> DO all pass. Co-authored-by: Cursor <cursoragent@cursor.com>
Cloudflare interoperability — found and fixed a real one (
|
| Backend | exec() |
Multi-line SQL |
|---|---|---|
| Cloudflare D1 | native | rejects |
| local | better-sqlite3 db.exec() |
accepts |
| mieweb | libSQL executeMultiple() |
accepts |
So the libSQL/Valkey/MinIO conformance results posted earlier were genuine and also structurally incapable of catching this. Typecheck couldn't either — exec(string) is a valid signature. cf is the one target that would have caught it, and it auto-skips in CI when wrangler isn't installed.
Fix
Schema now applies as individual prepared statements — the one path all three adapters implement, with no line restrictions:
export async function initSchema(db: CloudDatabase): Promise<void> {
for (const statement of SCHEMA_STATEMENTS) {
await db.prepare(statement).run();
}
}The storage test mock now rejects multi-line exec() the way D1 does, so the old code would fail the suite.
Verified on workerd
npx wrangler dev with real D1, Queues, and Durable Object bindings:
| Check | Result |
|---|---|
GET /health |
{"ok":true,"agent":"smoke"} |
GET /v1/sessions/:id/status |
{"status":"idle"} — DO + D1 |
POST /v1/events |
{"ok":true,"count":1} — D1 write |
POST /messages |
{"message":"...","finishReason":"stop"} — sync turn via DO |
POST /enqueue |
{"status":"queued"} |
| Queue consumer | QUEUE cloud-agent-smoke-turns 1/1 |
Repro harness committed at packages/cloud-agent/smoke/ (worker + wrangler config + README). Uses a stub runtime, so no model provider or CF account needed.
On the rest of the checklist
Audited each item; no other violations found:
- Runtime constraints — zero Node imports in
cloud-agent/src. Onlycrypto.randomUUID(), which is Web Crypto. One runtime dep (cloud-types, types-only). - Auth — N/A, this PR has no auth flow, tokens, or credentials.
- Interoperability — no AWS SDK, no IAM, no S3 assumptions. Alarms use native
state.storage.setAlarm(). - Deploy target alignment —
cloud-agentis a library, not a deployable. The wrangler config that mounts it lives in the Jerry repo.
Still open
jerry/wrangler.jsonc has placeholder binding IDs (database_id:"local-mieweb-jerry") that need real values from wrangler d1 create before a production deploy, and there's no env.staging/env.production separation.
That's a Jerry-repo task — tracking separately, not a blocker here.
Suggest we also make the cf conformance target non-skippable in CI, since a skipped target is what let this through. Happy to do that as a follow-up.
| `CREATE TABLE IF NOT EXISTS activity_events ( | ||
| id TEXT PRIMARY KEY, | ||
| source TEXT NOT NULL, | ||
| payload TEXT, | ||
| occurred_at TEXT NOT NULL, | ||
| ingested_at TEXT NOT NULL | ||
| )`, | ||
| `CREATE TABLE IF NOT EXISTS summaries ( | ||
| id TEXT PRIMARY KEY, | ||
| session_id TEXT, | ||
| range_start TEXT NOT NULL, | ||
| range_end TEXT NOT NULL, | ||
| summary TEXT, | ||
| created_at TEXT NOT NULL |
There was a problem hiding this comment.
Jerry specific tables, other agents won't use these.
| export async function getStatus( | ||
| baseUrl: string, | ||
| sessionId: string | ||
| ): Promise<{ status: string; continuation?: unknown }> { | ||
| const url = `${baseUrl}/v1/sessions/${sessionId}/status`; | ||
|
|
||
| const response = await fetch(url); | ||
|
|
||
| if (!response.ok) { | ||
| const text = await response.text(); | ||
| throw new Error(`Request failed: ${text}`); | ||
| } | ||
|
|
||
| return response.json() as Promise<{ status: string; continuation?: unknown }>; | ||
| } |
There was a problem hiding this comment.
dead export, session getStatus is not being called anywhere.
| case "report": | ||
| console.log("Report mode not yet implemented"); | ||
| break; |
Add two exported Cursor chat transcripts under chats/ so reviewers of PR #1 can follow the exploration and reasoning behind the cloud-agent and cloud-agent-cli packages. - chat2.md: latest update overview (package walkthrough + PR evaluation) - chat3.md: forked continuation of the same overview
Accumulate unique tool-call names during handleTurn and thread them through the CLI client's finish/suspended events so Jerry's REPL can render a post-answer tools footer without SSE. Co-authored-by: Cursor <cursoragent@cursor.com>
Surface which tools ran at the end of --call turns so scripted usage matches the REPL audit trail. Co-authored-by: Cursor <cursoragent@cursor.com>
…uncations JSON one-shot replies were hard-coding finishReason=stop, hiding output token-limit cuts. Surface the real reason and print a truncation notice. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
PR.1-vid.mp4
Summary
Adds
@mieweb/cloud-agentand@mieweb/cloud-agent-cli— the L2 event shell that binds anAgentDefinition+AgentRuntimeto a Durable Object with queue-driven turns, suspend/resume, and alarms.Developed and exercised by the Jerry project (Phase 1 MVP) via the
vendor/cloudsubmodule pin atecb8aa7.@mieweb/cloud-agenthostAgent()— returnsSessionClass,handleFetch,handleQueue, and optionalhandleScheduledTurnJobmessages one at a timeCloudDatabase)createTools(ctx)factory — optional per-turn tool injection (used by Jerry for DB/vector/alarm-bound tools)POST /v1/sessions/:id/messagesruns turns synchronously for--callCLI ergonomics;/enqueuekeeps the async path@mieweb/cloud-agent-cli--call(streaming fetch),-txt/--put(fire-and-forget enqueue)basename(argv[0]); agent-specific packages (jerry, etc.) wrap with configJerry consumer
vendor/cloud@ecb8aa7ondevelopmentpackages/jerry-appuseshostAgent()+createJerryToolspackages/clithin wrapper over@mieweb/cloud-agent-cliCommits
c154c39— initialcloud-agent+cloud-agent-clipackagesecb8aa7— synchronous--callturns;createToolsinjection; profile passthroughTest plan
storage.test.ts— schema init, session/event/message CRUDparse.test.ts— CLI flag/message parsinglocaltarget (pnpm dev+jerry --call …)mieweb(libSQL + Valkey + MinIO) — tracked in Jerry Phase 1Made with Cursor