Chapter 6: Command System
Commands are ChronoAI's side-effect model. They keep deterministic state propagation separate from external work such as LLM calls, HTTP requests, file writes, or tool execution.
The Three Parts
1. defineCommandKind
A command kind defines the payload type.
const ModelCall = defineCommandKind<{
messages: Array<{ role: string; content: string }>;
}>('model-call');
2. defineCommand / useCommand
A command rule decides when to plan a command.
useCommand('call-model', {
runAt: 'after-commit',
plan({ read }) {
const messages = read('messages-ready')?.value;
if (!messages) return null;
return ModelCall.plan({ messages });
},
});
The rule runs after the graph has settled, so it sees stable derived state.
3. defineCommandExecutor / useCommandExecutor
An executor performs the external work.
useCommandExecutor(ModelCall, async (command, { stream }) => {
const writer = stream('ai-response');
for await (const chunk of callLLM(command.payload.messages)) {
writer.feed(chunk);
}
writer.commit();
});
Executors can write or stream results back into timelines.
Complete Example
const ModelCall = defineCommandKind<{
messages: Array<{ role: string; content: string }>;
}>('model-call');
const AssistantFeature = defineFeature('assistant', () => {
const userInput = useTimeline<string>({ name: 'user-input' });
const response = useTimeline<string>({ name: 'ai-response', retain: 1 });
const messages = useTimeline<Array<{ role: string; content: string }>>({
name: 'messages-ready',
fill: reactionFrame({
triggers: { userInput },
compute({ userInput }) {
if (!userInput?.value) return [];
return [{ role: 'user', content: userInput.value }];
},
}),
});
useCommand('call-model', {
runAt: 'after-commit',
plan({ read }) {
return ModelCall.plan({ messages: read(messages)?.value ?? [] });
},
});
useCommandExecutor(ModelCall, async (command, { write }) => {
const text = await callModel(command.payload.messages);
await write(response, text);
});
});
The Two-phase Principle
ChronoAI separates work into two phases:
- State settles through pure computation.
- Commands are planned and executed from the stable state.
This makes application behavior easier to reason about and easier to test.
When to Use Commands
Use commands for:
- LLM calls
- HTTP requests
- database writes
- file system work
- tool execution
- analytics and background jobs
Do not put these side effects inside reactionFrame.compute().
Summary
- Command kinds type the payload.
- Command rules plan side effects after graph convergence.
- Executors perform the side effect and write results back.
- Streaming executors can update UI progressively.
Next
Continue with Chapter 7: Feature Modules.