566 lines
18 KiB
Markdown
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*
|