Skip to content

Architecture

How the Chickpea source tree is laid out, how one codebase builds for Cloudflare and Node, and which module owns what.

Updated View as Markdown

Chickpea’s source tree is one TypeScript project that builds for two targets, Cloudflare Workers and Node, out of the same modules. Where Flue, the open agent framework from the Astro team, now part of Cloudflare, owns the agent loop, durable sessions, and provider plumbing, Chickpea owns Slack, the authority model, Admin, and the state stores under them. This page maps src/, explains how one tree serves two targets, names the three Flue agents and the work ledger, and says where tests live.

The source map

CONTRIBUTING.md has the short version under “Where things live”. Directory by directory:

Directory What it owns
src/slack/ Slack transport, event admission, routing, delivery, attachments, presentation, streaming, handles. The largest directory, holding transport/, gateway/, and agent-presence/.
src/agents/ The three Flue agent modules and runtime-plan.ts, which turns one Agent’s resolved configuration into the plan a turn executes.
src/config/ Configuration and settings stores, connector presets, model policy, seeding, and per-target store selection (state-backend.ts).
src/state/ The StateDb mini interface, its Node backend, the async facade, and cross-domain link DDL.
src/work/ The work ledger: admission, run lifecycle, leases, executor, retention, migrations.
src/management/ The workspace management service, its schemas and policy, the management MCP server, the Slack tools.
src/admin/ Admin pages and their HTTP routes.
src/auth/, src/identity/ Slack sign-in, roles, setup capability links, invitations, workspace identity.
src/connections/ Connections, the managed Composio lane, and credential resolution at egress.
src/routines/ Schedules: parsing, admission, scheduler, execution, delivery. routine is the code word for a schedule.
src/memory/, src/sandbox/ One memory per Agent with its validation and tool policy; the coding sandbox lifecycle, egress handler, and session caps.
src/model-catalog/, src/model-compat/ The pinned model catalog and the compatibility layer over Pi.
src/usage/, src/telemetry/, src/audit/, src/activity/ Usage and pricing, the content-free telemetry contract, audit events, live Slack activity status.
src/security/, src/http/ Shared primitives: constant-time compare, digests, content validation, bounded response bodies, request origin.

Four files sit at the root of src/. src/app.ts builds the Hono application, registers the runtime interceptors, and default-exports it; both targets serve it. src/cloudflare.ts is Worker only: the TagStateStore and SlackGatewaySession Durable Objects, the Sandbox class, and the scheduled handler. src/db.node.ts selects file-backed Flue persistence for the Node build. src/runtime-bootstrap.ts installs the app-owned Pi providers once per module graph.

Two targets, one tree

There are two Vite configs. vite.config.ts runs the Flue plugin first, then @cloudflare/vite-plugin, emits assets/ as Static Assets, and builds into dist-cf. vite.node.config.ts runs the Flue plugin alone and passes db: 'src/db.node.ts', which is how Node gets file-backed persistence the Cloudflare target rejects. npm run build builds the Cloudflare profile then validates the artifact with scripts/flue-build-cf.mjs; npm run dev uses the Node config.

flue.config.ts keeps the shared provider graph to anthropic, openai, and openrouter, and sets tracing: false. The Cloudflare config adds the cloudflare provider, since only that target has a Workers AI binding.

The target is a runtime check, not a build flag. isCloudflareTarget() in src/config/runtime-target.ts compares globalThis.navigator?.userAgent to Cloudflare-Workers. The same modules are bundled for both targets and pick their backend on first use, so a new store, provider, or capability needs a decision on both sides of that branch.

On Cloudflare

wrangler.jsonc declares the whole surface: a chickpea Worker with Static Assets (ASSETS), the Workers AI binding (AI), CF_VERSION_METADATA, two app-owned Durable Objects (TAG_STATE and SLACK_GATEWAY_SESSION), and one D1 database, AUTH_DB, with migrations in migrations/better-auth. Flue’s own agent Durable Objects are added by the build. There is no KV and no R2.

State runs inside TagStateStore. DoSqlStateDb in src/cloudflare.ts wraps ctx.storage.sql, the store logic classes run in the Durable Object, and the Worker reaches them through the Cf*Store proxies in src/config/cf-state-proxies.ts. A cron trigger fires every minute (* * * * *), driving the scheduler and the turn relay. The Durable Object migration list is history and is append only: v6 is the Flue 2 reset that created the three v2 agent classes.

On Node

src/db.node.ts points Flue at ./tmp/flue.db, overridable with TAG_DB_PATH. The application stores use a sibling SQLite file, resolved by resolveStateDbPath() as <TAG_DB_PATH>.state unless SLACK_STATE_DB_PATH overrides it; a :memory: transcript database gets a :memory: state store, so test runs stay ephemeral. The Sqlite*Store classes run in process against those files rather than proxying to a Durable Object.

