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:

  1. State settles through pure computation.
  2. 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.