Skip to content

feat(cloud-agent): event-driven agent host + message-first CLI - #1

Open
SarkarShubhdeep wants to merge 13 commits into
mainfrom
feature/cloud-agent
Open

feat(cloud-agent): event-driven agent host + message-first CLI#1
SarkarShubhdeep wants to merge 13 commits into
mainfrom
feature/cloud-agent

Conversation

@SarkarShubhdeep

@SarkarShubhdeep SarkarShubhdeep commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator
PR.1-vid.mp4

Summary

Adds @mieweb/cloud-agent and @mieweb/cloud-agent-cli — the L2 event shell that binds an AgentDefinition + AgentRuntime to a Durable Object with queue-driven turns, suspend/resume, and alarms.

Developed and exercised by the Jerry project (Phase 1 MVP) via the vendor/cloud submodule pin at ecb8aa7.

@mieweb/cloud-agent

  • hostAgent() — returns SessionClass, handleFetch, handleQueue, and optional handleScheduled
  • Session DO: one instance per session key; drains TurnJob messages one at a time
  • Storage layer: sessions, events, messages, activity_events, summaries (D1/SQLite via CloudDatabase)
  • createTools(ctx) factory — optional per-turn tool injection (used by Jerry for DB/vector/alarm-bound tools)
  • POST /v1/sessions/:id/messages runs turns synchronously for --call CLI ergonomics; /enqueue keeps the async path

@mieweb/cloud-agent-cli

  • Message-first dispatcher: --call (streaming fetch), -txt / --put (fire-and-forget enqueue)
  • Agent identity from basename(argv[0]); agent-specific packages (jerry, etc.) wrap with config
  • Parse tests + HTTP client

Jerry consumer

Jerry repo Detail
Submodule pin vendor/cloud @ ecb8aa7 on development
Consumer packages/jerry-app uses hostAgent() + createJerryTools
CLI packages/cli thin wrapper over @mieweb/cloud-agent-cli

Commits

  • c154c39 — initial cloud-agent + cloud-agent-cli packages
  • ecb8aa7 — synchronous --call turns; createTools injection; profile passthrough

Test plan

  • storage.test.ts — schema init, session/event/message CRUD
  • parse.test.ts — CLI flag/message parsing
  • Jerry integration on local target (pnpm dev + jerry --call …)
  • Conformance on mieweb (libSQL + Valkey + MinIO) — tracked in Jerry Phase 1

Made with Cursor

SarkarShubhdeep and others added 2 commits June 30, 2026 16:49
- 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>
@SarkarShubhdeep SarkarShubhdeep moved this to Review in Scrum Team Jerry Jul 1, 2026
@SarkarShubhdeep SarkarShubhdeep self-assigned this Jul 1, 2026
SarkarShubhdeep added a commit to mieweb/jerry that referenced this pull request Jul 1, 2026
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>
@wreiske
wreiske requested a review from Copilot July 1, 2026 19:38
@wreiske

wreiske commented Jul 1, 2026

Copy link
Copy Markdown
Member
  • fix broken CI failures
  • resolve any open copilot code review comments
  • add a video on the PR description showing what this is doing, why, who's going to use it, etc.

Comment thread packages/cloud-agent/src/storage.ts Outdated
Comment on lines +28 to +42
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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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-agent host wiring (hostAgent, session DO) and a D1/SQLite-compatible storage layer with initial tests.
  • Added @mieweb/cloud-agent-cli with 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.

Comment thread packages/cloud-agent/src/host.ts Outdated
Comment thread packages/cloud-agent/src/host.ts
Comment thread packages/cloud-agent/src/session.ts Outdated
Comment thread packages/cloud-agent/src/session.ts
Comment thread packages/cloud-agent/src/host.ts
Comment thread packages/cloud-agent/src/host.ts Outdated
Comment thread packages/cloud-agent/package.json
Comment thread packages/cloud-agent-cli/package.json
SarkarShubhdeep and others added 2 commits July 1, 2026 16:16
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>
@SarkarShubhdeep

Copy link
Copy Markdown
Collaborator Author

mieweb conformance verified (libSQL + Valkey + MinIO)

Ran the conformance checklist item locally against the real @mieweb/cloud-os docker stack — no changes to vendor/cloud required.

Infrastructure (pnpm --filter @mieweb/cloud-os infra:up)

libSQL (:8080), MinIO (:9000), Valkey (:6379) — all healthy.

1. Adapter conformance (pnpm --filter @mieweb/cloud-os test)

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).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 17 changed files in this pull request and generated 8 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment thread packages/cloud-agent/src/host.ts
Comment thread packages/cloud-agent/src/session.ts Outdated
Comment thread packages/cloud-agent/src/session.ts
Comment thread packages/cloud-agent/src/host.ts
Comment thread packages/cloud-agent/src/storage.ts Outdated
Comment thread packages/cloud-agent/src/host.ts
Comment thread packages/cloud-agent/src/host.ts
Comment thread packages/cloud-agent/src/session.ts
SarkarShubhdeep and others added 2 commits July 2, 2026 11:55
- 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>
@SarkarShubhdeep

Copy link
Copy Markdown
Collaborator Author

Removed "Jerry" from the host layer (e371f40)

TL;DR — Only three "Jerry" references were functional (JERRY_URL, JERRY_SESSION in the CLI). Rather than renaming them, the env namespace is now derived from the agent name that callers already pass, so no consumer is named in the package and nothing breaks for existing ones. 9 files, +141/−17, no behavior change.


The rest of the references were @example blocks and test fixtures. Renaming to something neutral would break existing setups and just pick another arbitrary name, so the prefix is derived from config.agent instead.

envPrefix("jerry")JERRY_*, envPrefix("my-agent")MY_AGENT_*, with a shared AGENT_* fallback. Existing behaviors unchanged: an agent named jerry still reads JERRY_URL / JERRY_SESSION, and its --help output is byte-identical.

Files changed

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_SESSIONreadEnv(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.

@github-project-automation github-project-automation Bot moved this from Review to Done in Scrum Team Jerry Aug 11, 2026
…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>
@SarkarShubhdeep

Copy link
Copy Markdown
Collaborator Author

Cloudflare interoperability — found and fixed a real one (46c5a4b)

TL;DR — You were right that something was wrong, though not on the checklist items. I booted cloud-agent on workerd under Miniflare and every endpoint returned 500. Root cause was initSchema() using db.exec() with multi-line SQL, which D1 rejects. Fixed, verified on workerd, and added a smoke harness so
it can't regress.

The failure

D1_EXEC_ERROR: Error in line 1: CREATE TABLE IF NOT EXISTS sessions (: incomplete input
    at async initSchema (packages/cloud-agent/src/storage.ts:29)
    at async Object.handleFetch (packages/cloud-agent/src/host.ts:61)

D1's exec() splits on newlines and requires each line to be a complete
statement. Our CREATE TABLE spanned eight lines. Since handleFetch calls
initSchema on every request, nothing worked on Cloudflare at all.

Why conformance missed it

The other two backends are permissive exactly where D1 is strict:

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. Only crypto.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 alignmentcloud-agent is 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.

Comment on lines +56 to +69
`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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Jerry specific tables, other agents won't use these.

Comment on lines +216 to +230
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 }>;
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

dead export, session getStatus is not being called anywhere.

Comment on lines +43 to +45
case "report":
console.log("Report mode not yet implemented");
break;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Dead end

SarkarShubhdeep and others added 5 commits August 11, 2026 15:20
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants