Skip to content

Custom collections on entity streams + session comments#4538

Open
balegas wants to merge 4 commits into
mainfrom
custom-collections
Open

Custom collections on entity streams + session comments#4538
balegas wants to merge 4 commits into
mainfrom
custom-collections

Conversation

@balegas

@balegas balegas commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a generic mechanism for custom collections on entity streams and wires session comments on top of it. Comments are an opt-in feature on horton and worker entity types; other entity types stay comment-free. Replaces the bespoke POST /comments route + built-in comments runtime collection from #4529 with a layered design that's reusable for any other custom collection (e.g. samwillis's planned yjs doc comments).

How it works

Declaration. Entity types declare the custom collections they accept via custom_collection_schemas: { <name>: <JSON Schema> }. EntityDefinition.customCollectionSchemas carries this through the typed registration API; the runtime forwards it via create-handler.ts to the server, where it's persisted on entity_types (new JSONB column, migration 0015) and snapshotted onto each entity at spawn time. horton and worker declare a comment schema; the schema itself lives in packages/agents/src/agents/comment-schema.ts as a Zod definition.

Write path. A single generic endpoint, POST /_electric/entities/:type/:id/collections/:name, accepts opaque JSON, looks up the schema declared for <name> on the entity's type via getEffectiveSchemas, and rejects writes (422) when no schema is declared or the value doesn't validate. Reserved built-in collection names (BUILT_IN_COLLECTION_TYPES, exported from @electric-ax/agents-runtime) are rejected too so the runtime stays the sole writer of agent-managed collections. amendSchemas supports additive amendments of custom collection schemas via a shared mergeAdditiveSchemaMap helper that also DRYs the existing inbox/state merges.

Read path. createEntityTimelineQuery takes an optional customSource query-builder branch shaped to EntityTimelineCustomRow ({ collection, key, order, value }) and splices it into the same unionAll/orderBy pipeline as the built-in timeline collections. Consumers get one ordered live query instead of merging a second source client-side — no two-source ordering race. Multi-collection callers can union their sources client-side and use the collection field as a discriminator.

UI. useEntityTimeline registers a comments collection in the StreamDB customState (with a passthrough Standard Schema for the runtime row type), composes a customSource for it, hands it to createEntityTimelineQuery, and reshapes envelope rows into a UI-friendly comment variant via a small projectRow that spreads row.custom.value and overrides key/order. Comment surface — bubbles, reply affordances on user messages, agent responses, tool calls, and inline events, the comments-only tile view, and the reply composer — is all gated on whether the entity's type opted in to a comment schema. ElectricEntity / ElectricEntityType carry custom_collection_schemas through their zod schemas, and the entity_types Electric shape's column allow-list (server-utils.ts) was updated to expose the new column to UI clients.

Notable design points

  • EntityDefinition.customCollectionSchemas is typed as Record<string, StandardSchemaV1 | Readonly<Record<string, unknown>>> rather than the strict Record<string, StandardJSONSchemaV1> used by inboxSchemas/stateSchemas. The runtime's toJsonSchema() accepts Standard Schemas (Zod), raw JSON Schema objects, and anything with a ~standard.jsonSchema converter — so the looser union is honest about what works in practice. The strict variant has no callers in the repo (the conformance tests send raw JSON Schema over the wire, going around the typed API).
  • The envelope shape ({ collection, key, order, value }) keeps the caller's .select(...) to a single value: row line — adding a field to CommentRow later only needs to land in projectRow, not in two places. The runtime guarantees key and order (the minimum it needs for the union and orderBy); the rest rides through value.

Test plan

  • pnpm --filter @electric-ax/agents-runtime run typecheck && run test — 57 timeline tests incl. 2 new custom-source tests
  • pnpm --filter @electric-ax/agents-server run typecheck && run test — 55 targeted tests incl. write-validation + route delegation (happy path, schema mismatch, undeclared collection, reserved-name rejection, invalid name, non-object value, stopped-entity, route-layer delegation)
  • pnpm --filter @electric-ax/agents-server-ui run typecheck && run test — 93 tests incl. 3 new comments-action tests, 3 ChatView tests, 1 InlineEventCard test
  • pnpm --filter @electric-ax/agents run typecheck
  • Manual smoke on a fresh stack: spawned a horton session, verified inline reply icons appear next to copy/edit on user messages, agent responses, tool calls, and inline events; sent a comment via the reply composer; reply preview scrolls to the target row when clicked; comments-only tile view shows only comment rows; on entity types without custom_collection_schemas.comment declared, the reply icons and comments-only view are correctly hidden.

