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
|
||||
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
|
||||
**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
|
||||
|
||||
### 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 |
|
||||
|-------|-----------|----------------|--------|-----------|
|
||||
| 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 | - |
|
||||
|
|
|
|||
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