Status
Draft
Summary
This RFC adds a standardized parsed composer-input convention to Electric Agents.
Concretely:
composer_input becomes a well-known inbox message type
- the payload always preserves the original source text
- the payload preserves the original source text and may also carry parsed structure as an ordered node list
slash_command is a first-class composer node kind within the parsed input
- files, symbols, branches, and similar rich references can become additional node kinds over time
- entities can still declare slash commands up front and discover more dynamically at runtime
- UIs can autocomplete commands, show rich chips, and send both raw text and parsed structure
- handler-side helpers remain responsible for interpretation and behavior
Motivation
Today, users can already ask an agent to do things in natural language, and the agent will often do the right thing (if they're provided with a tool).
But modern coding-assistant UIs increasingly allow richer composition than a single slash command followed by plain text. Users may mix:
- multiple slash commands
- plain text between them
- file references
- symbol references
- branch references
- other structured tokens
In practice, the input is starting to behave more like source text in a tiny composer language than like a single command invocation.
That means the runtime should not standardize only a singular slash_command payload. It should standardize a broader parsed input convention that:
- preserves the original source text
- may also carry parsed structure that the UI already knows about
- leaves semantics to handlers and helper libraries
This RFC does not introduce the general ability to send structured payloads to handlers; the typed inbox model already supports that. Instead, it defines a shared composer_input convention for UIs and handlers that want interoperable parsed composer data. The more deeply integrated addition in this RFC is the slash-command declaration and discovery system, including EntityDefinition.slashCommands and db.collections.slashCommands.
Deterministic means:
- the UI and runtime know what structured tokens were recognized
- handlers need not infer everything from free-form text
- parsed structure can be transported faithfully alongside the raw source
Discoverable means:
- UIs can show available slash commands
- users can browse and autocomplete them
- commands can carry descriptions and argument hints
- rich references like files and symbols can be represented explicitly
We do not need a new transport abstraction for this work. The existing send + typed inbox message model is already the right base.
What is missing is a standard parsed composer-input model.
Goals
- Add first-class parsed composer input support to Electric Agents
- Standardize the
composer_input inbox message type
- Preserve raw source text alongside parsed structure
- Support multiple structured tokens, including slash commands, in one user input
- Support additional node kinds over time, such as files and symbols
- Keep slash-command declaration and discovery as first-class capabilities
- Give UIs structured metadata they can build against
- Keep execution behavior in the handler
- Preserve room for related typed message families like
rpc
Non-goals
- Redefining
send
- Replacing the existing typed inbox message model
- Building a centralized global command registry
- Fully specifying RPC request/response semantics in this RFC
- Defining the full composer UI in this RFC
- Rich argument typing/validation in v1
- Nested composer parsing in v1
- Fully specifying every future composer node kind in this RFC
Current state
Electric Agents already has the important pieces:
- typed sends to an entity inbox
- inbox schema validation by message type
- handler-side processing of inbox messages
What we do not have yet is a first-class standardized parsed composer-input convention.
Proposal
To avoid ambiguity, this RFC uses the following terminology:
EntityDefinition.slashCommands = the static slash-command declarations authored with the entity definition
db.collections.slashCommands = the built-in StreamDB collection that stores the current slash-command list for that entity
ComposerInputPayload = the typed payload sent when a UI submits parsed composer input
ComposerNode = one parsed node inside that payload, such as text, slash command, file, or symbol
The runtime seeds db.collections.slashCommands from EntityDefinition.slashCommands and keeps it up to date as entity handlers register, overwrite, or unregister dynamic commands.
1. Standardize composer_input as a first-class inbox message type
Parsed composer input should be represented on the wire as a typed inbox message.
Suggested request shape:
export type SendRequest = {
from?: string
type: string
payload: unknown
mode?: 'immediate' | 'queued' | 'paused' | 'steer'
position?: string
}
UIs will typically send parsed input with mode: 'immediate', but composer_input is not a special exception to the inbox transport.
UIs may continue sending plain text or other structured payloads as they do today. composer_input is the standardized convention for cases where a UI wants to send preserved source text plus parsed composer nodes.
Suggested message example:
{
"type": "composer_input",
"payload": {
"source": "/quickstart",
"nodes": [
{
"kind": "slash_command",
"start": 0,
"end": 11,
"raw": "/quickstart",
"name": "quickstart"
}
]
}
}
Another example:
{
"type": "composer_input",
"payload": {
"source": "/pr-review 123 in /worktree see @Branch",
"nodes": [
{
"kind": "slash_command",
"start": 0,
"end": 10,
"raw": "/pr-review",
"name": "pr-review"
},
{
"kind": "text",
"start": 10,
"end": 18,
"raw": " 123 in "
},
{
"kind": "slash_command",
"start": 18,
"end": 27,
"raw": "/worktree",
"name": "worktree"
},
{
"kind": "text",
"start": 27,
"end": 32,
"raw": " see "
},
{
"kind": "branch",
"start": 32,
"end": 39,
"raw": "@Branch",
"name": "Branch"
}
]
}
}
Suggested payload shape:
export type ComposerInputPayload = {
source: string
nodes?: ComposerNode[]
}
export type ComposerNode =
| TextNode
| SlashCommandNode
| FileNode
| SymbolNode
| BranchNode
export type BaseNode = {
kind: string
start: number
end: number
raw: string
}
export type TextNode = BaseNode & {
kind: 'text'
}
export type SlashCommandNode = BaseNode & {
kind: 'slash_command'
name: string
}
export type FileNode = BaseNode & {
kind: 'file'
path: string
}
export type SymbolNode = BaseNode & {
kind: 'symbol'
name: string
}
export type BranchNode = BaseNode & {
kind: 'branch'
name: string
}
V1 should keep this simple:
- the UI or client SDK always preserves the full source text
- the UI or client SDK is responsible for parsing composer input into recognized node kinds before sending
composer_input
- the UI may also attach parsed nodes it has already resolved; when omitted, handlers receive raw source only
- the node list is ordered and flat
- command-specific argument parsing and higher-level interpretation remain handler-owned
- only
slash_command nodes have a corresponding declaration and discovery system in v1; other node kinds are parse-only and handler-interpreted
V1 validation is structural, not semantic: the runtime validates payload shape, but higher-level meaning is handled by the entity at runtime.
2. Treat slash commands as one composer node kind
Slash commands are one first-class token kind within parsed composer input.
Instead, slash commands become one ComposerNode variant:
type SlashCommandNode = BaseNode & {
kind: 'slash_command'
name: string
}
This matches the direction of richer composer UIs:
- multiple slash commands may appear in one input
- plain text may appear between them
- files, symbols, branches, and other references may appear alongside them
The transport should preserve those facts rather than collapsing everything into a single command payload.
In v1, only slash_command nodes have a corresponding declaration and discovery system. Other node kinds are parse-only: the UI emits them, the runtime transports them structurally, and handlers interpret them. No registry or global validation model is defined for those node kinds in this RFC.
3. Add slashCommands to EntityDefinition
Entities still need a static slash-command declaration so UIs and handlers can reason about available commands structurally.
This declaration is discovery metadata, distinct from invocation payloads. It defines the slash-command vocabulary an entity exposes, and the runtime materializes it into db.collections.slashCommands.
Suggested definition shape:
export type SlashCommandDefinition = {
name: string
description?: string
arguments?: Array<{
name: string
type: 'string' | 'number' | 'boolean'
required?: boolean
description?: string
}>
}
SlashCommandDefinition.arguments is discovery metadata for the UI. It does not imply a standardized invocation-time argument object in v1.
UIs can use db.collections.slashCommands for command pickers, autocomplete, argument hints, and help text. Entity handlers can use the static declaration as the baseline command set.
Entities may optionally declare slash commands up front via EntityDefinition.slashCommands.
Potential shape:
export type EntityDefinition = {
// existing fields
slashCommands?: SlashCommandDefinition[]
}
These statically declared slash commands are written into db.collections.slashCommands.
Examples:
/quickstart
/init
- app-specific commands defined by an entity author
Static slashCommands should land in the same change as composer_input transport support.
4. Support dynamic slash-command discovery from handlers
Not all slash commands need to be predeclared.
Entity handlers can discover additional slash commands dynamically at runtime.
Examples:
- commands exposed by skill packages
- commands available only in certain environments or states
- commands derived from runtime capabilities
This RFC leaves dynamic discovery to handlers.
To support that, the TypeScript runtime should expose helper APIs that entity handlers can use to manage dynamic slash commands.
Suggested runtime helper surface:
ctx.slashCommands.get(name)
ctx.slashCommands.register(command)
ctx.slashCommands.unregister(name)
ctx.slashCommands.list()
register(command) upserts by name; registering an existing command replaces its current materialized definition.
Suggested behavior:
db.collections.slashCommands is the built-in collection for the current slash-command list
- static commands from
EntityDefinition.slashCommands and dynamic commands from runtime helpers both write into that collection
- UIs read it through the existing sync/query path
register(command) upserts by name
- if a dynamic command and static command share the same
name, the dynamic command wins
5. Keep execution in the handler
The runtime owns transport and structural validation; handlers own interpretation.
A SKILL loader example
One possible helper pattern is:
export const handler = async (ctx: EntityHandlerContext) => {
await skillsLoader(ctx)
// continue normal handler flow
}
The wake inspection happens inside skillsLoader(ctx), not in the handler after the call.
A helper following that pattern could:
- discover skill-backed slash commands
- publish them through the runtime slash-command helpers
- inspect
ctx.wake for a newly delivered composer_input
- scan the parsed nodes for slash-command nodes
- if present, inject the relevant
SKILL.md or package-defined content into context
Such a helper should be idempotent and cheap on repeated wakes so it can be safely called at the top of the handler.
6. Put the current wake on handler context
Because wakes can come from many sources, composer-input helpers cannot assume that every wake is a new composer invocation.
Handlers therefore need a reliable way to detect whether the current wake delivered composer_input and to read its payload.
The current runtime model passes wake: WakeEvent as a separate handler argument. This RFC proposes moving that onto handler context as ctx.wake. That wake-shape change is part of this RFC, not a separate follow-on.
A goal of this change is a tighter typed ctx.wake overall, ideally as a discriminated union across wake sources. Handler code should not need to peel apart an untyped payload object just to tell that parsed composer input arrived.
The desired shape is something closer to:
type HandlerWake = InboxWake | SpawnWake | ScheduleWake | ObservationWake
type InboxWake = {
type: 'inbox'
message: {
message_type: string
payload: unknown
}
}
With a discriminated union like that, composer-input detection becomes straightforward:
if (ctx.wake.type === 'inbox' && ctx.wake.message.message_type === 'composer_input') {
const payload = ctx.wake.message.payload as ComposerInputPayload
// inspect payload.source and payload.nodes
}
The exact final ctx.wake union can still be refined by the implementer, but this RFC should aim for a discriminated wake type with an inbox variant at least this direct.
7. Keep parsed composer structure flat in v1
As the input model evolves beyond a single slash command, the runtime will likely need to carry both raw source text and parsed structure for slash commands, files, symbols, branches, and other rich composer tokens.
V1 should keep that parsed structure flat, not nested.
The transport should preserve:
- the original source text
- the ordered parsed nodes
- source spans for each node
- the raw text for each node
A flat model is enough for the current use cases:
- multiple slash commands in one input
- plain text between commands
- file, symbol, branch, and similar references mixed into the same input
This RFC does not propose nested parsing in v1. Associations like “this slash command is the primary action” or “this branch reference modifies that command” should be derived later by handlers or helper libraries, not baked into the transport grammar yet.
This keeps the parser and wire format simpler while leaving room for richer syntax later if the product genuinely needs it.
Example flows
Parsed composer input with command-triggered behavior
- entity declares
/quickstart in EntityDefinition.slashCommands
- runtime materializes it into
db.collections.slashCommands
- UI reads that collection and shows it in the slash command picker
- user invokes it
- UI sends:
{
"type": "composer_input",
"payload": {
"source": "/quickstart",
"nodes": [
{
"kind": "slash_command",
"start": 0,
"end": 11,
"raw": "/quickstart",
"name": "quickstart"
}
]
}
}
- server persists it to the entity stream
- wake fires
- entity handler processes it
For example, a /quickstart handler path could scan the node list and inject the quickstart instructions into context:
if (ctx.wake.type === 'inbox' && ctx.wake.message.message_type === 'composer_input') {
const payload = ctx.wake.message.payload as ComposerInputPayload
for (const node of payload.nodes) {
if (node.kind === 'slash_command' && node.name === 'quickstart') {
await ctx.insertContext({
type: 'skill',
title: 'Quickstart instructions',
content: quickstartSkillMarkdown,
metadata: {
source: 'composer_input',
nodeKind: 'slash_command',
command: 'quickstart',
rawInput: payload.source,
},
})
}
}
}
A single-command /search postgres logical replication handler path could scan for the search slash-command node, derive the query from the remaining source text, call a search API, and inject the result into context with explicit provenance metadata:
if (ctx.wake.type === 'inbox' && ctx.wake.message.message_type === 'composer_input') {
const payload = ctx.wake.message.payload as ComposerInputPayload
for (const node of payload.nodes ?? []) {
if (node.kind === 'slash_command' && node.name === 'search') {
// This simple extraction only works for a single-command input.
// Multi-command association should be handled by a helper that
// groups or slices the relevant text region more carefully.
const query = payload.source.slice(node.end).trim()
const result = await searchDocs(query)
await ctx.insertContext({
type: 'search_result',
title: `Search result for: ${query}`,
content: result.summary,
metadata: {
source: 'composer_input',
nodeKind: 'slash_command',
command: 'search',
query,
url: result.url,
rawInput: payload.source,
},
})
}
}
}
Mixed composer input with multiple commands and references
A richer input might look like:
/pr-review 123 in /worktree be sure to check @Branch see @Horton
The UI could send:
{
"type": "composer_input",
"payload": {
"source": "/pr-review 123 in /worktree be sure to check @Branch see @Horton",
"nodes": [
{
"kind": "slash_command",
"start": 0,
"end": 10,
"raw": "/pr-review",
"name": "pr-review"
},
{
"kind": "text",
"start": 10,
"end": 18,
"raw": " 123 in "
},
{
"kind": "slash_command",
"start": 18,
"end": 27,
"raw": "/worktree",
"name": "worktree"
},
{
"kind": "text",
"start": 27,
"end": 46,
"raw": " be sure to check "
},
{
"kind": "branch",
"start": 46,
"end": 53,
"raw": "@Branch",
"name": "Branch"
},
{
"kind": "text",
"start": 53,
"end": 58,
"raw": " see "
},
{
"kind": "symbol",
"start": 58,
"end": 65,
"raw": "@Horton",
"name": "Horton"
}
]
}
}
The handler can then decide, from the flat ordered node list, that:
/pr-review is the primary action
/worktree acts as an execution modifier
@Branch attaches branch context
@Horton attaches symbol context
Those semantics are intentionally derived at the handler/helper layer rather than imposed by the transport grammar.
Multi-word references may require explicit UI-level selection or chip-based insertion so the node boundaries are unambiguous. This RFC does not standardize that parsing UX in v1.
Dynamic skill-backed slash command
- handler-side
skillsLoader(ctx) discovers commands from skill packages
- it registers those commands via
ctx.slashCommands.register(...)
- UI reads
db.collections.slashCommands and surfaces one of those commands
- user invokes
/quickstart
- inbox receives
composer_input
- entity handler runs
await skillsLoader(ctx) again on the invocation wake
- because the loader is idempotent, it cheaply confirms registration state and inspects
ctx.wake
- loader recognizes the relevant slash-command node and injects the corresponding skill content
- handler continues with enriched context
If a dynamic command temporarily overrides a static command with the same name, unregistering the dynamic command should reveal the static declaration again in the effective command list.
Runtime responsibilities
The runtime has three jobs in this design:
- transport and persist
composer_input messages
- materialize the slash-command registry that UIs read
- give handlers typed wake and slash-command-management helpers
What the runtime should do
On send:
- validate the top-level
send fields such as type, payload, mode, and position
- if
type === 'composer_input', validate payload against ComposerInputPayload
- treat
composer_input as a built-in standardized inbox message type rather than requiring each entity to redefine its schema
- persist the typed inbox message to the entity stream
- wake the target entity
On wake delivery:
- preserve a reliable inbox-to-wake mapping
- expose that mapping through a typed
ctx.wake, so handlers can tell when the current wake corresponds to new composer_input
For slash-command discovery:
- materialize statically declared slash commands into the built-in
db.collections.slashCommands collection
- expose TypeScript runtime helper APIs for handlers to
get, register, unregister, and list dynamic slash commands in that same collection
- reconcile static and dynamic commands into the single effective slash-command list that UIs query
What the runtime should not do
The runtime should not need to:
- understand skill package layout
- centrally interpret composer input
- own the discovery logic for dynamic slash commands
- decide what a slash command, file reference, or branch reference means beyond basic shape validation and delivery
Error handling
Handlers should use the existing entity error/reporting path for unknown or failed commands in v1 rather than introducing a new composer-input-specific error transport.
UI responsibilities
UIs should be able to:
- discover available slash commands by querying the synced
db.collections.slashCommands collection
- show command names and descriptions
- prompt for or hint arguments where possible
- parse composer input into a flat ordered node list where the UI has recognized rich tokens
- preserve the full original source text
- in chip-based UIs, serialize the visible composed input into a canonical
source string
- invoke parsed input as a structured
composer_input message
- react to slash-command creation and deletion over time via the synced collection
- optionally attach parse-only references and other structured tokens alongside slash-command nodes
Codebase impact
Likely main touchpoints:
Shared/public types
packages/agents-server/src/electric-agents-types.ts
packages/agents-runtime/src/types.ts
packages/agents-runtime/src/skills/types.ts if skill metadata gains slash-command metadata
- composer-input payload and node types
Entity/inbox schema
packages/agents-runtime/src/entity-schema.ts
packages/agents-runtime/src/define-entity.ts
- entity definition types that would gain
slashCommands
Send route / persistence
packages/agents-server/src/routing/entities-router.ts
packages/agents-server/src/entity-manager.ts
Wake/handler access
packages/agents-runtime/src/process-wake.ts
packages/agents-runtime/src/types.ts to add ctx.wake
packages/agents-runtime/src/context-factory.ts where handler context is constructed
- inbox-to-wake mapping for
composer_input detection
- TypeScript runtime helper APIs on handler context for dynamic slash-command management
UI
packages/agents-server-ui/src/components/MessageInput.tsx
packages/agents-server-ui/src/lib/sendMessage.ts
- slash-command picker/discovery UI
- composer parser / chip-to-node serialization
Built-in agents / entity handlers
packages/agents/src/agents/horton.ts
- handler-side helpers like
skillsLoader(ctx)
Open questions
1. What should the discriminated ctx.wake union look like?
Composer-input helpers must be able to inspect ctx.wake and reliably distinguish composer-input invocations from other wake sources without digging through an untyped payload blob.
2. Do we need richer argument schemas in v1?
This is intentionally left open for the implementer. The default bias is no: source text plus handler-owned interpretation is enough to start unless a richer typed input model is clearly needed for a good UI and invocation experience. SlashCommandNode does not carry a standardized arguments object in v1; commands like /init my-app are interpreted from surrounding source text or adjacent nodes by handler-side logic.
3. When should the parser move beyond a flat node list?
The default bias is to keep parsed composer structure flat in v1. Nested structure should only be added if real product needs emerge that cannot be handled cleanly by an ordered node list plus handler-side interpretation.
Recommendation
Build parsed composer input on the existing typed inbox model.
Specifically:
- standardize
composer_input as a well-known inbox message type
- preserve the full source text in every parsed composer payload
- carry parsed structure as a flat ordered node list
- treat slash commands as one first-class node kind among others
- add
slashCommands to EntityDefinition
- allow entities to predeclare slash commands
- materialize both static and dynamic slash commands into the built-in
db.collections.slashCommands StreamDB collection
- add TypeScript runtime helper APIs for entity handlers to manage dynamic slash commands in that collection
- have UIs read that collection through the existing sync/query path
- leave higher-level interpretation to handlers
- put
wake on handler context as ctx.wake
- give inbox-triggered wakes a tighter typed shape
- use the same typed inbox model for future typed message families like
rpc
- use helper patterns like
skillsLoader(ctx) on top of those runtime APIs for skill-backed command behavior
This keeps the architecture simple:
- no new transport abstraction
- a single materialized slash-command collection for UI reads
- raw source always preserved when a UI opts into
composer_input
- no standardized CLI-style argument grammar in v1
- no nested composer grammar in v1
- strong support for the richer UI composition model the product is moving toward
Status
Draft
Summary
This RFC adds a standardized parsed composer-input convention to Electric Agents.
Concretely:
composer_inputbecomes a well-known inbox message typeslash_commandis a first-class composer node kind within the parsed inputMotivation
Today, users can already ask an agent to do things in natural language, and the agent will often do the right thing (if they're provided with a tool).
But modern coding-assistant UIs increasingly allow richer composition than a single slash command followed by plain text. Users may mix:
In practice, the input is starting to behave more like source text in a tiny composer language than like a single command invocation.
That means the runtime should not standardize only a singular
slash_commandpayload. It should standardize a broader parsed input convention that:This RFC does not introduce the general ability to send structured payloads to handlers; the typed inbox model already supports that. Instead, it defines a shared
composer_inputconvention for UIs and handlers that want interoperable parsed composer data. The more deeply integrated addition in this RFC is the slash-command declaration and discovery system, includingEntityDefinition.slashCommandsanddb.collections.slashCommands.Deterministic means:
Discoverable means:
We do not need a new transport abstraction for this work. The existing
send+ typed inbox message model is already the right base.What is missing is a standard parsed composer-input model.
Goals
composer_inputinbox message typerpcNon-goals
sendCurrent state
Electric Agents already has the important pieces:
What we do not have yet is a first-class standardized parsed composer-input convention.
Proposal
To avoid ambiguity, this RFC uses the following terminology:
EntityDefinition.slashCommands= the static slash-command declarations authored with the entity definitiondb.collections.slashCommands= the built-in StreamDB collection that stores the current slash-command list for that entityComposerInputPayload= the typed payload sent when a UI submits parsed composer inputComposerNode= one parsed node inside that payload, such as text, slash command, file, or symbolThe runtime seeds
db.collections.slashCommandsfromEntityDefinition.slashCommandsand keeps it up to date as entity handlers register, overwrite, or unregister dynamic commands.1. Standardize
composer_inputas a first-class inbox message typeParsed composer input should be represented on the wire as a typed inbox message.
Suggested request shape:
UIs will typically send parsed input with
mode: 'immediate', butcomposer_inputis not a special exception to the inbox transport.UIs may continue sending plain text or other structured payloads as they do today.
composer_inputis the standardized convention for cases where a UI wants to send preserved source text plus parsed composer nodes.Suggested message example:
{ "type": "composer_input", "payload": { "source": "/quickstart", "nodes": [ { "kind": "slash_command", "start": 0, "end": 11, "raw": "/quickstart", "name": "quickstart" } ] } }Another example:
{ "type": "composer_input", "payload": { "source": "/pr-review 123 in /worktree see @Branch", "nodes": [ { "kind": "slash_command", "start": 0, "end": 10, "raw": "/pr-review", "name": "pr-review" }, { "kind": "text", "start": 10, "end": 18, "raw": " 123 in " }, { "kind": "slash_command", "start": 18, "end": 27, "raw": "/worktree", "name": "worktree" }, { "kind": "text", "start": 27, "end": 32, "raw": " see " }, { "kind": "branch", "start": 32, "end": 39, "raw": "@Branch", "name": "Branch" } ] } }Suggested payload shape:
V1 should keep this simple:
composer_inputslash_commandnodes have a corresponding declaration and discovery system in v1; other node kinds are parse-only and handler-interpretedV1 validation is structural, not semantic: the runtime validates payload shape, but higher-level meaning is handled by the entity at runtime.
2. Treat slash commands as one composer node kind
Slash commands are one first-class token kind within parsed composer input.
Instead, slash commands become one
ComposerNodevariant:This matches the direction of richer composer UIs:
The transport should preserve those facts rather than collapsing everything into a single command payload.
In v1, only
slash_commandnodes have a corresponding declaration and discovery system. Other node kinds are parse-only: the UI emits them, the runtime transports them structurally, and handlers interpret them. No registry or global validation model is defined for those node kinds in this RFC.3. Add
slashCommandstoEntityDefinitionEntities still need a static slash-command declaration so UIs and handlers can reason about available commands structurally.
This declaration is discovery metadata, distinct from invocation payloads. It defines the slash-command vocabulary an entity exposes, and the runtime materializes it into
db.collections.slashCommands.Suggested definition shape:
SlashCommandDefinition.argumentsis discovery metadata for the UI. It does not imply a standardized invocation-time argument object in v1.UIs can use
db.collections.slashCommandsfor command pickers, autocomplete, argument hints, and help text. Entity handlers can use the static declaration as the baseline command set.Entities may optionally declare slash commands up front via
EntityDefinition.slashCommands.Potential shape:
These statically declared slash commands are written into
db.collections.slashCommands.Examples:
/quickstart/initStatic
slashCommandsshould land in the same change ascomposer_inputtransport support.4. Support dynamic slash-command discovery from handlers
Not all slash commands need to be predeclared.
Entity handlers can discover additional slash commands dynamically at runtime.
Examples:
This RFC leaves dynamic discovery to handlers.
To support that, the TypeScript runtime should expose helper APIs that entity handlers can use to manage dynamic slash commands.
Suggested runtime helper surface:
register(command)upserts byname; registering an existing command replaces its current materialized definition.Suggested behavior:
db.collections.slashCommandsis the built-in collection for the current slash-command listEntityDefinition.slashCommandsand dynamic commands from runtime helpers both write into that collectionregister(command)upserts bynamename, the dynamic command wins5. Keep execution in the handler
The runtime owns transport and structural validation; handlers own interpretation.
A SKILL loader example
One possible helper pattern is:
The wake inspection happens inside
skillsLoader(ctx), not in the handler after the call.A helper following that pattern could:
ctx.wakefor a newly deliveredcomposer_inputSKILL.mdor package-defined content into contextSuch a helper should be idempotent and cheap on repeated wakes so it can be safely called at the top of the handler.
6. Put the current wake on handler context
Because wakes can come from many sources, composer-input helpers cannot assume that every wake is a new composer invocation.
Handlers therefore need a reliable way to detect whether the current wake delivered
composer_inputand to read its payload.The current runtime model passes
wake: WakeEventas a separate handler argument. This RFC proposes moving that onto handler context asctx.wake. That wake-shape change is part of this RFC, not a separate follow-on.A goal of this change is a tighter typed
ctx.wakeoverall, ideally as a discriminated union across wake sources. Handler code should not need to peel apart an untyped payload object just to tell that parsed composer input arrived.The desired shape is something closer to:
With a discriminated union like that, composer-input detection becomes straightforward:
The exact final
ctx.wakeunion can still be refined by the implementer, but this RFC should aim for a discriminated wake type with an inbox variant at least this direct.7. Keep parsed composer structure flat in v1
As the input model evolves beyond a single slash command, the runtime will likely need to carry both raw source text and parsed structure for slash commands, files, symbols, branches, and other rich composer tokens.
V1 should keep that parsed structure flat, not nested.
The transport should preserve:
A flat model is enough for the current use cases:
This RFC does not propose nested parsing in v1. Associations like “this slash command is the primary action” or “this branch reference modifies that command” should be derived later by handlers or helper libraries, not baked into the transport grammar yet.
This keeps the parser and wire format simpler while leaving room for richer syntax later if the product genuinely needs it.
Example flows
Parsed composer input with command-triggered behavior
/quickstartinEntityDefinition.slashCommandsdb.collections.slashCommands{ "type": "composer_input", "payload": { "source": "/quickstart", "nodes": [ { "kind": "slash_command", "start": 0, "end": 11, "raw": "/quickstart", "name": "quickstart" } ] } }For example, a
/quickstarthandler path could scan the node list and inject the quickstart instructions into context:A single-command
/search postgres logical replicationhandler path could scan for thesearchslash-command node, derive the query from the remaining source text, call a search API, and inject the result into context with explicit provenance metadata:Mixed composer input with multiple commands and references
A richer input might look like:
The UI could send:
{ "type": "composer_input", "payload": { "source": "/pr-review 123 in /worktree be sure to check @Branch see @Horton", "nodes": [ { "kind": "slash_command", "start": 0, "end": 10, "raw": "/pr-review", "name": "pr-review" }, { "kind": "text", "start": 10, "end": 18, "raw": " 123 in " }, { "kind": "slash_command", "start": 18, "end": 27, "raw": "/worktree", "name": "worktree" }, { "kind": "text", "start": 27, "end": 46, "raw": " be sure to check " }, { "kind": "branch", "start": 46, "end": 53, "raw": "@Branch", "name": "Branch" }, { "kind": "text", "start": 53, "end": 58, "raw": " see " }, { "kind": "symbol", "start": 58, "end": 65, "raw": "@Horton", "name": "Horton" } ] } }The handler can then decide, from the flat ordered node list, that:
/pr-reviewis the primary action/worktreeacts as an execution modifier@Branchattaches branch context@Hortonattaches symbol contextThose semantics are intentionally derived at the handler/helper layer rather than imposed by the transport grammar.
Multi-word references may require explicit UI-level selection or chip-based insertion so the node boundaries are unambiguous. This RFC does not standardize that parsing UX in v1.
Dynamic skill-backed slash command
skillsLoader(ctx)discovers commands from skill packagesctx.slashCommands.register(...)db.collections.slashCommandsand surfaces one of those commands/quickstartcomposer_inputawait skillsLoader(ctx)again on the invocation wakectx.wakeIf a dynamic command temporarily overrides a static command with the same
name, unregistering the dynamic command should reveal the static declaration again in the effective command list.Runtime responsibilities
The runtime has three jobs in this design:
composer_inputmessagesWhat the runtime should do
On send:
sendfields such astype,payload,mode, andpositiontype === 'composer_input', validatepayloadagainstComposerInputPayloadcomposer_inputas a built-in standardized inbox message type rather than requiring each entity to redefine its schemaOn wake delivery:
ctx.wake, so handlers can tell when the current wake corresponds to newcomposer_inputFor slash-command discovery:
db.collections.slashCommandscollectionget,register,unregister, andlistdynamic slash commands in that same collectionWhat the runtime should not do
The runtime should not need to:
Error handling
Handlers should use the existing entity error/reporting path for unknown or failed commands in v1 rather than introducing a new composer-input-specific error transport.
UI responsibilities
UIs should be able to:
db.collections.slashCommandscollectionsourcestringcomposer_inputmessageCodebase impact
Likely main touchpoints:
Shared/public types
packages/agents-server/src/electric-agents-types.tspackages/agents-runtime/src/types.tspackages/agents-runtime/src/skills/types.tsif skill metadata gains slash-command metadataEntity/inbox schema
packages/agents-runtime/src/entity-schema.tspackages/agents-runtime/src/define-entity.tsslashCommandsSend route / persistence
packages/agents-server/src/routing/entities-router.tspackages/agents-server/src/entity-manager.tsWake/handler access
packages/agents-runtime/src/process-wake.tspackages/agents-runtime/src/types.tsto addctx.wakepackages/agents-runtime/src/context-factory.tswhere handler context is constructedcomposer_inputdetectionUI
packages/agents-server-ui/src/components/MessageInput.tsxpackages/agents-server-ui/src/lib/sendMessage.tsBuilt-in agents / entity handlers
packages/agents/src/agents/horton.tsskillsLoader(ctx)Open questions
1. What should the discriminated
ctx.wakeunion look like?Composer-input helpers must be able to inspect
ctx.wakeand reliably distinguish composer-input invocations from other wake sources without digging through an untyped payload blob.2. Do we need richer argument schemas in v1?
This is intentionally left open for the implementer. The default bias is no: source text plus handler-owned interpretation is enough to start unless a richer typed input model is clearly needed for a good UI and invocation experience.
SlashCommandNodedoes not carry a standardized arguments object in v1; commands like/init my-appare interpreted from surrounding source text or adjacent nodes by handler-side logic.3. When should the parser move beyond a flat node list?
The default bias is to keep parsed composer structure flat in v1. Nested structure should only be added if real product needs emerge that cannot be handled cleanly by an ordered node list plus handler-side interpretation.
Recommendation
Build parsed composer input on the existing typed inbox model.
Specifically:
composer_inputas a well-known inbox message typeslashCommandstoEntityDefinitiondb.collections.slashCommandsStreamDB collectionwakeon handler context asctx.wakerpcskillsLoader(ctx)on top of those runtime APIs for skill-backed command behaviorThis keeps the architecture simple:
composer_input