name: solar-router description: > Shared router that runs AI providers (Codex, Claude, Gemini, Agent, Ollama) with Solar repo context. Single source of truth for provider selection, fallback, and async routing policy. Use when solar-gateway, async-tasks, or other runtimes need to invoke an AI with cwd = SOLAR_WORKSPACE and paths resolved against the active workspace.
Solar Router
Purpose
Single source of truth for all AI execution in Solar:
- Provider selection and fallback live only here.
- Async routing policy (
direct_replyvsasync_draft_created) lives only here. - Used by solar-gateway (WebSocket/bridge) and solar-async-tasks (task execution).
Operational boundary
Use this skill as infrastructure for Solar runtimes and controlled diagnostics. Do not use solar-router or provider CLIs as the normal path for work that is deferred, multiprovider, browser/MCP-dependent, network/auth/keychain-dependent, long-running, or likely to block the conversation.
For that work, create or propose a task through solar-async-tasks. When solar-system supervises async-tasks, approve/queue the task and let the system runtime execute it.
Scope
- Accept JSON payload (router contract v3) on stdin; output structured JSON on stdout.
- Run the selected provider with
cwd=SOLAR_WORKSPACEso all providers seesun/,planets/, and workspaceAGENTS.md. - Resolve
SOLAR_ROUTER_SYSTEM_PROMPT_FILEandSOLAR_ROUTER_RUNTIME_DIRagainstSOLAR_WORKSPACEwhen relative. - Codex default command includes
-C <repo-root>and--add-dir ~/.codex. - Persist conversation turns in runtime dir (JSONL) for continuity.
- Implement
DecisionEngine: decidedecision.kindbased onmode,channel, and AI semantic output. - Resolve JIT context from
metadata: lookup agent/skills in planet → fallback to core → generate role inline if not found. - Write audit log (
sun/runtime/router/audit.jsonl) withstart/endevents per execution for traceability (including failed early-exit paths).
Internal architecture
scripts/
run_router.py — thin entrypoint: stdin → route() → stdout + exit
router.py — all provider-agnostic logic (parse, validate, JIT, prompt, decision engine)
providers/
__init__.py — PROVIDERS dict, exports all adapters
base.py — BaseProvider: resolve_binary, get_cmd, prepare_env, clean_output, run
claude.py — static default_cmd
codex.py — build_default_cmd() with SOLAR_WORKSPACE + CODEX_STATE_DIR
gemini.py — prepare_env (GEMINI_* vars) + clean_output (ANSI strip, OAuth guard)
agent.py — build_default_cmd() with SOLAR_WORKSPACE
Automated tests live under `core/tests/skills/solar-router/` (framework-wide layout; see `core/AGENTS.md`).
Layer contract:
router.pydecides (context, prompt, policy, response shape).providers/executes (subprocess, env, normalization). No routing logic.run_router.pydoes I/O only. No logic.
Required MCP
None
Setup
# Configure router environment variables (SOLAR_ROUTER_*, timeouts, etc.)
bash core/skills/solar-router/scripts/onboard_router_env.sh
Key environment variables:
SOLAR_ROUTER_PROVIDER_PRIORITY— Comma-separated provider list (e.g.,codex,claude,gemini,ollama)SOLAR_ROUTER_RUNTIME_DIR— Where conversation history is stored (default:sun/runtime/router)SOLAR_ROUTER_SYSTEM_PROMPT_FILE— System prompt file path (default:core/skills/solar-router/assets/system_prompt.md)SOLAR_ROUTER_CONTEXT_TURNS— Number of conversation turns to include (default:12)SOLAR_ROUTER_TIMEOUT_SEC— End-to-end router timeout, including provider execution (default:300)
Optional command overrides:
SOLAR_ROUTER_CODEX_CMDSOLAR_ROUTER_CLAUDE_CMDSOLAR_ROUTER_GEMINI_CMDSOLAR_ROUTER_OLLAMA_CMD
Ollama setup:
provider=ollamaalways targets the local model namedsolar- Build or refresh it with
bash core/skills/solar-router/scripts/setup_ollama.sh
Validation commands
# Validate skill structure
python3 core/skills/solar-skill-creator/scripts/package_skill.py core/skills/solar-router /tmp
# List configured providers (reads SOLAR_ROUTER_PROVIDER_PRIORITY)
bash core/skills/solar-router/scripts/list_providers.sh
bash core/skills/solar-router/scripts/list_providers.sh --exclude claude
bash core/skills/solar-router/scripts/list_providers.sh --exclude claude --format csv
# Diagnose router / preflight providers (native helper in this skill)
bash core/skills/solar-router/scripts/diagnose_router.sh --dry-run
bash core/skills/solar-router/scripts/diagnose_router.sh
# Full error output when a provider fails (e.g. 401, binary not found)
bash core/skills/solar-router/scripts/diagnose_router.sh --verbose
# Unit tests: router logic + provider adapters (centralized under core/tests; pytest runs unittest-style tests; no real AI calls)
uv run --project core/tests pytest core/tests/skills/solar-router -q
# Without uv: PYTHONPATH=core/skills/solar-router/scripts python3 -m unittest discover -s core/tests/skills/solar-router -p "test_*.py" -v
# Smoke tests: validate router contract v3, bridge delegation, execute_active.py JSON parsing
bash core/skills/solar-router/scripts/check_router.sh
# Live status: provider health, in-flight processes, last executions
bash core/skills/solar-router/scripts/status_router.sh
bash core/skills/solar-router/scripts/status_router.sh --last 20
# Close historical orphan audit records (append reconciled end events)
bash core/skills/solar-router/scripts/reconcile_router_audit.sh --dry-run
bash core/skills/solar-router/scripts/reconcile_router_audit.sh
Router contract v3
Input (stdin JSON)
{
"request_id": "string",
"session_id": "string",
"user_id": "string",
"text": "string",
"channel": "telegram|n8n|async-task|other",
"mode": "auto|direct_only|async_only",
"provider": "codex|claude|gemini|agent|ollama|null",
"metadata": {
"agent": "agent-name|null",
"skills": ["planet:skill-name", "core-skill-name"],
"planet": "planet-name|null"
}
}
provider: optional. If set, strict mode — no fallback. If fails →error_code: provider_locked_failed.mode: defaults toauto.direct_onlyalways returnsdirect_reply.async_onlyrequiresasync-tasksfeature enabled.channel: used byDecisionEnginefor semantic routing inmode=auto.metadata.agent: existing agent name from planet'sagents/, ornullfor JIT role generation.metadata.skills: skill name format —planet:skillresolves toplanets/<planet>/skills/<skill>/SKILL.md; unprefixedskillresolves toplanets/<metadata.planet>/skills/<skill>/SKILL.mdfirst (ifmetadata.planetis set), then falls back tocore/skills/<skill>/SKILL.md. Only description is injected (on-demand).metadata.planet: planet that owns the task domain. Used for agent/skill lookup.
Secure Invocation Protocol (Required)
To prevent JSON parsing errors (invalid control characters, unescaped newlines), always use one of these two methods when calling the router from an AI agent or shell:
Method A: Temporary JSON File (Recommended for Agents)
- Use
write_fileto create a temporary JSON file (e.g.,sun/runtime/router/request_<id>.json). - Ensure the
textfield contains explicit\nfor newlines. - Execute the router piping the file:
python3 core/skills/solar-router/scripts/run_router.py < sun/runtime/router/request_<id>.json.
Method B: Heredoc with Single Quotes (Shell)
Use a heredoc with 'EOF' (single quotes) to prevent the shell from interpreting backslashes or special characters:
cat << 'EOF' | python3 core/skills/solar-router/scripts/run_router.py
{
"request_id": "my-id",
"text": "Line 1\nLine 2",
"channel": "other",
"mode": "direct_only"
}
EOF
Critical Rules:
- Prefer escaped newlines (
\n) inside JSON strings for portability. - Escape double quotes inside the
textstring as\". - Validate JSON before sending if using a custom script.
Output (stdout JSON)
{
"status": "success|failed",
"request_id": "string",
"provider_used": "codex|claude|gemini|agent|ollama",
"reply_text": "string",
"decision": {
"kind": "direct_reply|async_draft_proposal|async_draft_created|async_activation_needed",
"task_id": "string|null",
"priority_suggested": "high|normal|low|null"
},
"error_code": "string|null",
"error": "string|null"
}
DecisionEngine rules
mode=direct_only→decision.kind=direct_replyalways.mode=async_only+async-tasksenabled →decision.kind=async_draft_created.mode=async_only+async-tasksdisabled →status=failed+ explicit error.mode=auto+channel=async-task→decision.kind=direct_reply(already in queue).mode=auto+channel=telegram|n8n|other→ AI decides semantically via structured JSON output.
Consumers
- solar-gateway:
run_websocket_bridge.pyand HTTP webhook bridge call the router with full v3 contract. - solar-async-tasks:
execute_active.py(viaexecute_active.sh) calls the router withchannel=async-task,mode=direct_only.
Runtime files
sun/runtime/router/conversations/<user_id>.jsonl— conversation history per user (for context continuity).sun/runtime/router/audit.jsonl— audit log with onestart/endrecord pair per execution. Fields:router_id(internal UUID),request_id(caller ref),user_id,metadata,provider,status,jit_generated,duration_ms.
References
references/routing-policy.md— provider priority, env keys, repo-context policy, v3 contract rules.