docs(28): create phase plan
This commit is contained in:
parent
5713d996bb
commit
9e7c2890d5
4 changed files with 656 additions and 2 deletions
|
|
@ -43,7 +43,11 @@ Plans:
|
||||||
5. The agent config page shows Nexus-managed skills alongside Hermes native skills in a single unified list
|
5. The agent config page shows Nexus-managed skills alongside Hermes native skills in a single unified list
|
||||||
6. The dashboard agent card for a Hermes agent shows model name, memory usage, and native skill count
|
6. The dashboard agent card for a Hermes agent shows model name, memory usage, and native skill count
|
||||||
7. Token usage and estimated model cost are recorded per heartbeat and surfaced in the cost tracking view
|
7. Token usage and estimated model cost are recorded per heartbeat and surfaced in the cost tracking view
|
||||||
**Plans**: TBD
|
**Plans:** 3 plans
|
||||||
|
Plans:
|
||||||
|
- [ ] 28-01-PLAN.md — Ollama service, routes, model catalog, and unit tests
|
||||||
|
- [ ] 28-02-PLAN.md — UI model selector dropdown, install callout, Hermes skill badge
|
||||||
|
- [ ] 28-03-PLAN.md — Hermes stateJson runtime data and dashboard HermesRuntimeCard
|
||||||
**UI hint**: yes
|
**UI hint**: yes
|
||||||
|
|
||||||
### Phase 29: Default Provider & End-to-End
|
### Phase 29: Default Provider & End-to-End
|
||||||
|
|
@ -89,5 +93,5 @@ All 16 v1 requirements are mapped to exactly one phase. No orphans.
|
||||||
| Phase | Milestone | Plans Complete | Status | Completed |
|
| Phase | Milestone | Plans Complete | Status | Completed |
|
||||||
|-------|-----------|----------------|--------|-----------|
|
|-------|-----------|----------------|--------|-----------|
|
||||||
| 27. Hermes Adapter | v1.4 | 1/1 | Complete | 2026-04-02 |
|
| 27. Hermes Adapter | v1.4 | 1/1 | Complete | 2026-04-02 |
|
||||||
| 28. Ollama Integration & Agent Surface | v1.4 | 0/? | Not started | - |
|
| 28. Ollama Integration & Agent Surface | v1.4 | 0/3 | Not started | - |
|
||||||
| 29. Default Provider & End-to-End | v1.4 | 0/? | Not started | - |
|
| 29. Default Provider & End-to-End | v1.4 | 0/? | Not started | - |
|
||||||
|
|
|
||||||
194
.planning/phases/28-ollama-integration/28-01-PLAN.md
Normal file
194
.planning/phases/28-ollama-integration/28-01-PLAN.md
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
---
|
||||||
|
phase: 28-ollama-integration
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- server/src/services/ollama.ts
|
||||||
|
- server/src/routes/ollama.ts
|
||||||
|
- server/src/routes/index.ts
|
||||||
|
- server/src/app.ts
|
||||||
|
- server/src/data/ollama-model-catalog.json
|
||||||
|
- server/src/__tests__/ollama-service.test.ts
|
||||||
|
autonomous: true
|
||||||
|
requirements: [OLLA-01, OLLA-02, OLLA-04, OLLA-05]
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "detectOllama() returns installed:true + version when Ollama responds at /api/version"
|
||||||
|
- "detectOllama() returns installed:false + installUrl when Ollama is absent or times out"
|
||||||
|
- "listOllamaModels() returns model list with parameterSize, family, quantization from /api/tags"
|
||||||
|
- "getRecommendedModel() returns highest-quality model that fits within 75% system RAM"
|
||||||
|
- "GET /companies/:companyId/ollama/status returns OllamaStatus JSON"
|
||||||
|
- "GET /companies/:companyId/ollama/models returns OllamaModel[] + ramGb"
|
||||||
|
artifacts:
|
||||||
|
- path: "server/src/services/ollama.ts"
|
||||||
|
provides: "Ollama detection, model listing, recommendation logic"
|
||||||
|
exports: ["detectOllama", "listOllamaModels", "OllamaStatus", "OllamaModel"]
|
||||||
|
- path: "server/src/routes/ollama.ts"
|
||||||
|
provides: "HTTP routes for Ollama status and model listing"
|
||||||
|
exports: ["ollamaRoutes"]
|
||||||
|
- path: "server/src/data/ollama-model-catalog.json"
|
||||||
|
provides: "Static model catalog for RAM-based recommendations"
|
||||||
|
contains: "qwen2.5-coder"
|
||||||
|
- path: "server/src/__tests__/ollama-service.test.ts"
|
||||||
|
provides: "Unit tests for ollamaService"
|
||||||
|
min_lines: 60
|
||||||
|
key_links:
|
||||||
|
- from: "server/src/routes/ollama.ts"
|
||||||
|
to: "server/src/services/ollama.ts"
|
||||||
|
via: "import detectOllama, listOllamaModels"
|
||||||
|
pattern: "import.*from.*services/ollama"
|
||||||
|
- from: "server/src/app.ts"
|
||||||
|
to: "server/src/routes/ollama.ts"
|
||||||
|
via: "api.use(ollamaRoutes())"
|
||||||
|
pattern: "ollamaRoutes"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Create the server-side Ollama integration service, HTTP routes, model catalog, and unit tests.
|
||||||
|
|
||||||
|
Purpose: Provides the backend API surface that the UI (Plan 02) will consume to detect Ollama, list available models, and recommend a model based on system RAM.
|
||||||
|
Output: Working server routes at /companies/:companyId/ollama/status and /models, plus unit tests.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/phases/28-ollama-integration/28-RESEARCH.md
|
||||||
|
|
||||||
|
@server/src/app.ts
|
||||||
|
@server/src/routes/index.ts
|
||||||
|
@server/src/routes/agents.ts (for assertCompanyAccess pattern)
|
||||||
|
@ui/src/api/client.ts (for API client pattern reference)
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 1: Create ollamaService + model catalog + unit tests</name>
|
||||||
|
<files>server/src/services/ollama.ts, server/src/data/ollama-model-catalog.json, server/src/__tests__/ollama-service.test.ts</files>
|
||||||
|
<read_first>
|
||||||
|
- server/src/services/heartbeat.ts (lines 1-30 for import patterns)
|
||||||
|
- .planning/phases/28-ollama-integration/28-RESEARCH.md (Pattern 1, Pattern 5, Code Examples)
|
||||||
|
</read_first>
|
||||||
|
<behavior>
|
||||||
|
- detectOllama returns { installed: true, version: "0.5.x" } when fetch to /api/version succeeds
|
||||||
|
- detectOllama returns { installed: false, version: null, installUrl: "https://ollama.com/download" } when fetch rejects (ECONNREFUSED)
|
||||||
|
- detectOllama returns { installed: false, version: null, installUrl } when fetch times out (AbortController)
|
||||||
|
- listOllamaModels returns OllamaModel[] mapped from /api/tags response with name, parameterSize, quantization, sizeBytes, family
|
||||||
|
- listOllamaModels returns empty array when Ollama is absent
|
||||||
|
- getRecommendedModel marks the highest-quality model that fits within 75% of given RAM budget as recommended=true
|
||||||
|
- getRecommendedModel with 8GB RAM recommends a 7b model, not a 32b model
|
||||||
|
- Model catalog JSON contains at least qwen2, llama, mistral, phi, deepseek families
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
1. Create `server/src/data/ollama-model-catalog.json` with the static catalog from RESEARCH Pattern 5. Include families: qwen2, llama, mistral, phi, deepseek. Each variant has name, ramGb, vramGb, quality fields.
|
||||||
|
|
||||||
|
2. Create `server/src/services/ollama.ts`:
|
||||||
|
- `const OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL ?? "http://localhost:11434"`
|
||||||
|
- `const OLLAMA_TIMEOUT_MS = 3000`
|
||||||
|
- `const INSTALL_URL = "https://ollama.com/download"`
|
||||||
|
- Export interface `OllamaStatus { installed: boolean; version: string | null; installUrl: string }`
|
||||||
|
- Export interface `OllamaModel { name: string; parameterSize: string; quantization: string; sizeBytes: number; family: string; recommended: boolean; recommendationReason: string | null }`
|
||||||
|
- Export `async function detectOllama(): Promise<OllamaStatus>` — fetch /api/version with AbortController 3s timeout. On success return installed:true + version. On any error return installed:false + installUrl.
|
||||||
|
- Export `async function listOllamaModels(): Promise<OllamaModel[]>` — fetch /api/tags, map response.models to OllamaModel[]. On error return [].
|
||||||
|
- Export `function getRecommendedModel(models: OllamaModel[], systemRamBytes: number): OllamaModel[]` — reads catalog JSON, computes usableRamGb = (systemRamBytes / 1024^3) * 0.75, for each model checks if a catalog entry matches by name and fits in RAM. Returns models with `recommended` field set. The highest-quality model within budget gets recommended=true + a recommendationReason string. All others get recommended=false.
|
||||||
|
- Anti-pattern: Do NOT poll in a loop. Do NOT hard-code localhost:11434 — always use OLLAMA_BASE_URL.
|
||||||
|
|
||||||
|
3. Create `server/src/__tests__/ollama-service.test.ts`:
|
||||||
|
- Mock global fetch using vi.stubGlobal("fetch", vi.fn())
|
||||||
|
- Test detectOllama success case (mock returns { version: "0.5.1" })
|
||||||
|
- Test detectOllama failure case (mock rejects with ECONNREFUSED)
|
||||||
|
- Test detectOllama timeout case (mock never resolves, verify AbortController fires)
|
||||||
|
- Test listOllamaModels success with mock /api/tags response matching OllamaTagsResponse shape from RESEARCH
|
||||||
|
- Test listOllamaModels returns [] on fetch error
|
||||||
|
- Test getRecommendedModel with 8GB RAM → recommends 7b-class model
|
||||||
|
- Test getRecommendedModel with 32GB RAM → recommends 32b-class model
|
||||||
|
- Test getRecommendedModel with models not in catalog → recommended=false for all
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /opt/nexus/server && npx vitest run src/__tests__/ollama-service.test.ts</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- grep -q "detectOllama" server/src/services/ollama.ts
|
||||||
|
- grep -q "listOllamaModels" server/src/services/ollama.ts
|
||||||
|
- grep -q "getRecommendedModel" server/src/services/ollama.ts
|
||||||
|
- grep -q "OLLAMA_BASE_URL" server/src/services/ollama.ts
|
||||||
|
- grep -q "qwen2.5-coder" server/src/data/ollama-model-catalog.json
|
||||||
|
- grep -q "detectOllama" server/src/__tests__/ollama-service.test.ts
|
||||||
|
- grep -q "getRecommendedModel" server/src/__tests__/ollama-service.test.ts
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>All ollama service tests pass. detectOllama, listOllamaModels, and getRecommendedModel functions exported and tested. Model catalog JSON file exists with 5+ model families.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Create Ollama HTTP routes and mount in app</name>
|
||||||
|
<files>server/src/routes/ollama.ts, server/src/routes/index.ts, server/src/app.ts</files>
|
||||||
|
<read_first>
|
||||||
|
- server/src/routes/agents.ts (lines 1-60 for route pattern, assertCompanyAccess usage)
|
||||||
|
- server/src/app.ts (lines 134-170 for route mounting pattern)
|
||||||
|
- server/src/routes/index.ts
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
1. Create `server/src/routes/ollama.ts`:
|
||||||
|
- Import Router from express, import assertCompanyAccess from "./authz.js", import detectOllama/listOllamaModels/getRecommendedModel from "../services/ollama.js"
|
||||||
|
- Import `os` for `os.totalmem()`
|
||||||
|
- Export function `ollamaRoutes()` returning Router
|
||||||
|
- `GET /companies/:companyId/ollama/status`:
|
||||||
|
- Call `assertCompanyAccess(req, companyId)`
|
||||||
|
- Call `detectOllama()`, return JSON response
|
||||||
|
- `GET /companies/:companyId/ollama/models`:
|
||||||
|
- Call `assertCompanyAccess(req, companyId)`
|
||||||
|
- Call `detectOllama()` first — if not installed, return `{ models: [], ramGb: 0 }`
|
||||||
|
- Call `listOllamaModels()`, then `getRecommendedModel(models, os.totalmem())`
|
||||||
|
- Return `{ models: enrichedModels, ramGb: Math.round(os.totalmem() / 1073741824) }`
|
||||||
|
- Wrap each handler in try/catch, return 500 on unexpected errors
|
||||||
|
|
||||||
|
2. Add `export { ollamaRoutes } from "./ollama.js"` to `server/src/routes/index.ts`
|
||||||
|
|
||||||
|
3. In `server/src/app.ts`:
|
||||||
|
- Add import: `import { ollamaRoutes } from "./routes/ollama.js"`
|
||||||
|
- Add `api.use(ollamaRoutes())` after the `api.use(agentRoutes(db))` line (around line 152)
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /opt/nexus/server && npx tsc --noEmit 2>&1 | head -20</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- grep -q "ollamaRoutes" server/src/routes/ollama.ts
|
||||||
|
- grep -q "assertCompanyAccess" server/src/routes/ollama.ts
|
||||||
|
- grep -q "/companies/:companyId/ollama/status" server/src/routes/ollama.ts
|
||||||
|
- grep -q "/companies/:companyId/ollama/models" server/src/routes/ollama.ts
|
||||||
|
- grep -q "ollamaRoutes" server/src/routes/index.ts
|
||||||
|
- grep -q "ollamaRoutes" server/src/app.ts
|
||||||
|
- grep -q "os.totalmem" server/src/routes/ollama.ts
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>Ollama routes mounted at /companies/:companyId/ollama/status and /models. TypeScript compiles without errors. Routes use assertCompanyAccess for auth and os.totalmem() for RAM detection.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `cd /opt/nexus/server && npx vitest run src/__tests__/ollama-service.test.ts` — all tests pass
|
||||||
|
- `cd /opt/nexus/server && npx tsc --noEmit` — no type errors
|
||||||
|
- Ollama service gracefully returns installed:false when Ollama is not running (no crashes)
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Ollama detection service exists with detectOllama, listOllamaModels, getRecommendedModel
|
||||||
|
- Model catalog JSON ships with 5+ model families
|
||||||
|
- HTTP routes mounted and accessible at /companies/:companyId/ollama/status and /models
|
||||||
|
- Unit tests cover success, failure, timeout, and recommendation scenarios
|
||||||
|
- All code compiles without TypeScript errors
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/28-ollama-integration/28-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
198
.planning/phases/28-ollama-integration/28-02-PLAN.md
Normal file
198
.planning/phases/28-ollama-integration/28-02-PLAN.md
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
---
|
||||||
|
phase: 28-ollama-integration
|
||||||
|
plan: 02
|
||||||
|
type: execute
|
||||||
|
wave: 2
|
||||||
|
depends_on: [28-01]
|
||||||
|
files_modified:
|
||||||
|
- ui/src/api/ollama.ts
|
||||||
|
- ui/src/adapters/hermes-local/config-fields.tsx
|
||||||
|
- ui/src/pages/AgentDetail.tsx
|
||||||
|
autonomous: true
|
||||||
|
requirements: [OLLA-02, OLLA-03, OLLA-05, HERM-05]
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "When Ollama is installed, the Hermes agent config shows a model dropdown listing all locally pulled models"
|
||||||
|
- "When an Ollama model is selected, adapterConfig saves model + provider:custom + base_url:http://localhost:11434/v1"
|
||||||
|
- "When Ollama is absent, the config shows an install callout with a link to https://ollama.com/download"
|
||||||
|
- "Recommended models are visually highlighted in the dropdown"
|
||||||
|
- "Hermes native skills show an 'Hermes skill' badge in the Skills tab"
|
||||||
|
- "Manual model entry still works as fallback when Ollama is absent"
|
||||||
|
artifacts:
|
||||||
|
- path: "ui/src/api/ollama.ts"
|
||||||
|
provides: "API client for Ollama status and model listing"
|
||||||
|
exports: ["ollamaApi"]
|
||||||
|
- path: "ui/src/adapters/hermes-local/config-fields.tsx"
|
||||||
|
provides: "Ollama model dropdown, install callout, manual fallback"
|
||||||
|
contains: "ollamaApi"
|
||||||
|
- path: "ui/src/pages/AgentDetail.tsx"
|
||||||
|
provides: "Hermes skill badge in AgentSkillsTab"
|
||||||
|
contains: "Hermes skill"
|
||||||
|
key_links:
|
||||||
|
- from: "ui/src/adapters/hermes-local/config-fields.tsx"
|
||||||
|
to: "ui/src/api/ollama.ts"
|
||||||
|
via: "useQuery + ollamaApi.status / ollamaApi.models"
|
||||||
|
pattern: "ollamaApi"
|
||||||
|
- from: "ui/src/adapters/hermes-local/config-fields.tsx"
|
||||||
|
to: "server/src/routes/ollama.ts"
|
||||||
|
via: "fetch /companies/:companyId/ollama/*"
|
||||||
|
pattern: "ollama"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Create the UI surface for Ollama model selection in Hermes agent config and improve Hermes skill visibility.
|
||||||
|
|
||||||
|
Purpose: Users can discover, browse, and select local Ollama models when configuring a Hermes agent, with hardware-aware recommendations highlighted. When Ollama is absent, users see install instructions. Hermes native skills are clearly labeled in the Skills tab.
|
||||||
|
Output: Working model selector dropdown, install callout, and Hermes skill badges.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/phases/28-ollama-integration/28-RESEARCH.md
|
||||||
|
@.planning/phases/28-ollama-integration/28-01-SUMMARY.md
|
||||||
|
|
||||||
|
@ui/src/adapters/hermes-local/config-fields.tsx
|
||||||
|
@ui/src/adapters/types.ts
|
||||||
|
@ui/src/api/client.ts
|
||||||
|
@ui/src/pages/AgentDetail.tsx (AgentSkillsTab around line 2362, unmanagedSkillRows around 2566)
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- From Plan 01 server routes — endpoints the UI will call -->
|
||||||
|
|
||||||
|
GET /api/companies/:companyId/ollama/status
|
||||||
|
Response: { installed: boolean; version: string | null; installUrl: string }
|
||||||
|
|
||||||
|
GET /api/companies/:companyId/ollama/models
|
||||||
|
Response: { models: OllamaModel[]; ramGb: number }
|
||||||
|
Where OllamaModel = {
|
||||||
|
name: string; // e.g. "qwen2.5-coder:32b"
|
||||||
|
parameterSize: string; // e.g. "32.8B"
|
||||||
|
quantization: string; // e.g. "Q4_K_M"
|
||||||
|
sizeBytes: number;
|
||||||
|
family: string; // e.g. "qwen2"
|
||||||
|
recommended: boolean;
|
||||||
|
recommendationReason: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- From ui/src/adapters/types.ts -->
|
||||||
|
AdapterConfigFieldsProps: { mode, isCreate, adapterType, values, set, config, eff, mark, models, hideInstructionsFile }
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Create ollamaApi client and enhance HermesLocalConfigFields with model dropdown</name>
|
||||||
|
<files>ui/src/api/ollama.ts, ui/src/adapters/hermes-local/config-fields.tsx</files>
|
||||||
|
<read_first>
|
||||||
|
- ui/src/api/client.ts (full file — for api.get pattern)
|
||||||
|
- ui/src/api/health.ts (for simple API client pattern)
|
||||||
|
- ui/src/adapters/hermes-local/config-fields.tsx (full file — current state)
|
||||||
|
- ui/src/adapters/types.ts (AdapterConfigFieldsProps interface)
|
||||||
|
- .planning/phases/28-ollama-integration/28-RESEARCH.md (Pattern 3, Pattern 4, Pitfall 1, Pitfall 4)
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
1. Create `ui/src/api/ollama.ts`:
|
||||||
|
- Import `api` from "./client" (the request helper)
|
||||||
|
- Define types: `OllamaStatus { installed: boolean; version: string | null; installUrl: string }`, `OllamaModel { name: string; parameterSize: string; quantization: string; sizeBytes: number; family: string; recommended: boolean; recommendationReason: string | null }`, `OllamaModelsResponse { models: OllamaModel[]; ramGb: number }`
|
||||||
|
- Export `ollamaApi` object with:
|
||||||
|
- `status(companyId: string): Promise<OllamaStatus>` — GET `/companies/${companyId}/ollama/status`
|
||||||
|
- `models(companyId: string): Promise<OllamaModelsResponse>` — GET `/companies/${companyId}/ollama/models`
|
||||||
|
|
||||||
|
2. Rewrite `ui/src/adapters/hermes-local/config-fields.tsx`:
|
||||||
|
- Add imports: `useQuery` from `@tanstack/react-query`, `ollamaApi` from `../../api/ollama`
|
||||||
|
- Need companyId: extract from URL params using `useParams` from react-router-dom, or accept via a context. Check existing config-fields patterns for how companyId is obtained — look at the component's parent to find how it gets companyId. If not available via props, use `useParams<{ companyId?: string }>()` matching the route pattern `/companies/:companyId/agents/...`.
|
||||||
|
- Add two queries (only enabled when companyId is truthy):
|
||||||
|
```
|
||||||
|
const { data: ollamaStatus } = useQuery({ queryKey: ["ollama", "status", companyId], queryFn: () => ollamaApi.status(companyId!), enabled: Boolean(companyId), staleTime: 60_000 })
|
||||||
|
const { data: ollamaModels } = useQuery({ queryKey: ["ollama", "models", companyId], queryFn: () => ollamaApi.models(companyId!), enabled: Boolean(companyId && ollamaStatus?.installed), staleTime: 60_000 })
|
||||||
|
```
|
||||||
|
- Replace the current free-text Model `<DraftInput>` with a hybrid control:
|
||||||
|
- **When ollamaStatus?.installed AND ollamaModels?.models.length > 0**: Render a `<select>` dropdown with an empty option ("Select a model..."), then each ollamaModels.models item as an `<option>`. Recommended models get a star prefix in label (e.g., "* qwen2.5-coder:7b (7B, Q4_K_M) - Recommended for your system"). Non-recommended models show plain label (e.g., "qwen2.5-coder:32b (32.8B, Q4_K_M)"). Add a final option "Other (manual entry)..." that switches to a text input.
|
||||||
|
- **When ollamaStatus?.installed === false**: Render an install callout div: "Ollama is not detected." with a link to ollamaStatus.installUrl ("Install Ollama"). Below the callout, keep the existing DraftInput for manual model entry (user may have a remote provider).
|
||||||
|
- **When ollamaStatus is loading or undefined**: Show the existing DraftInput as fallback.
|
||||||
|
- CRITICAL (Pitfall 1 + Pitfall 4): When an Ollama model is selected from the dropdown, set ALL THREE fields atomically:
|
||||||
|
- Create mode: `set!({ model: selectedModel, provider: "custom", base_url: "http://localhost:11434/v1" })`
|
||||||
|
- Edit mode: `mark("adapterConfig", "model", selectedModel)`, `mark("adapterConfig", "provider", "custom")`, `mark("adapterConfig", "base_url", "http://localhost:11434/v1")`
|
||||||
|
- When "Other (manual entry)" is selected or when using the fallback text input, do NOT set provider/base_url — user manages those fields themselves.
|
||||||
|
- Style the select with the existing `inputClass` CSS classes for consistency.
|
||||||
|
- Style the install callout: use `rounded-md border border-amber-500/30 bg-amber-500/5 p-3 text-sm` with an external link icon or similar.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /opt/nexus/ui && npx tsc --noEmit 2>&1 | head -20</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- grep -q "ollamaApi" ui/src/api/ollama.ts
|
||||||
|
- grep -q "status" ui/src/api/ollama.ts
|
||||||
|
- grep -q "models" ui/src/api/ollama.ts
|
||||||
|
- grep -q "ollamaApi" ui/src/adapters/hermes-local/config-fields.tsx
|
||||||
|
- grep -q "useQuery" ui/src/adapters/hermes-local/config-fields.tsx
|
||||||
|
- grep -q 'provider.*custom' ui/src/adapters/hermes-local/config-fields.tsx
|
||||||
|
- grep -q 'base_url.*localhost:11434' ui/src/adapters/hermes-local/config-fields.tsx
|
||||||
|
- grep -q "ollama.com/download" ui/src/adapters/hermes-local/config-fields.tsx
|
||||||
|
- grep -q "recommended" ui/src/adapters/hermes-local/config-fields.tsx
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>Hermes config shows Ollama model dropdown when Ollama is detected, install callout when absent, and manual fallback. Selecting an Ollama model sets provider:custom + base_url atomically. TypeScript compiles without errors.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Add Hermes skill badge rendering in AgentSkillsTab</name>
|
||||||
|
<files>ui/src/pages/AgentDetail.tsx</files>
|
||||||
|
<read_first>
|
||||||
|
- ui/src/pages/AgentDetail.tsx (lines 2550-2600 for unmanagedSkillRows, lines 2950-2970 for originLabel rendering, lines 3050-3070 for unmanagedSkillRows section header)
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Update the `AgentSkillsTab` component in `AgentDetail.tsx` to better distinguish Hermes native skills:
|
||||||
|
|
||||||
|
1. In the unmanagedSkillRows section header (around line 3063), change the label text to be conditional:
|
||||||
|
- If `agent.adapterType === "hermes_local"`: display `(${unmanagedSkillRows.length}) Hermes native skills & user-installed skills`
|
||||||
|
- Otherwise: keep existing text `(${unmanagedSkillRows.length}) User-installed skills, not managed by ${VOCAB.appName}`
|
||||||
|
|
||||||
|
2. In the `renderSkillRow` function (around line 2960-2961), where `skill.readOnly && skill.originLabel` renders a `<p>` with originLabel text:
|
||||||
|
- When `skill.originLabel === "Hermes skill"`, render a small inline badge instead of plain text:
|
||||||
|
`<span className="inline-flex items-center rounded-full bg-purple-500/10 px-2 py-0.5 text-xs font-medium text-purple-400">Hermes skill</span>`
|
||||||
|
- Keep the existing `<p>` rendering for other originLabel values unchanged.
|
||||||
|
|
||||||
|
3. The `unmanagedSkillRows` data already flows from the skill registry API which includes Hermes native skills with `originLabel: "Hermes skill"` and `readOnly: true`. No data changes needed.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /opt/nexus/ui && npx tsc --noEmit 2>&1 | head -20</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- grep -q "Hermes skill" ui/src/pages/AgentDetail.tsx
|
||||||
|
- grep -q "hermes_local" ui/src/pages/AgentDetail.tsx
|
||||||
|
- grep -q "Hermes native" ui/src/pages/AgentDetail.tsx
|
||||||
|
- grep -q "purple" ui/src/pages/AgentDetail.tsx
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>Hermes native skills show a purple "Hermes skill" badge in the Skills tab. Section header indicates "Hermes native skills" when viewing a Hermes agent. TypeScript compiles without errors.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `cd /opt/nexus/ui && npx tsc --noEmit` — no type errors
|
||||||
|
- Config-fields dropdown renders when Ollama is available (verified by code structure)
|
||||||
|
- Install callout renders when Ollama is absent (verified by code structure)
|
||||||
|
- Hermes skill badge uses distinct purple styling
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- ollamaApi client exists with status() and models() methods
|
||||||
|
- HermesLocalConfigFields shows model dropdown when Ollama detected, install callout when absent
|
||||||
|
- Selecting an Ollama model atomically sets model + provider:custom + base_url
|
||||||
|
- Manual model entry works as fallback
|
||||||
|
- Hermes native skills show "Hermes skill" badge in Skills tab
|
||||||
|
- All TypeScript compiles without errors
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/28-ollama-integration/28-02-SUMMARY.md`
|
||||||
|
</output>
|
||||||
258
.planning/phases/28-ollama-integration/28-03-PLAN.md
Normal file
258
.planning/phases/28-ollama-integration/28-03-PLAN.md
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
---
|
||||||
|
phase: 28-ollama-integration
|
||||||
|
plan: 03
|
||||||
|
type: execute
|
||||||
|
wave: 2
|
||||||
|
depends_on: [28-01]
|
||||||
|
files_modified:
|
||||||
|
- server/src/services/heartbeat.ts
|
||||||
|
- ui/src/pages/AgentDetail.tsx
|
||||||
|
autonomous: true
|
||||||
|
requirements: [HERM-06, HERM-07]
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "After a Hermes heartbeat completes, stateJson contains hermesModel and hermesNativeSkillCount"
|
||||||
|
- "Dashboard AgentOverview shows model name and native skill count for Hermes agents"
|
||||||
|
- "Memory usage from /api/ps is shown when Ollama model is actively loaded"
|
||||||
|
- "Cost tracking shows $0.00 for Ollama-based runs (correct behavior, no code change needed)"
|
||||||
|
- "stateJson merge uses spread/jsonb concat, never overwrites existing fields"
|
||||||
|
artifacts:
|
||||||
|
- path: "server/src/services/heartbeat.ts"
|
||||||
|
provides: "Hermes stateJson merge in updateRuntimeState"
|
||||||
|
contains: "hermesModel"
|
||||||
|
- path: "ui/src/pages/AgentDetail.tsx"
|
||||||
|
provides: "HermesRuntimeCard component in AgentOverview"
|
||||||
|
contains: "HermesRuntimeCard"
|
||||||
|
key_links:
|
||||||
|
- from: "server/src/services/heartbeat.ts"
|
||||||
|
to: "server/src/services/ollama.ts"
|
||||||
|
via: "import for /api/ps memory query"
|
||||||
|
pattern: "ollama"
|
||||||
|
- from: "ui/src/pages/AgentDetail.tsx"
|
||||||
|
to: "agentRuntimeState.stateJson"
|
||||||
|
via: "runtimeState.stateJson?.hermesModel"
|
||||||
|
pattern: "hermesModel"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Wire Hermes runtime data into stateJson after heartbeat runs and render it in the dashboard.
|
||||||
|
|
||||||
|
Purpose: Users see Hermes-specific information (model name, native skill count, memory usage) on the agent dashboard. Cost tracking for Ollama-based runs correctly shows $0.00 (already wired, just needs verification).
|
||||||
|
Output: stateJson populated after Hermes runs, HermesRuntimeCard visible in AgentOverview.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/phases/28-ollama-integration/28-RESEARCH.md
|
||||||
|
@.planning/phases/28-ollama-integration/28-01-SUMMARY.md
|
||||||
|
|
||||||
|
@server/src/services/heartbeat.ts (updateRuntimeState around line 1846)
|
||||||
|
@server/src/services/ollama.ts (from Plan 01)
|
||||||
|
@ui/src/pages/AgentDetail.tsx (AgentOverview around line 1168)
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- From heartbeat.ts updateRuntimeState signature -->
|
||||||
|
async function updateRuntimeState(
|
||||||
|
agent: typeof agents.$inferSelect,
|
||||||
|
run: typeof heartbeatRuns.$inferSelect,
|
||||||
|
result: AdapterExecutionResult,
|
||||||
|
session: { legacySessionId: string | null },
|
||||||
|
normalizedUsage?: UsageTotals | null,
|
||||||
|
): void
|
||||||
|
|
||||||
|
<!-- AdapterExecutionResult has: model?: string, usage?, costUsd?, resultJson? -->
|
||||||
|
|
||||||
|
<!-- stateJson patch shape for Hermes -->
|
||||||
|
{
|
||||||
|
hermesModel: string; // from result.model
|
||||||
|
hermesNativeSkillCount: number; // from skill registry query or UI-derived
|
||||||
|
hermesMemoryBytes: number | null; // from /api/ps size_vram
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- AgentOverview props -->
|
||||||
|
agent: AgentDetailRecord (has adapterType)
|
||||||
|
runtimeState?: AgentRuntimeState (has stateJson: Record<string, unknown>)
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Merge Hermes runtime data into stateJson after heartbeat</name>
|
||||||
|
<files>server/src/services/heartbeat.ts</files>
|
||||||
|
<read_first>
|
||||||
|
- server/src/services/heartbeat.ts (lines 1846-1900 for updateRuntimeState, lines 2690-2710 for where updateRuntimeState is called)
|
||||||
|
- server/src/services/ollama.ts (for import path — Plan 01 created this)
|
||||||
|
- .planning/phases/28-ollama-integration/28-RESEARCH.md (Pitfall 3 — stateJson merge, Pitfall 5 — /api/ps empty case, Pitfall 6 — cost tracking)
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Modify `updateRuntimeState` in `server/src/services/heartbeat.ts` to merge Hermes-specific data into stateJson:
|
||||||
|
|
||||||
|
1. At the top of heartbeat.ts, add: `import { getOllamaMemoryUsage } from "../services/ollama.js"` (this function needs to be added to ollama.ts first — see step 3).
|
||||||
|
|
||||||
|
2. After the existing `db.update(agentRuntimeState).set(...)` call (around line 1879), add a conditional block:
|
||||||
|
```typescript
|
||||||
|
if (agent.adapterType === "hermes_local") {
|
||||||
|
const hermesModel = result.model ?? null;
|
||||||
|
// Query Ollama /api/ps for memory usage (non-blocking, best-effort)
|
||||||
|
let hermesMemoryBytes: number | null = null;
|
||||||
|
try {
|
||||||
|
hermesMemoryBytes = await getOllamaMemoryUsage(hermesModel);
|
||||||
|
} catch {
|
||||||
|
// Ollama may not be running or model not loaded — acceptable
|
||||||
|
}
|
||||||
|
// Merge into stateJson using jsonb concat to avoid overwriting other fields
|
||||||
|
await db
|
||||||
|
.update(agentRuntimeState)
|
||||||
|
.set({
|
||||||
|
stateJson: sql`COALESCE(${agentRuntimeState.stateJson}, '{}'::jsonb) || ${JSON.stringify({
|
||||||
|
hermesModel,
|
||||||
|
hermesMemoryBytes,
|
||||||
|
})}::jsonb`,
|
||||||
|
})
|
||||||
|
.where(eq(agentRuntimeState.agentId, agent.id));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Add `getOllamaMemoryUsage` function to `server/src/services/ollama.ts` (modify the file created in Plan 01):
|
||||||
|
- `export async function getOllamaMemoryUsage(modelName: string | null): Promise<number | null>`
|
||||||
|
- Fetch `${OLLAMA_BASE_URL}/api/ps` with 3s AbortController timeout
|
||||||
|
- Parse response as OllamaPsResponse
|
||||||
|
- Find the model matching `modelName` in the response.models array
|
||||||
|
- Return `size_vram` from the match, or null if no match / empty array / error
|
||||||
|
- On any error, return null (graceful degradation per Pitfall 5)
|
||||||
|
|
||||||
|
4. HERM-06 verification: The existing cost tracking code in updateRuntimeState already handles Hermes correctly:
|
||||||
|
- `result.costUsd` is undefined for local Ollama → `normalizeBilledCostCents(undefined, ...)` returns 0
|
||||||
|
- Token usage may or may not be emitted by Hermes → cost event created only if `hasTokenUsage`
|
||||||
|
- No code changes needed for HERM-06 — just verify the path works by reading the code.
|
||||||
|
|
||||||
|
NOTE: hermesNativeSkillCount is NOT stored in stateJson. Per RESEARCH Open Question 3, the UI will derive this from the existing `agentsApi.skills(agentId)` query (which already syncs Hermes native skills via skillRegistryService). This avoids cross-DB complexity in the heartbeat path.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /opt/nexus/server && npx tsc --noEmit 2>&1 | head -20</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- grep -q "hermesModel" server/src/services/heartbeat.ts
|
||||||
|
- grep -q "hermesMemoryBytes" server/src/services/heartbeat.ts
|
||||||
|
- grep -q "hermes_local" server/src/services/heartbeat.ts
|
||||||
|
- grep -q "COALESCE" server/src/services/heartbeat.ts
|
||||||
|
- grep -q "getOllamaMemoryUsage" server/src/services/ollama.ts
|
||||||
|
- grep -q "api/ps" server/src/services/ollama.ts
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>After a Hermes heartbeat run, stateJson is updated with hermesModel and hermesMemoryBytes via jsonb merge. getOllamaMemoryUsage queries /api/ps gracefully. TypeScript compiles without errors.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Create HermesRuntimeCard component in AgentOverview</name>
|
||||||
|
<files>ui/src/pages/AgentDetail.tsx</files>
|
||||||
|
<read_first>
|
||||||
|
- ui/src/pages/AgentDetail.tsx (lines 1168-1230 for AgentOverview structure, lines 1002-1010 for AgentOverview props passed)
|
||||||
|
- ui/src/pages/AgentDetail.tsx (lines 2362-2380 for AgentSkillsTab — to see how skills query works)
|
||||||
|
- .planning/phases/28-ollama-integration/28-RESEARCH.md (HERM-07 section)
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Add a `HermesRuntimeCard` component to `AgentDetail.tsx` and render it in `AgentOverview`:
|
||||||
|
|
||||||
|
1. Create `HermesRuntimeCard` function component (define it near AgentOverview, around line 1160):
|
||||||
|
```typescript
|
||||||
|
function HermesRuntimeCard({ runtimeState, agentId }: { runtimeState: AgentRuntimeState; agentId: string }) {
|
||||||
|
const hermesModel = runtimeState.stateJson?.hermesModel as string | undefined;
|
||||||
|
const hermesMemoryBytes = runtimeState.stateJson?.hermesMemoryBytes as number | null | undefined;
|
||||||
|
|
||||||
|
// Derive native skill count from existing skills query
|
||||||
|
const { data: skillsData } = useQuery({
|
||||||
|
queryKey: ["agents", agentId, "skills"],
|
||||||
|
queryFn: () => agentsApi.skills(agentId),
|
||||||
|
});
|
||||||
|
const nativeSkillCount = useMemo(() => {
|
||||||
|
if (!skillsData?.adapterEntries) return 0;
|
||||||
|
return skillsData.adapterEntries.filter((e: any) => e.originLabel === "Hermes skill").length;
|
||||||
|
}, [skillsData]);
|
||||||
|
|
||||||
|
const formatBytes = (bytes: number) => {
|
||||||
|
const gb = bytes / (1024 * 1024 * 1024);
|
||||||
|
return gb >= 1 ? `${gb.toFixed(1)} GB` : `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-border p-4 space-y-3">
|
||||||
|
<h3 className="text-sm font-medium flex items-center gap-2">
|
||||||
|
Hermes Runtime
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">Model</p>
|
||||||
|
<p className="text-sm font-mono">{hermesModel ?? "Not set"}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">Native Skills</p>
|
||||||
|
<p className="text-sm">{nativeSkillCount}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">Memory (VRAM)</p>
|
||||||
|
<p className="text-sm">
|
||||||
|
{hermesMemoryBytes != null ? formatBytes(hermesMemoryBytes) : "Not loaded"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. In `AgentOverview` component (around line 1202, after the charts grid div), add:
|
||||||
|
```tsx
|
||||||
|
{agent.adapterType === "hermes_local" && runtimeState && (
|
||||||
|
<HermesRuntimeCard runtimeState={runtimeState} agentId={agentId} />
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Ensure imports: `useMemo` should already be imported. `agentsApi` should already be imported. `useQuery` should already be imported. If not, add the necessary imports.
|
||||||
|
|
||||||
|
4. Check the `AgentRuntimeState` type — stateJson should be typed as `Record<string, unknown>` or similar. If it's typed more narrowly, cast as needed.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /opt/nexus/ui && npx tsc --noEmit 2>&1 | head -20</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- grep -q "HermesRuntimeCard" ui/src/pages/AgentDetail.tsx
|
||||||
|
- grep -q "hermesModel" ui/src/pages/AgentDetail.tsx
|
||||||
|
- grep -q "hermesMemoryBytes" ui/src/pages/AgentDetail.tsx
|
||||||
|
- grep -q "Native Skills" ui/src/pages/AgentDetail.tsx
|
||||||
|
- grep -q "hermes_local.*runtimeState" ui/src/pages/AgentDetail.tsx
|
||||||
|
- grep -q "Hermes Runtime" ui/src/pages/AgentDetail.tsx
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>HermesRuntimeCard renders in AgentOverview for Hermes agents, showing model name, native skill count (from skills API), and memory usage (from stateJson). TypeScript compiles without errors.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `cd /opt/nexus/server && npx tsc --noEmit` — no type errors
|
||||||
|
- `cd /opt/nexus/ui && npx tsc --noEmit` — no type errors
|
||||||
|
- stateJson merge uses COALESCE + jsonb concat (not overwrite)
|
||||||
|
- HermesRuntimeCard gated by adapterType === "hermes_local"
|
||||||
|
- Cost tracking path verified (no changes needed per HERM-06)
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Hermes heartbeat runs populate stateJson with hermesModel and hermesMemoryBytes
|
||||||
|
- HermesRuntimeCard displays in AgentOverview for Hermes agents
|
||||||
|
- Native skill count derived from skills API (no cross-DB query in heartbeat)
|
||||||
|
- Memory usage shows "Not loaded" when Ollama model isn't active
|
||||||
|
- Cost tracking correctly shows $0.00 for Ollama runs (existing behavior, verified)
|
||||||
|
- All code compiles without TypeScript errors
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/28-ollama-integration/28-03-SUMMARY.md`
|
||||||
|
</output>
|
||||||
Loading…
Add table
Reference in a new issue