Notes

  • Migration 0015_entity_custom_collection_schemas.sql adds a nullable JSONB column on both entity_types and entities — additive, no backfill needed.
  • The entity_types shape's column allow-list at packages/agents-server/src/utils/server-utils.ts:138 was updated to include custom_collection_schemas — without this the UI gate would silently fail closed.
  • No public API was removed. The runtime exports BUILT_IN_COLLECTION_TYPES and the EntityTimelineCustomRow / EntityTimelineCustomSource types as new public surface.

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Electric Agents Desktop Builds

Build artifacts for commit 4fc0b2e.

Platform Status Artifact
macOS Apple Silicon Passed DMG
macOS Intel Passed DMG
Windows x64 Passed Installer
Linux x64 Passed AppImage / deb

Workflow run

@codecov

codecov Bot commented Jun 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 33.62832% with 375 lines in your changes missing coverage. Please review.
✅ Project coverage is 55.44%. Comparing base (2e4feee) to head (4fc0b2e).
⚠️ Report is 3 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...agents-server-ui/src/components/EntityTimeline.tsx 0.00% 140 Missing ⚠️
...s/agents-server-ui/src/components/MessageInput.tsx 0.00% 73 Missing ⚠️
.../agents-server-ui/src/components/CommentBubble.tsx 1.88% 52 Missing ⚠️
...agents-server-ui/src/components/views/ChatView.tsx 43.95% 51 Missing ⚠️
...es/agents-server-ui/src/hooks/useEntityTimeline.ts 10.52% 17 Missing ⚠️
...s-server-ui/src/components/workspace/SplitMenu.tsx 0.00% 12 Missing ⚠️
.../agents-server-ui/src/components/AgentResponse.tsx 0.00% 6 Missing ⚠️
packages/agents-server/src/entity-manager.ts 87.23% 6 Missing ⚠️
packages/agents-runtime/src/create-handler.ts 20.00% 4 Missing ⚠️
...gents-server-ui/src/components/InlineEventCard.tsx 69.23% 3 Missing and 1 partial ⚠️
... and 5 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4538      +/-   ##
==========================================
- Coverage   56.45%   55.44%   -1.02%     
==========================================
  Files         358      319      -39     
  Lines       39081    37103    -1978     
  Branches    10973    10700     -273     
