# Query Flow This page traces the end-to-end journey of a user query through the OpenJarvis system, from the moment it enters the CLI or SDK to the final response and telemetry recording. --- ## Sequence Diagram ```mermaid sequenceDiagram actor User participant CLI as CLI / SDK participant CFG as Config & Discovery participant LRN as Learning (Router) participant AGT as Agent participant MEM as Memory Backend participant CTX as Context Injection participant ENG as Inference Engine participant TEL as Telemetry participant TRC as Trace Collector User->>CLI: jarvis ask "query" / j.ask("query") CLI->>CFG: load_config() CFG-->>CLI: JarvisConfig (hardware, engine defaults) CLI->>CFG: get_engine(config) CFG-->>CLI: (engine_key, engine_instance) CLI->>CFG: discover_engines() + discover_models() CFG-->>CLI: available models per engine alt Model not specified CLI->>LRN: select_model(RoutingContext) LRN-->>CLI: model_key (e.g., "qwen3:8b") end alt Agent mode (--agent flag) CLI->>AGT: agent.run(query, context) AGT->>MEM: retrieve(query, top_k=5) MEM-->>AGT: RetrievalResult[] AGT->>CTX: inject_context(query, messages, backend) CTX-->>AGT: messages with context prepended loop Tool-calling loop (max_turns) AGT->>ENG: generate(messages, model, tools) ENG-->>AGT: {content, tool_calls, usage} opt Tool calls present AGT->>AGT: ToolExecutor.execute(tool_call) AGT->>AGT: Append tool results to messages end end AGT-->>CLI: AgentResult(content, tool_results, turns) else Direct mode (no agent) CLI->>MEM: retrieve(query) MEM-->>CLI: RetrievalResult[] CLI->>CTX: inject_context(query, messages, backend) CTX-->>CLI: messages with context CLI->>ENG: instrumented_generate(messages, model) ENG-->>CLI: {content, usage} end CLI->>TEL: TelemetryStore records metrics CLI->>TRC: TraceCollector saves Trace CLI-->>User: Response text ``` --- ## Direct Mode vs Agent Mode OpenJarvis supports two query processing paths, selected by the `--agent` CLI flag or the `agent` parameter in the SDK. ### Direct Mode (Default) In direct mode, the query goes straight to the inference engine with optional memory context. This is the simplest path -- one inference call, no tool loop. ```bash # CLI jarvis ask "What is the capital of France?" # SDK j = Jarvis() response = j.ask("What is the capital of France?") ``` ### Agent Mode In agent mode, the query is handled by a named agent that can perform multiple inference rounds and invoke tools. The `OrchestratorAgent` is the most common choice, enabling a multi-turn tool-calling loop. ```bash # CLI jarvis ask --agent orchestrator --tools calculator,think "What is 2^10 + 3^5?" # SDK response = j.ask("What is 2^10 + 3^5?", agent="orchestrator", tools=["calculator"]) ``` --- ## Step-by-Step Walkthrough ### Step 1: Configuration Loading The journey begins with loading the system configuration: ```python config = load_config() # Reads ~/.openjarvis/config.toml ``` This step: - Detects system hardware (GPU vendor/model, CPU, RAM) - Recommends the best inference engine for the detected hardware - Overlays any user overrides from the TOML file - Returns a `JarvisConfig` dataclass with all settings ### Step 2: Engine Discovery Next, the system finds a running inference engine: ```python resolved = get_engine(config, engine_key) # Returns (engine_key, engine_instance) or None ``` The discovery process: 1. If a specific engine was requested (`--engine` flag), try that engine 2. Otherwise, try the default engine from config (e.g., `"ollama"`) 3. If the default is unhealthy, probe all registered engines and use the first healthy one 4. If no engine is available, exit with an error message ### Step 3: Model Discovery and Registration Once an engine is found, the system discovers available models: ```python register_builtin_models() # Register known models (catalog) all_engines = discover_engines(config) all_models = discover_models(all_engines) for ek, model_ids in all_models.items(): merge_discovered_models(ek, model_ids) # Register runtime-discovered models ``` ### Step 4: Model Routing If no model was explicitly specified, the router policy selects one: ```python from openjarvis.learning import ensure_registered from openjarvis.learning.router import build_routing_context ensure_registered() # Ensure learning policies are registered policy_key = router_policy or config.learning.routing.policy router_cls = RouterPolicyRegistry.get(policy_key) router = router_cls( available_models=all_models.get(engine_name, []), default_model=config.intelligence.default_model, fallback_model=config.intelligence.fallback_model, ) ctx = build_routing_context(query_text) model_name = router.select_model(ctx) ``` The `build_routing_context()` function (in `learning/router.py`) analyzes the query for code patterns, math keywords, length, and urgency. The router then applies its rules (heuristic or learned) to select the optimal model. ### Step 5: Memory Context Injection If memory context injection is enabled (default: `true`) and the memory backend has indexed documents: ```python backend = _get_memory_backend(config) if backend is not None: ctx_cfg = ContextConfig( top_k=config.memory.context_top_k, # Default: 5 min_score=config.memory.context_min_score, # Default: 0.1 max_context_tokens=config.memory.context_max_tokens, # Default: 2048 ) messages = inject_context(query_text, messages, backend, config=ctx_cfg) ``` This retrieves relevant chunks from the memory backend and prepends a system message with the retrieved context and source attribution. !!! tip "Disabling context injection" Use `--no-context` on the CLI or `context=False` in the SDK to skip memory context injection. ### Step 6: Inference Generation **In direct mode**, the query is sent to the engine via the instrumented wrapper: ```python result = instrumented_generate( engine, messages, model=model_name, bus=bus, temperature=temperature, max_tokens=max_tokens, ) ``` The `instrumented_generate()` wrapper: 1. Publishes `INFERENCE_START` on the event bus 2. Records the start time 3. Calls `engine.generate()` 4. Records end time, calculates latency 5. Publishes `INFERENCE_END` with timing and token counts 6. Publishes `TELEMETRY_RECORD` with the full `TelemetryRecord` **In agent mode**, the agent manages inference calls internally, potentially making multiple rounds with tool calls in between. ### Step 7: Tool Execution (Agent Mode Only) When the `OrchestratorAgent` receives tool calls in the model's response: 1. Each tool call is dispatched to the `ToolExecutor` 2. The executor publishes `TOOL_CALL_START`, executes the tool, publishes `TOOL_CALL_END` 3. Tool results are appended to the message history as `TOOL` messages 4. The updated messages are sent back to the engine for the next round 5. This loop continues until the model responds without tool calls or `max_turns` is reached ### Step 8: Telemetry Recording After every inference call, a `TelemetryRecord` is created and persisted: ```python @dataclass(slots=True) class TelemetryRecord: timestamp: float model_id: str prompt_tokens: int completion_tokens: int total_tokens: int latency_seconds: float ttft: float # Time to first token cost_usd: float energy_joules: float power_watts: float engine: str agent: str metadata: Dict[str, Any] ``` The `TelemetryStore` subscribes to `TELEMETRY_RECORD` events on the EventBus and writes records to `~/.openjarvis/telemetry.db`. ### Step 9: Trace Recording When a `TraceCollector` is wrapping the agent, a complete `Trace` is built from the events captured during execution: 1. All `INFERENCE_START`/`END` events become `GENERATE` steps 2. All `TOOL_CALL_START`/`END` events become `TOOL_CALL` steps 3. All `MEMORY_RETRIEVE` events become `RETRIEVE` steps 4. A final `RESPOND` step captures the output 5. The trace is saved to the `TraceStore` and `TRACE_COMPLETE` is published ### Step 10: Response Delivery The final response is delivered to the user: - **CLI:** Printed to stdout (or as JSON with `--json`) - **SDK:** Returned as a string from `ask()` or as a dict from `ask_full()` --- ## EventBus Activity During a Query The following events are published during a typical query in agent mode: ``` AGENT_TURN_START {agent: "orchestrator", input: "What is 2+2?"} INFERENCE_START {model: "qwen3:8b", engine: "ollama", turn: 1} INFERENCE_END {model: "qwen3:8b", engine: "ollama", turn: 1} TELEMETRY_RECORD {model_id: "qwen3:8b", latency: 0.8, tokens: 150} TOOL_CALL_START {tool: "calculator", arguments: {expression: "2+2"}} TOOL_CALL_END {tool: "calculator", success: true, latency: 0.01} INFERENCE_START {model: "qwen3:8b", engine: "ollama", turn: 2} INFERENCE_END {model: "qwen3:8b", engine: "ollama", turn: 2} TELEMETRY_RECORD {model_id: "qwen3:8b", latency: 0.5, tokens: 80} AGENT_TURN_END {agent: "orchestrator", turns: 2, content_length: 12} TRACE_COMPLETE {trace: Trace(...)} ``` --- ## SDK Query Flow The `Jarvis` class in `sdk.py` provides the same query flow through a Python API: ```python from openjarvis import Jarvis j = Jarvis(model="qwen3:8b", engine_key="ollama") # Direct mode response = j.ask("Hello") # Agent mode with tools response = j.ask( "What is 2^10?", agent="orchestrator", tools=["calculator"], ) # Full result with metadata result = j.ask_full("Hello") # { # "content": "Hello! How can I help you?", # "usage": {"prompt_tokens": 10, "completion_tokens": 15, "total_tokens": 25}, # "model": "qwen3:8b", # "engine": "ollama", # } j.close() ``` The SDK handles lazy engine initialization, telemetry setup, memory context injection, and resource cleanup internally. The `ask()` method delegates to `ask_full()` and extracts just the content string.