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 5 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.
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.
| } | ||
|
|
||
| if (path.endsWith("/enqueue") && request.method === "POST") { | ||
| const body = await request.json() as { message: string }; |
| await env.JOBS.send({ | ||
| sessionId, | ||
| eventId, | ||
| message: body.message, | ||
| }); |
| // Read job from request (used for context, e.g. scheduledPayload) | ||
| const job = await request.json() as TurnJob; | ||
| void job; // Used in future for scheduledPayload handling | ||
| // Ensure session exists (also validates sessionId) | ||
| await getOrCreateSession(this.env.DB, sessionId); |
| } finally { | ||
| this.turnInProgress = false; | ||
| } |
| console.error(`Turn failed for session ${job.sessionId}:`, error); | ||
| } | ||
|
|
||
| message.ack(); |
| message.ack(); | ||
| } catch (err) { | ||
| console.error(`Queue processing error for session ${job.sessionId}:`, err); | ||
| message.ack(); |
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).
| async alarm(): Promise<void> { | ||
| const sessionId = this.state.id.toString(); | ||
| const payload = await this.state.storage.get<unknown>("alarm_payload"); | ||
| await this.state.storage.delete("alarm_payload"); | ||
|
|
||
| await insertEvent(this.env.DB, sessionId, "scheduled_wake", payload); | ||
|
|
||
| await this.env.JOBS.send({ | ||
| sessionId, | ||
| eventId: crypto.randomUUID(), | ||
| scheduledPayload: payload, | ||
| }); | ||
| } |
| `INSERT INTO events (id, session_id, type, payload, created_at) | ||
| VALUES (?, ?, ?, ?, ?)` | ||
| ) | ||
| .bind(id, sessionId, type, payload ? JSON.stringify(payload) : null, now) |
| for await (const event of runtime.runTurn({ | ||
| messages: coreMessages, | ||
| tools, | ||
| system: agent.instructions, | ||
| maxSteps: 10, | ||
| })) { | ||
| if (event.type === "text-delta") { | ||
| assistantContent += event.text; | ||
| } else if (event.type === "finish") { | ||
| finishReason = event.finishReason; | ||
| } else if (event.type === "suspend") { | ||
| this.suspendReason = event.reason; | ||
| this.suspendMessage = event.message ?? null; | ||
| } else if (event.type === "error") { | ||
| await insertEvent(this.env.DB, sessionId, "error", { | ||
| message: event.message, | ||
| }); | ||
| await updateSessionStatus(this.env.DB, sessionId, "idle"); | ||
| return json({ error: event.message }, 500); | ||
| } | ||
| } |
- 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>
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