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,
|
resolveSessionCompactionPolicy,
|
||||||
type SessionCompactionPolicy,
|
type SessionCompactionPolicy,
|
||||||
} from "@paperclipai/adapter-utils";
|
} from "@paperclipai/adapter-utils";
|
||||||
|
import { getOllamaMemoryUsage } from "./ollama.js";
|
||||||
|
|
||||||
const MAX_LIVE_LOG_CHUNK_BYTES = 8 * 1024;
|
const MAX_LIVE_LOG_CHUNK_BYTES = 8 * 1024;
|
||||||
const HEARTBEAT_MAX_CONCURRENT_RUNS_DEFAULT = 1;
|
const HEARTBEAT_MAX_CONCURRENT_RUNS_DEFAULT = 1;
|
||||||
|
|
@ -1905,6 +1906,27 @@ export function heartbeatService(db: Db) {
|
||||||
occurredAt: new Date(),
|
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) {
|
async function startNextQueuedRunForAgent(agentId: string) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { createRequire } from "node:module";
|
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import fs from "node:fs";
|
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 {
|
interface CatalogVariant {
|
||||||
name: string;
|
name: string;
|
||||||
ramGb: number;
|
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> = {
|
const QUALITY_RANK: Record<string, number> = {
|
||||||
best: 4,
|
best: 4,
|
||||||
reasoning: 3,
|
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) ---- */
|
/* ---- Agent Overview (main single-page view) ---- */
|
||||||
|
|
||||||
function AgentOverview({
|
function AgentOverview({
|
||||||
|
|
@ -1201,6 +1247,11 @@ function AgentOverview({
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Hermes Runtime Info */}
|
||||||
|
{agent.adapterType === "hermes_local" && runtimeState && (
|
||||||
|
<HermesRuntimeCard runtimeState={runtimeState} agentId={agentId} />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Recent Issues */}
|
{/* Recent Issues */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue