94 lines
8.6 KiB
Markdown
94 lines
8.6 KiB
Markdown
# Agent conversation entry normalization migration
|
|
## Context
|
|
Linear: https://linear.app/warpdotdev/issue/APP-4382/normalize-agent-conversation-entries-for-list-and-navigation-surfaces
|
|
`AgentConversationsModel` currently exposes `ConversationOrTask`, a wrapper over either `AmbientAgentTask` or `ConversationMetadata`, to list, details, filtering, and navigation surfaces in `app/src/ai/agent_conversations_model.rs (399-813)`. The wrapper centralizes several display helpers, but still encodes important behavior as “task vs conversation” decisions. The most problematic example is `link_preference()` and `get_open_action()` in `app/src/ai/agent_conversations_model.rs (624-813)`: task rows choose between session, conversation transcript, or no action from cached task fields, while local conversation rows restore/navigate from `ConversationNavigationData`.
|
|
The model also intentionally hides local conversation rows when a task appears to represent the same logical run in `app/src/ai/agent_conversations_model.rs (1382-1497)`. That shadowing is useful for preserving cloud-run affordances, but it means the visible task row can discard better local-conversation navigation data. A task row with a stale or incomplete `session_id`, `session_link`, `conversation_id`, or `is_sandbox_running` can therefore be unopenable even when the hidden conversation representation is restorable.
|
|
The underlying sources are already separate. `BlocklistAIHistoryModel` owns loaded `AIConversation` contents in `conversations_by_id` and lightweight historical/cloud metadata in `all_conversations_metadata` in `app/src/ai/blocklist/history_model.rs (49-214)`. Cloud metadata can exist without a loaded conversation via `AIConversationMetadata` and `merge_cloud_conversation_metadata` in `app/src/ai/blocklist/history_model/conversation_loader.rs (334-453)`, while `load_conversation_data` loads content only on demand in `app/src/ai/blocklist/history_model/conversation_loader.rs (145-236)`. `ActiveAgentViewsModel` separately tracks which local conversations and ambient sessions are currently open in `app/src/ai/active_agent_views_model.rs (45-531)`.
|
|
This migration introduces a projection-layer `AgentConversationEntry` for list/navigation/detail surfaces. It should not replace `AIConversation`, `BlocklistAIHistoryModel`, or transcript content APIs. Its job is to merge provenance and lightweight display data into one logical row so UI surfaces no longer choose between task and conversation representations.
|
|
## Proposed changes
|
|
### Target model boundary
|
|
Add a normalized entry API to `AgentConversationsModel` while keeping raw task and conversation storage as implementation details:
|
|
```rust
|
|
pub struct AgentConversationEntry {
|
|
pub id: AgentConversationEntryId,
|
|
pub identity: AgentConversationIdentity,
|
|
pub display: AgentConversationDisplayData,
|
|
pub provenance: AgentConversationProvenance,
|
|
pub backing: AgentConversationBackingData,
|
|
pub capabilities: AgentConversationCapabilities,
|
|
}
|
|
```
|
|
`AgentConversationEntry` is a projection. It carries owned IDs and display snapshots, not borrowed references to `AmbientAgentTask` or `AIConversation`. Callers that need transcript contents still go through `BlocklistAIHistoryModel` and loader APIs.
|
|
Use local/client identities for listed entries:
|
|
```rust
|
|
pub enum AgentConversationEntryId {
|
|
AmbientRun(AmbientAgentTaskId),
|
|
Conversation(AIConversationId),
|
|
}
|
|
```
|
|
Do not use `ServerConversationToken` as a normal list identity. Server tokens are global fetch/navigation handles; cloud-only metadata rows already receive a local `AIConversationId` when metadata is merged.
|
|
The entry identity should preserve every known reference:
|
|
```rust
|
|
pub struct AgentConversationIdentity {
|
|
pub local_conversation_id: Option<AIConversationId>,
|
|
pub ambient_run_id: Option<AmbientAgentTaskId>,
|
|
pub server_conversation_token: Option<ServerConversationToken>,
|
|
pub parent_conversation_id: Option<AIConversationId>,
|
|
pub parent_run_id: Option<AmbientAgentTaskId>,
|
|
}
|
|
```
|
|
Keep provenance semantic and move loadability into backing:
|
|
```rust
|
|
pub enum AgentConversationProvenance {
|
|
LocalInteractive,
|
|
AmbientRun,
|
|
CloudSyncedConversation,
|
|
}
|
|
|
|
pub struct AgentConversationBackingData {
|
|
pub has_loaded_conversation: bool,
|
|
pub has_local_persisted_data: bool,
|
|
pub has_cloud_data: bool,
|
|
pub has_ambient_run: bool,
|
|
}
|
|
```
|
|
`AgentConversationDisplayData` should centralize list/detail fields currently read from `ConversationOrTask`: title, initial query, created/updated timestamps, `AgentRunDisplayStatus`, creator, working directory, source, environment, harness, request usage, run time, and artifacts. The display status should continue to use the derived `AgentRunDisplayStatus` algorithm instead of raw `AmbientAgentTaskState` or raw `ConversationStatus`.
|
|
`AgentConversationCapabilities` should expose eligibility booleans for UI affordances such as open, copy link, share, delete, fork locally, continue locally, and cancel. It must not store the final `WorkspaceAction`. Open actions should be resolved dynamically from entry identity at click time.
|
|
### Increment sequence
|
|
Increment 1, `INCREMENT-1-entry-schema-and-builder.md`, adds the entry schema, merge builder, and tests without migrating UI. It introduces `AgentConversationsModel::get_entries(...)` or equivalent behind existing code paths.
|
|
Increment 2, `INCREMENT-2-navigation-and-conversation-list.md`, adds a dynamic navigation resolver and migrates the conversation list to normalized entries. This is the first user-visible behavior fix for task rows that shadow restorable conversations.
|
|
Increment 3, `INCREMENT-3-agent-management-and-details.md`, migrates Agent Management cards, filters, action buttons, and details panel data to normalized entries.
|
|
Increment 4, `INCREMENT-4-cleanup-and-hardening.md`, removes or narrows `ConversationOrTask`, deletes `link_preference()` as a source of truth, and adds regression coverage for previously inconsistent entrypoints.
|
|
## End-to-end flow
|
|
1. `AgentConversationsModel` keeps its existing task and conversation caches.
|
|
2. The entry builder indexes tasks by stable run id, conversations by local id, and metadata by server token.
|
|
3. For each ambient run, the builder creates one `AmbientRun` entry and attaches matching local conversation/server-token data when available.
|
|
4. For each local/cloud metadata conversation not already attached to a run, the builder creates one `Conversation` entry.
|
|
5. List/detail surfaces render from `AgentConversationEntry`.
|
|
6. On click, the navigation resolver re-reads current task, history, active-view, and workspace state before producing a `WorkspaceAction`.
|
|
## Testing and validation
|
|
The migration should be implemented as stacked PRs. Each increment spec lists its focused test coverage. Across the full migration, add tests for:
|
|
- local conversation only
|
|
- cloud metadata only with `has_local_data = false`
|
|
- task only with active joinable session
|
|
- task only with terminal state and server token
|
|
- task plus local conversation by run id
|
|
- task plus local conversation by server token
|
|
- task with stale in-progress state but terminal local conversation status
|
|
- task with `session_link` but no parseable `session_id`
|
|
- existing open local conversation focus
|
|
- existing open ambient session focus
|
|
- copied link and open action using the same resolved identity data
|
|
Before opening PRs, run the focused `agent_conversations_model` tests for each increment and a targeted app check. Follow repo PR workflow before review.
|
|
## Risks and mitigations
|
|
### Merge identity regressions
|
|
Wrong merge rules can duplicate rows or hide rows. Mitigate by writing the merge tests before migrating UI and keeping merge precedence explicit: ambient run owns the UI identity when present, but local conversation/server token refs remain attached.
|
|
### Recreating stale navigation in a new type
|
|
If entries cache `WorkspaceAction`, they will reproduce the current bug. Store identities and capabilities only; resolve final actions at click time.
|
|
### Overreaching into transcript content
|
|
`AgentConversationEntry` should not expose exchanges, block data, or content mutation methods. Keep transcript operations in `BlocklistAIHistoryModel` and loader paths.
|
|
### Incomplete event invalidation
|
|
Entries depend on task updates, history events, metadata updates, and active view changes. Initially prefer rebuilding entries on any relevant `AgentConversationsModelEvent` instead of maintaining fine-grained derived caches.
|
|
## Parallelization
|
|
Increment 1 should be implemented first and mostly sequentially because later increments depend on the entry schema. After that, Increment 2 and Increment 3 can be implemented on separate stacked branches if the entry API is stable. Increment 4 should wait until both UI migrations land.
|