feat(28-03): merge Hermes runtime data into stateJson and add HermesRuntimeCard
- Add OllamaPsResponse interface and getOllamaMemoryUsage() to ollama.ts - Import getOllamaMemoryUsage in heartbeat.ts - Add hermes_local block in updateRuntimeState: COALESCE jsonb merge of hermesModel + hermesMemoryBytes - Add HermesRuntimeCard component in AgentDetail.tsx - Render HermesRuntimeCard in AgentOverview gated by adapterType === hermes_local - Native skill count derived from agentsApi.skills entries with originLabel === Hermes skill
This commit is contained in:
parent
f0b568ddd6
commit
39327b7660
3 changed files with 111 additions and 1 deletions
|
|
@ -59,6 +59,7 @@ import {
|
|||
resolveSessionCompactionPolicy,
|
||||
type SessionCompactionPolicy,
|
||||
} from "@paperclipai/adapter-utils";
|
||||
import { getOllamaMemoryUsage } from "./ollama.js";
|
||||
|
||||
const MAX_LIVE_LOG_CHUNK_BYTES = 8 * 1024;
|
||||
const HEARTBEAT_MAX_CONCURRENT_RUNS_DEFAULT = 1;
|
||||
|
|
@ -1905,6 +1906,27 @@ export function heartbeatService(db: Db) {
|
|||
occurredAt: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
async function startNextQueuedRunForAgent(agentId: string) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { createRequire } from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
|
|
@ -41,6 +40,25 @@ interface OllamaTagsResponse {
|
|||
}>;
|
||||
}
|
||||
|
||||
interface OllamaPsResponse {
|
||||
models: Array<{
|
||||
name: string;
|
||||
model: string;
|
||||
size: number;
|
||||
digest: string;
|
||||
details: {
|
||||
parent_model: string;
|
||||
format: string;
|
||||
family: string;
|
||||
families: string[];
|
||||
parameter_size: string;
|
||||
quantization_level: string;
|
||||
};
|
||||
expires_at: string;
|
||||
size_vram: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface CatalogVariant {
|
||||
name: string;
|
||||
ramGb: number;
|
||||
|
|
@ -111,6 +129,25 @@ export async function listOllamaModels(): Promise<OllamaModel[]> {
|
|||
}
|
||||
}
|
||||
|
||||
export async function getOllamaMemoryUsage(modelName: string | null): Promise<number | null> {
|
||||
if (!modelName) return null;
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), OLLAMA_TIMEOUT_MS);
|
||||
try {
|
||||
const res = await fetch(`${OLLAMA_BASE_URL}/api/ps`, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const body = (await res.json()) as OllamaPsResponse;
|
||||
const match = (body.models ?? []).find((m) => m.name === modelName || m.model === modelName);
|
||||
return match?.size_vram ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
const QUALITY_RANK: Record<string, number> = {
|
||||
best: 4,
|
||||
reasoning: 3,
|
||||
|
|
|
|||
|
|
@ -1163,6 +1163,52 @@ function LatestRunCard({ runs, agentId }: { runs: HeartbeatRun[]; agentId: strin
|
|||
);
|
||||
}
|
||||
|
||||
/* ---- Hermes Runtime Card ---- */
|
||||
|
||||
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?.entries) return 0;
|
||||
return skillsData.entries.filter((e) => 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>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---- Agent Overview (main single-page view) ---- */
|
||||
|
||||
function AgentOverview({
|
||||
|
|
@ -1201,6 +1247,11 @@ function AgentOverview({
|
|||
</ChartCard>
|
||||
</div>
|
||||
|
||||
{/* Hermes Runtime Info */}
|
||||
{agent.adapterType === "hermes_local" && runtimeState && (
|
||||
<HermesRuntimeCard runtimeState={runtimeState} agentId={agentId} />
|
||||
)}
|
||||
|
||||
{/* Recent Issues */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue