feat(FN-2709): migrate Hermes runtime plugin to pi-ai streaming client

- Replace Hermes pi module integration with pi-ai session streaming and updated runtime adapter contracts
- Remove legacy engine guard scaffolding and add hermes-stream-client coverage for streaming behavior
- Rewrite plugin and engine e2e tests to align with the new runtime flow and regenerate dist artifacts
- Update Hermes runtime README and package metadata to document pi-ai execution expectations
This commit is contained in:
Fusion
2026-04-27 11:32:05 -07:00
committed by gsxdsm
parent 597daeaf33
commit 626e768f39
33 changed files with 1010 additions and 635 deletions

View File

@@ -1,13 +1,85 @@
/**
* Pi Module Seam
*
* Provides a mockable import path for pi functions used by the HermesRuntimeAdapter.
* Tests intercept this module via `vi.mock("../pi-module.js", ...)`. The runtime
* implementations come from @fusion/engine; the local types provide a loose
* surface so the adapter doesn't have to depend on @fusion/engine's full types.
*/
import { createFnAgent as _createFnAgent, promptWithFallback as _promptWithFallback, describeModel as _describeModel, } from "@fusion/engine";
export const createFnAgent = _createFnAgent;
export const promptWithFallback = _promptWithFallback;
export const describeModel = _describeModel;
import { randomUUID } from "node:crypto";
import { getModel, streamSimple, } from "@mariozechner/pi-ai";
const DEFAULT_PROVIDER = "anthropic";
const DEFAULT_MODEL_ID = "claude-sonnet-4-5";
function resolveStringSetting(value) {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
}
export function resolveModelConfig(settings) {
const provider = resolveStringSetting(settings?.provider) ?? resolveStringSetting(process.env.HERMES_PROVIDER) ?? DEFAULT_PROVIDER;
const modelId = resolveStringSetting(settings?.modelId) ?? resolveStringSetting(process.env.HERMES_MODEL_ID) ?? DEFAULT_MODEL_ID;
const apiKey = resolveStringSetting(settings?.apiKey) ?? resolveStringSetting(process.env.HERMES_API_KEY);
const thinkingLevel = resolveStringSetting(settings?.thinkingLevel) ?? resolveStringSetting(process.env.HERMES_THINKING_LEVEL) ?? undefined;
return { provider, modelId, apiKey, thinkingLevel };
}
export function createStreamSession(options) {
const model = getModel(options.provider, options.modelId);
return {
model,
systemPrompt: options.systemPrompt,
messages: [],
apiKey: options.apiKey,
thinkingLevel: options.thinkingLevel,
sessionId: randomUUID(),
lastModelDescription: `${model.provider}/${model.id}`,
callbacks: options.callbacks ?? {},
usage: undefined,
dispose: () => undefined,
};
}
export async function streamPrompt(session, _userMessage) {
const context = {
systemPrompt: session.systemPrompt,
messages: [...session.messages],
};
const options = {
sessionId: session.sessionId,
};
if (session.apiKey) {
options.apiKey = session.apiKey;
}
if (session.thinkingLevel) {
options.reasoning = session.thinkingLevel;
}
const stream = streamSimple(session.model, context, options);
let fullText = "";
for await (const event of stream) {
handleStreamEvent(event, session, (delta) => {
fullText += delta;
});
}
const finalMessage = await stream.result();
const responseText = finalMessage.content
.filter((content) => content.type === "text")
.map((content) => content.text)
.join("") || fullText;
session.messages.push({ role: "assistant", content: responseText });
session.lastModelDescription = `${session.model.provider}/${session.model.id}`;
}
function handleStreamEvent(event, session, onTextDelta) {
if (event.type === "text_delta") {
session.callbacks.onText?.(event.delta);
onTextDelta(event.delta);
return;
}
if (event.type === "thinking_delta") {
session.callbacks.onThinking?.(event.delta);
return;
}
if (event.type === "toolcall_end") {
session.callbacks.onToolStart?.(event.toolCall.name, event.toolCall.arguments);
session.callbacks.onToolEnd?.(event.toolCall.name, false, event.toolCall.arguments);
return;
}
if (event.type === "error") {
const errorMessage = event.error.errorMessage ?? "Hermes stream failed";
throw new Error(errorMessage);
}
if (event.type === "done") {
session.usage = event.message.usage;
}
}
export function describeStreamModel(session) {
return session.lastModelDescription;
}
//# sourceMappingURL=pi-module.js.map