Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 710d42d6b9 | |||
| 072d900fcb | |||
| cfebd07124 | |||
| f2be6fc057 | |||
| d392563549 | |||
| 2af8196451 | |||
| ad74768630 | |||
| a38ca7e8db | |||
| 3d97968887 | |||
| ade6227ffe | |||
| 13789e2c66 | |||
| 6758adc1d5 | |||
| 7c12015855 |
@@ -11,8 +11,8 @@
|
||||
"uwf": "./src/cli.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uncaged/json-cas": "^0.1.3",
|
||||
"@uncaged/json-cas-fs": "^0.1.2",
|
||||
"@uncaged/json-cas": "^0.3.0",
|
||||
"@uncaged/json-cas-fs": "^0.3.0",
|
||||
"@uncaged/uwf-agent-kit": "workspace:^",
|
||||
"@uncaged/uwf-moderator": "workspace:^",
|
||||
"@uncaged/uwf-protocol": "workspace:^",
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
cmdCasGet,
|
||||
cmdCasHas,
|
||||
cmdCasPut,
|
||||
cmdCasReindex,
|
||||
cmdCasRefs,
|
||||
cmdCasSchemaGet,
|
||||
cmdCasSchemaList,
|
||||
@@ -239,6 +240,16 @@ cas
|
||||
});
|
||||
});
|
||||
|
||||
cas
|
||||
.command("reindex")
|
||||
.description("Rebuild type index from all CAS nodes")
|
||||
.action(() => {
|
||||
const storageRoot = resolveStorageRoot();
|
||||
runAction(async () => {
|
||||
writeOutput(await cmdCasReindex(storageRoot));
|
||||
});
|
||||
});
|
||||
|
||||
const casSchema = cas.command("schema").description("CAS schema operations");
|
||||
|
||||
casSchema
|
||||
|
||||
@@ -100,10 +100,10 @@ export async function cmdCasSchemaList(
|
||||
// Include meta-schema itself
|
||||
entries.push({ hash: metaHash, title: "(meta-schema)" });
|
||||
|
||||
for (const hash of store.list()) {
|
||||
for (const hash of store.listByType(metaHash)) {
|
||||
if (hash === metaHash) continue;
|
||||
const node = store.get(hash);
|
||||
if (node !== null && node.type === metaHash) {
|
||||
if (node !== null) {
|
||||
const schema = node.payload as JSONSchema;
|
||||
const title =
|
||||
(schema.title as string | undefined) ??
|
||||
@@ -115,6 +115,17 @@ export async function cmdCasSchemaList(
|
||||
return entries;
|
||||
}
|
||||
|
||||
export async function cmdCasReindex(
|
||||
storageRoot: string,
|
||||
): Promise<{ status: string }> {
|
||||
const indexDir = join(storageRoot, "cas", "_index");
|
||||
const { rmSync } = await import("node:fs");
|
||||
rmSync(indexDir, { recursive: true, force: true });
|
||||
// Re-open store to trigger migration rebuild
|
||||
openStore(storageRoot);
|
||||
return { status: "reindexed" };
|
||||
}
|
||||
|
||||
export async function cmdCasSchemaGet(
|
||||
storageRoot: string,
|
||||
hash: string,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,7 @@
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uncaged/json-cas": "^0.3.0",
|
||||
"@uncaged/uwf-agent-kit": "workspace:^"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
import { type AgentContext, createAgent } from "@uncaged/uwf-agent-kit";
|
||||
import { type AgentContext, type AgentRunResult, createAgent } from "@uncaged/uwf-agent-kit";
|
||||
|
||||
import {
|
||||
loadHermesSession,
|
||||
parseSessionIdFromStdout,
|
||||
storeHermesRawOutput,
|
||||
storeHermesSessionDetail,
|
||||
} from "./session-detail.js";
|
||||
|
||||
const HERMES_COMMAND = "hermes";
|
||||
const HERMES_MAX_TURNS = 90;
|
||||
|
||||
function buildHistorySummary(history: AgentContext["history"]): string {
|
||||
if (history.length === 0) {
|
||||
function buildHistorySummary(steps: AgentContext["steps"]): string {
|
||||
if (steps.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const lines: string[] = ["## Previous Steps"];
|
||||
for (let i = 0; i < history.length; i++) {
|
||||
const step = history[i];
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
const step = steps[i];
|
||||
if (step === undefined) {
|
||||
continue;
|
||||
}
|
||||
@@ -26,8 +33,10 @@ function buildHistorySummary(history: AgentContext["history"]): string {
|
||||
|
||||
/** Assemble system prompt, task, and prior step outputs for Hermes. */
|
||||
export function buildHermesPrompt(ctx: AgentContext): string {
|
||||
const parts: string[] = [ctx.systemPrompt, "", "## Task", ctx.prompt];
|
||||
const historyBlock = buildHistorySummary(ctx.history);
|
||||
const roleDef = ctx.workflow.roles[ctx.role];
|
||||
const systemPrompt = roleDef?.systemPrompt ?? "";
|
||||
const parts: string[] = [systemPrompt, "", "## Task", ctx.start.prompt];
|
||||
const historyBlock = buildHistorySummary(ctx.steps);
|
||||
if (historyBlock !== "") {
|
||||
parts.push("", historyBlock);
|
||||
}
|
||||
@@ -76,9 +85,22 @@ function spawnHermesChat(prompt: string): Promise<string> {
|
||||
});
|
||||
}
|
||||
|
||||
async function runHermes(ctx: AgentContext): Promise<string> {
|
||||
async function runHermes(ctx: AgentContext): Promise<AgentRunResult> {
|
||||
const fullPrompt = buildHermesPrompt(ctx);
|
||||
return spawnHermesChat(fullPrompt);
|
||||
const rawOutput = await spawnHermesChat(fullPrompt);
|
||||
const { store } = ctx;
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
/** Agent CLI factory: parses argv, runs Hermes, extracts output, writes StepNode. */
|
||||
|
||||
@@ -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[];
|
||||
};
|
||||
@@ -18,8 +18,8 @@
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@uncaged/json-cas": "^0.1.3",
|
||||
"@uncaged/json-cas-fs": "^0.1.2",
|
||||
"@uncaged/json-cas": "^0.3.0",
|
||||
"@uncaged/json-cas-fs": "^0.3.0",
|
||||
"@uncaged/uwf-protocol": "workspace:^",
|
||||
"dotenv": "^16.6.1",
|
||||
"yaml": "^2.8.4"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Store } from "@uncaged/json-cas";
|
||||
import type {
|
||||
CasRef,
|
||||
StartNodePayload,
|
||||
@@ -6,6 +7,7 @@ import type {
|
||||
ThreadId,
|
||||
} from "@uncaged/uwf-protocol";
|
||||
import { createAgentStore, loadThreadsIndex, resolveStorageRoot } from "./storage.js";
|
||||
import type { AgentStore } from "./storage.js";
|
||||
import type { AgentContext } from "./types.js";
|
||||
|
||||
type ChainState = {
|
||||
@@ -20,8 +22,8 @@ function fail(message: string): never {
|
||||
}
|
||||
|
||||
function walkChain(
|
||||
store: Awaited<ReturnType<typeof createAgentStore>>["store"],
|
||||
schemas: Awaited<ReturnType<typeof createAgentStore>>["schemas"],
|
||||
store: Store,
|
||||
schemas: AgentStore["schemas"],
|
||||
headHash: CasRef,
|
||||
): ChainState {
|
||||
const headNode = store.get(headHash);
|
||||
@@ -77,7 +79,7 @@ function walkChain(
|
||||
}
|
||||
|
||||
function expandOutput(
|
||||
store: Awaited<ReturnType<typeof createAgentStore>>["store"],
|
||||
store: Store,
|
||||
outputRef: CasRef,
|
||||
): unknown {
|
||||
const node = store.get(outputRef);
|
||||
@@ -88,7 +90,7 @@ function expandOutput(
|
||||
}
|
||||
|
||||
async function buildHistory(
|
||||
store: Awaited<ReturnType<typeof createAgentStore>>["store"],
|
||||
store: Store,
|
||||
stepsNewestFirst: StepNodePayload[],
|
||||
): Promise<StepContext[]> {
|
||||
const chronological = [...stepsNewestFirst].reverse();
|
||||
@@ -105,8 +107,8 @@ async function buildHistory(
|
||||
}
|
||||
|
||||
async function loadWorkflow(
|
||||
store: Awaited<ReturnType<typeof createAgentStore>>["store"],
|
||||
schemas: Awaited<ReturnType<typeof createAgentStore>>["schemas"],
|
||||
store: Store,
|
||||
schemas: AgentStore["schemas"],
|
||||
workflowRef: CasRef,
|
||||
) {
|
||||
const node = store.get(workflowRef);
|
||||
@@ -141,22 +143,22 @@ export async function buildContext(threadId: ThreadId, role: string): Promise<Ag
|
||||
fail(`unknown role "${role}" in workflow "${workflow.name}"`);
|
||||
}
|
||||
|
||||
const history = await buildHistory(store, chain.stepsNewestFirst);
|
||||
const steps = await buildHistory(store, chain.stepsNewestFirst);
|
||||
|
||||
return {
|
||||
threadId,
|
||||
role,
|
||||
systemPrompt: roleDef.systemPrompt,
|
||||
prompt: chain.start.prompt,
|
||||
history,
|
||||
start: chain.start,
|
||||
steps,
|
||||
workflow,
|
||||
store,
|
||||
};
|
||||
}
|
||||
|
||||
export type BuildContextMeta = {
|
||||
storageRoot: string;
|
||||
store: Awaited<ReturnType<typeof createAgentStore>>["store"];
|
||||
schemas: Awaited<ReturnType<typeof createAgentStore>>["schemas"];
|
||||
store: Store;
|
||||
schemas: AgentStore["schemas"];
|
||||
headHash: CasRef;
|
||||
chain: ChainState;
|
||||
};
|
||||
@@ -185,15 +187,15 @@ export async function buildContextWithMeta(
|
||||
fail(`unknown role "${role}" in workflow "${workflow.name}"`);
|
||||
}
|
||||
|
||||
const history = await buildHistory(store, chain.stepsNewestFirst);
|
||||
const steps = await buildHistory(store, chain.stepsNewestFirst);
|
||||
|
||||
return {
|
||||
threadId,
|
||||
role,
|
||||
systemPrompt: roleDef.systemPrompt,
|
||||
prompt: chain.start.prompt,
|
||||
history,
|
||||
start: chain.start,
|
||||
steps,
|
||||
workflow,
|
||||
store,
|
||||
meta: { storageRoot, store, schemas, headHash, chain },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export type { BuildContextMeta } from "./context.js";
|
||||
export { buildContext, buildContextWithMeta } from "./context.js";
|
||||
export { getConfigPath, getEnvPath, loadWorkflowConfig } from "./storage.js";
|
||||
export type { ExtractResult, ResolvedLlmProvider } from "./extract.js";
|
||||
export {
|
||||
extract,
|
||||
@@ -8,4 +7,5 @@ export {
|
||||
resolveModel,
|
||||
} from "./extract.js";
|
||||
export { createAgent } from "./run.js";
|
||||
export type { AgentContext, AgentOptions, AgentRunFn } from "./types.js";
|
||||
export { getConfigPath, getEnvPath, loadWorkflowConfig } from "./storage.js";
|
||||
export type { AgentContext, AgentOptions, AgentRunFn, AgentRunResult } from "./types.js";
|
||||
|
||||
@@ -6,7 +6,7 @@ import { buildContextWithMeta } from "./context.js";
|
||||
import { extract } from "./extract.js";
|
||||
import type { AgentStore } from "./storage.js";
|
||||
import { getEnvPath, loadWorkflowConfig, resolveStorageRoot } from "./storage.js";
|
||||
import type { AgentContext, AgentOptions } from "./types.js";
|
||||
import type { AgentContext, AgentOptions, AgentRunResult } from "./types.js";
|
||||
|
||||
function fail(message: string): never {
|
||||
process.stderr.write(`${message}\n`);
|
||||
@@ -65,7 +65,7 @@ async function writeStepNode(options: {
|
||||
return hash;
|
||||
}
|
||||
|
||||
async function runAgent(options: AgentOptions, ctx: AgentContext): Promise<string> {
|
||||
async function runAgent(options: AgentOptions, ctx: AgentContext): Promise<AgentRunResult> {
|
||||
return runWithMessage("agent run failed", () => options.run(ctx));
|
||||
}
|
||||
|
||||
@@ -85,12 +85,11 @@ async function extractOutput(
|
||||
|
||||
async function persistStep(options: {
|
||||
ctx: Awaited<ReturnType<typeof buildContextWithMeta>>;
|
||||
rawOutput: string;
|
||||
outputHash: CasRef;
|
||||
detailHash: CasRef;
|
||||
agentName: string;
|
||||
}): Promise<CasRef> {
|
||||
const { store, schemas, chain, headHash } = options.ctx.meta;
|
||||
const detailHash = await store.put(null, options.rawOutput);
|
||||
return writeStepNode({
|
||||
store,
|
||||
schemas,
|
||||
@@ -98,7 +97,7 @@ async function persistStep(options: {
|
||||
prevHash: chain.headIsStart ? null : headHash,
|
||||
role: options.ctx.role,
|
||||
outputHash: options.outputHash,
|
||||
detailHash,
|
||||
detailHash: options.detailHash,
|
||||
agentName: options.agentName,
|
||||
});
|
||||
}
|
||||
@@ -121,12 +120,12 @@ export function createAgent(options: AgentOptions): () => Promise<void> {
|
||||
fail(`unknown role: ${role}`);
|
||||
}
|
||||
|
||||
const rawOutput = await runAgent(options, ctx);
|
||||
const outputHash = await extractOutput(rawOutput, roleDef.outputSchema, storageRoot);
|
||||
const agentResult = await runAgent(options, ctx);
|
||||
const outputHash = await extractOutput(agentResult.output, roleDef.outputSchema, storageRoot);
|
||||
const stepHash = await persistStep({
|
||||
ctx,
|
||||
rawOutput,
|
||||
outputHash,
|
||||
detailHash: agentResult.detailHash,
|
||||
agentName: agentLabel(options.name),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import type { Hash, Store } from "@uncaged/json-cas";
|
||||
import { putSchema } from "@uncaged/json-cas";
|
||||
import {
|
||||
START_NODE_SCHEMA,
|
||||
STEP_NODE_SCHEMA,
|
||||
WORKFLOW_SCHEMA,
|
||||
} from "@uncaged/uwf-protocol";
|
||||
import { START_NODE_SCHEMA, STEP_NODE_SCHEMA, WORKFLOW_SCHEMA } from "@uncaged/uwf-protocol";
|
||||
|
||||
export type UwfAgentSchemaHashes = {
|
||||
workflow: Hash;
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import type { StepContext, ThreadId, WorkflowPayload } from "@uncaged/uwf-protocol";
|
||||
import type { Store } from "@uncaged/json-cas";
|
||||
import type { ModeratorContext, ThreadId, WorkflowPayload } from "@uncaged/uwf-protocol";
|
||||
|
||||
export type AgentContext = {
|
||||
export type AgentContext = ModeratorContext & {
|
||||
threadId: ThreadId;
|
||||
role: string;
|
||||
systemPrompt: string;
|
||||
prompt: string;
|
||||
history: StepContext[];
|
||||
store: Store;
|
||||
workflow: WorkflowPayload;
|
||||
};
|
||||
|
||||
export type AgentRunFn = (ctx: AgentContext) => Promise<string>;
|
||||
export type AgentRunResult = {
|
||||
output: string;
|
||||
detailHash: string;
|
||||
};
|
||||
|
||||
export type AgentRunFn = (ctx: AgentContext) => Promise<AgentRunResult>;
|
||||
|
||||
export type AgentOptions = {
|
||||
name: string;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@uncaged/json-cas-fs": "^0.1.3"
|
||||
"@uncaged/json-cas-fs": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.3"
|
||||
|
||||
+17
-1
@@ -1,11 +1,27 @@
|
||||
#!/usr/bin/env bun
|
||||
// Mock agent for smoke testing
|
||||
import { bootstrap, type JSONSchema, putSchema } from "@uncaged/json-cas";
|
||||
import { createAgent } from "../packages/uwf-agent-kit/src/index.js";
|
||||
|
||||
const MOCK_RAW_OUTPUT_SCHEMA: JSONSchema = {
|
||||
title: "mock-raw-output",
|
||||
type: "object",
|
||||
required: ["text"],
|
||||
properties: {
|
||||
text: { type: "string" },
|
||||
},
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
const agent = createAgent({
|
||||
name: "mock",
|
||||
run: async (ctx) => {
|
||||
return `Mock output for role ${ctx.role}: task was "${ctx.prompt}"`;
|
||||
const output = `Mock output for role ${ctx.role}: task was "${ctx.start.prompt}"`;
|
||||
const { store } = ctx;
|
||||
await bootstrap(store);
|
||||
const schemaHash = await putSchema(store, MOCK_RAW_OUTPUT_SCHEMA);
|
||||
const detailHash = await store.put(schemaHash, { text: output });
|
||||
return { output, detailHash };
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user