Clapilot-code Native Agent Loop

Implementation-level guide to Clapilot-code profiles, prompt assembly, provider tool loops, steering, safety guards, persistence, and diagnostics.

Generated explanatory diagrams

Clapilot-code explained visually

These raster diagrams are generated documentation assets and are paired with exact captions below so the page is readable even when a tiny in-image label is hard to inspect.

Clapilot-code loop lifecycle
Clapilot-code loop lifecycle

Summarizes the per-turn cycle from session queue and context assembly through provider/tool iteration, verification, durable persistence, and the separate Assistant and Coding step budgets.

Prompt and tool architecture
Prompt and tool architecture

Separates cache-stable prompt layers from query-dependent context, then shows how the acting provider model starts with direct tools and progressively expands catalog families on demand.

Reliability and observability guards
Reliability and observability guards

Shows the bounded provider/tool cycle and the controls around it: idempotency, repeated-batch detection, timeout backoff, no replay after side effects, live steering, and the durable event audit trail.

Clapilot-code is the product name for Clapilot's in-process agent harness inside clapilot-agent. It is not a separate model provider and it is not the legacy compatibility runtime. It takes a configured direct-provider model, assembles the runtime context, repeatedly lets the model call Clapilot tools, persists the resulting state, and exposes the same run to web chat, channels, jobs, and Agent Orchestrator.

The harness has two profiles that share the same execution machinery:

Product profileInternal wire valueIntended behaviorTool-step budget
Clapilot-code (Assistant)nativeGeneral assistant behavior with full Clapilot context, tools, memory, and UI-aware routing24 by default
Clapilot-code (Coding)embedded_piPersistent coding/workspace behavior on the same selected provider transport40

embedded_pi, embedded-pi, pi, and clapilot_code still appear at compatibility boundaries. New user-facing configuration and Agent Orchestrator requests use clapilot-code; the internal names do not identify a second deployed service.

The key architectural distinction is harness versus transport. Selecting Clapilot-code changes the prompt contract and loop budget. It does not replace an OpenAI, Anthropic, Bedrock, Gemini, xAI, Azure, or OpenAI-compatible model with a hidden provider.

One turn at a glance

One call to sessions.executeRun() passes through these phases:

  1. Serialize and deduplicate. A per-session FIFO prevents overlapping turns in the same conversation. An idempotency key reuses an already completed run or waits for the in-flight copy.
  2. Resolve identity, model, and profile. The runtime binds the Clapilot user/service principal, resolves the provider/model and fallbacks, then selects Assistant or Coding mode.
  3. Bootstrap context. It loads prompt files, memory retrieval, approved Learning objects, compacted history, recent turns, UI/page context, repository context, and the current request.
  4. Fit the context window. Token estimates reserve output and safety headroom, drop optional volatile context in a fixed order, and trim replay history before calling a provider.
  5. Run the provider loop. The model either returns a final answer or one or more tool calls. Tool results become the next provider input until the model stops or a guard ends the loop.
  6. Validate and persist. The runtime applies response-quality retries when required, writes the run/session/event records, and stores a lossless replay turn.
  7. Post-process asynchronously. Context-graph compaction, shared fact extraction, Memory v2 projection work, and Learning extraction continue without delaying the user-facing answer.

The primary implementation is split intentionally:

  • services/clapilot-agent/src/sessions/index.mjs owns session serialization, prompt assembly, tool selection/execution, steering, persistence, and post-processing.
  • services/clapilot-agent/src/providers/index.mjs owns provider selection, provider-specific request formats, the iterative tool-call loops, retries, fallbacks, and provider request logs.
  • services/clapilot-agent/src/providers/message-sanitizer.mjs normalizes imperfect model output before a tool call is trusted.
  • services/clapilot-agent/src/tool-definitions.mjs supplies the shared typed Clapilot tool contracts.

Profile resolution

The selectable non-specialized profile applies to direct-provider runs. Specialized agents reuse the underlying execution machinery with a stricter prompt/tool envelope, while subscription-backed Codex and Claude rows keep their specialized bridges.