Node runs no scheduler and no coding sandbox. RoutineCapability in src/routines/scheduler-adapter.ts reports the reason unsupported_target there.

The state layer

StateDb in src/state/state-db.ts is a five-method SQL interface: run, get, all, exec, and a synchronous transaction. It stays small because everything in it must be implementable over Durable Object SQLite, which has no prepared statements, rejects multi-statement strings, and cannot span an await inside a transaction. Write DDL one statement per exec call, or the code passes on Node and fails on Cloudflare.

Each store is written twice. A synchronous *Logic class owns the SQL and runs unchanged in both backends. A Sqlite*Store class satisfies the async store interface, and promisify() in src/state/async-facade.ts generates that forwarding with a Proxy, keeping close() synchronous. Columns that point across domains, such as routines.work_id and usage_operations.run_id, are installed by the idempotent installLedgerLinks() in src/state/schema-links.ts, which every owner calls.

The three Flue agents

Chickpea registers exactly three Flue agents, named in src/agents/names.ts:

Constant Identifier Module
CHICKPEA_SLACK_AGENT_NAME chickpea-slack-v2 src/agents/slack-thread.ts
CHICKPEA_ROUTINE_INTENT_AGENT_NAME chickpea-routine-intent-v2 src/agents/routine-intent.ts
CHICKPEA_ROUTINE_EXECUTION_AGENT_NAME chickpea-routine-execution-v2 src/agents/routine-execution.ts

Each module starts with 'use agent' and assigns its name as a string literal, because the Flue build reads <Agent>.agentName = '<literal>' statically to derive Durable Object class and binding names. The constants exist separately because runtime policy keys off the same strings, and tests/agent-names.test.ts asserts the two agree. A Chickpea Agent is configuration these agents load, not a class of its own: runtime-plan.ts compiles one Agent’s instructions, skills, connections, and repositories into the plan a turn runs. UNATTENDED_AGENT_NAMES covers the two routine agents, which have no human in the loop and so need authority written into the saved task text.

The work ledger

src/work/ is the canonical record of what ran. Four nouns carry it, defined in src/work/types.ts and created in src/work/migrations.ts:

  • Work is a unit of ongoing activity, of kind conversation, routine, or web_admin, and lifecycle open, closed, or expired.
  • A binding attaches one work item to one external conversation through an adapter (slack, routine, web_admin, conformance) and says whether configuration freezes when the binding opens or resolves on each run.
  • A run is one admitted attempt, carrying its dedupe key, actor trust tier, configuration revision, capability digest, lease and fencing token, status, terminal disposition, and delivery status.
  • A run execution is one model-invoking attempt inside a run.

Two supporting tables round it out. effective_config_revisions stores canonical configuration by digest, so a run records which authority it ran under. ledger_content holds request and response bodies under a 262,144 byte ceiling, with an expiry; a purged row keeps its metadata and drops its body.

Management and its adapters

WorkspaceManagementService in src/management/service.ts holds every workspace mutation. src/management/tool-adapter.ts is the transport-neutral seam above it and lists the tool names in WORKSPACE_MANAGEMENT_TOOL_NAMES. Three adapters sit on that seam: the management MCP server (src/management/mcp.ts, Zod schemas), the Flue tools the Slack Agent calls (src/management/slack-tools.ts, Valibot schemas), and Admin’s HTTP routes (src/admin/routes.ts). Adding a capability to one door and not the other two is how this seam drifts.

Where tests live

Tests are Node’s test runner over TypeScript: npm test typechecks, then runs tests/*.test.ts and tests/usage/*.test.ts with tsx. There are 271 top-level test files, plus tests/helpers/ for fake services, tests/fixtures/ for recorded payloads, and tests/parity/fake-slack.ts. Anything needing a build, a workerd run, or a fake provider is a script under scripts/, invoked through a verify:* npm script. Verification is local by design; the repository runs no GitHub Actions workflows.

Why it works this way

Two targets share one tree so behaviour cannot fork. The alternative, a Worker build and a server build with their own stores, doubles every authority check and hides the divergence until a live workspace finds it. The cost is the narrow StateDb waist and the doubled store classes, which is why that constraint sits in the interface’s own comments rather than with reviewers.

The work ledger exists for the same reason. A turn crosses Slack, a durable Flue session, a model provider, and a delivery attempt, and each can retry on its own. Recording admission, authority, and delivery as rows makes a duplicate reply a constraint violation, not a judgement call.

What is not covered

  • Flue internals. Durable sessions, subagents, tool wiring, and provider streaming belong to @flue/runtime and Pi, not this repository.
  • Generated files. The model catalog, avatar pool, and brand assets have builder scripts; never edit their output by hand.

Next steps

Navigation

Type to search…

↑↓ navigate↵ selectEsc close