Merge pull request 'feat: hermes merkle detail — session turns as CAS tree (Phase 2 of #337)' (#339) from feat/337-agent-detail-merkle into main
This commit is contained in:
@@ -0,0 +1,121 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { createMemoryStore, refs, validate, walk } from "@uncaged/json-cas";
|
||||||
|
|
||||||
|
import {
|
||||||
|
computeDurationMs,
|
||||||
|
extractLastAssistantContent,
|
||||||
|
messageToTurnPayload,
|
||||||
|
parseSessionIdFromStdout,
|
||||||
|
storeHermesSessionDetail,
|
||||||
|
} from "../src/session-detail.js";
|
||||||
|
import type { HermesSessionJson, HermesSessionMessage } from "../src/types.js";
|
||||||
|
|
||||||
|
describe("parseSessionIdFromStdout", () => {
|
||||||
|
test("reads session_id from the last non-empty line", () => {
|
||||||
|
const stdout = "Done.\n\nsession_id: 20260518_223724_45ab80\n";
|
||||||
|
expect(parseSessionIdFromStdout(stdout)).toBe("20260518_223724_45ab80");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns null when trailing line is not session_id", () => {
|
||||||
|
expect(parseSessionIdFromStdout("only assistant text\n")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("messageToTurnPayload", () => {
|
||||||
|
test("maps assistant tool_calls to toolCalls", () => {
|
||||||
|
const msg: HermesSessionMessage = {
|
||||||
|
role: "assistant",
|
||||||
|
content: "",
|
||||||
|
reasoning: null,
|
||||||
|
tool_calls: [{ function: { name: "read_file", arguments: '{"path":"x"}' } }],
|
||||||
|
};
|
||||||
|
const turn = messageToTurnPayload(msg, 0);
|
||||||
|
expect(turn).toEqual({
|
||||||
|
index: 0,
|
||||||
|
role: "assistant",
|
||||||
|
content: "",
|
||||||
|
toolCalls: [{ name: "read_file", args: '{"path":"x"}' }],
|
||||||
|
reasoning: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("skips user messages", () => {
|
||||||
|
const msg: HermesSessionMessage = {
|
||||||
|
role: "user",
|
||||||
|
content: "hi",
|
||||||
|
reasoning: null,
|
||||||
|
tool_calls: null,
|
||||||
|
};
|
||||||
|
expect(messageToTurnPayload(msg, 0)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("extractLastAssistantContent", () => {
|
||||||
|
test("returns the last non-empty assistant content", () => {
|
||||||
|
const messages: HermesSessionMessage[] = [
|
||||||
|
{ role: "assistant", content: "first", reasoning: null, tool_calls: null },
|
||||||
|
{ role: "tool", content: "tool output", reasoning: null, tool_calls: null },
|
||||||
|
{ role: "assistant", content: "", reasoning: null, tool_calls: null },
|
||||||
|
{ role: "assistant", content: "final answer", reasoning: null, tool_calls: null },
|
||||||
|
];
|
||||||
|
expect(extractLastAssistantContent(messages)).toBe("final answer");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("computeDurationMs", () => {
|
||||||
|
test("computes elapsed time from session_start", () => {
|
||||||
|
const now = Date.parse("2026-05-18T13:32:59.028640Z");
|
||||||
|
const duration = computeDurationMs("2026-05-18T13:31:59.028640Z", now);
|
||||||
|
expect(duration).toBe(60_000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("storeHermesSessionDetail", () => {
|
||||||
|
test("stores hermes-detail root with cas_ref turns walkable", async () => {
|
||||||
|
const session: HermesSessionJson = {
|
||||||
|
session_id: "20260518_133159_6a84e8",
|
||||||
|
model: "claude-opus-4.6",
|
||||||
|
session_start: "2026-05-18T13:31:59.028640",
|
||||||
|
messages: [
|
||||||
|
{ role: "user", content: "task", reasoning: null, tool_calls: null },
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: "",
|
||||||
|
reasoning: "thinking",
|
||||||
|
tool_calls: [{ function: { name: "terminal", arguments: "{}" } }],
|
||||||
|
},
|
||||||
|
{ role: "tool", content: "ok", reasoning: null, tool_calls: null },
|
||||||
|
{ role: "assistant", content: "done", reasoning: null, tool_calls: null },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = createMemoryStore();
|
||||||
|
const now = Date.parse("2026-05-18T13:32:59.028640");
|
||||||
|
const { detailHash, output } = await storeHermesSessionDetail(store, session, now);
|
||||||
|
|
||||||
|
expect(output).toBe("done");
|
||||||
|
|
||||||
|
const detailNode = store.get(detailHash);
|
||||||
|
expect(detailNode).not.toBeNull();
|
||||||
|
if (detailNode === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect(validate(store, detailNode)).toBe(true);
|
||||||
|
expect(detailNode.payload).toMatchObject({
|
||||||
|
sessionId: "20260518_133159_6a84e8",
|
||||||
|
model: "claude-opus-4.6",
|
||||||
|
duration: 60_000,
|
||||||
|
turnCount: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
const turnRefs = refs(store, detailNode);
|
||||||
|
expect(turnRefs).toHaveLength(3);
|
||||||
|
|
||||||
|
const visited: string[] = [];
|
||||||
|
walk(store, detailHash, (hash) => visited.push(hash));
|
||||||
|
expect(visited).toContain(detailHash);
|
||||||
|
for (const turnHash of turnRefs) {
|
||||||
|
expect(visited).toContain(turnHash);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
|
|
||||||
import { bootstrap, type JSONSchema, putSchema } from "@uncaged/json-cas";
|
|
||||||
import {
|
import {
|
||||||
type AgentContext,
|
type AgentContext,
|
||||||
type AgentRunResult,
|
type AgentRunResult,
|
||||||
@@ -9,19 +8,16 @@ import {
|
|||||||
resolveStorageRoot,
|
resolveStorageRoot,
|
||||||
} from "@uncaged/uwf-agent-kit";
|
} from "@uncaged/uwf-agent-kit";
|
||||||
|
|
||||||
|
import {
|
||||||
|
loadHermesSession,
|
||||||
|
parseSessionIdFromStdout,
|
||||||
|
storeHermesRawOutput,
|
||||||
|
storeHermesSessionDetail,
|
||||||
|
} from "./session-detail.js";
|
||||||
|
|
||||||
const HERMES_COMMAND = "hermes";
|
const HERMES_COMMAND = "hermes";
|
||||||
const HERMES_MAX_TURNS = 90;
|
const HERMES_MAX_TURNS = 90;
|
||||||
|
|
||||||
const HERMES_RAW_OUTPUT_SCHEMA: JSONSchema = {
|
|
||||||
title: "hermes-raw-output",
|
|
||||||
type: "object",
|
|
||||||
required: ["text"],
|
|
||||||
properties: {
|
|
||||||
text: { type: "string" },
|
|
||||||
},
|
|
||||||
additionalProperties: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
function buildHistorySummary(history: AgentContext["history"]): string {
|
function buildHistorySummary(history: AgentContext["history"]): string {
|
||||||
if (history.length === 0) {
|
if (history.length === 0) {
|
||||||
return "";
|
return "";
|
||||||
@@ -93,18 +89,22 @@ function spawnHermesChat(prompt: string): Promise<string> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function storeHermesRawOutput(rawOutput: string): Promise<string> {
|
|
||||||
const storageRoot = resolveStorageRoot();
|
|
||||||
const { store } = await createAgentStore(storageRoot);
|
|
||||||
await bootstrap(store);
|
|
||||||
const schemaHash = await putSchema(store, HERMES_RAW_OUTPUT_SCHEMA);
|
|
||||||
return store.put(schemaHash, { text: rawOutput });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runHermes(ctx: AgentContext): Promise<AgentRunResult> {
|
async function runHermes(ctx: AgentContext): Promise<AgentRunResult> {
|
||||||
const fullPrompt = buildHermesPrompt(ctx);
|
const fullPrompt = buildHermesPrompt(ctx);
|
||||||
const rawOutput = await spawnHermesChat(fullPrompt);
|
const rawOutput = await spawnHermesChat(fullPrompt);
|
||||||
const detailHash = await storeHermesRawOutput(rawOutput);
|
const storageRoot = resolveStorageRoot();
|
||||||
|
const { store } = await createAgentStore(storageRoot);
|
||||||
|
|
||||||
|
const sessionId = parseSessionIdFromStdout(rawOutput);
|
||||||
|
if (sessionId !== null) {
|
||||||
|
const session = await loadHermesSession(sessionId);
|
||||||
|
if (session !== null) {
|
||||||
|
const { detailHash, output } = await storeHermesSessionDetail(store, session);
|
||||||
|
return { output, detailHash };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const detailHash = await storeHermesRawOutput(store, rawOutput);
|
||||||
return { output: rawOutput, detailHash };
|
return { output: rawOutput, detailHash };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import type { JSONSchema } from "@uncaged/json-cas";
|
||||||
|
|
||||||
|
const HERMES_TOOL_CALL_SCHEMA: JSONSchema = {
|
||||||
|
type: "object",
|
||||||
|
required: ["name", "args"],
|
||||||
|
properties: {
|
||||||
|
name: { type: "string" },
|
||||||
|
args: { type: "string" },
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HERMES_TURN_SCHEMA: JSONSchema = {
|
||||||
|
title: "hermes-turn",
|
||||||
|
type: "object",
|
||||||
|
required: ["index", "role", "content"],
|
||||||
|
properties: {
|
||||||
|
index: { type: "integer" },
|
||||||
|
role: { type: "string", enum: ["assistant", "tool"] },
|
||||||
|
content: { type: "string" },
|
||||||
|
toolCalls: {
|
||||||
|
anyOf: [{ type: "array", items: HERMES_TOOL_CALL_SCHEMA }, { type: "null" }],
|
||||||
|
},
|
||||||
|
reasoning: {
|
||||||
|
anyOf: [{ type: "string" }, { type: "null" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HERMES_DETAIL_SCHEMA: JSONSchema = {
|
||||||
|
title: "hermes-detail",
|
||||||
|
type: "object",
|
||||||
|
required: ["sessionId", "model", "duration", "turnCount", "turns"],
|
||||||
|
properties: {
|
||||||
|
sessionId: { type: "string" },
|
||||||
|
model: { type: "string" },
|
||||||
|
duration: { type: "integer" },
|
||||||
|
turnCount: { type: "integer" },
|
||||||
|
turns: {
|
||||||
|
type: "array",
|
||||||
|
items: { type: "string", format: "cas_ref" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Fallback detail when Hermes session file is unavailable. */
|
||||||
|
export const HERMES_RAW_OUTPUT_SCHEMA: JSONSchema = {
|
||||||
|
title: "hermes-raw-output",
|
||||||
|
type: "object",
|
||||||
|
required: ["text"],
|
||||||
|
properties: {
|
||||||
|
text: { type: "string" },
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
};
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import { homedir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
import { bootstrap, putSchema, type Store } from "@uncaged/json-cas";
|
||||||
|
|
||||||
|
import { HERMES_DETAIL_SCHEMA, HERMES_RAW_OUTPUT_SCHEMA, HERMES_TURN_SCHEMA } from "./schemas.js";
|
||||||
|
import type {
|
||||||
|
HermesDetailPayload,
|
||||||
|
HermesSessionJson,
|
||||||
|
HermesSessionMessage,
|
||||||
|
HermesToolCall,
|
||||||
|
HermesTurnPayload,
|
||||||
|
HermesTurnRole,
|
||||||
|
} from "./types.js";
|
||||||
|
|
||||||
|
const SESSION_ID_LINE = /^session_id:\s*(\S+)\s*$/i;
|
||||||
|
|
||||||
|
export function getHermesSessionsDir(): string {
|
||||||
|
return join(homedir(), ".hermes", "sessions");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHermesSessionPath(sessionId: string): string {
|
||||||
|
return join(getHermesSessionsDir(), `session_${sessionId}.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse `session_id: …` from the last non-empty line of Hermes stdout. */
|
||||||
|
export function parseSessionIdFromStdout(stdout: string): string | null {
|
||||||
|
const lines = stdout.split(/\r?\n/).map((line) => line.trim());
|
||||||
|
for (let i = lines.length - 1; i >= 0; i--) {
|
||||||
|
const line = lines[i];
|
||||||
|
if (line === undefined || line === "") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const match = SESSION_ID_LINE.exec(line);
|
||||||
|
if (match?.[1] !== undefined) {
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseToolCalls(raw: unknown): HermesSessionMessage["tool_calls"] {
|
||||||
|
if (!Array.isArray(raw) || raw.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const calls: NonNullable<HermesSessionMessage["tool_calls"]> = [];
|
||||||
|
for (const entry of raw) {
|
||||||
|
if (!isRecord(entry)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const fn = entry.function;
|
||||||
|
if (!isRecord(fn)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const name = fn.name;
|
||||||
|
const args = fn.arguments;
|
||||||
|
if (typeof name !== "string" || typeof args !== "string") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
calls.push({ function: { name, arguments: args } });
|
||||||
|
}
|
||||||
|
return calls.length > 0 ? calls : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMessage(raw: unknown): HermesSessionMessage | null {
|
||||||
|
if (!isRecord(raw)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const role = raw.role;
|
||||||
|
if (role !== "assistant" && role !== "tool" && role !== "user") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const content = typeof raw.content === "string" ? raw.content : raw.content === null ? null : "";
|
||||||
|
const reasoning =
|
||||||
|
typeof raw.reasoning === "string"
|
||||||
|
? raw.reasoning
|
||||||
|
: raw.reasoning === null || raw.reasoning === undefined
|
||||||
|
? null
|
||||||
|
: null;
|
||||||
|
const tool_calls = parseToolCalls(raw.tool_calls);
|
||||||
|
return { role, content, reasoning, tool_calls };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSessionJson(raw: unknown): HermesSessionJson | null {
|
||||||
|
if (!isRecord(raw)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const session_id = raw.session_id;
|
||||||
|
const model = raw.model;
|
||||||
|
const session_start = raw.session_start;
|
||||||
|
const messagesRaw = raw.messages;
|
||||||
|
if (
|
||||||
|
typeof session_id !== "string" ||
|
||||||
|
typeof model !== "string" ||
|
||||||
|
typeof session_start !== "string" ||
|
||||||
|
!Array.isArray(messagesRaw)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const messages: HermesSessionMessage[] = [];
|
||||||
|
for (const entry of messagesRaw) {
|
||||||
|
const msg = normalizeMessage(entry);
|
||||||
|
if (msg !== null) {
|
||||||
|
messages.push(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { session_id, model, session_start, messages };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadHermesSession(sessionId: string): Promise<HermesSessionJson | null> {
|
||||||
|
const path = getHermesSessionPath(sessionId);
|
||||||
|
try {
|
||||||
|
const text = await readFile(path, "utf8");
|
||||||
|
const raw = JSON.parse(text) as unknown;
|
||||||
|
return parseSessionJson(raw);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeDurationMs(sessionStart: string, nowMs: number = Date.now()): number {
|
||||||
|
const startMs = Date.parse(sessionStart);
|
||||||
|
if (Number.isNaN(startMs)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Math.max(0, nowMs - startMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapSessionToolCalls(
|
||||||
|
toolCalls: HermesSessionMessage["tool_calls"],
|
||||||
|
): HermesToolCall[] | null {
|
||||||
|
if (toolCalls === null || toolCalls.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return toolCalls.map((call) => ({
|
||||||
|
name: call.function.name,
|
||||||
|
args: call.function.arguments,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function messageToTurnPayload(
|
||||||
|
message: HermesSessionMessage,
|
||||||
|
index: number,
|
||||||
|
): HermesTurnPayload | null {
|
||||||
|
if (message.role !== "assistant" && message.role !== "tool") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const role = message.role as HermesTurnRole;
|
||||||
|
return {
|
||||||
|
index,
|
||||||
|
role,
|
||||||
|
content: message.content ?? "",
|
||||||
|
toolCalls: mapSessionToolCalls(message.tool_calls),
|
||||||
|
reasoning: message.reasoning,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Last assistant message with non-empty text content (walks backward). */
|
||||||
|
export function extractLastAssistantContent(messages: HermesSessionMessage[]): string {
|
||||||
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
|
const msg = messages[i];
|
||||||
|
if (msg === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (msg.role === "assistant" && msg.content !== null && msg.content.trim() !== "") {
|
||||||
|
return msg.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
type HermesSchemaHashes = {
|
||||||
|
turn: string;
|
||||||
|
detail: string;
|
||||||
|
rawOutput: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function registerHermesSchemas(store: Store): Promise<HermesSchemaHashes> {
|
||||||
|
await bootstrap(store);
|
||||||
|
const [turn, detail, rawOutput] = await Promise.all([
|
||||||
|
putSchema(store, HERMES_TURN_SCHEMA),
|
||||||
|
putSchema(store, HERMES_DETAIL_SCHEMA),
|
||||||
|
putSchema(store, HERMES_RAW_OUTPUT_SCHEMA),
|
||||||
|
]);
|
||||||
|
return { turn, detail, rawOutput };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function storeHermesSessionDetail(
|
||||||
|
store: Store,
|
||||||
|
session: HermesSessionJson,
|
||||||
|
nowMs: number = Date.now(),
|
||||||
|
): Promise<{ detailHash: string; output: string }> {
|
||||||
|
const schemas = await registerHermesSchemas(store);
|
||||||
|
const turnHashes: string[] = [];
|
||||||
|
let turnIndex = 0;
|
||||||
|
|
||||||
|
for (const message of session.messages) {
|
||||||
|
const turn = messageToTurnPayload(message, turnIndex);
|
||||||
|
if (turn === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const hash = await store.put(schemas.turn, turn);
|
||||||
|
turnHashes.push(hash);
|
||||||
|
turnIndex += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const detail: HermesDetailPayload = {
|
||||||
|
sessionId: session.session_id,
|
||||||
|
model: session.model,
|
||||||
|
duration: computeDurationMs(session.session_start, nowMs),
|
||||||
|
turnCount: turnHashes.length,
|
||||||
|
turns: turnHashes,
|
||||||
|
};
|
||||||
|
const detailHash = await store.put(schemas.detail, detail);
|
||||||
|
const output = extractLastAssistantContent(session.messages);
|
||||||
|
return { detailHash, output };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function storeHermesRawOutput(store: Store, rawOutput: string): Promise<string> {
|
||||||
|
const schemas = await registerHermesSchemas(store);
|
||||||
|
return store.put(schemas.rawOutput, { text: rawOutput });
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
export type HermesTurnRole = "assistant" | "tool";
|
||||||
|
|
||||||
|
export type HermesToolCall = {
|
||||||
|
name: string;
|
||||||
|
args: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HermesTurnPayload = {
|
||||||
|
index: number;
|
||||||
|
role: HermesTurnRole;
|
||||||
|
content: string;
|
||||||
|
toolCalls: HermesToolCall[] | null;
|
||||||
|
reasoning: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HermesDetailPayload = {
|
||||||
|
sessionId: string;
|
||||||
|
model: string;
|
||||||
|
duration: number;
|
||||||
|
turnCount: number;
|
||||||
|
turns: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HermesSessionToolCall = {
|
||||||
|
function: {
|
||||||
|
name: string;
|
||||||
|
arguments: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HermesSessionMessage = {
|
||||||
|
role: string;
|
||||||
|
content: string | null;
|
||||||
|
tool_calls: HermesSessionToolCall[] | null;
|
||||||
|
reasoning: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HermesSessionJson = {
|
||||||
|
session_id: string;
|
||||||
|
model: string;
|
||||||
|
session_start: string;
|
||||||
|
messages: HermesSessionMessage[];
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user