For a normal direct-provider run, profile selection uses this precedence:

  1. Explicit agentCore on the run.
  2. Explicit runtimePath on the run.
  3. The selected provider row's metadata.agent_core override.
  4. Global model_routing.non_specialized_agent_core.
  5. native as the default.

resolveNonSpecializedAgentCore() canonicalizes the values:

native | clapilot-code | clapilot_code
  -> Clapilot-code (Assistant)

embedded_pi | embedded-pi | clapilot-code-coding
  -> Clapilot-code (Coding)

The Coding profile prepends a persistence-oriented operating contract to the normal native prompt. It tells the model to inspect, act, verify, maintain its todo list, and continue until a meaningful step is complete or a real blocker is reached. The selected provider and model remain unchanged.

Session serialization and idempotency

The loop is concurrent across unrelated sessions but ordered within one session.

executeRun() keeps one process-local queue per sessionKey. Every new turn waits for the previous turn in that session, while another session can run at the same time. The default maximum is 48 queued/running entries per session and can be changed with CLAPILOT_AGENT_SESSION_RUN_QUEUE_MAX_PENDING.

Idempotency works at two layers:

  • runtimeQueueState.inFlightByIdempotencyKey deduplicates concurrent copies inside the process.
  • The unique database index on (session_key, run_idempotency_key(input_payload)) prevents a second durable agent_runs row.

The key may come from idempotencyKey, messageId, or clientMessageId in either the run payload or its nested input payload. A completed duplicate returns the persisted output with deduped: true. A matching queued/running row is polled for up to 90 seconds before the caller receives an in-progress error.

This matters for web retries and channel webhook redelivery: the runtime can safely acknowledge the same logical user turn without executing its tools twice.

Context assembly

1. Resolve execution identity

Before prompt construction, the runtime resolves:

  • linked Clapilot user;
  • service principal and its permissions;
  • web chat session or channel thread;
  • durable and transient client context;
  • active repository checkout and ephemeral GitHub shell environment for Orchestrator work;
  • specialized-agent restrictions, when present.

Tool execution receives this resolved envelope. A model never chooses its own user id, session binding, filesystem root, or service principal.

2. Bootstrap memory and maintenance

memory.bootstrap() loads prompt files and query-relevant native memory for the current session/user. Before the provider call, the session layer may also:

  • flush a growing transcript into durable memory;
  • compact older session history into a summary;
  • reload bootstrap context after a successful flush;
  • preserve recent turns verbatim after compaction.

Channel runs may skip pre-response maintenance to avoid webhook latency. Post-response maintenance still records the lossless turn and durable outputs.

3. Build the stable and volatile prompt layers

The message order is deliberate because provider prompt caches depend on a stable prefix.

OrderLayerCache behaviorSource
1Runtime system promptStableAssistant/Coding profile, safety, tool rules, workspace and identity
2Compaction summaryStableagent_session_state.state_json compaction state
3Bootstrap prompt filesStableIDENTITY.md, SOUL.md, USER.md, MEMORY.md, repository instructions, and related files
4Extra system/UI contextStable within one cache lineagePage context and caller instructions; a changed value intentionally starts a cold lineage
5Stored replay framesAppend-only conversationExact prior per-turn runtime context, request, and assistant output after the last compacted boundary
6Current turn frameAppend-only conversationCurrent time, retrieved memory, approved Learning context, optional Mixture-of-Agents synthesis, and the current request

