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:
@@ -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
|
||||
Reference in New Issue
Block a user