Skip to content

Record types

Concrete schemas for the new record types. These are specific enough that someone could add them to the core and wire them through the mutation executor without re-litigating what they mean.

@arbe/core currently has three record types: agent, house, room. Each is a record with a typed content schema validated by zod. Permissions use rwx bitmasks resolved by parent_id chain-walking. Mutations go through a framework-agnostic executor with db adapter and stream hooks.

Resource

A resource is an external thing an agent can operate on. It has identity in the substrate so activation policies and audit trails can reference it by record ID rather than by opaque config blob.

type: 'resource'
parent_id: house_id
content: {
name: string
kind: 'discord-channel' | 'github-repo' | 'sandbox' | 'durable-stream'
| 'linear-workspace' | 'pagerduty-service' | 'webhook-endpoint' | string
external_id: string // the platform's own identifier
config: Record<string, unknown> // kind-specific, non-secret configuration
}

The kind field is an open enum — new integrations add new kinds without schema changes. external_id is how we correlate back to the outside world (a Discord channel ID, a GitHub owner/repo, a Fly app name). config holds whatever the adapter needs that isn’t secret (webhook URL, default branch, channel name for display).

Resources are scoped to houses because that’s the permission boundary that makes sense — a house admin controls which external targets are available within their scope.

Resources do not carry secret references. A Discord channel does not own the Discord credential — many resources may share one credential, and one resource may need several. Credential binding is a separate concern (see capabilities).

Tool

Tools are already implied by the x permission bit but they’re not records yet. They should be.

type: 'tool'
parent_id: null (global) or house_id (house-scoped)
content: {
name: string // 'send-message', 'create-pr', 'run-command'
description?: string
kind: 'builtin' | 'mcp' | 'http' | string
schema?: Record<string, unknown> // input schema for validation/documentation
config?: Record<string, unknown> // kind-specific (MCP server URL, endpoint pattern)
}

Global tools are platform capabilities. House-scoped tools are integrations specific to that house.

Activation policy

type: 'activation-policy'
parent_id: agent_id (an agent owns its policies)
content: {
name?: string
trigger: 'cron' | 'interval' | 'webhook' | 'stream-event' | 'github-event'
| 'mention' | 'ambient' | string
config: Record<string, unknown> // trigger-specific
// cron: { expression: '0 * * * *' }
// interval: { ms: 3600000 }
// webhook: { path: '/hooks/my-agent' }
// stream-event: { stream_id: '...', filter?: {...} }
// mention: { scope_ids: ['...'] }
// ambient: { scope_ids: ['...'], delay_ms: 5000 }
resource_ids?: string[] // which resources this policy targets
tool_ids?: string[] // which tools to use when activated
enabled: boolean
}

Parent is the agent because policies define the agent’s activation behavior. resource_ids and tool_ids say what the policy cares about — the runtime still checks that the agent actually has permission on those tools and resources before executing.

This replaces the current trigger_mode field on AgentContent. The agent record itself should not carry activation configuration — that’s policy, not identity.

Run

When a run is shared (work dispatched remotely, involving shared resources, or involving multiple agents/humans):

type: 'run'
parent_id: house_id
content: {
status: 'queued' | 'dispatching' | 'running' | 'waiting' | 'completed'
| 'failed' | 'aborted'
task_ref?: string // local task ID or shared task record
agent_id: string
sandbox_ref?: string // sandbox identity
session_id?: string
activation_id?: string // which policy triggered this
started_at?: string
finished_at?: string
failure_phase?: 'dispatch' | 'execution' | 'observation' | 'result-collection'
}

Room (extended for sessions)

type: 'room'
parent_id: run_id or house_id
content: {
name: string
kind: 'conversation' | 'session' // distinguishes human rooms from execution rooms
durable_stream_id?: string
status?: 'active' | 'idle' | 'blocked' | 'errored' | 'completed'
sandbox_ref?: string
started_at?: string
last_activity_at?: string
}

A query for active sessions is where type='room' and content->>'kind'='session' and content->>'status' in ('active','idle','blocked').

Artifact

type: 'artifact'
parent_id: house_id or project_id (a durable scope, not a run)
content: {
kind: 'message' | 'file' | 'url' | 'commit' | 'screenshot' | 'log-ref' | string
label?: string
external_id?: string // remote message ID, PR URL, commit hash
mime_type?: string
byte_ref?: string // pointer to blob storage, git ref, or stream offset
source_run_id?: string // which run produced this
source_session_id?: string // which session produced this
resource_id?: string // which resource this artifact relates to
metadata?: Record<string, unknown>
}

Artifacts live under a durable scope because they outlive the sessions and runs that produced them. Provenance goes in content where it can be queried without polluting the ownership tree.

Memory

type: 'memory'
parent_id: agent_id or house_id
content: {
kind: 'fact' | 'preference' | 'commitment' | 'reference' | string
subject?: string // what this is about
text: string // the actual memory content
confidence?: number // 0-1, decays or strengthens over time
source_ref?: string // run_id or session_id where this was learned
supersedes?: string // ID of a memory record this replaces
tags?: string[]
}

Agent-level memories are private to that agent. House-level memories are shared knowledge within the house. See memory for the design rationale.

Secret binding

type: 'secret-binding'
parent_id: house_id
content: {
name: string // 'discord-bot-token', 'github-app-key'
vault_key: string // stable key into the vault/secret store
resource_ids?: string[] // which resources this credential serves
agent_ids?: string[] // which agents can resolve this credential
}

A Discord bot token serves many Discord channel resources. A GitHub App key serves many repo resources. The binding record says which credential goes where. See capabilities for the full secrets model.

What changes in @arbe/core

Schema additions:

  1. Add record types: resource, tool, activation-policy, artifact, run, memory, secret-binding.
  2. Extend RoomContentSchema with optional kind, status, sandbox_ref, started_at, last_activity_at fields.
  3. Add content schemas for each new type.
  4. Add them to contentSchemaMap and TypedRecord.
  5. Add mutation builders: createResource, createTool, createActivationPolicy, createArtifact, createRun, createMemory, createSecretBinding.
  6. The mutation executor already handles arbitrary record types — new types mostly need content validation, not new executor logic.
  7. Remove trigger_mode and ambient_delay_ms from AgentContentSchema — these move to activation-policy records.

Runtime additions:

  1. resolveCapabilities(agentId, db) — query tool and resource records the agent has x on, return the intersection.
  2. resolveActivationPolicies(agentId, db) — query policies for an agent, return with resolved resource and tool references.
  3. resolveSecrets(agentId, resourceIds, db) — query secret-binding records, return vault keys for runtime resolution.

Open questions

Should the tool schema be a full JSON Schema, or something lighter? Full JSON Schema is future-proof but heavyweight. A simpler shape might be enough to start.

Should constraint records exist from day one, or should the “capability = tool permission ∩ resource permission” model run until it breaks? Start without constraints. Add them when a real use case demands narrowing beyond what permissions express.