For text-only native turns, query-dependent context is wrapped into a runtime-generated [# Trusted Current Turn Context] user turn and persisted in agent_runs.input_payload.promptCacheReplay. The next request replays that exact augmented turn before appending the new one, making the serialized conversation append-only for local prefix/KV caches. The replay record carries a hash of the exact stable model/system/tool prefix; changes to the model, stable prompt, bootstrap, compaction summary, page context, or initial tool surface deliberately reset the lineage and fall back to raw user/assistant history. Attachments and other non-text requests retain the legacy safe assembly path.

Anthropic uses an ephemeral cache_control breakpoint after the final stable system block. Bedrock inserts a cache point at the same conceptual boundary. OpenAI/Azure can receive a stable prompt_cache_key routing hint. OpenAI-compatible local models additionally keep their query-routed initial compact tool surface stable while prompt caching is enabled; new tool families remain available through catalog expansion.

4. Fit the model context window

resolveContextBudget() calculates:

prompt budget = context window - reserved output - safety headroom

The runtime estimates every layer separately and stores the result as promptLayerTokens. When the estimate is too large it first removes optional approved-Learning context, then optional retrieved memory, then fits recent history to the remaining budget. An optional Mixture-of-Agents block is also dropped if it is the final reason the request does not fit.

The runtime logs both the configured limit source and whether a shipped/generic fallback limit was used. If a provider still rejects the request as too large, the provider layer can perform one additional provider-neutral context-compression retry, but only before any visible output or tool side effect.

Tool surface architecture

Clapilot-code does not send the entire application API as one enormous tool schema on every turn. The effective default profile is compact_tools: a direct core plus progressive disclosure.

Direct tools

Frequently needed tools remain directly callable, including:

  • todo tracking and session status;
  • context, memory, Knowledge Graph, and Learning recall;
  • public web search;
  • UI rendering and navigation;
  • image/video generation status paths;
  • specialized-agent and subagent delegation.

The runtime also adds tools routed from the current request and active page context. For example, an explicit calendar request can make the calendar family directly available on the first provider step.

Catalog discovery

Three meta-tools keep the rest discoverable:

  • tool_catalog_search searches concrete tools and installed workspace skills.
  • tool_catalog_expand expands one or more named families into real typed functions.
  • tool_execute dispatches a known hidden tool by name when direct expansion is unnecessary.

When a catalog tool returns expandedTools, the provider loop merges those definitions into its active tool list. The model can call the newly exposed function on the next step without restarting the run.

Skills mode

When app_settings.tool_surface_mode = "skills", most application families move behind the clapilot-cli binary and workspace SKILL.md files. Shell access, context/memory/knowledge recall, UI/navigation, delegation, session status, and a small set of high-value media/skill tools remain typed.

This changes discovery and schema cost; it does not change the provider loop, identity envelope, persistence model, or safety boundaries.

Specialized agents

A specialized agent receives its explicit allowlist plus optional core memory tools. Recursive delegation tools are removed from that surface. Public external specialized-agent runs use a smaller public-safe prompt and can exclude subscription bridges unless an explicit model authorizes one.

The provider-neutral loop

At the session layer, the loop call is conceptually:

providers.runConversation({
  model,
  messages,
  tools,
  maxToolSteps,
  parallelToolCalls: true,
  executeTool,
  onDelta,
  onToolEvent,
  onProviderAttempt
})

Each provider adapter maps the same contract onto its native protocol:

Provider familyRequest protocolTool continuation
OpenAI / Azure / compatibleChat Completions for ordinary text; Responses for rich content and current OpenAI/xAI pathsassistant tool_calls + tool messages, or Responses function_call_output with previous_response_id
Anthropic APIMessages APItool_use blocks followed by tool_result blocks
AWS BedrockConverseBedrock tool-use/result content blocks
Google GeminigenerateContentfunctionCall followed by functionResponse
xAIResponses-compatible pathResponses function calls and outputs
Claude subscriptionClaude CLI bridgeSeparate specialized bridge, not the direct Clapilot-code profile
OpenAI Codex subscriptionCodex app-server bridgeSeparate specialized bridge, not the direct Clapilot-code profile

For a direct provider, every adapter follows the same state machine:

provider request
  -> final text? return
  -> tool calls? sanitize and validate
  -> execute safe batch
  -> append tool results
  -> merge newly expanded tool definitions
  -> next provider request

The Assistant profile uses the default maximum of 24 provider/tool steps (CLAPILOT_MAX_TOOL_STEPS). The Coding profile explicitly raises that run budget to 40. Reaching the limit fails the run; the runtime does not silently turn an unfinished tool trace into a successful answer.

Tool-call normalization and weak-model robustness

Every provider output passes through sanitizeAssistantMessage() before tool execution.

The sanitizer can:

  • remove closed or boundary-gated unterminated reasoning tags;
  • recover tool calls emitted as <tool_call>, <tool_calls>, <function_call>, or boundary-safe <function name="..."> text blocks;
  • repair invalid control characters and likely unescaped quotes in JSON arguments;
  • close one unambiguous missing JSON brace/bracket;
  • trim trailing junk after one complete JSON value;
  • remove lone UTF-16 surrogates;
  • preserve clean strong-model output byte-for-byte on the fast path.

Structured provider tool calls win over text salvage; the runtime does not execute both copies. If the Responses API output had to be repaired or salvaged, the next request reconstructs the assistant/tool-call history manually instead of trusting previous_response_id to reproduce malformed content.

Tool schemas can also be coerced per provider when a configured compatibility mode requires stricter or simpler JSON Schema.

Tool execution, parallelism, and events

Each tool call emits a canonical start event, executes inside the resolved identity/workspace envelope, then emits an end event with a short preview and any structured UI actions.

Parallel execution is opt-in and conservative. A batch runs with Promise.all only when:

  1. the provider returned at least two calls;
  2. the run requested parallel tool calls; and
  3. every call is in PARALLEL_SAFE_NATIVE_TOOL_NAMES.

The allowlist consists primarily of reads: context/memory/knowledge lookup, document reads, list/get operations, session status, and similar non-mutating inspection. If one call is not allowlisted, the entire batch executes sequentially in provider order. Mutations are therefore not parallelized merely because a model grouped them together.

tool.start and tool.end rows in agent_events include:

  • display tool name and call id;
  • provider and arguments;
  • error flag and result preview;
  • Copilot UI actions;
  • normalized todo list;
  • reload/refresh metadata and mutation event id.

The display layer unwraps tool_execute so diagnostics show the actual dispatched tool instead of only the catalog wrapper.

Live steering and cancellation

Direct Assistant and Coding loops are steerable at tool boundaries.

When a user sends a follow-up during an active run, the session stores a pending steer containing text and normalized attachments. After the current tool finishes, the executor appends a LIVE USER STEER notice to that tool result before it returns to the model. If no tool is active because the model is already generating, the runtime aborts that provider request, holds the original stream open, preserves any text already emitted, and immediately starts a continuation provider step with the steer. A steer is not acknowledged during the short transition/finalization gap, so the browser retains it for normal queued delivery instead of dropping it.

Steering uses the earliest safe interruption point:

  • a run that never reaches another tool boundary cannot consume the pending steer;
  • multiple steers are drained together in arrival order;
  • text-file attachments can include a bounded decoded preview;
  • the active run reports the pending count and active tool count.

Cancellation uses the same safe boundary. An abort request marks the active run; the next tool boundary throws a user-abort error so the durable run becomes cancelled instead of failed.

Codex and Claude subscription bridges use their own direct steering mechanisms and are documented separately in Request Flows.

Loop guards and failure semantics

Repeated batch guard

The provider layer creates a stable signature from tool names plus recursively key-sorted arguments. If the model emits the same batch three consecutive times by default, the run fails with a repeated-tool-batch error. CLAPILOT_MAX_IDENTICAL_TOOL_BATCH_REPEATS changes the threshold.

This guard catches a model that keeps reading or mutating the same target without incorporating the previous result.

Provider timeouts

Timeout-shaped failures use bounded exponential backoff with up to 50 percent positive jitter. The default retry configuration is:

  • maximum three timeout attempts;
  • 2-second base backoff;
  • 30-second backoff cap;
  • circuit opens after three exhausted timeout failures;
  • circuit stays open for 10 minutes.

All values have CLAPILOT_PROVIDER_TIMEOUT_* environment overrides.

No replay after irreversible progress

A provider attempt becomes irreversible when it emits any visible delta, emits a tool event, or invokes a tool. After that point, a timeout/rate-limit/provider failure is surfaced instead of replaying the request on the same or a fallback model.

This is a core side-effect guarantee: a flaky upstream response must not cause Clapilot to send a message twice, create two tasks, or repeat a repository mutation.

Rate limits, quotas, and fallbacks

Rate-limited models enter a process-local cooldown. Quota exhaustion uses the provider reset time when available, otherwise a conservative retry delay. Before irreversible progress, the provider layer can try the configured fallback chain. Authentication failures quarantine the affected provider for the current request and, for Clapilot-owned credentials, the process runtime.

Every attempt is surfaced to the session layer as provider.attempt, provider.retry, provider.circuit_open, or provider.completed, and every actual upstream call gets its own agent_model_request_logs row.

Response-quality retries

After the provider loop returns, the session layer may run one targeted retry with an amended system instruction when the output violates a product contract:

  • an explicit/obvious Copilot UI choice response omitted copilot_ui_render;
  • the reply is only a generic completion word even though no tool action was recorded;
  • compact routing missed an applicable tool family, in which case the runtime expands the routed tool set and rebuilds the system prompt.

These are complete provider reruns, not hidden text rewrites. They are logged as run.retry, and later persistence uses the final accepted result.

Persistence and observability

The durable model is intentionally layered:

TableWhat it answers
agent_session_stateWhat model/profile/context/compaction state is bound to this session now?
agent_runsWhat was the logical user turn, its final status/output, and aggregate usage?
agent_eventsWhat lifecycle, provider, and tool events happened inside that run?
agent_model_request_logsWhich concrete upstream requests were made, with what provider/model/transport/tokens/cache metadata?
lossless context graph tablesWhich user/assistant messages are available for exact replay and later compaction?

A successful run performs the critical writes in this order:

  1. Update agent_runs with completed status, final model, output, usage, and retry summary.
  2. Update agent_session_state.last_run_id, effective model, prompt-file hash, and activity timestamp.
  3. Record the user/assistant turn for lossless replay.
  4. Write run.completed with profile, runtime path, tool stage/count, and post-process scheduling state.
  5. Return the final answer to the caller.
  6. Run detached memory/learning maintenance and write run.postprocess.completed or run.postprocess.failed.

The synchronous lossless turn write happens before final run completion is returned, so the next queued message can replay the completed turn even if detached enrichment is still running.

Post-response memory and Learning work

Detached maintenance uses the exact model reference that produced the response. It does not silently move a Codex/Claude result to a weaker local maintenance model.

The maintenance pipeline:

  • compacts the lossless context graph when needed;
  • extracts and stores shared facts under the correct visibility scope;
  • processes up to 20 pending Memory v2 projection/outbox entries;
  • records canonical assertion projection status;
  • runs conservative legacy Learning extraction only when the Memory v2 canonical path was not used;
  • writes counts, ids, failures, and skip reasons to run.postprocess.completed.

Failure here does not convert an already delivered answer into a failed user run. It is visible as a separate post-process event for repair and replay.

Debugging a stuck or wrong run

Use this order to avoid mixing failure layers:

  1. Confirm the runtime path. In agent_session_state.bootstrap_meta, check agentCore, configuredNonSpecializedAgentCore, runtimePath, toolProfile, and toolStage. embedded_pi should appear for Coding, native for Assistant.
  2. Read the logical run. Inspect agent_runs.status, effective_model, error_message, usage_json, and input_payload.promptLayerTokens.
  3. Read the event timeline. Order agent_events by creation time and look for run.started, provider events, tool.start, tool.end, retries, completion, and post-process.
  4. Split provider from tool failures. agent_model_request_logs shows each upstream HTTP/bridge call. A tool result error can occur after a successful provider request and must not be charged to the provider circuit.
  5. Check loop guards. Repeated identical tool batches and the 24/40 step ceiling are explicit terminal failures.
  6. Check irreversible progress. If a fallback did not run after a timeout, verify whether a delta/tool event/tool invocation had already happened; suppressing replay is expected.
  7. Check queue/idempotency state. A session can be waiting behind a prior turn even when the provider is healthy. Duplicate message ids intentionally reuse the first run.
  8. Check post-processing separately. A completed answer with run.postprocess.failed is a memory/learning enrichment problem, not a failed agent loop.

Useful admin surfaces:

  • Settings → ClapilotAICore → Sessions for session/run/event/harness metadata;
  • Settings → ClapilotAICore → Request Logs for concrete provider calls and prompt/cache token layers;
  • Agent Orchestrator for repository-backed Clapilot-code jobs/sessions and follow-ups;
  • the active chat tool timeline for streamed clapilot.tool events.

Configuration reference

SettingDefaultEffect
model_routing.non_specialized_agent_corenativeGlobal Assistant/Coding profile for direct providers
provider metadata.agent_coreinheritPer-provider profile override
app_settings.tool_surface_modenative_toolsTyped progressive tools or skill/CLI-oriented surface
app_settings.main_agent_tool_restrictions_enabledfalseExplicitly activates the built-in main agent's per-tool denylist; specialized/public authorization boundaries remain independent
app_settings.main_agent_disabled_tool_names[]Exact tool names blocked for the built-in main agent while the opt-in is enabled; editable individually or by catalog group
CLAPILOT_MAX_TOOL_STEPS24Default Assistant provider/tool-step ceiling
Coding maxToolSteps40Explicit Coding ceiling set by the session layer
CLAPILOT_MAX_IDENTICAL_TOOL_BATCH_REPEATS3Consecutive identical batch limit
CLAPILOT_AGENT_SESSION_RUN_QUEUE_MAX_PENDING48Maximum queued/running entries for one session
CLAPILOT_PROVIDER_TIMEOUT_MAX_ATTEMPTS3Timeout attempt budget
CLAPILOT_PROVIDER_TIMEOUT_BACKOFF_BASE_MS2000Retry backoff base
CLAPILOT_PROVIDER_TIMEOUT_BACKOFF_MAX_MS30000Retry backoff cap
CLAPILOT_PROVIDER_TIMEOUT_CIRCUIT_THRESHOLD3Exhausted timeout failures before circuit open
CLAPILOT_PROVIDER_TIMEOUT_CIRCUIT_OPEN_MS600000Circuit-open duration

Verification map

The most relevant focused tests are:

  • services/clapilot-agent/src/sessions/index.test.mjs
    • profile precedence and normalization;
    • per-session FIFO behavior and queue pressure;
    • reduced/compact tool reachability;
    • tool wrapper display names and Copilot UI retry detection.
  • services/clapilot-agent/src/providers/index.test.mjs
    • timeout retry/backoff/circuit behavior;
    • no retry or fallback after streamed/tool progress;
    • provider context compression retry;
    • parallel tool progress tracking;
    • progressive tool-definition merge;
    • provider-specific cache/tool schema transformations.
  • services/clapilot-agent/src/providers/message-sanitizer.test.mjs
    • reasoning removal;
    • text tool-call salvage;
    • malformed JSON argument repair;
    • strong-model fast-path preservation;
    • invalid surrogate removal.

Source map

ConcernCanonical source
Profile aliases and precedenceservices/clapilot-agent/src/sessions/index.mjsnormalizeNonSpecializedAgentCore, resolveNonSpecializedAgentCore
Session FIFO and idempotencyservices/clapilot-agent/src/sessions/index.mjscreateSessionRunQueue, executeRun
Prompt/profile constructionservices/clapilot-agent/src/sessions/index.mjsbuildNativeSystemPrompt, buildEmbeddedPiSystemPrompt
Tool profiles and progressive disclosureservices/clapilot-agent/src/sessions/index.mjsgetNativeTools, executeNativeTool
Context budget and run orchestrationservices/clapilot-agent/src/sessions/index.mjsexecuteRunNow
Provider selection and fallback chainservices/clapilot-agent/src/providers/index.mjsresolveProviderSelection, runConversation
Provider-specific loopsservices/clapilot-agent/src/providers/index.mjsrunOpenAiResponsesWithTools, runOpenAiLike, runAnthropic, runBedrock, runGemini
Tool-call recoveryservices/clapilot-agent/src/providers/message-sanitizer.mjs
Tool contractsservices/clapilot-agent/src/tool-definitions.mjs
Durable schemadb/migrations/028_native_agent_runtime.sql, 052_agent_model_request_logs.sql, and later agent-runtime migrations

Related documentation

  • Request Flows — ingress, provider/bridge choice, prompt assembly, caching, and bridge behavior.
  • Sessions — identity, replay history, compaction, and the Sessions diagnostics surface.
  • Memory — bootstrap files, hybrid retrieval, shared facts, and Memory v2 lifecycle.
  • Providers + Models — provider catalog, model limits, auth modes, and fallback configuration.
  • Agent Tool Contracts — application tool schemas and mutation contracts.
  • Agent Orchestrator — how provider=clapilot-code creates repository-backed jobs and sessions.