Cortex API and Eventing#
The Cortex system manages AI agent execution through the Callosum message bus with file-based persistence. It acts as a process manager for agent instances, receiving requests via Callosum and writing execution events to both JSONL files (for persistence) and the message bus (for real-time distribution).
For details on the Callosum protocol and message format, see CALLOSUM.md.
Architecture#
Event Flow#
- Request Creation: Client calls
cortex_request()which broadcasts to Callosum (tract="cortex",event="request") - Request Reception: Cortex receives message via Callosum callback and creates
<name>/<timestamp>_active.jsonl - Agent Spawning: Cortex spawns agent process via
sol agentswith merged configuration - Event Emission: Agents write JSON events to stdout (captured by Cortex)
- Event Distribution: Cortex appends events to JSONL file AND broadcasts to Callosum
- Agent Completion: Cortex renames file to
<name>/<timestamp>.jsonlwhen agent finishes
Key Components#
- Message Bus Integration: Cortex connects to Callosum to receive requests and broadcast events
- Process Management: Spawns agent subprocesses (both tool agents and generators)
- Configuration Delegation: Passes raw requests to
sol agents, which handles all config loading, validation, and hydration - Event Capture: Monitors agent stdout/stderr and appends to JSONL files
- Dual Event Distribution: Events go to both persistent files and real-time message bus
- NDJSON Input Mode: Agent processes accept newline-delimited JSON via stdin containing the full merged configuration
File States#
<name>/<timestamp>_active.jsonl: Agent currently executing (Cortex is appending events)<name>/<timestamp>.jsonl: Agent completed (contains full event history)
Note: Files provide persistence and historical record, while Callosum provides real-time event distribution to all interested services.
Request Format#
Requests are created via cortex_request() from think.cortex_client, which broadcasts to Callosum. The request message follows this format:
{
"event": "request",
"ts": 1234567890123, // Required: millisecond timestamp (must match agent_id in filename)
"prompt": "Analyze this code for security issues", // Required for agents (not generators)
"name": "default", // Optional: agent name from talent/*.md
"provider": "openai", // Optional: override provider (openai, google, anthropic)
"max_output_tokens": 8192, // Optional: maximum response tokens
"thinking_budget": 10000, // Optional: thinking token budget (ignored by OpenAI)
"session_id": "sess-abc123", // Optional: CLI session ID for continuation
"chat_id": "1234567890122", // Optional: chat ID for reverse lookup
"facet": "my-project", // Optional: project context
"output": "md", // Optional: output format ("md" or "json"), writes to agents/
"day": "20250109", // Optional: YYYYMMDD format, defaults to current day
"env": { // Optional: environment variables for subprocess
"API_KEY": "secret",
"DEBUG": "true"
}
}
The model is automatically resolved based on the talent context (talent.{source}.{name})
and the configured tier in journal.json. Provider can optionally be overridden at
request time, which will resolve the appropriate model for that provider at the same tier.
Generator Request Format#
Generators are spawned via Cortex when a request has an output field but no tools field. They produce analysis output (markdown or JSON) from clustered transcripts.
{
"event": "request",
"ts": 1234567890123, // Required: millisecond timestamp
"name": "activity", // Required: generator name from talent/*.md
"day": "20250109", // Required: day in YYYYMMDD format
"output": "md", // Required: output format ("md" or "json")
"segment": "120000_300", // Optional: single segment key (HHMMSS_duration)
"span": ["120000_300", "120500_300"], // Optional: list of sequential segment keys
"output_path": "/path/to/file.md", // Optional: override output location
"refresh": false, // Optional: regenerate even if output exists
"provider": "google", // Optional: AI provider override
"model": "gemini-2.0-flash" // Optional: model override
}
Generator Events#
Generators emit the same event types as agents:
start- When generation beginsfinish- On completion, withresultcontaining generated contenterror- On failure
The finish event may include a skipped field when generation is skipped:
"no_input"- Insufficient transcript content to analyze"disabled"- Generator is marked as disabled in frontmatter
Conversation Continuations#
All providers (Anthropic, OpenAI, Google) support continuing conversations via CLI
session resumption. Include a session_id field in the request with the CLI session
ID from a previous agent's finish event. The provider CLI tool resumes the conversation
internally using its native session management (e.g., claude --resume, codex exec resume).
Chats are locked to their original provider — continuations must use the same provider
that started the conversation. The chat_id field enables reverse lookup from an
agent back to its parent chat.
Agent Event Format#
All subsequent lines are JSON objects with event and millisecond ts fields. The ts field is automatically added by Cortex if not provided by the provider. Additionally, Cortex automatically adds an agent_id field (matching the timestamp component in the filename) to all events for tracking purposes.
request#
The initial spawn request (first line of file, written by client).
{
"event": "request",
"ts": 1234567890123,
"agent_id": "1234567890123",
"prompt": "User's task or question",
"provider": "openai",
"name": "default",
"output": "md",
"day": "20250109"
}
start#
Emitted when an agent run begins.
{
"event": "start",
"ts": 1234567890123,
"agent_id": "1234567890123",
"name": "default",
"model": "gpt-4o",
"session_id": "sess-abc",
"chat_id": "1234567890122"
}
tool_start#
Emitted when a tool execution begins.
{
"event": "tool_start",
"ts": 1234567890123,
"agent_id": "1234567890123",
"tool": "search_journal",
"args": {"query": "search terms", "limit": 10},
"call_id": "search_journal-1"
}
tool_end#
Emitted when a tool execution completes.
{
"event": "tool_end",
"ts": 1234567890123,
"agent_id": "1234567890123",
"tool": "search_journal",
"args": {"query": "search terms"},
"result": ["result", "array", "or", "object"],
"call_id": "search_journal-1"
}
thinking#
Emitted when the model produces reasoning/thinking content (model-dependent, primarily o1 models).
{
"event": "thinking",
"ts": 1234567890123,
"agent_id": "1234567890123",
"summary": "Model's internal reasoning about the task...",
"model": "o1-mini"
}
agent_updated#
Emitted when control is handed off to a different agent (multi-agent scenarios).
{
"event": "agent_updated",
"ts": 1234567890123,
"agent_id": "1234567890123",
"agent": "SpecializedAgent"
}
finish#
Emitted when the agent run completes successfully.
{
"event": "finish",
"ts": 1234567890123,
"agent_id": "1234567890123",
"result": "Final response text to the owner"
}
error#
Emitted when an error occurs during execution.
{
"event": "error",
"ts": 1234567890123,
"agent_id": "1234567890123",
"error": "Error message",
"trace": "Full stack trace..."
}
info#
Emitted when non-JSON output is captured from agent stdout.
{
"event": "info",
"ts": 1234567890123,
"agent_id": "1234567890123",
"message": "Non-JSON output line from agent"
}
Tool Call Tracking#
Tool events use call_id to pair tool_start and tool_end events. This allows tracking:
- Which tools are currently running
- Tool execution duration
- Tool inputs and outputs
- Concurrent tool executions
The frontend uses this to show real-time status updates as tools execute, changing from "running..." to "✓" when complete.
Agent Output#
When an agent completes successfully, its result can be automatically written to a file. This uses the same output path logic as generators.
- Include an
outputfield in the agent's frontmatter with the format ("md" or "json") - Output path is derived from agent name + format + schedule:
- Daily agents:
YYYYMMDD/agents/{name}.{ext} - Segment agents:
YYYYMMDD/{segment}/{name}.{ext}
- Daily agents:
- Writing occurs before completion
- Write failures are logged but don't interrupt the agent flow
- Commonly used for scheduled agents that generate daily reports
Agent Configuration#
Agents use configurations stored in the talent/ directory. Each agent is a .md file containing:
- JSON frontmatter with metadata and configuration
- The agent-specific prompt and instructions in the content
When spawning an agent:
- Cortex passes the raw request to
sol agentsvia stdin (NDJSON format) - The agent process (
think/agents.py) handles all config loading viaprepare_config():- Loads agent configuration using
get_agent()fromthink/talent.py - Merges request parameters with agent defaults
- Resolves provider and model based on context
- Loads agent configuration using
- The agent validates the config via
validate_config()before execution - Instructions are built with three components:
system_instruction:journal.md(shared base prompt, cacheable)extra_context: Runtime context (facets, generators list, datetime)user_instruction: The agent's.mdfile content
Agents define specialized behaviors and facet expertise. Available agents can be discovered using get_talent_configs(type="cogitate") or by listing files in the talent/ directory.
Agent Configuration Options#
The JSON frontmatter for an agent can include:
max_tokens: Maximum response token limitschedule: Scheduling configuration for automated execution"daily": Run automatically at midnight each day
priority: Execution order for scheduled prompts (integer, required for scheduled prompts)- Lower numbers run first (e.g., priority 10 runs before priority 40)
- See THINK.md for priority bands
multi_facet: Boolean flag for facet-aware agents (default: false)- When true, the agent is spawned once for each active facet (see Multi-Facet Agents section)
- Each instance receives a facet-specific prompt with the facet name
- Useful for creating per-facet reports, newsletters, or analyses
always: Override active facet detection for multi-facet agents (default: false)- When true, agent runs for all non-muted facets regardless of activity
env: Environment variables to set for the agent subprocess (object)- Keys are variable names, values are coerced to strings
- Request-level
envoverrides agent defaults - Note:
_SOLSTONE_JOURNAL_OVERRIDEis set by Cortex for child processes
Model Resolution#
Models are resolved automatically based on context and tier:
- Each talent config has a context pattern:
talent.{source}.{name}(e.g.,talent.system.default) - The context determines the tier (pro/flash/lite) from
journal.jsonor system defaults - The tier + provider determines the actual model to use
This allows controlling model selection via tier configuration rather than hardcoding models:
{
"providers": {
"contexts": {
"talent.system.default": {"tier": 1},
"talent.*": {"tier": 2}
}
}
}
Agent Providers#
The system supports multiple AI providers, each implementing the same event interface:
- OpenAI (
think/providers/openai.py): GPT models with OpenAI Agents SDK - Google (
think/providers/google.py): Gemini models with Google AI SDK - Anthropic (
think/providers/anthropic.py): Claude models with Anthropic SDK
All providers:
- Emit JSON events to stdout (one per line)
- Are spawned as subprocesses by Cortex
- Use consistent event structures across providers
- Process events are written to stdout for Cortex to capture
Scheduled Agents and Generators#
Both agents and generators support scheduling via sol dream. Agents have "schedule": "daily" and generators have "schedule": "segment" or "schedule": "daily".
Execution Order#
Scheduled items run in priority order (lower numbers first):
- Items are sorted by their
priorityfield (required for all scheduled prompts) - Items with the same priority run in parallel, then dream waits for completion
- After each generator completes, incremental indexing runs for its output
Priority bands (recommended):
- 10-30: Generators (content-producing prompts)
- 40-60: Analysis agents
- 90+: Late-stage agents
- 99: Fun/optional prompts
Multi-Facet Agents#
When an agent has "multi_facet": true:
- The agent is spawned once for each active facet
- Each instance receives a prompt including the facet name
- The agent should call
get_facet(facet_name)to load facet context - This enables per-facet reports, newsletters, and analyses
Daily Multi-Facet Agents#
Active Facet Detection: By default, daily multi-facet agents only run for facets that had activity the previous day. Activity is determined by the presence of occurrence events (not anticipations) in facets/{facet}/events/{day}.jsonl. This prevents unnecessary agent runs for inactive facets.
To force an agent to run for all facets regardless of activity, set "always": true:
{
"title": "Facet Newsletter Generator",
"schedule": "daily",
"priority": 10,
"multi_facet": true
}
{
"title": "Facet Auditor",
"schedule": "daily",
"multi_facet": true,
"always": true
}
Segment Multi-Facet Agents#
Segment agents can also be multi-facet. Active facets are determined from the facets.json output written by the facets generator (priority 90) during segment processing.
{
"title": "Facet Activity Tracker",
"schedule": "segment",
"multi_facet": true
}
The facets generator outputs an array of detected facets for each segment:
[
{"facet": "work", "activity": "Code review", "level": "high"},
{"facet": "personal", "activity": "Email check", "level": "low"}
]
Multi-facet segment agents spawn once per non-muted facet in this array. Muted facets are filtered out, consistent with daily agent behavior. If no enabled facets are detected (empty array, missing file, or all facets muted), the agent simply doesn't spawn for that segment.
Note: The "always" flag is not supported for segment agents since facet detection is inherent to the segment content.
Process Management#
The sol supervisor command provides process management for the Cortex ecosystem:
- Starts and monitors the Cortex file watcher service
- Handles process restarts on failure
- Monitors system health indicators
- Triggers
sol dreamat midnight for daily processing (generators + agents)
This is distinct from agent lifecycle management, which Cortex handles internally through file state transitions.