feat: add session persistence to CLI
- Add save-only endpoint (POST /conversations/{id}/save) for persisting
messages without triggering agent execution
- Add sessions command to list previous conversation sessions
- Add --resume flag to chat command for resuming sessions by ID
- Buffer streamed responses and save after completion
- Update AGENTS.md with session commands and remove outdated limitation
- Add 3 new tests for save endpoint (208 total)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+12
-10
@@ -2,9 +2,9 @@
|
||||
|
||||
> Tracking progress towards Claude Code-like functionality
|
||||
|
||||
## Current Status: ~80% Complete
|
||||
## Current Status: ~85% Complete
|
||||
|
||||
Last updated: 2026-01-11
|
||||
Last updated: 2026-01-14
|
||||
|
||||
---
|
||||
|
||||
@@ -122,11 +122,13 @@ Last updated: 2026-01-11
|
||||
|---------|----------|-------------|------------|
|
||||
| **Web search summarizer** | Tools | Agent to extract core content from web pages (remove nav, footers, etc.) and preserve relevant links for nested fetching | Medium |
|
||||
| **Tool result caching** | Infrastructure | Cache file reads for performance | Low |
|
||||
| **Session persistence** | CLI | Save/resume conversations | Medium |
|
||||
| ~~**Session persistence**~~ | CLI | ✅ Save/resume conversations via `sessions` and `chat --resume` | Medium |
|
||||
| **Todo tracking** | CLI | Built-in task list (`/todo`) | Medium |
|
||||
| **Git integration** | CLI | Auto-commit, branch management | Medium |
|
||||
| **Agent handoff** | Orchestration | Explore → Plan → Task workflow | High |
|
||||
| ~~**Agent handoff**~~ | Orchestration | ✅ Task agent is main agent, spawns Explore/Plan as needed (Claude Code pattern) | High |
|
||||
| ~~**Retry logic**~~ | Infrastructure | ✅ Auto-retry with exponential backoff | Low |
|
||||
| ~~**Permission modes**~~ | CLI | ✅ default/plan/auto_accept modes controlling tool access | Medium |
|
||||
| ~~**CLI shell features**~~ | CLI | ✅ prompt_toolkit: history, tab completion, auto-suggest | Low |
|
||||
|
||||
### Low Priority
|
||||
|
||||
@@ -149,14 +151,14 @@ Last updated: 2026-01-11
|
||||
| API tests | 11 | 11 | ✅ |
|
||||
| Plan agent tests | 15 | 15 | ✅ |
|
||||
| Task agent tests | 15 | 15 | ✅ |
|
||||
| Conversation tests | 19 | 19 | ✅ |
|
||||
| Conversation tests | 22 | 22 | ✅ |
|
||||
| Token tests | 6 | 6 | ✅ |
|
||||
| Retry tests | 29 | 29 | ✅ |
|
||||
| Security tests | 14 | 14 | ✅ |
|
||||
| Integration tests | 10 | 10 | ✅ Agent + real LLM |
|
||||
| E2E tests | 12 | 12 | ✅ Full API workflow |
|
||||
|
||||
**Total: 205 tests passing**
|
||||
**Total: 208 tests passing**
|
||||
|
||||
**Test breakdown:**
|
||||
- Read/Glob/Grep tools: 17 tests
|
||||
@@ -167,7 +169,7 @@ Last updated: 2026-01-11
|
||||
- API endpoints: 11 tests
|
||||
- Plan agent: 15 tests
|
||||
- Task agent: 15 tests
|
||||
- Conversations: 19 tests
|
||||
- Conversations: 22 tests
|
||||
- Tokens: 6 tests
|
||||
- Retry: 29 tests
|
||||
- Security: 14 tests
|
||||
@@ -224,9 +226,9 @@ cd webber-api && ./wakeup.sh
|
||||
|
||||
# CLI commands (from webber-cli/)
|
||||
.venv/bin/webber-cli status # Check API connection
|
||||
.venv/bin/webber-cli explore "find tests" # One-shot exploration
|
||||
.venv/bin/webber-cli explore "query" --no-stream # Batch mode
|
||||
.venv/bin/webber-cli chat # Interactive mode
|
||||
.venv/bin/webber-cli chat # Interactive mode (Task agent, full tools)
|
||||
.venv/bin/webber-cli chat --mode plan # Read-only mode (safe exploration)
|
||||
.venv/bin/webber-cli chat --mode auto_accept # No approval prompts (use with caution)
|
||||
|
||||
# API endpoints
|
||||
curl http://localhost:8095/health
|
||||
|
||||
@@ -15,6 +15,8 @@ from src.domains.conversations.schemas import (
|
||||
ConversationResponse,
|
||||
CreateConversationRequest,
|
||||
MessageResponse,
|
||||
SaveMessagesRequest,
|
||||
SaveMessagesResponse,
|
||||
)
|
||||
from src.domains.conversations.service import ConversationService
|
||||
from src.shared.auth import require_auth
|
||||
@@ -187,3 +189,51 @@ async def add_message(
|
||||
total_tokens=conversation.total_tokens if conversation else 0,
|
||||
summarized=summarized,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{conversation_id}/save", response_model=SaveMessagesResponse)
|
||||
@logged()
|
||||
async def save_messages(
|
||||
conversation_id: UUID,
|
||||
request: SaveMessagesRequest,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
user=Depends(require_auth),
|
||||
) -> SaveMessagesResponse:
|
||||
"""
|
||||
Save a user/assistant message pair without triggering agent execution.
|
||||
|
||||
Used by CLI when streaming responses separately via /agents/stream.
|
||||
This allows persisting the exchange after streaming completes.
|
||||
"""
|
||||
service = ConversationService(session)
|
||||
|
||||
# Verify conversation exists and user owns it
|
||||
conversation = await service.get(conversation_id)
|
||||
if not conversation:
|
||||
raise HTTPException(status_code=404, detail="Conversation not found")
|
||||
|
||||
if conversation.user_id != user.id:
|
||||
raise HTTPException(status_code=403, detail="Not authorized")
|
||||
|
||||
# Save user message
|
||||
user_message = await service.add_message(
|
||||
conversation_id=conversation_id,
|
||||
role="user",
|
||||
content=request.user_content,
|
||||
)
|
||||
|
||||
# Save assistant message
|
||||
assistant_message = await service.add_message(
|
||||
conversation_id=conversation_id,
|
||||
role="assistant",
|
||||
content=request.assistant_content,
|
||||
)
|
||||
|
||||
# Get updated conversation for total tokens
|
||||
conversation = await service.get(conversation_id)
|
||||
|
||||
return SaveMessagesResponse(
|
||||
user_message=MessageResponse.model_validate(user_message),
|
||||
assistant_message=MessageResponse.model_validate(assistant_message),
|
||||
total_tokens=conversation.total_tokens if conversation else 0,
|
||||
)
|
||||
|
||||
@@ -21,6 +21,15 @@ class AddMessageRequest(BaseModel):
|
||||
content: str = Field(..., min_length=1, description="Message content")
|
||||
|
||||
|
||||
class SaveMessagesRequest(BaseModel):
|
||||
"""Request to save a message pair without triggering agent execution.
|
||||
|
||||
Used by CLI when streaming responses separately via /agents/stream.
|
||||
"""
|
||||
user_content: str = Field(..., min_length=1, description="User message content")
|
||||
assistant_content: str = Field(..., min_length=1, description="Assistant response content")
|
||||
|
||||
|
||||
# === Response Schemas ===
|
||||
|
||||
class MessageResponse(BaseModel):
|
||||
@@ -77,3 +86,10 @@ class AddMessageResponse(BaseModel):
|
||||
default=False,
|
||||
description="Whether context was summarized due to token limit"
|
||||
)
|
||||
|
||||
|
||||
class SaveMessagesResponse(BaseModel):
|
||||
"""Response after saving messages (no agent execution)."""
|
||||
user_message: MessageResponse
|
||||
assistant_message: MessageResponse
|
||||
total_tokens: int
|
||||
|
||||
@@ -297,3 +297,87 @@ class TestSummarization:
|
||||
|
||||
assert "[Previous Summary]" in formatted
|
||||
assert "Previous context summary" in formatted
|
||||
|
||||
|
||||
class TestSaveMessagesAPI:
|
||||
"""Tests for the save messages endpoint (no agent execution)."""
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_save_messages(self, auth_client):
|
||||
"""Test saving a message pair without triggering agent."""
|
||||
# First create a conversation
|
||||
response = await auth_client.post(
|
||||
"/conversations/",
|
||||
json={"agent_type": "task", "working_dir": "."}
|
||||
)
|
||||
assert response.status_code == 201
|
||||
conv_id = response.json()["id"]
|
||||
|
||||
# Save a message pair
|
||||
response = await auth_client.post(
|
||||
f"/conversations/{conv_id}/save",
|
||||
json={
|
||||
"user_content": "Find all Python files",
|
||||
"assistant_content": "I found 5 Python files in the project.",
|
||||
}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
assert data["user_message"]["role"] == "user"
|
||||
assert data["user_message"]["content"] == "Find all Python files"
|
||||
assert data["assistant_message"]["role"] == "assistant"
|
||||
assert data["assistant_message"]["content"] == "I found 5 Python files in the project."
|
||||
assert data["total_tokens"] > 0
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_save_messages_not_found(self, auth_client):
|
||||
"""Test saving messages to non-existent conversation."""
|
||||
fake_id = uuid4()
|
||||
response = await auth_client.post(
|
||||
f"/conversations/{fake_id}/save",
|
||||
json={
|
||||
"user_content": "Test",
|
||||
"assistant_content": "Response",
|
||||
}
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_save_messages_updates_token_count(self, auth_client):
|
||||
"""Test that saving messages updates the conversation token count."""
|
||||
# Create conversation
|
||||
response = await auth_client.post(
|
||||
"/conversations/",
|
||||
json={"agent_type": "explore", "working_dir": "."}
|
||||
)
|
||||
conv_id = response.json()["id"]
|
||||
assert response.json()["total_tokens"] == 0
|
||||
|
||||
# Save first message pair
|
||||
response = await auth_client.post(
|
||||
f"/conversations/{conv_id}/save",
|
||||
json={
|
||||
"user_content": "Hello",
|
||||
"assistant_content": "Hi there!",
|
||||
}
|
||||
)
|
||||
first_tokens = response.json()["total_tokens"]
|
||||
assert first_tokens > 0
|
||||
|
||||
# Save second message pair
|
||||
response = await auth_client.post(
|
||||
f"/conversations/{conv_id}/save",
|
||||
json={
|
||||
"user_content": "How are you?",
|
||||
"assistant_content": "I'm doing well, thank you for asking!",
|
||||
}
|
||||
)
|
||||
second_tokens = response.json()["total_tokens"]
|
||||
assert second_tokens > first_tokens
|
||||
|
||||
# Verify via get endpoint
|
||||
response = await auth_client.get(f"/conversations/{conv_id}")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["total_tokens"] == second_tokens
|
||||
assert len(response.json()["messages"]) == 4
|
||||
|
||||
Reference in New Issue
Block a user