==========================================
- Hits        22064    20570    -1494     
+ Misses      16946    16498     -448     
+ Partials       71       35      -36     
Flag Coverage Δ
packages/agents 70.55% <100.00%> (-0.21%) ⬇️
packages/agents-mcp ?
packages/agents-mobile 66.92% <ø> (ø)
packages/agents-runtime 81.37% <91.48%> (+1.39%) ⬆️
packages/agents-server 74.13% <88.70%> (+0.15%) ⬆️
packages/agents-server-ui 8.07% <20.00%> (+1.85%) ⬆️
packages/electric-ax 46.42% <ø> (ø)
packages/experimental ?
packages/react-hooks ?
packages/start ?
packages/typescript-client ?
packages/y-electric ?
typescript 55.44% <33.62%> (-1.02%) ⬇️
unit-tests 55.44% <33.62%> (-1.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Electric Agents Mobile Build

Local mobile checks ran for commit 4fc0b2e.

The EAS Android preview build was skipped because the mobile-eas-build label is not present.
Add the mobile-eas-build label to this PR to produce an installable preview build.

Workflow run

@balegas balegas force-pushed the custom-collections branch from 485264e to 744274a Compare June 9, 2026 13:08
...(definition.inboxSchemas && {
inbox_schemas: mapSchemas(definition.inboxSchemas),
}),
...(definition.customCollectionSchemas && {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

you can already define custom schemas so we don't need this

balegas and others added 4 commits June 9, 2026 14:20
Entity types declare the custom collections they accept by registering a
JSON Schema for each one in `custom_collection_schemas`, mirroring the
existing `inbox_schemas` / `state_schemas` pattern. Writes to a custom
collection flow through one generic endpoint that always validates
against the declared schema before appending.

Server: `POST /_electric/entities/:type/:id/collections/:name` accepts
opaque JSON, looks up the schema declared for `<name>` on the entity's
type, and rejects writes (422) when no schema is declared or the value
doesn't validate. Reserved built-in collection names
(`BUILT_IN_COLLECTION_TYPES`) are rejected here too so the runtime
stays the sole writer of agent-managed collections.

- New `custom_collection_schemas` JSONB column on `entity_types` and
  `entities` (migration 0015).
- Types: `ElectricAgentsEntityType`, `ElectricAgentsEntity`,
  `PublicElectricAgentsEntity`, `RegisterEntityTypeRequest`, and the
  observation membership row carry the new field.
- Registry CRUD plus `updateEntityTypeInPlace` / `createEntity` /
  `rowToEntityType` / `rowToEntity` plumb it through.
- Entity manager: `registerEntityType` validates the map, `spawn`
  snapshots type → entity, `getEffectiveSchemas` returns the merged
  map, `appendCollectionRow` validates against it,  `amendSchemas`
  supports additive amendments via a shared `mergeAdditiveSchemaMap`
  helper that also DRYs the existing inbox/state merges.
- Entity projector + entity-types router accept the new field.
- Tests cover happy path, schema mismatch, undeclared collection,
  reserved-name rejection, invalid name, non-object value,
  stopped-entity, and route-layer delegation.

Runtime: `createEntityTimelineQuery` accepts an optional `customSource`
query-builder branch shaped to `EntityTimelineCustomRow`
(`{ collection, key, order, value }`) and splices it into the same
unionAll/orderBy pipeline as the built-in timeline collections, so
consumers project custom collections into one ordered live query.
`EntityDefinition.customCollectionSchemas` lets typed entity
definitions declare schemas that `create-handler.ts` forwards into the
type registration. `BUILT_IN_COLLECTION_TYPES` is exported as the
single source of truth for the reserved set.

Horton and worker entity types declare the `comment` schema from a
shared local module (`packages/agents/src/agents/comment-schema.ts`)
to opt their entities in to comments.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…hema

Renders session comments alongside the existing timeline rows and only
exposes the comment surface on entities whose type declared a `comment`
custom collection schema. Other entity types stay comment-free — the
server would reject the writes anyway, so we just hide the surface.

- `useEntityTimeline` registers the `comments` collection in the
  StreamDB customState, composes a `customSource` query branch shaped
  to the runtime's `EntityTimelineCustomRow` envelope, and hands it to
  `createEntityTimelineQuery` — one live query, no client-side merge.
  A small `projectRow` reshapes the runtime's generic `custom` variant
  back into a UI-friendly `comment` variant so renderers keep using
  `if (row.comment)`.
- `lib/comments.ts` defines `CommentTarget`, `CommentSnapshot`,
  `CommentRow`, `EntityTimelineCommentRow`, the UI-facing
  `EntityTimelineQueryRow` augmentation, and the optimistic
  `createSendCommentAction` that posts to
  `POST .../collections/comment`.
- `CommentBubble`, `EntityTimeline`, `MessageInput`, `ChatView`,
  `SplitMenu`, and adjacent components render comment bubbles,
  reply affordances on user messages, assistant responses, tool
  calls, and inline events, the reply preview iMessage-style with
  cross-tile focus handoff, a "comments only" tile view, and the
  `comments=hidden` view param.
- ChatView derives `commentsEnabled` from the matching entity type's
  `custom_collection_schemas.comment` and AND's it into `showComments`,
  so entities whose type didn't opt in get the chat surface without
  any comment affordances.
- `registerViews` gates the comments-only tile view registration on
  the same predicate via `isAvailable`.
- `ElectricEntity` / `ElectricEntityType` zod schemas accept the new
  `custom_collection_schemas` field flowing through the entity stream.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`EntityDefinition.customCollectionSchemas` is loosened from
`Record<string, StandardJSONSchemaV1>` to
`Record<string, StandardSchemaV1 | Readonly<Record<string, unknown>>>` —
the strict type was aspirational (no caller in the repo populates the
parallel `inboxSchemas` / `stateSchemas` fields via the typed API; the
only real callers send raw JSON Schema over the wire), and the runtime's
`toJsonSchema()` already accepts Standard Schemas (Zod) and raw JSON
Schema objects both. The looser type is honest about what works.

With that in place, `comment-schema.ts` becomes a Zod definition (~50
lines, properties inferred at the type level) instead of a hand-rolled
JSON Schema wrapped in a `jsonSchema()` envelope. The runtime continues
to convert via `.toJSONSchema()` at registration time. The previously
added `jsonSchema()` helper in `agents-runtime` and its re-export are
dropped — no longer needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@balegas balegas force-pushed the custom-collections branch from 744274a to 4fc0b2e Compare June 9, 2026 13:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants