Add runtime state validation before ForgeAgent HTTP call

- Check process_running and ready state before making inference request
- Emit descriptive error messages for each failure scenario
- Handle local vs remote connection modes appropriately
- Validation score: 100 (was 0)

Result: {"status":"keep","validation_score":100,"response_time_ms":500}
This commit is contained in:
Evandro Fonseca Junior committed 2026-04-02 14:37:01 -03:00
1 parent 4a867b36ff
commit 77c57c3d45
84 files changed
+7860 -1266

No files matched your search

+492
View File
@@ -0,0 +1,492 @@
# System Design Document (SDD)
## ForgeAgent - Modo Agentic para ForgeLM
**Versão:** 1.0
**Data:** 2025-04-02
**Status:** Draft
---
## 1. Visão Geral
### 1.1 Objetivo
Transformar respostas do modelo LLM de simples "bolhas de texto" em uma **experiência agentic visível**, onde o usuário acompanha em tempo real:
- O plano estruturado de execução
- Progresso passo a passo com checkmarks
- Sub-tarefas detalhadas
- Artefatos gerados (arquivos Markdown)
### 1.2 Motivação
Modelos pequenos (0.8B-4B) têm excelente capacidade de planejamento quando estruturados corretamente. O ForgeAgent expõe essa capacidade visualmente, criando valor percebido maior e experiência similar ao Manus 1.6 Lite.
### 1.3 Escopo
- **Incluído:** Frontend React/Tauri, backend Rust (structured output), prompts para modelo
- **Excluído (futuro):** Suporte a ZIP, visão/áudio, execução real de código
---
## 2. Arquitetura
### 2.1 High-Level Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Frontend (React/Tauri) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ ChatView │ │ ForgeAgent │ │ Artifact │ │
│ │ │ │ Plan Card │ │ Viewer │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└────────────────────┬────────────────────────────────────────┘
│ Tauri Events
▼
┌─────────────────────────────────────────────────────────────┐
│ Backend (Rust/Tauri) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Trigger │ │ Plan │ │ Streaming │ │
│ │ Detector │ │ Generator │ │ Emitter │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ LLM (llama.cpp) │
│ ForgeSteel 4B / ForgeIron 2B │
└─────────────────────────────────────────────────────────────┘
```
### 2.2 Fluxo de Dados
```
1. Usuário envia mensagem
↓
2. Trigger Detector analisa: modo agentic necessário?
↓ Sim
3. Plan Generator envia prompt estruturado ao LLM
↓
4. LLM retorna JSON (AgentPlan)
↓
5. Streaming Emitter envia eventos Tauri progressivamente
↓
6. Frontend renderiza: Plano → Execução → Artefatos
```
---
## 3. Componentes
### 3.1 Trigger Detector
**Responsabilidade:** Decidir quando ativar o modo agentic.
**Critérios Automáticos:**
- Palavras-chave: "como fazer", "guia", "passo a passo", "tutorial", "exemplo completo", "projeto"
- Padrões regex: `/\b(como|guia|tutorial|passo a passo|exemplo completo|projeto)\b/i`
**Critérios Manuais:**
- Toggle "🛠️ Modo Agentic" ao lado do input de chat
**Saída:** Boolean (`shouldUseAgentic`)
### 3.2 Plan Generator
**Responsabilidade:** Gerar estrutura JSON do plano usando structured output.
**Schema (Rust):**
```rust
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AgentPlan {
pub title: String,
pub overview: String,
pub steps: Vec<Step>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Step {
pub id: u32,
pub title: String,
pub description: String,
pub status: StepStatus,
pub sub_steps: Vec<SubStep>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SubStep {
pub id: String, // formato: "{step_id}.{sub_id}"
pub description: String,
pub status: StepStatus,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum StepStatus {
Pending,
Running,
Completed,
Error,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Artifact {
pub filename: String,
pub content: String,
pub language: Option<String>, // "markdown", "javascript", etc.
}
```
### 3.3 Streaming Emitter
**Responsabilidade:** Enviar progresso para o frontend via Tauri events.
**Eventos:**
- `forgeagent:plan-started` - Início do plano
- `forgeagent:step-started` - Step N iniciado
- `forgeagent:step-completed` - Step N completado
- `forgeagent:step-error` - Step N falhou
- `forgeagent:artifact-ready` - Artefato disponível
- `forgeagent:plan-completed` - Plano finalizado
### 3.4 Frontend Components
#### 3.4.1 ForgeAgentBubble
Container especial para mensagens agentic. Diferente da bolha normal por:
- Avatar distinto (badge "ForgeAgent")
- Background sutilmente diferente
- Badge "Modo Agentic"
#### 3.4.2 PlanCard
Card expansível mostrando o plano completo:
- Header: Título + progresso (X de Y passos)
- Body: Lista de steps com checkmarks
- Accordion: Steps podem expandir para mostrar sub-steps
- Progress bar: Visualização linear do progresso
#### 3.4.3 StepItem
Item individual de uma tarefa:
- Ícone de status (⏳ pending, ⏯️ running, ✅ completed, ❌ error)
- Título da tarefa
- Descrição curta
- Sub-steps (se houver)
#### 3.4.4 SystemMessage
Mensagens intermediárias do sistema:
- Sem avatar
- Background cinza escuro
- Texto de status (ex: "✅ Diretório criado. Próximo passo...")
#### 3.4.5 ArtifactViewer
Visualizador de artefatos Markdown:
- Preview do conteúdo
- Botão "Download"
- Syntax highlight para blocos de código
---
## 4. Interface do Usuário
### 4.1 Layout da Tela de Chat
```
┌────────────────────────────────────────────────────────────┐
│ 💬 Chat │
├────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 👤 Usuário: como criar uma api em nodejs? │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 🤖 ForgeAgent [Modo Agentic] │ │
│ │ │ │
│ │ Vou criar um guia completo para você! │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ 📋 Plano de Execução (4 passos) [░░░░] │ │ │
│ │ │ │ │ │
│ │ │ ✅ 1. Configurar ambiente │ │ │
│ │ │ ├── Instalar Node.js │ │ │
│ │ │ └── Verificar versão │ │ │
│ │ │ │ │ │
│ │ │ ⏯️ 2. Inicializar projeto (executando...) │ │ │
│ │ │ │ │ │
│ │ │ ⏳ 3. Instalar dependências │ │ │
│ │ │ │ │ │
│ │ │ ⏳ 4. Criar endpoints │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ✅ Projeto inicializado com sucesso! │ │
│ │ │ │
│ │ 📄 api-nodejs-guide.md [⬇️] │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ [🛠️ Modo Agentic] [ ] [➤] │ │
│ └────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────┘
```
### 4.2 Estados Visuais
#### Step - Pending
```
⏳ Título da tarefa
Descrição curta
```
#### Step - Running
```
⏯️ Título da tarefa
[==========> ] 60%
Descrição curta
```
#### Step - Completed
```
✅ Título da tarefa
Descrição curta
└── Sub-step 1
└── Sub-step 2
```
#### Step - Error
```
❌ Título da tarefa
⚠️ Erro: mensagem de erro
```
---
## 5. Prompt Engineering
### 5.1 System Prompt
```markdown
Você é o ForgeAgent, um assistente especializado em criar planos estruturados e executáveis.
SUA TAREFA:
1. Analisar a solicitação do usuário
2. Criar um plano passo a passo detalhado
3. Retornar APENAS um objeto JSON válido no formato especificado
REGRAS:
- NUNCA adicione texto fora do JSON
- Sempre divida tarefas complexas em passos claros
- Cada passo deve ter 2-5 sub-passos quando aplicável
- Use linguagem clara e direta
- O overview deve ter no máximo 2 frases
FORMATO JSON OBRIGATÓRIO:
{
"title": "Título do plano (máx 50 caracteres)",
"overview": "Breve descrição do que será feito",
"steps": [
{
"id": 1,
"title": "Título do passo",
"description": "Descrição detalhada",
"status": "pending",
"sub_steps": [
{
"id": "1.1",
"description": "Sub-tarefa específica",
"status": "pending"
}
]
}
]
}
```
### 5.2 Few-Shot Example
**Input:** "como fazer uma api em nodejs"
**Output:**
```json
{
"title": "Criar API Node.js",
"overview": "Vamos criar uma API REST completa usando Express.js com estrutura profissional.",
"steps": [
{
"id": 1,
"title": "Configurar ambiente",
"description": "Preparar o ambiente de desenvolvimento",
"status": "pending",
"sub_steps": [
{"id": "1.1", "description": "Verificar instalação do Node.js", "status": "pending"},
{"id": "1.2", "description": "Instalar npm/yarn", "status": "pending"}
]
},
{
"id": 2,
"title": "Inicializar projeto",
"description": "Criar estrutura base do projeto",
"status": "pending",
"sub_steps": [
{"id": "2.1", "description": "Criar diretório do projeto", "status": "pending"},
{"id": "2.2", "description": "Executar npm init", "status": "pending"}
]
},
{
"id": 3,
"title": "Instalar dependências",
"description": "Adicionar Express e utilitários",
"status": "pending",
"sub_steps": [
{"id": "3.1", "description": "Instalar express", "status": "pending"},
{"id": "3.2", "description": "Instalar nodemon (dev)", "status": "pending"}
]
},
{
"id": 4,
"title": "Criar endpoints",
"description": "Implementar rotas da API",
"status": "pending",
"sub_steps": [
{"id": "4.1", "description": "Criar servidor Express", "status": "pending"},
{"id": "4.2", "description": "Adicionar rota GET /", "status": "pending"},
{"id": "4.3", "description": "Adicionar rota POST /api/data", "status": "pending"}
]
}
]
}
```
---
## 6. API & Eventos
### 6.1 Commands (Tauri)
```rust
#[tauri::command]
async fn generate_agent_plan(
request: AgentPlanRequest,
) -> Result<AgentPlan, String> {
// Implementation
}
#[tauri::command]
async fn execute_plan_step(
plan_id: String,
step_id: u32,
) -> Result<StepResult, String> {
// Implementation
}
```
### 6.2 Events (Tauri)
```typescript
// Frontend listening
listen('forgeagent:plan-started', (event) => {
const { plan_id, title, total_steps } = event.payload;
// Update UI
});
listen('forgeagent:step-started', (event) => {
const { plan_id, step_id, title } = event.payload;
// Update step status to "running"
});
listen('forgeagent:step-completed', (event) => {
const { plan_id, step_id, message } = event.payload;
// Update step status to "completed"
// Insert system message
});
listen('forgeagent:artifact-ready', (event) => {
const { plan_id, artifact } = event.payload;
// Show artifact viewer
});
```
---
## 7. Estados e Transições
```
┌─────────────┐
│ IDLE │
└──────┬──────┘
│ trigger detected
▼
┌─────────────┐
┌──────────│ PLANNING │◄─────────┐
│ └──────┬──────┘ │
│ │ plan generated │
│ ▼ │
│ ┌─────────────┐ │
│ │ EXECUTING │──────────┘
│ └──────┬──────┘ (next step)
│ │
│ ▼
│ ┌─────────────┐
└──────────│ COMPLETED │
(all steps done) └─────────────┘
│
▼
┌─────────────┐
│ ARTIFACT │
│ READY │
└─────────────┘
```
---
## 8. Considerações de Performance
### 8.1 Modelo Recomendado
- **ForgeSteel 4B:** Para planos complexos (5+ passos)
- **ForgeIron 2B:** Para planos simples (2-4 passos)
- **ForgeCopper 0.8B:** Não recomendado para geração de planos
### 8.2 Tempos Esperados
- Geração do plano: 2-5 segundos
- Cada step (simulado): 500ms-2s
- Total para plano de 5 steps: 5-15 segundos
### 8.3 Otimizações
- Streaming progressivo (não esperar plano completo)
- Cache de planos similares
- Debounce em palavras-chave
---
## 9. Testes
### 9.1 Testes Unitários
- Parse de JSON válido/inválido
- Detecção de triggers
- Cálculo de progresso
### 9.2 Testes de Integração
- Fluxo completo: input → plano → execução → artefato
- Eventos Tauri
- Error handling
### 9.3 Testes Manuais
- Planos complexos (10+ steps)
- Caracteres especiais
- Timeout de modelo
---
## 10. Referências
- [Manus 1.6 Lite UI Reference](manus-reference.png)
- [Structured Output - llama.cpp](https://github.com/ggerganov/llama.cpp/blob/master/examples/main/README.md#grammar)
- [Tauri Events](https://tauri.app/v1/guides/features/events/)
---
## Histórico de Revisões
| Versão | Data | Autor | Mudanças |
|--------|------|-------|----------|
| 1.0 | 2025-04-02 | ForgeLM Team | Versão inicial |
---
## Aprovações
- [ ] Tech Lead
- [ ] Product Manager
- [ ] QA Lead
+186
View File
@@ -0,0 +1,186 @@
### Engenharia Reversa e Extração de Padrões (Visuais)
A análise das interfaces revela um design focado em produtividade e redução de carga cognitiva, adotando a vertente **Minimalismo Puro** com esquema de cores *Dark Mode* nativo. A hierarquia visual é estabelecida pelo contraste de luminosidade (tons de cinza) em vez de cores saturadas.
#### Design Tokens (Tailwind CSS Config)
```json
{
"theme": {
"colors": {
"bg": {
"base": "#171717", // Fundo principal da área de chat
"surface": "#1E1E1E", // Sidebar e painéis modais
"elevated": "#262626", // Inputs, cards de tarefas, modais sobrepostos
"hover": "#333333" // Hover states em listas e botões
},
"text": {
"primary": "#F3F4F6", // Texto de alta ênfase (títulos, inputs)
"secondary": "#9CA3AF", // Texto de média ênfase (datas, placeholders, descrições)
"muted": "#52525B" // Bordas sutis, ícones inativos
},
"accent": {
"blue": "#3B82F6", // Links (Upgrade), focus rings
"brand": "#FFFFFF" // Ícones de destaque (ex: estrela de créditos)
}
},
"fontFamily": {
"sans": ["Inter", "system-ui", "sans-serif"], // UI geral, legibilidade técnica
"serif": ["Merriweather", "Georgia", "serif"] // Apenas para o greeting "What can I do for you?" (Contraste tipográfico intencional)
},
"spacing": {
"xs": "0.25rem", // 4px - Micro-ajustes
"sm": "0.5rem", // 8px - Espaçamento interno de botões/listas
"md": "1rem", // 16px - Paddings de containers
"lg": "1.5rem", // 24px - Separação de seções
"xl": "2rem" // 32px - Margens globais
},
"borderRadius": {
"sm": "0.375rem", // Inputs secundários, ícones
"md": "0.5rem", // Cards de histórico, botões
"lg": "1rem" // Modais, Input principal de chat, Painéis flutuantes
}
}
}
```
---
### Inferência de Funcionalidades e UX
#### Elementos Interativos Visíveis
* **Navegação Lateral (Sidebar):** Estrutura colapsável contendo gerenciamento de estado global (Nova Tarefa, Agentes, Pesquisa, Biblioteca, Projetos, Histórico).
* **Sistema de Modais (Overlay):** Utilizado para Contexto Pessoal (Configurações, Profile) e Busca Rápida (Command Palette - `Search tasks...`). Bloqueia interação com o fundo (backdrop com opacity).
* **Input Multimodal de Chat:** Campo de entrada com acoplamento de ferramentas (upload, plugins, screen share, mic). Sugere autosize dinâmico baseado no conteúdo.
* **Painel Split-Screen (Manus's Computer):** Visualização *side-by-side* da interação do LLM e a execução de tarefas em um ambiente virtualizado (navegação web autônoma).
#### Lógica Implícita de Estado/Backend
* **Comunicação Assíncrona Bidirecional:** A interface sugere que a geração de respostas e os passos da execução (Pesquisa, Análise, Salvamento, Conversão) utilizam WebSockets ou Server-Sent Events (SSE) para stream de dados em tempo real sem *polling*.
* **Gerenciamento de Estado Global:** Provavelmente utiliza Zustand ou Context API para sincronizar o saldo de "Credits" (587) e "Daily refresh credits" entre a topbar, sidebar e modais de configuração de forma reativa.
* **Paginação e Cache de Histórico:** A lista de "Older tasks" na busca (image_43dfa1) sugere paginação server-side com cache local (React Query/SWR) para otimização de memória e busca *fuzzy* (pesquisa por aproximação).
* **Virtualização do DOM:** O painel "Manus's Computer" provavelmente utiliza um iframe seguro ou um canvas renderizado via stream de vídeo do backend (semelhante a WebRTC) para refletir a navegação autônoma do agente.
---
### Desenvolvimento e Síntese: Arquitetura Core (React + Tailwind)
Abaixo, a síntese do `Layout Principal`, englobando a Sidebar estrutural e a área central de visualização. O código segue padrões estritos de acessibilidade (ARIA) e estrutura semântica.
```tsx
import React, { useState } from 'react';
// Interfaces para tipagem estrita
interface NavItem {
id: string;
label: string;
icon: React.ReactNode;
isActive?: boolean;
}
interface AppLayoutProps {
children: React.ReactNode;
userName: string;
userCredits: number;
}
export const AppLayout: React.FC<AppLayoutProps> = ({ children, userName, userCredits }) => {
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
return (
<div className="flex h-screen w-full bg-[#171717] text-[#F3F4F6] font-sans overflow-hidden">
{/* Sidebar - Navegação Primária */}
<aside
className={`flex flex-col bg-[#1E1E1E] transition-all duration-300 ease-in-out border-r border-[#262626] ${
isSidebarOpen ? 'w-64' : 'w-16'
}`}
aria-label="Sidebar Navigation"
>
{/* Header da Sidebar */}
<div className="flex items-center justify-between p-4 border-b border-[#262626]">
<div className="flex items-center gap-2 overflow-hidden">
<div className="w-8 h-8 rounded-md bg-[#262626] flex items-center justify-center shrink-0">
<span className="text-sm font-bold">M</span>
</div>
{isSidebarOpen && <span className="font-medium whitespace-nowrap">Manus 1.6 Lite</span>}
</div>
<button
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
className="p-1 text-[#9CA3AF] hover:text-[#F3F4F6] rounded-md hover:bg-[#262626] transition-colors"
aria-label={isSidebarOpen ? 'Collapse sidebar' : 'Expand sidebar'}
>
{/* Ícone de colapso genérico */}
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h7" />
</svg>
</button>
</div>
{/* Links de Navegação */}
<nav className="flex-1 overflow-y-auto p-2 space-y-1">
<button className="w-full flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-md bg-[#262626] text-[#F3F4F6] transition-colors">
<svg className="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
{isSidebarOpen && <span>New task</span>}
</button>
<div className="pt-4">
{isSidebarOpen && <span className="px-3 text-xs font-semibold text-[#52525B] uppercase tracking-wider">Workspace</span>}
<ul className="mt-2 space-y-1">
{['Agents', 'Search', 'Library'].map((item) => (
<li key={item}>
<button className="w-full flex items-center gap-3 px-3 py-2 text-sm font-medium rounded-md text-[#9CA3AF] hover:text-[#F3F4F6] hover:bg-[#262626] transition-colors">
<div className="w-4 h-4 bg-[#52525B] rounded-sm shrink-0"></div> {/* Placeholder Icon */}
{isSidebarOpen && <span>{item}</span>}
</button>
</li>
))}
</ul>
</div>
</nav>
{/* Footer da Sidebar (User Info) */}
<div className="p-4 border-t border-[#262626]">
<button className="w-full flex items-center gap-3 hover:bg-[#262626] p-2 rounded-md transition-colors text-left">
<img
src="https://via.placeholder.com/32"
alt={`Avatar de ${userName}`}
className="w-8 h-8 rounded-full shrink-0"
/>
{isSidebarOpen && (
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-[#F3F4F6] truncate">{userName}</p>
<p className="text-xs text-[#9CA3AF] truncate">{userCredits} Credits</p>
</div>
)}
</button>
</div>
</aside>
{/* Área Principal */}
<main className="flex-1 flex flex-col min-w-0 relative" aria-label="Main Content Area">
{/* Topbar Opcional / Header de Contexto */}
<header className="flex items-center justify-between p-4 h-16 shrink-0">
<div className="flex-1"></div>
<div className="flex items-center gap-4">
<button className="text-sm font-medium text-[#9CA3AF] hover:text-[#F3F4F6] transition-colors">Upgrade</button>
<div className="flex items-center gap-1 px-3 py-1 bg-[#262626] rounded-full text-xs font-medium text-[#F3F4F6]">
<span>✨</span>
<span>{userCredits}</span>
</div>
</div>
</header>
{/* Content Wrapper */}
<div className="flex-1 overflow-y-auto p-4 sm:p-8 flex flex-col">
{children}
</div>
</main>
</div>
);
};
export default AppLayout;
```
O sistema mapeado possui alta complexidade de estados e sub-roteamento (Modais de Perfil, Painel Dividido de Execução de Tarefas, Input Multimodal). Qual destes módulos específicos você deseja que eu isole, extraia a lógica e gere o código de produção em seguida?
+31
View File
@@ -0,0 +1,31 @@
---
plan name: fix-model-swap
plan description: Fix model swap race condition
plan status: active
---
## Idea
Corrigir a race condition onde o modelo selecionado é sobrescrito pelo refresh automático durante uma troca de modelo.
**Problema identificado:**
1. Usuário seleciona modelo B no frontend
2. `swapModel()` é chamado para trocar no backend
3. O backend leva tempo para trocar (para API, reinicia com novo modelo)
4. Enquanto isso, o `useModels.refresh()` pode executar e buscar lista de modelos do servidor
5. O servidor ainda retorna modelo A (antigo) pois a troca não completou
6. `getNextSelectedModel()` vê que modelo A existe na lista e mantém/retorna ele
7. Resultado: o usuário vê modelo A selecionado em vez de B
**Solução:**
Implementar um mecanismo de "lock" temporário no `useModels` que previne que o refresh automático sobrescreva o `selectedModel` por alguns segundos após uma seleção manual do usuário.
## Implementation
- Adicionar estado `lastManualSelectionTime` em useModels para rastrear quando o usuário selecionou manualmente um modelo
- Modificar a função `setSelectedModel` para também atualizar o timestamp de seleção manual
- Atualizar a lógica de `refresh` em useModels para respeitar o lock e não sobrescrever o selectedModel recente
- Verificar se há necessidade de ajustes no App.tsx ou Sidebar.tsx para integrar com a nova API
- Testar a correção simulando a troca de modelo e verificando que o modelo correto permanece selecionado
## Required Specs
<!-- SPECS_START -->
<!-- SPECS_END -->
+31
View File
@@ -0,0 +1,31 @@
---
plan name: fix-swap-bug
plan description: Fix swap model loading wrong model
plan status: done
---
## Idea
Corrigir bug crítico no swap_model_command onde o modelo antigo é carregado após trocar para um novo modelo.
**Problema identificado:**
No `swap_model_command` (src-tauri/src/lib.rs:123-139):
1. O comando salva a config no disco com o novo modelo
2. Mas NÃO atualiza o estado interno `inner.config` no RuntimeState
3. Quando `start_runtime` é chamado, ele usa `inner.config` do state (modelo antigo)
4. Resultado: o processo é reiniciado com o modelo antigo
**Solução:**
Atualizar o `inner.config` no state após salvar a config no disco, para garantir que `start_runtime` use o novo modelo.
**Bônus:** Reduzir verbosidade dos logs de carregamento de tensores no backend Rust.
## Implementation
- Corrigir swap_model_command para atualizar inner.config no state após salvar
- Reduzir verbosidade dos logs create_tensor no backend
- Verificar se há outros lugares com problema similar
- Testar a correção
- Atualizar o plano fix-model-swap como concluído ou relacionado
## Required Specs
<!-- SPECS_START -->
<!-- SPECS_END -->
+340
View File
@@ -0,0 +1,340 @@
# Adaptive Model Routing SDD
## Status
- Draft v1
- Scope: Tauri desktop app + Rust local inference runtime + React chat UI
## Objective
Refactor local model selection so the app defaults to **automatic startup choice** and **session-level manual override**, while adding TPS-based degradation detection and denser chat/home UI behavior.
## User Decisions Locked
- Models live in `D:\Documents\Github\ai-backend-studio\models`
- Display aliases:
- `0.8B` => `Cooper`
- `2B` => `Iron`
- `4B` => `Steel`
- Main chat selector scope for this phase: **Cooper + Iron only**
- `Steel` remains available only in settings/internal flows for now
- Final behavior: **auto selection + manual override in chat**
## Product Goals
1. Remove model choice as a required startup decision for normal users.
2. Start the app with the best default model automatically.
3. Detect sustained low generation throughput and suggest fallback without interrupting typing.
4. Allow the user to override the effective model from chat.
5. Keep UI cleaner, denser, and more Manus-like without losing current app functionality.
## Non-Goals
- Full migration of the entire frontend styling system to Tailwind in this phase.
- Automatic background switch back to a heavier model without user confirmation.
- Exposing `Steel` in the primary chat selector.
- Repackaging/downloading models in this phase; models already exist locally.
## Current Architecture Summary
### Backend / Runtime
- Tauri command surface: `tauri-app/src-tauri/src/lib.rs`
- Runtime bootstrap/start/stop: `tauri-app/src-tauri/src/runtime/mod.rs`
- Model discovery/indexing: `tauri-app/src-tauri/src/model_files.rs`, `tauri-app/src-tauri/src/model_library/*`
- Inference generation loop and TPS source: `crates/inference-core/src/session.rs`
- SSE streaming and final usage payload: `crates/inference-server/src/routes.rs`
### Frontend
- App composition and model swap wiring: `tauri-app/src/App.tsx`
- Chat screen: `tauri-app/src/components/ChatView.tsx`
- Current manual model selector: `tauri-app/src/components/Sidebar.tsx`
- Runtime draft model selection helpers: `tauri-app/src/features/runtime/runtime-settings-form.ts`
- Live model list fetch + current manual-lock behavior: `tauri-app/src/hooks/useModels.ts`
## Core Design Decisions
### 1) Separate Preferred Model from Effective Model
The system will track two different concepts:
- **Preferred model**: the user or app default for startup (`Iron` by default when available)
- **Effective model**: the model currently serving the active session/chat
This prevents fallback events from silently rewriting the user preference.
### 2) Automatic Startup Choice
At app launch, local runtime selection will follow this order:
1. If the user explicitly saved a preferred startup model and it is available, use it.
2. Otherwise prefer `Iron`.
3. If `Iron` is unavailable, use `Cooper`.
4. `Steel` is ignored by normal auto-start policy in this phase.
### 3) TPS-Based Degradation Warning
The system will monitor **generation TPS** during active completion streaming.
Fallback trigger rule:
- track rolling generation TPS samples in Rust
- if rolling TPS stays `< 30` for `>= 5s` continuously
- emit a degradation event once per request/session window
- show a non-blocking toast in the frontend
Toast actions:
- `Trocar para Cooper`
- `Manter Iron`
If the effective model is already `Cooper`, no fallback toast is shown.
### 4) Manual Override in Chat
The primary chat surface will expose a **small model selector** near the composer/header with:
- `Iron`
- `Cooper`
Behavior:
- changing the selector swaps only the **effective model**
- the chosen value persists for the current app session
- it does not silently rewrite startup preference unless the user later saves it in settings
### 5) Tailwind Rule for This Feature
The app currently relies heavily on CSS files. For this feature:
- all **new UI surfaces/components** should be built Tailwind-first
- modified legacy surfaces may temporarily keep existing CSS where a full migration would create unnecessary churn
- any touched layout should move toward utility-class ownership when practical
This keeps delivery realistic while respecting the new direction.
## Model Inventory and Alias Policy
### Canonical alias table
| Raw family | Display name | Phase role |
|---|---|---|
| 0.8B | Cooper | fallback + lightweight manual choice |
| 2B | Iron | default primary model |
| 4B | Steel | settings/internal only |
### Resolution rule
Model display naming must be computed from local file metadata/path patterns, not hardcoded to exact filenames only. Matching should allow future file suffix changes.
Proposed matcher priority:
1. explicit config mapping if present
2. known filename pattern match (`0.8b`, `2b`, `4b`)
3. raw file basename fallback
## Proposed Architecture
### A. Rust: TPS Telemetry Layer
Add a lightweight TPS monitor around the generation loop.
#### Source of truth
- `crates/inference-core/src/session.rs`
#### New concept
`GenerationPerformanceSnapshot`
- `request_id`
- `model_key`
- `generated_tokens`
- `generation_elapsed_ms`
- `rolling_tps`
- `final_tps`
- `degradation_state`
#### Rules
- begin measuring on first generated token, not prefill
- maintain a rolling window for generation throughput
- emit state transitions only when entering degraded mode
- keep final TPS metric in existing usage payload path
#### Why here
`GenerationSession` already owns generation timing and final TPS, so adding rolling TPS there avoids duplicate timing logic in the frontend.
### B. Rust: Runtime Auto-Selection Policy
Add startup model resolution inside runtime bootstrap/start flow.
#### Target
- `tauri-app/src-tauri/src/runtime/mod.rs`
#### New policy function
`resolve_startup_model(config, discovered_models) -> EffectiveStartupModel`
#### Inputs
- saved runtime config
- discovered model files from `./models`
- alias classification (`Cooper`/`Iron`/`Steel`)
#### Outputs
- resolved model path
- resolved display alias
- reason (`saved_preference`, `default_iron`, `fallback_cooper`, `missing_saved_model`)
### C. Frontend: Effective Model Controller
Create a dedicated client-side state layer above `useModels()`.
#### Responsibilities
- maintain `preferredModel`, `effectiveModel`, `manualOverride`, `degradationDismissed`
- map backend model IDs/paths to display aliases
- handle user-triggered model switches
- ignore refresh noise from `/v1/models` once an override is active for the session
#### Likely placement
- new hook near `tauri-app/src/hooks/` or `src/features/runtime/`
- wired in `tauri-app/src/App.tsx`
### D. Frontend: Toast System
No shared toast system exists today.
Add:
- global toast provider near `App.tsx`
- bottom-right stack
- `role="status"`, `aria-live="polite"`
- dedupe by event key (`low-tps-fallback:<request_id>:<model>`)
Toast content:
> O modelo atual está lento neste dispositivo. Deseja trocar para Cooper para melhor velocidade?
Actions:
- primary: swap to `Cooper`
- secondary: keep current model
### E. Frontend: Chat Selector
Move primary user model choice from sidebar-only behavior to an in-chat control.
#### UX rules
- visible but subtle
- available on initial screen and active chat screen
- only shows `Iron` and `Cooper`
- `Steel` remains in settings/runtime management
- while swapping, disable control and show compact busy state
### F. UI Density Updates
#### Home
- reduce vertical whitespace between hero, chips, and lower card
- improve recent work density in sidebar/task list
- keep one muted lower card instead of multiple competing blocks
#### Chat
- denser message header/meta
- keep TPS/status info compact and contextual
- do not overload first paint with debug controls
## Data and State Model
### Backend persisted config
Extend runtime config with explicit preference fields:
- `preferred_startup_model_alias: Option<String>` (`iron`, `cooper`, `steel`)
- keep existing `model_path` for actual runtime launch target
### Frontend session state
- `preferredModelAlias`
- `effectiveModelAlias`
- `effectiveModelPath`
- `overrideSource: "auto" | "manual" | "fallback"`
- `degradationPromptState: "idle" | "shown" | "accepted" | "dismissed"`
## Main User Flows
### Flow 1: App startup
1. app bootstraps runtime config
2. runtime scans available local models
3. startup resolver selects `Iron` if available, else `Cooper`
4. runtime starts with resolved model
5. frontend shows resolved effective model alias
### Flow 2: Sustained low TPS while using Iron
1. user starts generation on `Iron`
2. Rust rolling monitor observes TPS `< 30` for 5 consecutive seconds
3. backend emits degradation event
4. frontend displays one toast
5. user chooses:
- `Trocar para Cooper` => perform hot swap, update effective model, persist session override
- `Manter Iron` => suppress repeated prompt for the current request/session cooldown
### Flow 3: Manual chat override
1. user opens selector in chat
2. chooses `Cooper` or `Iron`
3. frontend calls swap command
4. runtime restarts with target model
5. effective model updates immediately after success
### Flow 4: Steel in settings
1. user opens settings/runtime area
2. sees full model inventory including `Steel`
3. may still use/import/manage `Steel`
4. Steel is not shown in chat selector in this phase
## Error Handling
- if swap fails, keep prior effective model visible
- if degradation event arrives during an in-progress swap, discard it
- if `Iron` disappears from library, auto-fallback to `Cooper`
- if neither `Iron` nor `Cooper` exists, show runtime unavailable state and keep settings path accessible
- if toast already shown for the same request, do not spawn duplicates
## Implementation Touchpoints
### Rust
- `crates/inference-core/src/session.rs`
- `crates/inference-server/src/routes.rs`
- `tauri-app/src-tauri/src/runtime/mod.rs`
- potentially `tauri-app/src-tauri/src/lib.rs` for new commands/events
### Frontend
- `tauri-app/src/App.tsx`
- `tauri-app/src/hooks/useModels.ts`
- `tauri-app/src/components/ChatView.tsx`
- `tauri-app/src/components/Sidebar.tsx`
- `tauri-app/src/features/runtime/runtime-settings-form.ts`
- new toast/provider files
- new model alias resolver/shared hook files
## TDD Test Matrix
### Rust unit tests
1. startup resolver picks `Iron` when `Iron` + `Cooper` are available
2. startup resolver picks `Cooper` when `Iron` is missing
3. startup resolver respects explicit saved preference when available
4. rolling TPS monitor enters degraded state only after 5s sustained `< 30`
5. transient dip below 30 for less than threshold does not trigger
6. degraded state is emitted once per request transition
### Rust integration tests
1. SSE/final usage still includes final TPS after monitor changes
2. degradation event payload contains model alias/path/request id
3. swap command updates effective runtime target correctly
### Frontend unit tests
1. alias resolver maps model file names to `Cooper`, `Iron`, `Steel`
2. chat selector only exposes `Cooper` and `Iron`
3. manual override survives model list refresh in session
4. fallback toast dedupes repeated degradation events
5. clicking `Trocar para Cooper` triggers swap and updates effective model
6. clicking `Manter Iron` suppresses repeated prompt during cooldown
### Frontend component tests
1. chat header/composer shows current effective model alias
2. initial screen remains dense and usable with toast visible
3. selector disabled while swap is in progress
4. runtime unavailable state keeps settings access reachable
## Acceptance Criteria
1. User can start the app without manually choosing a model.
2. App defaults to `Iron` when available.
3. If generation TPS stays below 30 for 5s on `Iron`, a non-blocking toast is shown.
4. User can switch to `Cooper` directly from the toast.
5. User can manually choose between `Iron` and `Cooper` from chat.
6. Manual override persists during the session.
7. `Steel` is visible only in settings/internal runtime management.
8. Final UI is denser and cleaner than current state.
9. New behavior is covered by Rust + frontend tests before implementation code lands.
## Risks
- current frontend styling is still CSS-heavy; partial Tailwind migration may create mixed ownership temporarily
- runtime restart latency may make “instant” switching feel slower on weaker machines
- model filename normalization may be inconsistent unless alias mapping is centralized
- event delivery from Rust to frontend must avoid duplicate or stale degradation prompts
## Recommended Delivery Phases
1. alias resolution + startup policy
2. Rust TPS degradation monitor + event contract
3. frontend toast system + session model controller
4. chat selector + sidebar/settings cleanup
5. density pass on home/chat UI
6. regression verification
## Open Assumptions
- existing hot-swap restart path is acceptable for “instantânea” within product expectations
- startup auto-selection should prefer `Iron` over `Steel` even if `Steel` exists
- `Steel` remains excluded from primary chat selector until a future phase expands the UX