openclaw/CLI_VS_EMBEDDED_ARCHITECTURE.md

566 lines
18 KiB
Markdown

# Clawdbot: CLI vs Embedded Agent Architecture Analysis
## Executive Summary
This document provides a technical comparison of the two agent execution paths in Clawdbot:
1. **Embedded Path** - Direct API calls to Anthropic/OpenAI/Google
2. **CLI Path** - Subprocess invocation of claude-cli, codex-cli, etc.
**Key Finding:** The CLI path is significantly under-implemented compared to the embedded path. Multiple critical features are either missing or incorrectly handled, leading to degraded agent behavior.
---
## Architecture Overview
### Entry Point & Branching
**File:** `src/auto-reply/reply/agent-runner-execution.ts`
```typescript
// Line 157 - The branching decision
if (isCliProvider(provider, params.followupRun.run.config)) {
return runCliAgent({...}); // CLI PATH
}
return runEmbeddedPiAgent({...}); // EMBEDDED PATH
```
### Execution Flow Comparison
| Phase | Embedded Path | CLI Path |
|-------|--------------|----------|
| Entry | `runEmbeddedPiAgent()` | `runCliAgent()` |
| System Prompt | `buildEmbeddedSystemPrompt()` | `buildSystemPrompt()` |
| Tools | Full `AgentTool[]` via SDK | `tools: []` (disabled) |
| Context Files | Injected as SDK context + in prompt | In prompt only |
| Session | `SessionManager` (mariozechner) | CLI `--session-id` / `--resume` |
| Execution | SDK streaming | Subprocess with timeout |
---
## Critical Issues Found
### 1. 🔴 System Prompt NOT Sent on Resume
**Location:** `src/agents/cli-runner/helpers.ts:422`
```typescript
if (!params.useResume && params.systemPrompt && params.backend.systemPromptArg) {
args.push(params.backend.systemPromptArg, params.systemPrompt);
}
```
**Problem:** When `useResume=true`, the system prompt (including ALL workspace context like TOOLS.md, AGENTS.md) is **not passed to the CLI**.
**Impact:** On resumed sessions (heartbeats, follow-up messages), the agent has no access to:
- Workspace instructions (AGENTS.md)
- Tool documentation (TOOLS.md)
- Memory files (MEMORY.md)
- Identity (SOUL.md, IDENTITY.md)
- Heartbeat instructions (HEARTBEAT.md)
**Backend Config:**
```typescript
// src/agents/cli-backends.ts:46-48
systemPromptArg: "--append-system-prompt",
systemPromptMode: "append",
systemPromptWhen: "first", // <-- Only on FIRST message!
```
**Root Cause:** `systemPromptWhen: "first"` combined with `resolveSystemPromptUsage()` returning `null` for non-new sessions means context is only injected on session creation.
### 2. 🔴 Tools Explicitly Disabled
**Location:** `src/agents/cli-runner.ts:66-71`
```typescript
const extraSystemPrompt = [
params.extraSystemPrompt?.trim(),
"Tools are disabled in this session. Do not call tools.", // <-- Hardcoded!
]
```
**And:** `src/agents/cli-runner.ts:103`
```typescript
const systemPrompt = buildSystemPrompt({
// ...
tools: [], // <-- Empty array!
// ...
});
```
**Impact:** The CLI path fundamentally cannot use Clawdbot's tool system:
- No `exec` (shell commands)
- No `read/write/edit` (file operations)
- No `message` (send messages)
- No `web_search/web_fetch`
- No `browser`, `canvas`, `nodes`
- No `memory_search/memory_get`
- No `tts`, `image`, `cron`
**Embedded Path Comparison:** Gets full tool suite via `createClawdbotCodingTools()`:
```typescript
// src/agents/pi-embedded-runner/run/attempt.ts:202-232
const tools = createClawdbotCodingTools({
exec: { ... },
sandbox,
sessionKey,
agentDir,
workspaceDir,
config,
// ... full tool configuration
});
```
### 3. 🟡 Context Injection Differences
**Embedded Path:**
```typescript
// Context loaded via resolveBootstrapContextForRun()
// Passed to SDK as separate contextFiles parameter
// Also injected into system prompt text
```
**CLI Path:**
```typescript
// Context loaded (same function)
// BUT only injected into system prompt text
// AND only on first message (systemPromptWhen: "first")
```
**The CLI path DOES load context files correctly** - they go through `resolveBootstrapContextForRun()`. But they're only useful on the first message because of the `systemPromptWhen` setting.
### 4. 🟡 Session Management Asymmetry
**Embedded Path:**
- Uses `SessionManager` from `@mariozechner/pi-coding-agent`
- Full conversation history management
- Compaction support
- Transcript persistence
**CLI Path:**
- Relies entirely on Claude Code's internal session management
- Session ID passed via `--session-id` (new) or `--resume` (existing)
- No access to session internals
- Manual transcript persistence added via patches
### 5. 🟡 No Streaming Support
**Embedded Path:**
```typescript
// Uses streamSimple() for real-time token streaming
// Events emitted: "assistant", "thinking", "tool_use", etc.
```
**CLI Path:**
```typescript
// Subprocess with timeout, output buffered
// Only get result after CLI exits
// No intermediate events
```
---
## Feature Comparison Matrix
| Feature | Embedded | CLI | Gap Severity |
|---------|----------|-----|--------------|
| System Prompt (first msg) | ✅ | ✅ | - |
| System Prompt (resume) | ✅ | ❌ | 🔴 Critical |
| Workspace Context (first) | ✅ | ✅ | - |
| Workspace Context (resume) | ✅ | ❌ | 🔴 Critical |
| Tools (exec, read, write) | ✅ | ❌ | 🔴 Critical |
| Tools (message, browser) | ✅ | ❌ | 🔴 Critical |
| Session History | ✅ SDK | ✅ CLI-managed | 🟢 OK |
| Streaming Output | ✅ | ❌ | 🟡 Medium |
| Thinking/Reasoning | ✅ | ✅ (via CLI) | 🟢 OK |
| Image Input | ✅ | ✅ | 🟢 OK |
| Timeout Handling | ✅ | ✅ | 🟢 OK |
| Error Recovery | ✅ | ⚠️ Basic | 🟡 Medium |
| Usage Tracking | ✅ | ⚠️ Parsed from output | 🟢 OK |
---
## Code Paths Deep Dive
### Embedded Path: Full Context Flow
```
getReplyFromConfig()
└─> runPreparedReply()
└─> runAgentTurnWithFallback()
└─> runEmbeddedPiAgent()
└─> resolveBootstrapContextForRun() ✅ Load context
└─> createClawdbotCodingTools() ✅ Create tools
└─> buildEmbeddedSystemPrompt() ✅ Build prompt with context
└─> createAgentSession() ✅ Pass tools + context to SDK
└─> subscribeEmbeddedPiSession() ✅ Stream execution
```
### CLI Path: Incomplete Flow
```
getReplyFromConfig()
└─> runPreparedReply()
└─> runAgentTurnWithFallback()
└─> runCliAgent()
└─> resolveBootstrapContextForRun() ✅ Load context
└─> tools: [] ❌ No tools
└─> buildSystemPrompt() ⚠️ Context in prompt
└─> resolveSystemPromptUsage() ❌ Returns null if !isNew
└─> buildCliArgs() ❌ Skips prompt if useResume
└─> runCommandWithTimeout() ⚠️ Just subprocess
```
---
## Recommendations
### Immediate Fixes (High Priority)
1. **Always inject workspace context on CLI resume**
- Change `systemPromptWhen: "first"` to `"always"` for claude-cli
- Or implement separate context injection mechanism
- Without this, resumed sessions have NO agent identity/instructions
2. **Remove hardcoded "Tools are disabled" message**
- This is misleading - Claude Code has its OWN tools
- Should instead document which tools are available
3. **Consider hybrid approach**
- Use Clawdbot's tool system alongside Claude Code
- Or properly document that CLI mode delegates ALL tool execution to the CLI
### Medium-Term Improvements
4. **Add context injection flag to resumeArgs**
- Claude Code supports `--append-system-prompt` even with `--resume`
- Need to restructure `buildCliArgs()` to include it
5. **Implement streaming for CLI**
- Claude Code supports `--output-format stream-json`
- Would enable real-time output and better UX
6. **Unify session transcript management**
- Current patches add transcript writes but it's fragile
- Should have consistent approach across both paths
### Architecture Decision Required
The fundamental question: **What is the CLI path supposed to be?**
**Option A: Thin Wrapper**
- CLI handles everything (tools, context, session)
- Clawdbot just routes messages and parses output
- Current implementation is close to this, but broken
**Option B: Full Integration**
- Clawdbot manages context, tools, session
- CLI is just the execution engine
- Would require significant rework
**Option C: Hybrid**
- Clawdbot injects context and handles some tools
- CLI handles others via its native capabilities
- Most flexible but most complex
---
## Files Reference
### Core Branching
- `src/auto-reply/reply/agent-runner-execution.ts` - Main execution router
### Embedded Path
- `src/agents/pi-embedded-runner/run.ts` - Orchestrator
- `src/agents/pi-embedded-runner/run/attempt.ts` - Actual execution
- `src/agents/pi-tools.ts` - Tool creation
### CLI Path
- `src/agents/cli-runner.ts` - Main CLI executor
- `src/agents/cli-runner/helpers.ts` - Arg building, parsing
- `src/agents/cli-backends.ts` - Backend configs
### Shared
- `src/agents/system-prompt.ts` - Core prompt building
- `src/agents/bootstrap-files.ts` - Context file loading
- `src/agents/workspace.ts` - Workspace scanning
---
## Appendix: Current Backend Config
```typescript
// src/agents/cli-backends.ts
const DEFAULT_CLAUDE_BACKEND: CliBackendConfig = {
command: "claude",
args: ["-p", "--output-format", "json", "--dangerously-skip-permissions"],
resumeArgs: [
"-p",
"--output-format", "json",
"--dangerously-skip-permissions",
"--resume", "{sessionId}",
],
output: "json",
input: "arg",
modelArg: "--model",
sessionArg: "--session-id",
sessionMode: "always",
systemPromptArg: "--append-system-prompt",
systemPromptMode: "append",
systemPromptWhen: "first", // <-- THE PROBLEM
clearEnv: ["ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY_OLD"],
serialize: true,
};
```
---
## Token Tracking & Usage Parsing
### Issue: Incorrect Token Display (CLI Path)
**Symptom:** Token display showing `2.1m/200k (999%)` instead of actual context usage (~85k).
### Root Cause Analysis
#### 1. CLI `toUsage()` Missing `cache_creation_input_tokens`
**Location:** `src/agents/cli-runner/helpers.ts:229-240`
```typescript
function toUsage(raw: Record<string, unknown>): CliUsage | undefined {
const pick = (key: string) =>
typeof raw[key] === "number" && raw[key] > 0 ? (raw[key] as number) : undefined;
const input = pick("input_tokens") ?? pick("inputTokens");
const output = pick("output_tokens") ?? pick("outputTokens");
const cacheRead =
pick("cache_read_input_tokens") ?? pick("cached_input_tokens") ?? pick("cacheRead");
const cacheWrite = pick("cache_write_input_tokens") ?? pick("cacheWrite"); // ❌ MISSING cache_creation_input_tokens
const total = pick("total_tokens") ?? pick("total");
if (!input && !output && !cacheRead && !cacheWrite && !total) return undefined;
return { input, output, cacheRead, cacheWrite, total };
}
```
**Problem:** Claude Code returns `cache_creation_input_tokens`, but `toUsage()` only checks for `cache_write_input_tokens`. This means `cacheWrite` is always `undefined` for CLI calls.
**Compare to `normalizeUsage()` in `src/agents/usage.ts:59-62` (used by embedded path):**
```typescript
const cacheWrite = asFiniteNumber(
raw.cacheWrite ?? raw.cache_write ?? raw.cache_creation_input_tokens, // ✅ Handles all variants
);
```
#### 2. Anthropic Cache Token Semantics
Claude's API returns these usage fields:
```json
{
"usage": {
"input_tokens": 3, // New uncached tokens
"cache_read_input_tokens": 85000, // Tokens loaded from cache
"cache_creation_input_tokens": 500, // Tokens newly cached THIS request
"output_tokens": 100
}
}
```
**Key insight:** `cache_creation_input_tokens` is a **subset** of the input that was cached, NOT additional tokens.
**Correct context window usage:** `input_tokens + cache_read_input_tokens`
**What clawdbot calculates:** `input + cacheRead + cacheWrite` (double-counting if cacheWrite parsed)
#### 3. Token Persistence Flow
**File:** `src/auto-reply/reply/session-usage.ts:30-38`
```typescript
update: async (entry) => {
const input = params.usage?.input ?? 0;
const output = params.usage?.output ?? 0;
const promptTokens =
input + (params.usage?.cacheRead ?? 0) + (params.usage?.cacheWrite ?? 0);
const patch: Partial<SessionEntry> = {
inputTokens: input,
outputTokens: output,
totalTokens: promptTokens > 0 ? promptTokens : (params.usage?.total ?? input),
// ...
};
```
This **overwrites** `totalTokens` (doesn't accumulate), but the formula adds `cacheWrite` which would be incorrect for context display.
#### 4. Status Display Reading
**File:** `src/auto-reply/status.ts:297-315`
```typescript
let totalTokens = entry?.totalTokens ?? (entry?.inputTokens ?? 0) + (entry?.outputTokens ?? 0);
if (args.includeTranscriptUsage) {
const logUsage = readUsageFromSessionLog(entry?.sessionId, entry);
if (logUsage) {
const candidate = logUsage.promptTokens || logUsage.total;
if (!totalTokens || totalTokens === 0 || candidate > totalTokens) {
totalTokens = candidate; // Override if transcript has higher value
}
}
}
```
`readUsageFromSessionLog()` reads the clawdbot session transcript and can override the stored `totalTokens` if the transcript value is higher.
### Observed Token Values
**From sessions.json:**
```json
{
"inputTokens": 32,
"outputTokens": 5574,
"totalTokens": 2080469, // ❌ Wildly incorrect
"contextTokens": 200000 // ✅ Correct (model context window)
}
```
**From Claude Code session (typical recent call):**
```json
{
"input_tokens": 1,
"cache_creation_input_tokens": 500,
"cache_read_input_tokens": 85000,
"output_tokens": 100
}
```
**Expected totalTokens:** ~85,001 (input + cacheRead)
**Actual totalTokens:** 2,080,469 (source unclear - appears to be accumulation)
### Cumulative Sums from Claude Code Session
Analysis of all 418 API calls in current session:
- Total `input_tokens`: 492
- Total `cache_creation_input_tokens`: 2,801,000
- Total `cache_read_input_tokens`: 22,240,078
- Cumulative `cache_read` at entry 68: ~2,051,411 (close to displayed 2,080,469)
This suggests the 2.08M value may be coming from cumulative `cache_read_input_tokens` being summed somewhere rather than using the latest value.
### Comparison: Embedded vs CLI Token Handling
| Aspect | Embedded Path | CLI Path |
|--------|--------------|----------|
| Usage Parsing | `normalizeUsage()` ✅ | `toUsage()` ❌ |
| Handles `cache_creation_input_tokens` | ✅ Yes | ❌ No |
| Usage Source | SDK response | Parsed CLI JSON |
| Accumulation Risk | Low (SDK manages) | Higher (manual parsing) |
### Recommended Fixes
#### Fix 1: Add `cache_creation_input_tokens` to CLI `toUsage()`
```typescript
// In src/agents/cli-runner/helpers.ts:236
const cacheWrite =
pick("cache_write_input_tokens") ??
pick("cache_creation_input_tokens") ?? // ADD THIS
pick("cacheWrite");
```
#### Fix 2: Correct Context Display Formula
For **context window display**, use:
```typescript
const contextUsed = input + cacheRead; // NOT + cacheWrite
```
For **billing/total tokens**:
```typescript
const billedTokens = input + cacheRead + cacheWrite; // All tokens processed
```
#### Fix 3: Investigate Accumulation Source
The 2.08M value's exact source is still unclear. Possibilities:
- Bug in `readUsageFromSessionLog()` accumulating across transcript entries
- Issue with how clawdbot transcript stores CLI usage
- Race condition in `updateSessionStoreEntry()`
---
## Session Compaction Coordination
### Current Behavior
**Embedded Path:**
- Clawdbot's `SessionManager` handles compaction
- Pre-compaction memory flush supported
- Compaction settings configurable (`reserveTokens`, `keepRecentTokens`)
- See `docs/reference/session-management-compaction.md`
**CLI Path:**
- Claude Code handles its own compaction internally
- Clawdbot has **no visibility** into when CC compacts
- Memory flush explicitly skipped: "The flush runs only for embedded Pi sessions (CLI backends skip it)"
### Observed Compaction Event
From Claude Code session `e385fe53-2e2e-4533-ab27-3ee6bf708fae`:
```json
{
"type": "system",
"subtype": "compact_boundary",
"content": "Conversation compacted",
"compactMetadata": {
"trigger": "auto",
"preTokens": 172423
},
"timestamp": "2026-01-26T09:55:06.386Z"
}
```
CC auto-compacted when context reached ~172k tokens (of 200k limit).
### Problem: Context Loss on CC Compaction
When Claude Code compacts:
1. System prompt (with TOOLS.md, AGENTS.md, etc.) may be summarized/compressed
2. Clawdbot is unaware this happened
3. Token counts in clawdbot's session store become stale
4. Agent loses detailed workspace context
### Potential Solutions
**Option A: Let CC manage compaction entirely**
- Current behavior, but track CC's compaction events
- Sync token counts after CC reports compaction
- Accept that clawdbot context gets compressed
**Option B: Disable CC compaction, clawdbot manages**
- Pass flag to disable CC auto-compact (if available)
- Clawdbot monitors context usage
- When threshold reached, clawdbot:
1. Runs memory flush
2. Starts NEW Claude Code session
3. Injects fresh system prompt with full context
- Continue using `--resume` until next compaction trigger
**Option C: Hybrid coordination**
- Monitor CC session for compaction events
- After CC compacts, re-inject critical context via `--append-system-prompt`
- Requires detecting `compact_boundary` events in CC output
### Claude Code Compaction Triggers
Based on observed behavior and code analysis:
- **Auto-compact threshold:** `contextTokens > contextWindow - reserveTokens`
- **Default reserveTokens:** ~16k-20k tokens headroom
- **Trigger point for 200k context:** ~180k-184k tokens
- **Observed trigger:** 172,423 tokens (with some variance)
---
*Document generated: 2026-01-26*
*Analysis based on clawdbot version 2026.1.24-3*
*Updated: 2026-01-26 with token tracking and compaction analysis*