feat(FN-3612): preserve fusion context in hermes runtime skill forwarding

Merges five commits implementing centralized runtime skill forwarding that preserves Fusion context across the Hermes runtime layer. The engine's `agent-runtime` and `agent-session-helpers` were updated to forward skills at runtime, with `runtime-adapter.ts` and its types extended to carry context.

Fusion-Task-Id: FN-3612
This commit is contained in:
Fusion
2026-05-06 12:57:21 -07:00
committed by gsxdsm
parent 884b91dfc2
commit 2ca67c02f2
9 changed files with 121 additions and 7 deletions

View File

@@ -36,7 +36,9 @@ Because we drive the CLI's `chat -q` mode:
- **No per-token streaming.** Hermes buffers output through prompt_toolkit; the full response arrives once the process exits. `onText` is called exactly once per turn.
- **No reasoning/thinking deltas.** `-Q` mode suppresses them. If you need streaming + reasoning, switch to Hermes's ACP mode (not yet implemented in this plugin).
- **No tool-call hooks.** Hermes runs tools internally; Fusion only sees the final assistant text. Use `yolo: true` to skip Hermes's interactive approval prompts in non-interactive sessions.
- **`AgentRuntimeOptions.cwd`, `tools`, `skills`, `sessionManager`, etc. are ignored** — Hermes's own session/tools/skills systems handle these.
- **No JS tool callbacks.** `customTools` callback functions are still not executable through Hermes CLI mode; Hermes runs its own tool layer and Fusion receives final text.
- **Fusion context is prompt-mediated.** The engine forwards requested Fusion skill names into `skills`, and the adapter prepends Fusion system/runtime context on the first turn of each session so capability expectations (for example messaging/delegation flows) are not silently dropped on non-pi runtimes.
- `AgentRuntimeOptions.cwd` / `sessionManager` are still adapter-noops in CLI mode.
## Settings

View File

@@ -61,7 +61,8 @@ describe("HermesRuntimeAdapter — promptWithFallback", () => {
expect(mockInvoke).toHaveBeenCalledTimes(1);
const [prompt, settings, resumeId] = mockInvoke.mock.calls[0];
expect(prompt).toBe("first prompt");
expect(prompt).toContain("User request:\nfirst prompt");
expect(prompt).toContain("Fusion runtime context:");
expect(settings.model).toBe("claude-sonnet-4-5");
expect(resumeId).toBeUndefined();
expect(onText).toHaveBeenCalledWith("hello from hermes");
@@ -78,7 +79,8 @@ describe("HermesRuntimeAdapter — promptWithFallback", () => {
await adapter.promptWithFallback(session, "p1");
await adapter.promptWithFallback(session, "p2");
const [, , resume2] = mockInvoke.mock.calls[1];
const [prompt2, , resume2] = mockInvoke.mock.calls[1];
expect(prompt2).toBe("p2");
expect(resume2).toBe("20260427_120000_abc123");
});

View File

@@ -16,6 +16,28 @@ import type {
HermesStreamSession,
} from "./types.js";
function buildRuntimeContextSection(options: AgentRuntimeOptions): string {
const skillNames = Array.isArray(options.skills) ? options.skills.filter((value): value is string => typeof value === "string" && value.trim().length > 0) : [];
const skillSelection = options.skillSelection as { requestedSkillNames?: unknown } | undefined;
const selectionSkillNames = Array.isArray(skillSelection?.requestedSkillNames)
? skillSelection.requestedSkillNames.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
: [];
const mergedSkills = skillNames.length > 0 ? skillNames : selectionSkillNames;
const lines: string[] = [
"Fusion runtime context:",
`- Tool mode: ${options.tools ?? "coding"}`,
];
if (mergedSkills.length > 0) {
lines.push(`- Requested skills: ${mergedSkills.join(", ")}`);
}
lines.push("- If fn_* tools are available in your runtime, use them directly for coordination/memory/task actions.");
return lines.join("\n");
}
export class HermesRuntimeAdapter implements AgentRuntime {
readonly id = "hermes";
readonly name = "Hermes Runtime";
@@ -43,6 +65,8 @@ export class HermesRuntimeAdapter implements AgentRuntime {
onToolStart: options.onToolStart,
onToolEnd: options.onToolEnd,
},
runtimeContext: options.runtimeContext,
fusedSystemPrompt: [options.systemPrompt.trim(), buildRuntimeContextSection(options).trim()].filter((part) => part.length > 0).join("\n\n"),
dispose: () => undefined,
};
@@ -55,7 +79,10 @@ export class HermesRuntimeAdapter implements AgentRuntime {
_options?: unknown,
): Promise<void> {
const resumeId = session.sessionId || undefined;
const result = await invokeHermesCli(prompt, this.settings, resumeId);
const promptWithContext = resumeId
? prompt
: `${session.fusedSystemPrompt}\n\nUser request:\n${prompt}`;
const result = await invokeHermesCli(promptWithContext, this.settings, resumeId);
session.sessionId = result.sessionId;
session.lastModelDescription = this.describeFromSettings();

View File

@@ -12,6 +12,13 @@ export interface HermesCallbacks {
onToolEnd?: (toolName: string, isError: boolean, result?: unknown) => void;
}
export interface HermesRuntimeContext {
sessionPurpose?: string;
toolMode?: "coding" | "readonly";
customToolNames?: string[];
requestedSkillNames?: string[];
}
export interface HermesStreamSession {
model: unknown;
systemPrompt: string;
@@ -22,6 +29,8 @@ export interface HermesStreamSession {
lastModelDescription: string;
callbacks: HermesCallbacks;
usage?: unknown;
runtimeContext?: HermesRuntimeContext;
fusedSystemPrompt: string;
dispose(): void;
}
@@ -49,6 +58,7 @@ export interface AgentRuntimeOptions {
sessionManager?: unknown;
skillSelection?: unknown;
skills?: string[];
runtimeContext?: HermesRuntimeContext;
}
/** Result of creating a session. */